Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d49d5c81c1 | ||
|
|
ff84f5676e | ||
|
|
4d0ce12c96 | ||
|
|
298b058e5c | ||
|
|
1e80ee1fb0 | ||
|
|
5901dc5f11 | ||
|
|
5721796378 | ||
|
|
8b374a37b4 | ||
|
|
c404dfebe2 | ||
|
|
e8199932b4 | ||
|
|
f4a7ca76b8 | ||
|
|
64cca1d9b8 | ||
|
|
68a5d698a2 | ||
|
|
bf0c55fa60 | ||
|
|
e7b816d156 | ||
|
|
a2e092190d | ||
|
|
c1c2f2925f | ||
|
|
e8d06e0f3e | ||
|
|
7d3670c26d | ||
|
|
430537f704 | ||
|
|
58ed7b456b | ||
|
|
6b3f4659df | ||
|
|
bbc6156281 | ||
|
|
df533ad3b1 | ||
|
|
6067bdb24e | ||
|
|
924b326244 | ||
|
|
8608072bfb | ||
|
|
3233d9b802 | ||
|
|
99e24b4134 | ||
|
|
b210aaddf2 | ||
|
|
65902e5b72 | ||
|
|
efd63ff85d | ||
|
|
242a497e7d | ||
|
|
05a6a84c37 |
@@ -1,2 +1,3 @@
|
|||||||
> 1%
|
> 1%
|
||||||
last 2 versions
|
last 2 versions
|
||||||
|
not dead
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[*.{js,jsx,ts,tsx,vue}]
|
[*.{js,jsx,ts,tsx,vue,sh}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
|
|||||||
1
.eslintignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dist/
|
||||||
190
.eslintrc.js
@@ -1,10 +1,6 @@
|
|||||||
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: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
|
||||||
const { rules: baseVariablesRules } = require('eslint-config-airbnb-base/rules/variables');
|
|
||||||
const tsconfigJson = require('./tsconfig.json');
|
const tsconfigJson = require('./tsconfig.json');
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
@@ -17,9 +13,7 @@ module.exports = {
|
|||||||
'plugin:vue/essential',
|
'plugin:vue/essential',
|
||||||
|
|
||||||
// Extends eslint-config-airbnb
|
// Extends eslint-config-airbnb
|
||||||
// Added by Vue CLI
|
'@vue/eslint-config-airbnb-with-typescript',
|
||||||
// Here until https://github.com/vuejs/eslint-config-airbnb/issues/23 is done
|
|
||||||
'@vue/airbnb',
|
|
||||||
|
|
||||||
// Extends @typescript-eslint/recommended
|
// Extends @typescript-eslint/recommended
|
||||||
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||||
@@ -27,7 +21,14 @@ module.exports = {
|
|||||||
'@vue/typescript/recommended',
|
'@vue/typescript/recommended',
|
||||||
],
|
],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 'latest',
|
ecmaVersion: 12, // ECMA 2021
|
||||||
|
/*
|
||||||
|
Having 'latest' leads to:
|
||||||
|
```
|
||||||
|
Parsing error: ecmaVersion must be a number. Received value of type string instead
|
||||||
|
```
|
||||||
|
For .js files in the project
|
||||||
|
*/
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...getOwnRules(),
|
...getOwnRules(),
|
||||||
@@ -45,18 +46,6 @@ module.exports = {
|
|||||||
mocha: true,
|
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)'],
|
files: ['**/tests/**/*.{j,t}s?(x)'],
|
||||||
rules: {
|
rules: {
|
||||||
@@ -108,6 +97,7 @@ function getOpinionatedRuleOverrides() {
|
|||||||
return {
|
return {
|
||||||
// https://erkinekici.com/articles/linting-trap#no-use-before-define
|
// https://erkinekici.com/articles/linting-trap#no-use-before-define
|
||||||
'no-use-before-define': 'off',
|
'no-use-before-define': 'off',
|
||||||
|
'@typescript-eslint/no-use-before-define': 'off',
|
||||||
// https://erkinekici.com/articles/linting-trap#arrow-body-style
|
// https://erkinekici.com/articles/linting-trap#arrow-body-style
|
||||||
'arrow-body-style': 'off',
|
'arrow-body-style': 'off',
|
||||||
// https://erkinekici.com/articles/linting-trap#no-plusplus
|
// https://erkinekici.com/articles/linting-trap#no-plusplus
|
||||||
@@ -127,164 +117,6 @@ function getOpinionatedRuleOverrides() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
function getAliasesFromTsConfig() {
|
||||||
return Object.keys(tsconfigJson.compilerOptions.paths)
|
return Object.keys(tsconfigJson.compilerOptions.paths)
|
||||||
.map((path) => `${path}*`);
|
.map((path) => `${path}*`);
|
||||||
|
|||||||
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: undergroundwires
|
||||||
@@ -21,8 +21,9 @@ A clear and concise description of what the bug is.
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Which OS are you using? What version of OS you were using?
|
Which OS are you using? What version of OS you were using?
|
||||||
On Windows you can find it using "Start button" > "Settings" > "System" > "About".
|
On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||||
On macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
|
On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
||||||
|
On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Reproduction steps
|
### Reproduction steps
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ You could alternatively send a PR directly (see CONTRIBUTING.md).
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Which OS will the new script configure?
|
Which OS will the new script configure?
|
||||||
Either "Windows" or "macOS".
|
One of the supported OSes: "Windows", "macOS" or "Linux".
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Name
|
### Name
|
||||||
@@ -30,10 +30,12 @@ E.g. "Disable webcam telemetry"
|
|||||||
<!--
|
<!--
|
||||||
Code that will be executed when script is selected.
|
Code that will be executed when script is selected.
|
||||||
Try to keep it as simple and backwards-compatible as possible.
|
Try to keep it as simple and backwards-compatible as possible.
|
||||||
Allowed languages:
|
Allowed languages:
|
||||||
- macOS: bash (sh)
|
|
||||||
- Windows: PowerShell (ps1) or batchfile
|
- Windows: PowerShell (ps1) or batchfile
|
||||||
- 💡 Prioritize the one that's simpler, batchfile if similar.
|
- 💡 Prioritize the one that's simpler, batchfile if similar.
|
||||||
|
- macOS: bash (sh)
|
||||||
|
- Linux: bash (sh) or Python 3
|
||||||
|
- 💡 Prioritize the one that's simpler, bash if similar.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Revert code
|
### Revert code
|
||||||
|
|||||||
8
.github/actions/setup-node/action.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 16.x
|
||||||
28
.github/workflows/checks.build.yaml
vendored
@@ -18,9 +18,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: 15.x
|
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -42,9 +40,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: 15.x
|
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -57,3 +53,23 @@ jobs:
|
|||||||
run: |-
|
run: |-
|
||||||
cross-env-shell NODE_ENV=${{ matrix.mode }}
|
cross-env-shell NODE_ENV=${{ matrix.mode }}
|
||||||
npm run electron:build -- --publish never --mode ${{ matrix.mode }}
|
npm run electron:build -- --publish never --mode ${{ matrix.mode }}
|
||||||
|
|
||||||
|
create-icons:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Allows to see results from other combinations
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Create icons
|
||||||
|
run: npm run create-icons
|
||||||
|
|||||||
4
.github/workflows/checks.quality.yaml
vendored
@@ -19,9 +19,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: 15.x
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Lint
|
- name: Lint
|
||||||
|
|||||||
4
.github/workflows/checks.security.yaml
vendored
@@ -16,9 +16,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: 15.x
|
|
||||||
-
|
-
|
||||||
name: NPM audit
|
name: NPM audit
|
||||||
run: exit "$(npm audit)" # Since node 15.x, it does not fail with error if we don't explicitly exit
|
run: exit "$(npm audit)" # Since node 15.x, it does not fail with error if we don't explicitly exit
|
||||||
|
|||||||
6
.github/workflows/release.desktop.yaml
vendored
@@ -20,9 +20,7 @@ jobs:
|
|||||||
- name: Checkout to bump commit
|
- name: Checkout to bump commit
|
||||||
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: 15.x
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
@@ -31,4 +29,4 @@ jobs:
|
|||||||
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074
|
EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074
|
||||||
|
|||||||
18
.github/workflows/release.site.yaml
vendored
@@ -1,8 +1,8 @@
|
|||||||
name: release-site
|
name: release-site
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [created] # will be triggered when a NON-draft release is created and published.
|
types: [created] # will be triggered when a NON-draft release is created and published.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
|
aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
|
||||||
@@ -77,30 +77,28 @@ jobs:
|
|||||||
name: "App: Checkout"
|
name: "App: Checkout"
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
path: site
|
path: app
|
||||||
ref: master # otherwise we don't get version bump commit
|
ref: master # otherwise we don't get version bump commit
|
||||||
-
|
-
|
||||||
name: "App: Setup node"
|
name: "App: Setup node"
|
||||||
uses: actions/setup-node@v1
|
uses: ./app/.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: 15.x
|
|
||||||
-
|
-
|
||||||
name: "App: Install dependencies"
|
name: "App: Install dependencies"
|
||||||
run: npm ci
|
run: npm ci
|
||||||
working-directory: site
|
working-directory: app
|
||||||
-
|
-
|
||||||
name: "App: Run unit tests"
|
name: "App: Run unit tests"
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
working-directory: site
|
working-directory: app
|
||||||
-
|
-
|
||||||
name: "App: Build"
|
name: "App: Build"
|
||||||
run: npm run build
|
run: npm run build
|
||||||
working-directory: site
|
working-directory: app
|
||||||
-
|
-
|
||||||
name: "App: Deploy to S3"
|
name: "App: Deploy to S3"
|
||||||
run: >-
|
run: >-
|
||||||
bash "aws/scripts/deploy/deploy-to-s3.sh" \
|
bash "aws/scripts/deploy/deploy-to-s3.sh" \
|
||||||
--folder site/dist \
|
--folder app/dist \
|
||||||
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
|
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
|
||||||
--storage-class ONEZONE_IA \
|
--storage-class ONEZONE_IA \
|
||||||
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \
|
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \
|
||||||
|
|||||||
4
.github/workflows/tests.e2e.yaml
vendored
@@ -17,9 +17,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: 15.x
|
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
4
.github/workflows/tests.integration.yaml
vendored
@@ -19,9 +19,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: 15.x
|
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
6
.github/workflows/tests.unit.yaml
vendored
@@ -16,10 +16,8 @@ jobs:
|
|||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Set-up node
|
||||||
uses: actions/setup-node@v1
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: 15.x
|
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
57
CHANGELOG.md
@@ -1,5 +1,62 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.12.0 (2023-08-03)
|
||||||
|
|
||||||
|
* Improve script/category name validation | [b210aad](https://github.com/undergroundwires/privacy.sexy/commit/b210aaddf26629179f77fe19f62f65d8a0ca2b87)
|
||||||
|
* Improve touch like hover on devices without mouse | [99e24b4](https://github.com/undergroundwires/privacy.sexy/commit/99e24b4134c461c336f6d08f49d193d853325d31)
|
||||||
|
* Improve click/touch without unintended interaction | [3233d9b](https://github.com/undergroundwires/privacy.sexy/commit/3233d9b8024dd59600edddef6d017e0089f59a9d)
|
||||||
|
* Align card icons vertically in cards view | [8608072](https://github.com/undergroundwires/privacy.sexy/commit/8608072bfb52d10a843a86d3d89b14e8b9776779)
|
||||||
|
* Fix broken npm installation and builds | [924b326](https://github.com/undergroundwires/privacy.sexy/commit/924b326244a175428175e0df3a50685ee5ac2ec6)
|
||||||
|
* Improve documentation support with markdown | [6067bdb](https://github.com/undergroundwires/privacy.sexy/commit/6067bdb24e6729d2249c9685f4f1c514c3167d91)
|
||||||
|
* win: add more Visual Studio scripts, support 2022 | [df533ad](https://github.com/undergroundwires/privacy.sexy/commit/df533ad3b19cebdf3454895aa2182bd4184e0360)
|
||||||
|
* win: add script to remove Widgets | [bbc6156](https://github.com/undergroundwires/privacy.sexy/commit/bbc6156281fb3fd4b66c63dec3f765780fafa855)
|
||||||
|
* Use line endings based on script language #88 | [6b3f465](https://github.com/undergroundwires/privacy.sexy/commit/6b3f4659df0afe1c99a8af6598df44a33c1f863a)
|
||||||
|
* win: improve OneDrive removal | [58ed7b4](https://github.com/undergroundwires/privacy.sexy/commit/58ed7b456b3cf11774c83c8c1c04db37ef3058c2)
|
||||||
|
* Use lowercase in script names and search text | [430537f](https://github.com/undergroundwires/privacy.sexy/commit/430537f70411756bbcaae837964c0223f78581e8)
|
||||||
|
* Improve manual execution instructions | [7d3670c](https://github.com/undergroundwires/privacy.sexy/commit/7d3670c26d0151ddc43303e8ed5e47715f0e0f00)
|
||||||
|
* Add multiline support for with expression | [e8d06e0](https://github.com/undergroundwires/privacy.sexy/commit/e8d06e0f3e178a69861e0197f9d1cce9af3958f1)
|
||||||
|
* Break line in inline codes in documentation | [c1c2f29](https://github.com/undergroundwires/privacy.sexy/commit/c1c2f2925fe88ec1f56bf7655b6b9a10aa3ea024)
|
||||||
|
* win: add script to increase RSA key exchange #165 | [a2e0921](https://github.com/undergroundwires/privacy.sexy/commit/a2e092190d8eb0fc9ceb8533572f04fff52f097b)
|
||||||
|
* win: add scripts to downloaded file handling #153 | [e7b816d](https://github.com/undergroundwires/privacy.sexy/commit/e7b816d1564afa98c63291f9d7fd6f3fee92f4ec)
|
||||||
|
* Drop support for dead browsers | [bf0c55f](https://github.com/undergroundwires/privacy.sexy/commit/bf0c55fa60bf2be070678ba27db14baf13fec511)
|
||||||
|
* Add support for nested templates | [68a5d69](https://github.com/undergroundwires/privacy.sexy/commit/68a5d698a2ce644ce25754016fb9e9bb642e41a7)
|
||||||
|
* mac: add scripts to configure Parallels Desktop | [64cca1d](https://github.com/undergroundwires/privacy.sexy/commit/64cca1d9b8946b92e21e86deb6db5612570befb1)
|
||||||
|
* Rework icon with higher quality and new color | [f4a7ca7](https://github.com/undergroundwires/privacy.sexy/commit/f4a7ca76b885b8346d8a9c32e6269eabc2d8139f)
|
||||||
|
* Relax and improve code validation | [e819993](https://github.com/undergroundwires/privacy.sexy/commit/e8199932b462380741d9f2d8b6b55485ab16af02)
|
||||||
|
* Add initial Linux support #150 | [c404dfe](https://github.com/undergroundwires/privacy.sexy/commit/c404dfebe2908bb165279f8279f3f5e805b647d7)
|
||||||
|
* mac: add script to disable personalized ads | [8b374a3](https://github.com/undergroundwires/privacy.sexy/commit/8b374a37b401699d5056bfd6b735b6a26c395ae0)
|
||||||
|
* Update dependencies and add npm setup script | [5721796](https://github.com/undergroundwires/privacy.sexy/commit/57217963787a8ab0c71d681c6b1673c484c88226)
|
||||||
|
* Fix macOS desktop build failure in CI | [5901dc5](https://github.com/undergroundwires/privacy.sexy/commit/5901dc5f11dd29be14c2616fc0ceb45196a43224)
|
||||||
|
* Change subtitle heading to new slogan | [1e80ee1](https://github.com/undergroundwires/privacy.sexy/commit/1e80ee1fb0208d92943619468dc427853cbe8de7)
|
||||||
|
* win: add new scripts to disable more telemetry | [298b058](https://github.com/undergroundwires/privacy.sexy/commit/298b058e5c89397db6f759b275442ba05499ac8c)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.4...0.12.0)
|
||||||
|
|
||||||
|
## 0.11.4 (2022-03-08)
|
||||||
|
|
||||||
|
* Improve performance of selecting scripts | [8e96c19](https://github.com/undergroundwires/privacy.sexy/commit/8e96c19126aa4cba6418de5ccaa9e2dcf8faab78)
|
||||||
|
* Fix reverting of Windows NVIDIA telemetry service | [2354f0b](https://github.com/undergroundwires/privacy.sexy/commit/2354f0ba9fed3aa23569b5ea6391a7119fe1ab53)
|
||||||
|
* Add AirBnb TypeScript overrides for linting | [834ce8c](https://github.com/undergroundwires/privacy.sexy/commit/834ce8cf9e8e46934dfa604526360870d109765b)
|
||||||
|
* Transpile dependencies for wider browser support | [0e52a99](https://github.com/undergroundwires/privacy.sexy/commit/0e52a99efa2b02d1aba10885a76e03aa6f9be7f8)
|
||||||
|
* Add more and unify tests for absent object cases | [44d79e2](https://github.com/undergroundwires/privacy.sexy/commit/44d79e2c9a97639bbd188a8fdfd740f1a5a1d6ee)
|
||||||
|
* Fix Windows DoSvc not being disabled #115 | [43ce834](https://github.com/undergroundwires/privacy.sexy/commit/43ce834750ddf471636d1ece4324d02357947f9f)
|
||||||
|
* Move stubs from `./stubs` to `./shared/Stubs` | [803ef2b](https://github.com/undergroundwires/privacy.sexy/commit/803ef2bb3eea68306377e40e326c791402998650)
|
||||||
|
* Improve documentation for developing | [3c3ec80](https://github.com/undergroundwires/privacy.sexy/commit/3c3ec80525b97e8a24db4c44bbf42a7b4e089056)
|
||||||
|
* Improve documentation for architecture | [1bcc6c8](https://github.com/undergroundwires/privacy.sexy/commit/1bcc6c8b2b923b4d4b1662f990d86b190ce73342)
|
||||||
|
* Improve existing documentation | [db47440](https://github.com/undergroundwires/privacy.sexy/commit/db47440d470ea6a6e100b620b10d078c01314992)
|
||||||
|
* Refactor to remove code coupling with Webpack | [5bbbb9c](https://github.com/undergroundwires/privacy.sexy/commit/5bbbb9cecca0a3828036e7fc34dcd66970ce334a)
|
||||||
|
* Refactor to remove hardcoding of aliases | [481a02a](https://github.com/undergroundwires/privacy.sexy/commit/481a02afd5190eb77a37fa450e50816b2268e99c)
|
||||||
|
* Document WpnService breaking on Windows 10 #110 | [3785e41](https://github.com/undergroundwires/privacy.sexy/commit/3785e410db461f667a834e0b388d81e4baa028e4)
|
||||||
|
* Fix error when reverting Windows Defender setting | [956052c](https://github.com/undergroundwires/privacy.sexy/commit/956052c8fff042812fe84fe4d7fa5c579365ff9b)
|
||||||
|
* Fix Windows 11 being detected as Windows 10 | [d6bc33e](https://github.com/undergroundwires/privacy.sexy/commit/d6bc33ec865d50efc6b8d4ccc2f789edd874fcee)
|
||||||
|
* Refactor to use version object #59 | [eeb1d5b](https://github.com/undergroundwires/privacy.sexy/commit/eeb1d5b0c40a55675921af3f67f366b2ff658acf)
|
||||||
|
* Fix Microsoft Defender alert for uninstaller #114 | [112e79a](https://github.com/undergroundwires/privacy.sexy/commit/112e79a64c6153f4ce3b48c27a09639e7647aebc)
|
||||||
|
* Add donation information | [05a6a84](https://github.com/undergroundwires/privacy.sexy/commit/05a6a84c3739ec900343591ac1f7a9f310cd73f2)
|
||||||
|
* Bump node environment to 16.x | [242a497](https://github.com/undergroundwires/privacy.sexy/commit/242a497e7debb351da19b20b63a3554f0cca4b5c)
|
||||||
|
* Bump dependencies to latest | [efd63ff](https://github.com/undergroundwires/privacy.sexy/commit/efd63ff85dea4c9a9c033c54bc1be378742de351)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.3...0.11.4)
|
||||||
|
|
||||||
## 0.11.3 (2022-01-05)
|
## 0.11.3 (2022-01-05)
|
||||||
|
|
||||||
* Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd)
|
* Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd)
|
||||||
|
|||||||
20
README.md
@@ -1,9 +1,15 @@
|
|||||||
# privacy.sexy
|
# privacy.sexy — Now you have the choice
|
||||||
|
|
||||||
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
> Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆
|
||||||
|
|
||||||
<!-- markdownlint-disable MD033 -->
|
<!-- markdownlint-disable MD033 -->
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
<a href="https://undergroundwires.dev/donate?project=privacy.sexy">
|
||||||
|
<img
|
||||||
|
alt="donation badge"
|
||||||
|
src="https://undergroundwires.dev/img/badges/donate/flat.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md">
|
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md">
|
||||||
<img
|
<img
|
||||||
alt="contributions are welcome"
|
alt="contributions are welcome"
|
||||||
@@ -114,13 +120,17 @@ Online version does not require to run any software on your computer. Offline ve
|
|||||||
- **Reversible**. Revert if something feels wrong.
|
- **Reversible**. Revert if something feels wrong.
|
||||||
- **Accessible**. No need to run any compiled software on your computer with web version.
|
- **Accessible**. No need to run any compiled software on your computer with web version.
|
||||||
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
||||||
- **Tested.** A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
- **Tested**. A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
||||||
- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md).
|
- **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.
|
- **Portable and simple**. Every script is independently executable without cross-dependencies.
|
||||||
|
|
||||||
## Contributing
|
## Support
|
||||||
|
|
||||||
Contributions of any type are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) as starting point, it includes useful information like [how to add new scripts](./CONTRIBUTING.md#extend-scripts).
|
**Sponsor 💕**. Consider sponsoring on [GitHub Sponsors](https://github.com/sponsors/undergroundwires), or you can donate using [other ways such as crypto or a coffee](https://undergroundwires.dev/donate).
|
||||||
|
|
||||||
|
**Star 🤩**. Feel free to give it a star ⭐ .
|
||||||
|
|
||||||
|
**Contribute 👷**. Contributions of any type are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) as the starting point. It includes useful information like [how to add new scripts](./CONTRIBUTING.md#extend-scripts).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# build
|
# build
|
||||||
|
|
||||||
- These are the file that are used by electron.
|
This folder contains files that are used by Electron to serve the desktop version.
|
||||||
- Logos are created by from the [PNG icon](./../public/icon.png)
|
|
||||||
- by running `npx electron-icon-builder --input=./public/icon.png --output=build --flatten`
|
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md).
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 740 B After Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |
14
cypress.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'cypress'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
fixturesFolder: 'tests/e2e/fixtures',
|
||||||
|
screenshotsFolder: 'tests/e2e/screenshots',
|
||||||
|
videosFolder: 'tests/e2e/videos',
|
||||||
|
e2e: {
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
return require('./tests/e2e/plugins/index.js')(on, config)
|
||||||
|
},
|
||||||
|
specPattern: 'tests/e2e/specs/**/*.cy.{js,jsx,ts,tsx}',
|
||||||
|
supportFile: 'tests/e2e/support/index.js',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"pluginsFile": "tests/e2e/plugins/index.js"
|
|
||||||
}
|
|
||||||
@@ -12,9 +12,9 @@ Everything that's merged in the master goes directly to production.
|
|||||||
|
|
||||||
privacy.sexy uses [GitHub actions](https://github.com/features/actions) to define and run pipelines as code.
|
privacy.sexy uses [GitHub actions](https://github.com/features/actions) to define and run pipelines as code.
|
||||||
|
|
||||||
GitHub workflows i.e. pipelines exist in [`/.github/.workflows/`](./../.github/workflows/) folder without any subfolders due to GitHub actions requirements [1] .
|
GitHub workflows i.e. pipelines exist in [`/.github/workflows/`](./../.github/workflows/) folder without any subfolders due to GitHub actions requirements [1] .
|
||||||
|
|
||||||
[1]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows
|
Local GitHub actions are defined in [`/.github/actions/`](./../.github/actions/) and used to reuse same workflow steps.
|
||||||
|
|
||||||
## Pipeline types
|
## Pipeline types
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
|
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
|
||||||
- 💡 Best practices
|
- 💡 Best practices
|
||||||
- If you repeat yourself, try to utilize [YAML-defined functions](#Function)
|
- If you repeat yourself, try to utilize [YAML-defined functions](#function)
|
||||||
- Always try to add documentation and a way to revert a tweak in [scripts](#Script)
|
- Always try to add documentation and a way to revert a tweak in [scripts](#script)
|
||||||
- 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
|
- 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
|
||||||
|
|
||||||
## Objects
|
## Objects
|
||||||
@@ -13,19 +13,19 @@
|
|||||||
- A collection simply defines:
|
- A collection simply defines:
|
||||||
- different categories and their scripts in a tree structure
|
- different categories and their scripts in a tree structure
|
||||||
- OS specific details
|
- OS specific details
|
||||||
- Also allows defining common [function](#Function)s to be used throughout the collection if you'd like different scripts to share same code.
|
- Also allows defining common [function](#function)s to be used throughout the collection if you'd like different scripts to share same code.
|
||||||
|
|
||||||
#### `Collection` syntax
|
#### `Collection` syntax
|
||||||
|
|
||||||
- `os:` *`string`* (**required**)
|
- `os:` *`string`* (**required**)
|
||||||
- Operating system that the [Collection](#collection) is written for.
|
- Operating system that the [Collection](#collection) is written for.
|
||||||
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
|
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
|
||||||
- `actions: [` ***[`Category`](#Category)*** `, ... ]` **(required)**
|
- `actions: [` ***[`Category`](#category)*** `, ... ]` **(required)**
|
||||||
- Each [category](#category) is rendered as different cards in card presentation.
|
- Each [category](#category) is rendered as different cards in card presentation.
|
||||||
- ❗ A [Collection](#collection) must consist of at least one category.
|
- ❗ A [Collection](#collection) must consist of at least one category.
|
||||||
- `functions: [` ***[`Function`](#Function)*** `, ... ]`
|
- `functions: [` ***[`Function`](#function)*** `, ... ]`
|
||||||
- Functions are optionally defined to re-use the same code throughout different scripts.
|
- Functions are optionally defined to re-use the same code throughout different scripts.
|
||||||
- `scripting:` ***[`ScriptingDefinition`](#ScriptingDefinition)*** **(required)**
|
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
|
||||||
- Defines the scripting language that the code of other action uses.
|
- Defines the scripting language that the code of other action uses.
|
||||||
|
|
||||||
### `Category`
|
### `Category`
|
||||||
@@ -38,9 +38,12 @@
|
|||||||
- `category:` *`string`* (**required**)
|
- `category:` *`string`* (**required**)
|
||||||
- Name of the category
|
- Name of the category
|
||||||
- ❗ Must be unique throughout the [Collection](#collection)
|
- ❗ Must be unique throughout the [Collection](#collection)
|
||||||
- `children: [` ***[`Category`](#Category)*** `|` [***`Script`***](#Script) `, ... ]` (**required**)
|
- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#script) `, ... ]` (**required**)
|
||||||
- ❗ Category must consist of at least one subcategory or script.
|
- ❗ Category must consist of at least one subcategory or script.
|
||||||
- Children can be combination of scripts and subcategories.
|
- Children can be combination of scripts and subcategories.
|
||||||
|
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||||
|
- Documentation pieces related to the category.
|
||||||
|
- Rendered as markdown.
|
||||||
|
|
||||||
### `Script`
|
### `Script`
|
||||||
|
|
||||||
@@ -67,12 +70,12 @@
|
|||||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
- ❗ Do not define if `call` is defined.
|
- ❗ Do not define if `call` is defined.
|
||||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**)
|
||||||
- A shared function or sequence of functions to call (called in order)
|
- A shared function or sequence of functions to call (called in order)
|
||||||
- ❗ If not defined `code` must be defined
|
- ❗ If not defined `code` must be defined
|
||||||
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||||
- Single documentation URL or list of URLs for those who wants to learn more about the script
|
- Documentation pieces related to the script.
|
||||||
- E.g. `https://docs.microsoft.com/en-us/windows-server/`
|
- Rendered as markdown.
|
||||||
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
|
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
|
||||||
- If not defined then the script will not be recommended
|
- If not defined then the script will not be recommended
|
||||||
- If defined it can be either
|
- If defined it can be either
|
||||||
@@ -120,7 +123,7 @@
|
|||||||
- Convention is to use camelCase, and be verbs.
|
- Convention is to use camelCase, and be verbs.
|
||||||
- E.g. `uninstallStoreApp`
|
- E.g. `uninstallStoreApp`
|
||||||
- ❗ Function names must be unique
|
- ❗ Function names must be unique
|
||||||
- `parameters`: `[` ***[`FunctionParameter`](#FunctionParameter)*** `, ... ]`
|
- `parameters`: `[` ***[`FunctionParameter`](#functionparameter)*** `, ... ]`
|
||||||
- List of parameters that function code refers to.
|
- List of parameters that function code refers to.
|
||||||
- ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
|
- ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
|
||||||
`code`: *`string`* (**required** if `call` is undefined)
|
`code`: *`string`* (**required** if `call` is undefined)
|
||||||
@@ -133,7 +136,7 @@
|
|||||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in code
|
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in code
|
||||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**)
|
||||||
- A shared function or sequence of functions to call (called in order)
|
- A shared function or sequence of functions to call (called in order)
|
||||||
- The parameter values that are sent can use [expressions (templating)](./templating.md#expressions)
|
- The parameter values that are sent can use [expressions (templating)](./templating.md#expressions)
|
||||||
- ❗ If not defined `code` must be defined
|
- ❗ If not defined `code` must be defined
|
||||||
@@ -141,7 +144,7 @@
|
|||||||
### `FunctionParameter`
|
### `FunctionParameter`
|
||||||
|
|
||||||
- Defines a parameter that function requires optionally or mandatory.
|
- Defines a parameter that function requires optionally or mandatory.
|
||||||
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#FunctionCall).
|
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#functioncall).
|
||||||
|
|
||||||
#### `FunctionParameter` syntax
|
#### `FunctionParameter` syntax
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,14 @@ You could run other types of tests as well, but they may take longer time and ov
|
|||||||
|
|
||||||
- Build web application: `npm run build`
|
- Build web application: `npm run build`
|
||||||
- Build desktop application: `npm run electron:build`
|
- Build desktop application: `npm run electron:build`
|
||||||
|
- (Re)create icons (see [documentation](../img/README.md)): `npm run create-icons`
|
||||||
|
|
||||||
|
### Utility Scripts
|
||||||
|
|
||||||
|
- Run fresh NPM install: [`./scripts/fresh-npm-install.sh`](../scripts/fresh-npm-install.sh)
|
||||||
|
- This script provides a clean NPM install, removing existing node modules and optionally the package-lock.json (when run with -n), then installs dependencies and runs unit tests.
|
||||||
|
- Configure VSCode: [`./scripts/configure-vscode.sh`](../scripts/configure-vscode.sh)
|
||||||
|
- This script checks and sets the necessary configurations for VSCode in `settings.json` file.
|
||||||
|
|
||||||
## Recommended extensions
|
## Recommended extensions
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ It's designed event-driven from bottom to top. It listens user events (from top)
|
|||||||
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations 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`.
|
||||||
|
|
||||||
|
## Visual design best-practices
|
||||||
|
|
||||||
|
Add visual clues for clickable items. It should be as clear as possible that they're interactable at first look without hovering. They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility.
|
||||||
|
|
||||||
## Application data
|
## Application data
|
||||||
|
|
||||||
Components (should) use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again.
|
Components (should) use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again.
|
||||||
|
|||||||
@@ -10,10 +10,19 @@
|
|||||||
|
|
||||||
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
|
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
|
||||||
- E.g. `Hello {{ $name }} !`
|
- E.g. `Hello {{ $name }} !`
|
||||||
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) that has inspired this templating language.
|
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
|
||||||
- Functions enables usage of expressions.
|
- 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).
|
||||||
|
- Expressions inside expressions (nested templates) are supported.
|
||||||
|
- An expression can output another expression that will also be compiled.
|
||||||
|
- E.g. following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output.
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $condition }}
|
||||||
|
echo {{ $text }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
### Parameter substitution
|
### Parameter substitution
|
||||||
|
|
||||||
@@ -56,9 +65,31 @@ A function can call other functions such as:
|
|||||||
|
|
||||||
### with
|
### with
|
||||||
|
|
||||||
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions. E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}`.
|
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
|
||||||
|
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
|
||||||
|
|
||||||
Binds its context (`.`) value of provided argument for the parameter if provided one. E.g. `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
|
It binds its context (value of the provided parameter value) as arbitrary `.` value. It allows you to use the argument value of the given parameter when it is provided and not empty such as:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
It supports multiline text inside the block. You can have something like:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $argument }}
|
||||||
|
First line
|
||||||
|
Second line
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $condition }}
|
||||||
|
This is a different parameter: {{ $text }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ Common aspects for all tests:
|
|||||||
- Vue CLI plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) configures E2E tests.
|
- 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.
|
- Test names and folders have logical structure based on tests executed.
|
||||||
- The structure is following:
|
- The structure is following:
|
||||||
- [`cypress.json`](./../cypress.json): Cypress configuration file.
|
- [`cypress.config.ts`](./../cypress.config.ts): Cypress configuration file.
|
||||||
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
|
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
|
||||||
- [`/specs/`](./../tests/e2e/specs/): Test files named with `.spec.js` extension.
|
- [`/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.
|
- [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before loading project.
|
||||||
|
|||||||
12
img/README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# img
|
||||||
|
|
||||||
|
This folder contains image files and other resources related to images.
|
||||||
|
|
||||||
|
## logo.svg
|
||||||
|
|
||||||
|
[logo.svg](./logo.svg) is the master logo from which all other icons or images are created from.
|
||||||
|
It should be the only file that will be changed manually.
|
||||||
|
|
||||||
|
[`logo-update.mjs`](./logo-update.mjs) script in this folder updates all the logo files.
|
||||||
|
It should be executed everytime the logo is changed.
|
||||||
|
It automates recreation of logo files in different formats.
|
||||||
127
img/logo-update.mjs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
import { resolve, join } from 'path';
|
||||||
|
import { rm, mkdtemp, stat } from 'fs/promises';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { URL, fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
class Paths {
|
||||||
|
constructor(selfDirectory) {
|
||||||
|
const projectRoot = resolve(selfDirectory, '../');
|
||||||
|
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
||||||
|
this.publicDirectory = join(projectRoot, 'public');
|
||||||
|
this.electronBuildDirectory = join(projectRoot, 'build');
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `Source image: ${this.sourceImage}\n`
|
||||||
|
+ `Public directory: ${this.publicDirectory}\n`
|
||||||
|
+ `Electron build directory: ${this.electronBuildDirectory}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const paths = new Paths(getCurrentScriptDirectory());
|
||||||
|
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
||||||
|
await updateDesktopLauncherAndTrayIcon(paths.sourceImage, paths.publicDirectory);
|
||||||
|
await updateWebFavicon(paths.sourceImage, paths.publicDirectory);
|
||||||
|
await updateDesktopIcons(paths.sourceImage, paths.electronBuildDirectory);
|
||||||
|
console.log('🎉 (Re)created icons successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDesktopLauncherAndTrayIcon(sourceImage, publicFolder) {
|
||||||
|
await ensureFileExists(sourceImage);
|
||||||
|
await ensureFolderExists(publicFolder);
|
||||||
|
const electronTrayIconFile = join(publicFolder, 'icon.png');
|
||||||
|
console.log(`Updating desktop launcher and tray icon at ${electronTrayIconFile}.`);
|
||||||
|
await runCommand(
|
||||||
|
'npx',
|
||||||
|
'svgexport',
|
||||||
|
sourceImage,
|
||||||
|
electronTrayIconFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateWebFavicon(sourceImage, faviconFolder) {
|
||||||
|
console.log('Updating favicon');
|
||||||
|
await ensureFileExists(sourceImage);
|
||||||
|
await ensureFolderExists(faviconFolder);
|
||||||
|
await runCommand(
|
||||||
|
'npx',
|
||||||
|
'icon-gen',
|
||||||
|
`--input ${sourceImage}`,
|
||||||
|
`--output ${faviconFolder}`,
|
||||||
|
'--ico',
|
||||||
|
'--ico-name \'favicon\'',
|
||||||
|
'--report',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDesktopIcons(sourceImage, electronIconsDir) {
|
||||||
|
await ensureFileExists(sourceImage);
|
||||||
|
await ensureFolderExists(electronIconsDir);
|
||||||
|
const temporaryDir = await mkdtemp('icon-');
|
||||||
|
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
||||||
|
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by icon-builder
|
||||||
|
await runCommand(
|
||||||
|
'npx',
|
||||||
|
'svgexport',
|
||||||
|
sourceImage,
|
||||||
|
temporaryPngFile,
|
||||||
|
'1024:1024',
|
||||||
|
);
|
||||||
|
console.log(`Creating electron icons to ${electronIconsDir}.`);
|
||||||
|
await runCommand(
|
||||||
|
'npx',
|
||||||
|
'electron-icon-builder',
|
||||||
|
`--input="${temporaryPngFile}"`,
|
||||||
|
`--output="${electronIconsDir}"`,
|
||||||
|
'--flatten',
|
||||||
|
);
|
||||||
|
console.log('Cleaning up temporary directory.');
|
||||||
|
await rm(temporaryDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureFileExists(filePath) {
|
||||||
|
const path = await stat(filePath);
|
||||||
|
if (!path.isFile()) {
|
||||||
|
throw new Error(`Not a file: ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureFolderExists(folderPath) {
|
||||||
|
const path = await stat(folderPath);
|
||||||
|
if (!path.isDirectory()) {
|
||||||
|
throw new Error(`Not a directory: ${folderPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommand(...args) {
|
||||||
|
const command = args.join(' ');
|
||||||
|
console.log(`Running command: ${command}`);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const process = spawn(command, { shell: true });
|
||||||
|
process.stdout.on('data', (stdout) => {
|
||||||
|
console.log(stdout.toString());
|
||||||
|
});
|
||||||
|
process.stderr.on('data', (stderr) => {
|
||||||
|
console.error(stderr.toString());
|
||||||
|
});
|
||||||
|
process.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
process.on('close', (exitCode) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
reject(new Error(`Process exited with non-zero exit code: ${exitCode}`));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
process.stdin.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentScriptDirectory() {
|
||||||
|
return fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
56
img/logo.svg
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="90.2998mm" height="90.2998mm"
|
||||||
|
viewBox="0 0 256 256">
|
||||||
|
<path id="logo"
|
||||||
|
fill="#3a65ab" stroke="#3a65ab" stroke-width="1"
|
||||||
|
d="M 128.00,173.00
|
||||||
|
C 128.00,173.00 102.00,175.00 102.00,175.00
|
||||||
|
85.39,174.97 64.02,170.31 49.00,163.22
|
||||||
|
38.46,158.24 28.39,152.17 20.01,143.96
|
||||||
|
14.88,138.93 10.72,133.32 7.31,127.00
|
||||||
|
3.36,119.66 1.10,112.37 1.00,104.00
|
||||||
|
1.00,104.00 1.00,98.00 1.00,98.00
|
||||||
|
1.29,73.92 24.76,53.44 44.00,42.43
|
||||||
|
84.66,19.15 129.75,23.12 169.00,47.00
|
||||||
|
188.74,59.02 207.93,76.23 208.00,101.00
|
||||||
|
208.00,101.00 188.00,101.00 188.00,101.00
|
||||||
|
186.46,86.48 168.72,71.84 157.00,64.61
|
||||||
|
140.76,54.59 121.33,46.78 102.00,47.00
|
||||||
|
88.42,47.16 72.20,52.52 60.00,58.32
|
||||||
|
45.30,65.30 19.83,84.81 21.10,103.00
|
||||||
|
21.39,107.16 22.92,110.33 24.76,114.00
|
||||||
|
32.70,129.78 48.16,140.02 64.00,146.72
|
||||||
|
75.16,151.44 92.90,155.26 105.00,154.99
|
||||||
|
113.45,154.79 121.81,152.84 130.00,151.00
|
||||||
|
130.00,151.00 128.00,173.00 128.00,173.00 Z
|
||||||
|
M 136.00,79.00
|
||||||
|
C 142.71,81.35 144.84,93.60 144.99,100.00
|
||||||
|
145.51,122.74 130.31,140.73 107.00,141.00
|
||||||
|
83.63,141.26 67.43,126.52 66.09,103.00
|
||||||
|
64.82,80.73 85.85,58.90 104.00,64.00
|
||||||
|
100.18,69.73 95.45,74.53 96.20,82.00
|
||||||
|
97.29,92.87 110.06,102.98 121.00,99.03
|
||||||
|
129.92,95.81 134.61,87.96 136.00,79.00 Z
|
||||||
|
M 186.00,113.46
|
||||||
|
C 206.11,110.69 225.57,114.92 239.91,130.01
|
||||||
|
252.85,143.63 255.21,157.09 255.00,175.00
|
||||||
|
254.76,195.49 241.26,214.25 223.00,222.88
|
||||||
|
213.06,227.58 204.72,228.12 194.00,228.00
|
||||||
|
150.34,227.49 126.71,178.85 146.32,142.00
|
||||||
|
154.93,125.82 168.55,117.23 186.00,113.46 Z
|
||||||
|
M 233.00,181.00
|
||||||
|
C 242.24,158.78 221.84,133.54 199.00,133.01
|
||||||
|
188.40,132.77 182.75,135.31 174.00,141.00
|
||||||
|
178.60,146.85 195.92,157.24 203.00,161.86
|
||||||
|
209.82,166.32 226.61,178.55 233.00,181.00 Z
|
||||||
|
M 221.00,200.00
|
||||||
|
C 216.39,194.15 206.42,188.61 200.00,184.33
|
||||||
|
192.31,179.21 168.77,162.59 162.00,160.00
|
||||||
|
159.67,165.03 159.94,166.57 160.00,172.00
|
||||||
|
160.23,190.99 177.11,207.55 196.00,207.99
|
||||||
|
206.60,208.23 212.25,205.69 221.00,200.00 Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
35564
package-lock.json
generated
124
package.json
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.11.3",
|
"version": "0.12.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
"slogan": "Now you have the choice",
|
||||||
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
@@ -10,83 +11,98 @@
|
|||||||
"test:unit": "vue-cli-service test:unit",
|
"test:unit": "vue-cli-service test:unit",
|
||||||
"test:e2e": "vue-cli-service test:e2e",
|
"test:e2e": "vue-cli-service test:e2e",
|
||||||
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
||||||
|
"create-icons": "node img/logo-update.mjs",
|
||||||
"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:eslint": "vue-cli-service lint --no-fix --mode production",
|
||||||
"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: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\""
|
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\""
|
||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/vue-fontawesome": "^2.0.6",
|
"@fortawesome/vue-fontawesome": "^2.0.9",
|
||||||
"@juggle/resize-observer": "^3.3.1",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"ace-builds": "^1.4.13",
|
"ace-builds": "^1.23.4",
|
||||||
"core-js": "^3.18.3",
|
"core-js": "^3.32.0",
|
||||||
"cross-fetch": "^3.1.4",
|
"cross-fetch": "^4.0.0",
|
||||||
"electron-progressbar": "^2.0.1",
|
"electron-progressbar": "^2.1.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
"npm": "^8.1.1",
|
"markdown-it": "^13.0.1",
|
||||||
|
"npm": "^9.8.1",
|
||||||
"v-tooltip": "2.1.3",
|
"v-tooltip": "2.1.3",
|
||||||
"vue": "^2.6.14",
|
"vue": "^2.7.14",
|
||||||
"vue-class-component": "^7.2.6",
|
"vue-class-component": "^7.2.6",
|
||||||
"vue-js-modal": "^2.0.1",
|
"vue-js-modal": "^2.0.1",
|
||||||
"vue-property-decorator": "^9.1.2"
|
"vue-property-decorator": "^9.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ace": "0.0.47",
|
"@rushstack/eslint-patch": "^1.3.2",
|
||||||
"@types/chai": "^4.2.22",
|
"@types/ace": "^0.0.48",
|
||||||
"@types/file-saver": "^2.0.3",
|
"@types/chai": "^4.3.5",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/file-saver": "^2.0.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
"@types/mocha": "^10.0.1",
|
||||||
"@typescript-eslint/parser": "^5.4.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@vue/cli-plugin-babel": "~5.0.0-rc.1",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vue/cli-plugin-e2e-cypress": "~5.0.0-rc.1",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.0-rc.1",
|
"@vue/cli-plugin-e2e-cypress": "~5.0.8",
|
||||||
"@vue/cli-plugin-typescript": "~5.0.0-rc.1",
|
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||||
"@vue/cli-plugin-unit-mocha": "~5.0.0-rc.1",
|
"@vue/cli-plugin-typescript": "~5.0.8",
|
||||||
"@vue/cli-service": "~5.0.0-rc.1",
|
"@vue/cli-plugin-unit-mocha": "~5.0.8",
|
||||||
"@vue/eslint-config-airbnb": "^6.0.0",
|
"@vue/cli-service": "~5.0.8",
|
||||||
"@vue/eslint-config-typescript": "^9.1.0",
|
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
||||||
"@vue/test-utils": "1.2.2",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.7",
|
||||||
"cypress": "^8.3.0",
|
"cypress": "^12.17.2",
|
||||||
"electron": "^15.3.0",
|
"electron": "^25.3.2",
|
||||||
"electron-builder": "^22.14.13",
|
"electron-builder": "^24.6.3",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-log": "^4.4.1",
|
"electron-icon-builder": "^2.0.1",
|
||||||
"electron-updater": "^4.3.9",
|
"electron-log": "^4.4.8",
|
||||||
"eslint": "^7.32.0",
|
"electron-updater": "^6.1.4",
|
||||||
"eslint-plugin-import": "^2.25.3",
|
"eslint": "^8.46.0",
|
||||||
"eslint-plugin-vue": "^8.0.3",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-vuejs-accessibility": "^1.1.0",
|
"eslint-plugin-vue": "^9.6.0",
|
||||||
|
"eslint-plugin-vuejs-accessibility": "^1.2.0",
|
||||||
|
"icon-gen": "^3.0.1",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"markdownlint-cli": "^0.29.0",
|
"markdownlint-cli": "^0.35.0",
|
||||||
"remark-cli": "^10.0.0",
|
"remark-cli": "^11.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.2",
|
||||||
"remark-validate-links": "^11.0.1",
|
"remark-validate-links": "^12.1.1",
|
||||||
"sass": "^1.43.3",
|
"sass": "^1.64.1",
|
||||||
"sass-loader": "10.2.0",
|
"sass-loader": "^13.3.2",
|
||||||
"ts-loader": "9.0.1",
|
"svgexport": "^0.4.2",
|
||||||
"tslib": "^2.3.1",
|
"ts-loader": "^9.4.4",
|
||||||
"typescript": "^4.4.4",
|
"typescript": "~4.6.2",
|
||||||
"vue-cli-plugin-electron-builder": "^2.1.1",
|
"vue-cli-plugin-electron-builder": "^3.0.0-alpha.4",
|
||||||
"vue-template-compiler": "^2.6.14",
|
"yaml-lint": "^1.7.0",
|
||||||
"yaml-lint": "^1.2.4"
|
"tslib": "~2.4.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"vue-cli-plugin-electron-builder": {
|
||||||
|
"electron-builder": "^24.6.3"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"//devDependencies": {
|
"//devDependencies": {
|
||||||
"ts-loader": "Here as workaround for vue-cli-plugin-electron-builder using older webpack 4"
|
"typescript": [
|
||||||
|
"Cannot upgrade to 5.X.X due to unmaintained @vue/cli-plugin-typescript, https://github.com/vuejs/vue-cli/issues/7401",
|
||||||
|
"Cannot upgrade to > 4.6.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252"
|
||||||
|
],
|
||||||
|
"tslib": "Cannot upgrade to > 2.4.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252",
|
||||||
|
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60",
|
||||||
|
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60"
|
||||||
},
|
},
|
||||||
"homepage": "https://privacy.sexy",
|
"homepage": "https://privacy.sexy",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 353 KiB |
BIN
public/icon.png
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
@@ -2,9 +2,8 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows and macOS</title>
|
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows, macOS and Linux</title>
|
||||||
<meta name="robots" content="index,follow" />
|
<meta name="robots" content="index,follow" />
|
||||||
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
|
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
|||||||
74
scripts/configure-vscode.sh
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# This script ensures that the '.vscode/settings.json' file exists and is configured correctly for ESLint validation on Vue and JavaScript files.
|
||||||
|
# See https://web.archive.org/web/20230801024405/https://eslint.vuejs.org/user-guide/#visual-studio-code
|
||||||
|
|
||||||
|
declare -r SETTINGS_FILE='.vscode/settings.json'
|
||||||
|
declare -ra CONFIG_KEYS=('vue' 'javascript' 'typescript')
|
||||||
|
declare -r TEMP_FILE="tmp.$$.json"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
ensure_vscode_directory_exists
|
||||||
|
create_or_update_settings
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_vscode_directory_exists() {
|
||||||
|
local dir_name
|
||||||
|
dir_name=$(dirname "${SETTINGS_FILE}")
|
||||||
|
if [[ ! -d ${dir_name} ]]; then
|
||||||
|
mkdir -p "${dir_name}"
|
||||||
|
echo "🎉 Created directory: ${dir_name}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_or_update_settings() {
|
||||||
|
if [[ ! -f ${SETTINGS_FILE} ]]; then
|
||||||
|
create_default_settings
|
||||||
|
else
|
||||||
|
add_or_update_eslint_validate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_default_settings() {
|
||||||
|
local default_validate
|
||||||
|
default_validate=$(printf '%s' "${CONFIG_KEYS[*]}" | jq -R -s -c -M 'split(" ")')
|
||||||
|
echo "{ \"eslint.validate\": ${default_validate} }" | jq '.' > "${SETTINGS_FILE}"
|
||||||
|
echo "🎉 Created default ${SETTINGS_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
add_or_update_eslint_validate() {
|
||||||
|
if ! jq -e '.["eslint.validate"]' "${SETTINGS_FILE}" >/dev/null; then
|
||||||
|
add_default_eslint_validate
|
||||||
|
else
|
||||||
|
update_eslint_validate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
add_default_eslint_validate() {
|
||||||
|
jq --argjson keys "$(printf '%s' "${CONFIG_KEYS[*]}" \
|
||||||
|
| jq -R -s -c 'split(" ")')" '. += {"eslint.validate": $keys}' "${SETTINGS_FILE}" > "${TEMP_FILE}"
|
||||||
|
replace_and_confirm
|
||||||
|
echo "🎉 Added default 'eslint.validate' to ${SETTINGS_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_eslint_validate() {
|
||||||
|
local existing_keys
|
||||||
|
existing_keys=$(jq '.["eslint.validate"]' "${SETTINGS_FILE}")
|
||||||
|
for key in "${CONFIG_KEYS[@]}"; do
|
||||||
|
if ! echo "${existing_keys}" | jq 'index("'"${key}"'")' >/dev/null; then
|
||||||
|
jq '.["eslint.validate"] += ["'"${key}"'"]' "${SETTINGS_FILE}" > "${TEMP_FILE}"
|
||||||
|
mv "${TEMP_FILE}" "${SETTINGS_FILE}"
|
||||||
|
echo "🎉 Updated 'eslint.validate' in ${SETTINGS_FILE} for ${key}"
|
||||||
|
else
|
||||||
|
echo "⏩️ No updated needed for ${key} ${SETTINGS_FILE}."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
replace_and_confirm() {
|
||||||
|
if mv "${TEMP_FILE}" "${SETTINGS_FILE}"; then
|
||||||
|
echo "🎉 Updated ${SETTINGS_FILE}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
95
scripts/fresh-npm-install.sh
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Description:
|
||||||
|
# This script ensures npm is available, removes existing node modules, optionally
|
||||||
|
# removes package-lock.json (when -n flag is used), installs dependencies and runs unit tests.
|
||||||
|
# Usage:
|
||||||
|
# ./fresh-npm-install.sh # Regular execution
|
||||||
|
# ./fresh-npm-install.sh -n # Non-deterministic mode (removes package-lock.json)
|
||||||
|
|
||||||
|
declare NON_DETERMINISTIC_FLAG=0
|
||||||
|
|
||||||
|
|
||||||
|
main() {
|
||||||
|
parse_args "$@"
|
||||||
|
ensure_npm_is_available
|
||||||
|
ensure_npm_root
|
||||||
|
remove_existing_modules
|
||||||
|
if [[ $NON_DETERMINISTIC_FLAG -eq 1 ]]; then
|
||||||
|
remove_package_lock_json
|
||||||
|
fi
|
||||||
|
install_dependencies
|
||||||
|
run_unit_tests
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_npm_is_available() {
|
||||||
|
if ! command -v npm &> /dev/null; then
|
||||||
|
log::fatal 'npm could not be found, please install it first.'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_npm_root() {
|
||||||
|
if [ ! -f package.json ]; then
|
||||||
|
log::fatal 'Current directory is not a npm root. Please run the script in a npm root directory.'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_existing_modules() {
|
||||||
|
if [ -d ./node_modules ]; then
|
||||||
|
log::info 'Removing existing node modules...'
|
||||||
|
if ! rm -rf ./node_modules; then
|
||||||
|
log::fatal 'Could not remove existing node modules.'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_dependencies() {
|
||||||
|
log::info 'Installing dependencies...'
|
||||||
|
if ! npm install; then
|
||||||
|
log::fatal 'Failed to install dependencies.'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_package_lock_json() {
|
||||||
|
if [ -f ./package-lock.json ]; then
|
||||||
|
log::info 'Removing package-lock.json...'
|
||||||
|
if ! rm -rf ./package-lock.json; then
|
||||||
|
log::fatal 'Could not remove package-lock.json.'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_unit_tests() {
|
||||||
|
log::info 'Running unit tests...'
|
||||||
|
if ! npm run test:unit; then
|
||||||
|
pwd
|
||||||
|
log::fatal 'Failed to run unit tests.'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info() {
|
||||||
|
local -r message="$1"
|
||||||
|
echo "📣 ${message}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log::fatal() {
|
||||||
|
local -r message="$1"
|
||||||
|
echo "❌ ${message}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while getopts "n" opt; do
|
||||||
|
case ${opt} in
|
||||||
|
n)
|
||||||
|
NON_DETERMINISTIC_FLAG=1
|
||||||
|
;;
|
||||||
|
\?)
|
||||||
|
echo "Invalid option: $OPTARG" 1>&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$1"
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ICodeBuilder } from './ICodeBuilder';
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
const NewLine = '\n';
|
|
||||||
const TotalFunctionSeparatorChars = 58;
|
const TotalFunctionSeparatorChars = 58;
|
||||||
|
|
||||||
export abstract class CodeBuilder implements ICodeBuilder {
|
export abstract class CodeBuilder implements ICodeBuilder {
|
||||||
@@ -59,10 +58,12 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public toString(): string {
|
public toString(): string {
|
||||||
return this.lines.join(NewLine);
|
return this.lines.join(this.getNewLineTerminator());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract getCommentDelimiter(): string;
|
protected abstract getCommentDelimiter(): string;
|
||||||
|
|
||||||
protected abstract writeStandardOut(text: string): string;
|
protected abstract writeStandardOut(text: string): string;
|
||||||
|
|
||||||
|
protected abstract getNewLineTerminator(): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export class BatchBuilder extends CodeBuilder {
|
|||||||
protected writeStandardOut(text: string): string {
|
protected writeStandardOut(text: string): string {
|
||||||
return `echo ${escapeForEcho(text)}`;
|
return `echo ${escapeForEcho(text)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getNewLineTerminator(): string {
|
||||||
|
return '\r\n';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeForEcho(text: string) {
|
function escapeForEcho(text: string) {
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export class ShellBuilder extends CodeBuilder {
|
|||||||
protected writeStandardOut(text: string): string {
|
protected writeStandardOut(text: string): string {
|
||||||
return `echo '${escapeForEcho(text)}'`;
|
return `echo '${escapeForEcho(text)}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getNewLineTerminator(): string {
|
||||||
|
return '\n';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeForEcho(text: string) {
|
function escapeForEcho(text: string) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
|
|||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import WindowsData from '@/application/collections/windows.yaml';
|
import WindowsData from '@/application/collections/windows.yaml';
|
||||||
import MacOsData from '@/application/collections/macos.yaml';
|
import MacOsData from '@/application/collections/macos.yaml';
|
||||||
|
import LinuxData from '@/application/collections/linux.yaml';
|
||||||
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';
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
@@ -28,7 +29,7 @@ const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PreParsedCollections: readonly CollectionData [] = [
|
const PreParsedCollections: readonly CollectionData [] = [
|
||||||
WindowsData, MacOsData,
|
WindowsData, MacOsData, LinuxData,
|
||||||
];
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import type {
|
|||||||
} from '@/application/collections/';
|
} 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 { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
||||||
|
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||||
|
import { parseDocs } from './DocumentationParser';
|
||||||
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||||
import { parseScript } from './Script/ScriptParser';
|
import { parseScript } from './Script/ScriptParser';
|
||||||
|
|
||||||
@@ -12,35 +14,67 @@ let categoryIdCounter = 0;
|
|||||||
export function parseCategory(
|
export function parseCategory(
|
||||||
category: CategoryData,
|
category: CategoryData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
|
factory: CategoryFactoryType = CategoryFactory,
|
||||||
): Category {
|
): Category {
|
||||||
if (!context) { throw new Error('missing context'); }
|
if (!context) { throw new Error('missing context'); }
|
||||||
ensureValid(category);
|
return parseCategoryRecursively({
|
||||||
|
categoryData: category,
|
||||||
|
context,
|
||||||
|
factory,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICategoryParseContext {
|
||||||
|
readonly categoryData: CategoryData,
|
||||||
|
readonly context: ICategoryCollectionParseContext,
|
||||||
|
readonly factory: CategoryFactoryType,
|
||||||
|
readonly parentCategory?: CategoryData,
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
||||||
|
ensureValidCategory(context.categoryData, context.parentCategory);
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
subCategories: new Array<Category>(),
|
subCategories: new Array<Category>(),
|
||||||
subScripts: new Array<Script>(),
|
subScripts: new Array<Script>(),
|
||||||
};
|
};
|
||||||
for (const data of category.children) {
|
for (const data of context.categoryData.children) {
|
||||||
parseCategoryChild(data, children, category, context);
|
parseNode({
|
||||||
|
nodeData: data,
|
||||||
|
children,
|
||||||
|
parent: context.categoryData,
|
||||||
|
factory: context.factory,
|
||||||
|
context: context.context,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return context.factory(
|
||||||
|
/* id: */ categoryIdCounter++,
|
||||||
|
/* name: */ context.categoryData.category,
|
||||||
|
/* docs: */ parseDocs(context.categoryData),
|
||||||
|
/* categories: */ children.subCategories,
|
||||||
|
/* scripts: */ children.subScripts,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
new NodeValidator({
|
||||||
|
type: NodeType.Category,
|
||||||
|
selfNode: context.categoryData,
|
||||||
|
parentNode: context.parentCategory,
|
||||||
|
}).throw(err.message);
|
||||||
}
|
}
|
||||||
return new Category(
|
|
||||||
/* id: */ categoryIdCounter++,
|
|
||||||
/* name: */ category.category,
|
|
||||||
/* docs: */ parseDocUrls(category),
|
|
||||||
/* categories: */ children.subCategories,
|
|
||||||
/* scripts: */ children.subScripts,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValid(category: CategoryData) {
|
function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
|
||||||
if (!category) {
|
new NodeValidator({
|
||||||
throw Error('missing category');
|
type: NodeType.Category,
|
||||||
}
|
selfNode: category,
|
||||||
if (!category.children || category.children.length === 0) {
|
parentNode: parentCategory,
|
||||||
throw Error(`category has no children: "${category.category}"`);
|
})
|
||||||
}
|
.assertDefined(category)
|
||||||
if (!category.category || category.category.length === 0) {
|
.assertValidName(category.category)
|
||||||
throw Error('category has no name');
|
.assert(
|
||||||
}
|
() => category.children && category.children.length > 0,
|
||||||
|
`"${category.category}" has no children.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICategoryChildren {
|
interface ICategoryChildren {
|
||||||
@@ -48,22 +82,29 @@ interface ICategoryChildren {
|
|||||||
subScripts: Script[];
|
subScripts: Script[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategoryChild(
|
interface INodeParseContext {
|
||||||
data: CategoryOrScriptData,
|
readonly nodeData: CategoryOrScriptData;
|
||||||
children: ICategoryChildren,
|
readonly children: ICategoryChildren;
|
||||||
parent: CategoryData,
|
readonly parent: CategoryData;
|
||||||
context: ICategoryCollectionParseContext,
|
readonly factory: CategoryFactoryType;
|
||||||
) {
|
readonly context: ICategoryCollectionParseContext;
|
||||||
if (isCategory(data)) {
|
}
|
||||||
const subCategory = parseCategory(data as CategoryData, context);
|
function parseNode(context: INodeParseContext) {
|
||||||
children.subCategories.push(subCategory);
|
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
|
||||||
} else if (isScript(data)) {
|
validator.assertDefined(context.nodeData);
|
||||||
const scriptData = data as ScriptData;
|
if (isCategory(context.nodeData)) {
|
||||||
const script = parseScript(scriptData, context);
|
const subCategory = parseCategoryRecursively({
|
||||||
children.subScripts.push(script);
|
categoryData: context.nodeData as CategoryData,
|
||||||
|
context: context.context,
|
||||||
|
factory: context.factory,
|
||||||
|
parentCategory: context.parent,
|
||||||
|
});
|
||||||
|
context.children.subCategories.push(subCategory);
|
||||||
|
} else if (isScript(context.nodeData)) {
|
||||||
|
const script = parseScript(context.nodeData as ScriptData, context.context);
|
||||||
|
context.children.subScripts.push(script);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Child element is neither a category or a script.
|
validator.throw('Node is neither a category or a script.');
|
||||||
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,14 +114,22 @@ function isScript(data: CategoryOrScriptData): data is ScriptData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||||
const { category } = data as CategoryData;
|
return hasProperty(data, 'category');
|
||||||
return category && category.length > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(holder: InstructionHolder): boolean {
|
function hasCode(data: InstructionHolder): boolean {
|
||||||
return holder.code && holder.code.length > 0;
|
return hasProperty(data, 'code');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCall(holder: InstructionHolder) {
|
function hasCall(data: InstructionHolder) {
|
||||||
return holder.call !== undefined;
|
return hasProperty(data, 'call');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasProperty(object: unknown, propertyName: string) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CategoryFactoryType = (
|
||||||
|
...parameters: ConstructorParameters<typeof Category>) => Category;
|
||||||
|
|
||||||
|
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);
|
||||||
|
|||||||
@@ -1,64 +1,58 @@
|
|||||||
import type { DocumentableData, DocumentationUrlsData } from '@/application/collections/';
|
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||||
|
|
||||||
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||||
if (!documentable) {
|
if (!documentable) {
|
||||||
throw new Error('missing documentable');
|
throw new Error('missing documentable');
|
||||||
}
|
}
|
||||||
const { docs } = documentable;
|
const { docs } = documentable;
|
||||||
if (!docs || !docs.length) {
|
if (!docs) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
let result = new DocumentationUrlContainer();
|
let result = new DocumentationContainer();
|
||||||
result = addDocs(docs, result);
|
result = addDocs(docs, result);
|
||||||
return result.getAll();
|
return result.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDocs(
|
function addDocs(
|
||||||
docs: DocumentationUrlsData,
|
docs: DocumentationData,
|
||||||
urls: DocumentationUrlContainer,
|
container: DocumentationContainer,
|
||||||
): DocumentationUrlContainer {
|
): DocumentationContainer {
|
||||||
if (docs instanceof Array) {
|
if (docs instanceof Array) {
|
||||||
urls.addUrls(docs);
|
if (docs.length > 0) {
|
||||||
|
container.addParts(docs);
|
||||||
|
}
|
||||||
} else if (typeof docs === 'string') {
|
} else if (typeof docs === 'string') {
|
||||||
urls.addUrl(docs);
|
container.addPart(docs);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Docs field (documentation url) must a string or array of strings');
|
throwInvalidType();
|
||||||
}
|
}
|
||||||
return urls;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DocumentationUrlContainer {
|
class DocumentationContainer {
|
||||||
private readonly urls = new Array<string>();
|
private readonly parts = new Array<string>();
|
||||||
|
|
||||||
public addUrl(url: string) {
|
public addPart(documentation: string) {
|
||||||
validateUrl(url);
|
if (!documentation) {
|
||||||
this.urls.push(url);
|
throw Error('missing documentation');
|
||||||
|
}
|
||||||
|
if (typeof documentation !== 'string') {
|
||||||
|
throwInvalidType();
|
||||||
|
}
|
||||||
|
this.parts.push(documentation);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addUrls(urls: readonly string[]) {
|
public addParts(parts: readonly string[]) {
|
||||||
for (const url of urls) {
|
for (const part of parts) {
|
||||||
if (typeof url !== 'string') {
|
this.addPart(part);
|
||||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
|
||||||
}
|
|
||||||
this.addUrl(url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAll(): ReadonlyArray<string> {
|
public getAll(): ReadonlyArray<string> {
|
||||||
return this.urls;
|
return this.parts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateUrl(docUrl: string): void {
|
function throwInvalidType() {
|
||||||
if (!docUrl) {
|
throw new Error('docs field (documentation) must be an array of strings');
|
||||||
throw new Error('Documentation url is null or empty');
|
|
||||||
}
|
|
||||||
if (docUrl.includes('\n')) {
|
|
||||||
throw new Error('Documentation url cannot be multi-lined.');
|
|
||||||
}
|
|
||||||
const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
|
|
||||||
const res = docUrl.match(validUrlRegex);
|
|
||||||
if (res == null) {
|
|
||||||
throw new Error(`Invalid documentation url: ${docUrl}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/application/Parser/NodeValidation/NodeData.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { ScriptData, CategoryData } from '@/application/collections/';
|
||||||
|
|
||||||
|
export type NodeData = CategoryData | ScriptData;
|
||||||
35
src/application/Parser/NodeValidation/NodeDataError.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { NodeType } from './NodeType';
|
||||||
|
import { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export class NodeDataError extends Error {
|
||||||
|
constructor(message: string, public readonly context: INodeDataErrorContext) {
|
||||||
|
super(createMessage(message, context));
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype); // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
||||||
|
this.name = new.target.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeDataErrorContext {
|
||||||
|
readonly type?: NodeType;
|
||||||
|
readonly selfNode: NodeData;
|
||||||
|
readonly parentNode?: NodeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMessage(errorMessage: string, context: INodeDataErrorContext) {
|
||||||
|
let message = '';
|
||||||
|
if (context.type !== undefined) {
|
||||||
|
message += `${NodeType[context.type]}: `;
|
||||||
|
}
|
||||||
|
message += errorMessage;
|
||||||
|
message += `\n${dump(context)}`;
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dump(context: INodeDataErrorContext): string {
|
||||||
|
const printJson = (obj: unknown) => JSON.stringify(obj, undefined, 2);
|
||||||
|
let output = `Self: ${printJson(context.selfNode)}`;
|
||||||
|
if (context.parentNode) {
|
||||||
|
output += `\nParent: ${printJson(context.parentNode)}`;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
4
src/application/Parser/NodeValidation/NodeType.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum NodeType {
|
||||||
|
Script,
|
||||||
|
Category,
|
||||||
|
}
|
||||||
38
src/application/Parser/NodeValidation/NodeValidator.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { INodeDataErrorContext, NodeDataError } from './NodeDataError';
|
||||||
|
import { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export class NodeValidator {
|
||||||
|
constructor(private readonly context: INodeDataErrorContext) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertValidName(nameValue: string) {
|
||||||
|
return this
|
||||||
|
.assert(
|
||||||
|
() => Boolean(nameValue),
|
||||||
|
'missing name',
|
||||||
|
)
|
||||||
|
.assert(
|
||||||
|
() => typeof nameValue === 'string',
|
||||||
|
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertDefined(node: NodeData) {
|
||||||
|
return this.assert(
|
||||||
|
() => node !== undefined && node !== null && Object.keys(node).length > 0,
|
||||||
|
'missing node data',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public assert(validationPredicate: () => boolean, errorMessage: string) {
|
||||||
|
if (!validationPredicate()) {
|
||||||
|
this.throw(errorMessage);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public throw(errorMessage: string) {
|
||||||
|
throw new NodeDataError(errorMessage, this.context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,13 +3,26 @@ import { ProjectInformation } from '@/domain/ProjectInformation';
|
|||||||
import { Version } from '@/domain/Version';
|
import { Version } from '@/domain/Version';
|
||||||
|
|
||||||
export function parseProjectInformation(
|
export function parseProjectInformation(
|
||||||
environment: NodeJS.ProcessEnv,
|
environment: NodeJS.ProcessEnv | VueAppEnvironment,
|
||||||
): IProjectInformation {
|
): IProjectInformation {
|
||||||
const version = new Version(environment.VUE_APP_VERSION);
|
const version = new Version(environment[VueAppEnvironmentKeys.VUE_APP_VERSION]);
|
||||||
return new ProjectInformation(
|
return new ProjectInformation(
|
||||||
environment.VUE_APP_NAME,
|
environment[VueAppEnvironmentKeys.VUE_APP_NAME],
|
||||||
version,
|
version,
|
||||||
environment.VUE_APP_REPOSITORY_URL,
|
environment[VueAppEnvironmentKeys.VUE_APP_SLOGAN],
|
||||||
environment.VUE_APP_HOMEPAGE_URL,
|
environment[VueAppEnvironmentKeys.VUE_APP_REPOSITORY_URL],
|
||||||
|
environment[VueAppEnvironmentKeys.VUE_APP_HOMEPAGE_URL],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const VueAppEnvironmentKeys = {
|
||||||
|
VUE_APP_VERSION: 'VUE_APP_VERSION',
|
||||||
|
VUE_APP_NAME: 'VUE_APP_NAME',
|
||||||
|
VUE_APP_SLOGAN: 'VUE_APP_SLOGAN',
|
||||||
|
VUE_APP_REPOSITORY_URL: 'VUE_APP_REPOSITORY_URL',
|
||||||
|
VUE_APP_HOMEPAGE_URL: 'VUE_APP_HOMEPAGE_URL',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type VueAppEnvironment = {
|
||||||
|
[K in keyof typeof VueAppEnvironmentKeys]: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { FunctionData } from '@/application/collections/';
|
import type { FunctionData } from '@/application/collections/';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
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';
|
||||||
import { SyntaxFactory } from './Syntax/SyntaxFactory';
|
import { SyntaxFactory } from './Validation/Syntax/SyntaxFactory';
|
||||||
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
import { ISyntaxFactory } from './Validation/Syntax/ISyntaxFactory';
|
||||||
|
import { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||||
public readonly compiler: IScriptCompiler;
|
public readonly compiler: IScriptCompiler;
|
||||||
|
|||||||
@@ -13,4 +13,22 @@ export class ExpressionPosition {
|
|||||||
throw Error(`negative start position: ${start}`);
|
throw Error(`negative start position: ${start}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isInInsideOf(potentialParent: ExpressionPosition): boolean {
|
||||||
|
if (this.isSame(potentialParent)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return potentialParent.start <= this.start
|
||||||
|
&& potentialParent.end >= this.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isSame(other: ExpressionPosition): boolean {
|
||||||
|
return other.start === this.start
|
||||||
|
&& other.end === this.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isIntersecting(other: ExpressionPosition): boolean {
|
||||||
|
return (other.start < this.end && other.end > this.start)
|
||||||
|
|| (this.end > other.start && other.start >= this.start);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,21 +20,59 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
|
|||||||
if (!code) {
|
if (!code) {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
const expressions = this.extractor.findExpressions(code);
|
|
||||||
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
|
||||||
const context = new ExpressionEvaluationContext(args);
|
const context = new ExpressionEvaluationContext(args);
|
||||||
const compiledCode = compileExpressions(expressions, code, context);
|
const compiledCode = compileRecursively(code, context, this.extractor);
|
||||||
return compiledCode;
|
return compiledCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compileRecursively(
|
||||||
|
code: string,
|
||||||
|
context: IExpressionEvaluationContext,
|
||||||
|
extractor: IExpressionParser,
|
||||||
|
): string {
|
||||||
|
/*
|
||||||
|
Instead of compiling code at once and returning we compile expressions from the code.
|
||||||
|
And recompile expressions from resulting code recursively.
|
||||||
|
This allows using expressions inside expressions blocks. E.g.:
|
||||||
|
```
|
||||||
|
{{ with $condition }}
|
||||||
|
echo '{{ $text }}'
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
Without recursing parameter substitution for '{{ $text }}' is skipped once the outer
|
||||||
|
{{ with $condition }} is rendered.
|
||||||
|
A more optimized alternative to recursion would be to a parse an expression tree
|
||||||
|
instead of linear expression lists.
|
||||||
|
*/
|
||||||
|
if (!code) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
const expressions = extractor.findExpressions(code);
|
||||||
|
if (expressions.length === 0) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
const compiledCode = compileExpressions(expressions, code, context);
|
||||||
|
return compileRecursively(compiledCode, context, extractor);
|
||||||
|
}
|
||||||
|
|
||||||
function compileExpressions(
|
function compileExpressions(
|
||||||
expressions: readonly IExpression[],
|
expressions: readonly IExpression[],
|
||||||
code: string,
|
code: string,
|
||||||
context: IExpressionEvaluationContext,
|
context: IExpressionEvaluationContext,
|
||||||
) {
|
) {
|
||||||
|
ensureValidExpressions(expressions, code, context);
|
||||||
let compiledCode = '';
|
let compiledCode = '';
|
||||||
const sortedExpressions = expressions
|
const outerExpressions = expressions.filter(
|
||||||
|
(expression) => expressions
|
||||||
|
.filter((otherExpression) => otherExpression !== expression)
|
||||||
|
.every((otherExpression) => !expression.position.isInInsideOf(otherExpression.position)),
|
||||||
|
);
|
||||||
|
/*
|
||||||
|
This logic will only compile outer expressions if there were nested expressions.
|
||||||
|
So the output of this compilation may result in new uncompiled expressions.
|
||||||
|
*/
|
||||||
|
const sortedExpressions = outerExpressions
|
||||||
.slice() // copy the array to not mutate the parameter
|
.slice() // copy the array to not mutate the parameter
|
||||||
.sort((a, b) => b.position.start - a.position.start);
|
.sort((a, b) => b.position.start - a.position.start);
|
||||||
let index = 0;
|
let index = 0;
|
||||||
@@ -65,6 +103,43 @@ function extractRequiredParameterNames(
|
|||||||
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
|
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printList(list: readonly string[]): string {
|
||||||
|
return `"${list.join('", "')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidExpressions(
|
||||||
|
expressions: readonly IExpression[],
|
||||||
|
code: string,
|
||||||
|
context: IExpressionEvaluationContext,
|
||||||
|
) {
|
||||||
|
ensureParamsUsedInCodeHasArgsProvided(expressions, context.args);
|
||||||
|
ensureExpressionsDoesNotExtendCodeLength(expressions, code);
|
||||||
|
ensureNoExpressionsAtSamePosition(expressions);
|
||||||
|
ensureNoInvalidIntersections(expressions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureExpressionsDoesNotExtendCodeLength(
|
||||||
|
expressions: readonly IExpression[],
|
||||||
|
code: string,
|
||||||
|
) {
|
||||||
|
const expectedMax = code.length;
|
||||||
|
const expressionsOutOfRange = expressions
|
||||||
|
.filter((expression) => expression.position.end > expectedMax);
|
||||||
|
if (expressionsOutOfRange.length > 0) {
|
||||||
|
throw new Error(`Expressions out of range:\n${JSON.stringify(expressionsOutOfRange)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoExpressionsAtSamePosition(expressions: readonly IExpression[]) {
|
||||||
|
const instructionsAtSamePosition = expressions.filter(
|
||||||
|
(expression) => expressions
|
||||||
|
.filter((other) => expression.position.isSame(other.position)).length > 1,
|
||||||
|
);
|
||||||
|
if (instructionsAtSamePosition.length > 0) {
|
||||||
|
throw new Error(`Instructions at same position:\n${JSON.stringify(instructionsAtSamePosition)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ensureParamsUsedInCodeHasArgsProvided(
|
function ensureParamsUsedInCodeHasArgsProvided(
|
||||||
expressions: readonly IExpression[],
|
expressions: readonly IExpression[],
|
||||||
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
@@ -80,6 +155,16 @@ function ensureParamsUsedInCodeHasArgsProvided(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function printList(list: readonly string[]): string {
|
function ensureNoInvalidIntersections(expressions: readonly IExpression[]) {
|
||||||
return `"${list.join('", "')}"`;
|
const intersectingInstructions = expressions.filter(
|
||||||
|
(expression) => expressions
|
||||||
|
.filter((other) => expression.position.isIntersecting(other.position))
|
||||||
|
.filter((other) => !expression.position.isSame(other.position))
|
||||||
|
.filter((other) => !expression.position.isInInsideOf(other.position))
|
||||||
|
.filter((other) => !other.position.isInInsideOf(expression.position))
|
||||||
|
.length > 0,
|
||||||
|
);
|
||||||
|
if (intersectingInstructions.length > 0) {
|
||||||
|
throw new Error(`Instructions intersecting unexpectedly:\n${JSON.stringify(intersectingInstructions)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ export class ExpressionRegexBuilder {
|
|||||||
.addRawRegex('([^|\\s]+)');
|
.addRawRegex('([^|\\s]+)');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchAnythingExceptSurroundingWhitespaces() {
|
public matchMultilineAnythingExceptSurroundingWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectZeroOrMoreWhitespaces()
|
||||||
.addRawRegex('(.+?)')
|
.addRawRegex('([\\S\\s]+?)')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectZeroOrMoreWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class EscapeDoubleQuotes implements IPipe {
|
|||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
return raw.replaceAll('"', '"^""');
|
return raw.replaceAll('"', '"^""');
|
||||||
/* eslint-disable max-len */
|
/* eslint-disable vue/max-len */
|
||||||
/*
|
/*
|
||||||
"^"" is the most robust and stable choice.
|
"^"" is the most robust and stable choice.
|
||||||
Other options:
|
Other options:
|
||||||
@@ -28,6 +28,6 @@ export class EscapeDoubleQuotes implements IPipe {
|
|||||||
Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"`
|
Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"`
|
||||||
A good explanation: https://stackoverflow.com/a/31413730
|
A good explanation: https://stackoverflow.com/a/31413730
|
||||||
*/
|
*/
|
||||||
/* eslint-enable max-len */
|
/* eslint-enable vue/max-len */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class WithParser extends RegexParser {
|
|||||||
.matchUntilFirstWhitespace() // First match: parameter name
|
.matchUntilFirstWhitespace() // First match: parameter name
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
// ...
|
// ...
|
||||||
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||||
// {{ end }}
|
// {{ end }}
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('end')
|
.expectCharacters('end')
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function compileCode(
|
|||||||
compiler: IExpressionsCompiler,
|
compiler: IExpressionsCompiler,
|
||||||
): ICompiledFunctionCall {
|
): ICompiledFunctionCall {
|
||||||
return {
|
return {
|
||||||
code: compiler.compileExpressions(code.do, args),
|
code: compiler.compileExpressions(code.execute, args),
|
||||||
revertCode: compiler.compileExpressions(code.revert, args),
|
revertCode: compiler.compileExpressions(code.revert, args),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ export enum FunctionBodyType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IFunctionCode {
|
export interface IFunctionCode {
|
||||||
readonly do: string;
|
readonly execute: string;
|
||||||
readonly revert?: string;
|
readonly revert?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { FunctionData } from '@/application/collections/';
|
import type { FunctionData } from '@/application/collections/';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
|
|
||||||
export interface ISharedFunctionsParser {
|
export interface ISharedFunctionsParser {
|
||||||
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
|
parseFunctions(
|
||||||
|
functions: readonly FunctionData[],
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
|
): ISharedFunctionCollection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IFunctionCall } from './Call/IFunctionCall';
|
import { IFunctionCall } from './Call/IFunctionCall';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
||||||
} from './ISharedFunction';
|
} from './ISharedFunction';
|
||||||
@@ -25,7 +26,7 @@ export function createFunctionWithInlineCode(
|
|||||||
throw new Error(`undefined code in function "${name}"`);
|
throw new Error(`undefined code in function "${name}"`);
|
||||||
}
|
}
|
||||||
const content: IFunctionCode = {
|
const content: IFunctionCode = {
|
||||||
do: code,
|
execute: code,
|
||||||
revert: revertCode,
|
revert: revertCode,
|
||||||
};
|
};
|
||||||
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);
|
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { FunctionData, InstructionHolder } from '@/application/collections/';
|
import type { FunctionData, InstructionHolder } from '@/application/collections/';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
|
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
||||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
@@ -12,16 +17,20 @@ import { parseFunctionCalls } from './Call/FunctionCallParser';
|
|||||||
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||||
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
||||||
|
|
||||||
|
constructor(private readonly codeValidator: ICodeValidator = CodeValidator.instance) { }
|
||||||
|
|
||||||
public parseFunctions(
|
public parseFunctions(
|
||||||
functions: readonly FunctionData[],
|
functions: readonly FunctionData[],
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
): ISharedFunctionCollection {
|
): ISharedFunctionCollection {
|
||||||
|
if (!syntax) { throw new Error('missing syntax'); }
|
||||||
const collection = new SharedFunctionCollection();
|
const collection = new SharedFunctionCollection();
|
||||||
if (!functions || !functions.length) {
|
if (!functions || !functions.length) {
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
ensureValidFunctions(functions);
|
ensureValidFunctions(functions);
|
||||||
return functions
|
return functions
|
||||||
.map((func) => parseFunction(func))
|
.map((func) => parseFunction(func, syntax, this.codeValidator))
|
||||||
.reduce((acc, func) => {
|
.reduce((acc, func) => {
|
||||||
acc.addFunction(func);
|
acc.addFunction(func);
|
||||||
return acc;
|
return acc;
|
||||||
@@ -29,10 +38,15 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFunction(data: FunctionData): ISharedFunction {
|
function parseFunction(
|
||||||
|
data: FunctionData,
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
|
validator: ICodeValidator,
|
||||||
|
): ISharedFunction {
|
||||||
const { name } = data;
|
const { name } = data;
|
||||||
const parameters = parseParameters(data);
|
const parameters = parseParameters(data);
|
||||||
if (hasCode(data)) {
|
if (hasCode(data)) {
|
||||||
|
validateCode(data, syntax, validator);
|
||||||
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||||
}
|
}
|
||||||
// Has call
|
// Has call
|
||||||
@@ -40,6 +54,19 @@ function parseFunction(data: FunctionData): ISharedFunction {
|
|||||||
return createCallerFunction(name, parameters, calls);
|
return createCallerFunction(name, parameters, calls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateCode(
|
||||||
|
data: FunctionData,
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
|
validator: ICodeValidator,
|
||||||
|
): void {
|
||||||
|
[data.code, data.revertCode].forEach(
|
||||||
|
(code) => validator.throwIfInvalid(
|
||||||
|
code,
|
||||||
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
||||||
return (data.parameters || [])
|
return (data.parameters || [])
|
||||||
.map((parameter) => {
|
.map((parameter) => {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { FunctionData, ScriptData } from '@/application/collections/';
|
import type { FunctionData, ScriptData } from '@/application/collections/';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
import { IScriptCompiler } from './IScriptCompiler';
|
import { IScriptCompiler } from './IScriptCompiler';
|
||||||
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||||
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
|
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
|
||||||
@@ -8,18 +12,20 @@ import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompi
|
|||||||
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
|
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
|
||||||
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||||
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
||||||
|
import { ICompiledCode } from './Function/Call/Compiler/ICompiledCode';
|
||||||
|
|
||||||
export class ScriptCompiler implements IScriptCompiler {
|
export class ScriptCompiler implements IScriptCompiler {
|
||||||
private readonly functions: ISharedFunctionCollection;
|
private readonly functions: ISharedFunctionCollection;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
functions: readonly FunctionData[] | undefined,
|
functions: readonly FunctionData[] | undefined,
|
||||||
private readonly syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
|
||||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||||
|
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||||
|
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||||
) {
|
) {
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
if (!syntax) { throw new Error('missing syntax'); }
|
||||||
this.functions = sharedFunctionsParser.parseFunctions(functions);
|
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(script: ScriptData): boolean {
|
public canCompile(script: ScriptData): boolean {
|
||||||
@@ -35,13 +41,19 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
try {
|
try {
|
||||||
const calls = parseFunctionCalls(script.call);
|
const calls = parseFunctionCalls(script.call);
|
||||||
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
||||||
|
validateCompiledCode(compiledCode, this.codeValidator);
|
||||||
return new ScriptCode(
|
return new ScriptCode(
|
||||||
compiledCode.code,
|
compiledCode.code,
|
||||||
compiledCode.revertCode,
|
compiledCode.revertCode,
|
||||||
this.syntax,
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw Error(`Script "${script.name}" ${error.message}`);
|
throw Error(`Script "${script.name}" ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateCompiledCode(compiledCode: ICompiledCode, validator: ICodeValidator): void {
|
||||||
|
[compiledCode.code, compiledCode.revertCode].forEach(
|
||||||
|
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
|
import { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
export interface ICategoryCollectionParseContext {
|
export interface ICategoryCollectionParseContext {
|
||||||
readonly compiler: IScriptCompiler;
|
readonly compiler: IScriptCompiler;
|
||||||
|
|||||||
@@ -1,26 +1,41 @@
|
|||||||
import type { ScriptData } from '@/application/collections/';
|
import type { ScriptData } from '@/application/collections/';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
import { parseDocUrls } from '../DocumentationParser';
|
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
|
import { parseDocs } from '../DocumentationParser';
|
||||||
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
||||||
|
import { NodeType } from '../NodeValidation/NodeType';
|
||||||
|
import { NodeValidator } from '../NodeValidation/NodeValidator';
|
||||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
import { CodeValidator } from './Validation/CodeValidator';
|
||||||
|
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
export function parseScript(
|
export function parseScript(
|
||||||
data: ScriptData,
|
data: ScriptData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
levelParser = createEnumParser(RecommendationLevel),
|
levelParser = createEnumParser(RecommendationLevel),
|
||||||
|
scriptFactory: ScriptFactoryType = ScriptFactory,
|
||||||
|
codeValidator: ICodeValidator = CodeValidator.instance,
|
||||||
): Script {
|
): Script {
|
||||||
validateScript(data);
|
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
||||||
|
validateScript(data, validator);
|
||||||
if (!context) { throw new Error('missing context'); }
|
if (!context) { throw new Error('missing context'); }
|
||||||
const script = new Script(
|
try {
|
||||||
/* name: */ data.name,
|
const script = scriptFactory(
|
||||||
/* code: */ parseCode(data, context),
|
/* name: */ data.name,
|
||||||
/* docs: */ parseDocUrls(data),
|
/* code: */ parseCode(data, context, codeValidator),
|
||||||
/* level: */ parseLevel(data.recommend, levelParser),
|
/* docs: */ parseDocs(data),
|
||||||
);
|
/* level: */ parseLevel(data.recommend, levelParser),
|
||||||
return script;
|
);
|
||||||
|
return script;
|
||||||
|
} catch (err) {
|
||||||
|
validator.throw(err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLevel(
|
function parseLevel(
|
||||||
@@ -33,28 +48,50 @@ function parseLevel(
|
|||||||
return parser.parseEnum(level, 'level');
|
return parser.parseEnum(level, 'level');
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCode(script: ScriptData, context: ICategoryCollectionParseContext): IScriptCode {
|
function parseCode(
|
||||||
|
script: ScriptData,
|
||||||
|
context: ICategoryCollectionParseContext,
|
||||||
|
codeValidator: ICodeValidator,
|
||||||
|
): IScriptCode {
|
||||||
if (context.compiler.canCompile(script)) {
|
if (context.compiler.canCompile(script)) {
|
||||||
return context.compiler.compile(script);
|
return context.compiler.compile(script);
|
||||||
}
|
}
|
||||||
return new ScriptCode(script.code, script.revertCode, context.syntax);
|
const code = new ScriptCode(script.code, script.revertCode);
|
||||||
|
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
||||||
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNotBothCallAndCode(script: ScriptData) {
|
function validateHardcodedCodeWithoutCalls(
|
||||||
if (script.code && script.call) {
|
scriptCode: ScriptCode,
|
||||||
throw new Error('cannot define both "call" and "code"');
|
codeValidator: ICodeValidator,
|
||||||
}
|
syntax: ILanguageSyntax,
|
||||||
if (script.revertCode && script.call) {
|
) {
|
||||||
throw new Error('cannot define "revertCode" if "call" is defined');
|
[scriptCode.execute, scriptCode.revert].forEach(
|
||||||
}
|
(code) => codeValidator.throwIfInvalid(
|
||||||
|
code,
|
||||||
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateScript(script: ScriptData) {
|
function validateScript(script: ScriptData, validator: NodeValidator) {
|
||||||
if (!script) {
|
validator
|
||||||
throw new Error('missing script');
|
.assertDefined(script)
|
||||||
}
|
.assertValidName(script.name)
|
||||||
if (!script.code && !script.call) {
|
.assert(
|
||||||
throw new Error('must define either "call" or "code"');
|
() => Boolean(script.code || script.call),
|
||||||
}
|
'Must define either "call" or "code".',
|
||||||
ensureNotBothCallAndCode(script);
|
)
|
||||||
|
.assert(
|
||||||
|
() => !(script.code && script.call),
|
||||||
|
'Cannot define both "call" and "code".',
|
||||||
|
)
|
||||||
|
.assert(
|
||||||
|
() => !(script.revertCode && script.call),
|
||||||
|
'Cannot define "revertCode" if "call" is defined.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ScriptFactoryType = (...parameters: ConstructorParameters<typeof Script>) => Script;
|
||||||
|
|
||||||
|
const ScriptFactory: ScriptFactoryType = (...parameters) => new Script(...parameters);
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
|
|
||||||
export class ShellScriptSyntax implements ILanguageSyntax {
|
|
||||||
public readonly commentDelimiters = ['#'];
|
|
||||||
|
|
||||||
public readonly commonCodeParts = ['(', ')', 'else', 'fi'];
|
|
||||||
}
|
|
||||||
46
src/application/Parser/Script/Validation/CodeValidator.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { ICodeValidationRule, IInvalidCodeLine } from './ICodeValidationRule';
|
||||||
|
import { ICodeValidator } from './ICodeValidator';
|
||||||
|
import { ICodeLine } from './ICodeLine';
|
||||||
|
|
||||||
|
export class CodeValidator implements ICodeValidator {
|
||||||
|
public static readonly instance: ICodeValidator = new CodeValidator();
|
||||||
|
|
||||||
|
public throwIfInvalid(
|
||||||
|
code: string,
|
||||||
|
rules: readonly ICodeValidationRule[],
|
||||||
|
): void {
|
||||||
|
if (!rules || rules.length === 0) { throw new Error('missing rules'); }
|
||||||
|
if (!code) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = extractLines(code);
|
||||||
|
const invalidLines = rules.flatMap((rule) => rule.analyze(lines));
|
||||||
|
if (invalidLines.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const errorText = `Errors with the code.\n${printLines(lines, invalidLines)}`;
|
||||||
|
throw new Error(errorText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLines(code: string): ICodeLine[] {
|
||||||
|
return code
|
||||||
|
.split(/\r\n|\r|\n/)
|
||||||
|
.map((lineText, lineIndex): ICodeLine => ({
|
||||||
|
index: lineIndex + 1,
|
||||||
|
text: lineText,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function printLines(
|
||||||
|
lines: readonly ICodeLine[],
|
||||||
|
invalidLines: readonly IInvalidCodeLine[],
|
||||||
|
): string {
|
||||||
|
return lines.map((line) => {
|
||||||
|
const badLine = invalidLines.find((invalidLine) => invalidLine.index === line.index);
|
||||||
|
if (!badLine) {
|
||||||
|
return `[${line.index}] ✅ ${line.text}`;
|
||||||
|
}
|
||||||
|
return `[${badLine.index}] ❌ ${line.text}\n\t⟶ ${badLine.error}`;
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
4
src/application/Parser/Script/Validation/ICodeLine.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ICodeLine {
|
||||||
|
readonly index: number;
|
||||||
|
readonly text: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { ICodeLine } from './ICodeLine';
|
||||||
|
|
||||||
|
export interface IInvalidCodeLine {
|
||||||
|
readonly index: number;
|
||||||
|
readonly error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICodeValidationRule {
|
||||||
|
analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { ICodeValidationRule } from './ICodeValidationRule';
|
||||||
|
|
||||||
|
export interface ICodeValidator {
|
||||||
|
throwIfInvalid(
|
||||||
|
code: string,
|
||||||
|
rules: readonly ICodeValidationRule[],
|
||||||
|
): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { ICodeLine } from '../ICodeLine';
|
||||||
|
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||||
|
|
||||||
|
export class NoDuplicatedLines implements ICodeValidationRule {
|
||||||
|
constructor(private readonly syntax: ILanguageSyntax) {
|
||||||
|
if (!syntax) { throw new Error('missing syntax'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||||
|
return lines
|
||||||
|
.map((line): IDuplicateAnalyzedLine => ({
|
||||||
|
index: line.index,
|
||||||
|
isIgnored: shouldIgnoreLine(line.text, this.syntax),
|
||||||
|
occurrenceIndices: lines
|
||||||
|
.filter((other) => other.text === line.text)
|
||||||
|
.map((duplicatedLine) => duplicatedLine.index),
|
||||||
|
}))
|
||||||
|
.filter((line) => hasInvalidDuplicates(line))
|
||||||
|
.map((line): IInvalidCodeLine => ({
|
||||||
|
index: line.index,
|
||||||
|
error: `Line is duplicated at line numbers ${line.occurrenceIndices.join(',')}.`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDuplicateAnalyzedLine {
|
||||||
|
readonly index: number;
|
||||||
|
readonly occurrenceIndices: readonly number[];
|
||||||
|
readonly isIgnored: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasInvalidDuplicates(line: IDuplicateAnalyzedLine): boolean {
|
||||||
|
return !line.isIgnored && line.occurrenceIndices.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
||||||
|
const lowerCaseCodeLine = codeLine.toLowerCase();
|
||||||
|
const isCommentLine = () => syntax.commentDelimiters.some(
|
||||||
|
(delimiter) => lowerCaseCodeLine.startsWith(delimiter),
|
||||||
|
);
|
||||||
|
const consistsOfFrequentCommands = () => {
|
||||||
|
const trimmed = lowerCaseCodeLine.trim().split(' ');
|
||||||
|
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
|
||||||
|
};
|
||||||
|
return isCommentLine() || consistsOfFrequentCommands();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ICodeLine } from '../ICodeLine';
|
||||||
|
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||||
|
|
||||||
|
export class NoEmptyLines implements ICodeValidationRule {
|
||||||
|
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||||
|
return lines
|
||||||
|
.filter((line) => (line.text?.trim().length ?? 0) === 0)
|
||||||
|
.map((line): IInvalidCodeLine => ({
|
||||||
|
index: line.index,
|
||||||
|
error: (() => {
|
||||||
|
if (!line.text) {
|
||||||
|
return 'Empty line';
|
||||||
|
}
|
||||||
|
const markedText = line.text
|
||||||
|
.replaceAll(' ', '{whitespace}')
|
||||||
|
.replaceAll('\t', '{tab}');
|
||||||
|
return `Empty line: "${markedText}"`;
|
||||||
|
})(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
|
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
|
||||||
const PowerShellCommonCodeParts = ['{', '}'];
|
const PowerShellCommonCodeParts = ['{', '}'];
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ILanguageSyntax {
|
||||||
|
readonly commentDelimiters: string[];
|
||||||
|
readonly commonCodeParts: string[];
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||||
|
import { ILanguageSyntax } from './ILanguageSyntax';
|
||||||
|
|
||||||
export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;
|
export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
|
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||||
|
public readonly commentDelimiters = ['#'];
|
||||||
|
|
||||||
|
public readonly commonCodeParts = ['(', ')', 'else', 'fi', 'done'];
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { BatchFileSyntax } from './BatchFileSyntax';
|
import { BatchFileSyntax } from './BatchFileSyntax';
|
||||||
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||||
import { ISyntaxFactory } from './ISyntaxFactory';
|
import { ISyntaxFactory } from './ISyntaxFactory';
|
||||||
@@ -12,10 +12,10 @@ declare module '@/application/collections/*' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CategoryOrScriptData = CategoryData | ScriptData;
|
export type CategoryOrScriptData = CategoryData | ScriptData;
|
||||||
export type DocumentationUrlsData = ReadonlyArray<string> | string;
|
export type DocumentationData = ReadonlyArray<string> | string;
|
||||||
|
|
||||||
export interface DocumentableData {
|
export interface DocumentableData {
|
||||||
readonly docs?: DocumentationUrlsData;
|
readonly docs?: DocumentationData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstructionHolder {
|
export interface InstructionHolder {
|
||||||
|
|||||||
3690
src/application/collections/linux.yaml
Normal file
@@ -509,6 +509,83 @@ actions:
|
|||||||
function: PersistUserEnvironmentConfiguration
|
function: PersistUserEnvironmentConfiguration
|
||||||
parameters:
|
parameters:
|
||||||
configuration: export POWERSHELL_TELEMETRY_OPTOUT=1
|
configuration: export POWERSHELL_TELEMETRY_OPTOUT=1
|
||||||
|
-
|
||||||
|
category: Configure Parallels Desktop
|
||||||
|
docs: |-
|
||||||
|
Parallels Desktop for Mac is software providing hardware virtualization for macOS [1].
|
||||||
|
|
||||||
|
When you use it, it collects and share your personal data to third parties [2]. Personal
|
||||||
|
data include IP address of your device, your broad geographical location (country, state
|
||||||
|
(if applicable), and city) and used product [2].
|
||||||
|
|
||||||
|
It includes third-party ads [3] and automatic check for updates [4] by default. Both of these
|
||||||
|
behaviors communicate with online services that reveal data about you.
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20221012155943/https://en.wikipedia.org/wiki/Parallels_Desktop_for_Mac "Parallels Desktop for Mac - Wikipedia | en.wikipedia.org"
|
||||||
|
[2]: https://web.archive.org/web/20221012155829/https://www.parallels.com/about/legal/privacy/ "Privacy Statement | parallels.com"
|
||||||
|
[3]: https://web.archive.org/web/20221012151800/https://kb.parallels.com/114422 "How do I turn off notifications in Parallels Desktop and Parallels Access? | Knowledge Base | parallels.com"
|
||||||
|
[4]: https://web.archive.org/web/20221012151953/http://download.parallels.com/stm/docs/en/Parallels_Desktop_Users_Guide/22220.htm "Automatic Updating | Parallels Desktop Users Guide | download.parallels.com"
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Turn off ads in Parallels Desktop
|
||||||
|
recommend: standard
|
||||||
|
docs: |-
|
||||||
|
Parallels Desktop in-product notifications to show ads from Parallels or other third
|
||||||
|
party companies [1].
|
||||||
|
|
||||||
|
The main setting is `ProductPromo.ForcePromoOff` [1] that you can check using:
|
||||||
|
|
||||||
|
1. `defaults read 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff'`
|
||||||
|
2. `defaults read 'com.parallels.Parallels Desktop' 'WelcomeScreenPromo.PromoOff'`
|
||||||
|
|
||||||
|
By default, on clean installations the value is `0` which is equivalent of `no`.
|
||||||
|
|
||||||
|
There is also `WelcomeScreenPromo.PromoOff` setting that's pre-configured to `1` (`no` as
|
||||||
|
default). It's undocumented but still kept disabled by this script.
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/save/https://forum.parallels.com/threads/unable-to-process-the-upgrade-request.345603/ "Unable to process the upgrade request | Parallels Forums | forum.parallels.com"
|
||||||
|
[2]: https://web.archive.org/web/20221012151800/https://kb.parallels.com/114422 "How do I turn off notifications in Parallels Desktop and Parallels Access? | Knowledge Base | parallels.com"
|
||||||
|
code: |-
|
||||||
|
defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool yes
|
||||||
|
defaults write 'com.parallels.Parallels Desktop' 'WelcomeScreenPromo.PromoOff' -bool yes
|
||||||
|
revertCode: |-
|
||||||
|
defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool no
|
||||||
|
defaults write 'com.parallels.Parallels Desktop' 'WelcomeScreenPromo.PromoOff' -bool yes
|
||||||
|
-
|
||||||
|
category: Disable Parallels Desktop auto-updates
|
||||||
|
docs: |-
|
||||||
|
Parallels Desktop by default checks for updates frequently and automatically downloads them [1].
|
||||||
|
This reveal personal data about [2] you without your control.
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20221012151953/http://download.parallels.com/stm/docs/en/Parallels_Desktop_Users_Guide/22220.htm "Automatic Updating | Parallels Desktop Users Guide | download.parallels.com"
|
||||||
|
[2]: https://web.archive.org/web/20221012155829/https://www.parallels.com/about/legal/privacy/ "Privacy Statement | parallels.com"
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable automatically downloading Parallels Desktop updates
|
||||||
|
docs: |-
|
||||||
|
Automatic downloads are enabled by default, and this script disables automatic downloads.
|
||||||
|
|
||||||
|
Automatic downloads are configured using the `Application preferences.Download updates automatically` property [1].
|
||||||
|
|
||||||
|
- Check: `defaults read 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically'`
|
||||||
|
- Values: 0 - Disabled, 1 - Enabled (default)
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20221012153810/https://download.parallels.com/desktop/v18/docs/en_US/Parallels-Desktop-Business-Edition-Administrators-Guide/37744.htm "Parallels Desktop Business Edition Administrator's Guide v18 - Configuring individual Macs | download.parallels.com"
|
||||||
|
code: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool no
|
||||||
|
revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool yes
|
||||||
|
-
|
||||||
|
name: Disable automatically checking for Parallels Desktop updates
|
||||||
|
docs: |-
|
||||||
|
Automatic checks are weekly by default, and this script disables the checks completely.
|
||||||
|
|
||||||
|
Frequency to check for updates can be configured using `Application preferences.Check for updates` property [1].
|
||||||
|
|
||||||
|
- Check: `defaults read 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates'`
|
||||||
|
- Values: 0 - Never, 1 - Once a day, 2 - Once a week (default), 3 - Once a month
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20221012153810/https://download.parallels.com/desktop/v18/docs/en_US/Parallels-Desktop-Business-Edition-Administrators-Guide/37744.htm "Parallels Desktop Business Edition Administrator's Guide v18 - Configuring individual Macs | download.parallels.com"
|
||||||
|
code: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 0
|
||||||
|
revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 2
|
||||||
-
|
-
|
||||||
category: Configure OS
|
category: Configure OS
|
||||||
children:
|
children:
|
||||||
@@ -638,6 +715,58 @@ actions:
|
|||||||
name: Disable Spotlight indexing
|
name: Disable Spotlight indexing
|
||||||
code: sudo mdutil -i off -d /
|
code: sudo mdutil -i off -d /
|
||||||
revertCode: sudo mdutil -i on /
|
revertCode: sudo mdutil -i on /
|
||||||
|
-
|
||||||
|
name: Disable Personalized advertisements and identifier collection
|
||||||
|
recommend: standard
|
||||||
|
docs: |-
|
||||||
|
This script enhances your privacy by deactivating Personalized Ads and disabling the collection
|
||||||
|
of identifiers related to your device. The process involves modifying certain key configurations,
|
||||||
|
which prevents Apple's advertising platform from using your personal information to deliver targeted
|
||||||
|
ads [1].
|
||||||
|
|
||||||
|
When Personalized Ads is enabled, your information may be used to provide ads that closely align
|
||||||
|
with your interests [1]. You might occasionally encounter such targeted ads in Apple News, Stocks,
|
||||||
|
and the Mac App Store [2]. Disabling Personalized Ads will prevent Apple from using your data for
|
||||||
|
ad targeting [2]. Although this does not necessarily decrease the quantity of ads you receive,
|
||||||
|
it may result in the ads being less relevant to your interests [2].
|
||||||
|
|
||||||
|
The primary keys to deactivating personalized ads are:
|
||||||
|
|
||||||
|
- **`allowApplePersonalizedAdvertising`**: If set to false, this restricts Apple's personalized
|
||||||
|
advertising [3]. This is applicable on macOS 12 and subsequent versions [3].
|
||||||
|
- **`allowIdentifierForAdvertising`**: The `advertisingIdentifier` is a unique string assigned
|
||||||
|
to each device [5]. Apple uses this identifier and recommends its use in third-party
|
||||||
|
applications for tasks like frequency capping, attribution, conversion events, estimating the
|
||||||
|
number of unique users, detecting advertising fraud, and debugging [5]. Although there is no
|
||||||
|
official documentation on it, a discussion on JAMF.com corroborates its existence [6].
|
||||||
|
|
||||||
|
My tests show that disabling any of the keys mentioned above results in the
|
||||||
|
"System Preferences > Apple Advertising > Personalized ads" option being deactivated in the GUI,
|
||||||
|
starting from macOS Monterey.
|
||||||
|
|
||||||
|
Please note: The `forceLimitAdTracking` key limits ad tracking [3] [4] and is found in CIS
|
||||||
|
benchmarks for macOS [4]. However, the official macOS documentation specifies that it is
|
||||||
|
applicable only to iOS 7 and later versions, not to macOS [3]. The key does not exist on the OS
|
||||||
|
by default.
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20230731152633/https://www.apple.com/legal/privacy/data/en/apple-advertising/ "Legal - Apple Advertising & Privacy - Apple"
|
||||||
|
[2]: https://web.archive.org/web/20220805052411/https://support.apple.com/en-sg/guide/mac-help/mh32356/mac: "Change Privacy preferences on Mac - Apple Support (SG)"
|
||||||
|
[3]: https://web.archive.org/web/20230731155827/https://developer.apple.com/documentation/devicemanagement/restrictions "Restrictions | Apple Developer Documentation"
|
||||||
|
[4]: https://web.archive.org/web/20230731155653/https://paper.bobylive.com/Security/CIS/CIS_Apple_macOS_11_0_Big_Sur_Benchmark_v2_0_0.pdf "CIS Apple macOS 11.0 Big Sur Benchmark"
|
||||||
|
[5]: https://web.archive.org/web/20230731155131/https://developer.apple.com/documentation/adsupport/asidentifiermanager/1614151-advertisingidentifier "advertisingIdentifier | Apple Developer Documentation"
|
||||||
|
[6]: https://web.archive.org/web/20230731154840/https://community.jamf.com/t5/jamf-pro/macos-quot-limit-ad-tracking-quot/td-p/217001 'Solved: macOS "Limit Ad Tracking" - Jamf Nation Community - 217001'
|
||||||
|
code: |-
|
||||||
|
defaults write com.apple.AdLib allowIdentifierForAdvertising -bool false
|
||||||
|
defaults write com.apple.AdLib allowApplePersonalizedAdvertising -bool false
|
||||||
|
defaults write com.apple.AdLib forceLimitAdTracking -bool true
|
||||||
|
# Default: (`defaults read com.apple.AdLib`)
|
||||||
|
# - `defaults read com.apple.AdLib allowApplePersonalizedAdvertising`: true (1)
|
||||||
|
# - `defaults read com.apple.AdLib allowIdentifierForAdvertising`: true (1)
|
||||||
|
# - `defaults read com.apple.AdLib forceLimitAdTracking`: non-existing
|
||||||
|
revertCode: |-
|
||||||
|
defaults write com.apple.AdLib allowIdentifierForAdvertising -bool true
|
||||||
|
defaults write com.apple.AdLib allowApplePersonalizedAdvertising -bool true
|
||||||
|
sudo defaults delete com.apple.AdLib forceLimitAdTracking
|
||||||
-
|
-
|
||||||
category: Security improvements
|
category: Security improvements
|
||||||
children:
|
children:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class Category extends BaseEntity<number> implements ICategory {
|
|||||||
constructor(
|
constructor(
|
||||||
id: number,
|
id: number,
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly documentationUrls: ReadonlyArray<string>,
|
public readonly docs: ReadonlyArray<string>,
|
||||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
public readonly subCategories?: ReadonlyArray<ICategory>,
|
||||||
public readonly scripts?: ReadonlyArray<IScript>,
|
public readonly scripts?: ReadonlyArray<IScript>,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export interface IDocumentable {
|
export interface IDocumentable {
|
||||||
readonly documentationUrls: ReadonlyArray<string>;
|
readonly docs: ReadonlyArray<string>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Version } from '@/domain/Version';
|
|||||||
export interface IProjectInformation {
|
export interface IProjectInformation {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly version: Version;
|
readonly version: Version;
|
||||||
|
|
||||||
|
readonly slogan: string;
|
||||||
readonly repositoryUrl: string;
|
readonly repositoryUrl: string;
|
||||||
readonly homepage: string;
|
readonly homepage: string;
|
||||||
readonly feedbackUrl: string;
|
readonly feedbackUrl: string;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { IScriptCode } from './IScriptCode';
|
|||||||
export interface IScript extends IEntity<string>, IDocumentable {
|
export interface IScript extends IEntity<string>, IDocumentable {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly level?: RecommendationLevel;
|
readonly level?: RecommendationLevel;
|
||||||
readonly documentationUrls: ReadonlyArray<string>;
|
readonly docs: ReadonlyArray<string>;
|
||||||
readonly code: IScriptCode;
|
readonly code: IScriptCode;
|
||||||
canRevert(): boolean;
|
canRevert(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export class ProjectInformation implements IProjectInformation {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly version: Version,
|
public readonly version: Version,
|
||||||
|
public readonly slogan: string,
|
||||||
public readonly repositoryUrl: string,
|
public readonly repositoryUrl: string,
|
||||||
public readonly homepage: string,
|
public readonly homepage: string,
|
||||||
) {
|
) {
|
||||||
@@ -18,6 +19,9 @@ export class ProjectInformation implements IProjectInformation {
|
|||||||
if (!version) {
|
if (!version) {
|
||||||
throw new Error('undefined version');
|
throw new Error('undefined version');
|
||||||
}
|
}
|
||||||
|
if (!slogan) {
|
||||||
|
throw new Error('undefined slogan');
|
||||||
|
}
|
||||||
if (!repositoryUrl) {
|
if (!repositoryUrl) {
|
||||||
throw new Error('repositoryUrl is undefined');
|
throw new Error('repositoryUrl is undefined');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ export class Script extends BaseEntity<string> implements IScript {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly code: IScriptCode,
|
public readonly code: IScriptCode,
|
||||||
public readonly documentationUrls: ReadonlyArray<string>,
|
public readonly docs: ReadonlyArray<string>,
|
||||||
public readonly level?: RecommendationLevel,
|
public readonly level?: RecommendationLevel,
|
||||||
) {
|
) {
|
||||||
super(name);
|
super(name);
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error(`missing code (script: ${name})`);
|
throw new Error('missing code');
|
||||||
}
|
}
|
||||||
validateLevel(level);
|
validateLevel(level);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,25 +4,18 @@ export class ScriptCode implements IScriptCode {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly execute: string,
|
public readonly execute: string,
|
||||||
public readonly revert: string,
|
public readonly revert: string,
|
||||||
syntax: ILanguageSyntax,
|
|
||||||
) {
|
) {
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
validateCode(execute);
|
||||||
validateCode(execute, syntax);
|
validateRevertCode(revert, execute);
|
||||||
validateRevertCode(revert, execute, syntax);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILanguageSyntax {
|
function validateRevertCode(revertCode: string, execute: string) {
|
||||||
readonly commentDelimiters: string[];
|
|
||||||
readonly commonCodeParts: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateRevertCode(revertCode: string, execute: string, syntax: ILanguageSyntax) {
|
|
||||||
if (!revertCode) {
|
if (!revertCode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
validateCode(revertCode, syntax);
|
validateCode(revertCode);
|
||||||
if (execute === revertCode) {
|
if (execute === revertCode) {
|
||||||
throw new Error('Code itself and its reverting code cannot be the same');
|
throw new Error('Code itself and its reverting code cannot be the same');
|
||||||
}
|
}
|
||||||
@@ -31,54 +24,8 @@ function validateRevertCode(revertCode: string, execute: string, syntax: ILangua
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(code: string, syntax: ILanguageSyntax): void {
|
function validateCode(code: string): void {
|
||||||
if (!code || code.length === 0) {
|
if (!code || code.length === 0) {
|
||||||
throw new Error('missing code');
|
throw new Error('missing code');
|
||||||
}
|
}
|
||||||
ensureNoEmptyLines(code);
|
|
||||||
ensureCodeHasUniqueLines(code, syntax);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoEmptyLines(code: string): void {
|
|
||||||
const lines = code.split(/\r\n|\r|\n/);
|
|
||||||
if (lines.some((line) => line.trim().length === 0)) {
|
|
||||||
throw Error(`Script has empty lines:\n${lines.map((part, index) => `\n (${index}) ${part || '❌'}`).join('')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
|
|
||||||
const allLines = code.split(/\r\n|\r|\n/);
|
|
||||||
const checkedLines = allLines.filter((line) => !shouldIgnoreLine(line, syntax));
|
|
||||||
if (checkedLines.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const duplicateLines = checkedLines.filter((e, i, a) => a.indexOf(e) !== i);
|
|
||||||
if (duplicateLines.length !== 0) {
|
|
||||||
throw Error(`Duplicates detected in script:\n${printDuplicatedLines(allLines)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function printDuplicatedLines(allLines: string[]) {
|
|
||||||
return allLines
|
|
||||||
.map((line, index) => {
|
|
||||||
const occurrenceIndices = allLines
|
|
||||||
.map((e, i) => (e === line ? i : ''))
|
|
||||||
.filter(String);
|
|
||||||
const isDuplicate = occurrenceIndices.length > 1;
|
|
||||||
const indicator = isDuplicate ? `❌ (${occurrenceIndices.join(',')})\t` : '✅ ';
|
|
||||||
return `${indicator}[${index}] ${line}`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
|
||||||
const lowerCaseCodeLine = codeLine.toLowerCase();
|
|
||||||
const isCommentLine = () => syntax.commentDelimiters.some(
|
|
||||||
(delimiter) => lowerCaseCodeLine.startsWith(delimiter),
|
|
||||||
);
|
|
||||||
const consistsOfFrequentCommands = () => {
|
|
||||||
const trimmed = lowerCaseCodeLine.trim().split(' ');
|
|
||||||
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
|
|
||||||
};
|
|
||||||
return isCommentLine() || consistsOfFrequentCommands();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export class CodeRunner {
|
|||||||
|
|
||||||
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
||||||
switch (environment.os) {
|
switch (environment.os) {
|
||||||
|
case OperatingSystem.Linux:
|
||||||
|
return `x-terminal-emulator -e '${scriptPath}'`;
|
||||||
case OperatingSystem.macOS:
|
case OperatingSystem.macOS:
|
||||||
return `open -a Terminal.app ${scriptPath}`;
|
return `open -a Terminal.app ${scriptPath}`;
|
||||||
// Another option with graphical sudo would be
|
// Another option with graphical sudo would be
|
||||||
|
|||||||
6
src/presentation/assets/icons/external-link.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="M10 6v2H5v11h11v-5h2v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6zm11-3v9l-3.794-3.793-5.999 6-1.414-1.414 5.999-6L12 3h9z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 302 B |
@@ -4,18 +4,19 @@
|
|||||||
|
|
||||||
@use "@/presentation/assets/styles/colors" as *;
|
@use "@/presentation/assets/styles/colors" as *;
|
||||||
@use "@/presentation/assets/styles/fonts" as *;
|
@use "@/presentation/assets/styles/fonts" as *;
|
||||||
|
@use "@/presentation/assets/styles/mixins" as *;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$globals-color-hover: $color-primary;
|
||||||
a {
|
a {
|
||||||
color:inherit;
|
color:inherit;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
@include hover-or-touch {
|
||||||
color: $color-primary;
|
color: $globals-color-hover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
src/presentation/assets/styles/_mixins.scss
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
@mixin hover-or-touch($selector-suffix: '', $selector-prefix: '&') {
|
||||||
|
@media (hover: hover) {
|
||||||
|
/* We only do this if hover is truly supported; otherwise the emulator in mobile
|
||||||
|
keeps hovered style in-place even after touching, making it sticky. */
|
||||||
|
#{$selector-prefix}:hover #{$selector-suffix} {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (hover: none) {
|
||||||
|
/* We only do this if hover is not supported,otherwise the desktop behavior is not
|
||||||
|
as desired; it does not get activated on hover but only during click/touch. */
|
||||||
|
#{$selector-prefix}:active #{$selector-suffix} {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin clickable($cursor: 'pointer') {
|
||||||
|
cursor: #{$cursor};
|
||||||
|
user-select: none;
|
||||||
|
/*
|
||||||
|
It removes (blue) background during touch as seen in mobile webkit browsers (Chrome, Safari, Edge).
|
||||||
|
The default behavior is that any element (or containing element) that has cursor:pointer
|
||||||
|
explicitly set and is clicked will flash blue momentarily.
|
||||||
|
Removing it could have accessibility issue since that hides an interactive cue. But as we still provide
|
||||||
|
response to user actions through :active by `hover-or-touch` mixin.
|
||||||
|
*/
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||