Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
493fb1ec16 | ||
|
|
b167a69976 | ||
|
|
79b46bf210 | ||
|
|
98a26f9ae4 | ||
|
|
dbe3c5cfb9 | ||
|
|
25d7f7b2a4 | ||
|
|
b76e99ac0f | ||
|
|
67c3677621 | ||
|
|
bab6316e76 | ||
|
|
48730bca05 | ||
|
|
698b570ee6 | ||
|
|
a3f11dff18 | ||
|
|
5e359c2fb8 | ||
|
|
2147eae687 | ||
|
|
286295128d | ||
|
|
8501495c17 | ||
|
|
888c9166fc | ||
|
|
e5f6edf405 | ||
|
|
e8a52f717d | ||
|
|
d45750428c | ||
|
|
cf55ca9e28 | ||
|
|
3e5239f7d3 | ||
|
|
7669985f8e | ||
|
|
5047c9b6e7 | ||
|
|
bd2082e8c5 | ||
|
|
8f188acd3c | ||
|
|
0303ef2fd9 | ||
|
|
cb21a970b6 | ||
|
|
203daeb4a2 | ||
|
|
60dde11311 | ||
|
|
8b930fc57c | ||
|
|
f810ed0c14 | ||
|
|
53222fd83c | ||
|
|
a1f2497381 | ||
|
|
c27172c32e | ||
|
|
6e9b65d8b1 | ||
|
|
6d301f9961 | ||
|
|
659fea7afc | ||
|
|
e0303058a3 | ||
|
|
65f121c451 | ||
|
|
821cc62c4c | ||
|
|
4ce327eb6a | ||
|
|
4beb1bb574 | ||
|
|
0a2a1a026b | ||
|
|
eb096d07e2 | ||
|
|
19e42c9c52 | ||
|
|
f4d86fccfd | ||
|
|
ad0576a752 | ||
|
|
35be05df20 | ||
|
|
dae6d114da | ||
|
|
ecce47fdcd | ||
|
|
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,7 +1,11 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
[*.{js,jsx,ts,tsx,vue,sh}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
|
||||
[{Dockerfile}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
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
|
||||
@@ -31,9 +31,11 @@ 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)
|
||||
- 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
|
||||
|
||||
12
.github/actions/npm-install-dependencies/action.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
inputs:
|
||||
working-directory:
|
||||
required: false
|
||||
default: '.'
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
-
|
||||
name: Run `npm ci` with retries
|
||||
shell: bash
|
||||
run: npm run install-deps -- --ci
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
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@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
83
.github/workflows/checks.build.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
name: build-checks
|
||||
name: checks.build
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -9,51 +9,92 @@ 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:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Build
|
||||
name: Build web
|
||||
run: npm run build -- --mode ${{ matrix.mode }}
|
||||
-
|
||||
name: Verify web build artifacts
|
||||
run: npm run check:verify-build-artifacts -- --web
|
||||
|
||||
# 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:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
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 desktop
|
||||
run: npm run electron:prebuild -- --mode ${{ matrix.mode }}
|
||||
-
|
||||
name: Build
|
||||
name: Verify unbundled desktop build artifacts
|
||||
run: npm run check:verify-build-artifacts -- --electron-unbundled
|
||||
-
|
||||
name: Build (bundle and package) desktop application
|
||||
run: npm run electron:build -- --publish never
|
||||
-
|
||||
name: Verify bundled desktop build artifacts
|
||||
run: npm run check:verify-build-artifacts -- --electron-bundled
|
||||
|
||||
build-docker:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu ] # Windows runners do not support Linux containers
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Install Docker on macOS
|
||||
if: matrix.os == 'macos' # macOS runner is missing Docker
|
||||
run: |-
|
||||
cross-env-shell NODE_ENV=${{ matrix.mode }}
|
||||
npm run electron:build -- --publish never --mode ${{ matrix.mode }}
|
||||
# Install Docker
|
||||
brew install docker
|
||||
# Docker on macOS misses daemon due to licensing, so install colima as runtime
|
||||
brew install colima
|
||||
# Start the daemon
|
||||
colima start
|
||||
-
|
||||
name: Build Docker image
|
||||
run: docker build -t undergroundwires/privacy.sexy:latest .
|
||||
-
|
||||
name: Run Docker image on port 8080
|
||||
run: docker run -d -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest
|
||||
-
|
||||
name: Check server is up and returns HTTP 200
|
||||
run: node ./scripts/verify-web-server-status.js --url http://localhost:8080
|
||||
|
||||
72
.github/workflows/checks.desktop-runtime-errors.yaml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: checks.desktop-runtime-errors
|
||||
# Verifies desktop builds for Electron applications across multiple OS platforms (macOS ,Ubuntu, and Windows).
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
run-check:
|
||||
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@v4
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
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: |-
|
||||
export SCREENSHOT=true
|
||||
npm run check:desktop
|
||||
-
|
||||
name: Upload screenshot
|
||||
if: always() # Run even if previous step fails
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: screenshot-${{ matrix.os }}
|
||||
path: screenshot.png
|
||||
22
.github/workflows/checks.external-urls.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: checks.external-urls
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||
|
||||
jobs:
|
||||
run-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Test
|
||||
run: npm run check:external-urls
|
||||
20
.github/workflows/checks.quality.yaml
vendored
@@ -16,13 +16,15 @@ jobs:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Still interested to see results from other combinations
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Lint
|
||||
run: ${{ matrix.lint-command }}
|
||||
|
||||
55
.github/workflows/checks.scripts.yaml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: checks.scripts
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
icons-build:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Still interested to see results from other combinations
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Create icons
|
||||
run: npm run icons:build
|
||||
|
||||
install-deps:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
matrix:
|
||||
install-deps-before: [true, false]
|
||||
install-command:
|
||||
- npm run install-deps
|
||||
- npm run install-deps -- --no-errors
|
||||
- npm run install-deps -- --ci
|
||||
- npm run install-deps -- --fresh --non-deterministic
|
||||
- npm run install-deps -- --fresh
|
||||
- npm run install-deps -- --non-deterministic
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Still interested to see results from other combinations
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
if: matrix.install-deps-before == true
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run install-deps
|
||||
run: ${{ matrix.install-command }}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: security-checks
|
||||
name: checks.security.dependencies
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -13,12 +13,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
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
|
||||
42
.github/workflows/checks.security.sast.yaml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: checks.security.sast
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [
|
||||
javascript # analyzes code written in JavaScript, TypeScript and both.
|
||||
]
|
||||
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-and-quality
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
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@v4
|
||||
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
|
||||
run: npm ci
|
||||
- name: Run unit tests
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
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
|
||||
33
.github/workflows/release.site.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: "Infrastructure: Checkout"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: aws
|
||||
repository: undergroundwires/aws-static-site-with-cd
|
||||
@@ -75,32 +75,41 @@ jobs:
|
||||
working-directory: aws
|
||||
-
|
||||
name: "App: Checkout"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
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
|
||||
uses: ./app/.github/actions/npm-install-dependencies
|
||||
with:
|
||||
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: Verify web build artifacts"
|
||||
run: npm run check:verify-build-artifacts -- --web
|
||||
working-directory: app
|
||||
-
|
||||
name: "App: Deploy to S3"
|
||||
run: >-
|
||||
shell: bash
|
||||
run: |-
|
||||
declare web_output_dir
|
||||
if ! web_output_dir=$(cd app && node scripts/print-dist-dir.js --web); then
|
||||
echo 'Error: Could not determine distribution directory.'
|
||||
exit 1
|
||||
fi
|
||||
bash "aws/scripts/deploy/deploy-to-s3.sh" \
|
||||
--folder site/dist \
|
||||
--folder "${web_output_dir}" \
|
||||
--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}} \
|
||||
|
||||
10
.github/workflows/tests.e2e.yaml
vendored
@@ -14,15 +14,13 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run e2e tests
|
||||
run: npm run test:e2e -- --headless
|
||||
run: npm run test:cy:run
|
||||
|
||||
8
.github/workflows/tests.integration.yaml
vendored
@@ -16,15 +16,13 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15.x
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run integration tests
|
||||
run: npm run test:integration
|
||||
|
||||
10
.github/workflows/tests.unit.yaml
vendored
@@ -14,15 +14,13 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
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
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run unit tests
|
||||
run: npm run test:unit
|
||||
|
||||
7
.gitignore
vendored
@@ -1,10 +1,5 @@
|
||||
node_modules
|
||||
dist/
|
||||
/dist-*/
|
||||
.vs
|
||||
.vscode/**/*
|
||||
!.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.
|
||||
|
||||
144
CHANGELOG.md
@@ -1,5 +1,149 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.5 (2023-10-13)
|
||||
|
||||
* Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16)
|
||||
* Add SAST security checks with SECURITY.md #178 | [3e5239f](https://github.com/undergroundwires/privacy.sexy/commit/3e5239f7d35e57749c01adf3dbbcd365aebb39c8)
|
||||
* Add Scoop download instructions #174 | [cf55ca9](https://github.com/undergroundwires/privacy.sexy/commit/cf55ca9e28b064fa7a516077a9da23e3a8e3f534)
|
||||
* win: fix and improve temp dir cleanup #176, #89 | [d457504](https://github.com/undergroundwires/privacy.sexy/commit/d45750428cca010daf2721b33a8ae3a01b28813b)
|
||||
* win, linux: improve VSCode setting robustness #196 | [e8a52f7](https://github.com/undergroundwires/privacy.sexy/commit/e8a52f717dc799b34ceeb1c27c2b8219391dff6a)
|
||||
* linux: fix obsolete Firefox DPI script #239 | [e5f6edf](https://github.com/undergroundwires/privacy.sexy/commit/e5f6edf405bcec7c29ea4d7932d1910620fa15f8)
|
||||
* win: add removal of Edge assocations #64 | [888c916](https://github.com/undergroundwires/privacy.sexy/commit/888c9166fc66a2094137fa8be739cc21bafef5f6)
|
||||
* win: improve Edge & OneDrive shortcut removal #73 | [8501495](https://github.com/undergroundwires/privacy.sexy/commit/8501495c170af61913288a63dbd369db5bbc5003)
|
||||
* win: relocate and document SecHealthUI #190 | [2862951](https://github.com/undergroundwires/privacy.sexy/commit/286295128d0179358e0c6b7b6415d752175a1aed)
|
||||
* Add developer toolkit UI component | [2147eae](https://github.com/undergroundwires/privacy.sexy/commit/2147eae687b82d05bc43bb4605d9068f148bb92a)
|
||||
* win: fix and improve network data usage reset #265 | [5e359c2](https://github.com/undergroundwires/privacy.sexy/commit/5e359c2fb82a08e6acf7159b70ca86a8234b359b)
|
||||
* win: improve app reversion and docs #260 | [a3f11df](https://github.com/undergroundwires/privacy.sexy/commit/a3f11dff187c821a00910c20dac05e285cda9073)
|
||||
* Fix working directory in CI/CD web release | [698b570](https://github.com/undergroundwires/privacy.sexy/commit/698b570ee6e300d6703015464f4345b5e706f1cb)
|
||||
* Implement new UI component for icons #230 | [48730bc](https://github.com/undergroundwires/privacy.sexy/commit/48730bca0506120bca4bf3a23545d59f2b1a9009)
|
||||
* win: fix and improve AppCompat disabling #255 | [bab6316](https://github.com/undergroundwires/privacy.sexy/commit/bab6316e7625230cf4a4cf67c3aca417347db75c)
|
||||
* win, linux, mac: fix typos and improve naming | [67c3677](https://github.com/undergroundwires/privacy.sexy/commit/67c3677621b201525a813e8a26f07d607176e89b)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.4...0.12.5)
|
||||
|
||||
## 0.12.4 (2023-09-25)
|
||||
|
||||
* win: fix Windows spotlight revert, docs, recommend | [659fea7](https://github.com/undergroundwires/privacy.sexy/commit/659fea7afcabcd0ea273cfdcc8c4bae190c126f3)
|
||||
* win: fix Edge telemetry disabling for v116+ #242 | [6d301f9](https://github.com/undergroundwires/privacy.sexy/commit/6d301f99616ed49975876803d0098eafe4d3cb2e)
|
||||
* win: fix, improve disabling automatic updates #252 | [6e9b65d](https://github.com/undergroundwires/privacy.sexy/commit/6e9b65d8b1b481c1471dde90876c37838b4ac4e5)
|
||||
* win: refactor `update.mode` key for VSCode #215 | [c27172c](https://github.com/undergroundwires/privacy.sexy/commit/c27172c32e7c316b7cb0f44cab611eed89ca034e)
|
||||
* Fix wrong action path in website CI deployment | [a1f2497](https://github.com/undergroundwires/privacy.sexy/commit/a1f24973813ccbdd7e1f06c64e1912a991a6bb64)
|
||||
* Fix compiler bug with nested optional arguments | [53222fd](https://github.com/undergroundwires/privacy.sexy/commit/53222fd83c2846089746a217482195806f960d18)
|
||||
* Fix no spacing after lists in documentation text | [f810ed0](https://github.com/undergroundwires/privacy.sexy/commit/f810ed0c147c2a46cae3b70b635ed81128646fff)
|
||||
* Rewrite tooltip UI for efficiency and Vue 3.0 #230 | [8b930fc](https://github.com/undergroundwires/privacy.sexy/commit/8b930fc57c8ee6691ed6165bcb27d97e64a1a0c0)
|
||||
* win: fix uninstallation of newer Edge #236 | [60dde11](https://github.com/undergroundwires/privacy.sexy/commit/60dde11311a2409537f5965f370b0daaaec53339)
|
||||
* win: fix delivery optimization side-effects #173 | [203daeb](https://github.com/undergroundwires/privacy.sexy/commit/203daeb4a2fca0a0295cbc2a736394f9f87725e6)
|
||||
* win: fix Defender scan artifacts removal #246 | [cb21a97](https://github.com/undergroundwires/privacy.sexy/commit/cb21a970b6b867e1476a5eb8a72b9a7fdd53a744)
|
||||
* Fix outdated and broken links in README #161 | [0303ef2](https://github.com/undergroundwires/privacy.sexy/commit/0303ef2fd98b36306523e2a0c5f5ae812a4c6c99)
|
||||
* Fix loss of tree node state when switching views | [8f188ac](https://github.com/undergroundwires/privacy.sexy/commit/8f188acd3c2d93e40c89569c74bc5cff992f0052)
|
||||
* Fix slow appearance of nodes on tree view | [bd2082e](https://github.com/undergroundwires/privacy.sexy/commit/bd2082e8c574db065bb4462f30ea3ace2cb028cb)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.3...0.12.4)
|
||||
|
||||
## 0.12.3 (2023-09-09)
|
||||
|
||||
* linux: use user.js over prefs.js for Firefox #232 | [dae6d11](https://github.com/undergroundwires/privacy.sexy/commit/dae6d114daab6857d773071211eb57619b136281)
|
||||
* win: fix typo in Defender retention script #213 | [35be05d](https://github.com/undergroundwires/privacy.sexy/commit/35be05df2094ea8bba4ee4725e6fa4956a79493d)
|
||||
* Improve desktop runtime execution tests | [ad0576a](https://github.com/undergroundwires/privacy.sexy/commit/ad0576a752f8fd6ea2f917a59173fe61f9951246)
|
||||
* Fix Windows artifact naming in desktop packaging | [f4d86fc](https://github.com/undergroundwires/privacy.sexy/commit/f4d86fccfd0e73e94c8c6e400a33514900bc5abe)
|
||||
* Refactor and improve external URL checks | [19e42c9](https://github.com/undergroundwires/privacy.sexy/commit/19e42c9c52a18c813ded4265e687e01032cdd4c8)
|
||||
* Fix memory leaks via auto-unsubscribing and DI | [eb096d0](https://github.com/undergroundwires/privacy.sexy/commit/eb096d07e276e1b4c8040220c47f186d02841e14)
|
||||
* Refactor build configs and improve CI/CD checks | [0a2a1a0](https://github.com/undergroundwires/privacy.sexy/commit/0a2a1a026b0efb29624be82b06536c518c1ea439)
|
||||
* Introduce retry mechanism for npm install in CI/CD | [4beb1bb](https://github.com/undergroundwires/privacy.sexy/commit/4beb1bb5748a60886210187ca3cdc7f4b41067c0)
|
||||
* win: fix disable recent apps revert #211, #248 | [4ce327e](https://github.com/undergroundwires/privacy.sexy/commit/4ce327eb6af542ed2916d649553e5e1ba5833882)
|
||||
* Change license to AGPLv3 | [821cc62](https://github.com/undergroundwires/privacy.sexy/commit/821cc62c4c8347cb76d041f82f574754e4d948c5)
|
||||
* Introduce new TreeView UI component | [65f121c](https://github.com/undergroundwires/privacy.sexy/commit/65f121c451af87315e1c91df4198562e0445b2c2)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.2...0.12.3)
|
||||
|
||||
## 0.12.2 (2023-08-25)
|
||||
|
||||
* Add automated checks for desktop app runtime #233 | [04b3133](https://github.com/undergroundwires/privacy.sexy/commit/04b3133500485d0d278a81a177a1677134131405)
|
||||
* win: fix automatic updates revert #234 | [0873769](https://github.com/undergroundwires/privacy.sexy/commit/08737698c2283bdf535d1611a730031ebfc7c0df)
|
||||
* Migrate unit/integration tests to Vitest with Vite | [5f11c8d](https://github.com/undergroundwires/privacy.sexy/commit/5f11c8d98f782dd7c77f27649a1685fb7bd06e13)
|
||||
* Remove Vue ESLint plugin for Vite compatibility | [6e40edd](https://github.com/undergroundwires/privacy.sexy/commit/6e40edd3f8a063c1b7482c27d8368e14c2fbcfbf)
|
||||
* Migrate web builds from Vue CLI to Vite | [7365905](https://github.com/undergroundwires/privacy.sexy/commit/736590558be51a09435bb87e78b6655e8533bc2e)
|
||||
* Migrate Cypress (E2E) tests to Vite and TypeScript | [ec98d84](https://github.com/undergroundwires/privacy.sexy/commit/ec98d8417f779fa818ccdda6bb90f521e1738002)
|
||||
* Migrate to `electron-vite` and `electron-builder` | [75c9b51](https://github.com/undergroundwires/privacy.sexy/commit/75c9b51bf2d1dc7269adfd7b5ed71acfb5031299)
|
||||
* Fix searching/filtering bugs #235 | [62f8bfa](https://github.com/undergroundwires/privacy.sexy/commit/62f8bfac2f481c93598fe19a51594769f522d684)
|
||||
* Improve desktop security by isolating Electron | [e9e0001](https://github.com/undergroundwires/privacy.sexy/commit/e9e0001ef845fa6935c59a4e20a89aac9e71756a)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.1...0.12.2)
|
||||
|
||||
## 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)
|
||||
|
||||
15
Dockerfile
@@ -1,13 +1,16 @@
|
||||
# Build
|
||||
FROM node:lts-alpine as build-stage
|
||||
FROM node:lts-alpine AS build-stage
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
RUN npm run install-deps
|
||||
RUN npm run build \
|
||||
&& npm run check:verify-build-artifacts -- --web
|
||||
RUN mkdir /dist \
|
||||
&& dist_directory=$(node 'scripts/print-dist-dir.js' --web) \
|
||||
&& cp -a "${dist_directory}/." '/dist'
|
||||
|
||||
# Production stage
|
||||
FROM nginx:stable-alpine as production-stage
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
FROM nginx:stable-alpine AS production-stage
|
||||
COPY --from=build-stage /dist /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
141
LICENSE
@@ -1,5 +1,5 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@@ -7,17 +7,15 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
94
README.md
@@ -1,30 +1,22 @@
|
||||
# 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"
|
||||
/>
|
||||
</a>
|
||||
<!-- Code quality -->
|
||||
<br />
|
||||
<a href="https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript">
|
||||
<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,59 +24,85 @@
|
||||
</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"
|
||||
/>
|
||||
</a>
|
||||
<!-- Security checks -->
|
||||
<br />
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.sast.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Status of dependency security checks"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.security.sast/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.dependencies.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Status of Static Analysis Security Testing (SAST)"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.security.dependencies/badge.svg"
|
||||
/>
|
||||
</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.build.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Security checks status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/security-checks/badge.svg"
|
||||
alt="Status of build checks"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.build/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.desktop-runtime-errors.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Build checks status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
|
||||
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>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.scripts.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Status of script checks"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.scripts/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.external-urls.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Status of external URL checks"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.external-urls/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 +110,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"
|
||||
@@ -104,7 +122,7 @@
|
||||
## Get started
|
||||
|
||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||
- 🖥️ **Offline**: Check [releases page](https://github.com/undergroundwires/privacy.sexy/releases), or download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-Setup-0.11.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.dmg), [Linux](https://github.com/undergroundwires/pr.vacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.AppImage).
|
||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-Setup-0.12.5.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-0.12.5.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-0.12.5.AppImage). For more options, see [here](#additional-install-options).
|
||||
|
||||
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
||||
|
||||
@@ -120,18 +138,28 @@ 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).
|
||||
|
||||
## Additional Install Options
|
||||
|
||||
- Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions.
|
||||
- Using [Scoop](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) package manager on Windows:
|
||||
|
||||
```powershell
|
||||
scoop bucket add extras
|
||||
scoop install privacy.sexy
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Refer to [development.md](./docs/development.md) for Docker usage and reading more about setting up your development environment.
|
||||
@@ -139,3 +167,7 @@ Refer to [development.md](./docs/development.md) for Docker usage and reading mo
|
||||
Check [architecture.md](./docs/architecture.md) for an overview of design and how different parts and layers work together. You can refer to [application.md](./docs/application.md) for a closer look at application layer codebase and [presentation.md](./docs/presentation.md) for code related to GUI layer. [collection-files.md](./docs/collection-files.md) explains the YAML files that are the core of the application and [templating.md](./docs/templating.md) documents how to use templating language in those files. In [ci-cd.md](./docs/ci-cd.md), you can read more about the pipelines that automates maintenance tasks and ensures you get what see.
|
||||
|
||||
[docs/](./docs/) folder includes all other documentation.
|
||||
|
||||
## Security
|
||||
|
||||
Security is a top priority at privacy.sexy. An extensive commitment to security verification ensures this priority. For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md).
|
||||
|
||||
31
SECURITY.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Security Policy
|
||||
|
||||
privacy.sexy takes security seriously. Commitment is made to address all security issues with urgency. Responsible reporting of any discovered vulnerabilities in the project is highly encouraged.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Efforts to responsibly disclose findings are greatly appreciated. To report a security vulnerability, follow these steps:
|
||||
|
||||
- For general vulnerabilities, [open an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) using the bug report template.
|
||||
- For sensitive matters, [contact the developer directly](https://undergroundwires.dev).
|
||||
|
||||
## Security Report Handling
|
||||
|
||||
Upon receipt of a security report, the following actions will be taken:
|
||||
|
||||
- The report will be confirmed, identifying the affected components.
|
||||
- The impact and severity of the issue will be assessed.
|
||||
- Work on a fix and plan a release to address the vulnerability will be initiated.
|
||||
- The reporter will be kept updated about the progress.
|
||||
|
||||
## Testing
|
||||
|
||||
Regular and extensive testing is conducted to ensure robust security in the project. Information about testing practices can be found in the [Testing Documentation](./docs/tests.md).
|
||||
|
||||
## Support
|
||||
|
||||
For additional assistance or any unanswered questions, [submit a GitHub issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose). Security concerns are a priority, and necessary support to address them is assured.
|
||||
|
||||
---
|
||||
|
||||
Active contribution to the safety and security of privacy.sexy is thanked. This collaborative effort keeps the project resilient and trustworthy for all.
|
||||
@@ -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 |
18
cypress.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
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`,
|
||||
|
||||
video: true,
|
||||
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"
|
||||
}
|
||||
5
dist-dirs.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"electronUnbundled": "dist-electron-unbundled",
|
||||
"electronBundled": "dist-electron-bundled",
|
||||
"web": "dist-web"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -171,3 +174,19 @@
|
||||
- `endCode:` *`string`* (**required**)
|
||||
- Code that'll be inserted at the end of user created script.
|
||||
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||
|
||||
## Naming guidelines
|
||||
|
||||
- Prioritize consistency throughout all names.
|
||||
- Use an instruction format like "do this, do that" for clear, direct guidance. This approach reduces potential confusion and offers easy-to-follow steps. It provides specific, unambiguous instructions.
|
||||
- Ensure brand names adhere to their official casing.
|
||||
- Choose clear and uncomplicated language.
|
||||
- Favor the terms:
|
||||
- "Disable" over "Turn off"
|
||||
- "Configure" over "Set up"
|
||||
- "Clear" over "Erase" or "Clean"
|
||||
- "Minimize" over "Limit" or "Reduce" (when it enhances clarity)
|
||||
- "Remove" over "Uninstall"
|
||||
- Structure your phrases for clarity.
|
||||
- For instance, "Disable XX telemetry" or "Clear XX data" are preferred over "Clear data from XX", "Disable telemetry in XX", or "Clear data of XX".
|
||||
- Use sentence case rather than Title Case.
|
||||
|
||||
@@ -5,22 +5,28 @@ Before your commit, a good practice is to:
|
||||
1. [Run unit tests](#testing)
|
||||
2. [Lint your code](#linting)
|
||||
|
||||
You could run other types of tests as well, but they may take longer time and overkill for your changes. Automated actions executes the tests for a pull request or change in the main branch. See [ci-cd.md](./ci-cd.md) for more information.
|
||||
You could run other types of tests as well, but they may take longer time and overkill for your changes.
|
||||
Automated actions are set up to execute these tests as necessary.
|
||||
See [ci-cd.md](./ci-cd.md) for more information.
|
||||
|
||||
## Commands
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Install node >15.x.
|
||||
- Install dependencies using `npm install`.
|
||||
- Install Node >16.x.
|
||||
- Install dependencies using `npm install` (or [`npm run install-deps`](#utility-scripts) for more options).
|
||||
|
||||
### Testing
|
||||
|
||||
- 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.
|
||||
- Run checks:
|
||||
- `npm run check:desktop`: Run runtime checks for packaged desktop applications ([README.md](./../tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/README.md)).
|
||||
- You can set environment variables active its flags such as `BUILD=true SCREENSHOT=true npm run check:desktop`
|
||||
- `npm run check:external-urls`: Test whether external URLs used in applications are alive.
|
||||
|
||||
📖 Read more about testing in [tests](./tests.md).
|
||||
|
||||
@@ -35,16 +41,53 @@ 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:
|
||||
- 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`
|
||||
3. Application should be available at [`http://localhost:8080`](http://localhost:8080)
|
||||
|
||||
### 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`
|
||||
|
||||
### Scripts
|
||||
|
||||
📖 For detailed options and behavior for any of the following scripts, please refer to the script file itself.
|
||||
|
||||
#### Utility scripts
|
||||
|
||||
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
|
||||
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
|
||||
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
|
||||
- [**`./scripts/configure-vscode.sh`**](../scripts/configure-vscode.sh):
|
||||
- This script checks and sets the necessary configurations for VSCode in `settings.json` file.
|
||||
|
||||
#### Automation scripts
|
||||
|
||||
- [**`node scripts/print-dist-dir.js [<options>]`**](../scripts/print-dist-dir.js):
|
||||
- Determines the absolute path of a distribution directory based on CLI arguments and outputs its absolute path.
|
||||
- [**`npm run check:verify-build-artifacts [-- <options>]`**](../scripts/verify-build-artifacts.js):
|
||||
- Verifies the existence and content of build artifacts. Useful for ensuring that the build process is generating the expected output.
|
||||
- [**`node scripts/verify-web-server-status.js --url [URL]`**](../scripts/verify-web-server-status.js):
|
||||
- Checks if a specified server is up with retries and returns an HTTP 200 status code.
|
||||
|
||||
## Recommended extensions
|
||||
|
||||
|
||||
@@ -1,34 +1,41 @@
|
||||
# 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 coupled to Vue 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,34 +46,58 @@ 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.
|
||||
|
||||
## Sass naming convention
|
||||
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.
|
||||
|
||||
## Styles
|
||||
|
||||
### Style location
|
||||
|
||||
- **Global styles**: The [`assets/styles/`](#structure) directory is reserved for styles that have a broader scope, affecting multiple components or entire layouts. They are generic and should not be tightly coupled to a specific component's functionality.
|
||||
- **Component-specific styles**: Styles closely tied to a particular component's functionality or appearance should reside near the component they are used by. This makes it easier to locate and modify styles when working on a specific component.
|
||||
|
||||
### Sass naming convention
|
||||
|
||||
- Use lowercase for variables/functions/mixins, e.g.:
|
||||
- Variable: `$variable: value;`
|
||||
|
||||
@@ -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 }}`.
|
||||
|
||||
|
||||
127
docs/tests.md
@@ -5,77 +5,84 @@ 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).
|
||||
|
||||
### Security checks
|
||||
|
||||
- [`checks.security.sast`](./../.github/workflows/checks.security.sast.yaml): Utilizes CodeQL to conduct Static Analysis Security Testing (SAST) to ensure the secure integrity of the codebase.
|
||||
- [`checks.security.dependencies`](./../.github/workflows/checks.security.dependencies.yaml): Performs audits on third-party dependencies to identify and mitigate potential vulnerabilities, safeguarding the project from exploitable weaknesses.
|
||||
|
||||
## 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.
|
||||
|
||||
43
electron-builder.cjs
Normal file
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
|
||||
const { join } = require('path');
|
||||
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
||||
|
||||
module.exports = {
|
||||
// Common options
|
||||
publish: {
|
||||
provider: 'github',
|
||||
vPrefixedTagName: false, // default: true
|
||||
releaseType: 'release', // default: draft
|
||||
},
|
||||
directories: {
|
||||
output: electronBundled,
|
||||
},
|
||||
extraMetadata: {
|
||||
main: join(electronUnbundled, 'main/index.cjs'), // do not `path.resolve`, it expects a relative path
|
||||
},
|
||||
|
||||
// Windows
|
||||
win: {
|
||||
target: 'nsis',
|
||||
},
|
||||
nsis: {
|
||||
artifactName: '${name}-Setup-${version}.${ext}',
|
||||
},
|
||||
|
||||
// Linux
|
||||
linux: {
|
||||
target: 'AppImage',
|
||||
},
|
||||
appImage: {
|
||||
artifactName: '${name}-${version}.${ext}',
|
||||
},
|
||||
|
||||
// macOS
|
||||
mac: {
|
||||
target: 'dmg',
|
||||
},
|
||||
dmg: {
|
||||
artifactName: '${name}-${version}.${ext}',
|
||||
},
|
||||
};
|
||||
69
electron.vite.config.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
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';
|
||||
import distDirs from './dist-dirs.json' assert { type: 'json' };
|
||||
|
||||
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(distDirs.electronUnbundled);
|
||||
|
||||
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 |
52085
package-lock.json
generated
144
package.json
@@ -1,92 +1,98 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.11.3",
|
||||
"version": "0.12.5",
|
||||
"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",
|
||||
"main": "./dist-electron-unbundled/main/index.cjs",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"test:e2e": "vue-cli-service test:e2e",
|
||||
"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: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",
|
||||
"install-deps": "node scripts/npm-install.js",
|
||||
"icons:build": "node scripts/logo-update.js",
|
||||
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
|
||||
"check:external-urls": "vitest run --dir tests/checks/external-urls --environment node",
|
||||
"check:verify-build-artifacts": "node scripts/verify-build-artifacts",
|
||||
"electron:dev": "electron-vite dev",
|
||||
"electron:preview": "electron-vite preview",
|
||||
"electron:prebuild": "electron-vite build",
|
||||
"electron:build": "electron-builder",
|
||||
"lint:eslint": "eslint . --ignore-path .gitignore",
|
||||
"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",
|
||||
"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",
|
||||
"@floating-ui/vue": "^1.0.2",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"ace-builds": "^1.30.0",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-progressbar": "^2.1.0",
|
||||
"electron-updater": "^6.1.4",
|
||||
"file-saver": "^2.0.5",
|
||||
"install": "^0.13.0",
|
||||
"liquor-tree": "^0.2.70",
|
||||
"npm": "^8.1.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"
|
||||
"markdown-it": "^13.0.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.5.1",
|
||||
"@types/ace": "^0.0.49",
|
||||
"@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.16",
|
||||
"cypress": "^13.3.1",
|
||||
"electron": "^27.0.0",
|
||||
"electron-builder": "^24.6.4",
|
||||
"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-vite": "^1.0.27",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"eslint-plugin-vuejs-accessibility": "^2.2.0",
|
||||
"icon-gen": "^4.0.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"markdownlint-cli": "^0.37.0",
|
||||
"postcss": "^8.4.31",
|
||||
"remark-cli": "^12.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": "^13.0.0",
|
||||
"sass": "^1.69.3",
|
||||
"start-server-and-test": "^2.0.1",
|
||||
"svgexport": "^0.4.2",
|
||||
"terser": "^5.21.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.11",
|
||||
"vitest": "^0.34.6",
|
||||
"vue-tsc": "^1.8.19",
|
||||
"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",
|
||||
"@rushstack/eslint-patch": "Needed by @vue/eslint-config-typescript",
|
||||
"@vue/eslint-config-typescript": "Cannot upgrade to 12.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58",
|
||||
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58",
|
||||
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58"
|
||||
},
|
||||
"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 |
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
|
||||
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();
|
||||
199
scripts/npm-install.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
Description:
|
||||
This script manages NPM dependencies for a project.
|
||||
It offers capabilities like doing a fresh install, retries on network errors, and other features.
|
||||
|
||||
Usage:
|
||||
npm run install-deps [-- <options>]
|
||||
node scripts/npm-install.js [options]
|
||||
|
||||
Options:
|
||||
--root-directory <path>
|
||||
Specifies the root directory where package.json resides
|
||||
Defaults to the current working directory.
|
||||
Example: npm run install-deps -- --root-directory /your/path/here
|
||||
|
||||
--no-errors
|
||||
Ignores errors and continues the execution.
|
||||
Example: npm run install-deps -- --no-errors
|
||||
|
||||
--ci
|
||||
Uses 'npm ci' for dependency installation instead of 'npm install'.
|
||||
Example: npm run install-deps -- --ci
|
||||
|
||||
--fresh
|
||||
Removes the existing node_modules directory before installing dependencies.
|
||||
Example: npm run install-deps -- --fresh
|
||||
|
||||
--non-deterministic
|
||||
Removes package-lock.json for a non-deterministic installation.
|
||||
Example: npm run install-deps -- --non-deterministic
|
||||
|
||||
Note:
|
||||
|
||||
Flags can be combined as needed.
|
||||
Example: npm run install-deps -- --fresh --non-deterministic
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { resolve } from 'path';
|
||||
import { access, rm, unlink } from 'fs/promises';
|
||||
import { constants } from 'fs';
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const RETRY_DELAY_IN_MS = 5 /* seconds */ * 1000;
|
||||
const ARG_NAMES = {
|
||||
rootDirectory: '--root-directory',
|
||||
ignoreErrors: '--no-errors',
|
||||
ci: '--ci',
|
||||
fresh: '--fresh',
|
||||
nonDeterministic: '--non-deterministic',
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const options = getOptions();
|
||||
console.log('Options:', options);
|
||||
await ensureNpmRootDirectory(options.rootDirectory);
|
||||
await ensureNpmIsAvailable();
|
||||
if (options.fresh) {
|
||||
await removeNodeModules(options.rootDirectory);
|
||||
}
|
||||
if (options.nonDeterministic) {
|
||||
await removePackageLockJson(options.rootDirectory);
|
||||
}
|
||||
const command = buildCommand(options.ci, options.outputErrors);
|
||||
console.log('Starting dependency installation...');
|
||||
const exitCode = await executeWithRetry(
|
||||
command,
|
||||
options.workingDirectory,
|
||||
MAX_RETRIES,
|
||||
RETRY_DELAY_IN_MS,
|
||||
);
|
||||
if (exitCode === 0) {
|
||||
console.log('🎊 Installed dependencies...');
|
||||
} else {
|
||||
console.error(`💀 Failed to install dependencies, exit code: ${exitCode}`);
|
||||
}
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
async function removeNodeModules(workingDirectory) {
|
||||
const nodeModulesDirectory = resolve(workingDirectory, 'node_modules');
|
||||
if (await exists('./node_modules')) {
|
||||
console.log('Removing node_modules...');
|
||||
await rm(nodeModulesDirectory, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function removePackageLockJson(workingDirectory) {
|
||||
const packageLockJsonFile = resolve(workingDirectory, 'package-lock.json');
|
||||
if (await exists(packageLockJsonFile)) {
|
||||
console.log('Removing package-lock.json...');
|
||||
await unlink(packageLockJsonFile);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureNpmIsAvailable() {
|
||||
const exitCode = await executeCommand('npm --version');
|
||||
if (exitCode !== 0) {
|
||||
throw new Error('`npm` in not available!');
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureNpmRootDirectory(workingDirectory) {
|
||||
const packageJsonPath = resolve(workingDirectory, 'package.json');
|
||||
if (!await exists(packageJsonPath)) {
|
||||
throw new Error(`Not an NPM project root: ${workingDirectory}`);
|
||||
}
|
||||
}
|
||||
|
||||
function buildCommand(ci, outputErrors) {
|
||||
const baseCommand = ci ? 'npm ci' : 'npm install';
|
||||
if (!outputErrors) {
|
||||
return `${baseCommand} --loglevel=error`;
|
||||
}
|
||||
return baseCommand;
|
||||
}
|
||||
|
||||
function getOptions() {
|
||||
const processArgs = process.argv.slice(2); // Slice off the node and script name
|
||||
return {
|
||||
rootDirectory: processArgs.includes('--root-directory') ? processArgs[processArgs.indexOf('--root-directory') + 1] : process.cwd(),
|
||||
outputErrors: !processArgs.includes(ARG_NAMES.ignoreErrors),
|
||||
ci: processArgs.includes(ARG_NAMES.ci),
|
||||
fresh: processArgs.includes(ARG_NAMES.fresh),
|
||||
nonDeterministic: processArgs.includes(ARG_NAMES.nonDeterministic),
|
||||
};
|
||||
}
|
||||
|
||||
async function executeWithRetry(
|
||||
command,
|
||||
workingDirectory,
|
||||
maxRetries,
|
||||
retryDelayInMs,
|
||||
currentAttempt = 1,
|
||||
) {
|
||||
const statusCode = await executeCommand(command, workingDirectory, true, true);
|
||||
if (statusCode === 0 || currentAttempt >= maxRetries) {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
console.log(`⚠️🔄 Attempt ${currentAttempt} failed. Retrying in ${retryDelayInMs / 1000} seconds...`);
|
||||
await sleep(retryDelayInMs);
|
||||
|
||||
const retryResult = await executeWithRetry(
|
||||
command,
|
||||
workingDirectory,
|
||||
maxRetries,
|
||||
retryDelayInMs,
|
||||
currentAttempt + 1,
|
||||
);
|
||||
return retryResult;
|
||||
}
|
||||
|
||||
async function executeCommand(
|
||||
command,
|
||||
workingDirectory = process.cwd(),
|
||||
logStdout = false,
|
||||
logCommand = false,
|
||||
) {
|
||||
if (logCommand) {
|
||||
console.log(`▶️ Executing command "${command}" at "${workingDirectory}"`);
|
||||
}
|
||||
const process = exec(
|
||||
command,
|
||||
{
|
||||
cwd: workingDirectory,
|
||||
},
|
||||
);
|
||||
if (logStdout) {
|
||||
process.stdout.on('data', (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
}
|
||||
process.stderr.on('data', (data) => {
|
||||
console.error(data.toString());
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
process.on('exit', (code) => {
|
||||
resolve(code);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sleep(milliseconds) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, milliseconds);
|
||||
});
|
||||
}
|
||||
|
||||
async function exists(path) {
|
||||
try {
|
||||
await access(path, constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
58
scripts/print-dist-dir.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Description:
|
||||
* This script determines the absolute path of a distribution directory based on CLI arguments
|
||||
* and outputs its absolute path. It is designed to be run programmatically by other scripts.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/print-dist-dir.js [options]
|
||||
*
|
||||
* Options:
|
||||
* --electron-unbundled Path for the unbundled Electron application
|
||||
* --electron-bundled Path for the bundled Electron application
|
||||
* --web Path for the web application
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
const DIST_DIRS_JSON_FILE_PATH = resolve(process.cwd(), 'dist-dirs.json'); // cannot statically import because ESLint does not support it https://github.com/eslint/eslint/discussions/15305
|
||||
const CLI_ARGUMENTS = process.argv.slice(2);
|
||||
|
||||
async function main() {
|
||||
const distDirs = await readDistDirsJsonFile(DIST_DIRS_JSON_FILE_PATH);
|
||||
const relativeDistDir = determineRelativeDistDir(distDirs, CLI_ARGUMENTS);
|
||||
const absoluteDistDir = resolve(process.cwd(), relativeDistDir);
|
||||
console.log(absoluteDistDir);
|
||||
}
|
||||
|
||||
function mapCliFlagsToDistDirs(distDirs) {
|
||||
return {
|
||||
'--electron-unbundled': distDirs.electronUnbundled,
|
||||
'--electron-bundled': distDirs.electronBundled,
|
||||
'--web': distDirs.web,
|
||||
};
|
||||
}
|
||||
|
||||
function determineRelativeDistDir(distDirsJsonObject, cliArguments) {
|
||||
const cliFlagDistDirMap = mapCliFlagsToDistDirs(distDirsJsonObject);
|
||||
const availableCliFlags = Object.keys(cliFlagDistDirMap);
|
||||
const requestedCliFlags = cliArguments.filter((arg) => {
|
||||
return availableCliFlags.includes(arg);
|
||||
});
|
||||
if (!requestedCliFlags.length) {
|
||||
throw new Error(`No distribution directory was requested. Please use one of these flags: ${availableCliFlags.join(', ')}`);
|
||||
}
|
||||
if (requestedCliFlags.length > 1) {
|
||||
throw new Error(`Multiple distribution directories were requested, but this script only supports one: ${requestedCliFlags.join(', ')}`);
|
||||
}
|
||||
const selectedCliFlag = requestedCliFlags[0];
|
||||
return cliFlagDistDirMap[selectedCliFlag];
|
||||
}
|
||||
|
||||
async function readDistDirsJsonFile(absoluteConfigJsonFilePath) {
|
||||
const fileContentAsText = await readFile(absoluteConfigJsonFilePath, 'utf8');
|
||||
const parsedJsonData = JSON.parse(fileContentAsText);
|
||||
return parsedJsonData;
|
||||
}
|
||||
|
||||
await main();
|
||||
133
scripts/verify-build-artifacts.js
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Description:
|
||||
* This script verifies the existence and content of build artifacts based on the
|
||||
* provided CLI flags. It exists with exit code `0` if all verifications pass, otherwise
|
||||
* with exit code `1`.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/verify-build-artifacts.js [options]
|
||||
*
|
||||
* Options:
|
||||
* --electron-unbundled Verify artifacts for the unbundled Electron application.
|
||||
* --electron-bundled Verify artifacts for the bundled Electron application.
|
||||
* --web Verify artifacts for the web application.
|
||||
*/
|
||||
|
||||
import { access, readdir } from 'fs/promises';
|
||||
import { exec } from 'child_process';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const PROCESS_ARGUMENTS = process.argv.slice(2);
|
||||
const PRINT_DIST_DIR_SCRIPT_BASE_COMMAND = 'node scripts/print-dist-dir';
|
||||
|
||||
async function main() {
|
||||
const buildConfigs = getBuildVerificationConfigs();
|
||||
if (!anyCommandsFound(Object.keys(buildConfigs))) {
|
||||
die(`No valid command found in process arguments. Expected one of: ${Object.keys(buildConfigs).join(', ')}`);
|
||||
}
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const [command, config] of Object.entries(buildConfigs)) {
|
||||
if (PROCESS_ARGUMENTS.includes(command)) {
|
||||
const distDir = await executePrintDistDirScript(config.printDistDirScriptArgument);
|
||||
await verifyDirectoryExists(distDir);
|
||||
await verifyNonEmptyDirectory(distDir);
|
||||
await verifyFilesExist(distDir, config.filePatterns);
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
console.log('✅ Build completed successfully and all expected artifacts are in place.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function getBuildVerificationConfigs() {
|
||||
return {
|
||||
'--electron-unbundled': {
|
||||
printDistDirScriptArgument: '--electron-unbundled',
|
||||
filePatterns: [
|
||||
/main[/\\]index\.cjs/,
|
||||
/preload[/\\]index\.cjs/,
|
||||
/renderer[/\\]index\.htm(l)?/,
|
||||
],
|
||||
},
|
||||
'--electron-bundled': {
|
||||
printDistDirScriptArgument: '--electron-bundled',
|
||||
filePatterns: [
|
||||
/latest.*\.yml/, // generates latest.yml for auto-updates
|
||||
/.*-\d+\.\d+\.\d+\..*/, // a file with extension and semantic version (packaged application)
|
||||
],
|
||||
},
|
||||
'--web': {
|
||||
printDistDirScriptArgument: '--web',
|
||||
filePatterns: [
|
||||
/index\.htm(l)?/,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function anyCommandsFound(commands) {
|
||||
return PROCESS_ARGUMENTS.some((arg) => commands.includes(arg));
|
||||
}
|
||||
|
||||
async function verifyDirectoryExists(directoryPath) {
|
||||
try {
|
||||
await access(directoryPath);
|
||||
} catch (error) {
|
||||
die(`Directory does not exist at \`${directoryPath}\`:\n\t${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyNonEmptyDirectory(directoryPath) {
|
||||
const files = await readdir(directoryPath);
|
||||
if (files.length === 0) {
|
||||
die(`Directory is empty at \`${directoryPath}\``);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyFilesExist(directoryPath, filePatterns) {
|
||||
const files = await listAllFilesRecursively(directoryPath);
|
||||
for (const pattern of filePatterns) {
|
||||
const match = files.some((file) => pattern.test(file));
|
||||
if (!match) {
|
||||
die(
|
||||
`No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``,
|
||||
`\nFiles in directory:\n${files.map((file) => `\t- ${file}`).join('\n')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function listAllFilesRecursively(directoryPath) {
|
||||
const dir = await readdir(directoryPath, { withFileTypes: true });
|
||||
const files = await Promise.all(dir.map(async (dirent) => {
|
||||
const absolutePath = resolve(directoryPath, dirent.name);
|
||||
if (dirent.isDirectory()) {
|
||||
return listAllFilesRecursively(absolutePath);
|
||||
}
|
||||
return absolutePath;
|
||||
}));
|
||||
return files.flat();
|
||||
}
|
||||
|
||||
async function executePrintDistDirScript(flag) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const commandToRun = `${PRINT_DIST_DIR_SCRIPT_BASE_COMMAND} ${flag}`;
|
||||
|
||||
exec(commandToRun, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`Execution failed with error: ${error}`));
|
||||
} else if (stderr) {
|
||||
reject(new Error(`Execution failed with stderr: ${stderr}`));
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function die(...message) {
|
||||
console.error(...message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await main();
|
||||
62
scripts/verify-web-server-status.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Description:
|
||||
* This script checks if a server, provided as a CLI argument, is up
|
||||
* and returns an HTTP 200 status code.
|
||||
* It is designed to provide easy verification of server availability
|
||||
* and will retry a specified number of times.
|
||||
*
|
||||
* Usage:
|
||||
* node ./scripts/verify-web-server-status.js --url [URL]
|
||||
*
|
||||
* Options:
|
||||
* --url URL of the server to check
|
||||
*/
|
||||
|
||||
import { get } from 'http';
|
||||
|
||||
const MAX_RETRIES = 30;
|
||||
const RETRY_DELAY_IN_SECONDS = 3;
|
||||
const URL_PARAMETER_NAME = '--url';
|
||||
|
||||
function checkServer(currentRetryCount = 1) {
|
||||
const serverUrl = getServerUrl();
|
||||
console.log(`Requesting ${serverUrl}...`);
|
||||
get(serverUrl, (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
console.log('🎊 Success: The server is up and returned HTTP 200.');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`Server returned HTTP status code ${res.statusCode}.`);
|
||||
retry(currentRetryCount);
|
||||
}
|
||||
}).on('error', (err) => {
|
||||
console.error('Error making the request:', err);
|
||||
retry(currentRetryCount);
|
||||
});
|
||||
}
|
||||
|
||||
function retry(currentRetryCount) {
|
||||
console.log(`Attempt ${currentRetryCount}/${MAX_RETRIES}:`);
|
||||
console.log(`Retrying in ${RETRY_DELAY_IN_SECONDS} seconds.`);
|
||||
|
||||
const remainingTime = (MAX_RETRIES - currentRetryCount) * RETRY_DELAY_IN_SECONDS;
|
||||
console.log(`Time remaining before timeout: ${remainingTime}s`);
|
||||
|
||||
if (currentRetryCount < MAX_RETRIES) {
|
||||
setTimeout(() => checkServer(currentRetryCount + 1), RETRY_DELAY_IN_SECONDS * 1000);
|
||||
} else {
|
||||
console.log('Failure: The server at did not return HTTP 200 within the allocated time. Exiting.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getServerUrl() {
|
||||
const urlIndex = process.argv.indexOf(URL_PARAMETER_NAME);
|
||||
if (urlIndex === -1 || urlIndex === process.argv.length - 1) {
|
||||
console.error(`Parameter "${URL_PARAMETER_NAME}" is not provided.`);
|
||||
process.exit(1);
|
||||
}
|
||||
return process.argv[urlIndex + 1];
|
||||
}
|
||||
|
||||
checkServer();
|
||||
16
src/TypeHelpers.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export type Constructible<T, TArgs extends unknown[] = never> = {
|
||||
prototype: T;
|
||||
apply: (this: unknown, args: TArgs) => void;
|
||||
readonly name: string;
|
||||
};
|
||||
|
||||
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,25 +1,23 @@
|
||||
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 { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { IApplicationFactory } from '../IApplicationFactory';
|
||||
import { ApplicationFactory } from '../ApplicationFactory';
|
||||
import { ApplicationContext } from './ApplicationContext';
|
||||
|
||||
export async function buildContext(
|
||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||
environment = Environment.CurrentEnvironment,
|
||||
environment = RuntimeEnvironment.CurrentEnvironment,
|
||||
): Promise<IApplicationContext> {
|
||||
if (!factory) { throw new Error('missing factory'); }
|
||||
if (!environment) { throw new Error('missing environment'); }
|
||||
const app = await factory.getApp();
|
||||
const os = getInitialOs(app, environment);
|
||||
const os = getInitialOs(app, environment.os);
|
||||
return new ApplicationContext(app, os);
|
||||
}
|
||||
|
||||
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
||||
const currentOs = environment.os;
|
||||
function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
|
||||
const supportedOsList = app.getSupportedOsList();
|
||||
if (supportedOsList.includes(currentOs)) {
|
||||
return currentOs;
|
||||
|
||||
@@ -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/EnvironmentVariables/IAppMetadata';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
|
||||
export function parseApplication(
|
||||
parser = CategoryCollectionParser,
|
||||
processEnv: NodeJS.ProcessEnv = process.env,
|
||||
categoryParser = parseCategoryCollection,
|
||||
informationParser = parseProjectInformation,
|
||||
metadata: IAppMetadata = EnvironmentVariablesFactory.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,
|
||||
});
|
||||
}
|
||||
return new Category(
|
||||
try {
|
||||
return context.factory(
|
||||
/* id: */ categoryIdCounter++,
|
||||
/* name: */ category.category,
|
||||
/* docs: */ parseDocUrls(category),
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
|
||||
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/EnvironmentVariables/IAppMetadata';
|
||||
import { Version } from '@/domain/Version';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { ConstructorArguments } from '@/TypeHelpers';
|
||||
|
||||
export function parseProjectInformation(
|
||||
environment: NodeJS.ProcessEnv,
|
||||
export function
|
||||
parseProjectInformation(
|
||||
metadata: IAppMetadata = EnvironmentVariablesFactory.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,21 +20,59 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||
if (!code) {
|
||||
return code;
|
||||
}
|
||||
const expressions = this.extractor.findExpressions(code);
|
||||
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
||||
const context = new ExpressionEvaluationContext(args);
|
||||
const compiledCode = compileExpressions(expressions, code, context);
|
||||
const compiledCode = compileRecursively(code, context, this.extractor);
|
||||
return compiledCode;
|
||||
}
|
||||
}
|
||||
|
||||
function compileRecursively(
|
||||
code: string,
|
||||
context: IExpressionEvaluationContext,
|
||||
extractor: IExpressionParser,
|
||||
): string {
|
||||
/*
|
||||
Instead of compiling code at once and returning we compile expressions from the code.
|
||||
And recompile expressions from resulting code recursively.
|
||||
This allows using expressions inside expressions blocks. E.g.:
|
||||
```
|
||||
{{ with $condition }}
|
||||
echo '{{ $text }}'
|
||||
{{ end }}
|
||||
```
|
||||
Without recursing parameter substitution for '{{ $text }}' is skipped once the outer
|
||||
{{ with $condition }} is rendered.
|
||||
A more optimized alternative to recursion would be to a parse an expression tree
|
||||
instead of linear expression lists.
|
||||
*/
|
||||
if (!code) {
|
||||
return code;
|
||||
}
|
||||
const expressions = extractor.findExpressions(code);
|
||||
if (expressions.length === 0) {
|
||||
return code;
|
||||
}
|
||||
const compiledCode = compileExpressions(expressions, code, context);
|
||||
return compileRecursively(compiledCode, context, extractor);
|
||||
}
|
||||
|
||||
function compileExpressions(
|
||||
expressions: readonly IExpression[],
|
||||
code: string,
|
||||
context: IExpressionEvaluationContext,
|
||||
) {
|
||||
ensureValidExpressions(expressions, code, context);
|
||||
let compiledCode = '';
|
||||
const sortedExpressions = expressions
|
||||
const outerExpressions = expressions.filter(
|
||||
(expression) => expressions
|
||||
.filter((otherExpression) => otherExpression !== expression)
|
||||
.every((otherExpression) => !expression.position.isInInsideOf(otherExpression.position)),
|
||||
);
|
||||
/*
|
||||
This logic will only compile outer expressions if there were nested expressions.
|
||||
So the output of this compilation may result in new uncompiled expressions.
|
||||
*/
|
||||
const sortedExpressions = outerExpressions
|
||||
.slice() // copy the array to not mutate the parameter
|
||||
.sort((a, b) => b.position.start - a.position.start);
|
||||
let index = 0;
|
||||
@@ -65,6 +103,43 @@ function extractRequiredParameterNames(
|
||||
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('", "')}"`;
|
||||
}
|
||||
|
||||
function ensureValidExpressions(
|
||||
expressions: readonly IExpression[],
|
||||
code: string,
|
||||
context: IExpressionEvaluationContext,
|
||||
) {
|
||||
ensureParamsUsedInCodeHasArgsProvided(expressions, context.args);
|
||||
ensureExpressionsDoesNotExtendCodeLength(expressions, code);
|
||||
ensureNoExpressionsAtSamePosition(expressions);
|
||||
ensureNoInvalidIntersections(expressions);
|
||||
}
|
||||
|
||||
function ensureExpressionsDoesNotExtendCodeLength(
|
||||
expressions: readonly IExpression[],
|
||||
code: string,
|
||||
) {
|
||||
const expectedMax = code.length;
|
||||
const expressionsOutOfRange = expressions
|
||||
.filter((expression) => expression.position.end > expectedMax);
|
||||
if (expressionsOutOfRange.length > 0) {
|
||||
throw new Error(`Expressions out of range:\n${JSON.stringify(expressionsOutOfRange)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoExpressionsAtSamePosition(expressions: readonly IExpression[]) {
|
||||
const instructionsAtSamePosition = expressions.filter(
|
||||
(expression) => expressions
|
||||
.filter((other) => expression.position.isSame(other.position)).length > 1,
|
||||
);
|
||||
if (instructionsAtSamePosition.length > 0) {
|
||||
throw new Error(`Instructions at same position:\n${JSON.stringify(instructionsAtSamePosition)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureParamsUsedInCodeHasArgsProvided(
|
||||
expressions: readonly IExpression[],
|
||||
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
@@ -80,6 +155,16 @@ function ensureParamsUsedInCodeHasArgsProvided(
|
||||
}
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('", "')}"`;
|
||||
function ensureNoInvalidIntersections(expressions: readonly IExpression[]) {
|
||||
const intersectingInstructions = expressions.filter(
|
||||
(expression) => expressions
|
||||
.filter((other) => expression.position.isIntersecting(other.position))
|
||||
.filter((other) => !expression.position.isSame(other.position))
|
||||
.filter((other) => !expression.position.isInInsideOf(other.position))
|
||||
.filter((other) => !other.position.isInInsideOf(expression.position))
|
||||
.length > 0,
|
||||
);
|
||||
if (intersectingInstructions.length > 0) {
|
||||
throw new Error(`Instructions intersecting unexpectedly:\n${JSON.stringify(intersectingInstructions)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ export class ExpressionRegexBuilder {
|
||||
.addRawRegex('([^|\\s]+)');
|
||||
}
|
||||
|
||||
public matchAnythingExceptSurroundingWhitespaces() {
|
||||
public matchMultilineAnythingExceptSurroundingWhitespaces() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.addRawRegex('(.+?)')
|
||||
.addRawRegex('([\\S\\s]+?)')
|
||||
.expectZeroOrMoreWhitespaces();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export class EscapeDoubleQuotes implements IPipe {
|
||||
return raw;
|
||||
}
|
||||
return raw.replaceAll('"', '"^""');
|
||||
/* eslint-disable max-len */
|
||||
/* eslint-disable vue/max-len */
|
||||
/*
|
||||
"^"" is the most robust and stable choice.
|
||||
Other options:
|
||||
@@ -28,6 +28,6 @@ export class EscapeDoubleQuotes implements IPipe {
|
||||
Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"`
|
||||
A good explanation: https://stackoverflow.com/a/31413730
|
||||
*/
|
||||
/* eslint-enable max-len */
|
||||
/* eslint-enable vue/max-len */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export class WithParser extends RegexParser {
|
||||
.matchUntilFirstWhitespace() // First match: parameter name
|
||||
.expectExpressionEnd()
|
||||
// ...
|
||||
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||
// {{ end }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('end')
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
|
||||
export interface CodeSegmentMerger {
|
||||
mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
import { CodeSegmentMerger } from './CodeSegmentMerger';
|
||||
|
||||
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
||||
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
||||
if (!codeSegments?.length) {
|
||||
throw new Error('missing segments');
|
||||
}
|
||||
return {
|
||||
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
||||
revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function joinCodeParts(codeSegments: readonly string[]): string {
|
||||
return codeSegments
|
||||
.filter((segment) => segment?.length > 0)
|
||||
.join('\n');
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ICompiledCode {
|
||||
export interface CompiledCode {
|
||||
readonly code: string;
|
||||
readonly revertCode?: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { FunctionCall } from '../FunctionCall';
|
||||
import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
|
||||
|
||||
export interface FunctionCallCompilationContext {
|
||||
readonly allFunctions: ISharedFunctionCollection;
|
||||
readonly rootCallSequence: readonly FunctionCall[];
|
||||
readonly singleCallCompiler: SingleCallCompiler;
|
||||
}
|
||||
@@ -1,149 +1,10 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
|
||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
|
||||
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
|
||||
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { FunctionCall } from '../FunctionCall';
|
||||
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
|
||||
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
import { CompiledCode } from './CompiledCode';
|
||||
|
||||
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||
|
||||
protected constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||
) {
|
||||
}
|
||||
|
||||
public compileCall(
|
||||
calls: IFunctionCall[],
|
||||
export interface FunctionCallCompiler {
|
||||
compileFunctionCalls(
|
||||
calls: readonly FunctionCall[],
|
||||
functions: ISharedFunctionCollection,
|
||||
): ICompiledCode {
|
||||
if (!functions) { throw new Error('missing functions'); }
|
||||
if (!calls) { throw new Error('missing calls'); }
|
||||
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
|
||||
const context: ICompilationContext = {
|
||||
allFunctions: functions,
|
||||
callSequence: calls,
|
||||
expressionsCompiler: this.expressionsCompiler,
|
||||
};
|
||||
const code = compileCallSequence(context);
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
interface ICompilationContext {
|
||||
allFunctions: ISharedFunctionCollection;
|
||||
callSequence: readonly IFunctionCall[];
|
||||
expressionsCompiler: IExpressionsCompiler;
|
||||
}
|
||||
|
||||
interface ICompiledFunctionCall {
|
||||
readonly code: string;
|
||||
readonly revertCode: string;
|
||||
}
|
||||
|
||||
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
|
||||
const compiledFunctions = context.callSequence
|
||||
.flatMap((call) => compileSingleCall(call, context));
|
||||
return {
|
||||
code: merge(compiledFunctions.map((f) => f.code)),
|
||||
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
||||
};
|
||||
}
|
||||
|
||||
function compileSingleCall(
|
||||
call: IFunctionCall,
|
||||
context: ICompilationContext,
|
||||
): ICompiledFunctionCall[] {
|
||||
const func = context.allFunctions.getFunctionByName(call.functionName);
|
||||
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||
if (func.body.code) { // Function with inline code
|
||||
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
||||
return [compiledCode];
|
||||
}
|
||||
// Function with inner calls
|
||||
return func.body.calls
|
||||
.map((innerCall) => {
|
||||
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
||||
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
|
||||
return compileSingleCall(compiledCall, context);
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function compileCode(
|
||||
code: IFunctionCode,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
compiler: IExpressionsCompiler,
|
||||
): ICompiledFunctionCall {
|
||||
return {
|
||||
code: compiler.compileExpressions(code.do, args),
|
||||
revertCode: compiler.compileExpressions(code.revert, args),
|
||||
};
|
||||
}
|
||||
|
||||
function compileArgs(
|
||||
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
compiler: IExpressionsCompiler,
|
||||
): IReadOnlyFunctionCallArgumentCollection {
|
||||
return argsToCompile
|
||||
.getAllParameterNames()
|
||||
.map((parameterName) => {
|
||||
const { argumentValue } = argsToCompile.getArgument(parameterName);
|
||||
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
||||
return new FunctionCallArgument(parameterName, compiledValue);
|
||||
})
|
||||
.reduce((compiledArgs, arg) => {
|
||||
compiledArgs.addArgument(arg);
|
||||
return compiledArgs;
|
||||
}, new FunctionCallArgumentCollection());
|
||||
}
|
||||
|
||||
function merge(codeParts: readonly string[]): string {
|
||||
return codeParts
|
||||
.filter((part) => part?.length > 0)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function ensureThatCallArgumentsExistInParameterDefinition(
|
||||
func: ISharedFunction,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
): void {
|
||||
const callArgumentNames = args.getAllParameterNames();
|
||||
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
|
||||
}
|
||||
|
||||
function findUnexpectedParameters(
|
||||
callArgumentNames: string[],
|
||||
functionParameterNames: string[],
|
||||
): string[] {
|
||||
if (!callArgumentNames.length && !functionParameterNames.length) {
|
||||
return [];
|
||||
}
|
||||
return callArgumentNames
|
||||
.filter((callParam) => !functionParameterNames.includes(callParam));
|
||||
}
|
||||
|
||||
function throwIfNotEmpty(
|
||||
functionName: string,
|
||||
unexpectedParameters: string[],
|
||||
expectedParameters: string[],
|
||||
) {
|
||||
if (!unexpectedParameters.length) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
// eslint-disable-next-line prefer-template
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||
+ `"${unexpectedParameters.join('", "')}"`
|
||||
+ '. Expected parameter(s): '
|
||||
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||
);
|
||||
): CompiledCode;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||
import { FunctionCallCompiler } from './FunctionCallCompiler';
|
||||
import { CompiledCode } from './CompiledCode';
|
||||
import { FunctionCallCompilationContext } from './FunctionCallCompilationContext';
|
||||
import { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
|
||||
import { AdaptiveFunctionCallCompiler } from './SingleCall/AdaptiveFunctionCallCompiler';
|
||||
import { CodeSegmentMerger } from './CodeSegmentJoin/CodeSegmentMerger';
|
||||
import { NewlineCodeSegmentMerger } from './CodeSegmentJoin/NewlineCodeSegmentMerger';
|
||||
|
||||
export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
|
||||
public static readonly instance: FunctionCallCompiler = new FunctionCallSequenceCompiler();
|
||||
|
||||
/* The constructor is protected to enforce the singleton pattern. */
|
||||
protected constructor(
|
||||
private readonly singleCallCompiler: SingleCallCompiler = new AdaptiveFunctionCallCompiler(),
|
||||
private readonly codeSegmentMerger: CodeSegmentMerger = new NewlineCodeSegmentMerger(),
|
||||
) { }
|
||||
|
||||
public compileFunctionCalls(
|
||||
calls: readonly FunctionCall[],
|
||||
functions: ISharedFunctionCollection,
|
||||
): CompiledCode {
|
||||
if (!functions) { throw new Error('missing functions'); }
|
||||
if (!calls?.length) { throw new Error('missing calls'); }
|
||||
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
|
||||
const context: FunctionCallCompilationContext = {
|
||||
allFunctions: functions,
|
||||
rootCallSequence: calls,
|
||||
singleCallCompiler: this.singleCallCompiler,
|
||||
};
|
||||
const codeSegments = context.rootCallSequence
|
||||
.flatMap((call) => this.singleCallCompiler.compileSingleCall(call, context));
|
||||
return this.codeSegmentMerger.mergeCodeParts(codeSegments);
|
||||
}
|
||||
}
|
||||