Compare commits
41 Commits
disableser
...
moremacos
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfe5704328 | ||
|
|
d16846fa3c | ||
|
|
112e79a64c | ||
|
|
eeb1d5b0c4 | ||
|
|
d6bc33ec86 | ||
|
|
956052c8ff | ||
|
|
3785e410db | ||
|
|
481a02afd5 | ||
|
|
5bbbb9cecc | ||
|
|
db47440d47 | ||
|
|
1bcc6c8b2b | ||
|
|
3c3ec80525 | ||
|
|
803ef2bb3e | ||
|
|
43ce834750 | ||
|
|
44d79e2c9a | ||
|
|
0e52a99efa | ||
|
|
834ce8cf9e | ||
|
|
2354f0ba9f | ||
|
|
8e96c19126 | ||
|
|
99fb4c73f5 | ||
|
|
d11a674a3c | ||
|
|
31f70913a2 | ||
|
|
bd23faa28f | ||
|
|
5b1fbe1e2f | ||
|
|
96265b75de | ||
|
|
17298f0b2c | ||
|
|
61b475fa8d | ||
|
|
455084c17b | ||
|
|
c3c5b897f3 | ||
|
|
a1871a2982 | ||
|
|
87de017afd | ||
|
|
5a2c263af3 | ||
|
|
ddd2e704db | ||
|
|
9b5e0b0591 | ||
|
|
9b6636e21a | ||
|
|
a8358b8e7a | ||
|
|
5f091bb6ab | ||
|
|
17b334aaad | ||
|
|
c65209e6a9 | ||
|
|
d2518b11a7 | ||
|
|
70cdf3865a |
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[*.{js,jsx,ts,tsx,vue}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 100
|
||||||
291
.eslintrc.js
Normal file
291
.eslintrc.js
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
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}*`);
|
||||||
|
}
|
||||||
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Prevent Git from auto-converting to CRLF on Windows, and convert to LF on checkin.
|
||||||
|
# * : All files
|
||||||
|
# text=auto : If Git decides content it text, it converts CRLF to LF on checkin.
|
||||||
|
# eol=lf : forces Git to normalize line endings to LF on checkin and prevents conversion
|
||||||
|
# to CRLF when the file is checked out.
|
||||||
|
* text=auto eol=lf
|
||||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: undergroundwires
|
||||||
59
.github/workflows/checks.build.yaml
vendored
Normal file
59
.github/workflows/checks.build.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: build-checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-web:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
mode: [ development, test, production ]
|
||||||
|
fail-fast: false # Allows to see results from other combinations
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 15.x
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Build
|
||||||
|
run: npm run build -- --mode ${{ matrix.mode }}
|
||||||
|
|
||||||
|
# A new job is used due to environments/modes different from Vue CLI, https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
|
||||||
|
build-desktop:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
mode: [ development, production ] # "test" is not supported https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1627
|
||||||
|
fail-fast: false # Allows to see results from other combinations
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 15.x
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Install cross-env
|
||||||
|
# Used to set NODE_ENV due to https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
|
||||||
|
run: npm install --global cross-env
|
||||||
|
-
|
||||||
|
name: Build
|
||||||
|
run: |-
|
||||||
|
cross-env-shell NODE_ENV=${{ matrix.mode }}
|
||||||
|
npm run electron:build -- --publish never --mode ${{ matrix.mode }}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Quality checks
|
name: quality-checks
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
@@ -8,12 +8,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
lint-command:
|
lint-command:
|
||||||
- npm run lint:vue
|
- npm run lint:eslint
|
||||||
- npm run lint:yaml
|
- npm run lint:yaml
|
||||||
- npm run lint:md
|
- npm run lint:md
|
||||||
- npm run lint:md:relative-urls
|
- npm run lint:md:relative-urls
|
||||||
- npm run lint:md:consistency
|
- npm run lint:md:consistency
|
||||||
fail-fast: false # So it continues with other commands if one fails
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Security checks
|
name: security-checks
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy desktop
|
name: release-desktop
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Bump & release
|
name: release-git
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push: # Ensure a new release is created for each new tag
|
push: # Ensure a new release is created for each new tag
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy site
|
name: release-site
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
28
.github/workflows/tests.e2e.yaml
vendored
Normal file
28
.github/workflows/tests.e2e.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: e2e-tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos, ubuntu, windows]
|
||||||
|
fail-fast: false # So it still runs on other OSes if one of them fails
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
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: Run e2e tests
|
||||||
|
run: npm run test:e2e -- --headless
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
name: Test
|
name: integration-tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule: # for integration tests
|
schedule: # To get notified about problems from third party dependencies
|
||||||
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -25,9 +25,6 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
-
|
|
||||||
name: Run unit tests
|
|
||||||
run: npm run test:unit
|
|
||||||
-
|
-
|
||||||
name: Run integration tests
|
name: Run integration tests
|
||||||
run: npm run test:integration
|
run: npm run test:integration
|
||||||
28
.github/workflows/tests.unit.yaml
vendored
Normal file
28
.github/workflows/tests.unit.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: unit-tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos, ubuntu, windows]
|
||||||
|
fail-fast: false # So it still runs on other OSes if one of them fails
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
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: Run unit tests
|
||||||
|
run: npm run test:unit
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,6 +1,10 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist/
|
dist/
|
||||||
.vs
|
.vs
|
||||||
.vscode
|
.vscode/**/*
|
||||||
|
!.vscode/extensions.json
|
||||||
#Electron-builder output
|
#Electron-builder output
|
||||||
/dist_electron
|
/dist_electron
|
||||||
|
# Cypress
|
||||||
|
/tests/e2e/screenshots
|
||||||
|
/tests/e2e/videos
|
||||||
|
|||||||
23
.vscode/extensions.json
vendored
Normal file
23
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
// Common
|
||||||
|
"editorconfig.editorconfig", // Applies .editorconfig to follow project style.
|
||||||
|
"wengerk.highlight-bad-chars", // Highlights bad chars.
|
||||||
|
"wayou.vscode-todo-highlight", // Highlights TODO.
|
||||||
|
"wix.vscode-import-cost", // Shows in KB how much a require include in code.
|
||||||
|
// Documentation
|
||||||
|
"davidanson.vscode-markdownlint", // Lints markdown.
|
||||||
|
// TypeScript / JavaScript
|
||||||
|
"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.
|
||||||
|
// Scripting
|
||||||
|
"timonwong.shellcheck", // Lints bash files.
|
||||||
|
"ms-vscode.powershell", // Lints PowerShell files.
|
||||||
|
"ms-python.python", // Lints Python files.
|
||||||
|
// Distribution
|
||||||
|
"ms-azuretools.vscode-docker" // Adds Docker support.
|
||||||
|
]
|
||||||
|
}
|
||||||
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,5 +1,42 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.11.3 (2022-01-05)
|
||||||
|
|
||||||
|
* Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd)
|
||||||
|
* Fix OS desktop detection tests and edge cases | [a8358b8](https://github.com/undergroundwires/privacy.sexy/commit/a8358b8e7a93214f3d22a4488007ded5f623d845)
|
||||||
|
* Fix clearing Windows product key showing dialog | [9b6636e](https://github.com/undergroundwires/privacy.sexy/commit/9b6636e21a922a4750dc19f4854f8ae679187926)
|
||||||
|
* Document and unrecommend Cloud Experience Host | [9b5e0b0](https://github.com/undergroundwires/privacy.sexy/commit/9b5e0b0591fee56af52d83334a1f19180a49516f)
|
||||||
|
* Add initial e2e testing with cypress | [ddd2e70](https://github.com/undergroundwires/privacy.sexy/commit/ddd2e704dbd361cbd219f3dfe644b983ad254095)
|
||||||
|
* Restructure pipelines and badges | [5a2c263](https://github.com/undergroundwires/privacy.sexy/commit/5a2c263af35b8785e75ead6c43c3f17186dc15c8)
|
||||||
|
* Fix failing of functions without revert code | [87de017](https://github.com/undergroundwires/privacy.sexy/commit/87de017afd6e08acbd2deea150c6af9c7ee778fc)
|
||||||
|
* Fix typos in privacy modal #109 | [a1871a2](https://github.com/undergroundwires/privacy.sexy/commit/a1871a2982c9e3192193f836b97b1a6ccda5a2ab)
|
||||||
|
* Refactor to add readonly interfaces | [c3c5b89](https://github.com/undergroundwires/privacy.sexy/commit/c3c5b897f308f613c252182a02cdd4cfa7150fa3)
|
||||||
|
* Document and unrecommend AAD app removal #24, #54 | [455084c](https://github.com/undergroundwires/privacy.sexy/commit/455084c17b32d11d046515e8dc1447adf4bea4c3)
|
||||||
|
* Migrate from TSLint to ESLint | [61b475f](https://github.com/undergroundwires/privacy.sexy/commit/61b475fa8de433cdada2efa7eac197683aacd956)
|
||||||
|
* Add build checks and improve existing CI/CD checks | [17298f0](https://github.com/undergroundwires/privacy.sexy/commit/17298f0b2c51cb9becc0eb2ffe0d93d6a4c503a6)
|
||||||
|
* Upgrade to Vue CLI 5 (and webpack 5) | [96265b7](https://github.com/undergroundwires/privacy.sexy/commit/96265b75deafb85978b16460138fb4a814c07cfe)
|
||||||
|
* Refactor code to comply with ESLint rules | [5b1fbe1](https://github.com/undergroundwires/privacy.sexy/commit/5b1fbe1e2fb1354a5f060f8c8e3794ce756e16a7)
|
||||||
|
* Fix mutated line endings on Windows | [bd23faa](https://github.com/undergroundwires/privacy.sexy/commit/bd23faa28f6d781581a33d5b780f4b33f7e2cd8b)
|
||||||
|
* Refactor to improve iterations | [31f7091](https://github.com/undergroundwires/privacy.sexy/commit/31f70913a2f30baf5a9d6690f192e6a63da50114)
|
||||||
|
* win: unrecommend and document Live ID service #100 | [d11a674](https://github.com/undergroundwires/privacy.sexy/commit/d11a674a3c4ad8f4972a870c2f0977ac53297273)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.2...0.11.3)
|
||||||
|
|
||||||
|
## 0.11.2 (2021-12-03)
|
||||||
|
|
||||||
|
* Fix Windows TrustedInstaller session errors | [20a0071](https://github.com/undergroundwires/privacy.sexy/commit/20a0071c0d3d769a8f31218abdbfc4cafa25c6ff)
|
||||||
|
* Improve tests for `UserSelection` | [2f90cac](https://github.com/undergroundwires/privacy.sexy/commit/2f90cac52ab9e57615aeec41f9daa842bce770a5)
|
||||||
|
* Fix disabling/enabling Defender on Windows #104 | [2e08293](https://github.com/undergroundwires/privacy.sexy/commit/2e082932c952b0849ab2b8709ff0c75293b88e95)
|
||||||
|
* Refactor Saas naming, structure and modules | [bf83c58](https://github.com/undergroundwires/privacy.sexy/commit/bf83c58982ffa178facc6d35e50c7f1eac7ff236)
|
||||||
|
* Fix Defender features errors in Windows #104 | [d7761ab](https://github.com/undergroundwires/privacy.sexy/commit/d7761ab30e7f1e10a2919c196804d67511d6163a)
|
||||||
|
* Fix unintendedly inlined Windows scripts | [f2d9881](https://github.com/undergroundwires/privacy.sexy/commit/f2d988138257ff184884e4adc83c39e3bc247e9b)
|
||||||
|
* Fix Defender error due to non-english Windows #104 | [7c02ffb](https://github.com/undergroundwires/privacy.sexy/commit/7c02ffb6c95382b94f0b05e6f259cc418ec91c93)
|
||||||
|
* Improve and unify disabling of Windows services | [70cdf38](https://github.com/undergroundwires/privacy.sexy/commit/70cdf3865a0de3214fc9e26fbdada4b0cb413c46)
|
||||||
|
* Improve Windows defender docs and errors #104 | [d2518b1](https://github.com/undergroundwires/privacy.sexy/commit/d2518b11a7774ec58b9b46a691e2f013855bf0f9)
|
||||||
|
* Unrecommend and complete Windows Push Notif. #101 | [c65209e](https://github.com/undergroundwires/privacy.sexy/commit/c65209e6a99230f15ace8955e8d5a6f3333d146b)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.1...0.11.2)
|
||||||
|
|
||||||
## 0.11.1 (2021-11-04)
|
## 0.11.1 (2021-11-04)
|
||||||
|
|
||||||
* Update dependencies | [64631a4](https://github.com/undergroundwires/privacy.sexy/commit/64631a4552fad7f7b06286aba8d3ca2d731f9342)
|
* Update dependencies | [64631a4](https://github.com/undergroundwires/privacy.sexy/commit/64631a4552fad7f7b06286aba8d3ca2d731f9342)
|
||||||
|
|||||||
@@ -1,34 +1,51 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
- Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
|
Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
|
||||||
- Reporting a bug
|
|
||||||
- Discussing the current state of the code
|
- reporting a bug,
|
||||||
- Submitting a fix
|
- discussing the current state of the code,
|
||||||
- Proposing new features
|
- submitting a fix,
|
||||||
- Becoming a maintainer
|
- proposing new features,
|
||||||
|
- or becoming a maintainer.
|
||||||
|
|
||||||
|
As a small open source project with small community, it can sometimes take a long time to address the issues so please be patient.
|
||||||
|
|
||||||
## Pull request process
|
## Pull request process
|
||||||
|
|
||||||
- [GitHub flow](https://guides.github.com/introduction/flow/index.html) with [GitOps](./img/architecture/gitops.png) is used
|
Your pull requests are actively welcomed. We collaborate using [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow).
|
||||||
- Your pull requests are actively welcomed.
|
|
||||||
- The steps:
|
The steps:
|
||||||
1. Fork the repo and create your branch from master.
|
|
||||||
2. If you've added code that should be tested, add tests.
|
1. Fork the repo and create your branch from master.
|
||||||
3. If you've changed APIs, update the documentation.
|
2. If you've added code that requires testing, add tests. See [tests.md](./docs/tests.md).
|
||||||
4. Ensure the test suite passes.
|
3. If you've done a major change, update the documentation. See [docs/](./docs/).
|
||||||
5. Make sure your code lints.
|
4. Ensure the test suite passes. See [development.md | Testing](./docs/development.md#testing) for commands.
|
||||||
6. Issue that pull request!
|
5. Make sure your code lints.See [development.md | Linting](./docs/development.md#linting) for commands.
|
||||||
- 🙏 DO
|
6. Issue that pull request!
|
||||||
- Document your changes in the pull request
|
|
||||||
- ❗ DON'T
|
**🙏 DO:**
|
||||||
- Do not update the versions, current version is only [set by the maintainer](./img/architecture/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
|
|
||||||
|
- Document why (what you're trying to solve) rather than what in the pull request.
|
||||||
|
|
||||||
|
**❗ DON'T:**
|
||||||
|
|
||||||
|
- Do not update the versions, current version is [set by the maintainer](./docs/ci-cd.md#gitops) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
||||||
|
|
||||||
|
Automated pipelines will run to control your PR and they will publish your code once the maintainer merges your PR.
|
||||||
|
|
||||||
|
📖 You can read more in [ci-cd.md](./docs/ci-cd.md).
|
||||||
|
|
||||||
|
## Extend scripts
|
||||||
|
|
||||||
|
Here's quick information for you who want to add more scripts.
|
||||||
|
|
||||||
|
You have two alternatives:
|
||||||
|
|
||||||
|
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) and ask for someone else to add the script for you.
|
||||||
|
2. Or send a PR yourself. This would make it faster to get your code into the project. You need to add scripts to related OS in [collections](src/application/collections/) folder. Then you'd sent a pull request, see [pull request process](#pull-request-process).
|
||||||
|
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md).
|
||||||
|
- 📖 If you wish to use templates, use [templating.md](./docs/templating.md).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
By contributing, you agree that your contributions will be licensed under its [GNU General Public License v3.0](./LICENSE).
|
By contributing, you agree that your [GNU General Public License v3.0](./LICENSE) will be the license for your contributions.
|
||||||
|
|
||||||
## Read more
|
|
||||||
|
|
||||||
- See [tests](./docs/tests.md) for testing
|
|
||||||
- See [extend script](./README.md#extend-scripts) for quick steps to extend scripts
|
|
||||||
- See [architecture overview](./README.md#architecture-overview) to deep dive into privacy.sexy codebase
|
|
||||||
|
|||||||
189
README.md
189
README.md
@@ -2,87 +2,140 @@
|
|||||||
|
|
||||||
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
||||||
|
|
||||||
[](./CONTRIBUTING.md)
|
<!-- markdownlint-disable MD033 -->
|
||||||
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
<p align="center">
|
||||||
[](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
|
<a href="https://undergroundwires.dev/donate?project=privacy.sexy">
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
<img
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
alt="donation badge"
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
src="https://undergroundwires.dev/img/badges/donate/flat.svg"
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
/>
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
</a>
|
||||||
[](https://github.com/undergroundwires/bump-everywhere)
|
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md">
|
||||||
|
<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">
|
||||||
|
<img
|
||||||
|
alt="Maintainability"
|
||||||
|
src="https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Tests -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.unit.yaml">
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<img
|
||||||
|
alt="E2E tests status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/e2e-tests/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Checks -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml">
|
||||||
|
<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">
|
||||||
|
<img
|
||||||
|
alt="Security checks status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/security-checks/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml">
|
||||||
|
<img
|
||||||
|
alt="Build checks status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Release -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml">
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<img
|
||||||
|
alt="Desktop application release status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-desktop/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Others -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/bump-everywhere">
|
||||||
|
<img
|
||||||
|
alt="Auto-versioned by bump-everywhere"
|
||||||
|
src="https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 💡 No need to run any compiled software on your computer.
|
- 🖥️ **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).
|
||||||
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-Setup-0.11.1.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-0.11.1.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-0.11.1.AppImage).
|
|
||||||
- 💡 Single click to execute your script.
|
|
||||||
- ❗ Come back regularly to apply latest version for stronger privacy and security.
|
|
||||||
|
|
||||||
[](https://privacy.sexy)
|
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
||||||
|
|
||||||
## Why
|
💡 You should apply your configuration from time to time (more than once). It would strengthen your privacy and security control because privacy.sexy and its scripts get better and stronger in every new version.
|
||||||
|
|
||||||
- Rich tweak pool to harden security & privacy of the OS and other software on it
|
[](https://privacy.sexy)
|
||||||
- Free (both free as in beer and free as in speech)
|
|
||||||
- No need to run any compiled software that has access to your system, just run the generated scripts
|
|
||||||
- Have full visibility into what the tweaks do as you enable them
|
|
||||||
- Ability to revert (undo) applied scripts
|
|
||||||
- Everything is transparent: both application and its infrastructure are open-source and automated
|
|
||||||
- Easily extendable with [own powerful templating language](./docs/templating.md)
|
|
||||||
- Each script is independently executable without cross-dependencies
|
|
||||||
|
|
||||||
## Extend scripts
|
## Features
|
||||||
|
|
||||||
- You can either [create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose)
|
- **Rich**: Hundreds of scripts that aims to give you control of your data.
|
||||||
- Or send a PR:
|
- **Free**: Both free as in "beer" and free as in "speech".
|
||||||
1. Fork the repository
|
- **Transparent**. Have full visibility into what the tweaks do as you enable them.
|
||||||
2. Add more scripts in respective script collection in [collections](src/application/collections/) folder.
|
- **Reversible**. Revert if something feels wrong.
|
||||||
- 📖 If you're unsure about the syntax you can refer to the [collection files | documentation](docs/collection-files.md).
|
- **Accessible**. No need to run any compiled software on your computer with web version.
|
||||||
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
- **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).
|
||||||
3. Send a pull request 👌
|
- **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.
|
||||||
|
|
||||||
## Commands
|
## Support
|
||||||
|
|
||||||
- Project setup: `npm install`
|
**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).
|
||||||
- Testing
|
|
||||||
- Run unit tests: `npm run test:unit`
|
|
||||||
- Run integration tests: `npm run test:integration`
|
|
||||||
- Lint: `npm run lint`
|
|
||||||
- **Desktop app**
|
|
||||||
- Development: `npm run electron:serve`
|
|
||||||
- Production: `npm run electron:build` to build an executable
|
|
||||||
- **Webpage**
|
|
||||||
- Development: `npm run serve` to compile & hot-reload for development.
|
|
||||||
- Production: `npm run build` to prepare files for distribution.
|
|
||||||
- Or run using Docker:
|
|
||||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.11.1 .`
|
|
||||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.1 undergroundwires/privacy.sexy:0.11.1`
|
|
||||||
|
|
||||||
## Architecture overview
|
**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.
|
||||||
|
|
||||||
### Application
|
**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).
|
||||||
|
|
||||||
- Powered by **TypeScript**, **Vue.js** and **Electron** 💪
|
## Development
|
||||||
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
|
|
||||||
- Application uses highly decoupled models & services in different DDD layers.
|
|
||||||
- 📖 Read more on • [Presentation](./docs/presentation.md) • [Application](./docs/application.md)
|
|
||||||
|
|
||||||

|
Refer to [development.md](./docs/development.md) for Docker usage and reading more about setting up your development environment.
|
||||||
|
|
||||||
### AWS Infrastructure
|
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.
|
||||||
|
|
||||||
[](https://github.com/undergroundwires/aws-static-site-with-cd)
|
[docs/](./docs/) folder includes all other documentation.
|
||||||
|
|
||||||
- It uses infrastructure from the following repository: [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd)
|
|
||||||
- Runs on AWS 100% serverless and automatically provisioned using [GitHub Actions](.github/workflows/).
|
|
||||||
- Maximum security & automation and minimum AWS costs are the highest priorities of the design.
|
|
||||||
|
|
||||||
#### GitOps: CI/CD to AWS
|
|
||||||
|
|
||||||
- CI/CD is fully automated for this repo using different GIT events & GitHub actions.
|
|
||||||
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
|
|
||||||
- Everything that's merged in the master goes directly to production.
|
|
||||||
|
|
||||||
[](.github/workflows/)
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
'@vue/cli-plugin-babel/preset'
|
'@vue/cli-plugin-babel/preset',
|
||||||
]
|
],
|
||||||
}
|
};
|
||||||
|
|||||||
3
cypress.json
Normal file
3
cypress.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"pluginsFile": "tests/e2e/plugins/index.js"
|
||||||
|
}
|
||||||
@@ -1,44 +1,45 @@
|
|||||||
# Application
|
# Application
|
||||||
|
|
||||||
- It's mainly responsible for
|
Application layer is mainly responsible for:
|
||||||
- creating and event based [application state](#application-state)
|
|
||||||
- [parsing](#parsing) and [compiling](#compiling) [application data](#application-data)
|
- creating an event-based and mutable [application state](#application-state),
|
||||||
- Consumed by [presentation layer](./presentation.md)
|
- [parsing and compiling](#parsing-and-compiling) the [application data](#application-data).
|
||||||
|
|
||||||
|
📖 Refer to [architecture.md | Layered Application](./architecture.md#layered-application) to read more about the layered architecture.
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- [`/src/` **`application/`**](./../src/application/): Contains all application related code.
|
Application layer code exists in [`/src/application`](./../src/application/) and includes following structure:
|
||||||
- [**`collections/`**](./../src/application/collections/): Holds [collection files](./collection-files.md)
|
|
||||||
- [**`Common/`**](./../src/application/Common/): Contains common functionality that is shared in application layer.
|
- [**`collections/`**](./../src/application/collections/): Holds [collection files](./collection-files.md).
|
||||||
- `..`: other classes are categorized using folders-by-feature structure
|
- [**`Common/`**](./../src/application/Common/): Contains common functionality in application layer.
|
||||||
|
- `...`: rest of the application layer source code organized using folders-by-feature structure.
|
||||||
|
|
||||||
## Application state
|
## Application state
|
||||||
|
|
||||||
- [ApplicationContext.ts](./../src/application/Context/ApplicationContext.ts) holds the [CategoryCollectionState](./../src/application/Context/State/CategoryCollectionState.ts) for each OS
|
It uses [state pattern](https://en.wikipedia.org/wiki/State_pattern) with context and state objects. [`ApplicationContext.ts`](./../src/application/Context/ApplicationContext.ts) the "Context" of state pattern provides an instance of [`CategoryCollectionState.ts`](./../src/application/Context/State/CategoryCollectionState.ts) (the "State" of the state pattern) for every supported collection.
|
||||||
- Uses [state pattern](https://en.wikipedia.org/wiki/State_pattern)
|
|
||||||
- Same instance is shared throughout the application to ensure consistent state
|
Presentation layer uses a singleton (same instance of) [`ApplicationContext.ts`](./../src/application/Context/ApplicationContext.ts) throughout the application to ensure consistent state.
|
||||||
- 📖 See [Application State | Presentation layer](./presentation.md#application-state) to read more about how the state should be managed by the presentation layer.
|
|
||||||
- 📖 See [ApplicationContext.ts](./../src/application/Context/ApplicationContext.ts) to start diving into the state code.
|
📖 Refer to [architecture.md | Application State](./architecture.md#application-state) to get an overview of event handling and [presentation.md | Application State](./presentation.md#application-state) for deeper look into how the presentation layer manages state.
|
||||||
|
|
||||||
## Application data
|
## Application data
|
||||||
|
|
||||||
- Compiled to [`Application`](./../src/domain/Application.ts) domain object.
|
Application data is collection files using YAML. You can refer to [collection-files.md](./collection-files.md) to read more about the scheme and structure of application data files. You can also check the source code [collection yaml files](./../src/application/collections/) to directly see the application data using that scheme.
|
||||||
- The scripts are defined and controlled in different data files per OS
|
|
||||||
- Enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) and easier contributions
|
|
||||||
- Application data is defined in collection files and
|
|
||||||
- 📖 See [Application data | Presentation layer](./presentation.md#application-data) to read how the application data is read by the presentation layer.
|
|
||||||
- 📖 See [collection files documentation](./collection-files.md) to read more about how the data files are structured/defined and see [collection yaml files](./../src/application/collections/) to directly check the code.
|
|
||||||
|
|
||||||
## Parsing
|
Application layer [parses and compiles](#parsing-and-compiling) application data into [`Application`](./../src/domain/Application.ts)). Once parsed, application layer provides the necessary functionality to presentation layer based on the application data. You can read more about how presentation layer consumes the application data in [presentation.md | Application Data](./presentation.md#application-data).
|
||||||
|
|
||||||
- Application data is parsed to domain object [`Application.ts`](./../src/domain/Application.ts)
|
Application layer enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) by leveraging the data to the rest of the source code. It makes it easy for community to contribute on the project by using a declarative language used in collection files.
|
||||||
- Steps
|
|
||||||
1. (Compile time) Load application data from [collection yaml files](./../src/application/collections/) using webpack loader
|
|
||||||
2. (Runtime) Parse and compile application and make it available to presentation layer by [`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)
|
|
||||||
|
|
||||||
### Compiling
|
### Parsing and compiling
|
||||||
|
|
||||||
- Parsing the application files includes compiling scripts using [collection file defined functions](./collection-files.md#function)
|
Application layer parses the application data to compile the domain object [`Application.ts`](./../src/domain/Application.ts).
|
||||||
- To extend the syntax:
|
|
||||||
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
|
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.
|
||||||
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts)
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
The steps to extend the templating syntax:
|
||||||
|
|
||||||
|
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
|
||||||
|
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).
|
||||||
|
|||||||
66
docs/architecture.md
Normal file
66
docs/architecture.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Architecture overview
|
||||||
|
|
||||||
|
This repository consists of:
|
||||||
|
|
||||||
|
- A [layered application](#layered-application).
|
||||||
|
- [AWS infrastructure](#aws-infrastructure) as code and instructions to host the website.
|
||||||
|
- [GitOps](#gitops) practices for development, maintenance and deployment.
|
||||||
|
|
||||||
|
## Layered application
|
||||||
|
|
||||||
|
Application is
|
||||||
|
|
||||||
|
- powered by **TypeScript**, **Vue.js** and **Electron** 💪,
|
||||||
|
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
|
||||||
|
|
||||||
|
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 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Application state
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Each layer treat application layer differently.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*[Presentation layer](./presentation.md)*:
|
||||||
|
|
||||||
|
- Each component holds their own state about presentation-related data.
|
||||||
|
- Components register shared state changes into application state using functions.
|
||||||
|
- Components listen to shared state changes using event subscriptions.
|
||||||
|
- 📖 Read more: [presentation.md | Application state](./presentation.md#application-state).
|
||||||
|
|
||||||
|
*[Application layer](./application.md)*:
|
||||||
|
|
||||||
|
- Stores the application-specific state.
|
||||||
|
- The state it exposed for read with getter functions and set using setter functions, setter functions also fire application events that allows other parts of application and the view in presentation layer to react.
|
||||||
|
- 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).
|
||||||
|
|
||||||
|
## AWS infrastructure
|
||||||
|
|
||||||
|
The web-site runs on serverless AWS infrastructure. Infrastructure is open-source and deployed as code. [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd) project includes the source code.
|
||||||
|
|
||||||
|
[](https://github.com/undergroundwires/aws-static-site-with-cd)
|
||||||
|
|
||||||
|
The design priorities highest security then minimizing cloud infrastructure costs.
|
||||||
|
|
||||||
|
This project includes [GitHub Actions](../.github/workflows/) to automatically provision the infrastructure with zero-touch and without any "hidden" steps, ensuring everything is open-source and transparent. Git repositories includes all necessary instructions and automation with [GitOps](#gitops) practices.
|
||||||
|
|
||||||
|
## GitOps
|
||||||
|
|
||||||
|
CI/CD pipelines automate operational tasks based on different Git events. [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) enables this automation.
|
||||||
|
|
||||||
|
📖 Read more in [`ci-cd.md`](./ci-cd.md#gitops).
|
||||||
|
|
||||||
|
[](../.github/workflows/)
|
||||||
45
docs/ci-cd.md
Normal file
45
docs/ci-cd.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# CI/CD overview
|
||||||
|
|
||||||
|
## GitOps
|
||||||
|
|
||||||
|
CI/CD is fully automated using different Git events and GitHub actions. This repository uses [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) to automate versioning, tagging, creation of `CHANGELOG.md` and GitHub releases. A dedicated workflow [release.desktop.yaml](./../.github/workflows/release.desktop.yaml) creates desktop installers and executables and attaches them into GitHub releases.
|
||||||
|
|
||||||
|
Everything that's merged in the master goes directly to production.
|
||||||
|
|
||||||
|
[](../.github/workflows/)
|
||||||
|
|
||||||
|
## Pipeline files
|
||||||
|
|
||||||
|
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] .
|
||||||
|
|
||||||
|
[1]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows
|
||||||
|
|
||||||
|
## Pipeline types
|
||||||
|
|
||||||
|
We categorize pipelines into different categories. We use these names in convention when naming files and actions, see [naming conventions](#naming-conventions).
|
||||||
|
|
||||||
|
The categories consist of:
|
||||||
|
|
||||||
|
- `tests`: Different types of tests to verify functionality.
|
||||||
|
- `checks`: Other controls such as vulnerability scans or styling checks.
|
||||||
|
- `release`: Pipelines used for release of deployment such as building and testing.
|
||||||
|
|
||||||
|
## Naming conventions
|
||||||
|
|
||||||
|
Convention for naming pipeline files: **`<type>.<name>.yaml`**.
|
||||||
|
|
||||||
|
**`type`**:
|
||||||
|
|
||||||
|
- Sub-folders do not work for GitHub workflows [1] so we use `<type>.` prefix to organize them.
|
||||||
|
- See also [pipeline types](#pipeline-types) for list of all usable types.
|
||||||
|
|
||||||
|
**`name`**:
|
||||||
|
|
||||||
|
- We name workflows using kebab-case.
|
||||||
|
- E.g. file name `tests.unit.yaml`, pipeline file should set the naem as: `name: unit-tests`.
|
||||||
|
- Kebab-case allows to have better URL references to them.
|
||||||
|
- [README.md](./../README.md) uses URL references to show status badges for actions.
|
||||||
|
|
||||||
|
[1]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows
|
||||||
53
docs/development.md
Normal file
53
docs/development.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Development
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Install node >15.x.
|
||||||
|
- Install dependencies using `npm install`.
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
📖 Read more about testing in [tests](./tests.md).
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
- Lint all (recommended 💡): `npm run lint`
|
||||||
|
- Markdown: `npm run lint:md`
|
||||||
|
- Markdown consistency `npm run lint:md:consistency`
|
||||||
|
- Markdown relative URLs: `npm run lint:md:relative-urls`
|
||||||
|
- JavaScript/TypeScript: `npm run lint:eslint`
|
||||||
|
- Yaml: `npm run lint:yaml`
|
||||||
|
|
||||||
|
### Running
|
||||||
|
|
||||||
|
- Run in local server: `npm run serve`
|
||||||
|
- 💡 Meant for local development with features such as hot-reloading.
|
||||||
|
- Run using Docker:
|
||||||
|
1. Build: `docker build -t undergroundwires/privacy.sexy:latest .`
|
||||||
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest`
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
- Build web application: `npm run build`
|
||||||
|
- Build desktop application: `npm run electron:build`
|
||||||
|
|
||||||
|
## Recommended extensions
|
||||||
|
|
||||||
|
You should use EditorConfig to follow project style.
|
||||||
|
|
||||||
|
For Visual Studio Code, [`.vscode/extensions.json`](./../.vscode/extensions.json) includes list of recommended extensions.
|
||||||
@@ -1,53 +1,63 @@
|
|||||||
# Presentation layer
|
# Presentation layer
|
||||||
|
|
||||||
- Consists of Vue.js components and other UI-related code.
|
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.
|
||||||
- Desktop application is created using [Electron](https://www.electronjs.org/).
|
|
||||||
- Event driven as in components simply listens to events from the state and act accordingly.
|
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.
|
||||||
|
|
||||||
|
📖 Refer to [architecture.md (Layered Application)](./architecture.md#layered-application) to read more about the layered architecture.
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- [`/src/` **`presentation/`**](./../src/presentation/): Contains all presentation related code including Vue and Electron configurations
|
- [`/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.
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
|
||||||
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
|
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
|
||||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components.
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that other components share.
|
||||||
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that will be processed by webpack.
|
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that webpack will process.
|
||||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
|
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
|
||||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
|
||||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles that are reusable and tightly coupled a Vue/HTML component.
|
- [**`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.
|
- [**`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 only file used from other components.
|
- [**`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.
|
- [**`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.
|
- [**`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.
|
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
|
||||||
- [**`/public/`**](./../public/): Contains static assets that will directly be copied and not go through webpack.
|
- [**`/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`
|
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`.
|
||||||
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally
|
- [**`/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`
|
- [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`.
|
||||||
|
|
||||||
## Application data
|
## Application data
|
||||||
|
|
||||||
- Components and should use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain.
|
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.
|
||||||
- [Application.ts](../src/domain/Application.ts) domain model is the stateless application representation including
|
|
||||||
- available scripts, collections as defined in [collection files](./collection-files.md)
|
[Application.ts](../src/domain/Application.ts) is an immutable domain model that represents application state. It includes:
|
||||||
- package information as defined in [`package.json`](./../package.json)
|
|
||||||
- 📖 See [Application data | Application layer](./presentation.md#application-data) where application data is parsed and compiled.
|
- available scripts, collections as defined in [collection files](./collection-files.md),
|
||||||
|
- package information as defined in [`package.json`](./../package.json).
|
||||||
|
|
||||||
|
You can read more about how application layer provides application data to he presentation in [application.md | Application data](./application.md#application-data).
|
||||||
|
|
||||||
## Application state
|
## Application state
|
||||||
|
|
||||||
- Stateful components mutate or/and react to state changes in [ApplicationContext](./../src/application/Context/ApplicationContext.ts).
|
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.
|
||||||
- Stateless components that does not handle state extends `Vue`
|
|
||||||
- Stateful components that depends on the collection state such as user selection, search queries and more extends [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts)
|
[`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) functions include:
|
||||||
- The single source of truth is a singleton of the state created and made available to presentation layer by [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts)
|
|
||||||
- `StatefulVue` includes abstract `handleCollectionState` that is fired once the component is loaded and also each time [collection](./collection-files.md) is changed.
|
- Creating a singleton of the state and makes it available to presentation layer as single source of truth.
|
||||||
- Do not forget to subscribe from events when component is destroyed or if needed [collection](./collection-files.md) is changed.
|
- Providing virtual abstract `handleCollectionState` callback that it calls when
|
||||||
- 💡 `events` in base class [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) makes lifecycling easier
|
- the Vue loads the component,
|
||||||
- 📖 See [Application state | Application layer](./presentation.md#application-state) where the state is implemented using using state pattern.
|
- 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.
|
||||||
|
|
||||||
|
📖 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.
|
||||||
|
|
||||||
## Modals
|
## Modals
|
||||||
|
|
||||||
- [Dialog.vue](./../src/presentation/components/Shared/Dialog.vue) is a shared component that can be used to show modal windows
|
[Dialog.vue](./../src/presentation/components/Shared/Dialog.vue) is a shared component that other components used to show modal windows.
|
||||||
- Simply wrap the content inside of its slot and call `.show()` method on its reference.
|
|
||||||
- Example:
|
You can use it by wrapping the content inside of its `slot` and call `.show()` function on its reference. For example:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<Dialog ref="testDialog">
|
<Dialog ref="testDialog">
|
||||||
@@ -58,15 +68,15 @@
|
|||||||
|
|
||||||
## Sass naming convention
|
## Sass naming convention
|
||||||
|
|
||||||
- Use lowercase for variables/functions/mixins e.g.
|
- Use lowercase for variables/functions/mixins, e.g.:
|
||||||
- Variable: `$variable: value;`
|
- Variable: `$variable: value;`
|
||||||
- Function: `@function function() {}`
|
- Function: `@function function() {}`
|
||||||
- Mixin: `@mixin mixin() {}`
|
- Mixin: `@mixin mixin() {}`
|
||||||
- Use - for a phrase/compound word e.g.
|
- Use - for a phrase/compound word, e.g.:
|
||||||
- Variable: `$some-variable: value;`
|
- Variable: `$some-variable: value;`
|
||||||
- Function: `@function some-function() {}`
|
- Function: `@function some-function() {}`
|
||||||
- Mixin: `@mixin some-mixin() {}`
|
- Mixin: `@mixin some-mixin() {}`
|
||||||
- Grouping and name variables from generic to specific e.g.
|
- Grouping and name variables from generic to specific, e.g.:
|
||||||
- ✅ `$border-blue`, `$border-blue-light`, `$border-blue-lightest`, `$border-red`
|
- ✅ `$border-blue`, `$border-blue-light`, `$border-blue-lightest`, `$border-red`
|
||||||
- ❌ `$blue-border`, `$light-blue-border`, `$lightest-blue-border`, `$red-border`
|
- ❌ `$blue-border`, `$light-blue-border`, `$lightest-blue-border`, `$red-border`
|
||||||
|
|
||||||
@@ -3,14 +3,15 @@
|
|||||||
## Benefits of templating
|
## Benefits of templating
|
||||||
|
|
||||||
- Generating scripts by sharing code to increase best-practice usage and maintainability.
|
- Generating scripts by sharing code to increase best-practice usage and maintainability.
|
||||||
- Creating self-contained scripts without depending on each other that can be easily shared.
|
- Creating self-contained scripts without cross-dependencies.
|
||||||
- Use of pipes for writing cleaner code and letting pipes do dirty work.
|
- Use of pipes for writing cleaner code and letting pipes do dirty work.
|
||||||
|
|
||||||
## Expressions
|
## Expressions
|
||||||
|
|
||||||
- Expressions in the language are defined inside mustaches (double brackets, `{{` and `}}`).
|
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
|
||||||
- Expression syntax is inspired mainly by [Go Templates](https://pkg.go.dev/text/template).
|
- E.g. `Hello {{ $name }} !`
|
||||||
- Expressions are used in and enabled by functions where they can be used.
|
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) that has inspired this templating language.
|
||||||
|
- Functions enables usage of expressions.
|
||||||
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
- 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).
|
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
||||||
|
|
||||||
@@ -55,34 +56,36 @@ A function can call other functions such as:
|
|||||||
|
|
||||||
### with
|
### with
|
||||||
|
|
||||||
- Skips the block if the variable is absent or empty.
|
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 }}`.
|
||||||
- Binds its context (`.`) value of provided argument for the parameter if provided one.
|
|
||||||
- A block is defined as `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
|
|
||||||
- The parameters used for `with` condition should be declared as optional, otherwise `with` block becomes redundant.
|
|
||||||
- Example:
|
|
||||||
|
|
||||||
```yaml
|
Binds its context (`.`) value of provided argument for the parameter if provided one. E.g. `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
|
||||||
function: FunctionThatOutputsConditionally
|
|
||||||
parameters:
|
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
||||||
- name: 'argument'
|
|
||||||
optional: true
|
Example:
|
||||||
code: |-
|
|
||||||
{{ with $argument }}
|
```yaml
|
||||||
Value is: {{ . }}
|
function: FunctionThatOutputsConditionally
|
||||||
{{ end }}
|
parameters:
|
||||||
```
|
- name: 'argument'
|
||||||
|
optional: true
|
||||||
|
code: |-
|
||||||
|
{{ with $argument }}
|
||||||
|
Value is: {{ . }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
### Pipes
|
### Pipes
|
||||||
|
|
||||||
- Pipes are set of functions available for handling text in privacy.sexy.
|
- Pipes are functions available for handling text.
|
||||||
- Allows stacking actions one after another also known as "chaining".
|
- Allows stacking actions one after another also known as "chaining".
|
||||||
- Just like [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), the concept is simple: each pipeline's output becomes the input of the following pipe.
|
- Like [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), the concept is simple: each pipeline's output becomes the input of the following pipe.
|
||||||
- Pipes are provided and defined by the compiler and consumed by collection files.
|
- You cannot create pipes. [A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
|
||||||
- Pipes can be combined with [parameter substitution](#parameter-substitution) and [with](#with).
|
- You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
|
||||||
- ❗ Pipe names must be camelCase without any space or special characters.
|
- ❗ Pipe names must be camelCase without any space or special characters.
|
||||||
- **Existing pipes**
|
- **Existing pipes**
|
||||||
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
|
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
|
||||||
- `escapeDoubleQuotes`: Escapes `"` characters to be used inside double quotes (`"`)
|
- `escapeDoubleQuotes`: Escapes `"` characters, allows you to use them inside double quotes (`"`).
|
||||||
- **Example usages**
|
- **Example usages**
|
||||||
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
|
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
|
||||||
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
|
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
|
||||||
|
|||||||
@@ -1,43 +1,81 @@
|
|||||||
# Tests
|
# Tests
|
||||||
|
|
||||||
- There are two different types of tests executed:
|
There are different types of tests executed:
|
||||||
1. [Unit tests](#unit-tests)
|
|
||||||
2. [Integration tests](#integration-tests)
|
1. [Unit tests](#unit-tests)
|
||||||
- 💡 You can use path/module alias `@/tests` in import statements.
|
2. [Integration tests](#integration-tests)
|
||||||
|
3. [End-to-end (E2E) tests](#e2e-tests)
|
||||||
|
|
||||||
|
Common aspects for all 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
|
||||||
|
|
||||||
- Tests each component in isolation
|
- Unit tests test each component in isolation.
|
||||||
- Defined in [`./tests/unit`](./../tests/unit)
|
- All unit tests goes under [`./tests/unit`](./../tests/unit).
|
||||||
- They follow same folder structure as [`./src`](./../src)
|
- They rely on [stubs](./../tests/unit/shared/Stubs) for isolation.
|
||||||
|
|
||||||
### Naming
|
### Unit tests structure
|
||||||
|
|
||||||
- Each test suite first describe the system under test
|
- [`./src/`](./../src/)
|
||||||
- E.g. tests for class `Application` is categorized under `Application`
|
- Includes source code that unit tests will test.
|
||||||
- Tests for specific methods are categorized under method name (if applicable)
|
- [`./tests/unit/`](./../tests/unit/)
|
||||||
- E.g. test for `run()` is categorized under `run`
|
- 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', () => ..)`.
|
||||||
|
|
||||||
### Act, arrange, assert
|
### Act, arrange, assert
|
||||||
|
|
||||||
- Tests use act, arrange and assert (AAA) pattern when applicable
|
- Tests use act, arrange and assert (AAA) pattern when applicable.
|
||||||
- **Arrange**
|
- **Arrange**
|
||||||
- Should set up the test case
|
- Sets up the test case.
|
||||||
- Starts with comment line `// arrange`
|
- Starts with comment line `// arrange`.
|
||||||
- **Act**
|
- **Act**
|
||||||
- Should cover the main thing to be tested
|
- Executes the actual test.
|
||||||
- Starts with comment line `// act`
|
- Starts with comment line `// act`.
|
||||||
- **Assert**
|
- **Assert**
|
||||||
- Should elicit some sort of response
|
- Elicit some sort of expectation.
|
||||||
- Starts with comment line `// assert`
|
- Starts with comment line `// assert`.
|
||||||
|
|
||||||
### Stubs
|
|
||||||
|
|
||||||
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs)
|
|
||||||
- They implement dummy behavior to be functional
|
|
||||||
|
|
||||||
## Integration tests
|
## Integration tests
|
||||||
|
|
||||||
- Tests functionality of a component in combination with others (not isolated)
|
- Tests functionality of a component in combination with others (not isolated).
|
||||||
- Ensure dependencies to third parties work as expected
|
- Ensure dependencies to third parties work as expected.
|
||||||
- Defined in [`./tests/integration`](./../tests/integration)
|
- Defined in [./tests/integration](./../tests/integration).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|||||||
1
img/architecture/app-state.drawio
Normal file
1
img/architecture/app-state.drawio
Normal file
File diff suppressed because one or more lines are too long
BIN
img/architecture/app-state.png
Normal file
BIN
img/architecture/app-state.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 579 KiB After Width: | Height: | Size: 255 KiB |
36915
package-lock.json
generated
36915
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.11.1",
|
"version": "0.11.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
@@ -8,17 +8,18 @@
|
|||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"test:unit": "vue-cli-service test:unit",
|
||||||
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\"",
|
"test:e2e": "vue-cli-service test:e2e",
|
||||||
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
|
"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:build": "vue-cli-service electron:build",
|
||||||
"electron:serve": "vue-cli-service electron:serve",
|
"electron:serve": "vue-cli-service electron:serve",
|
||||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||||
"lint:vue": "vue-cli-service lint --no-fix",
|
"lint:eslint": "vue-cli-service lint --no-fix --mode production",
|
||||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstall": "electron-builder install-app-deps",
|
||||||
|
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\""
|
||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "background.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -47,31 +48,46 @@
|
|||||||
"@types/chai": "^4.2.22",
|
"@types/chai": "^4.2.22",
|
||||||
"@types/file-saver": "^2.0.3",
|
"@types/file-saver": "^2.0.3",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^9.0.0",
|
||||||
"@vue/cli-plugin-babel": "^4.5.14",
|
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||||
"@vue/cli-plugin-typescript": "^4.5.14",
|
"@typescript-eslint/parser": "^5.4.0",
|
||||||
"@vue/cli-plugin-unit-mocha": "^4.5.14",
|
"@vue/cli-plugin-babel": "~5.0.0-rc.1",
|
||||||
"@vue/cli-service": "^4.5.14",
|
"@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",
|
"@vue/test-utils": "1.2.2",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
|
"cypress": "^8.3.0",
|
||||||
"electron": "^15.3.0",
|
"electron": "^15.3.0",
|
||||||
|
"electron-builder": "^22.14.13",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-log": "^4.4.1",
|
"electron-log": "^4.4.1",
|
||||||
"electron-updater": "^4.3.9",
|
"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",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"markdownlint-cli": "^0.29.0",
|
"markdownlint-cli": "^0.29.0",
|
||||||
"raw-loader": "^4.0.2",
|
|
||||||
"remark-cli": "^10.0.0",
|
"remark-cli": "^10.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^5.1.0",
|
"remark-preset-lint-consistent": "^5.1.0",
|
||||||
"remark-validate-links": "^11.0.1",
|
"remark-validate-links": "^11.0.1",
|
||||||
"sass": "^1.43.3",
|
"sass": "^1.43.3",
|
||||||
"sass-loader": "10.2.0",
|
"sass-loader": "10.2.0",
|
||||||
|
"ts-loader": "9.0.1",
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"typescript": "^4.4.4",
|
"typescript": "^4.4.4",
|
||||||
"vue-cli-plugin-electron-builder": "^2.1.1",
|
"vue-cli-plugin-electron-builder": "^2.1.1",
|
||||||
"vue-template-compiler": "^2.6.14",
|
"vue-template-compiler": "^2.6.14",
|
||||||
"yaml-lint": "^1.2.4"
|
"yaml-lint": "^1.2.4"
|
||||||
},
|
},
|
||||||
|
"//devDependencies": {
|
||||||
|
"ts-loader": "Here as workaround for vue-cli-plugin-electron-builder using older webpack 4"
|
||||||
|
},
|
||||||
"homepage": "https://privacy.sexy",
|
"homepage": "https://privacy.sexy",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
autoprefixer: {}
|
autoprefixer: {},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -3,19 +3,22 @@ import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
|||||||
import { IApplicationFactory } from './IApplicationFactory';
|
import { IApplicationFactory } from './IApplicationFactory';
|
||||||
import { parseApplication } from './Parser/ApplicationParser';
|
import { parseApplication } from './Parser/ApplicationParser';
|
||||||
|
|
||||||
export type ApplicationGetter = () => IApplication;
|
export type ApplicationGetterType = () => IApplication;
|
||||||
const ApplicationGetter: ApplicationGetter = parseApplication;
|
const ApplicationGetter: ApplicationGetterType = parseApplication;
|
||||||
|
|
||||||
export class ApplicationFactory implements IApplicationFactory {
|
export class ApplicationFactory implements IApplicationFactory {
|
||||||
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
||||||
private readonly getter: AsyncLazy<IApplication>;
|
|
||||||
protected constructor(costlyGetter: ApplicationGetter) {
|
private readonly getter: AsyncLazy<IApplication>;
|
||||||
if (!costlyGetter) {
|
|
||||||
throw new Error('undefined getter');
|
protected constructor(costlyGetter: ApplicationGetterType) {
|
||||||
}
|
if (!costlyGetter) {
|
||||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
throw new Error('missing getter');
|
||||||
}
|
|
||||||
public getApp(): Promise<IApplication> {
|
|
||||||
return this.getter.getValue();
|
|
||||||
}
|
}
|
||||||
|
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getApp(): Promise<IApplication> {
|
||||||
|
return this.getter.getValue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
// Compares to Array<T> objects for equality, ignoring order
|
// Compares to Array<T> objects for equality, ignoring order
|
||||||
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('undefined first array'); }
|
if (!array1) { throw new Error('missing first array'); }
|
||||||
if (!array2) { throw new Error('undefined second array'); }
|
if (!array2) { throw new Error('missing second array'); }
|
||||||
const sortedArray1 = sort(array1);
|
const sortedArray1 = sort(array1);
|
||||||
const sortedArray2 = sort(array2);
|
const sortedArray2 = sort(array2);
|
||||||
return sequenceEqual(sortedArray1, sortedArray2);
|
return sequenceEqual(sortedArray1, sortedArray2);
|
||||||
function sort(array: readonly T[]) {
|
function sort(array: readonly T[]) {
|
||||||
return array.slice().sort();
|
return array.slice().sort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compares to Array<T> objects for equality in same order
|
// Compares to Array<T> objects for equality in same order
|
||||||
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('undefined first array'); }
|
if (!array1) { throw new Error('missing first array'); }
|
||||||
if (!array2) { throw new Error('undefined second array'); }
|
if (!array2) { throw new Error('missing second array'); }
|
||||||
if (array1.length !== array2.length) {
|
if (array1.length !== array2.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return array1.every((val, index) => val === array2[index]);
|
return array1.every((val, index) => val === array2[index]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,63 @@
|
|||||||
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
||||||
export type EnumType = number | string;
|
export type EnumType = number | string;
|
||||||
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
|
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
||||||
|
= { [key in T]: TEnumValue };
|
||||||
|
|
||||||
export interface IEnumParser<TEnum> {
|
export interface IEnumParser<TEnum> {
|
||||||
parseEnum(value: string, propertyName: string): TEnum;
|
parseEnum(value: string, propertyName: string): TEnum;
|
||||||
}
|
|
||||||
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> {
|
|
||||||
return {
|
|
||||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
|
||||||
value: string,
|
|
||||||
enumName: string,
|
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue {
|
|
||||||
if (!value) {
|
|
||||||
throw new Error(`undefined ${enumName}`);
|
|
||||||
}
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
|
|
||||||
}
|
|
||||||
const casedValue = getEnumNames(enumVariable)
|
|
||||||
.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
|
|
||||||
if (!casedValue) {
|
|
||||||
throw new Error(`unknown ${enumName}: "${value}"`);
|
|
||||||
}
|
|
||||||
return enumVariable[casedValue as keyof typeof enumVariable];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
|
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): string[] {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
return Object
|
): IEnumParser<TEnumValue> {
|
||||||
.values(enumVariable)
|
return {
|
||||||
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
value: string,
|
||||||
|
enumName: string,
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
): TEnumValue {
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`missing ${enumName}`);
|
||||||
|
}
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
|
||||||
|
}
|
||||||
|
const casedValue = getEnumNames(enumVariable)
|
||||||
|
.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
|
||||||
|
if (!casedValue) {
|
||||||
|
throw new Error(`unknown ${enumName}: "${value}"`);
|
||||||
|
}
|
||||||
|
return enumVariable[casedValue as keyof typeof enumVariable];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEnumNames
|
||||||
|
<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
): string[] {
|
||||||
|
return Object
|
||||||
|
.values(enumVariable)
|
||||||
|
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
return getEnumNames(enumVariable)
|
): TEnumValue[] {
|
||||||
.map((level) => enumVariable[level]) as TEnumValue[];
|
return getEnumNames(enumVariable)
|
||||||
|
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
value: TEnumValue,
|
value: TEnumValue,
|
||||||
enumVariable: EnumVariable<T, TEnumValue>) {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
if (value === undefined) {
|
) {
|
||||||
throw new Error('undefined enum value');
|
if (value === undefined || value === null) {
|
||||||
}
|
throw new Error('absent enum value');
|
||||||
if (!(value in enumVariable)) {
|
}
|
||||||
throw new RangeError(`enum value "${value}" is out of range`);
|
if (!(value in enumVariable)) {
|
||||||
}
|
throw new RangeError(`enum value "${value}" is out of range`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
|
||||||
export interface IScriptingLanguageFactory<T> {
|
export interface IScriptingLanguageFactory<T> {
|
||||||
create(language: ScriptingLanguage): T;
|
create(language: ScriptingLanguage): T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
|
||||||
import { assertInRange } from '@/application/Common/Enum';
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
|
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
||||||
|
|
||||||
type Getter<T> = () => T;
|
type Getter<T> = () => T;
|
||||||
|
|
||||||
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<T> {
|
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<T> {
|
||||||
private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
|
private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
|
||||||
|
|
||||||
public create(language: ScriptingLanguage): T {
|
public create(language: ScriptingLanguage): T {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!this.getters.has(language)) {
|
if (!this.getters.has(language)) {
|
||||||
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||||
}
|
|
||||||
const getter = this.getters.get(language);
|
|
||||||
const instance = getter();
|
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
const getter = this.getters.get(language);
|
||||||
|
const instance = getter();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!getter) {
|
if (!getter) {
|
||||||
throw new Error('undefined getter');
|
throw new Error('missing getter');
|
||||||
}
|
|
||||||
if (this.getters.has(language)) {
|
|
||||||
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
|
||||||
}
|
|
||||||
this.getters.set(language, getter);
|
|
||||||
}
|
}
|
||||||
|
if (this.getters.has(language)) {
|
||||||
|
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
||||||
|
}
|
||||||
|
this.getters.set(language, getter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,64 @@
|
|||||||
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
|
||||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
|
||||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { assertInRange } from '@/application/Common/Enum';
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
|
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||||
|
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||||
|
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
||||||
|
|
||||||
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
||||||
|
|
||||||
export class ApplicationContext implements IApplicationContext {
|
export class ApplicationContext implements IApplicationContext {
|
||||||
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
|
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
|
||||||
public collection: ICategoryCollection;
|
|
||||||
public currentOs: OperatingSystem;
|
|
||||||
|
|
||||||
public get state(): ICategoryCollectionState {
|
public collection: ICategoryCollection;
|
||||||
return this.states[this.collection.os];
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly states: StateMachine;
|
public currentOs: OperatingSystem;
|
||||||
public constructor(
|
|
||||||
public readonly app: IApplication,
|
|
||||||
initialContext: OperatingSystem) {
|
|
||||||
validateApp(app);
|
|
||||||
assertInRange(initialContext, OperatingSystem);
|
|
||||||
this.states = initializeStates(app);
|
|
||||||
this.changeContext(initialContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
public changeContext(os: OperatingSystem): void {
|
public get state(): ICategoryCollectionState {
|
||||||
if (this.currentOs === os) {
|
return this.states[this.collection.os];
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
this.collection = this.app.getCollection(os);
|
private readonly states: StateMachine;
|
||||||
if (!this.collection) {
|
|
||||||
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
public constructor(
|
||||||
}
|
public readonly app: IApplication,
|
||||||
const event: IApplicationContextChangedEvent = {
|
initialContext: OperatingSystem,
|
||||||
newState: this.states[os],
|
) {
|
||||||
oldState: this.states[this.currentOs],
|
validateApp(app);
|
||||||
};
|
this.states = initializeStates(app);
|
||||||
this.contextChanged.notify(event);
|
this.changeContext(initialContext);
|
||||||
this.currentOs = os;
|
}
|
||||||
|
|
||||||
|
public changeContext(os: OperatingSystem): void {
|
||||||
|
assertInRange(os, OperatingSystem);
|
||||||
|
if (this.currentOs === os) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this.collection = this.app.getCollection(os);
|
||||||
|
if (!this.collection) {
|
||||||
|
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
||||||
|
}
|
||||||
|
const event: IApplicationContextChangedEvent = {
|
||||||
|
newState: this.states[os],
|
||||||
|
oldState: this.states[this.currentOs],
|
||||||
|
};
|
||||||
|
this.contextChanged.notify(event);
|
||||||
|
this.currentOs = os;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateApp(app: IApplication) {
|
function validateApp(app: IApplication) {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
throw new Error('undefined app');
|
throw new Error('missing app');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeStates(app: IApplication): StateMachine {
|
function initializeStates(app: IApplication): StateMachine {
|
||||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||||
for (const collection of app.collections) {
|
for (const collection of app.collections) {
|
||||||
machine[collection.os] = new CategoryCollectionState(collection);
|
machine[collection.os] = new CategoryCollectionState(collection);
|
||||||
}
|
}
|
||||||
return machine;
|
return machine;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
import { ApplicationContext } from './ApplicationContext';
|
|
||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { Environment } from '../Environment/Environment';
|
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { Environment } from '../Environment/Environment';
|
||||||
import { IEnvironment } from '../Environment/IEnvironment';
|
import { IEnvironment } from '../Environment/IEnvironment';
|
||||||
import { IApplicationFactory } from '../IApplicationFactory';
|
import { IApplicationFactory } from '../IApplicationFactory';
|
||||||
import { ApplicationFactory } from '../ApplicationFactory';
|
import { ApplicationFactory } from '../ApplicationFactory';
|
||||||
|
import { ApplicationContext } from './ApplicationContext';
|
||||||
|
|
||||||
export async function buildContext(
|
export async function buildContext(
|
||||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||||
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
|
environment = Environment.CurrentEnvironment,
|
||||||
if (!factory) { throw new Error('undefined factory'); }
|
): Promise<IApplicationContext> {
|
||||||
if (!environment) { throw new Error('undefined environment'); }
|
if (!factory) { throw new Error('missing factory'); }
|
||||||
const app = await factory.getApp();
|
if (!environment) { throw new Error('missing environment'); }
|
||||||
const os = getInitialOs(app, environment);
|
const app = await factory.getApp();
|
||||||
return new ApplicationContext(app, os);
|
const os = getInitialOs(app, environment);
|
||||||
|
return new ApplicationContext(app, os);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
||||||
const currentOs = environment.os;
|
const currentOs = environment.os;
|
||||||
const supportedOsList = app.getSupportedOsList();
|
const supportedOsList = app.getSupportedOsList();
|
||||||
if (supportedOsList.includes(currentOs)) {
|
if (supportedOsList.includes(currentOs)) {
|
||||||
return currentOs;
|
return currentOs;
|
||||||
}
|
}
|
||||||
supportedOsList.sort((os1, os2) => {
|
supportedOsList.sort((os1, os2) => {
|
||||||
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||||
return getPriority(os2) - getPriority(os1);
|
return getPriority(os2) - getPriority(os1);
|
||||||
});
|
});
|
||||||
return supportedOsList[0];
|
return supportedOsList[0];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
|
||||||
|
|
||||||
export interface IApplicationContext {
|
export interface IReadOnlyApplicationContext {
|
||||||
readonly app: IApplication;
|
readonly app: IApplication;
|
||||||
readonly state: ICategoryCollectionState;
|
readonly state: IReadOnlyCategoryCollectionState;
|
||||||
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
||||||
changeContext(os: OperatingSystem): void;
|
}
|
||||||
|
|
||||||
|
export interface IApplicationContext extends IReadOnlyApplicationContext {
|
||||||
|
readonly state: ICategoryCollectionState;
|
||||||
|
changeContext(os: OperatingSystem): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IApplicationContextChangedEvent {
|
export interface IApplicationContextChangedEvent {
|
||||||
readonly newState: ICategoryCollectionState;
|
readonly newState: ICategoryCollectionState;
|
||||||
readonly oldState: ICategoryCollectionState;
|
readonly oldState: ICategoryCollectionState;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { UserFilter } from './Filter/UserFilter';
|
import { UserFilter } from './Filter/UserFilter';
|
||||||
import { IUserFilter } from './Filter/IUserFilter';
|
import { IUserFilter } from './Filter/IUserFilter';
|
||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
@@ -5,19 +7,20 @@ import { UserSelection } from './Selection/UserSelection';
|
|||||||
import { IUserSelection } from './Selection/IUserSelection';
|
import { IUserSelection } from './Selection/IUserSelection';
|
||||||
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
import { ICategoryCollection } from '../../../domain/ICategoryCollection';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
|
|
||||||
export class CategoryCollectionState implements ICategoryCollectionState {
|
export class CategoryCollectionState implements ICategoryCollectionState {
|
||||||
public readonly os: OperatingSystem;
|
public readonly os: OperatingSystem;
|
||||||
public readonly code: IApplicationCode;
|
|
||||||
public readonly selection: IUserSelection;
|
|
||||||
public readonly filter: IUserFilter;
|
|
||||||
|
|
||||||
public constructor(readonly collection: ICategoryCollection) {
|
public readonly code: IApplicationCode;
|
||||||
this.selection = new UserSelection(collection, []);
|
|
||||||
this.code = new ApplicationCode(this.selection, collection.scripting);
|
public readonly selection: IUserSelection;
|
||||||
this.filter = new UserFilter(collection);
|
|
||||||
this.os = collection.os;
|
public readonly filter: IUserFilter;
|
||||||
}
|
|
||||||
|
public constructor(readonly collection: ICategoryCollection) {
|
||||||
|
this.selection = new UserSelection(collection, []);
|
||||||
|
this.code = new ApplicationCode(this.selection, collection.scripting);
|
||||||
|
this.filter = new UserFilter(collection);
|
||||||
|
this.os = collection.os;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,41 @@
|
|||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
|
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||||
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||||
import { CodePosition } from './Position/CodePosition';
|
import { CodePosition } from './Position/CodePosition';
|
||||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
|
||||||
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
|
||||||
import { IApplicationCode } from './IApplicationCode';
|
import { IApplicationCode } from './IApplicationCode';
|
||||||
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
|
||||||
|
|
||||||
export class ApplicationCode implements IApplicationCode {
|
export class ApplicationCode implements IApplicationCode {
|
||||||
public readonly changed = new EventSource<ICodeChangedEvent>();
|
public readonly changed = new EventSource<ICodeChangedEvent>();
|
||||||
public current: string;
|
|
||||||
|
|
||||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
public current: string;
|
||||||
|
|
||||||
constructor(
|
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||||
userSelection: IUserSelection,
|
|
||||||
private readonly scriptingDefinition: IScriptingDefinition,
|
|
||||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
|
||||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
|
||||||
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
|
||||||
if (!generator) { throw new Error('generator is null or undefined'); }
|
|
||||||
this.setCode(userSelection.selectedScripts);
|
|
||||||
userSelection.changed.on((scripts) => {
|
|
||||||
this.setCode(scripts);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
constructor(
|
||||||
const oldScripts = Array.from(this.scriptPositions.keys());
|
userSelection: IReadOnlyUserSelection,
|
||||||
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
|
private readonly scriptingDefinition: IScriptingDefinition,
|
||||||
this.current = code.code;
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
||||||
this.scriptPositions = code.scriptPositions;
|
) {
|
||||||
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
if (!userSelection) { throw new Error('missing userSelection'); }
|
||||||
this.changed.notify(event);
|
if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); }
|
||||||
}
|
if (!generator) { throw new Error('missing generator'); }
|
||||||
|
this.setCode(userSelection.selectedScripts);
|
||||||
|
userSelection.changed.on((scripts) => {
|
||||||
|
this.setCode(scripts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
||||||
|
const oldScripts = Array.from(this.scriptPositions.keys());
|
||||||
|
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
|
||||||
|
this.current = code.code;
|
||||||
|
this.scriptPositions = code.scriptPositions;
|
||||||
|
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
||||||
|
this.changed.notify(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,71 @@
|
|||||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
|
||||||
import { SelectedScript } from '../../Selection/SelectedScript';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||||
|
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
|
||||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
public readonly code: string;
|
public readonly code: string;
|
||||||
public readonly addedScripts: ReadonlyArray<IScript>;
|
|
||||||
public readonly removedScripts: ReadonlyArray<IScript>;
|
|
||||||
public readonly changedScripts: ReadonlyArray<IScript>;
|
|
||||||
|
|
||||||
private readonly scripts: Map<IScript, ICodePosition>;
|
public readonly addedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
constructor(
|
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||||
code: string,
|
|
||||||
oldScripts: ReadonlyArray<SelectedScript>,
|
|
||||||
scripts: Map<SelectedScript, ICodePosition>) {
|
|
||||||
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
|
||||||
this.code = code;
|
|
||||||
const newScripts = Array.from(scripts.keys());
|
|
||||||
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
|
||||||
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
|
||||||
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
|
||||||
this.scripts = new Map<IScript, ICodePosition>();
|
|
||||||
scripts.forEach((position, selection) => {
|
|
||||||
this.scripts.set(selection.script, position);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public isEmpty(): boolean {
|
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||||
return this.scripts.size === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getScriptPositionInCode(script: IScript): ICodePosition {
|
private readonly scripts: Map<IScript, ICodePosition>;
|
||||||
return this.scripts.get(script);
|
|
||||||
}
|
constructor(
|
||||||
|
code: string,
|
||||||
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
scripts: Map<SelectedScript, ICodePosition>,
|
||||||
|
) {
|
||||||
|
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
||||||
|
this.code = code;
|
||||||
|
const newScripts = Array.from(scripts.keys());
|
||||||
|
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
||||||
|
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
||||||
|
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
||||||
|
this.scripts = new Map<IScript, ICodePosition>();
|
||||||
|
scripts.forEach((position, selection) => {
|
||||||
|
this.scripts.set(selection.script, position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEmpty(): boolean {
|
||||||
|
return this.scripts.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||||
|
return this.scripts.get(script);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||||
const totalLines = script.split(/\r\n|\r|\n/).length;
|
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||||
for (const position of positions) {
|
const missingPositions = positions.filter((position) => position.endLine > totalLines);
|
||||||
if (position.endLine > totalLines) {
|
if (missingPositions.length > 0) {
|
||||||
throw new Error(`script end line (${position.endLine}) is out of range.` +
|
throw new Error(
|
||||||
`(total code lines: ${totalLines}`);
|
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
|
||||||
}
|
+ `(total code lines: ${totalLines}).`,
|
||||||
}
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChangedScripts(
|
function getChangedScripts(
|
||||||
oldScripts: ReadonlyArray<SelectedScript>,
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
|
newScripts: ReadonlyArray<SelectedScript>,
|
||||||
return newScripts
|
): ReadonlyArray<IScript> {
|
||||||
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
return newScripts
|
||||||
&& oldScript.revert !== newScript.revert ))
|
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||||
.map((selection) => selection.script);
|
&& oldScript.revert !== newScript.revert))
|
||||||
|
.map((selection) => selection.script);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectIfNotExists(
|
function selectIfNotExists(
|
||||||
selectableContainer: ReadonlyArray<SelectedScript>,
|
selectableContainer: ReadonlyArray<SelectedScript>,
|
||||||
test: ReadonlyArray<SelectedScript>) {
|
test: ReadonlyArray<SelectedScript>,
|
||||||
return selectableContainer
|
) {
|
||||||
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
return selectableContainer
|
||||||
.map((selection) => selection.script);
|
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
||||||
|
.map((selection) => selection.script);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { IScript } from '@/domain/IScript';
|
|||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
export interface ICodeChangedEvent {
|
export interface ICodeChangedEvent {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
addedScripts: ReadonlyArray<IScript>;
|
addedScripts: ReadonlyArray<IScript>;
|
||||||
removedScripts: ReadonlyArray<IScript>;
|
removedScripts: ReadonlyArray<IScript>;
|
||||||
changedScripts: ReadonlyArray<IScript>;
|
changedScripts: ReadonlyArray<IScript>;
|
||||||
isEmpty(): boolean;
|
isEmpty(): boolean;
|
||||||
getScriptPositionInCode(script: IScript): ICodePosition;
|
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,64 +4,65 @@ const NewLine = '\n';
|
|||||||
const TotalFunctionSeparatorChars = 58;
|
const TotalFunctionSeparatorChars = 58;
|
||||||
|
|
||||||
export abstract class CodeBuilder implements ICodeBuilder {
|
export abstract class CodeBuilder implements ICodeBuilder {
|
||||||
private readonly lines = new Array<string>();
|
private readonly lines = new Array<string>();
|
||||||
|
|
||||||
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
||||||
public get currentLine(): number {
|
public get currentLine(): number {
|
||||||
return this.lines.length;
|
return this.lines.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public appendLine(code?: string): CodeBuilder {
|
||||||
|
if (!code) {
|
||||||
|
this.lines.push('');
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
|
this.lines.push(...lines);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public appendLine(code?: string): CodeBuilder {
|
public appendTrailingHyphensCommentLine(
|
||||||
if (!code) {
|
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
|
||||||
this.lines.push('');
|
): CodeBuilder {
|
||||||
return this;
|
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
|
||||||
}
|
}
|
||||||
const lines = code.match(/[^\r\n]+/g);
|
|
||||||
for (const line of lines) {
|
public appendCommentLine(commentLine?: string): CodeBuilder {
|
||||||
this.lines.push(line);
|
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
|
||||||
}
|
return this;
|
||||||
return this;
|
}
|
||||||
|
|
||||||
|
public appendFunction(name: string, code: string): CodeBuilder {
|
||||||
|
if (!name) { throw new Error('name cannot be empty or null'); }
|
||||||
|
if (!code) { throw new Error('code cannot be empty or null'); }
|
||||||
|
return this
|
||||||
|
.appendCommentLineWithHyphensAround(name)
|
||||||
|
.appendLine(this.writeStandardOut(`--- ${name}`))
|
||||||
|
.appendLine(code)
|
||||||
|
.appendTrailingHyphensCommentLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
public appendCommentLineWithHyphensAround(
|
||||||
|
sectionName: string,
|
||||||
|
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
|
||||||
|
): CodeBuilder {
|
||||||
|
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
|
||||||
|
if (sectionName.length >= totalRepeatHyphens) {
|
||||||
|
return this.appendCommentLine(sectionName);
|
||||||
}
|
}
|
||||||
|
const firstHyphens = '-'.repeat(Math.floor((totalRepeatHyphens - sectionName.length) / 2));
|
||||||
|
const secondHyphens = '-'.repeat(Math.ceil((totalRepeatHyphens - sectionName.length) / 2));
|
||||||
|
return this
|
||||||
|
.appendTrailingHyphensCommentLine()
|
||||||
|
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
|
||||||
|
.appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars);
|
||||||
|
}
|
||||||
|
|
||||||
public appendTrailingHyphensCommentLine(
|
public toString(): string {
|
||||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
return this.lines.join(NewLine);
|
||||||
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public appendCommentLine(commentLine?: string): CodeBuilder {
|
protected abstract getCommentDelimiter(): string;
|
||||||
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public appendFunction(name: string, code: string): CodeBuilder {
|
protected abstract writeStandardOut(text: string): string;
|
||||||
if (!name) { throw new Error('name cannot be empty or null'); }
|
|
||||||
if (!code) { throw new Error('code cannot be empty or null'); }
|
|
||||||
return this
|
|
||||||
.appendCommentLineWithHyphensAround(name)
|
|
||||||
.appendLine(this.writeStandardOut(`--- ${name}`))
|
|
||||||
.appendLine(code)
|
|
||||||
.appendTrailingHyphensCommentLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
public appendCommentLineWithHyphensAround(
|
|
||||||
sectionName: string,
|
|
||||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
|
||||||
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
|
|
||||||
if (sectionName.length >= totalRepeatHyphens) {
|
|
||||||
return this.appendCommentLine(sectionName);
|
|
||||||
}
|
|
||||||
const firstHyphens = '-'.repeat(Math.floor((totalRepeatHyphens - sectionName.length) / 2));
|
|
||||||
const secondHyphens = '-'.repeat(Math.ceil((totalRepeatHyphens - sectionName.length) / 2));
|
|
||||||
return this
|
|
||||||
.appendTrailingHyphensCommentLine()
|
|
||||||
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
|
|
||||||
.appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars);
|
|
||||||
}
|
|
||||||
|
|
||||||
public toString(): string {
|
|
||||||
return this.lines.join(NewLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract getCommentDelimiter(): string;
|
|
||||||
protected abstract writeStandardOut(text: string): string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { BatchBuilder } from './Languages/BatchBuilder';
|
|||||||
import { ShellBuilder } from './Languages/ShellBuilder';
|
import { ShellBuilder } from './Languages/ShellBuilder';
|
||||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||||
|
|
||||||
export class CodeBuilderFactory extends ScriptingLanguageFactory<ICodeBuilder> implements ICodeBuilderFactory {
|
export class CodeBuilderFactory
|
||||||
constructor() {
|
extends ScriptingLanguageFactory<ICodeBuilder>
|
||||||
super();
|
implements ICodeBuilderFactory {
|
||||||
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
|
constructor() {
|
||||||
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
|
super();
|
||||||
}
|
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
|
||||||
|
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export interface ICodeBuilder {
|
export interface ICodeBuilder {
|
||||||
currentLine: number;
|
currentLine: number;
|
||||||
appendLine(code?: string): ICodeBuilder;
|
appendLine(code?: string): ICodeBuilder;
|
||||||
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
|
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
|
||||||
appendCommentLine(commentLine?: string): ICodeBuilder;
|
appendCommentLine(commentLine?: string): ICodeBuilder;
|
||||||
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
|
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
|
||||||
appendFunction(name: string, code: string): ICodeBuilder;
|
appendFunction(name: string, code: string): ICodeBuilder;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ICodeBuilder } from './ICodeBuilder';
|
|
||||||
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||||
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
export interface ICodeBuilderFactory extends IScriptingLanguageFactory<ICodeBuilder> {
|
export type ICodeBuilderFactory = IScriptingLanguageFactory<ICodeBuilder>;
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import { SelectedScript } from '@/application/Context/State/Selection/SelectedSc
|
|||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
export interface IUserScript {
|
export interface IUserScript {
|
||||||
code: string;
|
code: string;
|
||||||
scriptPositions: Map<SelectedScript, ICodePosition>;
|
scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { IUserScript } from './IUserScript';
|
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
|
|
||||||
export interface IUserScriptGenerator {
|
export interface IUserScriptGenerator {
|
||||||
buildCode(
|
buildCode(
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
scriptingDefinition: IScriptingDefinition): IUserScript;
|
scriptingDefinition: IScriptingDefinition): IUserScript;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
||||||
|
|
||||||
export class BatchBuilder extends CodeBuilder {
|
export class BatchBuilder extends CodeBuilder {
|
||||||
protected getCommentDelimiter(): string {
|
protected getCommentDelimiter(): string {
|
||||||
return '::';
|
return '::';
|
||||||
}
|
}
|
||||||
protected writeStandardOut(text: string): string {
|
|
||||||
return `echo ${escapeForEcho(text)}`;
|
protected writeStandardOut(text: string): string {
|
||||||
}
|
return `echo ${escapeForEcho(text)}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeForEcho(text: string) {
|
function escapeForEcho(text: string) {
|
||||||
return text
|
return text
|
||||||
.replace(/&/g, '^&')
|
.replace(/&/g, '^&')
|
||||||
.replace(/%/g, '%%');
|
.replace(/%/g, '%%');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
||||||
|
|
||||||
export class ShellBuilder extends CodeBuilder {
|
export class ShellBuilder extends CodeBuilder {
|
||||||
protected getCommentDelimiter(): string {
|
protected getCommentDelimiter(): string {
|
||||||
return '#';
|
return '#';
|
||||||
}
|
}
|
||||||
protected writeStandardOut(text: string): string {
|
|
||||||
return `echo '${escapeForEcho(text)}'`;
|
protected writeStandardOut(text: string): string {
|
||||||
}
|
return `echo '${escapeForEcho(text)}'`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeForEcho(text: string) {
|
function escapeForEcho(text: string) {
|
||||||
return text
|
return text
|
||||||
.replace(/'/g, '\'\\\'\'');
|
.replace(/'/g, '\'\\\'\'');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,75 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { CodePosition } from '../Position/CodePosition';
|
|
||||||
import { IUserScript } from './IUserScript';
|
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { CodePosition } from '../Position/CodePosition';
|
||||||
|
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
import { ICodeBuilder } from './ICodeBuilder';
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||||
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
||||||
|
|
||||||
export class UserScriptGenerator implements IUserScriptGenerator {
|
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||||
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildCode(
|
||||||
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
scriptingDefinition: IScriptingDefinition,
|
||||||
|
): IUserScript {
|
||||||
|
if (!selectedScripts) { throw new Error('missing scripts'); }
|
||||||
|
if (!scriptingDefinition) { throw new Error('missing definition'); }
|
||||||
|
if (!selectedScripts.length) {
|
||||||
|
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
||||||
}
|
}
|
||||||
public buildCode(
|
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
builder = initializeCode(scriptingDefinition.startCode, builder);
|
||||||
scriptingDefinition: IScriptingDefinition): IUserScript {
|
const scriptPositions = selectedScripts.reduce((result, selection) => {
|
||||||
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
return appendSelection(selection, result, builder);
|
||||||
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
}, new Map<SelectedScript, ICodePosition>());
|
||||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
||||||
if (!selectedScripts.length) {
|
return { code, scriptPositions };
|
||||||
return { code: '', scriptPositions };
|
}
|
||||||
}
|
|
||||||
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
|
||||||
builder = initializeCode(scriptingDefinition.startCode, builder);
|
|
||||||
for (const selection of selectedScripts) {
|
|
||||||
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
|
||||||
}
|
|
||||||
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
|
||||||
return { code, scriptPositions };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder {
|
function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder {
|
||||||
if (!startCode) {
|
if (!startCode) {
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
return builder
|
return builder
|
||||||
.appendLine(startCode)
|
.appendLine(startCode)
|
||||||
.appendLine();
|
.appendLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalizeCode(builder: ICodeBuilder, endCode: string): string {
|
function finalizeCode(builder: ICodeBuilder, endCode: string): string {
|
||||||
if (!endCode) {
|
if (!endCode) {
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
return builder.appendLine()
|
return builder.appendLine()
|
||||||
.appendLine(endCode)
|
.appendLine(endCode)
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendSelection(
|
function appendSelection(
|
||||||
selection: SelectedScript,
|
selection: SelectedScript,
|
||||||
scriptPositions: Map<SelectedScript, ICodePosition>,
|
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||||
builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
|
builder: ICodeBuilder,
|
||||||
const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts
|
): Map<SelectedScript, ICodePosition> {
|
||||||
builder = appendCode(selection, builder);
|
// Start from next line because first line will be empty to separate scripts
|
||||||
const endPosition = builder.currentLine - 1;
|
const startPosition = builder.currentLine + 1;
|
||||||
builder.appendLine();
|
appendCode(selection, builder);
|
||||||
const position = new CodePosition(startPosition, endPosition);
|
const endPosition = builder.currentLine - 1;
|
||||||
scriptPositions.set(selection, position);
|
builder.appendLine();
|
||||||
return scriptPositions;
|
const position = new CodePosition(startPosition, endPosition);
|
||||||
|
scriptPositions.set(selection, position);
|
||||||
|
return scriptPositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
||||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
const { script } = selection;
|
||||||
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
|
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
||||||
return builder
|
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
|
||||||
.appendLine()
|
return builder
|
||||||
.appendFunction(name, scriptCode);
|
.appendLine()
|
||||||
|
.appendFunction(name, scriptCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
|
|
||||||
export interface IApplicationCode {
|
export interface IApplicationCode {
|
||||||
readonly changed: IEventSource<ICodeChangedEvent>;
|
readonly changed: IEventSource<ICodeChangedEvent>;
|
||||||
readonly current: string;
|
readonly current: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import { ICodePosition } from './ICodePosition';
|
import { ICodePosition } from './ICodePosition';
|
||||||
|
|
||||||
export class CodePosition implements ICodePosition {
|
export class CodePosition implements ICodePosition {
|
||||||
public get totalLines(): number {
|
public get totalLines(): number {
|
||||||
return this.endLine - this.startLine;
|
return this.endLine - this.startLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly startLine: number,
|
public readonly startLine: number,
|
||||||
public readonly endLine: number) {
|
public readonly endLine: number,
|
||||||
if (startLine < 0) {
|
) {
|
||||||
throw new Error('Code cannot start in a negative line');
|
if (startLine < 0) {
|
||||||
}
|
throw new Error('Code cannot start in a negative line');
|
||||||
if (endLine < 0) {
|
|
||||||
throw new Error('Code cannot end in a negative line');
|
|
||||||
}
|
|
||||||
if (endLine === startLine) {
|
|
||||||
throw new Error('Empty code');
|
|
||||||
}
|
|
||||||
if (endLine < startLine) {
|
|
||||||
throw new Error('End line cannot be less than start line');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (endLine < 0) {
|
||||||
|
throw new Error('Code cannot end in a negative line');
|
||||||
|
}
|
||||||
|
if (endLine === startLine) {
|
||||||
|
throw new Error('Empty code');
|
||||||
|
}
|
||||||
|
if (endLine < startLine) {
|
||||||
|
throw new Error('End line cannot be less than start line');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface ICodePosition {
|
export interface ICodePosition {
|
||||||
readonly startLine: number;
|
readonly startLine: number;
|
||||||
readonly endLine: number;
|
readonly endLine: number;
|
||||||
readonly totalLines: number;
|
readonly totalLines: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { IFilterResult } from './IFilterResult';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { IFilterResult } from './IFilterResult';
|
||||||
|
|
||||||
export class FilterResult implements IFilterResult {
|
export class FilterResult implements IFilterResult {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly scriptMatches: ReadonlyArray<IScript>,
|
public readonly scriptMatches: ReadonlyArray<IScript>,
|
||||||
public readonly categoryMatches: ReadonlyArray<ICategory>,
|
public readonly categoryMatches: ReadonlyArray<ICategory>,
|
||||||
public readonly query: string) {
|
public readonly query: string,
|
||||||
if (!query) { throw new Error('Query is empty or undefined'); }
|
) {
|
||||||
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
|
if (!query) { throw new Error('Query is empty or undefined'); }
|
||||||
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
|
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
|
||||||
}
|
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
|
||||||
public hasAnyMatches(): boolean {
|
}
|
||||||
return this.scriptMatches.length > 0
|
|
||||||
|| this.categoryMatches.length > 0;
|
public hasAnyMatches(): boolean {
|
||||||
}
|
return this.scriptMatches.length > 0
|
||||||
|
|| this.categoryMatches.length > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { IScript, ICategory } from '@/domain/ICategory';
|
import { IScript, ICategory } from '@/domain/ICategory';
|
||||||
|
|
||||||
export interface IFilterResult {
|
export interface IFilterResult {
|
||||||
readonly categoryMatches: ReadonlyArray<ICategory>;
|
readonly categoryMatches: ReadonlyArray<ICategory>;
|
||||||
readonly scriptMatches: ReadonlyArray<IScript>;
|
readonly scriptMatches: ReadonlyArray<IScript>;
|
||||||
readonly query: string;
|
readonly query: string;
|
||||||
hasAnyMatches(): boolean;
|
hasAnyMatches(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import { IFilterResult } from './IFilterResult';
|
import { IFilterResult } from './IFilterResult';
|
||||||
|
|
||||||
export interface IUserFilter {
|
export interface IReadOnlyUserFilter {
|
||||||
readonly currentFilter: IFilterResult | undefined;
|
readonly currentFilter: IFilterResult | undefined;
|
||||||
readonly filtered: IEventSource<IFilterResult>;
|
readonly filtered: IEventSource<IFilterResult>;
|
||||||
readonly filterRemoved: IEventSource<void>;
|
readonly filterRemoved: IEventSource<void>;
|
||||||
setFilter(filter: string): void;
|
}
|
||||||
removeFilter(): void;
|
|
||||||
|
export interface IUserFilter extends IReadOnlyUserFilter {
|
||||||
|
setFilter(filter: string): void;
|
||||||
|
removeFilter(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,56 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { FilterResult } from './FilterResult';
|
import { FilterResult } from './FilterResult';
|
||||||
import { IFilterResult } from './IFilterResult';
|
import { IFilterResult } from './IFilterResult';
|
||||||
import { IUserFilter } from './IUserFilter';
|
import { IUserFilter } from './IUserFilter';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
|
||||||
|
|
||||||
export class UserFilter implements IUserFilter {
|
export class UserFilter implements IUserFilter {
|
||||||
public readonly filtered = new EventSource<IFilterResult>();
|
public readonly filtered = new EventSource<IFilterResult>();
|
||||||
public readonly filterRemoved = new EventSource<void>();
|
|
||||||
public currentFilter: IFilterResult | undefined;
|
|
||||||
|
|
||||||
constructor(private collection: ICategoryCollection) {
|
public readonly filterRemoved = new EventSource<void>();
|
||||||
|
|
||||||
|
public currentFilter: IFilterResult | undefined;
|
||||||
|
|
||||||
|
constructor(private collection: ICategoryCollection) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public setFilter(filter: string): void {
|
||||||
|
if (!filter) {
|
||||||
|
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter');
|
||||||
}
|
}
|
||||||
|
const filterLowercase = filter.toLocaleLowerCase();
|
||||||
|
const filteredScripts = this.collection.getAllScripts().filter(
|
||||||
|
(script) => isScriptAMatch(script, filterLowercase),
|
||||||
|
);
|
||||||
|
const filteredCategories = this.collection.getAllCategories().filter(
|
||||||
|
(category) => category.name.toLowerCase().includes(filterLowercase),
|
||||||
|
);
|
||||||
|
const matches = new FilterResult(
|
||||||
|
filteredScripts,
|
||||||
|
filteredCategories,
|
||||||
|
filter,
|
||||||
|
);
|
||||||
|
this.currentFilter = matches;
|
||||||
|
this.filtered.notify(matches);
|
||||||
|
}
|
||||||
|
|
||||||
public setFilter(filter: string): void {
|
public removeFilter(): void {
|
||||||
if (!filter) {
|
this.currentFilter = undefined;
|
||||||
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter');
|
this.filterRemoved.notify();
|
||||||
}
|
}
|
||||||
const filterLowercase = filter.toLocaleLowerCase();
|
|
||||||
const filteredScripts = this.collection.getAllScripts().filter(
|
|
||||||
(script) => isScriptAMatch(script, filterLowercase));
|
|
||||||
const filteredCategories = this.collection.getAllCategories().filter(
|
|
||||||
(category) => category.name.toLowerCase().includes(filterLowercase));
|
|
||||||
const matches = new FilterResult(
|
|
||||||
filteredScripts,
|
|
||||||
filteredCategories,
|
|
||||||
filter,
|
|
||||||
);
|
|
||||||
this.currentFilter = matches;
|
|
||||||
this.filtered.notify(matches);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeFilter(): void {
|
|
||||||
this.currentFilter = undefined;
|
|
||||||
this.filterRemoved.notify();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isScriptAMatch(script: IScript, filterLowercase: string) {
|
function isScriptAMatch(script: IScript, filterLowercase: string) {
|
||||||
if (script.name.toLowerCase().includes(filterLowercase)) {
|
if (script.name.toLowerCase().includes(filterLowercase)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (script.code.execute.toLowerCase().includes(filterLowercase)) {
|
if (script.code.execute.toLowerCase().includes(filterLowercase)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (script.code.revert) {
|
if (script.code.revert) {
|
||||||
return script.code.revert.toLowerCase().includes(filterLowercase);
|
return script.code.revert.toLowerCase().includes(filterLowercase);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { IUserFilter } from './Filter/IUserFilter';
|
|
||||||
import { IUserSelection } from './Selection/IUserSelection';
|
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
|
||||||
|
import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
|
||||||
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
|
||||||
export interface ICategoryCollectionState {
|
export interface IReadOnlyCategoryCollectionState {
|
||||||
readonly code: IApplicationCode;
|
readonly code: IApplicationCode;
|
||||||
readonly filter: IUserFilter;
|
readonly os: OperatingSystem;
|
||||||
readonly selection: IUserSelection;
|
readonly filter: IReadOnlyUserFilter;
|
||||||
readonly collection: ICategoryCollection;
|
readonly selection: IReadOnlyUserSelection;
|
||||||
readonly os: OperatingSystem;
|
readonly collection: ICategoryCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
|
||||||
|
readonly filter: IUserFilter;
|
||||||
|
readonly selection: IUserSelection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
|
||||||
export interface IUserSelection {
|
export interface IReadOnlyUserSelection {
|
||||||
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
||||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||||
areAllSelected(category: ICategory): boolean;
|
isSelected(scriptId: string): boolean;
|
||||||
isAnySelected(category: ICategory): boolean;
|
areAllSelected(category: ICategory): boolean;
|
||||||
removeAllInCategory(categoryId: number): void;
|
isAnySelected(category: ICategory): boolean;
|
||||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
}
|
||||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
|
||||||
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
export interface IUserSelection extends IReadOnlyUserSelection {
|
||||||
removeSelectedScript(scriptId: string): void;
|
removeAllInCategory(categoryId: number): void;
|
||||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
||||||
isSelected(scriptId: string): boolean;
|
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
selectAll(): void;
|
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
deselectAll(): void;
|
removeSelectedScript(scriptId: string): void;
|
||||||
|
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
||||||
|
selectAll(): void;
|
||||||
|
deselectAll(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
|||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
export class SelectedScript extends BaseEntity<string> {
|
export class SelectedScript extends BaseEntity<string> {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly script: IScript,
|
public readonly script: IScript,
|
||||||
public readonly revert: boolean,
|
public readonly revert: boolean,
|
||||||
) {
|
) {
|
||||||
super(script.id);
|
super(script.id);
|
||||||
if (revert && !script.canRevert()) {
|
if (revert && !script.canRevert()) {
|
||||||
throw new Error('cannot revert an irreversible script');
|
throw new Error('cannot revert an irreversible script');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,141 +1,167 @@
|
|||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
import { IUserSelection } from './IUserSelection';
|
|
||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { IUserSelection } from './IUserSelection';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
|
||||||
export class UserSelection implements IUserSelection {
|
export class UserSelection implements IUserSelection {
|
||||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||||
private readonly scripts: IRepository<string, SelectedScript>;
|
|
||||||
|
|
||||||
constructor(
|
private readonly scripts: IRepository<string, SelectedScript>;
|
||||||
private readonly collection: ICategoryCollection,
|
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>) {
|
|
||||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
|
||||||
if (selectedScripts && selectedScripts.length > 0) {
|
|
||||||
for (const script of selectedScripts) {
|
|
||||||
this.scripts.addItem(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public areAllSelected(category: ICategory): boolean {
|
constructor(
|
||||||
if (this.selectedScripts.length === 0) {
|
private readonly collection: ICategoryCollection,
|
||||||
return false;
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
}
|
) {
|
||||||
const scripts = category.getAllScriptsRecursively();
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
if (this.selectedScripts.length < scripts.length) {
|
for (const script of selectedScripts) {
|
||||||
return false;
|
this.scripts.addItem(script);
|
||||||
}
|
|
||||||
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public isAnySelected(category: ICategory): boolean {
|
public areAllSelected(category: ICategory): boolean {
|
||||||
if (this.selectedScripts.length === 0) {
|
if (this.selectedScripts.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
return this.selectedScripts.some((s) => category.includes(s.script));
|
|
||||||
}
|
}
|
||||||
|
const scripts = category.getAllScriptsRecursively();
|
||||||
|
if (this.selectedScripts.length < scripts.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return scripts.every(
|
||||||
|
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public removeAllInCategory(categoryId: number): void {
|
public isAnySelected(category: ICategory): boolean {
|
||||||
const category = this.collection.findCategory(categoryId);
|
if (this.selectedScripts.length === 0) {
|
||||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
return false;
|
||||||
.filter((script) => this.scripts.exists(script.id));
|
|
||||||
if (!scriptsToRemove.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const script of scriptsToRemove) {
|
|
||||||
this.scripts.removeItem(script.id);
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
}
|
||||||
|
return this.selectedScripts.some((s) => category.includes(s.script));
|
||||||
|
}
|
||||||
|
|
||||||
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
|
public removeAllInCategory(categoryId: number): void {
|
||||||
const category = this.collection.findCategory(categoryId);
|
const category = this.collection.findCategory(categoryId);
|
||||||
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||||
.filter((script) =>
|
.filter((script) => this.scripts.exists(script.id));
|
||||||
!this.scripts.exists(script.id)
|
if (!scriptsToRemove.length) {
|
||||||
|| this.scripts.getById(script.id).revert !== revert,
|
return;
|
||||||
);
|
|
||||||
if (!scriptsToAddOrUpdate.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const script of scriptsToAddOrUpdate) {
|
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
|
||||||
this.scripts.addOrUpdateItem(selectedScript);
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
}
|
||||||
|
for (const script of scriptsToRemove) {
|
||||||
|
this.scripts.removeItem(script.id);
|
||||||
|
}
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
||||||
const script = this.collection.findScript(scriptId);
|
const scriptsToAddOrUpdate = this.collection
|
||||||
if (!script) {
|
.findCategory(categoryId)
|
||||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
.getAllScriptsRecursively()
|
||||||
}
|
.filter(
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
(script) => !this.scripts.exists(script.id)
|
||||||
this.scripts.addItem(selectedScript);
|
|| this.scripts.getById(script.id).revert !== revert,
|
||||||
this.changed.notify(this.scripts.getItems());
|
)
|
||||||
|
.map((script) => new SelectedScript(script, revert));
|
||||||
|
if (!scriptsToAddOrUpdate.length) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
for (const script of scriptsToAddOrUpdate) {
|
||||||
|
this.scripts.addOrUpdateItem(script);
|
||||||
|
}
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
const script = this.collection.findScript(scriptId);
|
const script = this.collection.findScript(scriptId);
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
if (!script) {
|
||||||
this.scripts.addOrUpdateItem(selectedScript);
|
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
}
|
||||||
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
|
this.scripts.addItem(selectedScript);
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
public removeSelectedScript(scriptId: string): void {
|
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
this.scripts.removeItem(scriptId);
|
const script = this.collection.findScript(scriptId);
|
||||||
this.changed.notify(this.scripts.getItems());
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
}
|
this.scripts.addOrUpdateItem(selectedScript);
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
public isSelected(scriptId: string): boolean {
|
public removeSelectedScript(scriptId: string): void {
|
||||||
return this.scripts.exists(scriptId);
|
this.scripts.removeItem(scriptId);
|
||||||
}
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
/** Get users scripts based on his/her selections */
|
public isSelected(scriptId: string): boolean {
|
||||||
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
return this.scripts.exists(scriptId);
|
||||||
return this.scripts.getItems();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public selectAll(): void {
|
/** Get users scripts based on his/her selections */
|
||||||
for (const script of this.collection.getAllScripts()) {
|
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
||||||
if (!this.scripts.exists(script.id)) {
|
return this.scripts.getItems();
|
||||||
const selection = new SelectedScript(script, false);
|
}
|
||||||
this.scripts.addItem(selection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public deselectAll(): void {
|
public selectAll(): void {
|
||||||
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
const scriptsToSelect = this.collection
|
||||||
for (const scriptId of selectedScriptIds) {
|
.getAllScripts()
|
||||||
this.scripts.removeItem(scriptId);
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
}
|
.map((script) => new SelectedScript(script, false));
|
||||||
this.changed.notify([]);
|
if (scriptsToSelect.length === 0) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
for (const script of scriptsToSelect) {
|
||||||
|
this.scripts.addItem(script);
|
||||||
|
}
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
public selectOnly(scripts: readonly IScript[]): void {
|
public deselectAll(): void {
|
||||||
if (!scripts || scripts.length === 0) {
|
if (this.scripts.length === 0) {
|
||||||
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
return;
|
||||||
}
|
|
||||||
// Unselect from selected scripts
|
|
||||||
if (this.scripts.length !== 0) {
|
|
||||||
this.scripts.getItems()
|
|
||||||
.filter((existing) => !scripts.some((script) => existing.id === script.id))
|
|
||||||
.map((script) => script.id)
|
|
||||||
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
|
||||||
}
|
|
||||||
// Select from unselected scripts
|
|
||||||
const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
|
|
||||||
for (const toSelect of unselectedScripts) {
|
|
||||||
const selection = new SelectedScript(toSelect, false);
|
|
||||||
this.scripts.addItem(selection);
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
}
|
||||||
|
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
||||||
|
for (const scriptId of selectedScriptIds) {
|
||||||
|
this.scripts.removeItem(scriptId);
|
||||||
|
}
|
||||||
|
this.changed.notify([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectOnly(scripts: readonly IScript[]): void {
|
||||||
|
if (!scripts || scripts.length === 0) {
|
||||||
|
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
||||||
|
}
|
||||||
|
let totalChanged = 0;
|
||||||
|
totalChanged += this.unselectMissingWithoutNotifying(scripts);
|
||||||
|
totalChanged += this.selectNewWithoutNotifying(scripts);
|
||||||
|
if (totalChanged > 0) {
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unselectMissingWithoutNotifying(scripts: readonly IScript[]): number {
|
||||||
|
if (this.scripts.length === 0 || scripts.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const existingItems = this.scripts.getItems();
|
||||||
|
const missingIds = existingItems
|
||||||
|
.filter((existing) => !scripts.some((script) => existing.id === script.id))
|
||||||
|
.map((script) => script.id);
|
||||||
|
for (const id of missingIds) {
|
||||||
|
this.scripts.removeItem(id);
|
||||||
|
}
|
||||||
|
return missingIds.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectNewWithoutNotifying(scripts: readonly IScript[]): number {
|
||||||
|
const unselectedScripts = scripts
|
||||||
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
|
.map((script) => new SelectedScript(script, false));
|
||||||
|
for (const newScript of unselectedScripts) {
|
||||||
|
this.scripts.addItem(newScript);
|
||||||
|
}
|
||||||
|
return unselectedScripts.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,52 +3,55 @@ import { DetectorBuilder } from './DetectorBuilder';
|
|||||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
|
|
||||||
export class BrowserOsDetector implements IBrowserOsDetector {
|
export class BrowserOsDetector implements IBrowserOsDetector {
|
||||||
private readonly detectors = BrowserDetectors;
|
private readonly detectors = BrowserDetectors;
|
||||||
public detect(userAgent: string): OperatingSystem | undefined {
|
|
||||||
if (!userAgent) {
|
public detect(userAgent: string): OperatingSystem | undefined {
|
||||||
return undefined;
|
if (!userAgent) {
|
||||||
}
|
return undefined;
|
||||||
for (const detector of this.detectors) {
|
|
||||||
const os = detector.detect(userAgent);
|
|
||||||
if (os !== undefined) {
|
|
||||||
return os;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
for (const detector of this.detectors) {
|
||||||
|
const os = detector.detect(userAgent);
|
||||||
|
if (os !== undefined) {
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
|
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
|
||||||
const BrowserDetectors =
|
const BrowserDetectors = [
|
||||||
[
|
define(OperatingSystem.KaiOS, (b) => b
|
||||||
define(OperatingSystem.KaiOS, (b) =>
|
.mustInclude('KAIOS')),
|
||||||
b.mustInclude('KAIOS')),
|
define(OperatingSystem.ChromeOS, (b) => b
|
||||||
define(OperatingSystem.ChromeOS, (b) =>
|
.mustInclude('CrOS')),
|
||||||
b.mustInclude('CrOS')),
|
define(OperatingSystem.BlackBerryOS, (b) => b
|
||||||
define(OperatingSystem.BlackBerryOS, (b) =>
|
.mustInclude('BlackBerry')),
|
||||||
b.mustInclude('BlackBerry')),
|
define(OperatingSystem.BlackBerryTabletOS, (b) => b
|
||||||
define(OperatingSystem.BlackBerryTabletOS, (b) =>
|
.mustInclude('RIM Tablet OS')),
|
||||||
b.mustInclude('RIM Tablet OS')),
|
define(OperatingSystem.BlackBerry, (b) => b
|
||||||
define(OperatingSystem.BlackBerry, (b) =>
|
.mustInclude('BB10')),
|
||||||
b.mustInclude('BB10')),
|
define(OperatingSystem.Android, (b) => b
|
||||||
define(OperatingSystem.Android, (b) =>
|
.mustInclude('Android').mustNotInclude('Windows Phone')),
|
||||||
b.mustInclude('Android').mustNotInclude('Windows Phone')),
|
define(OperatingSystem.Android, (b) => b
|
||||||
define(OperatingSystem.Android, (b) =>
|
.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
||||||
b.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
define(OperatingSystem.iOS, (b) => b
|
||||||
define(OperatingSystem.iOS, (b) =>
|
.mustInclude('like Mac OS X')),
|
||||||
b.mustInclude('like Mac OS X')),
|
define(OperatingSystem.Linux, (b) => b
|
||||||
define(OperatingSystem.Linux, (b) =>
|
.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
||||||
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
define(OperatingSystem.Windows, (b) => b
|
||||||
define(OperatingSystem.Windows, (b) =>
|
.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
||||||
b.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
define(OperatingSystem.WindowsPhone, (b) => b
|
||||||
define(OperatingSystem.WindowsPhone, (b) =>
|
.mustInclude('Windows Phone')),
|
||||||
b.mustInclude('Windows Phone')),
|
define(OperatingSystem.macOS, (b) => b
|
||||||
define(OperatingSystem.macOS, (b) =>
|
.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
||||||
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
|
function define(
|
||||||
const builder = new DetectorBuilder(os);
|
os: OperatingSystem,
|
||||||
applyRules(builder);
|
applyRules: (builder: DetectorBuilder) => DetectorBuilder,
|
||||||
return builder.build();
|
): IBrowserOsDetector {
|
||||||
|
const builder = new DetectorBuilder(os);
|
||||||
|
applyRules(builder);
|
||||||
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,54 @@
|
|||||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
|
|
||||||
export class DetectorBuilder {
|
export class DetectorBuilder {
|
||||||
private readonly existingPartsInUserAgent = new Array<string>();
|
private readonly existingPartsInUserAgent = new Array<string>();
|
||||||
private readonly notExistingPartsInUserAgent = new Array<string>();
|
|
||||||
|
|
||||||
constructor(private readonly os: OperatingSystem) { }
|
private readonly notExistingPartsInUserAgent = new Array<string>();
|
||||||
|
|
||||||
public mustInclude(str: string): DetectorBuilder {
|
constructor(private readonly os: OperatingSystem) { }
|
||||||
return this.add(str, this.existingPartsInUserAgent);
|
|
||||||
|
public mustInclude(str: string): DetectorBuilder {
|
||||||
|
return this.add(str, this.existingPartsInUserAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public mustNotInclude(str: string): DetectorBuilder {
|
||||||
|
return this.add(str, this.notExistingPartsInUserAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): IBrowserOsDetector {
|
||||||
|
if (!this.existingPartsInUserAgent.length) {
|
||||||
|
throw new Error('Must include at least a part');
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
detect: (agent) => this.detect(agent),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public mustNotInclude(str: string): DetectorBuilder {
|
private detect(userAgent: string): OperatingSystem {
|
||||||
return this.add(str, this.notExistingPartsInUserAgent);
|
if (!userAgent) {
|
||||||
|
throw new Error('missing userAgent');
|
||||||
}
|
}
|
||||||
|
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.os;
|
||||||
|
}
|
||||||
|
|
||||||
public build(): IBrowserOsDetector {
|
private add(part: string, array: string[]): DetectorBuilder {
|
||||||
if (!this.existingPartsInUserAgent.length) {
|
if (!part) {
|
||||||
throw new Error('Must include at least a part');
|
throw new Error('part is empty or undefined');
|
||||||
}
|
|
||||||
return {
|
|
||||||
detect: (agent) => this.detect(agent),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
if (this.existingPartsInUserAgent.includes(part)) {
|
||||||
private detect(userAgent: string): OperatingSystem {
|
throw new Error(`part ${part} is already included as existing part`);
|
||||||
if (!userAgent) {
|
|
||||||
throw new Error('User agent is null or undefined');
|
|
||||||
}
|
|
||||||
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return this.os;
|
|
||||||
}
|
}
|
||||||
|
if (this.notExistingPartsInUserAgent.includes(part)) {
|
||||||
private add(part: string, array: string[]): DetectorBuilder {
|
throw new Error(`part ${part} is already included as not existing part`);
|
||||||
if (!part) {
|
|
||||||
throw new Error('part is empty or undefined');
|
|
||||||
}
|
|
||||||
if (this.existingPartsInUserAgent.includes(part)) {
|
|
||||||
throw new Error(`part ${part} is already included as existing part`);
|
|
||||||
}
|
|
||||||
if (this.notExistingPartsInUserAgent.includes(part)) {
|
|
||||||
throw new Error(`part ${part} is already included as not existing part`);
|
|
||||||
}
|
|
||||||
array.push(part);
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
array.push(part);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export interface IBrowserOsDetector {
|
export interface IBrowserOsDetector {
|
||||||
detect(userAgent: string): OperatingSystem | undefined;
|
detect(userAgent: string): OperatingSystem | undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +1,89 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||||
import { IEnvironment } from './IEnvironment';
|
import { IEnvironment } from './IEnvironment';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
|
|
||||||
interface IEnvironmentVariables {
|
export interface IEnvironmentVariables {
|
||||||
readonly window: Window & typeof globalThis;
|
readonly window: Window & typeof globalThis;
|
||||||
readonly process: NodeJS.Process;
|
readonly process: NodeJS.Process;
|
||||||
readonly navigator: Navigator;
|
readonly navigator: Navigator;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Environment implements IEnvironment {
|
export class Environment implements IEnvironment {
|
||||||
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
||||||
window,
|
window,
|
||||||
process,
|
process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
|
||||||
navigator,
|
navigator,
|
||||||
});
|
});
|
||||||
public readonly isDesktop: boolean;
|
|
||||||
public readonly os: OperatingSystem;
|
public readonly isDesktop: boolean;
|
||||||
protected constructor(
|
|
||||||
variables: IEnvironmentVariables,
|
public readonly os: OperatingSystem;
|
||||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
|
|
||||||
if (!variables) {
|
protected constructor(
|
||||||
throw new Error('variables is null or empty');
|
variables: IEnvironmentVariables,
|
||||||
}
|
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||||
this.isDesktop = isDesktop(variables);
|
) {
|
||||||
this.os = this.isDesktop ?
|
if (!variables) {
|
||||||
getDesktopOsType(getProcessPlatform(variables))
|
throw new Error('variables is null or empty');
|
||||||
: browserOsDetector.detect(getUserAgent(variables));
|
|
||||||
}
|
}
|
||||||
|
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 {
|
function getUserAgent(variables: IEnvironmentVariables): string {
|
||||||
if (!variables.window || !variables.window.navigator) {
|
if (!variables.window || !variables.window.navigator) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return variables.window.navigator.userAgent;
|
return variables.window.navigator.userAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProcessPlatform(variables: IEnvironmentVariables): string {
|
function getProcessPlatform(variables: IEnvironmentVariables): string {
|
||||||
if (!variables.process || !variables.process.platform) {
|
if (!variables.process || !variables.process.platform) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return variables.process.platform;
|
return variables.process.platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
|
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
|
||||||
// https://nodejs.org/api/process.html#process_process_platform
|
// https://nodejs.org/api/process.html#process_process_platform
|
||||||
if (processPlatform === 'darwin') {
|
switch (processPlatform) {
|
||||||
return OperatingSystem.macOS;
|
case 'darwin':
|
||||||
} else if (processPlatform === 'win32') {
|
return OperatingSystem.macOS;
|
||||||
return OperatingSystem.Windows;
|
case 'win32':
|
||||||
} else if (processPlatform === 'linux') {
|
return OperatingSystem.Windows;
|
||||||
return OperatingSystem.Linux;
|
case 'linux':
|
||||||
}
|
return OperatingSystem.Linux;
|
||||||
return undefined;
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDesktop(variables: IEnvironmentVariables): boolean {
|
function isDesktop(variables: IEnvironmentVariables): boolean {
|
||||||
// More: https://github.com/electron/electron/issues/2288
|
// More: https://github.com/electron/electron/issues/2288
|
||||||
// Renderer process
|
// Renderer process
|
||||||
if (variables.window
|
if (variables.window
|
||||||
&& variables.window.process
|
&& variables.window.process
|
||||||
&& variables.window.process.type === 'renderer') {
|
&& variables.window.process.type === 'renderer') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Main process
|
// Main process
|
||||||
if (variables.process
|
if (variables.process
|
||||||
&& variables.process.versions
|
&& variables.process.versions
|
||||||
&& Boolean(variables.process.versions.electron)) {
|
&& Boolean(variables.process.versions.electron)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Detect the user agent when the `nodeIntegration` option is set to true
|
// Detect the user agent when the `nodeIntegration` option is set to true
|
||||||
if (variables.navigator
|
if (variables.navigator
|
||||||
&& variables.navigator.userAgent
|
&& variables.navigator.userAgent
|
||||||
&& variables.navigator.userAgent.includes('Electron')) {
|
&& variables.navigator.userAgent.includes('Electron')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export interface IEnvironment {
|
export interface IEnvironment {
|
||||||
readonly isDesktop: boolean;
|
readonly isDesktop: boolean;
|
||||||
readonly os: OperatingSystem;
|
readonly os: OperatingSystem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
export interface IApplicationFactory {
|
export interface IApplicationFactory {
|
||||||
getApp(): Promise<IApplication>;
|
getApp(): Promise<IApplication>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,41 @@
|
|||||||
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
import WindowsData from '@/application/collections/windows.yaml';
|
||||||
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
import MacOsData from '@/application/collections/macos.yaml';
|
||||||
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
|
|
||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
|
|
||||||
export function parseApplication(
|
export function parseApplication(
|
||||||
parser = CategoryCollectionParser,
|
parser = CategoryCollectionParser,
|
||||||
processEnv: NodeJS.ProcessEnv = process.env,
|
processEnv: NodeJS.ProcessEnv = process.env,
|
||||||
collectionsData = PreParsedCollections): IApplication {
|
collectionsData = PreParsedCollections,
|
||||||
validateCollectionsData(collectionsData);
|
): IApplication {
|
||||||
const information = parseProjectInformation(processEnv);
|
validateCollectionsData(collectionsData);
|
||||||
const collections = collectionsData.map((collection) => parser(collection, information));
|
const information = parseProjectInformation(processEnv);
|
||||||
const app = new Application(information, collections);
|
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||||
return app;
|
const app = new Application(information, collections);
|
||||||
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CategoryCollectionParserType
|
export type CategoryCollectionParserType
|
||||||
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
||||||
|
|
||||||
const CategoryCollectionParser: CategoryCollectionParserType
|
const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
|
||||||
= (file, info) => parseCategoryCollection(file, info);
|
return parseCategoryCollection(file, info);
|
||||||
|
};
|
||||||
|
|
||||||
const PreParsedCollections: readonly CollectionData []
|
const PreParsedCollections: readonly CollectionData [] = [
|
||||||
= [ WindowsData, MacOsData ];
|
WindowsData, MacOsData,
|
||||||
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
if (!collections.length) {
|
if (!collections || !collections.length) {
|
||||||
throw new Error('no collection provided');
|
throw new Error('missing collections');
|
||||||
}
|
}
|
||||||
if (collections.some((collection) => !collection)) {
|
if (collections.some((collection) => !collection)) {
|
||||||
throw new Error('undefined collection provided');
|
throw new Error('missing collection provided');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,37 @@
|
|||||||
import { Category } from '@/domain/Category';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
|
||||||
import { parseCategory } from './CategoryParser';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { createEnumParser } from '../Common/Enum';
|
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
|
import { createEnumParser } from '../Common/Enum';
|
||||||
|
import { parseCategory } from './CategoryParser';
|
||||||
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||||
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
|
|
||||||
export function parseCategoryCollection(
|
export function parseCategoryCollection(
|
||||||
content: CollectionData,
|
content: CollectionData,
|
||||||
info: IProjectInformation,
|
info: IProjectInformation,
|
||||||
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
osParser = createEnumParser(OperatingSystem),
|
||||||
validate(content);
|
): ICategoryCollection {
|
||||||
const scripting = new ScriptingDefinitionParser()
|
validate(content);
|
||||||
.parse(content.scripting, info);
|
const scripting = new ScriptingDefinitionParser()
|
||||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
.parse(content.scripting, info);
|
||||||
const categories = new Array<Category>();
|
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||||
for (const action of content.actions) {
|
const categories = content.actions.map((action) => parseCategory(action, context));
|
||||||
const category = parseCategory(action, context);
|
const os = osParser.parseEnum(content.os, 'os');
|
||||||
categories.push(category);
|
const collection = new CategoryCollection(
|
||||||
}
|
os,
|
||||||
const os = osParser.parseEnum(content.os, 'os');
|
categories,
|
||||||
const collection = new CategoryCollection(
|
scripting,
|
||||||
os,
|
);
|
||||||
categories,
|
return collection;
|
||||||
scripting);
|
|
||||||
return collection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate(content: CollectionData): void {
|
function validate(content: CollectionData): void {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
throw new Error('content is null or undefined');
|
throw new Error('missing content');
|
||||||
}
|
}
|
||||||
if (!content.actions || content.actions.length <= 0) {
|
if (!content.actions || content.actions.length <= 0) {
|
||||||
throw new Error('content does not define any action');
|
throw new Error('content does not define any action');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,86 @@
|
|||||||
import { CategoryData, ScriptData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
|
import type {
|
||||||
|
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
|
||||||
|
} from '@/application/collections/';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||||
import { parseScript } from './Script/ScriptParser';
|
import { parseScript } from './Script/ScriptParser';
|
||||||
|
|
||||||
let categoryIdCounter: number = 0;
|
let categoryIdCounter = 0;
|
||||||
|
|
||||||
interface ICategoryChildren {
|
export function parseCategory(
|
||||||
subCategories: Category[];
|
category: CategoryData,
|
||||||
subScripts: Script[];
|
context: ICategoryCollectionParseContext,
|
||||||
}
|
): Category {
|
||||||
|
if (!context) { throw new Error('missing context'); }
|
||||||
export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category {
|
ensureValid(category);
|
||||||
if (!context) { throw new Error('undefined context'); }
|
const children: ICategoryChildren = {
|
||||||
ensureValid(category);
|
subCategories: new Array<Category>(),
|
||||||
const children: ICategoryChildren = {
|
subScripts: new Array<Script>(),
|
||||||
subCategories: new Array<Category>(),
|
};
|
||||||
subScripts: new Array<Script>(),
|
for (const data of category.children) {
|
||||||
};
|
parseCategoryChild(data, children, category, context);
|
||||||
for (const data of category.children) {
|
}
|
||||||
parseCategoryChild(data, children, category, context);
|
return new Category(
|
||||||
}
|
/* id: */ categoryIdCounter++,
|
||||||
return new Category(
|
/* name: */ category.category,
|
||||||
/*id*/ categoryIdCounter++,
|
/* docs: */ parseDocUrls(category),
|
||||||
/*name*/ category.category,
|
/* categories: */ children.subCategories,
|
||||||
/*docs*/ parseDocUrls(category),
|
/* scripts: */ children.subScripts,
|
||||||
/*categories*/ children.subCategories,
|
);
|
||||||
/*scripts*/ children.subScripts,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValid(category: CategoryData) {
|
function ensureValid(category: CategoryData) {
|
||||||
if (!category) {
|
if (!category) {
|
||||||
throw Error('category is null or undefined');
|
throw Error('missing category');
|
||||||
}
|
}
|
||||||
if (!category.children || category.children.length === 0) {
|
if (!category.children || category.children.length === 0) {
|
||||||
throw Error(`category has no children: "${category.category}"`);
|
throw Error(`category has no children: "${category.category}"`);
|
||||||
}
|
}
|
||||||
if (!category.category || category.category.length === 0) {
|
if (!category.category || category.category.length === 0) {
|
||||||
throw Error('category has no name');
|
throw Error('category has no name');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICategoryChildren {
|
||||||
|
subCategories: Category[];
|
||||||
|
subScripts: Script[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategoryChild(
|
function parseCategoryChild(
|
||||||
data: CategoryOrScriptData,
|
data: CategoryOrScriptData,
|
||||||
children: ICategoryChildren,
|
children: ICategoryChildren,
|
||||||
parent: CategoryData,
|
parent: CategoryData,
|
||||||
context: ICategoryCollectionParseContext) {
|
context: ICategoryCollectionParseContext,
|
||||||
if (isCategory(data)) {
|
) {
|
||||||
const subCategory = parseCategory(data as CategoryData, context);
|
if (isCategory(data)) {
|
||||||
children.subCategories.push(subCategory);
|
const subCategory = parseCategory(data as CategoryData, context);
|
||||||
} else if (isScript(data)) {
|
children.subCategories.push(subCategory);
|
||||||
const scriptData = data as ScriptData;
|
} else if (isScript(data)) {
|
||||||
const script = parseScript(scriptData, context);
|
const scriptData = data as ScriptData;
|
||||||
children.subScripts.push(script);
|
const script = parseScript(scriptData, context);
|
||||||
} else {
|
children.subScripts.push(script);
|
||||||
throw new Error(`Child element is neither a category or a script.
|
} else {
|
||||||
|
throw new Error(`Child element is neither a category or a script.
|
||||||
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
|
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isScript(data: any): boolean {
|
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||||
return (data.code && data.code.length > 0)
|
const holder = (data as InstructionHolder);
|
||||||
|| data.call;
|
return hasCode(holder) || hasCall(holder);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCategory(data: any): boolean {
|
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||||
return data.category && data.category.length > 0;
|
const { category } = data as CategoryData;
|
||||||
|
return category && category.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCode(holder: InstructionHolder): boolean {
|
||||||
|
return holder.code && holder.code.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCall(holder: InstructionHolder) {
|
||||||
|
return holder.call !== undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,64 @@
|
|||||||
import { DocumentableData, DocumentationUrlsData } from 'js-yaml-loader!@/*';
|
import type { DocumentableData, DocumentationUrlsData } from '@/application/collections/';
|
||||||
|
|
||||||
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
||||||
if (!documentable) {
|
if (!documentable) {
|
||||||
throw new Error('documentable is null or undefined');
|
throw new Error('missing documentable');
|
||||||
}
|
}
|
||||||
const docs = documentable.docs;
|
const { docs } = documentable;
|
||||||
if (!docs || !docs.length) {
|
if (!docs || !docs.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
let result = new DocumentationUrlContainer();
|
let result = new DocumentationUrlContainer();
|
||||||
result = addDocs(docs, result);
|
result = addDocs(docs, result);
|
||||||
return result.getAll();
|
return result.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer {
|
function addDocs(
|
||||||
if (docs instanceof Array) {
|
docs: DocumentationUrlsData,
|
||||||
urls.addUrls(docs);
|
urls: DocumentationUrlContainer,
|
||||||
} else if (typeof docs === 'string') {
|
): DocumentationUrlContainer {
|
||||||
urls.addUrl(docs);
|
if (docs instanceof Array) {
|
||||||
} else {
|
urls.addUrls(docs);
|
||||||
throw new Error('Docs field (documentation url) must a string or array of strings');
|
} else if (typeof docs === 'string') {
|
||||||
}
|
urls.addUrl(docs);
|
||||||
return urls;
|
} else {
|
||||||
|
throw new Error('Docs field (documentation url) must a string or array of strings');
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DocumentationUrlContainer {
|
class DocumentationUrlContainer {
|
||||||
private readonly urls = new Array<string>();
|
private readonly urls = new Array<string>();
|
||||||
|
|
||||||
public addUrl(url: string) {
|
public addUrl(url: string) {
|
||||||
validateUrl(url);
|
validateUrl(url);
|
||||||
this.urls.push(url);
|
this.urls.push(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addUrls(urls: readonly any[]) {
|
public addUrls(urls: readonly string[]) {
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
if (typeof url !== 'string') {
|
if (typeof url !== 'string') {
|
||||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
throw new Error('Docs field (documentation url) must be an array of strings');
|
||||||
}
|
}
|
||||||
this.addUrl(url);
|
this.addUrl(url);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public getAll(): ReadonlyArray<string> {
|
public getAll(): ReadonlyArray<string> {
|
||||||
return this.urls;
|
return this.urls;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateUrl(docUrl: string): void {
|
function validateUrl(docUrl: string): void {
|
||||||
if (!docUrl) {
|
if (!docUrl) {
|
||||||
throw new Error('Documentation url is null or empty');
|
throw new Error('Documentation url is null or empty');
|
||||||
}
|
}
|
||||||
if (docUrl.includes('\n')) {
|
if (docUrl.includes('\n')) {
|
||||||
throw new Error('Documentation url cannot be multi-lined.');
|
throw new Error('Documentation url cannot be multi-lined.');
|
||||||
}
|
}
|
||||||
const res = docUrl.match(
|
const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
|
||||||
/(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) {
|
if (res == null) {
|
||||||
throw new Error(`Invalid documentation url: ${docUrl}`);
|
throw new Error(`Invalid documentation url: ${docUrl}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
|
import { Version } from '@/domain/Version';
|
||||||
|
|
||||||
export function parseProjectInformation(
|
export function parseProjectInformation(
|
||||||
environment: NodeJS.ProcessEnv): IProjectInformation {
|
environment: NodeJS.ProcessEnv,
|
||||||
return new ProjectInformation(
|
): IProjectInformation {
|
||||||
environment.VUE_APP_NAME,
|
const version = new Version(environment.VUE_APP_VERSION);
|
||||||
environment.VUE_APP_VERSION,
|
return new ProjectInformation(
|
||||||
environment.VUE_APP_REPOSITORY_URL,
|
environment.VUE_APP_NAME,
|
||||||
environment.VUE_APP_HOMEPAGE_URL,
|
version,
|
||||||
);
|
environment.VUE_APP_REPOSITORY_URL,
|
||||||
|
environment.VUE_APP_HOMEPAGE_URL,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import type { FunctionData } from '@/application/collections/';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
import { FunctionData } from 'js-yaml-loader!@/*';
|
|
||||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
@@ -8,15 +8,17 @@ import { SyntaxFactory } from './Syntax/SyntaxFactory';
|
|||||||
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
||||||
|
|
||||||
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||||
public readonly compiler: IScriptCompiler;
|
public readonly compiler: IScriptCompiler;
|
||||||
public readonly syntax: ILanguageSyntax;
|
|
||||||
|
|
||||||
constructor(
|
public readonly syntax: ILanguageSyntax;
|
||||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
|
||||||
scripting: IScriptingDefinition,
|
constructor(
|
||||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) {
|
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||||
if (!scripting) { throw new Error('undefined scripting'); }
|
scripting: IScriptingDefinition,
|
||||||
this.syntax = syntaxFactory.create(scripting.language);
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
) {
|
||||||
}
|
if (!scripting) { throw new Error('missing scripting'); }
|
||||||
|
this.syntax = syntaxFactory.create(scripting.language);
|
||||||
|
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,65 @@
|
|||||||
import { ExpressionPosition } from './ExpressionPosition';
|
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||||
import { IExpression } from './IExpression';
|
|
||||||
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
|
||||||
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
import { IExpression } from './IExpression';
|
||||||
import { ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
|
import { ExpressionEvaluationContext, IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||||
|
|
||||||
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||||
export class Expression implements IExpression {
|
export class Expression implements IExpression {
|
||||||
constructor(
|
public readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
public readonly position: ExpressionPosition,
|
|
||||||
public readonly evaluator: ExpressionEvaluator,
|
constructor(
|
||||||
public readonly parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection()) {
|
public readonly position: ExpressionPosition,
|
||||||
if (!position) {
|
public readonly evaluator: ExpressionEvaluator,
|
||||||
throw new Error('undefined position');
|
parameters?: IReadOnlyFunctionParameterCollection,
|
||||||
}
|
) {
|
||||||
if (!evaluator) {
|
if (!position) {
|
||||||
throw new Error('undefined evaluator');
|
throw new Error('missing position');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
public evaluate(context: IExpressionEvaluationContext): string {
|
if (!evaluator) {
|
||||||
if (!context) {
|
throw new Error('missing evaluator');
|
||||||
throw new Error('undefined context');
|
|
||||||
}
|
|
||||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
|
||||||
const args = filterUnusedArguments(this.parameters, context.args);
|
|
||||||
context = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
|
||||||
return this.evaluator(context);
|
|
||||||
}
|
}
|
||||||
|
this.parameters = parameters ?? new FunctionParameterCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(context: IExpressionEvaluationContext): string {
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('missing context');
|
||||||
|
}
|
||||||
|
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||||
|
const args = filterUnusedArguments(this.parameters, context.args);
|
||||||
|
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||||
|
return this.evaluator(filteredContext);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateThatAllRequiredParametersAreSatisfied(
|
function validateThatAllRequiredParametersAreSatisfied(
|
||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
) {
|
) {
|
||||||
const requiredParameterNames = parameters
|
const requiredParameterNames = parameters
|
||||||
.all
|
.all
|
||||||
.filter((parameter) => !parameter.isOptional)
|
.filter((parameter) => !parameter.isOptional)
|
||||||
.map((parameter) => parameter.name);
|
.map((parameter) => parameter.name);
|
||||||
const missingParameterNames = requiredParameterNames
|
const missingParameterNames = requiredParameterNames
|
||||||
.filter((parameterName) => !args.hasArgument(parameterName));
|
.filter((parameterName) => !args.hasArgument(parameterName));
|
||||||
if (missingParameterNames.length) {
|
if (missingParameterNames.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`);
|
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`,
|
||||||
}
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterUnusedArguments(
|
function filterUnusedArguments(
|
||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection): IReadOnlyFunctionCallArgumentCollection {
|
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
const specificCallArgs = new FunctionCallArgumentCollection();
|
): IReadOnlyFunctionCallArgumentCollection {
|
||||||
for (const parameter of parameters.all) {
|
const specificCallArgs = new FunctionCallArgumentCollection();
|
||||||
if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) {
|
parameters.all
|
||||||
continue; // Optional parameter is not necessarily provided
|
.filter((parameter) => allFunctionArgs.hasArgument(parameter.name))
|
||||||
}
|
.map((parameter) => allFunctionArgs.getArgument(parameter.name))
|
||||||
const arg = allFunctionArgs.getArgument(parameter.name);
|
.forEach((argument) => specificCallArgs.addArgument(argument));
|
||||||
specificCallArgs.addArgument(arg);
|
return specificCallArgs;
|
||||||
}
|
|
||||||
return specificCallArgs;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ import { IPipelineCompiler } from '../Pipes/IPipelineCompiler';
|
|||||||
import { PipelineCompiler } from '../Pipes/PipelineCompiler';
|
import { PipelineCompiler } from '../Pipes/PipelineCompiler';
|
||||||
|
|
||||||
export interface IExpressionEvaluationContext {
|
export interface IExpressionEvaluationContext {
|
||||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||||
readonly pipelineCompiler: IPipelineCompiler;
|
readonly pipelineCompiler: IPipelineCompiler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
|
export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler()) {
|
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
||||||
if (!args) {
|
) {
|
||||||
throw new Error('undefined args, send empty collection instead');
|
if (!args) {
|
||||||
}
|
throw new Error('missing args, send empty collection instead.');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
export class ExpressionPosition {
|
export class ExpressionPosition {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly start: number,
|
public readonly start: number,
|
||||||
public readonly end: number) {
|
public readonly end: number,
|
||||||
if (start === end) {
|
) {
|
||||||
throw new Error(`no length (start = end = ${start})`);
|
if (start === end) {
|
||||||
}
|
throw new Error(`no length (start = end = ${start})`);
|
||||||
if (start > end) {
|
|
||||||
throw Error(`start (${start}) after end (${end})`);
|
|
||||||
}
|
|
||||||
if (start < 0) {
|
|
||||||
throw Error(`negative start position: ${start}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (start > end) {
|
||||||
|
throw Error(`start (${start}) after end (${end})`);
|
||||||
|
}
|
||||||
|
if (start < 0) {
|
||||||
|
throw Error(`negative start position: ${start}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ExpressionPosition } from './ExpressionPosition';
|
|
||||||
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||||
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||||
|
|
||||||
export interface IExpression {
|
export interface IExpression {
|
||||||
readonly position: ExpressionPosition;
|
readonly position: ExpressionPosition;
|
||||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
evaluate(context: IExpressionEvaluationContext): string;
|
evaluate(context: IExpressionEvaluationContext): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +1,85 @@
|
|||||||
|
import { IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
import { IExpressionsCompiler } from './IExpressionsCompiler';
|
import { IExpressionsCompiler } from './IExpressionsCompiler';
|
||||||
import { IExpression } from './Expression/IExpression';
|
import { IExpression } from './Expression/IExpression';
|
||||||
import { IExpressionParser } from './Parser/IExpressionParser';
|
import { IExpressionParser } from './Parser/IExpressionParser';
|
||||||
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
||||||
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
|
||||||
import { ExpressionEvaluationContext } from './Expression/ExpressionEvaluationContext';
|
|
||||||
import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
|
||||||
|
|
||||||
export class ExpressionsCompiler implements IExpressionsCompiler {
|
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
|
private readonly extractor: IExpressionParser = new CompositeExpressionParser(),
|
||||||
public compileExpressions(
|
) { }
|
||||||
code: string,
|
|
||||||
args: IReadOnlyFunctionCallArgumentCollection): string {
|
public compileExpressions(
|
||||||
if (!args) {
|
code: string | undefined,
|
||||||
throw new Error('undefined args, send empty collection instead');
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
}
|
): string {
|
||||||
const expressions = this.extractor.findExpressions(code);
|
if (!args) {
|
||||||
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
throw new Error('missing args, send empty collection instead.');
|
||||||
const context = new ExpressionEvaluationContext(args);
|
|
||||||
const compiledCode = compileExpressions(expressions, code, context);
|
|
||||||
return compiledCode;
|
|
||||||
}
|
}
|
||||||
|
if (!code) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
const expressions = this.extractor.findExpressions(code);
|
||||||
|
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
||||||
|
const context = new ExpressionEvaluationContext(args);
|
||||||
|
const compiledCode = compileExpressions(expressions, code, context);
|
||||||
|
return compiledCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileExpressions(
|
function compileExpressions(
|
||||||
expressions: readonly IExpression[],
|
expressions: readonly IExpression[],
|
||||||
code: string,
|
code: string,
|
||||||
context: IExpressionEvaluationContext) {
|
context: IExpressionEvaluationContext,
|
||||||
let compiledCode = '';
|
) {
|
||||||
const sortedExpressions = expressions
|
let compiledCode = '';
|
||||||
.slice() // copy the array to not mutate the parameter
|
const sortedExpressions = expressions
|
||||||
.sort((a, b) => b.position.start - a.position.start);
|
.slice() // copy the array to not mutate the parameter
|
||||||
let index = 0;
|
.sort((a, b) => b.position.start - a.position.start);
|
||||||
while (index !== code.length) {
|
let index = 0;
|
||||||
const nextExpression = sortedExpressions.pop();
|
while (index !== code.length) {
|
||||||
if (nextExpression) {
|
const nextExpression = sortedExpressions.pop();
|
||||||
compiledCode += code.substring(index, nextExpression.position.start);
|
if (nextExpression) {
|
||||||
const expressionCode = nextExpression.evaluate(context);
|
compiledCode += code.substring(index, nextExpression.position.start);
|
||||||
compiledCode += expressionCode;
|
const expressionCode = nextExpression.evaluate(context);
|
||||||
index = nextExpression.position.end;
|
compiledCode += expressionCode;
|
||||||
} else {
|
index = nextExpression.position.end;
|
||||||
compiledCode += code.substring(index, code.length);
|
} else {
|
||||||
break;
|
compiledCode += code.substring(index, code.length);
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
return compiledCode;
|
}
|
||||||
|
return compiledCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractRequiredParameterNames(
|
function extractRequiredParameterNames(
|
||||||
expressions: readonly IExpression[]): string[] {
|
expressions: readonly IExpression[],
|
||||||
const usedParameterNames = expressions
|
): string[] {
|
||||||
.map((e) => e.parameters.all
|
return expressions
|
||||||
.filter((p) => !p.isOptional)
|
.map((e) => e.parameters.all
|
||||||
.map((p) => p.name))
|
.filter((p) => !p.isOptional)
|
||||||
.filter((p) => p)
|
.map((p) => p.name))
|
||||||
.flat();
|
.filter(Boolean) // Remove empty or undefined
|
||||||
const uniqueParameterNames = Array.from(new Set(usedParameterNames));
|
.flat()
|
||||||
return uniqueParameterNames;
|
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureParamsUsedInCodeHasArgsProvided(
|
function ensureParamsUsedInCodeHasArgsProvided(
|
||||||
expressions: readonly IExpression[],
|
expressions: readonly IExpression[],
|
||||||
providedArgs: IReadOnlyFunctionCallArgumentCollection): void {
|
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
): void {
|
||||||
if (!usedParameterNames?.length) {
|
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||||
return;
|
if (!usedParameterNames?.length) {
|
||||||
}
|
return;
|
||||||
const notProvidedParameters = usedParameterNames
|
}
|
||||||
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
|
const notProvidedParameters = usedParameterNames
|
||||||
if (notProvidedParameters.length) {
|
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
|
||||||
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
|
if (notProvidedParameters.length) {
|
||||||
}
|
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function printList(list: readonly string[]): string {
|
function printList(list: readonly string[]): string {
|
||||||
return `"${list.join('", "')}"`;
|
return `"${list.join('", "')}"`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
export interface IExpressionsCompiler {
|
export interface IExpressionsCompiler {
|
||||||
compileExpressions(
|
compileExpressions(
|
||||||
code: string,
|
code: string | undefined,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection): string;
|
args: IReadOnlyFunctionCallArgumentCollection): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
import { IExpression } from '../Expression/IExpression';
|
import { IExpression } from '../Expression/IExpression';
|
||||||
import { IExpressionParser } from './IExpressionParser';
|
|
||||||
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
||||||
import { WithParser } from '../SyntaxParsers/WithParser';
|
import { WithParser } from '../SyntaxParsers/WithParser';
|
||||||
|
import { IExpressionParser } from './IExpressionParser';
|
||||||
|
|
||||||
const Parsers = [
|
const Parsers = [
|
||||||
new ParameterSubstitutionParser(),
|
new ParameterSubstitutionParser(),
|
||||||
new WithParser(),
|
new WithParser(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export class CompositeExpressionParser implements IExpressionParser {
|
export class CompositeExpressionParser implements IExpressionParser {
|
||||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||||
if (leafs.some((leaf) => !leaf)) {
|
if (!leafs) {
|
||||||
throw new Error('undefined leaf');
|
throw new Error('missing leafs');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
public findExpressions(code: string): IExpression[] {
|
if (leafs.some((leaf) => !leaf)) {
|
||||||
const expressions = new Array<IExpression>();
|
throw new Error('missing leaf');
|
||||||
for (const parser of this.leafs) {
|
|
||||||
const newExpressions = parser.findExpressions(code);
|
|
||||||
if (newExpressions && newExpressions.length) {
|
|
||||||
expressions.push(...newExpressions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return expressions;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public findExpressions(code: string): IExpression[] {
|
||||||
|
return this.leafs.flatMap(
|
||||||
|
(parser) => parser.findExpressions(code) || [],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IExpression } from '../Expression/IExpression';
|
import { IExpression } from '../Expression/IExpression';
|
||||||
|
|
||||||
export interface IExpressionParser {
|
export interface IExpressionParser {
|
||||||
findExpressions(code: string): IExpression[];
|
findExpressions(code: string): IExpression[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,60 @@
|
|||||||
export class ExpressionRegexBuilder {
|
export class ExpressionRegexBuilder {
|
||||||
private readonly parts = new Array<string>();
|
private readonly parts = new Array<string>();
|
||||||
|
|
||||||
public expectCharacters(characters: string) {
|
public expectCharacters(characters: string) {
|
||||||
return this.addRawRegex(
|
return this.addRawRegex(
|
||||||
characters
|
characters
|
||||||
.replaceAll('$', '\\$')
|
.replaceAll('$', '\\$')
|
||||||
.replaceAll('.', '\\.'),
|
.replaceAll('.', '\\.'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectOneOrMoreWhitespaces() {
|
public expectOneOrMoreWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.addRawRegex('\\s+');
|
.addRawRegex('\\s+');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchPipeline() {
|
public matchPipeline() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectZeroOrMoreWhitespaces()
|
||||||
.addRawRegex('(\\|\\s*.+?)?');
|
.addRawRegex('(\\|\\s*.+?)?');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchUntilFirstWhitespace() {
|
public matchUntilFirstWhitespace() {
|
||||||
return this
|
return this
|
||||||
.addRawRegex('([^|\\s]+)');
|
.addRawRegex('([^|\\s]+)');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchAnythingExceptSurroundingWhitespaces() {
|
public matchAnythingExceptSurroundingWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectZeroOrMoreWhitespaces()
|
||||||
.addRawRegex('(.+?)')
|
.addRawRegex('(.+?)')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectZeroOrMoreWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectExpressionStart() {
|
public expectExpressionStart() {
|
||||||
return this
|
return this
|
||||||
.expectCharacters('{{')
|
.expectCharacters('{{')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectZeroOrMoreWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectExpressionEnd() {
|
public expectExpressionEnd() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectZeroOrMoreWhitespaces()
|
||||||
.expectCharacters('}}');
|
.expectCharacters('}}');
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildRegExp(): RegExp {
|
public buildRegExp(): RegExp {
|
||||||
return new RegExp(this.parts.join(''), 'g');
|
return new RegExp(this.parts.join(''), 'g');
|
||||||
}
|
}
|
||||||
|
|
||||||
private expectZeroOrMoreWhitespaces() {
|
private expectZeroOrMoreWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.addRawRegex('\\s*');
|
.addRawRegex('\\s*');
|
||||||
}
|
}
|
||||||
private addRawRegex(regex: string) {
|
|
||||||
this.parts.push(regex);
|
private addRawRegex(regex: string) {
|
||||||
return this;
|
this.parts.push(regex);
|
||||||
}
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,43 +6,54 @@ import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParamet
|
|||||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||||
|
|
||||||
export abstract class RegexParser implements IExpressionParser {
|
export abstract class RegexParser implements IExpressionParser {
|
||||||
protected abstract readonly regex: RegExp;
|
protected abstract readonly regex: RegExp;
|
||||||
|
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
return Array.from(this.findRegexExpressions(code));
|
return Array.from(this.findRegexExpressions(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
||||||
|
|
||||||
|
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('missing code');
|
||||||
}
|
}
|
||||||
|
const matches = code.matchAll(this.regex);
|
||||||
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
for (const match of matches) {
|
||||||
|
const primitiveExpression = this.buildExpression(match);
|
||||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
|
||||||
const matches = Array.from(code.matchAll(this.regex));
|
const parameters = createParameters(primitiveExpression);
|
||||||
for (const match of matches) {
|
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||||
const startPos = match.index;
|
yield expression;
|
||||||
const endPos = startPos + match[0].length;
|
|
||||||
let position: ExpressionPosition;
|
|
||||||
try {
|
|
||||||
position = new ExpressionPosition(startPos, endPos);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
|
|
||||||
}
|
|
||||||
const primitiveExpression = this.buildExpression(match);
|
|
||||||
const parameters = getParameters(primitiveExpression);
|
|
||||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
|
||||||
yield expression;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private doOrRethrow<T>(action: () => T, errorText: string, code: string): T {
|
||||||
|
try {
|
||||||
|
return action();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`[${this.constructor.name}] ${errorText}: ${error.message}\nRegex: ${this.regex}\nCode: ${code}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPosition(match: RegExpMatchArray): ExpressionPosition {
|
||||||
|
const startPos = match.index;
|
||||||
|
const endPos = startPos + match[0].length;
|
||||||
|
return new ExpressionPosition(startPos, endPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createParameters(
|
||||||
|
expression: IPrimitiveExpression,
|
||||||
|
): FunctionParameterCollection {
|
||||||
|
return (expression.parameters || [])
|
||||||
|
.reduce((parameters, parameter) => {
|
||||||
|
parameters.addParameter(parameter);
|
||||||
|
return parameters;
|
||||||
|
}, new FunctionParameterCollection());
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPrimitiveExpression {
|
export interface IPrimitiveExpression {
|
||||||
evaluator: ExpressionEvaluator;
|
evaluator: ExpressionEvaluator;
|
||||||
parameters?: readonly IFunctionParameter[];
|
parameters?: readonly IFunctionParameter[];
|
||||||
}
|
|
||||||
|
|
||||||
function getParameters(
|
|
||||||
expression: IPrimitiveExpression): FunctionParameterCollection {
|
|
||||||
const parameters = new FunctionParameterCollection();
|
|
||||||
for (const parameter of expression.parameters || []) {
|
|
||||||
parameters.addParameter(parameter);
|
|
||||||
}
|
|
||||||
return parameters;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface IPipe {
|
export interface IPipe {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
apply(input: string): string;
|
apply(input: string): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export interface IPipelineCompiler {
|
export interface IPipelineCompiler {
|
||||||
compile(value: string, pipeline: string): string;
|
compile(value: string, pipeline: string): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,33 @@
|
|||||||
import { IPipe } from '../IPipe';
|
import { IPipe } from '../IPipe';
|
||||||
|
|
||||||
export class EscapeDoubleQuotes implements IPipe {
|
export class EscapeDoubleQuotes implements IPipe {
|
||||||
public readonly name: string = 'escapeDoubleQuotes';
|
public readonly name: string = 'escapeDoubleQuotes';
|
||||||
public apply(raw: string): string {
|
|
||||||
return raw?.replaceAll('"', '"^""');
|
public apply(raw: string): string {
|
||||||
/*
|
if (!raw) {
|
||||||
"^"" is the most robust and stable choice.
|
return raw;
|
||||||
Other options:
|
|
||||||
""
|
|
||||||
Breaks, because it is fundamentally unsupported
|
|
||||||
""""
|
|
||||||
Does not work with consecutive double quotes.
|
|
||||||
E.g. PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"
|
|
||||||
Works when using: PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"
|
|
||||||
\"
|
|
||||||
May break as they are interpreted by cmd.exe as metacharacters breaking the command
|
|
||||||
E.g. PowerShell -Command "Write-Host 'Hello \"w&orld\"'" does not work due to unescaped "&"
|
|
||||||
Works when using: PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"
|
|
||||||
\""
|
|
||||||
Normalizes interior whitespace
|
|
||||||
E.g. PowerShell -Command "\""a& c\"".length", outputs 4 and discards one of two whitespaces
|
|
||||||
Works when using "^"": PowerShell -Command ""^""a& c"^"".length"
|
|
||||||
A good explanation: https://stackoverflow.com/a/31413730
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
return raw.replaceAll('"', '"^""');
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
/*
|
||||||
|
"^"" is the most robust and stable choice.
|
||||||
|
Other options:
|
||||||
|
""
|
||||||
|
Breaks, because it is fundamentally unsupported
|
||||||
|
""""
|
||||||
|
Does not work with consecutive double quotes.
|
||||||
|
E.g. `PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"`
|
||||||
|
Works when using: `PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"`
|
||||||
|
\"
|
||||||
|
May break as they are interpreted by cmd.exe as metacharacters breaking the command
|
||||||
|
E.g. `PowerShell -Command "Write-Host 'Hello \"w&orld\"'"` does not work due to unescaped "&"
|
||||||
|
Works when using: `PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"`
|
||||||
|
\""
|
||||||
|
Normalizes interior whitespace
|
||||||
|
E.g. `PowerShell -Command "\""a& c\"".length"`, outputs 4 and discards one of two whitespaces
|
||||||
|
Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"`
|
||||||
|
A good explanation: https://stackoverflow.com/a/31413730
|
||||||
|
*/
|
||||||
|
/* eslint-enable max-len */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,155 +1,166 @@
|
|||||||
import { IPipe } from '../IPipe';
|
import { IPipe } from '../IPipe';
|
||||||
|
|
||||||
export class InlinePowerShell implements IPipe {
|
export class InlinePowerShell implements IPipe {
|
||||||
public readonly name: string = 'inlinePowerShell';
|
public readonly name: string = 'inlinePowerShell';
|
||||||
public apply(code: string): string {
|
|
||||||
if (!code || !hasLines(code)) {
|
public apply(code: string): string {
|
||||||
return code;
|
if (!code || !hasLines(code)) {
|
||||||
}
|
return code;
|
||||||
code = inlineComments(code);
|
|
||||||
code = mergeLinesWithBacktick(code);
|
|
||||||
code = mergeHereStrings(code);
|
|
||||||
const lines = getLines(code)
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter((line) => line.length > 0);
|
|
||||||
return lines
|
|
||||||
.join('; ');
|
|
||||||
}
|
}
|
||||||
|
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
|
||||||
|
inlineComments,
|
||||||
|
mergeLinesWithBacktick,
|
||||||
|
mergeHereStrings,
|
||||||
|
mergeNewLines,
|
||||||
|
]).reduce((a, b) => (data) => b(a(data)));
|
||||||
|
const newCode = processor(code);
|
||||||
|
return newCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasLines(text: string) {
|
function hasLines(text: string) {
|
||||||
return text.includes('\n') || text.includes('\r');
|
return text.includes('\n') || text.includes('\r');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
|
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
|
||||||
Otherwise single # comments out rest of the code
|
Otherwise single # comments out rest of the code
|
||||||
*/
|
*/
|
||||||
function inlineComments(code: string): string {
|
function inlineComments(code: string): string {
|
||||||
const makeInlineComment = (comment: string) => {
|
const makeInlineComment = (comment: string) => {
|
||||||
const value = comment?.trim();
|
const value = comment?.trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '<##>';
|
return '<##>';
|
||||||
}
|
}
|
||||||
return `<# ${value} #>`;
|
return `<# ${value} #>`;
|
||||||
};
|
};
|
||||||
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
||||||
if (captureComment === undefined) {
|
if (captureComment === undefined) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
return makeInlineComment(captureComment);
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
Other alternatives considered:
|
||||||
|
--------------------------
|
||||||
|
/#(?<!<#)(?![<>])(.*)$/gm
|
||||||
|
-------------------------
|
||||||
|
✅ Simple, yet matches and captures only what's necessary
|
||||||
|
❌ Fails to match some cases
|
||||||
|
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
||||||
|
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
||||||
|
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one`
|
||||||
|
❌ Uses lookbehind
|
||||||
|
Safari does not yet support lookbehind and syntax, leading application to not
|
||||||
|
load and throw "Invalid regular expression: invalid group specifier name"
|
||||||
|
https://caniuse.com/js-regexp-lookbehind
|
||||||
|
⏩ Usage
|
||||||
|
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (match, captureComment) => {
|
||||||
|
return makeInlineComment(captureComment)
|
||||||
|
});
|
||||||
|
----------------
|
||||||
|
/<#.*?#>|#(.*)/g
|
||||||
|
----------------
|
||||||
|
✅ Simple yet affective
|
||||||
|
❌ Matches all comments, but only captures dash comments
|
||||||
|
❌ Fails to match some cases
|
||||||
|
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
||||||
|
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
||||||
|
⏩ Usage
|
||||||
|
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
||||||
|
if (captureComment === undefined) {
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
return makeInlineComment(captureComment);
|
return makeInlineComment(captureComment);
|
||||||
});
|
});
|
||||||
/*
|
------------------------------------
|
||||||
Other alternatives considered:
|
/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
|
||||||
--------------------------
|
------------------------------------
|
||||||
/#(?<!<#)(?![<>])(.*)$/gm
|
✅ Covers all cases
|
||||||
-------------------------
|
❌ Matches every line, three capture groups are used to build result
|
||||||
✅ Simple, yet matches and captures only what's necessary
|
⏩ Usage
|
||||||
❌ Fails to match some cases
|
return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
|
||||||
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
(match, captureLeft, captureDash, captureComment) => {
|
||||||
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
if (!captureDash) {
|
||||||
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one`
|
return match;
|
||||||
❌ Uses lookbehind
|
}
|
||||||
Safari does not yet support lookbehind and syntax, leading application to not
|
return captureLeft + makeInlineComment(captureComment);
|
||||||
load and throw "Invalid regular expression: invalid group specifier name"
|
});
|
||||||
https://caniuse.com/js-regexp-lookbehind
|
|
||||||
⏩ Usage
|
|
||||||
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (match, captureComment) => {
|
|
||||||
return makeInlineComment(captureComment)
|
|
||||||
});
|
|
||||||
----------------
|
|
||||||
/<#.*?#>|#(.*)/g
|
|
||||||
----------------
|
|
||||||
✅ Simple yet affective
|
|
||||||
❌ Matches all comments, but only captures dash comments
|
|
||||||
❌ Fails to match some cases
|
|
||||||
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
|
||||||
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
|
||||||
⏩ Usage
|
|
||||||
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
|
||||||
if (captureComment === undefined) {
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
return makeInlineComment(captureComment);
|
|
||||||
});
|
|
||||||
------------------------------------
|
|
||||||
/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
|
|
||||||
------------------------------------
|
|
||||||
✅ Covers all cases
|
|
||||||
❌ Matches every line, three capture groups are used to build result
|
|
||||||
⏩ Usage
|
|
||||||
return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
|
|
||||||
(match, captureLeft, captureDash, captureComment) => {
|
|
||||||
if (!captureDash) {
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
return captureLeft + makeInlineComment(captureComment);
|
|
||||||
});
|
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLines(code: string): string [] {
|
function getLines(code: string): string[] {
|
||||||
return (code?.split(/\r\n|\r|\n/) || []);
|
return (code?.split(/\r\n|\r|\n/) || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||||
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
|
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
|
||||||
*/
|
*/
|
||||||
function mergeHereStrings(code: string) {
|
function mergeHereStrings(code: string) {
|
||||||
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
||||||
return code.replaceAll(regex, (_$, quotes, scope) => {
|
return code.replaceAll(regex, (_$, quotes, scope) => {
|
||||||
const newString = getHereStringHandler(quotes);
|
const newString = getHereStringHandler(quotes);
|
||||||
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
||||||
const lines = getLines(escaped);
|
const lines = getLines(escaped);
|
||||||
const inlined = lines.join(newString.separator);
|
const inlined = lines.join(newString.separator);
|
||||||
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
||||||
return quoted;
|
return quoted;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
interface IInlinedHereString {
|
interface IInlinedHereString {
|
||||||
readonly quotesAround: string;
|
readonly quotesAround: string;
|
||||||
readonly escapedQuotes: string;
|
readonly escapedQuotes: string;
|
||||||
readonly separator: string;
|
readonly separator: string;
|
||||||
}
|
}
|
||||||
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
|
|
||||||
function getHereStringHandler(quotes: string): IInlinedHereString {
|
function getHereStringHandler(quotes: string): IInlinedHereString {
|
||||||
const expandableNewLine = '`r`n';
|
/*
|
||||||
switch (quotes) {
|
We handle @' and @" differently.
|
||||||
case '\'':
|
Single quotes are interpreted literally and doubles are expandable.
|
||||||
return {
|
*/
|
||||||
quotesAround: '\'',
|
const expandableNewLine = '`r`n';
|
||||||
escapedQuotes: '\'\'',
|
switch (quotes) {
|
||||||
separator: `\'+"${expandableNewLine}"+\'`,
|
case '\'':
|
||||||
};
|
return {
|
||||||
case '"':
|
quotesAround: '\'',
|
||||||
return {
|
escapedQuotes: '\'\'',
|
||||||
quotesAround: '"',
|
separator: `'+"${expandableNewLine}"+'`,
|
||||||
escapedQuotes: '`"',
|
};
|
||||||
separator: expandableNewLine,
|
case '"':
|
||||||
};
|
return {
|
||||||
default:
|
quotesAround: '"',
|
||||||
throw new Error(`expected quotes: ${quotes}`);
|
escapedQuotes: '`"',
|
||||||
}
|
separator: expandableNewLine,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`expected quotes: ${quotes}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Input ->
|
Input ->
|
||||||
Get-Service * `
|
Get-Service * `
|
||||||
Sort-Object StartType `
|
Sort-Object StartType `
|
||||||
Format-Table Name, ServiceType, Status -AutoSize
|
Format-Table Name, ServiceType, Status -AutoSize
|
||||||
Output ->
|
Output ->
|
||||||
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
|
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
|
||||||
*/
|
*/
|
||||||
function mergeLinesWithBacktick(code: string) {
|
function mergeLinesWithBacktick(code: string) {
|
||||||
/*
|
/*
|
||||||
The regex actually wraps any whitespace character after backtick and before newline
|
The regex actually wraps any whitespace character after backtick and before newline
|
||||||
However, this is not always the case for PowerShell.
|
However, this is not always the case for PowerShell.
|
||||||
I see two behaviors:
|
I see two behaviors:
|
||||||
1. If inside string, it's accepted (inside " or ')
|
1. If inside string, it's accepted (inside " or ')
|
||||||
2. If part of a command, PowerShell throws "An empty pipe element is not allowed"
|
2. If part of a command, PowerShell throws "An empty pipe element is not allowed"
|
||||||
However we don't need to be so robust and handle this complexity (yet), so for easier regex
|
However we don't need to be so robust and handle this complexity (yet), so for easier regex
|
||||||
we wrap it anyway
|
we wrap it anyway
|
||||||
*/
|
*/
|
||||||
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeNewLines(code: string) {
|
||||||
|
return getLines(code)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0)
|
||||||
|
.join('; ');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,45 +3,51 @@ import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
|||||||
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
||||||
|
|
||||||
const RegisteredPipes = [
|
const RegisteredPipes = [
|
||||||
new EscapeDoubleQuotes(),
|
new EscapeDoubleQuotes(),
|
||||||
new InlinePowerShell(),
|
new InlinePowerShell(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface IPipeFactory {
|
export interface IPipeFactory {
|
||||||
get(pipeName: string): IPipe;
|
get(pipeName: string): IPipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PipeFactory implements IPipeFactory {
|
export class PipeFactory implements IPipeFactory {
|
||||||
private readonly pipes = new Map<string, IPipe>();
|
private readonly pipes = new Map<string, IPipe>();
|
||||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
|
||||||
if (pipes.some((pipe) => !pipe)) {
|
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||||
throw new Error('undefined pipe in list');
|
if (!pipes) {
|
||||||
}
|
throw new Error('missing pipes');
|
||||||
for (const pipe of pipes) {
|
|
||||||
this.registerPipe(pipe);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
public get(pipeName: string): IPipe {
|
if (pipes.some((pipe) => !pipe)) {
|
||||||
validatePipeName(pipeName);
|
throw new Error('missing pipe in list');
|
||||||
if (!this.pipes.has(pipeName)) {
|
|
||||||
throw new Error(`Unknown pipe: "${pipeName}"`);
|
|
||||||
}
|
|
||||||
return this.pipes.get(pipeName);
|
|
||||||
}
|
}
|
||||||
private registerPipe(pipe: IPipe): void {
|
for (const pipe of pipes) {
|
||||||
validatePipeName(pipe.name);
|
this.registerPipe(pipe);
|
||||||
if (this.pipes.has(pipe.name)) {
|
|
||||||
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
|
||||||
}
|
|
||||||
this.pipes.set(pipe.name, pipe);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(pipeName: string): IPipe {
|
||||||
|
validatePipeName(pipeName);
|
||||||
|
if (!this.pipes.has(pipeName)) {
|
||||||
|
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||||
|
}
|
||||||
|
return this.pipes.get(pipeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerPipe(pipe: IPipe): void {
|
||||||
|
validatePipeName(pipe.name);
|
||||||
|
if (this.pipes.has(pipe.name)) {
|
||||||
|
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
||||||
|
}
|
||||||
|
this.pipes.set(pipe.name, pipe);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validatePipeName(name: string) {
|
function validatePipeName(name: string) {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new Error('empty pipe name');
|
throw new Error('empty pipe name');
|
||||||
}
|
}
|
||||||
if (!/^[a-z][A-Za-z]*$/.test(name)) {
|
if (!/^[a-z][A-Za-z]*$/.test(name)) {
|
||||||
throw new Error(`Pipe name should be camelCase: "${name}"`);
|
throw new Error(`Pipe name should be camelCase: "${name}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,30 @@ import { IPipeFactory, PipeFactory } from './PipeFactory';
|
|||||||
import { IPipelineCompiler } from './IPipelineCompiler';
|
import { IPipelineCompiler } from './IPipelineCompiler';
|
||||||
|
|
||||||
export class PipelineCompiler implements IPipelineCompiler {
|
export class PipelineCompiler implements IPipelineCompiler {
|
||||||
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
||||||
public compile(value: string, pipeline: string): string {
|
|
||||||
ensureValidArguments(value, pipeline);
|
public compile(value: string, pipeline: string): string {
|
||||||
const pipeNames = extractPipeNames(pipeline);
|
ensureValidArguments(value, pipeline);
|
||||||
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
const pipeNames = extractPipeNames(pipeline);
|
||||||
for (const pipe of pipes) {
|
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
||||||
value = pipe.apply(value);
|
return pipes.reduce((previousValue, pipe) => {
|
||||||
}
|
return pipe.apply(previousValue);
|
||||||
return value;
|
}, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPipeNames(pipeline: string): string[] {
|
function extractPipeNames(pipeline: string): string[] {
|
||||||
return pipeline
|
return pipeline
|
||||||
.trim()
|
.trim()
|
||||||
.split('|')
|
.split('|')
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.map((p) => p.trim());
|
.map((p) => p.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidArguments(value: string, pipeline: string) {
|
function ensureValidArguments(value: string, pipeline: string) {
|
||||||
if (!value) { throw new Error('undefined value'); }
|
if (!value) { throw new Error('missing value'); }
|
||||||
if (!pipeline) { throw new Error('undefined pipeline'); }
|
if (!pipeline) { throw new Error('missing pipeline'); }
|
||||||
if (!pipeline.trimStart().startsWith('|')) {
|
if (!pipeline.trimStart().startsWith('|')) {
|
||||||
throw new Error('pipeline does not start with pipe');
|
throw new Error('pipeline does not start with pipe');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
|
||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
|
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
export class ParameterSubstitutionParser extends RegexParser {
|
export class ParameterSubstitutionParser extends RegexParser {
|
||||||
protected readonly regex = new ExpressionRegexBuilder()
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('$')
|
.expectCharacters('$')
|
||||||
.matchUntilFirstWhitespace() // First match: Parameter name
|
.matchUntilFirstWhitespace() // First match: Parameter name
|
||||||
.matchPipeline() // Second match: Pipeline
|
.matchPipeline() // Second match: Pipeline
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||||
const parameterName = match[1];
|
const parameterName = match[1];
|
||||||
const pipeline = match[2];
|
const pipeline = match[2];
|
||||||
return {
|
return {
|
||||||
parameters: [ new FunctionParameter(parameterName, false) ],
|
parameters: [new FunctionParameter(parameterName, false)],
|
||||||
evaluator: (context) => {
|
evaluator: (context) => {
|
||||||
const argumentValue = context.args.getArgument(parameterName).argumentValue;
|
const { argumentValue } = context.args.getArgument(parameterName);
|
||||||
if (!pipeline) {
|
if (!pipeline) {
|
||||||
return argumentValue;
|
return argumentValue;
|
||||||
}
|
}
|
||||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,59 @@
|
|||||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
|
||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
|
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
export class WithParser extends RegexParser {
|
export class WithParser extends RegexParser {
|
||||||
protected readonly regex = new ExpressionRegexBuilder()
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
// {{ with $parameterName }}
|
// {{ with $parameterName }}
|
||||||
.expectExpressionStart()
|
|
||||||
.expectCharacters('with')
|
|
||||||
.expectOneOrMoreWhitespaces()
|
|
||||||
.expectCharacters('$')
|
|
||||||
.matchUntilFirstWhitespace() // First match: parameter name
|
|
||||||
.expectExpressionEnd()
|
|
||||||
// ...
|
|
||||||
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
|
||||||
// {{ end }}
|
|
||||||
.expectExpressionStart()
|
|
||||||
.expectCharacters('end')
|
|
||||||
.expectExpressionEnd()
|
|
||||||
.buildRegExp();
|
|
||||||
|
|
||||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
|
||||||
const parameterName = match[1];
|
|
||||||
const scopeText = match[2];
|
|
||||||
return {
|
|
||||||
parameters: [ new FunctionParameter(parameterName, true) ],
|
|
||||||
evaluator: (context) => {
|
|
||||||
const argumentValue = context.args.hasArgument(parameterName) ?
|
|
||||||
context.args.getArgument(parameterName).argumentValue
|
|
||||||
: undefined;
|
|
||||||
if (!argumentValue) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
|
||||||
if (!pipeline) {
|
|
||||||
return argumentValue;
|
|
||||||
}
|
|
||||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
|
||||||
// {{ . | pipeName }}
|
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('.')
|
.expectCharacters('with')
|
||||||
.matchPipeline() // First match: pipeline
|
.expectOneOrMoreWhitespaces()
|
||||||
|
.expectCharacters('$')
|
||||||
|
.matchUntilFirstWhitespace() // First match: parameter name
|
||||||
|
.expectExpressionEnd()
|
||||||
|
// ...
|
||||||
|
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||||
|
// {{ end }}
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('end')
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||||
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those
|
const parameterName = match[1];
|
||||||
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1 ) => {
|
const scopeText = match[2];
|
||||||
return replacer(match1);
|
return {
|
||||||
});
|
parameters: [new FunctionParameter(parameterName, true)],
|
||||||
|
evaluator: (context) => {
|
||||||
|
const argumentValue = context.args.hasArgument(parameterName)
|
||||||
|
? context.args.getArgument(parameterName).argumentValue
|
||||||
|
: undefined;
|
||||||
|
if (!argumentValue) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
||||||
|
if (!pipeline) {
|
||||||
|
return argumentValue;
|
||||||
|
}
|
||||||
|
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
||||||
|
// {{ . | pipeName }}
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('.')
|
||||||
|
.matchPipeline() // First match: pipeline
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.buildRegExp();
|
||||||
|
|
||||||
|
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
||||||
|
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
|
||||||
|
// but instead letting the pipeline compiler to fail on those.
|
||||||
|
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
|
||||||
|
return replacer(match1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
|
||||||
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
|
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
|
||||||
|
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
|
||||||
export class FunctionCallArgument implements IFunctionCallArgument {
|
export class FunctionCallArgument implements IFunctionCallArgument {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly parameterName: string,
|
public readonly parameterName: string,
|
||||||
public readonly argumentValue: string) {
|
public readonly argumentValue: string,
|
||||||
ensureValidParameterName(parameterName);
|
) {
|
||||||
if (!argumentValue) {
|
ensureValidParameterName(parameterName);
|
||||||
throw new Error(`undefined argument value for "${parameterName}"`);
|
if (!argumentValue) {
|
||||||
}
|
throw new Error(`missing argument value for "${parameterName}"`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,33 +2,37 @@ import { IFunctionCallArgument } from './IFunctionCallArgument';
|
|||||||
import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
|
import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
|
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
|
||||||
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||||
public addArgument(argument: IFunctionCallArgument): void {
|
|
||||||
if (!argument) {
|
public addArgument(argument: IFunctionCallArgument): void {
|
||||||
throw new Error('undefined argument');
|
if (!argument) {
|
||||||
}
|
throw new Error('missing argument');
|
||||||
if (this.hasArgument(argument.parameterName)) {
|
|
||||||
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
|
||||||
}
|
|
||||||
this.arguments.set(argument.parameterName, argument);
|
|
||||||
}
|
}
|
||||||
public getAllParameterNames(): string[] {
|
if (this.hasArgument(argument.parameterName)) {
|
||||||
return Array.from(this.arguments.keys());
|
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
||||||
}
|
}
|
||||||
public hasArgument(parameterName: string): boolean {
|
this.arguments.set(argument.parameterName, argument);
|
||||||
if (!parameterName) {
|
}
|
||||||
throw new Error('undefined parameter name');
|
|
||||||
}
|
public getAllParameterNames(): string[] {
|
||||||
return this.arguments.has(parameterName);
|
return Array.from(this.arguments.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasArgument(parameterName: string): boolean {
|
||||||
|
if (!parameterName) {
|
||||||
|
throw new Error('missing parameter name');
|
||||||
}
|
}
|
||||||
public getArgument(parameterName: string): IFunctionCallArgument {
|
return this.arguments.has(parameterName);
|
||||||
if (!parameterName) {
|
}
|
||||||
throw new Error('undefined parameter name');
|
|
||||||
}
|
public getArgument(parameterName: string): IFunctionCallArgument {
|
||||||
const arg = this.arguments.get(parameterName);
|
if (!parameterName) {
|
||||||
if (!arg) {
|
throw new Error('missing parameter name');
|
||||||
throw new Error(`parameter does not exist: ${parameterName}`);
|
|
||||||
}
|
|
||||||
return arg;
|
|
||||||
}
|
}
|
||||||
|
const arg = this.arguments.get(parameterName);
|
||||||
|
if (!arg) {
|
||||||
|
throw new Error(`parameter does not exist: ${parameterName}`);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface IFunctionCallArgument {
|
export interface IFunctionCallArgument {
|
||||||
readonly parameterName: string;
|
readonly parameterName: string;
|
||||||
readonly argumentValue: string;
|
readonly argumentValue: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
|
||||||
export interface IReadOnlyFunctionCallArgumentCollection {
|
export interface IReadOnlyFunctionCallArgumentCollection {
|
||||||
getArgument(parameterName: string): IFunctionCallArgument;
|
getArgument(parameterName: string): IFunctionCallArgument;
|
||||||
getAllParameterNames(): string[];
|
getAllParameterNames(): string[];
|
||||||
hasArgument(parameterName: string): boolean;
|
hasArgument(parameterName: string): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
|
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
|
||||||
addArgument(argument: IFunctionCallArgument): void;
|
addArgument(argument: IFunctionCallArgument): void;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user