Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9e0001ef8 | ||
|
|
62f8bfac2f | ||
|
|
75c9b51bf2 | ||
|
|
ec98d8417f | ||
|
|
736590558b | ||
|
|
6e40edd3f8 | ||
|
|
5f11c8d98f | ||
|
|
08737698c2 | ||
|
|
04b3133500 | ||
|
|
0d15992d56 | ||
|
|
a14929a13c | ||
|
|
6a20d804dc | ||
|
|
ae75059cc1 | ||
|
|
39e650cf11 | ||
|
|
bc91237d7c | ||
|
|
9e5491fdbf | ||
|
|
986ba078a6 | ||
|
|
061afad967 | ||
|
|
3bc8da4cbf | ||
|
|
1b9be8fe2d | ||
|
|
3a594ac7fd | ||
|
|
ff84f5676e | ||
|
|
4d0ce12c96 | ||
|
|
298b058e5c | ||
|
|
1e80ee1fb0 | ||
|
|
5901dc5f11 | ||
|
|
5721796378 | ||
|
|
8b374a37b4 | ||
|
|
c404dfebe2 | ||
|
|
e8199932b4 | ||
|
|
f4a7ca76b8 | ||
|
|
64cca1d9b8 | ||
|
|
68a5d698a2 | ||
|
|
bf0c55fa60 | ||
|
|
e7b816d156 | ||
|
|
a2e092190d | ||
|
|
c1c2f2925f | ||
|
|
e8d06e0f3e | ||
|
|
7d3670c26d | ||
|
|
430537f704 | ||
|
|
58ed7b456b | ||
|
|
6b3f4659df | ||
|
|
bbc6156281 | ||
|
|
df533ad3b1 | ||
|
|
6067bdb24e | ||
|
|
924b326244 | ||
|
|
8608072bfb | ||
|
|
3233d9b802 | ||
|
|
99e24b4134 | ||
|
|
b210aaddf2 | ||
|
|
65902e5b72 | ||
|
|
efd63ff85d | ||
|
|
242a497e7d | ||
|
|
05a6a84c37 |
@@ -1,2 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
[*.{js,jsx,ts,tsx,vue,sh}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
|
||||
2
.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
dist/
|
||||
dist_electron/
|
||||
95
.eslintrc.cjs
Normal file
@@ -0,0 +1,95 @@
|
||||
const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
|
||||
const tsconfigJson = require('./tsconfig.json');
|
||||
require('@rushstack/eslint-patch/modern-module-resolution');
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
es2022: true, // add globals and sets parserOptions.ecmaVersion to 2022
|
||||
},
|
||||
extends: [
|
||||
// Vue specific rules, eslint-plugin-vue
|
||||
'plugin:vue/essential',
|
||||
|
||||
// Extends eslint-config-airbnb
|
||||
'@vue/eslint-config-airbnb-with-typescript',
|
||||
|
||||
// Extends @typescript-eslint/recommended
|
||||
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||
'@vue/typescript/recommended',
|
||||
],
|
||||
rules: {
|
||||
...getOwnRules(),
|
||||
...getTurnedOffBrokenRules(),
|
||||
...getOpinionatedRuleOverrides(),
|
||||
...getTodoRules(),
|
||||
},
|
||||
};
|
||||
|
||||
function getOwnRules() {
|
||||
return {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'linebreak-style': ['error', 'unix'], // This is also enforced in .editorconfig and .gitattributes files
|
||||
'import/order': [ // Enforce strict import order taking account into aliases
|
||||
'error',
|
||||
{
|
||||
groups: [ // Enforce more strict order than AirBnb
|
||||
'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||
pathGroups: [ // Fix manually configured paths being incorrectly grouped as "external"
|
||||
...getAliasesFromTsConfig(),
|
||||
'js-yaml-loader!@/**',
|
||||
].map((pattern) => ({ pattern, group: 'internal' })),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function getTodoRules() { // Should be worked on separate future commits
|
||||
return {
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
// Accessibility improvements:
|
||||
'vuejs-accessibility/form-control-has-label': 'off',
|
||||
'vuejs-accessibility/click-events-have-key-events': 'off',
|
||||
'vuejs-accessibility/anchor-has-content': 'off',
|
||||
'vuejs-accessibility/accessible-emoji': 'off',
|
||||
};
|
||||
}
|
||||
|
||||
function getTurnedOffBrokenRules() {
|
||||
return {
|
||||
// Broken in TypeScript
|
||||
'no-useless-constructor': 'off', // Cannot interpret TypeScript constructors
|
||||
'no-shadow': 'off', // Fails with TypeScript enums
|
||||
};
|
||||
}
|
||||
|
||||
function getOpinionatedRuleOverrides() {
|
||||
return {
|
||||
// https://erkinekici.com/articles/linting-trap#no-use-before-define
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#arrow-body-style
|
||||
'arrow-body-style': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#no-plusplus
|
||||
'no-plusplus': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#no-param-reassign
|
||||
'no-param-reassign': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#class-methods-use-this
|
||||
'class-methods-use-this': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#importprefer-default-export
|
||||
'import/prefer-default-export': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#disallowing-for-of
|
||||
// Original: https://github.com/airbnb/javascript/blob/d8cb404da74c302506f91e5928f30cc75109e74d/packages/eslint-config-airbnb-base/rules/style.js#L333-L351
|
||||
'no-restricted-syntax': [
|
||||
baseStyleRules['no-restricted-syntax'][0],
|
||||
...baseStyleRules['no-restricted-syntax'].slice(1).filter((rule) => rule.selector !== 'ForOfStatement'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function getAliasesFromTsConfig() {
|
||||
return Object.keys(tsconfigJson.compilerOptions.paths)
|
||||
.map((path) => `${path}*`);
|
||||
}
|
||||
291
.eslintrc.js
@@ -1,291 +0,0 @@
|
||||
const { rules: baseBestPracticesRules } = require('eslint-config-airbnb-base/rules/best-practices');
|
||||
const { rules: baseErrorsRules } = require('eslint-config-airbnb-base/rules/errors');
|
||||
const { rules: baseES6Rules } = require('eslint-config-airbnb-base/rules/es6');
|
||||
const { rules: baseImportsRules } = require('eslint-config-airbnb-base/rules/imports');
|
||||
const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
|
||||
const { rules: baseVariablesRules } = require('eslint-config-airbnb-base/rules/variables');
|
||||
const tsconfigJson = require('./tsconfig.json');
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
// Vue specific rules, eslint-plugin-vue
|
||||
// Added by Vue CLI
|
||||
'plugin:vue/essential',
|
||||
|
||||
// Extends eslint-config-airbnb
|
||||
// Added by Vue CLI
|
||||
// Here until https://github.com/vuejs/eslint-config-airbnb/issues/23 is done
|
||||
'@vue/airbnb',
|
||||
|
||||
// Extends @typescript-eslint/recommended
|
||||
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||
// Added by Vue CLI
|
||||
'@vue/typescript/recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
},
|
||||
rules: {
|
||||
...getOwnRules(),
|
||||
...getTurnedOffBrokenRules(),
|
||||
...getOpinionatedRuleOverrides(),
|
||||
...getTodoRules(),
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/*.{j,t}s?(x)',
|
||||
'**/tests/unit/**/*.spec.{j,t}s?(x)',
|
||||
],
|
||||
env: {
|
||||
mocha: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts?(x)', '**/*.d.ts'],
|
||||
parserOptions: {
|
||||
// Setting project is required for some rules such as @typescript-eslint/dot-notation,
|
||||
// @typescript-eslint/return-await and @typescript-eslint/no-throw-literal.
|
||||
// If this property is missing they fail due to missing parser.
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
rules: {
|
||||
...getTypeScriptOverrides(),
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/tests/**/*.{j,t}s?(x)'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function getOwnRules() {
|
||||
return {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'linebreak-style': ['error', 'unix'], // This is also enforced in .editorconfig and .gitattributes files
|
||||
'import/order': [ // Enforce strict import order taking account into aliases
|
||||
'error',
|
||||
{
|
||||
groups: [ // Enforce more strict order than AirBnb
|
||||
'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||
pathGroups: [ // Fix manually configured paths being incorrectly grouped as "external"
|
||||
...getAliasesFromTsConfig(),
|
||||
'js-yaml-loader!@/**',
|
||||
].map((pattern) => ({ pattern, group: 'internal' })),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function getTodoRules() { // Should be worked on separate future commits
|
||||
return {
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
// Accessibility improvements:
|
||||
'vuejs-accessibility/form-control-has-label': 'off',
|
||||
'vuejs-accessibility/click-events-have-key-events': 'off',
|
||||
'vuejs-accessibility/anchor-has-content': 'off',
|
||||
'vuejs-accessibility/accessible-emoji': 'off',
|
||||
};
|
||||
}
|
||||
|
||||
function getTurnedOffBrokenRules() {
|
||||
return {
|
||||
// Broken in TypeScript
|
||||
'no-useless-constructor': 'off', // Cannot interpret TypeScript constructors
|
||||
'no-shadow': 'off', // Fails with TypeScript enums
|
||||
};
|
||||
}
|
||||
|
||||
function getOpinionatedRuleOverrides() {
|
||||
return {
|
||||
// https://erkinekici.com/articles/linting-trap#no-use-before-define
|
||||
'no-use-before-define': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#arrow-body-style
|
||||
'arrow-body-style': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#no-plusplus
|
||||
'no-plusplus': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#no-param-reassign
|
||||
'no-param-reassign': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#class-methods-use-this
|
||||
'class-methods-use-this': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#importprefer-default-export
|
||||
'import/prefer-default-export': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#disallowing-for-of
|
||||
// Original: https://github.com/airbnb/javascript/blob/d8cb404da74c302506f91e5928f30cc75109e74d/packages/eslint-config-airbnb-base/rules/style.js#L333-L351
|
||||
'no-restricted-syntax': [
|
||||
baseStyleRules['no-restricted-syntax'][0],
|
||||
...baseStyleRules['no-restricted-syntax'].slice(1).filter((rule) => rule.selector !== 'ForOfStatement'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function getTypeScriptOverrides() {
|
||||
/*
|
||||
Here until Vue supports AirBnb Typescript overrides (vuejs/eslint-config-airbnb#23).
|
||||
Based on `eslint-config-airbnb-typescript`.
|
||||
Source: https://github.com/iamturns/eslint-config-airbnb-typescript/blob/v16.1.0/lib/shared.js
|
||||
It cannot be used directly due to compilation errors.
|
||||
*/
|
||||
return {
|
||||
'brace-style': 'off',
|
||||
'@typescript-eslint/brace-style': baseStyleRules['brace-style'],
|
||||
|
||||
camelcase: 'off',
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{ selector: 'variable', format: ['camelCase', 'PascalCase', 'UPPER_CASE'] },
|
||||
{ selector: 'function', format: ['camelCase', 'PascalCase'] },
|
||||
{ selector: 'typeLike', format: ['PascalCase'] },
|
||||
],
|
||||
|
||||
'comma-dangle': 'off',
|
||||
'@typescript-eslint/comma-dangle': [
|
||||
baseStyleRules['comma-dangle'][0],
|
||||
{
|
||||
...baseStyleRules['comma-dangle'][1],
|
||||
enums: baseStyleRules['comma-dangle'][1].arrays,
|
||||
generics: baseStyleRules['comma-dangle'][1].arrays,
|
||||
tuples: baseStyleRules['comma-dangle'][1].arrays,
|
||||
},
|
||||
],
|
||||
|
||||
'comma-spacing': 'off',
|
||||
'@typescript-eslint/comma-spacing': baseStyleRules['comma-spacing'],
|
||||
|
||||
'default-param-last': 'off',
|
||||
'@typescript-eslint/default-param-last': baseBestPracticesRules['default-param-last'],
|
||||
|
||||
'dot-notation': 'off',
|
||||
'@typescript-eslint/dot-notation': baseBestPracticesRules['dot-notation'],
|
||||
|
||||
'func-call-spacing': 'off',
|
||||
'@typescript-eslint/func-call-spacing': baseStyleRules['func-call-spacing'],
|
||||
|
||||
// ❌ Broken for some cases, but still useful.
|
||||
// Here until Prettifier is used.
|
||||
indent: 'off',
|
||||
'@typescript-eslint/indent': baseStyleRules.indent,
|
||||
|
||||
'keyword-spacing': 'off',
|
||||
'@typescript-eslint/keyword-spacing': baseStyleRules['keyword-spacing'],
|
||||
|
||||
'lines-between-class-members': 'off',
|
||||
'@typescript-eslint/lines-between-class-members': baseStyleRules['lines-between-class-members'],
|
||||
|
||||
'no-array-constructor': 'off',
|
||||
'@typescript-eslint/no-array-constructor': baseStyleRules['no-array-constructor'],
|
||||
|
||||
'no-dupe-class-members': 'off',
|
||||
'@typescript-eslint/no-dupe-class-members': baseES6Rules['no-dupe-class-members'],
|
||||
|
||||
'no-empty-function': 'off',
|
||||
'@typescript-eslint/no-empty-function': baseBestPracticesRules['no-empty-function'],
|
||||
|
||||
'no-extra-parens': 'off',
|
||||
'@typescript-eslint/no-extra-parens': baseErrorsRules['no-extra-parens'],
|
||||
|
||||
'no-extra-semi': 'off',
|
||||
'@typescript-eslint/no-extra-semi': baseErrorsRules['no-extra-semi'],
|
||||
|
||||
// ❌ Fails due to missing parser
|
||||
// 'no-implied-eval': 'off',
|
||||
// 'no-new-func': 'off',
|
||||
// '@typescript-eslint/no-implied-eval': baseBestPracticesRules['no-implied-eval'],
|
||||
|
||||
'no-loss-of-precision': 'off',
|
||||
'@typescript-eslint/no-loss-of-precision': baseErrorsRules['no-loss-of-precision'],
|
||||
|
||||
'no-loop-func': 'off',
|
||||
'@typescript-eslint/no-loop-func': baseBestPracticesRules['no-loop-func'],
|
||||
|
||||
'no-magic-numbers': 'off',
|
||||
'@typescript-eslint/no-magic-numbers': baseBestPracticesRules['no-magic-numbers'],
|
||||
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': baseBestPracticesRules['no-redeclare'],
|
||||
|
||||
// ESLint variant does not work with TypeScript enums.
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': baseVariablesRules['no-shadow'],
|
||||
|
||||
'no-throw-literal': 'off',
|
||||
'@typescript-eslint/no-throw-literal': baseBestPracticesRules['no-throw-literal'],
|
||||
|
||||
'no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': baseBestPracticesRules['no-unused-expressions'],
|
||||
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': baseVariablesRules['no-unused-vars'],
|
||||
|
||||
// https://erkinekici.com/articles/linting-trap#no-use-before-define
|
||||
// 'no-use-before-define': 'off',
|
||||
// '@typescript-eslint/no-use-before-define': baseVariablesRules['no-use-before-define'],
|
||||
|
||||
// ESLint variant does not understand TypeScript constructors.
|
||||
// eslint/eslint/#14118, typescript-eslint/typescript-eslint#873
|
||||
'no-useless-constructor': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': baseES6Rules['no-useless-constructor'],
|
||||
|
||||
quotes: 'off',
|
||||
'@typescript-eslint/quotes': baseStyleRules.quotes,
|
||||
|
||||
semi: 'off',
|
||||
'@typescript-eslint/semi': baseStyleRules.semi,
|
||||
|
||||
'space-before-function-paren': 'off',
|
||||
'@typescript-eslint/space-before-function-paren': baseStyleRules['space-before-function-paren'],
|
||||
|
||||
'require-await': 'off',
|
||||
'@typescript-eslint/require-await': baseBestPracticesRules['require-await'],
|
||||
|
||||
'no-return-await': 'off',
|
||||
'@typescript-eslint/return-await': baseBestPracticesRules['no-return-await'],
|
||||
|
||||
'space-infix-ops': 'off',
|
||||
'@typescript-eslint/space-infix-ops': baseStyleRules['space-infix-ops'],
|
||||
|
||||
'object-curly-spacing': 'off',
|
||||
'@typescript-eslint/object-curly-spacing': baseStyleRules['object-curly-spacing'],
|
||||
|
||||
'import/extensions': [
|
||||
baseImportsRules['import/extensions'][0],
|
||||
baseImportsRules['import/extensions'][1],
|
||||
{
|
||||
...baseImportsRules['import/extensions'][2],
|
||||
ts: 'never',
|
||||
tsx: 'never',
|
||||
},
|
||||
],
|
||||
|
||||
// Changes required is not yet implemented:
|
||||
// 'import/no-extraneous-dependencies': [
|
||||
// baseImportsRules['import/no-extraneous-dependencies'][0],
|
||||
// {
|
||||
// ...baseImportsRules['import/no-extraneous-dependencies'][1],
|
||||
// devDependencies: baseImportsRules[
|
||||
// 'import/no-extraneous-dependencies'
|
||||
// ][1].devDependencies.reduce((result, devDep) => {
|
||||
// const toAppend = [devDep];
|
||||
// const devDepWithTs = devDep.replace(/\bjs(x?)\b/g, 'ts$1');
|
||||
// if (devDepWithTs !== devDep) {
|
||||
// toAppend.push(devDepWithTs);
|
||||
// }
|
||||
// return [...result, ...toAppend];
|
||||
// }, []),
|
||||
// },
|
||||
// ],
|
||||
};
|
||||
}
|
||||
|
||||
function getAliasesFromTsConfig() {
|
||||
return Object.keys(tsconfigJson.compilerOptions.paths)
|
||||
.map((path) => `${path}*`);
|
||||
}
|
||||
@@ -21,8 +21,9 @@ A clear and concise description of what the bug is.
|
||||
|
||||
<!--
|
||||
Which OS are you using? What version of OS you were using?
|
||||
On Windows you can find it using "Start button" > "Settings" > "System" > "About".
|
||||
On macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
|
||||
On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||
On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
||||
On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
||||
-->
|
||||
|
||||
### Reproduction steps
|
||||
|
||||
@@ -14,7 +14,7 @@ You could alternatively send a PR directly (see CONTRIBUTING.md).
|
||||
|
||||
<!--
|
||||
Which OS will the new script configure?
|
||||
Either "Windows" or "macOS".
|
||||
One of the supported OSes: "Windows", "macOS" or "Linux".
|
||||
-->
|
||||
|
||||
### Name
|
||||
@@ -30,10 +30,12 @@ E.g. "Disable webcam telemetry"
|
||||
<!--
|
||||
Code that will be executed when script is selected.
|
||||
Try to keep it as simple and backwards-compatible as possible.
|
||||
Allowed languages:
|
||||
- macOS: bash (sh)
|
||||
Allowed languages:
|
||||
- Windows: PowerShell (ps1) or batchfile
|
||||
- 💡 Prioritize the one that's simpler, batchfile if similar.
|
||||
- macOS: bash (sh)
|
||||
- Linux: bash (sh) or Python 3
|
||||
- 💡 Prioritize the one that's simpler, bash if similar.
|
||||
-->
|
||||
|
||||
### Revert code
|
||||
|
||||
8
.github/actions/setup-node/action.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
-
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.x
|
||||
52
.github/workflows/checks.build.yaml
vendored
@@ -9,7 +9,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
mode: [ development, test, production ]
|
||||
mode: [
|
||||
# Vite mode: https://vitejs.dev/guide/env-and-mode.html
|
||||
development, # Used by `dev` command
|
||||
production, # Used by `build` command
|
||||
# Vitest mode: https://vitest.dev/guide/cli.html
|
||||
test, # Used by Vitest
|
||||
]
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
@@ -18,9 +24,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -28,12 +32,15 @@ jobs:
|
||||
name: Build
|
||||
run: npm run build -- --mode ${{ matrix.mode }}
|
||||
|
||||
# A new job is used due to environments/modes different from Vue CLI, https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
|
||||
build-desktop:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
mode: [ development, production ] # "test" is not supported https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1627
|
||||
mode: [
|
||||
# electron-vite modes: https://electron-vite.org/guide/env-and-mode.html#global-env-variables
|
||||
development, # Used by `dev` command
|
||||
production, # Used by `build` and `preview` commands
|
||||
]
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
@@ -42,18 +49,33 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
-
|
||||
name: Install cross-env
|
||||
# Used to set NODE_ENV due to https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
|
||||
run: npm install --global cross-env
|
||||
name: Prebuild
|
||||
run: npm run electron:prebuild -- --mode ${{ matrix.mode }}
|
||||
-
|
||||
name: Build
|
||||
run: |-
|
||||
cross-env-shell NODE_ENV=${{ matrix.mode }}
|
||||
npm run electron:build -- --publish never --mode ${{ matrix.mode }}
|
||||
run: npm run electron:build -- --publish never
|
||||
|
||||
create-icons:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
-
|
||||
name: Create icons
|
||||
run: npm run icons:build
|
||||
|
||||
67
.github/workflows/checks.desktop-runtime-errors.yaml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: checks.desktop-runtime-errors
|
||||
# Verifies desktop builds for Electron applications across multiple OS platforms (macOS ,Ubuntu, and Windows).
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build-desktop:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Configure Ubuntu
|
||||
if: matrix.os == 'ubuntu'
|
||||
shell: bash
|
||||
run: |-
|
||||
sudo apt update
|
||||
|
||||
# Configure AppImage dependencies
|
||||
sudo apt install -y libfuse2
|
||||
|
||||
# Configure DBUS (fixes `Failed to connect to the bus: Could not parse server address: Unknown address type`)
|
||||
if ! command -v 'dbus-launch' &> /dev/null; then
|
||||
echo 'DBUS does not exist, installing...'
|
||||
sudo apt install -y dbus-x11 # Gives both dbus and dbus-launch utility
|
||||
fi
|
||||
sudo systemctl start dbus
|
||||
DBUS_LAUNCH_OUTPUT=$(dbus-launch)
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "${DBUS_LAUNCH_OUTPUT}" >> $GITHUB_ENV
|
||||
else
|
||||
echo 'Error: dbus-launch command did not execute successfully. Exiting.' >&2
|
||||
echo "${DBUS_LAUNCH_OUTPUT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configure fake (virtual) display
|
||||
sudo apt install -y xvfb
|
||||
sudo Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
echo "DISPLAY=:99" >> $GITHUB_ENV
|
||||
|
||||
# Install ImageMagick for screenshots
|
||||
sudo apt install -y imagemagick
|
||||
|
||||
# Install xdotool and xprop (from x11-utils) for window title capturing
|
||||
sudo apt install -y xdotool x11-utils
|
||||
-
|
||||
name: Test
|
||||
shell: bash
|
||||
run: node ./scripts/check-desktop-runtime-errors --screenshot
|
||||
-
|
||||
name: Upload screenshot
|
||||
if: always() # Run even if previous step fails
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: screenshot-${{ matrix.os }}
|
||||
path: screenshot.png
|
||||
4
.github/workflows/checks.quality.yaml
vendored
@@ -19,9 +19,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
|
||||
6
.github/workflows/checks.security.yaml
vendored
@@ -16,9 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: NPM audit
|
||||
run: exit "$(npm audit)" # Since node 15.x, it does not fail with error if we don't explicitly exit
|
||||
run: npm audit --omit=dev
|
||||
|
||||
29
.github/workflows/release.desktop.yaml
vendored
@@ -13,22 +13,29 @@ jobs:
|
||||
fail-fast: false # So publish runs for other OSes if one fails
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
-
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master # otherwise it defaults to the version tag missing bump commit
|
||||
fetch-depth: 0 # fetch all history
|
||||
- name: Checkout to bump commit
|
||||
-
|
||||
name: Checkout to bump commit
|
||||
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
- name: Install dependencies
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run unit tests
|
||||
-
|
||||
name: Run unit tests
|
||||
run: npm run test:unit
|
||||
- name: Publish desktop app
|
||||
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
||||
-
|
||||
name: Prebuild
|
||||
run: npm run electron:prebuild
|
||||
-
|
||||
name: Build and publish
|
||||
run: npm run electron:build -- --publish always
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074
|
||||
EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074
|
||||
|
||||
18
.github/workflows/release.site.yaml
vendored
@@ -1,8 +1,8 @@
|
||||
name: release-site
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created] # will be triggered when a NON-draft release is created and published.
|
||||
release:
|
||||
types: [created] # will be triggered when a NON-draft release is created and published.
|
||||
|
||||
jobs:
|
||||
aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
|
||||
@@ -77,30 +77,28 @@ jobs:
|
||||
name: "App: Checkout"
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: site
|
||||
path: app
|
||||
ref: master # otherwise we don't get version bump commit
|
||||
-
|
||||
name: "App: Setup node"
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
uses: ./app/.github/actions/setup-node
|
||||
-
|
||||
name: "App: Install dependencies"
|
||||
run: npm ci
|
||||
working-directory: site
|
||||
working-directory: app
|
||||
-
|
||||
name: "App: Run unit tests"
|
||||
run: npm run test:unit
|
||||
working-directory: site
|
||||
working-directory: app
|
||||
-
|
||||
name: "App: Build"
|
||||
run: npm run build
|
||||
working-directory: site
|
||||
working-directory: app
|
||||
-
|
||||
name: "App: Deploy to S3"
|
||||
run: >-
|
||||
bash "aws/scripts/deploy/deploy-to-s3.sh" \
|
||||
--folder site/dist \
|
||||
--folder app/dist \
|
||||
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
|
||||
--storage-class ONEZONE_IA \
|
||||
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \
|
||||
|
||||
6
.github/workflows/tests.e2e.yaml
vendored
@@ -17,12 +17,10 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
-
|
||||
name: Run e2e tests
|
||||
run: npm run test:e2e -- --headless
|
||||
run: npm run test:cy:run
|
||||
|
||||
4
.github/workflows/tests.integration.yaml
vendored
@@ -19,9 +19,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
6
.github/workflows/tests.unit.yaml
vendored
@@ -16,10 +16,8 @@ jobs:
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
name: Set-up node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
3
.gitignore
vendored
@@ -5,6 +5,3 @@ dist/
|
||||
!.vscode/extensions.json
|
||||
#Electron-builder output
|
||||
/dist_electron
|
||||
# Cypress
|
||||
/tests/e2e/screenshots
|
||||
/tests/e2e/videos
|
||||
|
||||
4
.vscode/extensions.json
vendored
@@ -11,8 +11,8 @@
|
||||
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
|
||||
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.
|
||||
// Vue
|
||||
"jcbuisson.vue", // Highlights syntax.
|
||||
"octref.vetur", // Adds Vetur, Vue tooling support.
|
||||
"Vue.volar", // Official Vue extensions
|
||||
"Vue.vscode-typescript-vue-plugin", // Official TypeScript Vue Plugin
|
||||
// Scripting
|
||||
"timonwong.shellcheck", // Lints bash files.
|
||||
"ms-vscode.powershell", // Lints PowerShell files.
|
||||
|
||||
74
CHANGELOG.md
@@ -1,5 +1,79 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.1 (2023-08-17)
|
||||
|
||||
* Transition to eslint-config-airbnb-with-typescript | [ff84f56](https://github.com/undergroundwires/privacy.sexy/commit/ff84f5676e496dd7ec5b3599e34ec9627d181ea2)
|
||||
* Improve user privacy with secure outbound links | [3a594ac](https://github.com/undergroundwires/privacy.sexy/commit/3a594ac7fd708dc1e98155ffb9b21acd4e1fcf2d)
|
||||
* Refactor Vue components using Composition API #230 | [1b9be8f](https://github.com/undergroundwires/privacy.sexy/commit/1b9be8fe2d72d8fb5cf1fed6dcc0b9777171aa98)
|
||||
* Fix failing security tests | [3bc8da4](https://github.com/undergroundwires/privacy.sexy/commit/3bc8da4cbf1e2bd758dc3fffe4b1e62dc3beb7b3)
|
||||
* Improve Defender scripts #201 | [061afad](https://github.com/undergroundwires/privacy.sexy/commit/061afad9673a41454c2421c318898f2b4f4cf504)
|
||||
* Fix failing tests due to failed error logging | [986ba07](https://github.com/undergroundwires/privacy.sexy/commit/986ba078a643de6acbee50fff9cf77494ca7ea7f)
|
||||
* Implement custom lightweight modal #230 | [9e5491f](https://github.com/undergroundwires/privacy.sexy/commit/9e5491fdbf2d9d40d974f5ad0e879a6d5c6d1e55)
|
||||
* Refactor usage of tooltips for flexibility | [bc91237](https://github.com/undergroundwires/privacy.sexy/commit/bc91237d7c54bdcd15c5c39a55def50d172bb659)
|
||||
* Fix revert toggle partial rendering | [39e650c](https://github.com/undergroundwires/privacy.sexy/commit/39e650cf110bee6b1b21d9b2902b36b0e2568d54)
|
||||
* Increase testability through dependency injection | [ae75059](https://github.com/undergroundwires/privacy.sexy/commit/ae75059cc14db41f55dd2056f528442c7d319dd2)
|
||||
* Refactor filter (search query) event handling | [6a20d80](https://github.com/undergroundwires/privacy.sexy/commit/6a20d804dc365d22c1248d787f9912271f508eeb)
|
||||
* Migrate to ES6 modules | [a14929a](https://github.com/undergroundwires/privacy.sexy/commit/a14929a13cc6260b514692d9b4f1cdf5fb85d8b2)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.0...0.12.1)
|
||||
|
||||
## 0.12.0 (2023-08-03)
|
||||
|
||||
* Improve script/category name validation | [b210aad](https://github.com/undergroundwires/privacy.sexy/commit/b210aaddf26629179f77fe19f62f65d8a0ca2b87)
|
||||
* Improve touch like hover on devices without mouse | [99e24b4](https://github.com/undergroundwires/privacy.sexy/commit/99e24b4134c461c336f6d08f49d193d853325d31)
|
||||
* Improve click/touch without unintended interaction | [3233d9b](https://github.com/undergroundwires/privacy.sexy/commit/3233d9b8024dd59600edddef6d017e0089f59a9d)
|
||||
* Align card icons vertically in cards view | [8608072](https://github.com/undergroundwires/privacy.sexy/commit/8608072bfb52d10a843a86d3d89b14e8b9776779)
|
||||
* Fix broken npm installation and builds | [924b326](https://github.com/undergroundwires/privacy.sexy/commit/924b326244a175428175e0df3a50685ee5ac2ec6)
|
||||
* Improve documentation support with markdown | [6067bdb](https://github.com/undergroundwires/privacy.sexy/commit/6067bdb24e6729d2249c9685f4f1c514c3167d91)
|
||||
* win: add more Visual Studio scripts, support 2022 | [df533ad](https://github.com/undergroundwires/privacy.sexy/commit/df533ad3b19cebdf3454895aa2182bd4184e0360)
|
||||
* win: add script to remove Widgets | [bbc6156](https://github.com/undergroundwires/privacy.sexy/commit/bbc6156281fb3fd4b66c63dec3f765780fafa855)
|
||||
* Use line endings based on script language #88 | [6b3f465](https://github.com/undergroundwires/privacy.sexy/commit/6b3f4659df0afe1c99a8af6598df44a33c1f863a)
|
||||
* win: improve OneDrive removal | [58ed7b4](https://github.com/undergroundwires/privacy.sexy/commit/58ed7b456b3cf11774c83c8c1c04db37ef3058c2)
|
||||
* Use lowercase in script names and search text | [430537f](https://github.com/undergroundwires/privacy.sexy/commit/430537f70411756bbcaae837964c0223f78581e8)
|
||||
* Improve manual execution instructions | [7d3670c](https://github.com/undergroundwires/privacy.sexy/commit/7d3670c26d0151ddc43303e8ed5e47715f0e0f00)
|
||||
* Add multiline support for with expression | [e8d06e0](https://github.com/undergroundwires/privacy.sexy/commit/e8d06e0f3e178a69861e0197f9d1cce9af3958f1)
|
||||
* Break line in inline codes in documentation | [c1c2f29](https://github.com/undergroundwires/privacy.sexy/commit/c1c2f2925fe88ec1f56bf7655b6b9a10aa3ea024)
|
||||
* win: add script to increase RSA key exchange #165 | [a2e0921](https://github.com/undergroundwires/privacy.sexy/commit/a2e092190d8eb0fc9ceb8533572f04fff52f097b)
|
||||
* win: add scripts to downloaded file handling #153 | [e7b816d](https://github.com/undergroundwires/privacy.sexy/commit/e7b816d1564afa98c63291f9d7fd6f3fee92f4ec)
|
||||
* Drop support for dead browsers | [bf0c55f](https://github.com/undergroundwires/privacy.sexy/commit/bf0c55fa60bf2be070678ba27db14baf13fec511)
|
||||
* Add support for nested templates | [68a5d69](https://github.com/undergroundwires/privacy.sexy/commit/68a5d698a2ce644ce25754016fb9e9bb642e41a7)
|
||||
* mac: add scripts to configure Parallels Desktop | [64cca1d](https://github.com/undergroundwires/privacy.sexy/commit/64cca1d9b8946b92e21e86deb6db5612570befb1)
|
||||
* Rework icon with higher quality and new color | [f4a7ca7](https://github.com/undergroundwires/privacy.sexy/commit/f4a7ca76b885b8346d8a9c32e6269eabc2d8139f)
|
||||
* Relax and improve code validation | [e819993](https://github.com/undergroundwires/privacy.sexy/commit/e8199932b462380741d9f2d8b6b55485ab16af02)
|
||||
* Add initial Linux support #150 | [c404dfe](https://github.com/undergroundwires/privacy.sexy/commit/c404dfebe2908bb165279f8279f3f5e805b647d7)
|
||||
* mac: add script to disable personalized ads | [8b374a3](https://github.com/undergroundwires/privacy.sexy/commit/8b374a37b401699d5056bfd6b735b6a26c395ae0)
|
||||
* Update dependencies and add npm setup script | [5721796](https://github.com/undergroundwires/privacy.sexy/commit/57217963787a8ab0c71d681c6b1673c484c88226)
|
||||
* Fix macOS desktop build failure in CI | [5901dc5](https://github.com/undergroundwires/privacy.sexy/commit/5901dc5f11dd29be14c2616fc0ceb45196a43224)
|
||||
* Change subtitle heading to new slogan | [1e80ee1](https://github.com/undergroundwires/privacy.sexy/commit/1e80ee1fb0208d92943619468dc427853cbe8de7)
|
||||
* win: add new scripts to disable more telemetry | [298b058](https://github.com/undergroundwires/privacy.sexy/commit/298b058e5c89397db6f759b275442ba05499ac8c)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.4...0.12.0)
|
||||
|
||||
## 0.11.4 (2022-03-08)
|
||||
|
||||
* Improve performance of selecting scripts | [8e96c19](https://github.com/undergroundwires/privacy.sexy/commit/8e96c19126aa4cba6418de5ccaa9e2dcf8faab78)
|
||||
* Fix reverting of Windows NVIDIA telemetry service | [2354f0b](https://github.com/undergroundwires/privacy.sexy/commit/2354f0ba9fed3aa23569b5ea6391a7119fe1ab53)
|
||||
* Add AirBnb TypeScript overrides for linting | [834ce8c](https://github.com/undergroundwires/privacy.sexy/commit/834ce8cf9e8e46934dfa604526360870d109765b)
|
||||
* Transpile dependencies for wider browser support | [0e52a99](https://github.com/undergroundwires/privacy.sexy/commit/0e52a99efa2b02d1aba10885a76e03aa6f9be7f8)
|
||||
* Add more and unify tests for absent object cases | [44d79e2](https://github.com/undergroundwires/privacy.sexy/commit/44d79e2c9a97639bbd188a8fdfd740f1a5a1d6ee)
|
||||
* Fix Windows DoSvc not being disabled #115 | [43ce834](https://github.com/undergroundwires/privacy.sexy/commit/43ce834750ddf471636d1ece4324d02357947f9f)
|
||||
* Move stubs from `./stubs` to `./shared/Stubs` | [803ef2b](https://github.com/undergroundwires/privacy.sexy/commit/803ef2bb3eea68306377e40e326c791402998650)
|
||||
* Improve documentation for developing | [3c3ec80](https://github.com/undergroundwires/privacy.sexy/commit/3c3ec80525b97e8a24db4c44bbf42a7b4e089056)
|
||||
* Improve documentation for architecture | [1bcc6c8](https://github.com/undergroundwires/privacy.sexy/commit/1bcc6c8b2b923b4d4b1662f990d86b190ce73342)
|
||||
* Improve existing documentation | [db47440](https://github.com/undergroundwires/privacy.sexy/commit/db47440d470ea6a6e100b620b10d078c01314992)
|
||||
* Refactor to remove code coupling with Webpack | [5bbbb9c](https://github.com/undergroundwires/privacy.sexy/commit/5bbbb9cecca0a3828036e7fc34dcd66970ce334a)
|
||||
* Refactor to remove hardcoding of aliases | [481a02a](https://github.com/undergroundwires/privacy.sexy/commit/481a02afd5190eb77a37fa450e50816b2268e99c)
|
||||
* Document WpnService breaking on Windows 10 #110 | [3785e41](https://github.com/undergroundwires/privacy.sexy/commit/3785e410db461f667a834e0b388d81e4baa028e4)
|
||||
* Fix error when reverting Windows Defender setting | [956052c](https://github.com/undergroundwires/privacy.sexy/commit/956052c8fff042812fe84fe4d7fa5c579365ff9b)
|
||||
* Fix Windows 11 being detected as Windows 10 | [d6bc33e](https://github.com/undergroundwires/privacy.sexy/commit/d6bc33ec865d50efc6b8d4ccc2f789edd874fcee)
|
||||
* Refactor to use version object #59 | [eeb1d5b](https://github.com/undergroundwires/privacy.sexy/commit/eeb1d5b0c40a55675921af3f67f366b2ff658acf)
|
||||
* Fix Microsoft Defender alert for uninstaller #114 | [112e79a](https://github.com/undergroundwires/privacy.sexy/commit/112e79a64c6153f4ce3b48c27a09639e7647aebc)
|
||||
* Add donation information | [05a6a84](https://github.com/undergroundwires/privacy.sexy/commit/05a6a84c3739ec900343591ac1f7a9f310cd73f2)
|
||||
* Bump node environment to 16.x | [242a497](https://github.com/undergroundwires/privacy.sexy/commit/242a497e7debb351da19b20b63a3554f0cca4b5c)
|
||||
* Bump dependencies to latest | [efd63ff](https://github.com/undergroundwires/privacy.sexy/commit/efd63ff85dea4c9a9c033c54bc1be378742de351)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.3...0.11.4)
|
||||
|
||||
## 0.11.3 (2022-01-05)
|
||||
|
||||
* Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd)
|
||||
|
||||
44
README.md
@@ -1,16 +1,16 @@
|
||||
# privacy.sexy
|
||||
# privacy.sexy — Now you have the choice
|
||||
|
||||
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
||||
> Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆
|
||||
|
||||
<!-- markdownlint-disable MD033 -->
|
||||
<p align="center">
|
||||
<a href="https://undergroundwires.dev/donate?project=privacy.sexy">
|
||||
<a href="https://undergroundwires.dev/donate?project=privacy.sexy" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="donation badge"
|
||||
src="https://undergroundwires.dev/img/badges/donate/flat.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md">
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="contributions are welcome"
|
||||
src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"
|
||||
@@ -18,13 +18,13 @@
|
||||
</a>
|
||||
<!-- Code quality -->
|
||||
<br />
|
||||
<a href="https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript">
|
||||
<a href="https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Language grade: JavaScript/TypeScript"
|
||||
src="https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability">
|
||||
<a href="https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Maintainability"
|
||||
src="https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability"
|
||||
@@ -32,19 +32,19 @@
|
||||
</a>
|
||||
<!-- Tests -->
|
||||
<br />
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.unit.yaml">
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.unit.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Unit tests status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/unit-tests/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.integration.yaml">
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.integration.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Integration tests status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/integration-tests/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.e2e.yaml">
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.e2e.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="E2E tests status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/e2e-tests/badge.svg"
|
||||
@@ -52,39 +52,45 @@
|
||||
</a>
|
||||
<!-- Checks -->
|
||||
<br />
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml">
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Quality checks status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.yaml">
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Security checks status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/security-checks/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml">
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Build checks status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.desktop-runtime-errors.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Status of runtime error checks for the desktop application"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.desktop-runtime-errors/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<!-- Release -->
|
||||
<br />
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml">
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Git release status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-git/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.site.yaml">
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.site.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Site release status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-site/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.desktop.yaml">
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.desktop.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Desktop application release status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-desktop/badge.svg"
|
||||
@@ -92,7 +98,7 @@
|
||||
</a>
|
||||
<!-- Others -->
|
||||
<br />
|
||||
<a href="https://github.com/undergroundwires/bump-everywhere">
|
||||
<a href="https://github.com/undergroundwires/bump-everywhere" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Auto-versioned by bump-everywhere"
|
||||
src="https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true"
|
||||
@@ -120,15 +126,15 @@ Online version does not require to run any software on your computer. Offline ve
|
||||
- **Reversible**. Revert if something feels wrong.
|
||||
- **Accessible**. No need to run any compiled software on your computer with web version.
|
||||
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
||||
- **Tested.** A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
||||
- **Tested**. A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
||||
- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md).
|
||||
- **Portable and simple**. Every script is independently executable without cross-dependencies.
|
||||
|
||||
## Support
|
||||
|
||||
**Sponsor 💕**. This project is free, and it might not be tempting to donate since you don't have to pay. But your donations will ensure that this project stays alive. A monthly coffee from you would make a difference. Recurring donations allow me to spend more time and resources on this project. Consider sponsoring on [GitHub Sponsors](https://github.com/sponsors/undergroundwires), or you can donate using [other ways such as crypto or a coffee](https://undergroundwires.dev/donate).
|
||||
**Sponsor 💕**. Consider sponsoring on [GitHub Sponsors](https://github.com/sponsors/undergroundwires), or you can donate using [other ways such as crypto or a coffee](https://undergroundwires.dev/donate).
|
||||
|
||||
**Star 🤩**. I know that not everyone can afford donating a coffee to show support. In this case, feel free to give it a star ⭐ . It helps me to see that you appreciate the project.
|
||||
**Star 🤩**. Feel free to give it a star ⭐ .
|
||||
|
||||
**Contribute 👷**. Contributions of any type are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) as the starting point. It includes useful information like [how to add new scripts](./CONTRIBUTING.md#extend-scripts).
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset',
|
||||
],
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
# build
|
||||
|
||||
- These are the file that are used by electron.
|
||||
- Logos are created by from the [PNG icon](./../public/icon.png)
|
||||
- by running `npx electron-icon-builder --input=./public/icon.png --output=build --flatten`
|
||||
This folder contains files that are used by Electron to serve the desktop version.
|
||||
|
||||
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md).
|
||||
|
||||
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 740 B After Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |
15
cypress.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'cypress';
|
||||
import ViteConfig from './vite.config';
|
||||
|
||||
const CYPRESS_BASE_DIR = 'tests/e2e/';
|
||||
|
||||
export default defineConfig({
|
||||
fixturesFolder: `${CYPRESS_BASE_DIR}/fixtures`,
|
||||
screenshotsFolder: `${CYPRESS_BASE_DIR}/screenshots`,
|
||||
videosFolder: `${CYPRESS_BASE_DIR}/videos`,
|
||||
e2e: {
|
||||
baseUrl: `http://localhost:${ViteConfig.server.port}/`,
|
||||
specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
|
||||
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
|
||||
},
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"pluginsFile": "tests/e2e/plugins/index.js"
|
||||
}
|
||||
@@ -35,7 +35,7 @@ Application layer enables [data-driven programming](https://en.wikipedia.org/wik
|
||||
|
||||
Application layer parses the application data to compile the domain object [`Application.ts`](./../src/domain/Application.ts).
|
||||
|
||||
A webpack loader loads (or injects) application data ([collection yaml files](./../src/application/collections/)) into the application layer in compile time. Application layer ([`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)) parses and compiles this data in runtime.
|
||||
The build tool loads (or injects) application data ([collection yaml files](./../src/application/collections/)) into the application layer in compile time. Application layer ([`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)) parses and compiles this data in runtime.
|
||||
|
||||
Application layer compiles templating syntax during parsing to create the end scripts. You can read more about templating syntax in [templating.md](./templating.md) and how application data uses them through functions in [collection-files.md | Function](./collection-files.md#function).
|
||||
|
||||
|
||||
@@ -15,11 +15,23 @@ Application is
|
||||
|
||||
Application uses highly decoupled models & services in different DDD layers:
|
||||
|
||||
- presentation layer (see [presentation.md](./presentation.md)),
|
||||
- application layer (see [application.md](./application.md)),
|
||||
- and domain layer.
|
||||
**Application layer** (see [application.md](./application.md)):
|
||||
|
||||
Application layer depends on and consumes domain layer. [Presentation layer](./presentation.md) consumes and depends on application layer along with domain layer. Application and presentation layers can communicate through domain model.
|
||||
- Coordinates application activities and consumes the domain layer.
|
||||
|
||||
**Presentation layer** (see [presentation.md](./presentation.md)):
|
||||
|
||||
- Handles UI/UX, consumes both the application and domain layers.
|
||||
- May communicate directly with the infrastructure layer for technical needs, but avoids domain logic.
|
||||
|
||||
**Domain layer**:
|
||||
|
||||
- Serves as the system's core and central truth.
|
||||
- Facilitates communication between the application and presentation layers through the domain model.
|
||||
|
||||
**Infrastructure layer**:
|
||||
|
||||
- Manages technical implementations without dependencies on other layers or domain knowledge.
|
||||
|
||||

|
||||
|
||||
@@ -27,6 +39,8 @@ Application layer depends on and consumes domain layer. [Presentation layer](./p
|
||||
|
||||
State handling uses an event-driven subscription model to signal state changes and special functions to register changes. It does not depend on third party packages.
|
||||
|
||||
The presentation layer can read and modify state through the context. State changes trigger events that components can subscribe to for reactivity.
|
||||
|
||||
Each layer treat application layer differently.
|
||||
|
||||

|
||||
@@ -45,7 +59,7 @@ Each layer treat application layer differently.
|
||||
- So state is mutable, and fires related events when mutated.
|
||||
- 📖 Read more: [application.md | Application state](./application.md#application-state).
|
||||
|
||||
It's comparable with flux ([`redux`](https://redux.js.org/)) or flux-like ([`vuex`](https://vuex.vuejs.org/)) patterns. Flux component "view" is [presentation layer](./presentation.md) in Vue. Flux functions "dispatcher", "store" and "action creation" functions lie in the [application layer](./application.md). A difference is that application state in privacy.sexy is mutable and lies in single flux "store" that holds app state and logic. The "actions" mutate the state directly which in turns act as dispatcher to notify its own event subscriptions (callbacks).
|
||||
It's comparable with `flux`, `vuex`, and `pinia`. A difference is that mutable application layer state in privacy.sexy is mutable and lies in single "store" that holds app state and logic. The "actions" mutate the state directly which in turns act as dispatcher to notify its own event subscriptions (callbacks).
|
||||
|
||||
## AWS infrastructure
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ Everything that's merged in the master goes directly to production.
|
||||
|
||||
privacy.sexy uses [GitHub actions](https://github.com/features/actions) to define and run pipelines as code.
|
||||
|
||||
GitHub workflows i.e. pipelines exist in [`/.github/.workflows/`](./../.github/workflows/) folder without any subfolders due to GitHub actions requirements [1] .
|
||||
GitHub workflows i.e. pipelines exist in [`/.github/workflows/`](./../.github/workflows/) folder without any subfolders due to GitHub actions requirements [1] .
|
||||
|
||||
[1]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows
|
||||
Local GitHub actions are defined in [`/.github/actions/`](./../.github/actions/) and used to reuse same workflow steps.
|
||||
|
||||
## Pipeline types
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
|
||||
- 💡 Best practices
|
||||
- If you repeat yourself, try to utilize [YAML-defined functions](#Function)
|
||||
- Always try to add documentation and a way to revert a tweak in [scripts](#Script)
|
||||
- If you repeat yourself, try to utilize [YAML-defined functions](#function)
|
||||
- Always try to add documentation and a way to revert a tweak in [scripts](#script)
|
||||
- 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
|
||||
|
||||
## Objects
|
||||
@@ -13,19 +13,19 @@
|
||||
- A collection simply defines:
|
||||
- different categories and their scripts in a tree structure
|
||||
- OS specific details
|
||||
- Also allows defining common [function](#Function)s to be used throughout the collection if you'd like different scripts to share same code.
|
||||
- Also allows defining common [function](#function)s to be used throughout the collection if you'd like different scripts to share same code.
|
||||
|
||||
#### `Collection` syntax
|
||||
|
||||
- `os:` *`string`* (**required**)
|
||||
- Operating system that the [Collection](#collection) is written for.
|
||||
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
|
||||
- `actions: [` ***[`Category`](#Category)*** `, ... ]` **(required)**
|
||||
- `actions: [` ***[`Category`](#category)*** `, ... ]` **(required)**
|
||||
- Each [category](#category) is rendered as different cards in card presentation.
|
||||
- ❗ A [Collection](#collection) must consist of at least one category.
|
||||
- `functions: [` ***[`Function`](#Function)*** `, ... ]`
|
||||
- `functions: [` ***[`Function`](#function)*** `, ... ]`
|
||||
- Functions are optionally defined to re-use the same code throughout different scripts.
|
||||
- `scripting:` ***[`ScriptingDefinition`](#ScriptingDefinition)*** **(required)**
|
||||
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
|
||||
- Defines the scripting language that the code of other action uses.
|
||||
|
||||
### `Category`
|
||||
@@ -38,9 +38,12 @@
|
||||
- `category:` *`string`* (**required**)
|
||||
- Name of the category
|
||||
- ❗ Must be unique throughout the [Collection](#collection)
|
||||
- `children: [` ***[`Category`](#Category)*** `|` [***`Script`***](#Script) `, ... ]` (**required**)
|
||||
- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#script) `, ... ]` (**required**)
|
||||
- ❗ Category must consist of at least one subcategory or script.
|
||||
- Children can be combination of scripts and subcategories.
|
||||
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||
- Documentation pieces related to the category.
|
||||
- Rendered as markdown.
|
||||
|
||||
### `Script`
|
||||
|
||||
@@ -67,12 +70,12 @@
|
||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||
- ❗ Do not define if `call` is defined.
|
||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
||||
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**)
|
||||
- A shared function or sequence of functions to call (called in order)
|
||||
- ❗ If not defined `code` must be defined
|
||||
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||
- Single documentation URL or list of URLs for those who wants to learn more about the script
|
||||
- E.g. `https://docs.microsoft.com/en-us/windows-server/`
|
||||
- Documentation pieces related to the script.
|
||||
- Rendered as markdown.
|
||||
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
|
||||
- If not defined then the script will not be recommended
|
||||
- If defined it can be either
|
||||
@@ -120,7 +123,7 @@
|
||||
- Convention is to use camelCase, and be verbs.
|
||||
- E.g. `uninstallStoreApp`
|
||||
- ❗ Function names must be unique
|
||||
- `parameters`: `[` ***[`FunctionParameter`](#FunctionParameter)*** `, ... ]`
|
||||
- `parameters`: `[` ***[`FunctionParameter`](#functionparameter)*** `, ... ]`
|
||||
- List of parameters that function code refers to.
|
||||
- ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
|
||||
`code`: *`string`* (**required** if `call` is undefined)
|
||||
@@ -133,7 +136,7 @@
|
||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in code
|
||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
||||
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**)
|
||||
- A shared function or sequence of functions to call (called in order)
|
||||
- The parameter values that are sent can use [expressions (templating)](./templating.md#expressions)
|
||||
- ❗ If not defined `code` must be defined
|
||||
@@ -141,7 +144,7 @@
|
||||
### `FunctionParameter`
|
||||
|
||||
- Defines a parameter that function requires optionally or mandatory.
|
||||
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#FunctionCall).
|
||||
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#functioncall).
|
||||
|
||||
#### `FunctionParameter` syntax
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ You could run other types of tests as well, but they may take longer time and ov
|
||||
|
||||
- Run unit tests: `npm run test:unit`
|
||||
- Run integration tests: `npm run test:integration`
|
||||
- Run e2e (end-to-end) tests
|
||||
- Interactive mode with GUI: `npm run test:e2e`
|
||||
- Headless mode without GUI: `npm run test:e2e -- --headless`
|
||||
- Run end-to-end (e2e) tests:
|
||||
- `npm run test:cy:open`: Run tests interactively using the development server with hot-reloading.
|
||||
- `npm run test:cy:run`: Run tests on the production build in a headless mode.
|
||||
|
||||
📖 Read more about testing in [tests](./tests.md).
|
||||
|
||||
@@ -35,16 +35,38 @@ You could run other types of tests as well, but they may take longer time and ov
|
||||
|
||||
### Running
|
||||
|
||||
- Run in local server: `npm run serve`
|
||||
**Web:**
|
||||
|
||||
- Run in local server: `npm run dev`
|
||||
- 💡 Meant for local development with features such as hot-reloading.
|
||||
- Run using Docker:
|
||||
1. Build: `docker build -t undergroundwires/privacy.sexy:latest .`
|
||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest`
|
||||
- Preview production build: `npm run preview`
|
||||
- Start a local web server that serves the built solution from `./dist`.
|
||||
- 💡 Run `npm run build` before `npm run preview`.
|
||||
|
||||
**Desktop apps:**
|
||||
|
||||
- `npm run electron:dev`: The command will build the main process and preload scripts source code, and start a dev server for the renderer, and start the Electron app.
|
||||
- `npm run electron:preview`: The command will build the main process, preload scripts and renderer source code, and start the Electron app to preview.
|
||||
- `npm run electron:prebuild`: The command will build the main process, preload scripts and renderer source code. Usually before packaging the Electron application, you need to execute this command.
|
||||
- `npm run electron:build`: Prebuilds the Electron application, packages and publishes it through `electron-builder`.
|
||||
|
||||
**Docker:**
|
||||
|
||||
1. Build: `docker build -t undergroundwires/privacy.sexy:latest .`
|
||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest`
|
||||
|
||||
### Building
|
||||
|
||||
- Build web application: `npm run build`
|
||||
- Build desktop application: `npm run electron:build`
|
||||
- (Re)create icons (see [documentation](../img/README.md)): `npm run create-icons`
|
||||
|
||||
### Utility Scripts
|
||||
|
||||
- Run fresh NPM install: [`./scripts/fresh-npm-install.sh`](../scripts/fresh-npm-install.sh)
|
||||
- This script provides a clean NPM install, removing existing node modules and optionally the package-lock.json (when run with -n), then installs dependencies and runs unit tests.
|
||||
- Configure VSCode: [`./scripts/configure-vscode.sh`](../scripts/configure-vscode.sh)
|
||||
- This script checks and sets the necessary configurations for VSCode in `settings.json` file.
|
||||
|
||||
## Recommended extensions
|
||||
|
||||
|
||||
@@ -1,34 +1,42 @@
|
||||
# Presentation layer
|
||||
|
||||
Presentation layer consists of UI-related code. It uses Vue.js as JavaScript framework and includes Vue.js components. It also includes [Electron](https://www.electronjs.org/) to provide functionality to desktop application.
|
||||
The presentation layer handles UI concerns using Vue as JavaScript framework and Electron to provide desktop functionality.
|
||||
|
||||
It's designed event-driven from bottom to top. It listens user events (from top) and state events (from bottom) to update state or the GUI.
|
||||
It reflects the [application state](./application.md#application-state) and allows user interactions to modify it. Components manage their own local UI state.
|
||||
|
||||
The presentation layer uses an event-driven architecture for bidirectional reactivity between the application state and UI. State change events flow bottom-up to trigger UI updates, while user events flow top-down through components, some ultimately modifying the application state.
|
||||
|
||||
📖 Refer to [architecture.md (Layered Application)](./architecture.md#layered-application) to read more about the layered architecture.
|
||||
|
||||
## Structure
|
||||
|
||||
- [`/src/` **`presentation/`**](./../src/presentation/): Contains all presentation related code including Vue and Electron configurations
|
||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
|
||||
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
|
||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that other components share.
|
||||
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that webpack will process.
|
||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
|
||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
|
||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains reusable styles coupled to a Vue/HTML component.
|
||||
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles that override third-party components used.
|
||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Primary Sass file, passes along all other styles, should be the single file used from other components.
|
||||
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
|
||||
- [**`electron/`**](./../src/presentation/electron/): Electron configuration for the desktop application.
|
||||
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
|
||||
- [**`/public/`**](./../public/): Contains static assets that are directly copied and do not go through webpack.
|
||||
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`.
|
||||
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations used by Vue CLI internally.
|
||||
- [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`.
|
||||
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code.
|
||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
||||
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
||||
- [**`hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections).
|
||||
- [**`/public/`**](../src/presentation/public/): Contains static assets.
|
||||
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
|
||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles for Vue components.
|
||||
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles for third-party components.
|
||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
|
||||
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
||||
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
||||
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
||||
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
||||
|
||||
## Visual design best-practices
|
||||
|
||||
Add visual clues for clickable items. It should be as clear as possible that they're interactable at first look without hovering. They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility.
|
||||
|
||||
## Application data
|
||||
|
||||
Components (should) use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again.
|
||||
Components (should) use [`UseApplication`](./../src/presentation/components/Shared/Hooks/UseApplication.ts) to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again.
|
||||
|
||||
[Application.ts](../src/domain/Application.ts) is an immutable domain model that represents application state. It includes:
|
||||
|
||||
@@ -39,32 +47,49 @@ You can read more about how application layer provides application data to he pr
|
||||
|
||||
## Application state
|
||||
|
||||
Inheritance of a Vue components marks whether it uses application state . Components that does not handle application state extends `Vue`. Stateful components mutate or/and react to state changes (such as user selection or search queries) in [ApplicationContext](./../src/application/Context/ApplicationContext.ts) extend [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) class to access the context / state.
|
||||
This project uses a singleton instance of the application state, making it available to all Vue components.
|
||||
|
||||
[`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) functions include:
|
||||
The decision to not use third-party state management libraries like [`vuex`](https://web.archive.org/web/20230801191617/https://vuex.vuejs.org/) or [`pinia`](https://web.archive.org/web/20230801191743/https://pinia.vuejs.org/) was made to promote code independence and enhance portability.
|
||||
|
||||
- Creating a singleton of the state and makes it available to presentation layer as single source of truth.
|
||||
- Providing virtual abstract `handleCollectionState` callback that it calls when
|
||||
- the Vue loads the component,
|
||||
- and also every time when state changes.
|
||||
- Providing `events` member to make lifecycling of state subscriptions events easier because it ensures that components unsubscribe from listening to state events when
|
||||
- the component is no longer used (destroyed),
|
||||
- an if [ApplicationContext](./../src/application/Context/ApplicationContext.ts) changes the active [collection](./collection-files.md) to a different one.
|
||||
Stateful components can mutate and/or react to state changes (e.g., user selection, search queries) in the [ApplicationContext](./../src/application/Context/ApplicationContext.ts). Vue components import [`CollectionState.ts`](./../src/presentation/components/Shared/Hooks/UseCollectionState.ts) to access both the application context and the state.
|
||||
|
||||
📖 Refer to [architecture.md | Application State](./architecture.md#application-state) to get an overview of event handling and [application.md | Application State](./presentation.md#application-state) for deeper look into how the application layer manages state.
|
||||
[`UseCollectionState.ts`](./../src/presentation/components/Shared/Hooks/UseCollectionState.ts) provides several functionalities including:
|
||||
|
||||
## Modals
|
||||
- **Singleton State Instance**: It creates a singleton instance of the state, which is shared across the presentation layer. The singleton instance ensures that there's a single source of truth for the application's state.
|
||||
- **State Change Callback and Lifecycle Management**: It offers a mechanism to register callbacks, which will be invoked when the state initializes or mutates. It ensures that components unsubscribe from state events when they are no longer in use or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md).
|
||||
- **State Access and Modification**: It provides functions to read and mutate for accessing and modifying the state, encapsulating the details of these operations.
|
||||
- **Event Subscription Lifecycle Management**: Includes an `events` member that simplifies state subscription lifecycle events. This ensures that components unsubscribe from state events when they are no longer in use, or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md).
|
||||
|
||||
[Dialog.vue](./../src/presentation/components/Shared/Dialog.vue) is a shared component that other components used to show modal windows.
|
||||
📖 Refer to [architecture.md | Application State](./architecture.md#application-state) for an overview of event handling and [application.md | Application State](./presentation.md#application-state) for an in-depth understanding of state management in the application layer.
|
||||
|
||||
You can use it by wrapping the content inside of its `slot` and call `.show()` function on its reference. For example:
|
||||
## Dependency injections
|
||||
|
||||
```html
|
||||
<Dialog ref="testDialog">
|
||||
<div>Hello world</div>
|
||||
</Dialog>
|
||||
<div @click="$refs.testDialog.show()">Show dialog</div>
|
||||
```
|
||||
The presentation layer uses Vue's native dependency injection system to increase testability and decouple components.
|
||||
|
||||
To add a new dependency:
|
||||
|
||||
1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into:
|
||||
- **Singletons**: Shared across components, instantiated once.
|
||||
- **Transients**: Factories yielding a new instance on every access.
|
||||
2. **Provide the dependency**: Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency. [`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
|
||||
3. **Inject the dependency**: Use Vue's `inject` method alongside the defined symbol to incorporate the dependency into components.
|
||||
- For singletons, invoke the factory method: `inject(symbolKey)()`.
|
||||
- For transients, directly inject: `inject(symbolKey)`.
|
||||
|
||||
## Shared UI components
|
||||
|
||||
Shared UI components promote consistency and simplifies the creation of the front-end.
|
||||
|
||||
In order to maintain portability and easy maintainability, the preference is towards using homegrown components over third-party ones or comprehensive UI frameworks like Quasar.
|
||||
|
||||
Shared components include:
|
||||
|
||||
- [ModalDialog.vue](./../src/presentation/components/Shared/Modal/ModalDialog.vue) is utilized for rendering modal windows.
|
||||
- [TooltipWrapper.vue](./../src/presentation/components/Shared/TooltipWrapper.vue) acts as a wrapper for rendering tooltips.
|
||||
|
||||
## Desktop builds
|
||||
|
||||
Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages.
|
||||
|
||||
## Sass naming convention
|
||||
|
||||
|
||||
@@ -10,10 +10,19 @@
|
||||
|
||||
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
|
||||
- E.g. `Hello {{ $name }} !`
|
||||
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) that has inspired this templating language.
|
||||
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
|
||||
- Functions enables usage of expressions.
|
||||
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
||||
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
||||
- Expressions inside expressions (nested templates) are supported.
|
||||
- An expression can output another expression that will also be compiled.
|
||||
- E.g. following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output.
|
||||
|
||||
```go
|
||||
{{ with $condition }}
|
||||
echo {{ $text }}
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
### Parameter substitution
|
||||
|
||||
@@ -56,9 +65,31 @@ A function can call other functions such as:
|
||||
|
||||
### with
|
||||
|
||||
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions. E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}`.
|
||||
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
|
||||
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
|
||||
|
||||
Binds its context (`.`) value of provided argument for the parameter if provided one. E.g. `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
|
||||
It binds its context (value of the provided parameter value) as arbitrary `.` value. It allows you to use the argument value of the given parameter when it is provided and not empty such as:
|
||||
|
||||
```go
|
||||
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
|
||||
```
|
||||
|
||||
It supports multiline text inside the block. You can have something like:
|
||||
|
||||
```go
|
||||
{{ with $argument }}
|
||||
First line
|
||||
Second line
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
||||
|
||||
```go
|
||||
{{ with $condition }}
|
||||
This is a different parameter: {{ $text }}
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
||||
|
||||
|
||||
122
docs/tests.md
@@ -5,77 +5,79 @@ There are different types of tests executed:
|
||||
1. [Unit tests](#unit-tests)
|
||||
2. [Integration tests](#integration-tests)
|
||||
3. [End-to-end (E2E) tests](#e2e-tests)
|
||||
4. [Automated checks](#automated-checks)
|
||||
|
||||
Common aspects for all tests:
|
||||
## Unit and integration tests
|
||||
|
||||
- They use [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/).
|
||||
- Their files end with `.spec.{ts|js}` suffix.
|
||||
|
||||
💡 You can use path/module alias `@/tests` in import statements.
|
||||
|
||||
## Unit tests
|
||||
|
||||
- Unit tests test each component in isolation.
|
||||
- All unit tests goes under [`./tests/unit`](./../tests/unit).
|
||||
- They rely on [stubs](./../tests/unit/shared/Stubs) for isolation.
|
||||
|
||||
### Unit tests structure
|
||||
|
||||
- [`./src/`](./../src/)
|
||||
- Includes source code that unit tests will test.
|
||||
- [`./tests/unit/`](./../tests/unit/)
|
||||
- Includes test code.
|
||||
- Tests follow same folder structure as [`./src/`](./../src).
|
||||
- E.g. if system under test lies in [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) then its tests would be in test would be at [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
||||
- [`shared/`](./../tests/unit/shared/)
|
||||
- Includes common functionality that's shared across unit tests.
|
||||
- [`Assertions/`](./../tests/unit/shared/Assertions):
|
||||
- Common assertions that extend [Chai Assertion Library](https://www.chaijs.com/).
|
||||
- Asserting functions should start with `expect` prefix.
|
||||
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
||||
- Shared test cases.
|
||||
- Functions that calls `it()` from [Mocha test framework](https://mochajs.org/) should have `it` prefix.
|
||||
- E.g. `itEachAbsentCollectionValue()`.
|
||||
- [`Stubs/`](./../tests/unit/shared/Stubs)
|
||||
- Includes stubs to be able to test components in isolation.
|
||||
- Stubs have minimal and dummy behavior to be functional, they may also have spying or mocking functions.
|
||||
|
||||
### Unit tests naming
|
||||
|
||||
- Each test suite first describe the system under test.
|
||||
- E.g. tests for class `Application.ts` are all inside `Application.spec.ts`.
|
||||
- `describe` blocks tests for same function (if applicable).
|
||||
- E.g. test for `run()` are inside `describe('run', () => ..)`.
|
||||
- They utilize [Vitest](https://vitest.dev/).
|
||||
- Test files are suffixed with `.spec.ts`.
|
||||
|
||||
### Act, arrange, assert
|
||||
|
||||
- Tests use act, arrange and assert (AAA) pattern when applicable.
|
||||
- Tests implement the act, arrange, and assert (AAA) pattern.
|
||||
- **Arrange**
|
||||
- Sets up the test case.
|
||||
- Starts with comment line `// arrange`.
|
||||
- Sets up the test scenario and environment.
|
||||
- Begins with comment line `// arrange`.
|
||||
- **Act**
|
||||
- Executes the actual test.
|
||||
- Starts with comment line `// act`.
|
||||
- Begins with comment line `// act`.
|
||||
- **Assert**
|
||||
- Elicit some sort of expectation.
|
||||
- Starts with comment line `// assert`.
|
||||
- Sets an expectation for the test's outcome.
|
||||
- Begins with comment line `// assert`.
|
||||
|
||||
## Integration tests
|
||||
### Unit tests
|
||||
|
||||
- Tests functionality of a component in combination with others (not isolated).
|
||||
- Ensure dependencies to third parties work as expected.
|
||||
- Defined in [./tests/integration](./../tests/integration).
|
||||
- Evaluate individual components in isolation.
|
||||
- Located in [`./tests/unit`](./../tests/unit).
|
||||
- Achieve isolation using [stubs](./../tests/unit/shared/Stubs).
|
||||
- Include Vue component tests, enabled by `@vue/test-utils`.
|
||||
|
||||
#### Unit tests naming
|
||||
|
||||
- Test suites start with a description of the component or system under test.
|
||||
- E.g., tests for `Application.ts` are contained in `Application.spec.ts`.
|
||||
- Whenever possible, `describe` blocks group tests of the same function.
|
||||
- E.g., tests for `run()` are inside `describe('run', () => ...)`.
|
||||
|
||||
### Integration tests
|
||||
|
||||
- Assess the combined functionality of components.
|
||||
- They verify that third-party dependencies function as anticipated.
|
||||
|
||||
## E2E tests
|
||||
|
||||
- Test the functionality and performance of a running application.
|
||||
- Vue CLI plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) configures E2E tests.
|
||||
- Test names and folders have logical structure based on tests executed.
|
||||
- The structure is following:
|
||||
- [`cypress.json`](./../cypress.json): Cypress configuration file.
|
||||
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
|
||||
- [`/specs/`](./../tests/e2e/specs/): Test files named with `.spec.js` extension.
|
||||
- [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before loading project.
|
||||
- [`/support/index.js`](./../tests/e2e/support/index.js): Support file, runs before every single spec file.
|
||||
- *(Ignored)* `/videos`: Asset folder for videos taken during tests.
|
||||
- *(Ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
||||
- Examine the live web application's functionality and performance.
|
||||
- Uses Cypress to run the tests.
|
||||
|
||||
## Automated checks
|
||||
|
||||
These checks validate various qualities like runtime execution, building process, security testing, etc.
|
||||
|
||||
- Use [various tools](./../package.json) and [scripts](./../scripts).
|
||||
- Are automatically executed as [GitHub workflows](./../.github/workflows).
|
||||
|
||||
## Tests structure
|
||||
|
||||
- [`package.json`](./../package.json): Defines test commands and includes tools used in tests.
|
||||
- [`vite.config.ts`](./../vite.config.ts): Configures `vitest` for unit and integration tests.
|
||||
- [`./src/`](./../src/): Contains the code subject to testing.
|
||||
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
|
||||
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
|
||||
- [`./tests/unit/`](./../tests/unit/)
|
||||
- Stores unit test code.
|
||||
- The directory structure mirrors [`./src/`](./../src).
|
||||
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
||||
- [`shared/`](./../tests/unit/shared/)
|
||||
- Contains shared unit test functionalities.
|
||||
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
|
||||
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
||||
- Shared test cases.
|
||||
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
|
||||
- [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities.
|
||||
- [`./tests/integration/`](./../tests/integration/): Contains integration test files.
|
||||
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
|
||||
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension.
|
||||
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single spec file.
|
||||
- [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation.
|
||||
- *(git ignored)* `/videos`: Asset folder for videos taken during tests.
|
||||
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
||||
|
||||
31
electron-builder.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
# -------
|
||||
# Windows
|
||||
# -------
|
||||
win:
|
||||
target: nsis
|
||||
nsis:
|
||||
artifactName: ${name}-${version}-Setup.${ext}
|
||||
|
||||
# -----
|
||||
# Linux
|
||||
# -----
|
||||
linux:
|
||||
target: AppImage
|
||||
appImage:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
|
||||
# -----
|
||||
# macOS
|
||||
# -----
|
||||
mac:
|
||||
target: dmg
|
||||
dmg:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
|
||||
# ----------------
|
||||
# Publish options
|
||||
# ----------------
|
||||
publish:
|
||||
provider: 'github'
|
||||
vPrefixedTagName: false # default: true
|
||||
releaseType: release # default: draft
|
||||
68
electron.vite.config.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { resolve } from 'path';
|
||||
import { mergeConfig, UserConfig } from 'vite';
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||
import { getAliasesFromTsConfig, getClientEnvironmentVariables } from './vite-config-helper';
|
||||
import { createVueConfig } from './vite.config';
|
||||
|
||||
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
|
||||
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
|
||||
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
|
||||
const DIST_DIR = resolvePathFromProjectRoot('dist_electron/');
|
||||
|
||||
export default defineConfig({
|
||||
main: getSharedElectronConfig({
|
||||
distDirSubfolder: 'main',
|
||||
entryFilePath: MAIN_ENTRY_FILE,
|
||||
}),
|
||||
preload: getSharedElectronConfig({
|
||||
distDirSubfolder: 'preload',
|
||||
entryFilePath: PRELOAD_ENTRY_FILE,
|
||||
}),
|
||||
renderer: mergeConfig(
|
||||
createVueConfig({
|
||||
supportLegacyBrowsers: false,
|
||||
}),
|
||||
{
|
||||
build: {
|
||||
outDir: resolve(DIST_DIR, 'renderer'),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: WEB_INDEX_HTML_PATH,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
function getSharedElectronConfig(options: {
|
||||
readonly distDirSubfolder: string;
|
||||
readonly entryFilePath: string;
|
||||
}): UserConfig {
|
||||
return {
|
||||
build: {
|
||||
outDir: resolve(DIST_DIR, options.distDirSubfolder),
|
||||
lib: {
|
||||
entry: options.entryFilePath,
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: '[name].cjs', // This is needed so `type="module"` works
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
define: {
|
||||
...getClientEnvironmentVariables(),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
...getAliasesFromTsConfig(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePathFromProjectRoot(pathSegment: string) {
|
||||
return resolve(__dirname, pathSegment);
|
||||
}
|
||||
9
img/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# img
|
||||
|
||||
This folder contains image files and other resources related to images.
|
||||
|
||||
## logo.svg
|
||||
|
||||
[`logo.svg`](./logo.svg) serves as the primary logo from which all other icons and images are derived.
|
||||
Only modify this file manually.
|
||||
After making changes, execute `npm run build:icons` to regenerate logo files in various formats.
|
||||
56
img/logo.svg
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="90.2998mm" height="90.2998mm"
|
||||
viewBox="0 0 256 256">
|
||||
<path id="logo"
|
||||
fill="#3a65ab" stroke="#3a65ab" stroke-width="1"
|
||||
d="M 128.00,173.00
|
||||
C 128.00,173.00 102.00,175.00 102.00,175.00
|
||||
85.39,174.97 64.02,170.31 49.00,163.22
|
||||
38.46,158.24 28.39,152.17 20.01,143.96
|
||||
14.88,138.93 10.72,133.32 7.31,127.00
|
||||
3.36,119.66 1.10,112.37 1.00,104.00
|
||||
1.00,104.00 1.00,98.00 1.00,98.00
|
||||
1.29,73.92 24.76,53.44 44.00,42.43
|
||||
84.66,19.15 129.75,23.12 169.00,47.00
|
||||
188.74,59.02 207.93,76.23 208.00,101.00
|
||||
208.00,101.00 188.00,101.00 188.00,101.00
|
||||
186.46,86.48 168.72,71.84 157.00,64.61
|
||||
140.76,54.59 121.33,46.78 102.00,47.00
|
||||
88.42,47.16 72.20,52.52 60.00,58.32
|
||||
45.30,65.30 19.83,84.81 21.10,103.00
|
||||
21.39,107.16 22.92,110.33 24.76,114.00
|
||||
32.70,129.78 48.16,140.02 64.00,146.72
|
||||
75.16,151.44 92.90,155.26 105.00,154.99
|
||||
113.45,154.79 121.81,152.84 130.00,151.00
|
||||
130.00,151.00 128.00,173.00 128.00,173.00 Z
|
||||
M 136.00,79.00
|
||||
C 142.71,81.35 144.84,93.60 144.99,100.00
|
||||
145.51,122.74 130.31,140.73 107.00,141.00
|
||||
83.63,141.26 67.43,126.52 66.09,103.00
|
||||
64.82,80.73 85.85,58.90 104.00,64.00
|
||||
100.18,69.73 95.45,74.53 96.20,82.00
|
||||
97.29,92.87 110.06,102.98 121.00,99.03
|
||||
129.92,95.81 134.61,87.96 136.00,79.00 Z
|
||||
M 186.00,113.46
|
||||
C 206.11,110.69 225.57,114.92 239.91,130.01
|
||||
252.85,143.63 255.21,157.09 255.00,175.00
|
||||
254.76,195.49 241.26,214.25 223.00,222.88
|
||||
213.06,227.58 204.72,228.12 194.00,228.00
|
||||
150.34,227.49 126.71,178.85 146.32,142.00
|
||||
154.93,125.82 168.55,117.23 186.00,113.46 Z
|
||||
M 233.00,181.00
|
||||
C 242.24,158.78 221.84,133.54 199.00,133.01
|
||||
188.40,132.77 182.75,135.31 174.00,141.00
|
||||
178.60,146.85 195.92,157.24 203.00,161.86
|
||||
209.82,166.32 226.61,178.55 233.00,181.00 Z
|
||||
M 221.00,200.00
|
||||
C 216.39,194.15 206.42,188.61 200.00,184.33
|
||||
192.31,179.21 168.77,162.59 162.00,160.00
|
||||
159.67,165.03 159.94,166.57 160.00,172.00
|
||||
160.23,190.99 177.11,207.55 196.00,207.99
|
||||
206.60,208.23 212.25,205.69 221.00,200.00 Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
48697
package-lock.json
generated
144
package.json
@@ -1,92 +1,106 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.11.3",
|
||||
"version": "0.12.1",
|
||||
"private": true,
|
||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
||||
"slogan": "Now you have the choice",
|
||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||
"author": "undergroundwires",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest run --dir tests/unit",
|
||||
"test:integration": "vitest run --dir tests/integration",
|
||||
"test:e2e": "vue-cli-service test:e2e",
|
||||
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
|
||||
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
||||
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
||||
"electron:build": "vue-cli-service electron:build",
|
||||
"electron:serve": "vue-cli-service electron:serve",
|
||||
"icons:build": "node scripts/logo-update.js",
|
||||
"electron:dev": "electron-vite dev",
|
||||
"electron:preview": "electron-vite preview",
|
||||
"electron:prebuild": "electron-vite build",
|
||||
"electron:build": "electron-builder",
|
||||
"lint:eslint": "eslint .",
|
||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||
"lint:eslint": "vue-cli-service lint --no-fix --mode production",
|
||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstall": "electron-builder install-app-deps",
|
||||
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\""
|
||||
"postuninstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"main": "background.js",
|
||||
"main": "./dist_electron/main/index.cjs",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.6",
|
||||
"@juggle/resize-observer": "^3.3.1",
|
||||
"ace-builds": "^1.4.13",
|
||||
"core-js": "^3.18.3",
|
||||
"cross-fetch": "^3.1.4",
|
||||
"electron-progressbar": "^2.0.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.9",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"ace-builds": "^1.23.4",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"electron-progressbar": "^2.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"install": "^0.13.0",
|
||||
"liquor-tree": "^0.2.70",
|
||||
"npm": "^8.1.1",
|
||||
"markdown-it": "^13.0.1",
|
||||
"npm": "^9.8.1",
|
||||
"v-tooltip": "2.1.3",
|
||||
"vue": "^2.6.14",
|
||||
"vue-class-component": "^7.2.6",
|
||||
"vue-js-modal": "^2.0.1",
|
||||
"vue-property-decorator": "^9.1.2"
|
||||
"vue": "^2.7.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ace": "0.0.47",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/file-saver": "^2.0.3",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.0-rc.1",
|
||||
"@vue/cli-plugin-e2e-cypress": "~5.0.0-rc.1",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0-rc.1",
|
||||
"@vue/cli-plugin-typescript": "~5.0.0-rc.1",
|
||||
"@vue/cli-plugin-unit-mocha": "~5.0.0-rc.1",
|
||||
"@vue/cli-service": "~5.0.0-rc.1",
|
||||
"@vue/eslint-config-airbnb": "^6.0.0",
|
||||
"@vue/eslint-config-typescript": "^9.1.0",
|
||||
"@vue/test-utils": "1.2.2",
|
||||
"chai": "^4.3.4",
|
||||
"cypress": "^8.3.0",
|
||||
"electron": "^15.3.0",
|
||||
"electron-builder": "^22.14.13",
|
||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@types/ace": "^0.0.48",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@vitejs/plugin-legacy": "^4.1.1",
|
||||
"@vitejs/plugin-vue2": "^2.2.0",
|
||||
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/test-utils": "^1.3.6",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"cypress": "^12.17.2",
|
||||
"electron": "^25.3.2",
|
||||
"electron-builder": "^24.6.3",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-log": "^4.4.1",
|
||||
"electron-updater": "^4.3.9",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"eslint-plugin-vuejs-accessibility": "^1.1.0",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"markdownlint-cli": "^0.29.0",
|
||||
"remark-cli": "^10.0.0",
|
||||
"electron-icon-builder": "^2.0.1",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-updater": "^6.1.4",
|
||||
"electron-vite": "^1.0.27",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-plugin-cypress": "^2.14.0",
|
||||
"eslint-plugin-vue": "^9.6.0",
|
||||
"eslint-plugin-vuejs-accessibility": "^1.2.0",
|
||||
"icon-gen": "^3.0.1",
|
||||
"jsdom": "^22.1.0",
|
||||
"markdownlint-cli": "^0.35.0",
|
||||
"postcss": "^8.4.28",
|
||||
"remark-cli": "^11.0.0",
|
||||
"remark-lint-no-dead-urls": "^1.1.0",
|
||||
"remark-preset-lint-consistent": "^5.1.0",
|
||||
"remark-validate-links": "^11.0.1",
|
||||
"sass": "^1.43.3",
|
||||
"sass-loader": "10.2.0",
|
||||
"ts-loader": "9.0.1",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.4.4",
|
||||
"vue-cli-plugin-electron-builder": "^2.1.1",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"yaml-lint": "^1.2.4"
|
||||
"remark-preset-lint-consistent": "^5.1.2",
|
||||
"remark-validate-links": "^12.1.1",
|
||||
"sass": "^1.64.1",
|
||||
"start-server-and-test": "^2.0.0",
|
||||
"svgexport": "^0.4.2",
|
||||
"terser": "^5.19.2",
|
||||
"tslib": "~2.4.0",
|
||||
"typescript": "~4.6.2",
|
||||
"vite": "^4.4.9",
|
||||
"vitest": "^0.34.2",
|
||||
"vue-tsc": "^1.8.8",
|
||||
"yaml-lint": "^1.7.0"
|
||||
},
|
||||
"//devDependencies": {
|
||||
"ts-loader": "Here as workaround for vue-cli-plugin-electron-builder using older webpack 4"
|
||||
"terser": "Used by @vitejs/plugin-legacy for minification",
|
||||
"typescript": [
|
||||
"Cannot upgrade to 5.X.X due to unmaintained @vue/cli-plugin-typescript, https://github.com/vuejs/vue-cli/issues/7401",
|
||||
"Cannot upgrade to > 4.6.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252"
|
||||
],
|
||||
"tslib": "Cannot upgrade to > 2.4.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252",
|
||||
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60",
|
||||
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60"
|
||||
},
|
||||
"homepage": "https://privacy.sexy",
|
||||
"repository": {
|
||||
|
||||
9
postcss.config.cjs
Normal file
@@ -0,0 +1,9 @@
|
||||
const autoprefixer = require('autoprefixer');
|
||||
|
||||
module.exports = () => {
|
||||
return {
|
||||
plugins: [
|
||||
autoprefixer(),
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
Before Width: | Height: | Size: 106 KiB |
BIN
public/icon.png
|
Before Width: | Height: | Size: 14 KiB |
10
scripts/check-desktop-runtime-errors/.eslintrc.cjs
Normal file
@@ -0,0 +1,10 @@
|
||||
require('@rushstack/eslint-patch/modern-module-resolution.js');
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
'import/extensions': ['error', 'always'],
|
||||
},
|
||||
};
|
||||
35
scripts/check-desktop-runtime-errors/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# check-desktop-runtime-errors
|
||||
|
||||
This script automates the processes of:
|
||||
|
||||
1) Building
|
||||
2) Packaging
|
||||
3) Installing
|
||||
4) Executing
|
||||
5) Verifying Electron distributions
|
||||
|
||||
It runs the application for a duration and detects runtime errors in the packaged application via:
|
||||
|
||||
- **Log verification**: Checking application logs for errors and validating successful application initialization.
|
||||
- **`stderr` monitoring**: Continuous listening to the `stderr` stream for unexpected errors.
|
||||
- **Window title inspection**: Checking for window titles that indicate crashes before logging becomes possible.
|
||||
|
||||
Upon error, the script captures a screenshot (if `--screenshot` is provided) and terminates.
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
node ./scripts/check-desktop-runtime-errors
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
- `--build`: Clears the electron distribution directory and forces a rebuild of the Electron app.
|
||||
- `--screenshot`: Takes a screenshot of the desktop environment after running the application.
|
||||
|
||||
This module provides utilities for building, executing, and validating Electron desktop apps.
|
||||
It can be used to automate checking for runtime errors during development.
|
||||
|
||||
## Configs
|
||||
|
||||
Configurations are defined in [`config.js`](./config.js).
|
||||
55
scripts/check-desktop-runtime-errors/app/app-logs.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { unlink, readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { log, die, LOG_LEVELS } from '../utils/log.js';
|
||||
import { exists } from '../utils/io.js';
|
||||
import { SUPPORTED_PLATFORMS, CURRENT_PLATFORM } from '../utils/platform.js';
|
||||
import { getAppName } from '../utils/npm.js';
|
||||
|
||||
export async function clearAppLogFile(projectDir) {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
const logPath = await determineLogPath(projectDir);
|
||||
if (!logPath || !await exists(logPath)) {
|
||||
log(`Skipping clearing logs, log file does not exist: ${logPath}.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await unlink(logPath);
|
||||
log(`Successfully cleared the log file at: ${logPath}.`);
|
||||
} catch (error) {
|
||||
die(`Failed to clear the log file at: ${logPath}. Reason: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function readAppLogFile(projectDir) {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
const logPath = await determineLogPath(projectDir);
|
||||
if (!logPath || !await exists(logPath)) {
|
||||
log(`No log file at: ${logPath}`, LOG_LEVELS.WARN);
|
||||
return undefined;
|
||||
}
|
||||
const logContent = await readLogFile(logPath);
|
||||
return logContent;
|
||||
}
|
||||
|
||||
async function determineLogPath(projectDir) {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
const appName = await getAppName(projectDir);
|
||||
if (!appName) {
|
||||
die('App name not found.');
|
||||
}
|
||||
const logFilePaths = {
|
||||
[SUPPORTED_PLATFORMS.MAC]: () => join(process.env.HOME, 'Library', 'Logs', appName, 'main.log'),
|
||||
[SUPPORTED_PLATFORMS.LINUX]: () => join(process.env.HOME, '.config', appName, 'logs', 'main.log'),
|
||||
[SUPPORTED_PLATFORMS.WINDOWS]: () => join(process.env.USERPROFILE, 'AppData', 'Roaming', appName, 'logs', 'main.log'),
|
||||
};
|
||||
const logFilePath = logFilePaths[CURRENT_PLATFORM]?.();
|
||||
if (!logFilePath) {
|
||||
log(`Cannot determine log path, unsupported OS: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
|
||||
}
|
||||
return logFilePath;
|
||||
}
|
||||
|
||||
async function readLogFile(logFilePath) {
|
||||
const content = await readFile(logFilePath, 'utf-8');
|
||||
return content?.trim().length > 0 ? content : undefined;
|
||||
}
|
||||
126
scripts/check-desktop-runtime-errors/app/check-for-errors.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import { splitTextIntoLines, indentText } from '../utils/text.js';
|
||||
import { die } from '../utils/log.js';
|
||||
import { readAppLogFile } from './app-logs.js';
|
||||
|
||||
const ELECTRON_CRASH_TITLE = 'Error'; // Used by electron for early crashes
|
||||
const LOG_ERROR_MARKER = '[error]'; // from electron-log
|
||||
const EXPECTED_LOG_MARKERS = [
|
||||
'[WINDOW_INIT]',
|
||||
'[PRELOAD_INIT]',
|
||||
'[APP_MOUNT_INIT]',
|
||||
];
|
||||
|
||||
export async function checkForErrors(stderr, windowTitles, projectDir) {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
const errors = await gatherErrors(stderr, windowTitles, projectDir);
|
||||
if (errors.length) {
|
||||
die(formatErrors(errors));
|
||||
}
|
||||
}
|
||||
|
||||
async function gatherErrors(stderr, windowTitles, projectDir) {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
const logContent = await readAppLogFile(projectDir);
|
||||
return [
|
||||
verifyStdErr(stderr),
|
||||
verifyApplicationLogsExist(logContent),
|
||||
...EXPECTED_LOG_MARKERS.map((marker) => verifyLogMarkerExistsInLogs(logContent, marker)),
|
||||
verifyWindowTitle(windowTitles),
|
||||
verifyErrorsInLogs(logContent),
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
function formatErrors(errors) {
|
||||
if (!errors || !errors.length) { throw new Error('missing errors'); }
|
||||
return [
|
||||
'Errors detected during execution:',
|
||||
...errors.map(
|
||||
(error) => formatError(error),
|
||||
),
|
||||
].join('\n---\n');
|
||||
}
|
||||
|
||||
function formatError(error) {
|
||||
if (!error) { throw new Error('missing error'); }
|
||||
if (!error.reason) { throw new Error(`missing reason, error (${typeof error}): ${JSON.stringify(error)}`); }
|
||||
let message = `Reason: ${indentText(error.reason, 1)}`;
|
||||
if (error.description) {
|
||||
message += `\nDescription:\n${indentText(error.description, 2)}`;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function verifyApplicationLogsExist(logContent) {
|
||||
if (!logContent || !logContent.length) {
|
||||
return describeError(
|
||||
'Missing application logs',
|
||||
'Application logs are empty not were not found.',
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function verifyLogMarkerExistsInLogs(logContent, marker) {
|
||||
if (!marker) {
|
||||
throw new Error('missing marker');
|
||||
}
|
||||
if (!logContent?.includes(marker)) {
|
||||
return describeError(
|
||||
'Incomplete application logs',
|
||||
`Missing identifier "${marker}" in application logs.`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function verifyWindowTitle(windowTitles) {
|
||||
const errorTitles = windowTitles.filter(
|
||||
(title) => title.toLowerCase().includes(ELECTRON_CRASH_TITLE),
|
||||
);
|
||||
if (errorTitles.length) {
|
||||
return describeError(
|
||||
'Unexpected window title',
|
||||
'One or more window titles suggest an error occurred in the application:'
|
||||
+ `\nError Titles: ${errorTitles.join(', ')}`
|
||||
+ `\nAll Titles: ${windowTitles.join(', ')}`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function verifyStdErr(stderrOutput) {
|
||||
if (stderrOutput && stderrOutput.length > 0) {
|
||||
return describeError(
|
||||
'Standard error stream (`stderr`) is not empty.',
|
||||
stderrOutput,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function verifyErrorsInLogs(logContent) {
|
||||
if (!logContent || !logContent.length) {
|
||||
return undefined;
|
||||
}
|
||||
const logLines = getNonEmptyLines(logContent)
|
||||
.filter((line) => line.includes(LOG_ERROR_MARKER));
|
||||
if (!logLines.length) {
|
||||
return undefined;
|
||||
}
|
||||
return describeError(
|
||||
'Application log file',
|
||||
logLines.join('\n'),
|
||||
);
|
||||
}
|
||||
|
||||
function describeError(reason, description) {
|
||||
return {
|
||||
reason,
|
||||
description: `${description}\n\nThis might indicate an early crash or significant runtime issue.`,
|
||||
};
|
||||
}
|
||||
|
||||
function getNonEmptyLines(text) {
|
||||
return splitTextIntoLines(text)
|
||||
.filter((line) => line?.trim().length > 0);
|
||||
}
|
||||
34
scripts/check-desktop-runtime-errors/app/extractors/linux.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { access, chmod } from 'fs/promises';
|
||||
import { constants } from 'fs';
|
||||
import { findSingleFileByExtension } from '../../utils/io.js';
|
||||
import { log } from '../../utils/log.js';
|
||||
|
||||
export async function prepareLinuxApp(desktopDistPath) {
|
||||
const { absolutePath: appFile } = await findSingleFileByExtension(
|
||||
'AppImage',
|
||||
desktopDistPath,
|
||||
);
|
||||
await makeExecutable(appFile);
|
||||
return {
|
||||
appExecutablePath: appFile,
|
||||
};
|
||||
}
|
||||
|
||||
async function makeExecutable(appFile) {
|
||||
if (!appFile) { throw new Error('missing file'); }
|
||||
if (await isExecutable(appFile)) {
|
||||
log('AppImage is already executable.');
|
||||
return;
|
||||
}
|
||||
log('Making it executable...');
|
||||
await chmod(appFile, 0o755);
|
||||
}
|
||||
|
||||
async function isExecutable(file) {
|
||||
try {
|
||||
await access(file, constants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
66
scripts/check-desktop-runtime-errors/app/extractors/macos.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { runCommand } from '../../utils/run-command.js';
|
||||
import { findSingleFileByExtension, exists } from '../../utils/io.js';
|
||||
import { log, die, LOG_LEVELS } from '../../utils/log.js';
|
||||
|
||||
export async function prepareMacOsApp(desktopDistPath) {
|
||||
const { absolutePath: dmgPath } = await findSingleFileByExtension('dmg', desktopDistPath);
|
||||
const { mountPath } = await mountDmg(dmgPath);
|
||||
const appPath = await findMacAppExecutablePath(mountPath);
|
||||
return {
|
||||
appExecutablePath: appPath,
|
||||
cleanup: async () => {
|
||||
log('Cleaning up resources...');
|
||||
await detachMount(mountPath);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function mountDmg(dmgFile) {
|
||||
const { stdout: hdiutilOutput, error } = await runCommand(`hdiutil attach '${dmgFile}'`);
|
||||
if (error) {
|
||||
die(`Failed to mount DMG file at ${dmgFile}.\n${error}`);
|
||||
}
|
||||
const mountPathMatch = hdiutilOutput.match(/\/Volumes\/[^\n]+/);
|
||||
const mountPath = mountPathMatch ? mountPathMatch[0] : null;
|
||||
return {
|
||||
mountPath,
|
||||
};
|
||||
}
|
||||
|
||||
async function findMacAppExecutablePath(mountPath) {
|
||||
const { stdout: findOutput, error } = await runCommand(
|
||||
`find '${mountPath}' -maxdepth 1 -type d -name "*.app"`,
|
||||
);
|
||||
if (error) {
|
||||
die(`Failed to find executable path at mount path ${mountPath}\n${error}`);
|
||||
}
|
||||
const appFolder = findOutput.trim();
|
||||
const appName = appFolder.split('/').pop().replace('.app', '');
|
||||
const appPath = `${appFolder}/Contents/MacOS/${appName}`;
|
||||
if (await exists(appPath)) {
|
||||
log(`Application is located at ${appPath}`);
|
||||
} else {
|
||||
die(`Application does not exist at ${appPath}`);
|
||||
}
|
||||
return appPath;
|
||||
}
|
||||
|
||||
async function detachMount(mountPath, retries = 5) {
|
||||
const { error } = await runCommand(`hdiutil detach '${mountPath}'`);
|
||||
if (error) {
|
||||
if (retries <= 0) {
|
||||
log(`Failed to detach mount after multiple attempts: ${mountPath}\n${error}`, LOG_LEVELS.WARN);
|
||||
return;
|
||||
}
|
||||
await sleep(500);
|
||||
await detachMount(mountPath, retries - 1);
|
||||
return;
|
||||
}
|
||||
log(`Successfully detached from ${mountPath}`);
|
||||
}
|
||||
|
||||
function sleep(milliseconds) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, milliseconds);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { mkdtemp, rmdir } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { findSingleFileByExtension, exists } from '../../utils/io.js';
|
||||
import { log, die } from '../../utils/log.js';
|
||||
import { runCommand } from '../../utils/run-command.js';
|
||||
|
||||
export async function prepareWindowsApp(desktopDistPath) {
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'win-nsis-installation-'));
|
||||
if (await exists(workdir)) {
|
||||
log(`Temporary directory ${workdir} already exists, cleaning up...`);
|
||||
await rmdir(workdir, { recursive: true });
|
||||
}
|
||||
const { appExecutablePath } = await installNsis(workdir, desktopDistPath);
|
||||
return {
|
||||
appExecutablePath,
|
||||
cleanup: async () => {
|
||||
log(`Cleaning up working directory ${workdir}...`);
|
||||
await rmdir(workdir, { recursive: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function installNsis(installationPath, desktopDistPath) {
|
||||
const { absolutePath: installerPath } = await findSingleFileByExtension('exe', desktopDistPath);
|
||||
|
||||
log(`Silently installing contents of ${installerPath} to ${installationPath}...`);
|
||||
const { error } = await runCommand(`"${installerPath}" /S /D=${installationPath}`);
|
||||
if (error) {
|
||||
die(`Failed to install.\n${error}`);
|
||||
}
|
||||
|
||||
const { absolutePath: appExecutablePath } = await findSingleFileByExtension('exe', installationPath);
|
||||
|
||||
return {
|
||||
appExecutablePath,
|
||||
};
|
||||
}
|
||||
164
scripts/check-desktop-runtime-errors/app/runner.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { log, LOG_LEVELS, die } from '../utils/log.js';
|
||||
import { captureScreen } from './system-capture/screen-capture.js';
|
||||
import { captureWindowTitles } from './system-capture/window-title-capture.js';
|
||||
|
||||
const TERMINATION_GRACE_PERIOD_IN_SECONDS = 60;
|
||||
const TERMINATION_CHECK_INTERVAL_IN_MS = 1000;
|
||||
const WINDOW_TITLE_CAPTURE_INTERVAL_IN_MS = 100;
|
||||
|
||||
export function runApplication(
|
||||
appFile,
|
||||
executionDurationInSeconds,
|
||||
enableScreenshot,
|
||||
screenshotPath,
|
||||
) {
|
||||
if (!appFile) {
|
||||
throw new Error('Missing app file');
|
||||
}
|
||||
|
||||
logDetails(appFile, executionDurationInSeconds);
|
||||
|
||||
const processDetails = {
|
||||
stderrData: '',
|
||||
stdoutData: '',
|
||||
explicitlyKilled: false,
|
||||
windowTitles: [],
|
||||
isCrashed: false,
|
||||
isDone: false,
|
||||
process: undefined,
|
||||
resolve: () => { /* NOOP */ },
|
||||
};
|
||||
|
||||
const process = spawn(appFile);
|
||||
processDetails.process = process;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
processDetails.resolve = resolve;
|
||||
handleTitleCapture(process.pid, processDetails);
|
||||
handleProcessEvents(
|
||||
processDetails,
|
||||
enableScreenshot,
|
||||
screenshotPath,
|
||||
executionDurationInSeconds,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function logDetails(appFile, executionDurationInSeconds) {
|
||||
log(
|
||||
[
|
||||
'Executing the app to check for errors...',
|
||||
`Maximum execution time: ${executionDurationInSeconds}`,
|
||||
`Application path: ${appFile}`,
|
||||
].join('\n\t'),
|
||||
);
|
||||
}
|
||||
|
||||
function handleTitleCapture(processId, processDetails) {
|
||||
const capture = async () => {
|
||||
const titles = await captureWindowTitles(processId);
|
||||
|
||||
(titles || []).forEach((title) => {
|
||||
if (!title || !title.length) {
|
||||
return;
|
||||
}
|
||||
if (!processDetails.windowTitles.includes(title)) {
|
||||
log(`New window title captured: ${title}`);
|
||||
processDetails.windowTitles.push(title);
|
||||
}
|
||||
});
|
||||
|
||||
if (!processDetails.isDone) {
|
||||
setTimeout(capture, WINDOW_TITLE_CAPTURE_INTERVAL_IN_MS);
|
||||
}
|
||||
};
|
||||
|
||||
capture();
|
||||
}
|
||||
|
||||
function handleProcessEvents(
|
||||
processDetails,
|
||||
enableScreenshot,
|
||||
screenshotPath,
|
||||
executionDurationInSeconds,
|
||||
) {
|
||||
const { process } = processDetails;
|
||||
process.stderr.on('data', (data) => {
|
||||
processDetails.stderrData += data.toString();
|
||||
});
|
||||
process.stdout.on('data', (data) => {
|
||||
processDetails.stdoutData += data.toString();
|
||||
});
|
||||
|
||||
process.on('error', (error) => {
|
||||
die(`An issue spawning the child process: ${error}`, LOG_LEVELS.ERROR);
|
||||
});
|
||||
|
||||
process.on('exit', async (code) => {
|
||||
await onProcessExit(code, processDetails, enableScreenshot, screenshotPath);
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
await onExecutionLimitReached(process, processDetails, enableScreenshot, screenshotPath);
|
||||
}, executionDurationInSeconds * 1000);
|
||||
}
|
||||
|
||||
async function onProcessExit(code, processDetails, enableScreenshot, screenshotPath) {
|
||||
log(`Application exited ${code === null || Number.isNaN(code) ? '.' : `with code ${code}`}`);
|
||||
|
||||
if (processDetails.explicitlyKilled) return;
|
||||
|
||||
processDetails.isCrashed = true;
|
||||
|
||||
if (enableScreenshot) {
|
||||
await captureScreen(screenshotPath);
|
||||
}
|
||||
|
||||
finishProcess(processDetails);
|
||||
}
|
||||
|
||||
async function onExecutionLimitReached(process, processDetails, enableScreenshot, screenshotPath) {
|
||||
if (enableScreenshot) {
|
||||
await captureScreen(screenshotPath);
|
||||
}
|
||||
|
||||
processDetails.explicitlyKilled = true;
|
||||
await terminateGracefully(process);
|
||||
finishProcess(processDetails);
|
||||
}
|
||||
|
||||
function finishProcess(processDetails) {
|
||||
processDetails.isDone = true;
|
||||
processDetails.resolve({
|
||||
stderr: processDetails.stderrData,
|
||||
stdout: processDetails.stdoutData,
|
||||
windowTitles: [...processDetails.windowTitles],
|
||||
isCrashed: processDetails.isCrashed,
|
||||
});
|
||||
}
|
||||
|
||||
async function terminateGracefully(process) {
|
||||
let elapsedSeconds = 0;
|
||||
log('Attempting to terminate the process gracefully...');
|
||||
process.kill('SIGTERM');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const checkInterval = setInterval(() => {
|
||||
elapsedSeconds += TERMINATION_CHECK_INTERVAL_IN_MS / 1000;
|
||||
|
||||
if (!process.killed) {
|
||||
if (elapsedSeconds >= TERMINATION_GRACE_PERIOD_IN_SECONDS) {
|
||||
process.kill('SIGKILL');
|
||||
log('Process did not terminate gracefully within the grace period. Forcing termination.', LOG_LEVELS.WARN);
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
}
|
||||
} else {
|
||||
log('Process terminated gracefully.');
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
}
|
||||
}, TERMINATION_CHECK_INTERVAL_IN_MS);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { unlink } from 'fs/promises';
|
||||
import { runCommand } from '../../utils/run-command.js';
|
||||
import { log, LOG_LEVELS } from '../../utils/log.js';
|
||||
import { CURRENT_PLATFORM, SUPPORTED_PLATFORMS } from '../../utils/platform.js';
|
||||
import { exists } from '../../utils/io.js';
|
||||
|
||||
export async function captureScreen(imagePath) {
|
||||
if (!imagePath) {
|
||||
throw new Error('Path for screenshot not provided');
|
||||
}
|
||||
|
||||
if (await exists(imagePath)) {
|
||||
log(`Screenshot file already exists at ${imagePath}. It will be overwritten.`, LOG_LEVELS.WARN);
|
||||
unlink(imagePath);
|
||||
}
|
||||
|
||||
const platformCommands = {
|
||||
[SUPPORTED_PLATFORMS.MAC]: `screencapture -x ${imagePath}`,
|
||||
[SUPPORTED_PLATFORMS.LINUX]: `import -window root ${imagePath}`,
|
||||
[SUPPORTED_PLATFORMS.WINDOWS]: `powershell -NoProfile -EncodedCommand ${encodeForPowershell(getScreenshotPowershellScript(imagePath))}`,
|
||||
};
|
||||
|
||||
const commandForPlatform = platformCommands[CURRENT_PLATFORM];
|
||||
|
||||
if (!commandForPlatform) {
|
||||
log(`Screenshot capture not supported on: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Capturing screenshot to ${imagePath} using command:\n\t> ${commandForPlatform}`);
|
||||
|
||||
const { error } = await runCommand(commandForPlatform);
|
||||
if (error) {
|
||||
log(`Failed to capture screenshot.\n${error}`, LOG_LEVELS.WARN);
|
||||
return;
|
||||
}
|
||||
log(`Captured screenshot to ${imagePath}.`);
|
||||
}
|
||||
|
||||
function getScreenshotPowershellScript(imagePath) {
|
||||
return `
|
||||
$ProgressPreference = 'SilentlyContinue' # Do not pollute stderr
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$screenBounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
||||
|
||||
$bmp = New-Object System.Drawing.Bitmap $screenBounds.Width, $screenBounds.Height
|
||||
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$graphics.CopyFromScreen([System.Drawing.Point]::Empty, [System.Drawing.Point]::Empty, $screenBounds.Size)
|
||||
|
||||
$bmp.Save('${imagePath}')
|
||||
$graphics.Dispose()
|
||||
$bmp.Dispose()
|
||||
`;
|
||||
}
|
||||
|
||||
function encodeForPowershell(script) {
|
||||
const buffer = Buffer.from(script, 'utf-16le');
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { runCommand } from '../../utils/run-command.js';
|
||||
import { log, LOG_LEVELS } from '../../utils/log.js';
|
||||
import { SUPPORTED_PLATFORMS, CURRENT_PLATFORM } from '../../utils/platform.js';
|
||||
|
||||
export async function captureWindowTitles(processId) {
|
||||
if (!processId) { throw new Error('Missing process ID.'); }
|
||||
|
||||
const captureFunction = windowTitleCaptureFunctions[CURRENT_PLATFORM];
|
||||
if (!captureFunction) {
|
||||
log(`Cannot capture window title, unsupported OS: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return captureFunction(processId);
|
||||
}
|
||||
|
||||
const windowTitleCaptureFunctions = {
|
||||
[SUPPORTED_PLATFORMS.MAC]: captureTitlesOnMac,
|
||||
[SUPPORTED_PLATFORMS.LINUX]: captureTitlesOnLinux,
|
||||
[SUPPORTED_PLATFORMS.WINDOWS]: captureTitlesOnWindows,
|
||||
};
|
||||
|
||||
async function captureTitlesOnWindows(processId) {
|
||||
if (!processId) { throw new Error('Missing process ID.'); }
|
||||
|
||||
const { stdout: tasklistOutput, error } = await runCommand(
|
||||
`tasklist /FI "PID eq ${processId}" /fo list /v`,
|
||||
);
|
||||
if (error) {
|
||||
log(`Failed to retrieve window title.\n${error}`, LOG_LEVELS.WARN);
|
||||
return [];
|
||||
}
|
||||
const match = tasklistOutput.match(/Window Title:\s*(.*)/);
|
||||
if (match && match[1]) {
|
||||
const title = match[1].trim();
|
||||
if (title === 'N/A') {
|
||||
return [];
|
||||
}
|
||||
return [title];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async function captureTitlesOnLinux(processId) {
|
||||
if (!processId) { throw new Error('Missing process ID.'); }
|
||||
|
||||
const { stdout: windowIdsOutput, error: windowIdError } = await runCommand(
|
||||
`xdotool search --pid '${processId}'`,
|
||||
);
|
||||
|
||||
if (windowIdError || !windowIdsOutput) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const windowIds = windowIdsOutput.trim().split('\n');
|
||||
|
||||
const titles = await Promise.all(windowIds.map(async (windowId) => {
|
||||
const { stdout: titleOutput, error: titleError } = await runCommand(
|
||||
`xprop -id ${windowId} | grep "WM_NAME(STRING)" | cut -d '=' -f 2 | sed 's/^[[:space:]]*"\\(.*\\)"[[:space:]]*$/\\1/'`,
|
||||
);
|
||||
if (titleError || !titleOutput) {
|
||||
return undefined;
|
||||
}
|
||||
return titleOutput.trim();
|
||||
}));
|
||||
|
||||
return titles.filter(Boolean);
|
||||
}
|
||||
|
||||
let hasAssistiveAccessOnMac = true;
|
||||
|
||||
async function captureTitlesOnMac(processId) {
|
||||
if (!processId) { throw new Error('Missing process ID.'); }
|
||||
if (!hasAssistiveAccessOnMac) {
|
||||
return [];
|
||||
}
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
try
|
||||
set targetProcess to first process whose unix id is ${processId}
|
||||
on error
|
||||
return
|
||||
end try
|
||||
tell targetProcess
|
||||
if (count of windows) > 0 then
|
||||
set window_name to name of front window
|
||||
return window_name
|
||||
end if
|
||||
end tell
|
||||
end tell
|
||||
`;
|
||||
const argument = script.trim()
|
||||
.split(/[\r\n]+/)
|
||||
.map((line) => `-e '${line.trim()}'`)
|
||||
.join(' ');
|
||||
|
||||
const { stdout: titleOutput, error } = await runCommand(`osascript ${argument}`);
|
||||
if (error) {
|
||||
let errorMessage = '';
|
||||
if (error.includes('-25211')) {
|
||||
errorMessage += 'Capturing window title requires assistive access. You do not have it.\n';
|
||||
hasAssistiveAccessOnMac = false;
|
||||
}
|
||||
errorMessage += error;
|
||||
log(errorMessage, LOG_LEVELS.WARN);
|
||||
return [];
|
||||
}
|
||||
const title = titleOutput?.trim();
|
||||
if (!title) {
|
||||
return [];
|
||||
}
|
||||
return [title];
|
||||
}
|
||||
20
scripts/check-desktop-runtime-errors/cli-args.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { log } from './utils/log.js';
|
||||
|
||||
const PROCESS_ARGUMENTS = process.argv.slice(2);
|
||||
|
||||
export const COMMAND_LINE_FLAGS = Object.freeze({
|
||||
FORCE_REBUILD: '--build',
|
||||
TAKE_SCREENSHOT: '--screenshot',
|
||||
});
|
||||
|
||||
export function logCurrentArgs() {
|
||||
if (!PROCESS_ARGUMENTS.length) {
|
||||
log('No additional arguments provided.');
|
||||
return;
|
||||
}
|
||||
log(`Arguments: ${PROCESS_ARGUMENTS.join(', ')}`);
|
||||
}
|
||||
|
||||
export function hasCommandLineFlag(flag) {
|
||||
return PROCESS_ARGUMENTS.includes(flag);
|
||||
}
|
||||
7
scripts/check-desktop-runtime-errors/config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { join } from 'path';
|
||||
|
||||
export const DESKTOP_BUILD_COMMAND = 'npm run electron:prebuild && npm run electron:build -- --publish never';
|
||||
export const PROJECT_DIR = process.cwd();
|
||||
export const DESKTOP_DIST_PATH = join(PROJECT_DIR, 'dist');
|
||||
export const APP_EXECUTION_DURATION_IN_SECONDS = 60; // Long enough for CI runners
|
||||
export const SCREENSHOT_PATH = join(PROJECT_DIR, 'screenshot.png');
|
||||
3
scripts/check-desktop-runtime-errors/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { main } from './main.js';
|
||||
|
||||
await main();
|
||||
68
scripts/check-desktop-runtime-errors/main.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { logCurrentArgs, COMMAND_LINE_FLAGS, hasCommandLineFlag } from './cli-args.js';
|
||||
import { log, die } from './utils/log.js';
|
||||
import { ensureNpmProjectDir, npmInstall, npmBuild } from './utils/npm.js';
|
||||
import { clearAppLogFile } from './app/app-logs.js';
|
||||
import { checkForErrors } from './app/check-for-errors.js';
|
||||
import { runApplication } from './app/runner.js';
|
||||
import { CURRENT_PLATFORM, SUPPORTED_PLATFORMS } from './utils/platform.js';
|
||||
import { prepareLinuxApp } from './app/extractors/linux.js';
|
||||
import { prepareWindowsApp } from './app/extractors/windows.js';
|
||||
import { prepareMacOsApp } from './app/extractors/macos.js';
|
||||
import {
|
||||
DESKTOP_BUILD_COMMAND,
|
||||
PROJECT_DIR,
|
||||
DESKTOP_DIST_PATH,
|
||||
APP_EXECUTION_DURATION_IN_SECONDS,
|
||||
SCREENSHOT_PATH,
|
||||
} from './config.js';
|
||||
|
||||
export async function main() {
|
||||
logCurrentArgs();
|
||||
await ensureNpmProjectDir(PROJECT_DIR);
|
||||
await npmInstall(PROJECT_DIR);
|
||||
await npmBuild(
|
||||
PROJECT_DIR,
|
||||
DESKTOP_BUILD_COMMAND,
|
||||
DESKTOP_DIST_PATH,
|
||||
hasCommandLineFlag(COMMAND_LINE_FLAGS.FORCE_REBUILD),
|
||||
);
|
||||
await clearAppLogFile(PROJECT_DIR);
|
||||
const {
|
||||
stderr, stdout, isCrashed, windowTitles,
|
||||
} = await extractAndRun();
|
||||
if (stdout) {
|
||||
log(`Output (stdout) from application execution:\n${stdout}`);
|
||||
}
|
||||
if (isCrashed) {
|
||||
die('The application encountered an error during its execution.');
|
||||
}
|
||||
await checkForErrors(stderr, windowTitles, PROJECT_DIR);
|
||||
log('🥳🎈 Success! Application completed without any runtime errors.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function extractAndRun() {
|
||||
const extractors = {
|
||||
[SUPPORTED_PLATFORMS.MAC]: () => prepareMacOsApp(DESKTOP_DIST_PATH),
|
||||
[SUPPORTED_PLATFORMS.LINUX]: () => prepareLinuxApp(DESKTOP_DIST_PATH),
|
||||
[SUPPORTED_PLATFORMS.WINDOWS]: () => prepareWindowsApp(DESKTOP_DIST_PATH),
|
||||
};
|
||||
const extractor = extractors[CURRENT_PLATFORM];
|
||||
if (!extractor) {
|
||||
throw new Error(`Platform not supported: ${CURRENT_PLATFORM}`);
|
||||
}
|
||||
const { appExecutablePath, cleanup } = await extractor();
|
||||
try {
|
||||
return await runApplication(
|
||||
appExecutablePath,
|
||||
APP_EXECUTION_DURATION_IN_SECONDS,
|
||||
hasCommandLineFlag(COMMAND_LINE_FLAGS.TAKE_SCREENSHOT),
|
||||
SCREENSHOT_PATH,
|
||||
);
|
||||
} finally {
|
||||
if (cleanup) {
|
||||
log('Cleaning up post-execution resources...');
|
||||
await cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
48
scripts/check-desktop-runtime-errors/utils/io.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { extname, join } from 'path';
|
||||
import { readdir, access } from 'fs/promises';
|
||||
import { constants } from 'fs';
|
||||
import { log, die, LOG_LEVELS } from './log.js';
|
||||
|
||||
export async function findSingleFileByExtension(extension, directory) {
|
||||
if (!directory) { throw new Error('Missing directory'); }
|
||||
if (!extension) { throw new Error('Missing file extension'); }
|
||||
|
||||
if (!await exists(directory)) {
|
||||
die(`Directory does not exist: ${directory}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const directoryContents = await readdir(directory);
|
||||
const foundFileNames = directoryContents.filter((file) => extname(file) === `.${extension}`);
|
||||
const withoutUninstaller = foundFileNames.filter(
|
||||
(fileName) => !fileName.toLowerCase().includes('uninstall'), // NSIS build has `Uninstall {app-name}.exe`
|
||||
);
|
||||
if (!withoutUninstaller.length) {
|
||||
die(`No ${extension} found in ${directory} directory.`);
|
||||
}
|
||||
if (withoutUninstaller.length > 1) {
|
||||
log(`Found multiple ${extension} files: ${withoutUninstaller.join(', ')}. Using first occurrence`, LOG_LEVELS.WARN);
|
||||
}
|
||||
return {
|
||||
absolutePath: join(directory, withoutUninstaller[0]),
|
||||
};
|
||||
}
|
||||
|
||||
export async function exists(path) {
|
||||
if (!path) { throw new Error('Missing path'); }
|
||||
try {
|
||||
await access(path, constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function isDirMissingOrEmpty(dir) {
|
||||
if (!dir) { throw new Error('Missing directory'); }
|
||||
if (!await exists(dir)) {
|
||||
return true;
|
||||
}
|
||||
const contents = await readdir(dir);
|
||||
return contents.length === 0;
|
||||
}
|
||||
39
scripts/check-desktop-runtime-errors/utils/log.js
Normal file
@@ -0,0 +1,39 @@
|
||||
export const LOG_LEVELS = Object.freeze({
|
||||
INFO: 'INFO',
|
||||
WARN: 'WARN',
|
||||
ERROR: 'ERROR',
|
||||
});
|
||||
|
||||
export function log(message, level = LOG_LEVELS.INFO) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const config = LOG_LEVEL_CONFIG[level] || LOG_LEVEL_CONFIG[LOG_LEVELS.INFO];
|
||||
const formattedMessage = `[${timestamp}][${config.color}${level}${COLOR_CODES.RESET}] ${message}`;
|
||||
config.method(formattedMessage);
|
||||
}
|
||||
|
||||
export function die(message) {
|
||||
log(message, LOG_LEVELS.ERROR);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const COLOR_CODES = {
|
||||
RESET: '\x1b[0m',
|
||||
LIGHT_RED: '\x1b[91m',
|
||||
YELLOW: '\x1b[33m',
|
||||
LIGHT_BLUE: '\x1b[94m',
|
||||
};
|
||||
|
||||
const LOG_LEVEL_CONFIG = {
|
||||
[LOG_LEVELS.INFO]: {
|
||||
color: COLOR_CODES.LIGHT_BLUE,
|
||||
method: console.log,
|
||||
},
|
||||
[LOG_LEVELS.WARN]: {
|
||||
color: COLOR_CODES.YELLOW,
|
||||
method: console.warn,
|
||||
},
|
||||
[LOG_LEVELS.ERROR]: {
|
||||
color: COLOR_CODES.LIGHT_RED,
|
||||
method: console.error,
|
||||
},
|
||||
};
|
||||
87
scripts/check-desktop-runtime-errors/utils/npm.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { join } from 'path';
|
||||
import { rmdir, readFile } from 'fs/promises';
|
||||
import { exists, isDirMissingOrEmpty } from './io.js';
|
||||
import { runCommand } from './run-command.js';
|
||||
import { LOG_LEVELS, die, log } from './log.js';
|
||||
|
||||
export async function ensureNpmProjectDir(projectDir) {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
if (!await exists(join(projectDir, 'package.json'))) {
|
||||
die(`'package.json' not found in project directory: ${projectDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function npmInstall(projectDir) {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
const npmModulesPath = join(projectDir, 'node_modules');
|
||||
if (!await isDirMissingOrEmpty(npmModulesPath)) {
|
||||
log(`Directory "${npmModulesPath}" exists and has content. Skipping \`npm install\`.`);
|
||||
return;
|
||||
}
|
||||
log('Starting dependency installation...');
|
||||
const { error } = await runCommand('npm install --loglevel=error', {
|
||||
stdio: 'inherit',
|
||||
cwd: projectDir,
|
||||
});
|
||||
if (error) {
|
||||
die(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function npmBuild(projectDir, buildCommand, distDir, forceRebuild) {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
if (!buildCommand) { throw new Error('missing build command'); }
|
||||
if (!distDir) { throw new Error('missing distribution directory'); }
|
||||
|
||||
const isMissingBuild = await isDirMissingOrEmpty(distDir);
|
||||
|
||||
if (!isMissingBuild && !forceRebuild) {
|
||||
log(`Directory "${distDir}" exists and has content. Skipping build: '${buildCommand}'.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (forceRebuild) {
|
||||
log(`Removing directory "${distDir}" for a clean build (triggered by --build flag).`);
|
||||
await rmdir(distDir, { recursive: true });
|
||||
}
|
||||
|
||||
log('Starting project build...');
|
||||
const { error } = await runCommand(buildCommand, {
|
||||
stdio: 'inherit',
|
||||
cwd: projectDir,
|
||||
});
|
||||
if (error) {
|
||||
log(error, LOG_LEVELS.WARN); // Cannot disable Vue CLI errors, stderr contains false-positives.
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAppName(projectDir) {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
const packageData = await readPackageJsonContents(projectDir);
|
||||
try {
|
||||
const packageJson = JSON.parse(packageData);
|
||||
if (!packageJson.name) {
|
||||
die(`The 'package.json' file doesn't specify a name: ${packageData}`);
|
||||
}
|
||||
return packageJson.name;
|
||||
} catch (error) {
|
||||
die(`Unable to parse 'package.json'. Error: ${error}\nContent: ${packageData}`, LOG_LEVELS.ERROR);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function readPackageJsonContents(projectDir) {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
const packagePath = join(projectDir, 'package.json');
|
||||
if (!await exists(packagePath)) {
|
||||
die(`'package.json' file not found at ${packagePath}`);
|
||||
}
|
||||
try {
|
||||
const packageData = await readFile(packagePath, 'utf8');
|
||||
return packageData;
|
||||
} catch (error) {
|
||||
log(`Error reading 'package.json' from ${packagePath}.`, LOG_LEVELS.ERROR);
|
||||
die(`Error detail: ${error}`, LOG_LEVELS.ERROR);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
9
scripts/check-desktop-runtime-errors/utils/platform.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { platform } from 'os';
|
||||
|
||||
export const SUPPORTED_PLATFORMS = {
|
||||
MAC: 'darwin',
|
||||
LINUX: 'linux',
|
||||
WINDOWS: 'win32',
|
||||
};
|
||||
|
||||
export const CURRENT_PLATFORM = platform();
|
||||
44
scripts/check-desktop-runtime-errors/utils/run-command.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { exec } from 'child_process';
|
||||
import { indentText } from './text.js';
|
||||
|
||||
const TIMEOUT_IN_SECONDS = 180;
|
||||
const MAX_OUTPUT_BUFFER_SIZE = 1024 * 1024; // 1 MB
|
||||
|
||||
export function runCommand(commandString, options) {
|
||||
return new Promise((resolve) => {
|
||||
options = {
|
||||
cwd: process.cwd(),
|
||||
timeout: TIMEOUT_IN_SECONDS * 1000,
|
||||
maxBuffer: MAX_OUTPUT_BUFFER_SIZE * 2,
|
||||
...options,
|
||||
};
|
||||
|
||||
exec(commandString, options, (error, stdout, stderr) => {
|
||||
let errorText;
|
||||
if (error || stderr?.length > 0) {
|
||||
errorText = formatError(commandString, error, stdout, stderr);
|
||||
}
|
||||
resolve({
|
||||
stdout,
|
||||
error: errorText,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatError(commandString, error, stdout, stderr) {
|
||||
const errorParts = [
|
||||
'Error while running command.',
|
||||
`Command:\n${indentText(commandString, 1)}`,
|
||||
];
|
||||
if (error?.toString().trim()) {
|
||||
errorParts.push(`Error:\n${indentText(error.toString(), 1)}`);
|
||||
}
|
||||
if (stderr?.toString().trim()) {
|
||||
errorParts.push(`stderr:\n${indentText(stderr, 1)}`);
|
||||
}
|
||||
if (stdout?.toString().trim()) {
|
||||
errorParts.push(`stdout:\n${indentText(stdout, 1)}`);
|
||||
}
|
||||
return errorParts.join('\n---\n');
|
||||
}
|
||||
19
scripts/check-desktop-runtime-errors/utils/text.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export function indentText(text, indentLevel = 1) {
|
||||
validateText(text);
|
||||
const indentation = '\t'.repeat(indentLevel);
|
||||
return splitTextIntoLines(text)
|
||||
.map((line) => (line ? `${indentation}${line}` : line))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function splitTextIntoLines(text) {
|
||||
validateText(text);
|
||||
return text
|
||||
.split(/[\r\n]+/);
|
||||
}
|
||||
|
||||
function validateText(text) {
|
||||
if (typeof text !== 'string') {
|
||||
throw new Error(`text is not a string. It is: ${typeof text}\n${text}`);
|
||||
}
|
||||
}
|
||||
74
scripts/configure-vscode.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script ensures that the '.vscode/settings.json' file exists and is configured correctly for ESLint validation on Vue and JavaScript files.
|
||||
# See https://web.archive.org/web/20230801024405/https://eslint.vuejs.org/user-guide/#visual-studio-code
|
||||
|
||||
declare -r SETTINGS_FILE='.vscode/settings.json'
|
||||
declare -ra CONFIG_KEYS=('vue' 'javascript' 'typescript')
|
||||
declare -r TEMP_FILE="tmp.$$.json"
|
||||
|
||||
main() {
|
||||
ensure_vscode_directory_exists
|
||||
create_or_update_settings
|
||||
}
|
||||
|
||||
ensure_vscode_directory_exists() {
|
||||
local dir_name
|
||||
dir_name=$(dirname "${SETTINGS_FILE}")
|
||||
if [[ ! -d ${dir_name} ]]; then
|
||||
mkdir -p "${dir_name}"
|
||||
echo "🎉 Created directory: ${dir_name}"
|
||||
fi
|
||||
}
|
||||
|
||||
create_or_update_settings() {
|
||||
if [[ ! -f ${SETTINGS_FILE} ]]; then
|
||||
create_default_settings
|
||||
else
|
||||
add_or_update_eslint_validate
|
||||
fi
|
||||
}
|
||||
|
||||
create_default_settings() {
|
||||
local default_validate
|
||||
default_validate=$(printf '%s' "${CONFIG_KEYS[*]}" | jq -R -s -c -M 'split(" ")')
|
||||
echo "{ \"eslint.validate\": ${default_validate} }" | jq '.' > "${SETTINGS_FILE}"
|
||||
echo "🎉 Created default ${SETTINGS_FILE}"
|
||||
}
|
||||
|
||||
add_or_update_eslint_validate() {
|
||||
if ! jq -e '.["eslint.validate"]' "${SETTINGS_FILE}" >/dev/null; then
|
||||
add_default_eslint_validate
|
||||
else
|
||||
update_eslint_validate
|
||||
fi
|
||||
}
|
||||
|
||||
add_default_eslint_validate() {
|
||||
jq --argjson keys "$(printf '%s' "${CONFIG_KEYS[*]}" \
|
||||
| jq -R -s -c 'split(" ")')" '. += {"eslint.validate": $keys}' "${SETTINGS_FILE}" > "${TEMP_FILE}"
|
||||
replace_and_confirm
|
||||
echo "🎉 Added default 'eslint.validate' to ${SETTINGS_FILE}"
|
||||
}
|
||||
|
||||
update_eslint_validate() {
|
||||
local existing_keys
|
||||
existing_keys=$(jq '.["eslint.validate"]' "${SETTINGS_FILE}")
|
||||
for key in "${CONFIG_KEYS[@]}"; do
|
||||
if ! echo "${existing_keys}" | jq 'index("'"${key}"'")' >/dev/null; then
|
||||
jq '.["eslint.validate"] += ["'"${key}"'"]' "${SETTINGS_FILE}" > "${TEMP_FILE}"
|
||||
mv "${TEMP_FILE}" "${SETTINGS_FILE}"
|
||||
echo "🎉 Updated 'eslint.validate' in ${SETTINGS_FILE} for ${key}"
|
||||
else
|
||||
echo "⏩️ No updated needed for ${key} ${SETTINGS_FILE}."
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
replace_and_confirm() {
|
||||
if mv "${TEMP_FILE}" "${SETTINGS_FILE}"; then
|
||||
echo "🎉 Updated ${SETTINGS_FILE}"
|
||||
fi
|
||||
}
|
||||
|
||||
main
|
||||
95
scripts/fresh-npm-install.sh
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Description:
|
||||
# This script ensures npm is available, removes existing node modules, optionally
|
||||
# removes package-lock.json (when -n flag is used), installs dependencies and runs unit tests.
|
||||
# Usage:
|
||||
# ./fresh-npm-install.sh # Regular execution
|
||||
# ./fresh-npm-install.sh -n # Non-deterministic mode (removes package-lock.json)
|
||||
|
||||
declare NON_DETERMINISTIC_FLAG=0
|
||||
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
ensure_npm_is_available
|
||||
ensure_npm_root
|
||||
remove_existing_modules
|
||||
if [[ $NON_DETERMINISTIC_FLAG -eq 1 ]]; then
|
||||
remove_package_lock_json
|
||||
fi
|
||||
install_dependencies
|
||||
run_unit_tests
|
||||
}
|
||||
|
||||
ensure_npm_is_available() {
|
||||
if ! command -v npm &> /dev/null; then
|
||||
log::fatal 'npm could not be found, please install it first.'
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_npm_root() {
|
||||
if [ ! -f package.json ]; then
|
||||
log::fatal 'Current directory is not a npm root. Please run the script in a npm root directory.'
|
||||
fi
|
||||
}
|
||||
|
||||
remove_existing_modules() {
|
||||
if [ -d ./node_modules ]; then
|
||||
log::info 'Removing existing node modules...'
|
||||
if ! rm -rf ./node_modules; then
|
||||
log::fatal 'Could not remove existing node modules.'
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
log::info 'Installing dependencies...'
|
||||
if ! npm install; then
|
||||
log::fatal 'Failed to install dependencies.'
|
||||
fi
|
||||
}
|
||||
|
||||
remove_package_lock_json() {
|
||||
if [ -f ./package-lock.json ]; then
|
||||
log::info 'Removing package-lock.json...'
|
||||
if ! rm -rf ./package-lock.json; then
|
||||
log::fatal 'Could not remove package-lock.json.'
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
run_unit_tests() {
|
||||
log::info 'Running unit tests...'
|
||||
if ! npm run test:unit; then
|
||||
pwd
|
||||
log::fatal 'Failed to run unit tests.'
|
||||
fi
|
||||
}
|
||||
|
||||
log::info() {
|
||||
local -r message="$1"
|
||||
echo "📣 ${message}"
|
||||
}
|
||||
|
||||
log::fatal() {
|
||||
local -r message="$1"
|
||||
echo "❌ ${message}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while getopts "n" opt; do
|
||||
case ${opt} in
|
||||
n)
|
||||
NON_DETERMINISTIC_FLAG=1
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: $OPTARG" 1>&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
main "$1"
|
||||
127
scripts/logo-update.js
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
import { resolve, join } from 'path';
|
||||
import { rm, mkdtemp, stat } from 'fs/promises';
|
||||
import { spawn } from 'child_process';
|
||||
import { URL, fileURLToPath } from 'url';
|
||||
|
||||
class Paths {
|
||||
constructor(selfDirectory) {
|
||||
const projectRoot = resolve(selfDirectory, '../');
|
||||
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
||||
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
||||
this.electronBuildDirectory = join(projectRoot, 'build');
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Source image: ${this.sourceImage}\n`
|
||||
+ `Public directory: ${this.publicDirectory}\n`
|
||||
+ `Electron build directory: ${this.electronBuildDirectory}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const paths = new Paths(getCurrentScriptDirectory());
|
||||
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
||||
await updateDesktopLauncherAndTrayIcon(paths.sourceImage, paths.publicDirectory);
|
||||
await updateWebFavicon(paths.sourceImage, paths.publicDirectory);
|
||||
await updateDesktopIcons(paths.sourceImage, paths.electronBuildDirectory);
|
||||
console.log('🎉 (Re)created icons successfully.');
|
||||
}
|
||||
|
||||
async function updateDesktopLauncherAndTrayIcon(sourceImage, publicFolder) {
|
||||
await ensureFileExists(sourceImage);
|
||||
await ensureFolderExists(publicFolder);
|
||||
const electronTrayIconFile = join(publicFolder, 'icon.png');
|
||||
console.log(`Updating desktop launcher and tray icon at ${electronTrayIconFile}.`);
|
||||
await runCommand(
|
||||
'npx',
|
||||
'svgexport',
|
||||
sourceImage,
|
||||
electronTrayIconFile,
|
||||
);
|
||||
}
|
||||
|
||||
async function updateWebFavicon(sourceImage, faviconFolder) {
|
||||
console.log('Updating favicon');
|
||||
await ensureFileExists(sourceImage);
|
||||
await ensureFolderExists(faviconFolder);
|
||||
await runCommand(
|
||||
'npx',
|
||||
'icon-gen',
|
||||
`--input ${sourceImage}`,
|
||||
`--output ${faviconFolder}`,
|
||||
'--ico',
|
||||
'--ico-name \'favicon\'',
|
||||
'--report',
|
||||
);
|
||||
}
|
||||
|
||||
async function updateDesktopIcons(sourceImage, electronIconsDir) {
|
||||
await ensureFileExists(sourceImage);
|
||||
await ensureFolderExists(electronIconsDir);
|
||||
const temporaryDir = await mkdtemp('icon-');
|
||||
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
||||
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by `icon-builder`
|
||||
await runCommand(
|
||||
'npx',
|
||||
'svgexport',
|
||||
sourceImage,
|
||||
temporaryPngFile,
|
||||
'1024:1024',
|
||||
);
|
||||
console.log(`Creating electron icons to ${electronIconsDir}.`);
|
||||
await runCommand(
|
||||
'npx',
|
||||
'electron-icon-builder',
|
||||
`--input="${temporaryPngFile}"`,
|
||||
`--output="${electronIconsDir}"`,
|
||||
'--flatten',
|
||||
);
|
||||
console.log('Cleaning up temporary directory.');
|
||||
await rm(temporaryDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function ensureFileExists(filePath) {
|
||||
const path = await stat(filePath);
|
||||
if (!path.isFile()) {
|
||||
throw new Error(`Not a file: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureFolderExists(folderPath) {
|
||||
const path = await stat(folderPath);
|
||||
if (!path.isDirectory()) {
|
||||
throw new Error(`Not a directory: ${folderPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function runCommand(...args) {
|
||||
const command = args.join(' ');
|
||||
console.log(`Running command: ${command}`);
|
||||
await new Promise((resolve, reject) => {
|
||||
const process = spawn(command, { shell: true });
|
||||
process.stdout.on('data', (stdout) => {
|
||||
console.log(stdout.toString());
|
||||
});
|
||||
process.stderr.on('data', (stderr) => {
|
||||
console.error(stderr.toString());
|
||||
});
|
||||
process.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
process.on('close', (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
reject(new Error(`Process exited with non-zero exit code: ${exitCode}`));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
process.stdin.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentScriptDirectory() {
|
||||
return fileURLToPath(new URL('.', import.meta.url));
|
||||
}
|
||||
|
||||
await main();
|
||||
15
src/TypeHelpers.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type Constructible<T, TArgs extends unknown[] = never> = {
|
||||
prototype: T;
|
||||
apply: (this: unknown, args: TArgs) => void;
|
||||
};
|
||||
|
||||
export type PropertyKeys<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? never : K;
|
||||
}[keyof T];
|
||||
|
||||
export type ConstructorArguments<T> =
|
||||
T extends new (...args: infer U) => unknown ? U : never;
|
||||
|
||||
export type FunctionKeys<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never;
|
||||
}[keyof T];
|
||||
50
src/application/Common/CustomError.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Provides a unified and resilient way to extend errors across platforms.
|
||||
|
||||
Rationale:
|
||||
- Babel:
|
||||
> "Built-in classes cannot be properly subclassed due to limitations in ES5"
|
||||
> https://web.archive.org/web/20230810014108/https://babeljs.io/docs/caveats#classes
|
||||
- TypeScript:
|
||||
> "Extending built-ins like Error, Array, and Map may no longer work"
|
||||
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||
*/
|
||||
export abstract class CustomError extends Error {
|
||||
constructor(message?: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
|
||||
fixPrototype(this, new.target.prototype);
|
||||
ensureStackTrace(this);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
export const Environment = {
|
||||
getSetPrototypeOf: () => Object.setPrototypeOf,
|
||||
getCaptureStackTrace: () => Error.captureStackTrace,
|
||||
};
|
||||
|
||||
function fixPrototype(target: Error, prototype: CustomError) {
|
||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
||||
const setPrototypeOf = Environment.getSetPrototypeOf();
|
||||
if (!functionExists(setPrototypeOf)) {
|
||||
return;
|
||||
}
|
||||
setPrototypeOf(target, prototype);
|
||||
}
|
||||
|
||||
function ensureStackTrace(target: Error) {
|
||||
const captureStackTrace = Environment.getCaptureStackTrace();
|
||||
if (!functionExists(captureStackTrace)) {
|
||||
// captureStackTrace is only available on V8, if it's not available
|
||||
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
|
||||
return;
|
||||
}
|
||||
captureStackTrace(target, target.constructor);
|
||||
}
|
||||
|
||||
function functionExists(func: unknown): boolean {
|
||||
// Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness
|
||||
return typeof func === 'function';
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { Environment } from '../Environment/Environment';
|
||||
import { IEnvironment } from '../Environment/IEnvironment';
|
||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
||||
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
|
||||
import { IApplicationFactory } from '../IApplicationFactory';
|
||||
import { ApplicationFactory } from '../ApplicationFactory';
|
||||
import { ApplicationContext } from './ApplicationContext';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
|
||||
const NewLine = '\n';
|
||||
const TotalFunctionSeparatorChars = 58;
|
||||
|
||||
export abstract class CodeBuilder implements ICodeBuilder {
|
||||
@@ -59,10 +58,12 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.lines.join(NewLine);
|
||||
return this.lines.join(this.getNewLineTerminator());
|
||||
}
|
||||
|
||||
protected abstract getCommentDelimiter(): string;
|
||||
|
||||
protected abstract writeStandardOut(text: string): string;
|
||||
|
||||
protected abstract getNewLineTerminator(): string;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ export class BatchBuilder extends CodeBuilder {
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo ${escapeForEcho(text)}`;
|
||||
}
|
||||
|
||||
protected getNewLineTerminator(): string {
|
||||
return '\r\n';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeForEcho(text: string) {
|
||||
|
||||
@@ -8,6 +8,10 @@ export class ShellBuilder extends CodeBuilder {
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo '${escapeForEcho(text)}'`;
|
||||
}
|
||||
|
||||
protected getNewLineTerminator(): string {
|
||||
return '\n';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeForEcho(text: string) {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum FilterActionType {
|
||||
Apply,
|
||||
Clear,
|
||||
}
|
||||
37
src/application/Context/State/Filter/Event/FilterChange.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||
import { FilterActionType } from './FilterActionType';
|
||||
import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from './IFilterChangeDetails';
|
||||
|
||||
export class FilterChange implements IFilterChangeDetails {
|
||||
public static forApply(filter: IFilterResult) {
|
||||
if (!filter) {
|
||||
throw new Error('missing filter');
|
||||
}
|
||||
return new FilterChange(FilterActionType.Apply, filter);
|
||||
}
|
||||
|
||||
public static forClear() {
|
||||
return new FilterChange(FilterActionType.Clear);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
public readonly actionType: FilterActionType,
|
||||
public readonly filter?: IFilterResult,
|
||||
) { }
|
||||
|
||||
public visit(visitor: IFilterChangeDetailsVisitor): void {
|
||||
if (!visitor) {
|
||||
throw new Error('missing visitor');
|
||||
}
|
||||
switch (this.actionType) {
|
||||
case FilterActionType.Apply:
|
||||
visitor.onApply(this.filter);
|
||||
break;
|
||||
case FilterActionType.Clear:
|
||||
visitor.onClear();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown action type: ${this.actionType}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||
import { FilterActionType } from './FilterActionType';
|
||||
|
||||
export interface IFilterChangeDetails {
|
||||
readonly actionType: FilterActionType;
|
||||
readonly filter?: IFilterResult;
|
||||
|
||||
visit(visitor: IFilterChangeDetailsVisitor): void;
|
||||
}
|
||||
|
||||
export interface IFilterChangeDetailsVisitor {
|
||||
onClear(): void;
|
||||
onApply(filter: IFilterResult): void;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
import { IFilterChangeDetails } from './Event/IFilterChangeDetails';
|
||||
|
||||
export interface IReadOnlyUserFilter {
|
||||
readonly currentFilter: IFilterResult | undefined;
|
||||
readonly filtered: IEventSource<IFilterResult>;
|
||||
readonly filterRemoved: IEventSource<void>;
|
||||
readonly filterChanged: IEventSource<IFilterChangeDetails>;
|
||||
}
|
||||
|
||||
export interface IUserFilter extends IReadOnlyUserFilter {
|
||||
setFilter(filter: string): void;
|
||||
removeFilter(): void;
|
||||
applyFilter(filter: string): void;
|
||||
clearFilter(): void;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { FilterResult } from './FilterResult';
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
import { IUserFilter } from './IUserFilter';
|
||||
import { IFilterChangeDetails } from './Event/IFilterChangeDetails';
|
||||
import { FilterChange } from './Event/FilterChange';
|
||||
|
||||
export class UserFilter implements IUserFilter {
|
||||
public readonly filtered = new EventSource<IFilterResult>();
|
||||
|
||||
public readonly filterRemoved = new EventSource<void>();
|
||||
public readonly filterChanged = new EventSource<IFilterChangeDetails>();
|
||||
|
||||
public currentFilter: IFilterResult | undefined;
|
||||
|
||||
@@ -16,9 +16,9 @@ export class UserFilter implements IUserFilter {
|
||||
|
||||
}
|
||||
|
||||
public setFilter(filter: string): void {
|
||||
public applyFilter(filter: string): void {
|
||||
if (!filter) {
|
||||
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter');
|
||||
throw new Error('Filter must be defined and not empty. Use clearFilter() to remove the filter');
|
||||
}
|
||||
const filterLowercase = filter.toLocaleLowerCase();
|
||||
const filteredScripts = this.collection.getAllScripts().filter(
|
||||
@@ -33,12 +33,12 @@ export class UserFilter implements IUserFilter {
|
||||
filter,
|
||||
);
|
||||
this.currentFilter = matches;
|
||||
this.filtered.notify(matches);
|
||||
this.filterChanged.notify(FilterChange.forApply(this.currentFilter));
|
||||
}
|
||||
|
||||
public removeFilter(): void {
|
||||
public clearFilter(): void {
|
||||
this.currentFilter = undefined;
|
||||
this.filterRemoved.notify();
|
||||
this.filterChanged.notify(FilterChange.forClear());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||
import { IEnvironment } from './IEnvironment';
|
||||
|
||||
export interface IEnvironmentVariables {
|
||||
readonly window: Window & typeof globalThis;
|
||||
readonly process: NodeJS.Process;
|
||||
readonly navigator: Navigator;
|
||||
}
|
||||
|
||||
export class Environment implements IEnvironment {
|
||||
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
||||
window,
|
||||
process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
|
||||
navigator,
|
||||
});
|
||||
|
||||
public readonly isDesktop: boolean;
|
||||
|
||||
public readonly os: OperatingSystem;
|
||||
|
||||
protected constructor(
|
||||
variables: IEnvironmentVariables,
|
||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||
) {
|
||||
if (!variables) {
|
||||
throw new Error('variables is null or empty');
|
||||
}
|
||||
this.isDesktop = isDesktop(variables);
|
||||
if (this.isDesktop) {
|
||||
this.os = getDesktopOsType(getProcessPlatform(variables));
|
||||
} else {
|
||||
const userAgent = getUserAgent(variables);
|
||||
this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getUserAgent(variables: IEnvironmentVariables): string {
|
||||
if (!variables.window || !variables.window.navigator) {
|
||||
return undefined;
|
||||
}
|
||||
return variables.window.navigator.userAgent;
|
||||
}
|
||||
|
||||
function getProcessPlatform(variables: IEnvironmentVariables): string {
|
||||
if (!variables.process || !variables.process.platform) {
|
||||
return undefined;
|
||||
}
|
||||
return variables.process.platform;
|
||||
}
|
||||
|
||||
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
|
||||
// https://nodejs.org/api/process.html#process_process_platform
|
||||
switch (processPlatform) {
|
||||
case 'darwin':
|
||||
return OperatingSystem.macOS;
|
||||
case 'win32':
|
||||
return OperatingSystem.Windows;
|
||||
case 'linux':
|
||||
return OperatingSystem.Linux;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isDesktop(variables: IEnvironmentVariables): boolean {
|
||||
// More: https://github.com/electron/electron/issues/2288
|
||||
// Renderer process
|
||||
if (variables.window
|
||||
&& variables.window.process
|
||||
&& variables.window.process.type === 'renderer') {
|
||||
return true;
|
||||
}
|
||||
// Main process
|
||||
if (variables.process
|
||||
&& variables.process.versions
|
||||
&& Boolean(variables.process.versions.electron)) {
|
||||
return true;
|
||||
}
|
||||
// Detect the user agent when the `nodeIntegration` option is set to true
|
||||
if (variables.navigator
|
||||
&& variables.navigator.userAgent
|
||||
&& variables.navigator.userAgent.includes('Electron')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IEnvironment {
|
||||
readonly isDesktop: boolean;
|
||||
readonly os: OperatingSystem;
|
||||
}
|
||||
@@ -4,18 +4,22 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import WindowsData from '@/application/collections/windows.yaml';
|
||||
import MacOsData from '@/application/collections/macos.yaml';
|
||||
import LinuxData from '@/application/collections/linux.yaml';
|
||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||
import { Application } from '@/domain/Application';
|
||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
|
||||
export function parseApplication(
|
||||
parser = CategoryCollectionParser,
|
||||
processEnv: NodeJS.ProcessEnv = process.env,
|
||||
categoryParser = parseCategoryCollection,
|
||||
informationParser = parseProjectInformation,
|
||||
metadata: IAppMetadata = AppMetadataFactory.Current.instance,
|
||||
collectionsData = PreParsedCollections,
|
||||
): IApplication {
|
||||
validateCollectionsData(collectionsData);
|
||||
const information = parseProjectInformation(processEnv);
|
||||
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||
const information = informationParser(metadata);
|
||||
const collections = collectionsData.map((collection) => categoryParser(collection, information));
|
||||
const app = new Application(information, collections);
|
||||
return app;
|
||||
}
|
||||
@@ -23,16 +27,12 @@ export function parseApplication(
|
||||
export type CategoryCollectionParserType
|
||||
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
||||
|
||||
const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
|
||||
return parseCategoryCollection(file, info);
|
||||
};
|
||||
|
||||
const PreParsedCollections: readonly CollectionData [] = [
|
||||
WindowsData, MacOsData,
|
||||
WindowsData, MacOsData, LinuxData,
|
||||
];
|
||||
|
||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||
if (!collections || !collections.length) {
|
||||
if (!collections?.length) {
|
||||
throw new Error('missing collections');
|
||||
}
|
||||
if (collections.some((collection) => !collection)) {
|
||||
|
||||
@@ -3,7 +3,9 @@ import type {
|
||||
} from '@/application/collections/';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { parseDocUrls } from './DocumentationParser';
|
||||
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||
import { parseDocs } from './DocumentationParser';
|
||||
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||
import { parseScript } from './Script/ScriptParser';
|
||||
|
||||
@@ -12,35 +14,67 @@ let categoryIdCounter = 0;
|
||||
export function parseCategory(
|
||||
category: CategoryData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
factory: CategoryFactoryType = CategoryFactory,
|
||||
): Category {
|
||||
if (!context) { throw new Error('missing context'); }
|
||||
ensureValid(category);
|
||||
return parseCategoryRecursively({
|
||||
categoryData: category,
|
||||
context,
|
||||
factory,
|
||||
});
|
||||
}
|
||||
|
||||
interface ICategoryParseContext {
|
||||
readonly categoryData: CategoryData,
|
||||
readonly context: ICategoryCollectionParseContext,
|
||||
readonly factory: CategoryFactoryType,
|
||||
readonly parentCategory?: CategoryData,
|
||||
}
|
||||
// eslint-disable-next-line consistent-return
|
||||
function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
||||
ensureValidCategory(context.categoryData, context.parentCategory);
|
||||
const children: ICategoryChildren = {
|
||||
subCategories: new Array<Category>(),
|
||||
subScripts: new Array<Script>(),
|
||||
};
|
||||
for (const data of category.children) {
|
||||
parseCategoryChild(data, children, category, context);
|
||||
for (const data of context.categoryData.children) {
|
||||
parseNode({
|
||||
nodeData: data,
|
||||
children,
|
||||
parent: context.categoryData,
|
||||
factory: context.factory,
|
||||
context: context.context,
|
||||
});
|
||||
}
|
||||
try {
|
||||
return context.factory(
|
||||
/* id: */ categoryIdCounter++,
|
||||
/* name: */ context.categoryData.category,
|
||||
/* docs: */ parseDocs(context.categoryData),
|
||||
/* categories: */ children.subCategories,
|
||||
/* scripts: */ children.subScripts,
|
||||
);
|
||||
} catch (err) {
|
||||
new NodeValidator({
|
||||
type: NodeType.Category,
|
||||
selfNode: context.categoryData,
|
||||
parentNode: context.parentCategory,
|
||||
}).throw(err.message);
|
||||
}
|
||||
return new Category(
|
||||
/* id: */ categoryIdCounter++,
|
||||
/* name: */ category.category,
|
||||
/* docs: */ parseDocUrls(category),
|
||||
/* categories: */ children.subCategories,
|
||||
/* scripts: */ children.subScripts,
|
||||
);
|
||||
}
|
||||
|
||||
function ensureValid(category: CategoryData) {
|
||||
if (!category) {
|
||||
throw Error('missing category');
|
||||
}
|
||||
if (!category.children || category.children.length === 0) {
|
||||
throw Error(`category has no children: "${category.category}"`);
|
||||
}
|
||||
if (!category.category || category.category.length === 0) {
|
||||
throw Error('category has no name');
|
||||
}
|
||||
function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
|
||||
new NodeValidator({
|
||||
type: NodeType.Category,
|
||||
selfNode: category,
|
||||
parentNode: parentCategory,
|
||||
})
|
||||
.assertDefined(category)
|
||||
.assertValidName(category.category)
|
||||
.assert(
|
||||
() => category.children && category.children.length > 0,
|
||||
`"${category.category}" has no children.`,
|
||||
);
|
||||
}
|
||||
|
||||
interface ICategoryChildren {
|
||||
@@ -48,22 +82,29 @@ interface ICategoryChildren {
|
||||
subScripts: Script[];
|
||||
}
|
||||
|
||||
function parseCategoryChild(
|
||||
data: CategoryOrScriptData,
|
||||
children: ICategoryChildren,
|
||||
parent: CategoryData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
) {
|
||||
if (isCategory(data)) {
|
||||
const subCategory = parseCategory(data as CategoryData, context);
|
||||
children.subCategories.push(subCategory);
|
||||
} else if (isScript(data)) {
|
||||
const scriptData = data as ScriptData;
|
||||
const script = parseScript(scriptData, context);
|
||||
children.subScripts.push(script);
|
||||
interface INodeParseContext {
|
||||
readonly nodeData: CategoryOrScriptData;
|
||||
readonly children: ICategoryChildren;
|
||||
readonly parent: CategoryData;
|
||||
readonly factory: CategoryFactoryType;
|
||||
readonly context: ICategoryCollectionParseContext;
|
||||
}
|
||||
function parseNode(context: INodeParseContext) {
|
||||
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
|
||||
validator.assertDefined(context.nodeData);
|
||||
if (isCategory(context.nodeData)) {
|
||||
const subCategory = parseCategoryRecursively({
|
||||
categoryData: context.nodeData as CategoryData,
|
||||
context: context.context,
|
||||
factory: context.factory,
|
||||
parentCategory: context.parent,
|
||||
});
|
||||
context.children.subCategories.push(subCategory);
|
||||
} else if (isScript(context.nodeData)) {
|
||||
const script = parseScript(context.nodeData as ScriptData, context.context);
|
||||
context.children.subScripts.push(script);
|
||||
} else {
|
||||
throw new Error(`Child element is neither a category or a script.
|
||||
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
|
||||
validator.throw('Node is neither a category or a script.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,14 +114,22 @@ function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||
}
|
||||
|
||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||
const { category } = data as CategoryData;
|
||||
return category && category.length > 0;
|
||||
return hasProperty(data, 'category');
|
||||
}
|
||||
|
||||
function hasCode(holder: InstructionHolder): boolean {
|
||||
return holder.code && holder.code.length > 0;
|
||||
function hasCode(data: InstructionHolder): boolean {
|
||||
return hasProperty(data, 'code');
|
||||
}
|
||||
|
||||
function hasCall(holder: InstructionHolder) {
|
||||
return holder.call !== undefined;
|
||||
function hasCall(data: InstructionHolder) {
|
||||
return hasProperty(data, 'call');
|
||||
}
|
||||
|
||||
function hasProperty(object: unknown, propertyName: string) {
|
||||
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||
}
|
||||
|
||||
export type CategoryFactoryType = (
|
||||
...parameters: ConstructorParameters<typeof Category>) => Category;
|
||||
|
||||
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);
|
||||
|
||||
@@ -1,64 +1,58 @@
|
||||
import type { DocumentableData, DocumentationUrlsData } from '@/application/collections/';
|
||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||
|
||||
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||
if (!documentable) {
|
||||
throw new Error('missing documentable');
|
||||
}
|
||||
const { docs } = documentable;
|
||||
if (!docs || !docs.length) {
|
||||
if (!docs) {
|
||||
return [];
|
||||
}
|
||||
let result = new DocumentationUrlContainer();
|
||||
let result = new DocumentationContainer();
|
||||
result = addDocs(docs, result);
|
||||
return result.getAll();
|
||||
}
|
||||
|
||||
function addDocs(
|
||||
docs: DocumentationUrlsData,
|
||||
urls: DocumentationUrlContainer,
|
||||
): DocumentationUrlContainer {
|
||||
docs: DocumentationData,
|
||||
container: DocumentationContainer,
|
||||
): DocumentationContainer {
|
||||
if (docs instanceof Array) {
|
||||
urls.addUrls(docs);
|
||||
if (docs.length > 0) {
|
||||
container.addParts(docs);
|
||||
}
|
||||
} else if (typeof docs === 'string') {
|
||||
urls.addUrl(docs);
|
||||
container.addPart(docs);
|
||||
} else {
|
||||
throw new Error('Docs field (documentation url) must a string or array of strings');
|
||||
throwInvalidType();
|
||||
}
|
||||
return urls;
|
||||
return container;
|
||||
}
|
||||
|
||||
class DocumentationUrlContainer {
|
||||
private readonly urls = new Array<string>();
|
||||
class DocumentationContainer {
|
||||
private readonly parts = new Array<string>();
|
||||
|
||||
public addUrl(url: string) {
|
||||
validateUrl(url);
|
||||
this.urls.push(url);
|
||||
public addPart(documentation: string) {
|
||||
if (!documentation) {
|
||||
throw Error('missing documentation');
|
||||
}
|
||||
if (typeof documentation !== 'string') {
|
||||
throwInvalidType();
|
||||
}
|
||||
this.parts.push(documentation);
|
||||
}
|
||||
|
||||
public addUrls(urls: readonly string[]) {
|
||||
for (const url of urls) {
|
||||
if (typeof url !== 'string') {
|
||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
||||
}
|
||||
this.addUrl(url);
|
||||
public addParts(parts: readonly string[]) {
|
||||
for (const part of parts) {
|
||||
this.addPart(part);
|
||||
}
|
||||
}
|
||||
|
||||
public getAll(): ReadonlyArray<string> {
|
||||
return this.urls;
|
||||
return this.parts;
|
||||
}
|
||||
}
|
||||
|
||||
function validateUrl(docUrl: string): void {
|
||||
if (!docUrl) {
|
||||
throw new Error('Documentation url is null or empty');
|
||||
}
|
||||
if (docUrl.includes('\n')) {
|
||||
throw new Error('Documentation url cannot be multi-lined.');
|
||||
}
|
||||
const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
|
||||
const res = docUrl.match(validUrlRegex);
|
||||
if (res == null) {
|
||||
throw new Error(`Invalid documentation url: ${docUrl}`);
|
||||
}
|
||||
function throwInvalidType() {
|
||||
throw new Error('docs field (documentation) must be an array of strings');
|
||||
}
|
||||
|
||||
3
src/application/Parser/NodeValidation/NodeData.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { ScriptData, CategoryData } from '@/application/collections/';
|
||||
|
||||
export type NodeData = CategoryData | ScriptData;
|
||||
34
src/application/Parser/NodeValidation/NodeDataError.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { CustomError } from '@/application/Common/CustomError';
|
||||
import { NodeType } from './NodeType';
|
||||
import { NodeData } from './NodeData';
|
||||
|
||||
export class NodeDataError extends CustomError {
|
||||
constructor(message: string, public readonly context: INodeDataErrorContext) {
|
||||
super(createMessage(message, context));
|
||||
}
|
||||
}
|
||||
|
||||
export interface INodeDataErrorContext {
|
||||
readonly type?: NodeType;
|
||||
readonly selfNode: NodeData;
|
||||
readonly parentNode?: NodeData;
|
||||
}
|
||||
|
||||
function createMessage(errorMessage: string, context: INodeDataErrorContext) {
|
||||
let message = '';
|
||||
if (context.type !== undefined) {
|
||||
message += `${NodeType[context.type]}: `;
|
||||
}
|
||||
message += errorMessage;
|
||||
message += `\n${dump(context)}`;
|
||||
return message;
|
||||
}
|
||||
|
||||
function dump(context: INodeDataErrorContext): string {
|
||||
const printJson = (obj: unknown) => JSON.stringify(obj, undefined, 2);
|
||||
let output = `Self: ${printJson(context.selfNode)}`;
|
||||
if (context.parentNode) {
|
||||
output += `\nParent: ${printJson(context.parentNode)}`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
4
src/application/Parser/NodeValidation/NodeType.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum NodeType {
|
||||
Script,
|
||||
Category,
|
||||
}
|
||||
38
src/application/Parser/NodeValidation/NodeValidator.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { INodeDataErrorContext, NodeDataError } from './NodeDataError';
|
||||
import { NodeData } from './NodeData';
|
||||
|
||||
export class NodeValidator {
|
||||
constructor(private readonly context: INodeDataErrorContext) {
|
||||
|
||||
}
|
||||
|
||||
public assertValidName(nameValue: string) {
|
||||
return this
|
||||
.assert(
|
||||
() => Boolean(nameValue),
|
||||
'missing name',
|
||||
)
|
||||
.assert(
|
||||
() => typeof nameValue === 'string',
|
||||
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
||||
);
|
||||
}
|
||||
|
||||
public assertDefined(node: NodeData) {
|
||||
return this.assert(
|
||||
() => node !== undefined && node !== null && Object.keys(node).length > 0,
|
||||
'missing node data',
|
||||
);
|
||||
}
|
||||
|
||||
public assert(validationPredicate: () => boolean, errorMessage: string) {
|
||||
if (!validationPredicate()) {
|
||||
this.throw(errorMessage);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public throw(errorMessage: string) {
|
||||
throw new NodeDataError(errorMessage, this.context);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,29 @@
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
||||
import { Version } from '@/domain/Version';
|
||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
||||
import { ConstructorArguments } from '@/TypeHelpers';
|
||||
|
||||
export function parseProjectInformation(
|
||||
environment: NodeJS.ProcessEnv,
|
||||
export function
|
||||
parseProjectInformation(
|
||||
metadata: IAppMetadata = AppMetadataFactory.Current.instance,
|
||||
createProjectInformation: ProjectInformationFactory = (
|
||||
...args
|
||||
) => new ProjectInformation(...args),
|
||||
): IProjectInformation {
|
||||
const version = new Version(environment.VUE_APP_VERSION);
|
||||
return new ProjectInformation(
|
||||
environment.VUE_APP_NAME,
|
||||
const version = new Version(
|
||||
metadata.version,
|
||||
);
|
||||
return createProjectInformation(
|
||||
metadata.name,
|
||||
version,
|
||||
environment.VUE_APP_REPOSITORY_URL,
|
||||
environment.VUE_APP_HOMEPAGE_URL,
|
||||
metadata.slogan,
|
||||
metadata.repositoryUrl,
|
||||
metadata.homepageUrl,
|
||||
);
|
||||
}
|
||||
|
||||
export type ProjectInformationFactory = (
|
||||
...args: ConstructorArguments<typeof ProjectInformation>
|
||||
) => IProjectInformation;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { FunctionData } from '@/application/collections/';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||
import { SyntaxFactory } from './Syntax/SyntaxFactory';
|
||||
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
||||
import { SyntaxFactory } from './Validation/Syntax/SyntaxFactory';
|
||||
import { ISyntaxFactory } from './Validation/Syntax/ISyntaxFactory';
|
||||
import { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
|
||||
|
||||
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||
public readonly compiler: IScriptCompiler;
|
||||
|
||||
@@ -13,4 +13,22 @@ export class ExpressionPosition {
|
||||
throw Error(`negative start position: ${start}`);
|
||||
}
|
||||
}
|
||||
|
||||
public isInInsideOf(potentialParent: ExpressionPosition): boolean {
|
||||
if (this.isSame(potentialParent)) {
|
||||
return false;
|
||||
}
|
||||
return potentialParent.start <= this.start
|
||||
&& potentialParent.end >= this.end;
|
||||
}
|
||||
|
||||
public isSame(other: ExpressionPosition): boolean {
|
||||
return other.start === this.start
|
||||
&& other.end === this.end;
|
||||
}
|
||||
|
||||
public isIntersecting(other: ExpressionPosition): boolean {
|
||||
return (other.start < this.end && other.end > this.start)
|
||||
|| (this.end > other.start && other.start >= this.start);
|
||||
}
|
||||
}
|
||||
|
||||