Compare commits
31 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 |
@@ -1,2 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
[*.{js,jsx,ts,tsx,vue,sh}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
|
||||
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: baseVariablesRules } = require('eslint-config-airbnb-base/rules/variables');
|
||||
const tsconfigJson = require('./tsconfig.json');
|
||||
require('@rushstack/eslint-patch/modern-module-resolution');
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
@@ -17,9 +13,7 @@ module.exports = {
|
||||
'plugin:vue/essential',
|
||||
|
||||
// Extends eslint-config-airbnb
|
||||
// Added by Vue CLI
|
||||
// Here until https://github.com/vuejs/eslint-config-airbnb/issues/23 is done
|
||||
'@vue/airbnb',
|
||||
'@vue/eslint-config-airbnb-with-typescript',
|
||||
|
||||
// Extends @typescript-eslint/recommended
|
||||
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||
@@ -27,7 +21,14 @@ module.exports = {
|
||||
'@vue/typescript/recommended',
|
||||
],
|
||||
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: {
|
||||
...getOwnRules(),
|
||||
@@ -45,18 +46,6 @@ module.exports = {
|
||||
mocha: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts?(x)', '**/*.d.ts'],
|
||||
parserOptions: {
|
||||
// Setting project is required for some rules such as @typescript-eslint/dot-notation,
|
||||
// @typescript-eslint/return-await and @typescript-eslint/no-throw-literal.
|
||||
// If this property is missing they fail due to missing parser.
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
rules: {
|
||||
...getTypeScriptOverrides(),
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/tests/**/*.{j,t}s?(x)'],
|
||||
rules: {
|
||||
@@ -108,6 +97,7 @@ function getOpinionatedRuleOverrides() {
|
||||
return {
|
||||
// https://erkinekici.com/articles/linting-trap#no-use-before-define
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#arrow-body-style
|
||||
'arrow-body-style': 'off',
|
||||
// https://erkinekici.com/articles/linting-trap#no-plusplus
|
||||
@@ -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() {
|
||||
return Object.keys(tsconfigJson.compilerOptions.paths)
|
||||
.map((path) => `${path}*`);
|
||||
|
||||
@@ -21,8 +21,9 @@ A clear and concise description of what the bug is.
|
||||
|
||||
<!--
|
||||
Which OS are you using? What version of OS you were using?
|
||||
On Windows you can find it using "Start button" > "Settings" > "System" > "About".
|
||||
On macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
|
||||
On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||
On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
||||
On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
||||
-->
|
||||
|
||||
### Reproduction steps
|
||||
|
||||
@@ -14,7 +14,7 @@ You could alternatively send a PR directly (see CONTRIBUTING.md).
|
||||
|
||||
<!--
|
||||
Which OS will the new script configure?
|
||||
Either "Windows" or "macOS".
|
||||
One of the supported OSes: "Windows", "macOS" or "Linux".
|
||||
-->
|
||||
|
||||
### Name
|
||||
@@ -30,10 +30,12 @@ E.g. "Disable webcam telemetry"
|
||||
<!--
|
||||
Code that will be executed when script is selected.
|
||||
Try to keep it as simple and backwards-compatible as possible.
|
||||
Allowed languages:
|
||||
- macOS: bash (sh)
|
||||
Allowed languages:
|
||||
- Windows: PowerShell (ps1) or batchfile
|
||||
- 💡 Prioritize the one that's simpler, batchfile if similar.
|
||||
- macOS: bash (sh)
|
||||
- Linux: bash (sh) or Python 3
|
||||
- 💡 Prioritize the one that's simpler, bash if similar.
|
||||
-->
|
||||
|
||||
### Revert code
|
||||
|
||||
20
.github/workflows/checks.build.yaml
vendored
@@ -53,3 +53,23 @@ jobs:
|
||||
run: |-
|
||||
cross-env-shell NODE_ENV=${{ 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
|
||||
|
||||
57
CHANGELOG.md
@@ -1,5 +1,62 @@
|
||||
# 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)
|
||||
|
||||
* Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# privacy.sexy
|
||||
# privacy.sexy — Now you have the choice
|
||||
|
||||
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
||||
> Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆
|
||||
|
||||
<!-- markdownlint-disable MD033 -->
|
||||
<p align="center">
|
||||
@@ -120,7 +120,7 @@ Online version does not require to run any software on your computer. Offline ve
|
||||
- **Reversible**. Revert if something feels wrong.
|
||||
- **Accessible**. No need to run any compiled software on your computer with web version.
|
||||
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
||||
- **Tested.** A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
||||
- **Tested**. A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
||||
- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md).
|
||||
- **Portable and simple**. Every script is independently executable without cross-dependencies.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# build
|
||||
|
||||
- These are the file that are used by electron.
|
||||
- Logos are created by from the [PNG icon](./../public/icon.png)
|
||||
- by running `npx electron-icon-builder --input=./public/icon.png --output=build --flatten`
|
||||
This folder contains files that are used by Electron to serve the desktop version.
|
||||
|
||||
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md).
|
||||
|
||||
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 740 B After Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |
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"
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
|
||||
- 💡 Best practices
|
||||
- If you repeat yourself, try to utilize [YAML-defined functions](#Function)
|
||||
- Always try to add documentation and a way to revert a tweak in [scripts](#Script)
|
||||
- If you repeat yourself, try to utilize [YAML-defined functions](#function)
|
||||
- Always try to add documentation and a way to revert a tweak in [scripts](#script)
|
||||
- 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
|
||||
|
||||
## Objects
|
||||
@@ -13,19 +13,19 @@
|
||||
- A collection simply defines:
|
||||
- different categories and their scripts in a tree structure
|
||||
- OS specific details
|
||||
- Also allows defining common [function](#Function)s to be used throughout the collection if you'd like different scripts to share same code.
|
||||
- Also allows defining common [function](#function)s to be used throughout the collection if you'd like different scripts to share same code.
|
||||
|
||||
#### `Collection` syntax
|
||||
|
||||
- `os:` *`string`* (**required**)
|
||||
- Operating system that the [Collection](#collection) is written for.
|
||||
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
|
||||
- `actions: [` ***[`Category`](#Category)*** `, ... ]` **(required)**
|
||||
- `actions: [` ***[`Category`](#category)*** `, ... ]` **(required)**
|
||||
- Each [category](#category) is rendered as different cards in card presentation.
|
||||
- ❗ A [Collection](#collection) must consist of at least one category.
|
||||
- `functions: [` ***[`Function`](#Function)*** `, ... ]`
|
||||
- `functions: [` ***[`Function`](#function)*** `, ... ]`
|
||||
- Functions are optionally defined to re-use the same code throughout different scripts.
|
||||
- `scripting:` ***[`ScriptingDefinition`](#ScriptingDefinition)*** **(required)**
|
||||
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
|
||||
- Defines the scripting language that the code of other action uses.
|
||||
|
||||
### `Category`
|
||||
@@ -38,9 +38,12 @@
|
||||
- `category:` *`string`* (**required**)
|
||||
- Name of the category
|
||||
- ❗ Must be unique throughout the [Collection](#collection)
|
||||
- `children: [` ***[`Category`](#Category)*** `|` [***`Script`***](#Script) `, ... ]` (**required**)
|
||||
- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#script) `, ... ]` (**required**)
|
||||
- ❗ Category must consist of at least one subcategory or script.
|
||||
- Children can be combination of scripts and subcategories.
|
||||
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||
- Documentation pieces related to the category.
|
||||
- Rendered as markdown.
|
||||
|
||||
### `Script`
|
||||
|
||||
@@ -67,12 +70,12 @@
|
||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||
- ❗ Do not define if `call` is defined.
|
||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
||||
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**)
|
||||
- A shared function or sequence of functions to call (called in order)
|
||||
- ❗ If not defined `code` must be defined
|
||||
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||
- Single documentation URL or list of URLs for those who wants to learn more about the script
|
||||
- E.g. `https://docs.microsoft.com/en-us/windows-server/`
|
||||
- Documentation pieces related to the script.
|
||||
- Rendered as markdown.
|
||||
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
|
||||
- If not defined then the script will not be recommended
|
||||
- If defined it can be either
|
||||
@@ -120,7 +123,7 @@
|
||||
- Convention is to use camelCase, and be verbs.
|
||||
- E.g. `uninstallStoreApp`
|
||||
- ❗ Function names must be unique
|
||||
- `parameters`: `[` ***[`FunctionParameter`](#FunctionParameter)*** `, ... ]`
|
||||
- `parameters`: `[` ***[`FunctionParameter`](#functionparameter)*** `, ... ]`
|
||||
- List of parameters that function code refers to.
|
||||
- ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
|
||||
`code`: *`string`* (**required** if `call` is undefined)
|
||||
@@ -133,7 +136,7 @@
|
||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in code
|
||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
||||
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**)
|
||||
- A shared function or sequence of functions to call (called in order)
|
||||
- The parameter values that are sent can use [expressions (templating)](./templating.md#expressions)
|
||||
- ❗ If not defined `code` must be defined
|
||||
@@ -141,7 +144,7 @@
|
||||
### `FunctionParameter`
|
||||
|
||||
- Defines a parameter that function requires optionally or mandatory.
|
||||
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#FunctionCall).
|
||||
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#functioncall).
|
||||
|
||||
#### `FunctionParameter` syntax
|
||||
|
||||
|
||||
@@ -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 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
|
||||
|
||||
|
||||
@@ -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.
|
||||
- [**`/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
|
||||
|
||||
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 `}}`).
|
||||
- E.g. `Hello {{ $name }} !`
|
||||
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) that has inspired this templating language.
|
||||
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
|
||||
- Functions enables usage of expressions.
|
||||
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
||||
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
||||
- Expressions inside expressions (nested templates) are supported.
|
||||
- An expression can output another expression that will also be compiled.
|
||||
- E.g. following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output.
|
||||
|
||||
```go
|
||||
{{ with $condition }}
|
||||
echo {{ $text }}
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
### Parameter substitution
|
||||
|
||||
@@ -56,9 +65,31 @@ A function can call other functions such as:
|
||||
|
||||
### with
|
||||
|
||||
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions. E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}`.
|
||||
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
|
||||
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
|
||||
|
||||
Binds its context (`.`) value of provided argument for the parameter if provided one. E.g. `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
|
||||
It binds its context (value of the provided parameter value) as arbitrary `.` value. It allows you to use the argument value of the given parameter when it is provided and not empty such as:
|
||||
|
||||
```go
|
||||
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
|
||||
```
|
||||
|
||||
It supports multiline text inside the block. You can have something like:
|
||||
|
||||
```go
|
||||
{{ with $argument }}
|
||||
First line
|
||||
Second line
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
||||
|
||||
```go
|
||||
{{ with $condition }}
|
||||
This is a different parameter: {{ $text }}
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
||||
|
||||
|
||||
@@ -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.
|
||||
- Test names and folders have logical structure based on tests executed.
|
||||
- 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.
|
||||
- [`/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.
|
||||
|
||||
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 |
34826
package-lock.json
generated
121
package.json
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.11.3",
|
||||
"version": "0.12.0",
|
||||
"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",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
@@ -10,84 +11,98 @@
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"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",
|
||||
"create-icons": "node img/logo-update.mjs",
|
||||
"electron:build": "vue-cli-service electron:build",
|
||||
"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:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||
"lint:eslint": "vue-cli-service lint --no-fix --mode production",
|
||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstall": "electron-builder install-app-deps",
|
||||
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\""
|
||||
},
|
||||
"main": "background.js",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.0.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.0.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.0.0",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.6",
|
||||
"@juggle/resize-observer": "^3.3.1",
|
||||
"ace-builds": "^1.4.14",
|
||||
"core-js": "^3.21.1",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"electron-progressbar": "^2.0.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.9",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"ace-builds": "^1.23.4",
|
||||
"core-js": "^3.32.0",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"electron-progressbar": "^2.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"install": "^0.13.0",
|
||||
"liquor-tree": "^0.2.70",
|
||||
"npm": "^8.5.3",
|
||||
"markdown-it": "^13.0.1",
|
||||
"npm": "^9.8.1",
|
||||
"v-tooltip": "2.1.3",
|
||||
"vue": "^2.6.14",
|
||||
"vue": "^2.7.14",
|
||||
"vue-class-component": "^7.2.6",
|
||||
"vue-js-modal": "^2.0.1",
|
||||
"vue-property-decorator": "^9.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@types/ace": "^0.0.48",
|
||||
"@types/chai": "^4.3.0",
|
||||
"@types/chai": "^4.3.5",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.1",
|
||||
"@vue/cli-plugin-e2e-cypress": "~5.0.1",
|
||||
"@vue/cli-plugin-eslint": "~5.0.1",
|
||||
"@vue/cli-plugin-typescript": "~5.0.1",
|
||||
"@vue/cli-plugin-unit-mocha": "~5.0.1",
|
||||
"@vue/cli-service": "~5.0.1",
|
||||
"@vue/eslint-config-airbnb": "^6.0.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"@vue/test-utils": "1.3.0",
|
||||
"chai": "^4.3.6",
|
||||
"cypress": "^9.5.1",
|
||||
"electron": "^17.1.0",
|
||||
"electron-builder": "^22.14.13",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.8",
|
||||
"@vue/cli-plugin-e2e-cypress": "~5.0.8",
|
||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||
"@vue/cli-plugin-typescript": "~5.0.8",
|
||||
"@vue/cli-plugin-unit-mocha": "~5.0.8",
|
||||
"@vue/cli-service": "~5.0.8",
|
||||
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"chai": "^4.3.7",
|
||||
"cypress": "^12.17.2",
|
||||
"electron": "^25.3.2",
|
||||
"electron-builder": "^24.6.3",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-log": "^4.4.6",
|
||||
"electron-updater": "^5.0.0",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-vue": "^8.5.0",
|
||||
"eslint-plugin-vuejs-accessibility": "^1.1.1",
|
||||
"electron-icon-builder": "^2.0.1",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-updater": "^6.1.4",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-plugin-import": "^2.26.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",
|
||||
"markdownlint-cli": "^0.31.1",
|
||||
"remark-cli": "^10.0.1",
|
||||
"markdownlint-cli": "^0.35.0",
|
||||
"remark-cli": "^11.0.0",
|
||||
"remark-lint-no-dead-urls": "^1.1.0",
|
||||
"remark-preset-lint-consistent": "^5.1.1",
|
||||
"remark-validate-links": "^11.0.2",
|
||||
"sass": "^1.49.9",
|
||||
"sass-loader": "^12.6.0",
|
||||
"ts-loader": "9.0.1",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.6.2",
|
||||
"vue-cli-plugin-electron-builder": "^2.1.1",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"yaml-lint": "^1.2.4"
|
||||
"remark-preset-lint-consistent": "^5.1.2",
|
||||
"remark-validate-links": "^12.1.1",
|
||||
"sass": "^1.64.1",
|
||||
"sass-loader": "^13.3.2",
|
||||
"svgexport": "^0.4.2",
|
||||
"ts-loader": "^9.4.4",
|
||||
"typescript": "~4.6.2",
|
||||
"vue-cli-plugin-electron-builder": "^3.0.0-alpha.4",
|
||||
"yaml-lint": "^1.7.0",
|
||||
"tslib": "~2.4.0"
|
||||
},
|
||||
"overrides": {
|
||||
"vue-cli-plugin-electron-builder": {
|
||||
"electron-builder": "^24.6.3"
|
||||
}
|
||||
},
|
||||
"//devDependencies": {
|
||||
"ts-loader": "Here as workaround for vue-cli-plugin-electron-builder using older webpack 4",
|
||||
"eslint": "Stuck at 7.32.0 because Vue CLI not supporting 8.x.x"
|
||||
"typescript": [
|
||||
"Cannot upgrade to 5.X.X due to unmaintained @vue/cli-plugin-typescript, https://github.com/vuejs/vue-cli/issues/7401",
|
||||
"Cannot upgrade to > 4.6.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252"
|
||||
],
|
||||
"tslib": "Cannot upgrade to > 2.4.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252",
|
||||
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60",
|
||||
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60"
|
||||
},
|
||||
"homepage": "https://privacy.sexy",
|
||||
"repository": {
|
||||
|
||||
|
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">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<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="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">
|
||||
|
||||
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';
|
||||
|
||||
const NewLine = '\n';
|
||||
const TotalFunctionSeparatorChars = 58;
|
||||
|
||||
export abstract class CodeBuilder implements ICodeBuilder {
|
||||
@@ -59,10 +58,12 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.lines.join(NewLine);
|
||||
return this.lines.join(this.getNewLineTerminator());
|
||||
}
|
||||
|
||||
protected abstract getCommentDelimiter(): string;
|
||||
|
||||
protected abstract writeStandardOut(text: string): string;
|
||||
|
||||
protected abstract getNewLineTerminator(): string;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ export class BatchBuilder extends CodeBuilder {
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo ${escapeForEcho(text)}`;
|
||||
}
|
||||
|
||||
protected getNewLineTerminator(): string {
|
||||
return '\r\n';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeForEcho(text: string) {
|
||||
|
||||
@@ -8,6 +8,10 @@ export class ShellBuilder extends CodeBuilder {
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo '${escapeForEcho(text)}'`;
|
||||
}
|
||||
|
||||
protected getNewLineTerminator(): string {
|
||||
return '\n';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeForEcho(text: string) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import WindowsData from '@/application/collections/windows.yaml';
|
||||
import MacOsData from '@/application/collections/macos.yaml';
|
||||
import LinuxData from '@/application/collections/linux.yaml';
|
||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||
import { Application } from '@/domain/Application';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
@@ -28,7 +29,7 @@ const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
|
||||
};
|
||||
|
||||
const PreParsedCollections: readonly CollectionData [] = [
|
||||
WindowsData, MacOsData,
|
||||
WindowsData, MacOsData, LinuxData,
|
||||
];
|
||||
|
||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||
|
||||
@@ -3,7 +3,9 @@ import type {
|
||||
} from '@/application/collections/';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { parseDocUrls } from './DocumentationParser';
|
||||
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||
import { parseDocs } from './DocumentationParser';
|
||||
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||
import { parseScript } from './Script/ScriptParser';
|
||||
|
||||
@@ -12,35 +14,67 @@ let categoryIdCounter = 0;
|
||||
export function parseCategory(
|
||||
category: CategoryData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
factory: CategoryFactoryType = CategoryFactory,
|
||||
): Category {
|
||||
if (!context) { throw new Error('missing context'); }
|
||||
ensureValid(category);
|
||||
return parseCategoryRecursively({
|
||||
categoryData: category,
|
||||
context,
|
||||
factory,
|
||||
});
|
||||
}
|
||||
|
||||
interface ICategoryParseContext {
|
||||
readonly categoryData: CategoryData,
|
||||
readonly context: ICategoryCollectionParseContext,
|
||||
readonly factory: CategoryFactoryType,
|
||||
readonly parentCategory?: CategoryData,
|
||||
}
|
||||
// eslint-disable-next-line consistent-return
|
||||
function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
||||
ensureValidCategory(context.categoryData, context.parentCategory);
|
||||
const children: ICategoryChildren = {
|
||||
subCategories: new Array<Category>(),
|
||||
subScripts: new Array<Script>(),
|
||||
};
|
||||
for (const data of category.children) {
|
||||
parseCategoryChild(data, children, category, context);
|
||||
for (const data of context.categoryData.children) {
|
||||
parseNode({
|
||||
nodeData: data,
|
||||
children,
|
||||
parent: context.categoryData,
|
||||
factory: context.factory,
|
||||
context: context.context,
|
||||
});
|
||||
}
|
||||
try {
|
||||
return context.factory(
|
||||
/* id: */ categoryIdCounter++,
|
||||
/* name: */ context.categoryData.category,
|
||||
/* docs: */ parseDocs(context.categoryData),
|
||||
/* categories: */ children.subCategories,
|
||||
/* scripts: */ children.subScripts,
|
||||
);
|
||||
} catch (err) {
|
||||
new NodeValidator({
|
||||
type: NodeType.Category,
|
||||
selfNode: context.categoryData,
|
||||
parentNode: context.parentCategory,
|
||||
}).throw(err.message);
|
||||
}
|
||||
return new Category(
|
||||
/* id: */ categoryIdCounter++,
|
||||
/* name: */ category.category,
|
||||
/* docs: */ parseDocUrls(category),
|
||||
/* categories: */ children.subCategories,
|
||||
/* scripts: */ children.subScripts,
|
||||
);
|
||||
}
|
||||
|
||||
function ensureValid(category: CategoryData) {
|
||||
if (!category) {
|
||||
throw Error('missing category');
|
||||
}
|
||||
if (!category.children || category.children.length === 0) {
|
||||
throw Error(`category has no children: "${category.category}"`);
|
||||
}
|
||||
if (!category.category || category.category.length === 0) {
|
||||
throw Error('category has no name');
|
||||
}
|
||||
function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
|
||||
new NodeValidator({
|
||||
type: NodeType.Category,
|
||||
selfNode: category,
|
||||
parentNode: parentCategory,
|
||||
})
|
||||
.assertDefined(category)
|
||||
.assertValidName(category.category)
|
||||
.assert(
|
||||
() => category.children && category.children.length > 0,
|
||||
`"${category.category}" has no children.`,
|
||||
);
|
||||
}
|
||||
|
||||
interface ICategoryChildren {
|
||||
@@ -48,22 +82,29 @@ interface ICategoryChildren {
|
||||
subScripts: Script[];
|
||||
}
|
||||
|
||||
function parseCategoryChild(
|
||||
data: CategoryOrScriptData,
|
||||
children: ICategoryChildren,
|
||||
parent: CategoryData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
) {
|
||||
if (isCategory(data)) {
|
||||
const subCategory = parseCategory(data as CategoryData, context);
|
||||
children.subCategories.push(subCategory);
|
||||
} else if (isScript(data)) {
|
||||
const scriptData = data as ScriptData;
|
||||
const script = parseScript(scriptData, context);
|
||||
children.subScripts.push(script);
|
||||
interface INodeParseContext {
|
||||
readonly nodeData: CategoryOrScriptData;
|
||||
readonly children: ICategoryChildren;
|
||||
readonly parent: CategoryData;
|
||||
readonly factory: CategoryFactoryType;
|
||||
readonly context: ICategoryCollectionParseContext;
|
||||
}
|
||||
function parseNode(context: INodeParseContext) {
|
||||
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
|
||||
validator.assertDefined(context.nodeData);
|
||||
if (isCategory(context.nodeData)) {
|
||||
const subCategory = parseCategoryRecursively({
|
||||
categoryData: context.nodeData as CategoryData,
|
||||
context: context.context,
|
||||
factory: context.factory,
|
||||
parentCategory: context.parent,
|
||||
});
|
||||
context.children.subCategories.push(subCategory);
|
||||
} else if (isScript(context.nodeData)) {
|
||||
const script = parseScript(context.nodeData as ScriptData, context.context);
|
||||
context.children.subScripts.push(script);
|
||||
} else {
|
||||
throw new Error(`Child element is neither a category or a script.
|
||||
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
|
||||
validator.throw('Node is neither a category or a script.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,14 +114,22 @@ function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||
}
|
||||
|
||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||
const { category } = data as CategoryData;
|
||||
return category && category.length > 0;
|
||||
return hasProperty(data, 'category');
|
||||
}
|
||||
|
||||
function hasCode(holder: InstructionHolder): boolean {
|
||||
return holder.code && holder.code.length > 0;
|
||||
function hasCode(data: InstructionHolder): boolean {
|
||||
return hasProperty(data, 'code');
|
||||
}
|
||||
|
||||
function hasCall(holder: InstructionHolder) {
|
||||
return holder.call !== undefined;
|
||||
function hasCall(data: InstructionHolder) {
|
||||
return hasProperty(data, 'call');
|
||||
}
|
||||
|
||||
function hasProperty(object: unknown, propertyName: string) {
|
||||
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||
}
|
||||
|
||||
export type CategoryFactoryType = (
|
||||
...parameters: ConstructorParameters<typeof Category>) => Category;
|
||||
|
||||
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);
|
||||
|
||||
@@ -1,64 +1,58 @@
|
||||
import type { DocumentableData, DocumentationUrlsData } from '@/application/collections/';
|
||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||
|
||||
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||
if (!documentable) {
|
||||
throw new Error('missing documentable');
|
||||
}
|
||||
const { docs } = documentable;
|
||||
if (!docs || !docs.length) {
|
||||
if (!docs) {
|
||||
return [];
|
||||
}
|
||||
let result = new DocumentationUrlContainer();
|
||||
let result = new DocumentationContainer();
|
||||
result = addDocs(docs, result);
|
||||
return result.getAll();
|
||||
}
|
||||
|
||||
function addDocs(
|
||||
docs: DocumentationUrlsData,
|
||||
urls: DocumentationUrlContainer,
|
||||
): DocumentationUrlContainer {
|
||||
docs: DocumentationData,
|
||||
container: DocumentationContainer,
|
||||
): DocumentationContainer {
|
||||
if (docs instanceof Array) {
|
||||
urls.addUrls(docs);
|
||||
if (docs.length > 0) {
|
||||
container.addParts(docs);
|
||||
}
|
||||
} else if (typeof docs === 'string') {
|
||||
urls.addUrl(docs);
|
||||
container.addPart(docs);
|
||||
} else {
|
||||
throw new Error('Docs field (documentation url) must a string or array of strings');
|
||||
throwInvalidType();
|
||||
}
|
||||
return urls;
|
||||
return container;
|
||||
}
|
||||
|
||||
class DocumentationUrlContainer {
|
||||
private readonly urls = new Array<string>();
|
||||
class DocumentationContainer {
|
||||
private readonly parts = new Array<string>();
|
||||
|
||||
public addUrl(url: string) {
|
||||
validateUrl(url);
|
||||
this.urls.push(url);
|
||||
public addPart(documentation: string) {
|
||||
if (!documentation) {
|
||||
throw Error('missing documentation');
|
||||
}
|
||||
if (typeof documentation !== 'string') {
|
||||
throwInvalidType();
|
||||
}
|
||||
this.parts.push(documentation);
|
||||
}
|
||||
|
||||
public addUrls(urls: readonly string[]) {
|
||||
for (const url of urls) {
|
||||
if (typeof url !== 'string') {
|
||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
||||
}
|
||||
this.addUrl(url);
|
||||
public addParts(parts: readonly string[]) {
|
||||
for (const part of parts) {
|
||||
this.addPart(part);
|
||||
}
|
||||
}
|
||||
|
||||
public getAll(): ReadonlyArray<string> {
|
||||
return this.urls;
|
||||
return this.parts;
|
||||
}
|
||||
}
|
||||
|
||||
function validateUrl(docUrl: string): void {
|
||||
if (!docUrl) {
|
||||
throw new Error('Documentation url is null or empty');
|
||||
}
|
||||
if (docUrl.includes('\n')) {
|
||||
throw new Error('Documentation url cannot be multi-lined.');
|
||||
}
|
||||
const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
|
||||
const res = docUrl.match(validUrlRegex);
|
||||
if (res == null) {
|
||||
throw new Error(`Invalid documentation url: ${docUrl}`);
|
||||
}
|
||||
function throwInvalidType() {
|
||||
throw new Error('docs field (documentation) must be an array of strings');
|
||||
}
|
||||
|
||||
3
src/application/Parser/NodeValidation/NodeData.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { ScriptData, CategoryData } from '@/application/collections/';
|
||||
|
||||
export type NodeData = CategoryData | ScriptData;
|
||||
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';
|
||||
|
||||
export function parseProjectInformation(
|
||||
environment: NodeJS.ProcessEnv,
|
||||
environment: NodeJS.ProcessEnv | VueAppEnvironment,
|
||||
): IProjectInformation {
|
||||
const version = new Version(environment.VUE_APP_VERSION);
|
||||
const version = new Version(environment[VueAppEnvironmentKeys.VUE_APP_VERSION]);
|
||||
return new ProjectInformation(
|
||||
environment.VUE_APP_NAME,
|
||||
environment[VueAppEnvironmentKeys.VUE_APP_NAME],
|
||||
version,
|
||||
environment.VUE_APP_REPOSITORY_URL,
|
||||
environment.VUE_APP_HOMEPAGE_URL,
|
||||
environment[VueAppEnvironmentKeys.VUE_APP_SLOGAN],
|
||||
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 { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||
import { SyntaxFactory } from './Syntax/SyntaxFactory';
|
||||
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
||||
import { SyntaxFactory } from './Validation/Syntax/SyntaxFactory';
|
||||
import { ISyntaxFactory } from './Validation/Syntax/ISyntaxFactory';
|
||||
import { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
|
||||
|
||||
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||
public readonly compiler: IScriptCompiler;
|
||||
|
||||
@@ -13,4 +13,22 @@ export class ExpressionPosition {
|
||||
throw Error(`negative start position: ${start}`);
|
||||
}
|
||||
}
|
||||
|
||||
public isInInsideOf(potentialParent: ExpressionPosition): boolean {
|
||||
if (this.isSame(potentialParent)) {
|
||||
return false;
|
||||
}
|
||||
return potentialParent.start <= this.start
|
||||
&& potentialParent.end >= this.end;
|
||||
}
|
||||
|
||||
public isSame(other: ExpressionPosition): boolean {
|
||||
return other.start === this.start
|
||||
&& other.end === this.end;
|
||||
}
|
||||
|
||||
public isIntersecting(other: ExpressionPosition): boolean {
|
||||
return (other.start < this.end && other.end > this.start)
|
||||
|| (this.end > other.start && other.start >= this.start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,21 +20,59 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||
if (!code) {
|
||||
return code;
|
||||
}
|
||||
const expressions = this.extractor.findExpressions(code);
|
||||
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
||||
const context = new ExpressionEvaluationContext(args);
|
||||
const compiledCode = compileExpressions(expressions, code, context);
|
||||
const compiledCode = compileRecursively(code, context, this.extractor);
|
||||
return compiledCode;
|
||||
}
|
||||
}
|
||||
|
||||
function compileRecursively(
|
||||
code: string,
|
||||
context: IExpressionEvaluationContext,
|
||||
extractor: IExpressionParser,
|
||||
): string {
|
||||
/*
|
||||
Instead of compiling code at once and returning we compile expressions from the code.
|
||||
And recompile expressions from resulting code recursively.
|
||||
This allows using expressions inside expressions blocks. E.g.:
|
||||
```
|
||||
{{ with $condition }}
|
||||
echo '{{ $text }}'
|
||||
{{ end }}
|
||||
```
|
||||
Without recursing parameter substitution for '{{ $text }}' is skipped once the outer
|
||||
{{ with $condition }} is rendered.
|
||||
A more optimized alternative to recursion would be to a parse an expression tree
|
||||
instead of linear expression lists.
|
||||
*/
|
||||
if (!code) {
|
||||
return code;
|
||||
}
|
||||
const expressions = extractor.findExpressions(code);
|
||||
if (expressions.length === 0) {
|
||||
return code;
|
||||
}
|
||||
const compiledCode = compileExpressions(expressions, code, context);
|
||||
return compileRecursively(compiledCode, context, extractor);
|
||||
}
|
||||
|
||||
function compileExpressions(
|
||||
expressions: readonly IExpression[],
|
||||
code: string,
|
||||
context: IExpressionEvaluationContext,
|
||||
) {
|
||||
ensureValidExpressions(expressions, code, context);
|
||||
let compiledCode = '';
|
||||
const sortedExpressions = expressions
|
||||
const outerExpressions = expressions.filter(
|
||||
(expression) => expressions
|
||||
.filter((otherExpression) => otherExpression !== expression)
|
||||
.every((otherExpression) => !expression.position.isInInsideOf(otherExpression.position)),
|
||||
);
|
||||
/*
|
||||
This logic will only compile outer expressions if there were nested expressions.
|
||||
So the output of this compilation may result in new uncompiled expressions.
|
||||
*/
|
||||
const sortedExpressions = outerExpressions
|
||||
.slice() // copy the array to not mutate the parameter
|
||||
.sort((a, b) => b.position.start - a.position.start);
|
||||
let index = 0;
|
||||
@@ -65,6 +103,43 @@ function extractRequiredParameterNames(
|
||||
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('", "')}"`;
|
||||
}
|
||||
|
||||
function ensureValidExpressions(
|
||||
expressions: readonly IExpression[],
|
||||
code: string,
|
||||
context: IExpressionEvaluationContext,
|
||||
) {
|
||||
ensureParamsUsedInCodeHasArgsProvided(expressions, context.args);
|
||||
ensureExpressionsDoesNotExtendCodeLength(expressions, code);
|
||||
ensureNoExpressionsAtSamePosition(expressions);
|
||||
ensureNoInvalidIntersections(expressions);
|
||||
}
|
||||
|
||||
function ensureExpressionsDoesNotExtendCodeLength(
|
||||
expressions: readonly IExpression[],
|
||||
code: string,
|
||||
) {
|
||||
const expectedMax = code.length;
|
||||
const expressionsOutOfRange = expressions
|
||||
.filter((expression) => expression.position.end > expectedMax);
|
||||
if (expressionsOutOfRange.length > 0) {
|
||||
throw new Error(`Expressions out of range:\n${JSON.stringify(expressionsOutOfRange)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoExpressionsAtSamePosition(expressions: readonly IExpression[]) {
|
||||
const instructionsAtSamePosition = expressions.filter(
|
||||
(expression) => expressions
|
||||
.filter((other) => expression.position.isSame(other.position)).length > 1,
|
||||
);
|
||||
if (instructionsAtSamePosition.length > 0) {
|
||||
throw new Error(`Instructions at same position:\n${JSON.stringify(instructionsAtSamePosition)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureParamsUsedInCodeHasArgsProvided(
|
||||
expressions: readonly IExpression[],
|
||||
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
@@ -80,6 +155,16 @@ function ensureParamsUsedInCodeHasArgsProvided(
|
||||
}
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('", "')}"`;
|
||||
function ensureNoInvalidIntersections(expressions: readonly IExpression[]) {
|
||||
const intersectingInstructions = expressions.filter(
|
||||
(expression) => expressions
|
||||
.filter((other) => expression.position.isIntersecting(other.position))
|
||||
.filter((other) => !expression.position.isSame(other.position))
|
||||
.filter((other) => !expression.position.isInInsideOf(other.position))
|
||||
.filter((other) => !other.position.isInInsideOf(expression.position))
|
||||
.length > 0,
|
||||
);
|
||||
if (intersectingInstructions.length > 0) {
|
||||
throw new Error(`Instructions intersecting unexpectedly:\n${JSON.stringify(intersectingInstructions)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ export class ExpressionRegexBuilder {
|
||||
.addRawRegex('([^|\\s]+)');
|
||||
}
|
||||
|
||||
public matchAnythingExceptSurroundingWhitespaces() {
|
||||
public matchMultilineAnythingExceptSurroundingWhitespaces() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.addRawRegex('(.+?)')
|
||||
.addRawRegex('([\\S\\s]+?)')
|
||||
.expectZeroOrMoreWhitespaces();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export class EscapeDoubleQuotes implements IPipe {
|
||||
return raw;
|
||||
}
|
||||
return raw.replaceAll('"', '"^""');
|
||||
/* eslint-disable max-len */
|
||||
/* eslint-disable vue/max-len */
|
||||
/*
|
||||
"^"" is the most robust and stable choice.
|
||||
Other options:
|
||||
@@ -28,6 +28,6 @@ export class EscapeDoubleQuotes implements IPipe {
|
||||
Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"`
|
||||
A good explanation: https://stackoverflow.com/a/31413730
|
||||
*/
|
||||
/* eslint-enable max-len */
|
||||
/* eslint-enable vue/max-len */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export class WithParser extends RegexParser {
|
||||
.matchUntilFirstWhitespace() // First match: parameter name
|
||||
.expectExpressionEnd()
|
||||
// ...
|
||||
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||
// {{ end }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('end')
|
||||
|
||||
@@ -81,7 +81,7 @@ function compileCode(
|
||||
compiler: IExpressionsCompiler,
|
||||
): ICompiledFunctionCall {
|
||||
return {
|
||||
code: compiler.compileExpressions(code.do, args),
|
||||
code: compiler.compileExpressions(code.execute, args),
|
||||
revertCode: compiler.compileExpressions(code.revert, args),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ export enum FunctionBodyType {
|
||||
}
|
||||
|
||||
export interface IFunctionCode {
|
||||
readonly do: string;
|
||||
readonly execute: string;
|
||||
readonly revert?: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { FunctionData } from '@/application/collections/';
|
||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||
|
||||
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 {
|
||||
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
||||
} from './ISharedFunction';
|
||||
@@ -25,7 +26,7 @@ export function createFunctionWithInlineCode(
|
||||
throw new Error(`undefined code in function "${name}"`);
|
||||
}
|
||||
const content: IFunctionCode = {
|
||||
do: code,
|
||||
execute: code,
|
||||
revert: revertCode,
|
||||
};
|
||||
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
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 { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||
@@ -12,16 +17,20 @@ import { parseFunctionCalls } from './Call/FunctionCallParser';
|
||||
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
||||
|
||||
constructor(private readonly codeValidator: ICodeValidator = CodeValidator.instance) { }
|
||||
|
||||
public parseFunctions(
|
||||
functions: readonly FunctionData[],
|
||||
syntax: ILanguageSyntax,
|
||||
): ISharedFunctionCollection {
|
||||
if (!syntax) { throw new Error('missing syntax'); }
|
||||
const collection = new SharedFunctionCollection();
|
||||
if (!functions || !functions.length) {
|
||||
return collection;
|
||||
}
|
||||
ensureValidFunctions(functions);
|
||||
return functions
|
||||
.map((func) => parseFunction(func))
|
||||
.map((func) => parseFunction(func, syntax, this.codeValidator))
|
||||
.reduce((acc, func) => {
|
||||
acc.addFunction(func);
|
||||
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 parameters = parseParameters(data);
|
||||
if (hasCode(data)) {
|
||||
validateCode(data, syntax, validator);
|
||||
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||
}
|
||||
// Has call
|
||||
@@ -40,6 +54,19 @@ function parseFunction(data: FunctionData): ISharedFunction {
|
||||
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 {
|
||||
return (data.parameters || [])
|
||||
.map((parameter) => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { FunctionData, ScriptData } from '@/application/collections/';
|
||||
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 { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
|
||||
@@ -8,18 +12,20 @@ import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompi
|
||||
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
|
||||
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
||||
import { ICompiledCode } from './Function/Call/Compiler/ICompiledCode';
|
||||
|
||||
export class ScriptCompiler implements IScriptCompiler {
|
||||
private readonly functions: ISharedFunctionCollection;
|
||||
|
||||
constructor(
|
||||
functions: readonly FunctionData[] | undefined,
|
||||
private readonly syntax: ILanguageSyntax,
|
||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||
syntax: ILanguageSyntax,
|
||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||
) {
|
||||
if (!syntax) { throw new Error('missing syntax'); }
|
||||
this.functions = sharedFunctionsParser.parseFunctions(functions);
|
||||
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
||||
}
|
||||
|
||||
public canCompile(script: ScriptData): boolean {
|
||||
@@ -35,13 +41,19 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
try {
|
||||
const calls = parseFunctionCalls(script.call);
|
||||
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
||||
validateCompiledCode(compiledCode, this.codeValidator);
|
||||
return new ScriptCode(
|
||||
compiledCode.code,
|
||||
compiledCode.revertCode,
|
||||
this.syntax,
|
||||
);
|
||||
} catch (error) {
|
||||
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 { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
|
||||
|
||||
export interface ICategoryCollectionParseContext {
|
||||
readonly compiler: IScriptCompiler;
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
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 { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
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 { NodeType } from '../NodeValidation/NodeType';
|
||||
import { NodeValidator } from '../NodeValidation/NodeValidator';
|
||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||
import { CodeValidator } from './Validation/CodeValidator';
|
||||
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
export function parseScript(
|
||||
data: ScriptData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
levelParser = createEnumParser(RecommendationLevel),
|
||||
scriptFactory: ScriptFactoryType = ScriptFactory,
|
||||
codeValidator: ICodeValidator = CodeValidator.instance,
|
||||
): Script {
|
||||
validateScript(data);
|
||||
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
||||
validateScript(data, validator);
|
||||
if (!context) { throw new Error('missing context'); }
|
||||
const script = new Script(
|
||||
/* name: */ data.name,
|
||||
/* code: */ parseCode(data, context),
|
||||
/* docs: */ parseDocUrls(data),
|
||||
/* level: */ parseLevel(data.recommend, levelParser),
|
||||
);
|
||||
return script;
|
||||
try {
|
||||
const script = scriptFactory(
|
||||
/* name: */ data.name,
|
||||
/* code: */ parseCode(data, context, codeValidator),
|
||||
/* docs: */ parseDocs(data),
|
||||
/* level: */ parseLevel(data.recommend, levelParser),
|
||||
);
|
||||
return script;
|
||||
} catch (err) {
|
||||
validator.throw(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function parseLevel(
|
||||
@@ -33,28 +48,50 @@ function parseLevel(
|
||||
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)) {
|
||||
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) {
|
||||
if (script.code && script.call) {
|
||||
throw new Error('cannot define both "call" and "code"');
|
||||
}
|
||||
if (script.revertCode && script.call) {
|
||||
throw new Error('cannot define "revertCode" if "call" is defined');
|
||||
}
|
||||
function validateHardcodedCodeWithoutCalls(
|
||||
scriptCode: ScriptCode,
|
||||
codeValidator: ICodeValidator,
|
||||
syntax: ILanguageSyntax,
|
||||
) {
|
||||
[scriptCode.execute, scriptCode.revert].forEach(
|
||||
(code) => codeValidator.throwIfInvalid(
|
||||
code,
|
||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function validateScript(script: ScriptData) {
|
||||
if (!script) {
|
||||
throw new Error('missing script');
|
||||
}
|
||||
if (!script.code && !script.call) {
|
||||
throw new Error('must define either "call" or "code"');
|
||||
}
|
||||
ensureNotBothCallAndCode(script);
|
||||
function validateScript(script: ScriptData, validator: NodeValidator) {
|
||||
validator
|
||||
.assertDefined(script)
|
||||
.assertValidName(script.name)
|
||||
.assert(
|
||||
() => Boolean(script.code || script.call),
|
||||
'Must define either "call" or "code".',
|
||||
)
|
||||
.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 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 { ILanguageSyntax } from './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 { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { BatchFileSyntax } from './BatchFileSyntax';
|
||||
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||
import { ISyntaxFactory } from './ISyntaxFactory';
|
||||
@@ -12,10 +12,10 @@ declare module '@/application/collections/*' {
|
||||
}
|
||||
|
||||
export type CategoryOrScriptData = CategoryData | ScriptData;
|
||||
export type DocumentationUrlsData = ReadonlyArray<string> | string;
|
||||
export type DocumentationData = ReadonlyArray<string> | string;
|
||||
|
||||
export interface DocumentableData {
|
||||
readonly docs?: DocumentationUrlsData;
|
||||
readonly docs?: DocumentationData;
|
||||
}
|
||||
|
||||
export interface InstructionHolder {
|
||||
|
||||
3690
src/application/collections/linux.yaml
Normal file
@@ -509,6 +509,83 @@ actions:
|
||||
function: PersistUserEnvironmentConfiguration
|
||||
parameters:
|
||||
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
|
||||
children:
|
||||
@@ -638,6 +715,58 @@ actions:
|
||||
name: Disable Spotlight indexing
|
||||
code: sudo mdutil -i off -d /
|
||||
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
|
||||
children:
|
||||
|
||||
@@ -8,7 +8,7 @@ export class Category extends BaseEntity<number> implements ICategory {
|
||||
constructor(
|
||||
id: number,
|
||||
public readonly name: string,
|
||||
public readonly documentationUrls: ReadonlyArray<string>,
|
||||
public readonly docs: ReadonlyArray<string>,
|
||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
||||
public readonly scripts?: ReadonlyArray<IScript>,
|
||||
) {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface IDocumentable {
|
||||
readonly documentationUrls: ReadonlyArray<string>;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Version } from '@/domain/Version';
|
||||
export interface IProjectInformation {
|
||||
readonly name: string;
|
||||
readonly version: Version;
|
||||
|
||||
readonly slogan: string;
|
||||
readonly repositoryUrl: string;
|
||||
readonly homepage: string;
|
||||
readonly feedbackUrl: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IScriptCode } from './IScriptCode';
|
||||
export interface IScript extends IEntity<string>, IDocumentable {
|
||||
readonly name: string;
|
||||
readonly level?: RecommendationLevel;
|
||||
readonly documentationUrls: ReadonlyArray<string>;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
readonly code: IScriptCode;
|
||||
canRevert(): boolean;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export class ProjectInformation implements IProjectInformation {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly version: Version,
|
||||
public readonly slogan: string,
|
||||
public readonly repositoryUrl: string,
|
||||
public readonly homepage: string,
|
||||
) {
|
||||
@@ -18,6 +19,9 @@ export class ProjectInformation implements IProjectInformation {
|
||||
if (!version) {
|
||||
throw new Error('undefined version');
|
||||
}
|
||||
if (!slogan) {
|
||||
throw new Error('undefined slogan');
|
||||
}
|
||||
if (!repositoryUrl) {
|
||||
throw new Error('repositoryUrl is undefined');
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ export class Script extends BaseEntity<string> implements IScript {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly code: IScriptCode,
|
||||
public readonly documentationUrls: ReadonlyArray<string>,
|
||||
public readonly docs: ReadonlyArray<string>,
|
||||
public readonly level?: RecommendationLevel,
|
||||
) {
|
||||
super(name);
|
||||
if (!code) {
|
||||
throw new Error(`missing code (script: ${name})`);
|
||||
throw new Error('missing code');
|
||||
}
|
||||
validateLevel(level);
|
||||
}
|
||||
|
||||
@@ -4,25 +4,18 @@ export class ScriptCode implements IScriptCode {
|
||||
constructor(
|
||||
public readonly execute: string,
|
||||
public readonly revert: string,
|
||||
syntax: ILanguageSyntax,
|
||||
) {
|
||||
if (!syntax) { throw new Error('missing syntax'); }
|
||||
validateCode(execute, syntax);
|
||||
validateRevertCode(revert, execute, syntax);
|
||||
validateCode(execute);
|
||||
validateRevertCode(revert, execute);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ILanguageSyntax {
|
||||
readonly commentDelimiters: string[];
|
||||
readonly commonCodeParts: string[];
|
||||
}
|
||||
|
||||
function validateRevertCode(revertCode: string, execute: string, syntax: ILanguageSyntax) {
|
||||
function validateRevertCode(revertCode: string, execute: string) {
|
||||
if (!revertCode) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validateCode(revertCode, syntax);
|
||||
validateCode(revertCode);
|
||||
if (execute === revertCode) {
|
||||
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) {
|
||||
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 {
|
||||
switch (environment.os) {
|
||||
case OperatingSystem.Linux:
|
||||
return `x-terminal-emulator -e '${scriptPath}'`;
|
||||
case OperatingSystem.macOS:
|
||||
return `open -a Terminal.app ${scriptPath}`;
|
||||
// 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/fonts" as *;
|
||||
|
||||
@use "@/presentation/assets/styles/mixins" as *;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
$globals-color-hover: $color-primary;
|
||||
a {
|
||||
color:inherit;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
@include hover-or-touch {
|
||||
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;
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
@forward "./media";
|
||||
@forward "./colors";
|
||||
@forward "./globals";
|
||||
@forward "./mixins";
|
||||
|
||||
@forward "./components/card";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Overrides base styling for LiquorTree
|
||||
@use "@/presentation/assets/styles/colors" as *;
|
||||
@use "@/presentation/assets/styles/mixins" as *;
|
||||
|
||||
$color-tree-bg : $color-primary-darker;
|
||||
$color-node-arrow : $color-on-primary;
|
||||
@@ -18,14 +19,17 @@ $color-node-checkbox-tick-checked : $color-on-secondary;
|
||||
&-node {
|
||||
white-space: normal !important;
|
||||
> .tree-content {
|
||||
> .tree-anchor > span {
|
||||
color: $color-node-fg;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.5em;
|
||||
> .tree-anchor {
|
||||
> span {
|
||||
color: $color-node-fg;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
display: block; // so it takes full width to allow aligning items inside
|
||||
}
|
||||
&:hover {
|
||||
@include hover-or-touch {
|
||||
background: $color-node-hover-bg !important;
|
||||
}
|
||||
background: $color-tree-bg !important; // If not styled, it gets white background on mobile.
|
||||
}
|
||||
&.selected { // When using keyboard navigation it highlights current item and its child items
|
||||
background: $color-node-keyboard-bg;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div class="app__wrapper">
|
||||
<TheHeader class="app__row" />
|
||||
<TheSearchBar class="app__row" />
|
||||
<TheScriptArea class="app__row" />
|
||||
<TheCodeButtons class="app__row app__code-buttons" />
|
||||
<TheFooter />
|
||||
</div>
|
||||
</div>
|
||||
<div class="app__wrapper">
|
||||
<TheHeader class="app__row" />
|
||||
<TheSearchBar class="app__row" />
|
||||
<TheScriptArea class="app__row" />
|
||||
<TheCodeButtons class="app__row app__code-buttons" />
|
||||
<TheFooter />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<button class="button" @click="onClicked">
|
||||
<button
|
||||
class="button"
|
||||
type="button"
|
||||
@click="onClicked"
|
||||
>
|
||||
<font-awesome-icon
|
||||
class="button__icon"
|
||||
:icon="[iconPrefix, iconName]" size="2x" />
|
||||
:icon="[iconPrefix, iconName]"
|
||||
size="2x"
|
||||
/>
|
||||
<div class="button__text">{{text}}</div>
|
||||
</button>
|
||||
</template>
|
||||
@@ -42,17 +48,18 @@ export default class IconButton extends Vue {
|
||||
box-shadow: 0 3px 9px $color-primary-darkest;
|
||||
border-radius: 4px;
|
||||
|
||||
cursor: pointer;
|
||||
@include clickable;
|
||||
|
||||
width: 10%;
|
||||
min-width: 90px;
|
||||
&:hover {
|
||||
@include hover-or-touch {
|
||||
background: $color-surface;
|
||||
box-shadow: 0px 2px 10px 5px $color-secondary;
|
||||
}
|
||||
&:hover>&__text {
|
||||
@include hover-or-touch('>&__text') {
|
||||
display: block;
|
||||
}
|
||||
&:hover>&__icon {
|
||||
@include hover-or-touch('>&__icon') {
|
||||
display: none;
|
||||
}
|
||||
&__text {
|
||||
@@ -62,6 +69,9 @@ export default class IconButton extends Vue {
|
||||
color: $color-primary;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
@include hover-or-touch {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<span class="code-wrapper">
|
||||
<span class="dollar">$</span>
|
||||
<code><slot></slot></code>
|
||||
<code><slot /></code>
|
||||
<font-awesome-icon
|
||||
class="copy-button"
|
||||
:icon="['fas', 'copy']"
|
||||
@@ -28,13 +28,14 @@ export default class Code extends Vue {
|
||||
@use "@/presentation/assets/styles/main" as *;
|
||||
|
||||
.code-wrapper {
|
||||
display:flex;
|
||||
white-space: nowrap;
|
||||
justify-content: space-between;
|
||||
font-family: $font-normal;
|
||||
background-color: $color-primary-darker;
|
||||
color: $color-on-primary;
|
||||
padding-left: 0.3rem;
|
||||
padding-right: 0.3rem;
|
||||
align-items: center;
|
||||
padding: 0.2rem;
|
||||
.dollar {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
@@ -42,13 +43,13 @@ export default class Code extends Vue {
|
||||
}
|
||||
.copy-button {
|
||||
margin-left: 1rem;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
@include clickable;
|
||||
@include hover-or-touch {
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
code {
|
||||
font-size: 1.2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IInstructionListData, IInstructionListStep } from '../InstructionListData';
|
||||
|
||||
export interface IInstructionsBuilderData {
|
||||
readonly fileName: string;
|
||||
}
|
||||
|
||||
export type InstructionStepBuilderType = (data: IInstructionsBuilderData) => IInstructionListStep;
|
||||
|
||||
export class InstructionsBuilder {
|
||||
private readonly stepBuilders = new Array<InstructionStepBuilderType>();
|
||||
|
||||
constructor(private readonly os: OperatingSystem) {
|
||||
|
||||
}
|
||||
|
||||
public withStep(stepBuilder: InstructionStepBuilderType) {
|
||||
if (!stepBuilder) { throw new Error('missing stepBuilder'); }
|
||||
this.stepBuilders.push(stepBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(data: IInstructionsBuilderData): IInstructionListData {
|
||||
if (!data) { throw new Error('missing data'); }
|
||||
return {
|
||||
operatingSystem: this.os,
|
||||
steps: this.stepBuilders.map((stepBuilder) => stepBuilder(data)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { InstructionsBuilder } from './InstructionsBuilder';
|
||||
|
||||
export class LinuxInstructionsBuilder extends InstructionsBuilder {
|
||||
constructor() {
|
||||
super(OperatingSystem.Linux);
|
||||
super
|
||||
.withStep(() => ({
|
||||
action: {
|
||||
instruction: 'Download the file.',
|
||||
details: 'You should have already been prompted to save the script file.'
|
||||
+ '<br/>If this was not the case or you did not save the script when prompted,'
|
||||
+ '<br/>please try to download your script file again.',
|
||||
},
|
||||
}))
|
||||
.withStep(() => ({
|
||||
action: {
|
||||
instruction: 'Open terminal.',
|
||||
details:
|
||||
'Opening terminal changes based on the distro you run.'
|
||||
+ '<br/>You may search for "Terminal" in your application launcher to find it.'
|
||||
+ '<br/>'
|
||||
+ '<br/>Alternatively use terminal shortcut for your distro if it has one by default:'
|
||||
+ '<ul>'
|
||||
+ '<li><code>Ctrl-Alt-T</code>: Ubuntu, CentOS, Linux Mint, Elementary OS, ubermix, Kali…</li>'
|
||||
+ '<li><code>Super-T</code>: Pop!_OS…</li>'
|
||||
+ '<li><code>Alt-T</code>: Parrot OS…</li>'
|
||||
+ '<li><code>Ctrl-Alt-Insert</code>: Bodhi Linux…</li>'
|
||||
+ '</ul>'
|
||||
+ '<br/>'
|
||||
,
|
||||
},
|
||||
}))
|
||||
.withStep(() => ({
|
||||
action: {
|
||||
instruction: 'Navigate to the folder where you downloaded the file e.g.:',
|
||||
},
|
||||
code: {
|
||||
instruction: 'cd ~/Downloads',
|
||||
details: 'Press on <code>enter/return</code> key after running the command.'
|
||||
+ '<br/>If the file is not downloaded on Downloads folder,'
|
||||
+ '<br/>change <code>Downloads</code> to path where the file is downloaded.'
|
||||
+ '<br/>'
|
||||
+ '<br/>This command means:'
|
||||
+ '<ul>'
|
||||
+ '<li><code>cd</code> will change the current folder.</li>'
|
||||
+ '<li><code>~</code> is the user home directory.</li>'
|
||||
+ '</ul>',
|
||||
},
|
||||
}))
|
||||
.withStep((data) => ({
|
||||
action: {
|
||||
instruction: 'Give the file execute permissions:',
|
||||
},
|
||||
code: {
|
||||
instruction: `chmod +x ${data.fileName}`,
|
||||
details: 'Press on <code>enter/return</code> key after running the command.<br/>'
|
||||
+ 'It will make the file executable. <br/>'
|
||||
+ 'If you use desktop environment you can alternatively (instead of running the command):'
|
||||
+ '<ol>'
|
||||
+ '<li>Locate the file using your file manager.</li>'
|
||||
+ '<li>Right click on the file, select "Properties".</li>'
|
||||
+ '<li>Go to "Permissions" and check "Allow executing file as program".</li>'
|
||||
+ '</ol>'
|
||||
+ '<br/>These GUI steps and name of options may change depending on your file manager.'
|
||||
,
|
||||
},
|
||||
}))
|
||||
.withStep((data) => ({
|
||||
action: {
|
||||
instruction: 'Execute the file:',
|
||||
},
|
||||
code: {
|
||||
instruction: `./${data.fileName}`,
|
||||
details:
|
||||
'If you have desktop environment, instead of running this command you can alternatively:'
|
||||
+ '<ol>'
|
||||
+ '<li>Locate the file using your file manager.</li>'
|
||||
+ '<li>Right click on the file, select "Run as program".</li>'
|
||||
+ '</ol>'
|
||||
,
|
||||
},
|
||||
}))
|
||||
.withStep(() => ({
|
||||
action: {
|
||||
instruction: 'If asked, enter your administrator password.',
|
||||
details: 'As you type, your password will be hidden but the keys are still registered, so keep typing.'
|
||||
+ '<br/>Press on <code>enter/return</code> key after typing your password.'
|
||||
+ '<br/>Administrator privileges are required to configure OS.',
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { InstructionsBuilder } from './InstructionsBuilder';
|
||||
|
||||
export class MacOsInstructionsBuilder extends InstructionsBuilder {
|
||||
constructor() {
|
||||
super(OperatingSystem.macOS);
|
||||
super
|
||||
.withStep(() => ({
|
||||
action: {
|
||||
instruction: 'Download the file.',
|
||||
details: 'You should have already been prompted to save the script file.'
|
||||
+ '<br/>If this was not the case or you did not save the script when prompted,'
|
||||
+ '<br/>please try to download your script file again.'
|
||||
,
|
||||
},
|
||||
}))
|
||||
.withStep(() => ({
|
||||
action: {
|
||||
instruction: 'Open terminal.',
|
||||
details: 'Type Terminal into Spotlight or open it from the Applications -> Utilities folder.',
|
||||
},
|
||||
}))
|
||||
.withStep(() => ({
|
||||
action: {
|
||||
instruction: 'Navigate to the folder where you downloaded the file e.g.:',
|
||||
},
|
||||
code: {
|
||||
instruction: 'cd ~/Downloads',
|
||||
details: 'Press on <code>enter/return</code> key after running the command.'
|
||||
+ '<br/>If the file is not downloaded on Downloads folder,'
|
||||
+ '<br/>change <code>Downloads</code> to path where the file is downloaded.'
|
||||
+ '<br/>'
|
||||
+ '<br/>This command means:'
|
||||
+ '<ul>'
|
||||
+ '<li><code>cd</code> will change the current folder.</li>'
|
||||
+ '<li><code>~</code> is the user home directory.</li>'
|
||||
+ '</ul>',
|
||||
},
|
||||
}))
|
||||
.withStep((data) => ({
|
||||
action: {
|
||||
instruction: 'Give the file execute permissions:',
|
||||
},
|
||||
code: {
|
||||
instruction: `chmod +x ${data.fileName}`,
|
||||
details: 'Press on <code>enter/return</code> key after running the command.<br/>'
|
||||
+ 'It will make the file executable.'
|
||||
,
|
||||
},
|
||||
}))
|
||||
.withStep((data) => ({
|
||||
action: {
|
||||
instruction: 'Execute the file:',
|
||||
},
|
||||
code: {
|
||||
instruction: `./${data.fileName}`,
|
||||
details: 'Alternatively you can locate the file in <strong>Finder</strong> and double click on it.',
|
||||
},
|
||||
}))
|
||||
.withStep(() => ({
|
||||
action: {
|
||||
instruction: 'If asked, enter your administrator password.',
|
||||
details: 'As you type, your password will be hidden but the keys are still registered, so keep typing.'
|
||||
+ '<br/>Press on <code>enter/return</code> key after typing your password.'
|
||||
+ '<br/>Administrator privileges are required to configure OS.'
|
||||
,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="instructions">
|
||||
<p>
|
||||
You have two alternatives to apply your selection.
|
||||
</p>
|
||||
<hr />
|
||||
<p>
|
||||
<strong>1. The easy alternative</strong>. Run your script without any manual steps by
|
||||
<a :href="this.macOsDownloadUrl">downloading desktop version</a> of {{ this.appName }} on the
|
||||
{{ this.osName }} system you wish to configure, and then click on the Run button. This is
|
||||
recommended for most users.
|
||||
</p>
|
||||
<hr />
|
||||
<p>
|
||||
<strong>2. The hard (manual) alternative</strong>. This requires you to do additional manual
|
||||
steps. If you are unsure how to follow the instructions, hover on information
|
||||
(<font-awesome-icon :icon="['fas', 'info-circle']" />)
|
||||
icons near the steps, or follow the easy alternative described above.
|
||||
</p>
|
||||
<p>
|
||||
<ol>
|
||||
<li
|
||||
v-for='(step, index) in this.data.steps'
|
||||
v-bind:key="index"
|
||||
class="step"
|
||||
>
|
||||
<div class="step__action">
|
||||
<span>{{ step.action.instruction }}</span>
|
||||
<font-awesome-icon
|
||||
v-if="step.action.details"
|
||||
class="explanation"
|
||||
:icon="['fas', 'info-circle']"
|
||||
v-tooltip.top-center="step.action.details"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="step.code" class="step__code">
|
||||
<Code>{{ step.code.instruction }}</Code>
|
||||
<font-awesome-icon
|
||||
v-if="step.code.details"
|
||||
class="explanation"
|
||||
:icon="['fas', 'info-circle']"
|
||||
v-tooltip.top-center="step.code.details"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||
import Code from './Code.vue';
|
||||
import { IInstructionListData } from './InstructionListData';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
Code,
|
||||
},
|
||||
})
|
||||
export default class InstructionList extends Vue {
|
||||
public appName = '';
|
||||
|
||||
public macOsDownloadUrl = '';
|
||||
|
||||
public osName = '';
|
||||
|
||||
@Prop() public data: IInstructionListData;
|
||||
|
||||
public async created() {
|
||||
if (!this.data) {
|
||||
throw new Error('missing data');
|
||||
}
|
||||
const app = await ApplicationFactory.Current.getApp();
|
||||
this.appName = app.info.name;
|
||||
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
|
||||
this.osName = renderOsName(this.data.operatingSystem);
|
||||
}
|
||||
}
|
||||
|
||||
function renderOsName(os: OperatingSystem): string {
|
||||
switch (os) {
|
||||
case OperatingSystem.Windows: return 'Windows';
|
||||
case OperatingSystem.macOS: return 'macOS';
|
||||
case OperatingSystem.Linux: return 'Linux';
|
||||
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "@/presentation/assets/styles/main" as *;
|
||||
|
||||
.step {
|
||||
margin: 10px 0;
|
||||
&__action {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
&__code {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
.explanation {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IInstructionListData {
|
||||
readonly operatingSystem: OperatingSystem;
|
||||
readonly steps: readonly IInstructionListStep[];
|
||||
}
|
||||
|
||||
export interface IInstructionListStep {
|
||||
readonly action: IInstructionInfo;
|
||||
readonly code?: IInstructionInfo;
|
||||
}
|
||||
|
||||
export interface IInstructionInfo {
|
||||
readonly instruction: string;
|
||||
readonly details?: string;
|
||||
}
|
||||