Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d11a674a3c | ||
|
|
31f70913a2 | ||
|
|
bd23faa28f | ||
|
|
5b1fbe1e2f | ||
|
|
96265b75de | ||
|
|
17298f0b2c | ||
|
|
61b475fa8d | ||
|
|
455084c17b | ||
|
|
c3c5b897f3 | ||
|
|
a1871a2982 | ||
|
|
87de017afd | ||
|
|
5a2c263af3 | ||
|
|
ddd2e704db | ||
|
|
9b5e0b0591 | ||
|
|
9b6636e21a | ||
|
|
a8358b8e7a | ||
|
|
5f091bb6ab | ||
|
|
17b334aaad | ||
|
|
c65209e6a9 | ||
|
|
d2518b11a7 | ||
|
|
70cdf3865a | ||
|
|
7c02ffb6c9 | ||
|
|
f2d9881382 | ||
|
|
d7761ab30e | ||
|
|
bf83c58982 | ||
|
|
2e082932c9 | ||
|
|
2f90cac52a | ||
|
|
20a0071c0d | ||
|
|
a40f83d6b6 | ||
|
|
0db8cc4206 | ||
|
|
97ddc027cb | ||
|
|
82c43ba2e3 | ||
|
|
799fb091b8 | ||
|
|
5ead1a087d | ||
|
|
64631a4552 | ||
|
|
f47cb04860 | ||
|
|
504fa056d7 | ||
|
|
739287ac71 | ||
|
|
ab8bce7686 | ||
|
|
e6152fa76f | ||
|
|
a8031d18d5 | ||
|
|
9aa8166891 | ||
|
|
236a0f6c82 | ||
|
|
2492f2d814 | ||
|
|
410bcd8244 | ||
|
|
b08a6b5cec | ||
|
|
37ad26a082 | ||
|
|
0696ed8396 | ||
|
|
9942df16c8 | ||
|
|
20b7d283b0 | ||
|
|
f39ee76c0c | ||
|
|
4b2390736a | ||
|
|
c8cb7a5c28 | ||
|
|
5217b0b758 | ||
|
|
ddf417a16a | ||
|
|
2f0321f315 | ||
|
|
4d7ff7edc5 | ||
|
|
862914b06e | ||
|
|
6c3c2e6709 | ||
|
|
c92dc1e253 | ||
|
|
e73c0ad1bf | ||
|
|
6a89c6224b | ||
|
|
dcccb61781 | ||
|
|
c0c475ff56 | ||
|
|
6dc768817f | ||
|
|
439cd303ff | ||
|
|
ec0c972d34 | ||
|
|
2a08855e5d | ||
|
|
1c6b3057ea | ||
|
|
ea5f9ec27d | ||
|
|
f2935e4008 | ||
|
|
487001af48 | ||
|
|
71e70e50c5 | ||
|
|
0a857aa09e | ||
|
|
b976b92031 | ||
|
|
db62ed7f3a | ||
|
|
36f0805590 | ||
|
|
49600c5f37 | ||
|
|
eb9ac35a92 | ||
|
|
77148980e0 | ||
|
|
b3d2e82025 | ||
|
|
b25b8cc805 | ||
|
|
8141a01ef7 | ||
|
|
a2f10857e2 | ||
|
|
aea04e5f7c | ||
|
|
60c80611ea | ||
|
|
b1ed3ce55f | ||
|
|
040ed2701c | ||
|
|
00d8e551db | ||
|
|
3e9c99f5f8 | ||
|
|
02bdc4cf04 | ||
|
|
5c43965f0b | ||
|
|
b2376ecc30 | ||
|
|
aeaa6deeb4 |
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[*.{js,jsx,ts,tsx,vue}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 100
|
||||||
115
.eslintrc.js
Normal file
115
.eslintrc.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
// Vue specific rules, eslint-plugin-vue
|
||||||
|
// Added by Vue CLI
|
||||||
|
'plugin:vue/essential',
|
||||||
|
|
||||||
|
// Extends eslint-config-airbnb
|
||||||
|
// Added by Vue CLI
|
||||||
|
// Here until https://github.com/vuejs/eslint-config-airbnb/issues/23 is done
|
||||||
|
'@vue/airbnb',
|
||||||
|
|
||||||
|
// Extends @typescript-eslint/recommended
|
||||||
|
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||||
|
// Added by Vue CLI
|
||||||
|
'@vue/typescript/recommended',
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...getOwnRules(),
|
||||||
|
...getTurnedOffBrokenRules(),
|
||||||
|
...getOpinionatedRuleOverrides(),
|
||||||
|
...getTodoRules(),
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'**/__tests__/*.{j,t}s?(x)',
|
||||||
|
'**/tests/unit/**/*.spec.{j,t}s?(x)',
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
mocha: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/tests/**/*.{j,t}s?(x)'],
|
||||||
|
rules: {
|
||||||
|
'no-console': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getOwnRules() {
|
||||||
|
return {
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'linebreak-style': ['error', 'unix'], // This is also enforced in .editorconfig and .gitattributes files
|
||||||
|
'import/order': [ // Enforce strict import order taking account into aliases
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
groups: [ // Enforce more strict order than AirBnb
|
||||||
|
'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||||
|
pathGroups: [ // Fix manually configured paths being incorrectly grouped as "external"
|
||||||
|
'@/**', // @/..
|
||||||
|
'@tests/**', // @tests/.. (not matching anything after @** because there can be third parties as well)
|
||||||
|
'js-yaml-loader!@/**', // E.g. js-yaml-loader!@/..
|
||||||
|
].map((pattern) => ({ pattern, group: 'internal' })),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTodoRules() { // Should be worked on separate future commits
|
||||||
|
return {
|
||||||
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
// Requires webpack configuration change with import '..yaml' files.
|
||||||
|
'import/no-webpack-loader-syntax': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
// Accessibility improvements:
|
||||||
|
'vuejs-accessibility/form-control-has-label': 'off',
|
||||||
|
'vuejs-accessibility/click-events-have-key-events': 'off',
|
||||||
|
'vuejs-accessibility/anchor-has-content': 'off',
|
||||||
|
'vuejs-accessibility/accessible-emoji': 'off',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTurnedOffBrokenRules() {
|
||||||
|
return {
|
||||||
|
// Broken in TypeScript
|
||||||
|
'no-useless-constructor': 'off', // Cannot interpret TypeScript constructors
|
||||||
|
'no-shadow': 'off', // Fails with TypeScript enums
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpinionatedRuleOverrides() {
|
||||||
|
return {
|
||||||
|
// https://erkinekici.com/articles/linting-trap#no-use-before-define
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
// https://erkinekici.com/articles/linting-trap#arrow-body-style
|
||||||
|
'arrow-body-style': 'off',
|
||||||
|
// https://erkinekici.com/articles/linting-trap#no-plusplus
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
// https://erkinekici.com/articles/linting-trap#no-param-reassign
|
||||||
|
'no-param-reassign': 'off',
|
||||||
|
// https://erkinekici.com/articles/linting-trap#class-methods-use-this
|
||||||
|
'class-methods-use-this': 'off',
|
||||||
|
// https://erkinekici.com/articles/linting-trap#importprefer-default-export
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
// https://erkinekici.com/articles/linting-trap#disallowing-for-of
|
||||||
|
// Original: https://github.com/airbnb/javascript/blob/d8cb404da74c302506f91e5928f30cc75109e74d/packages/eslint-config-airbnb-base/rules/style.js#L333-L351
|
||||||
|
'no-restricted-syntax': [
|
||||||
|
baseStyleRules['no-restricted-syntax'][0],
|
||||||
|
...baseStyleRules['no-restricted-syntax'].slice(1).filter((rule) => rule.selector !== 'ForOfStatement'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Prevent Git from auto-converting to CRLF on Windows, and convert to LF on checkin.
|
||||||
|
# * : All files
|
||||||
|
# text=auto : If Git decides content it text, it converts CRLF to LF on checkin.
|
||||||
|
# eol=lf : forces Git to normalize line endings to LF on checkin and prevents conversion
|
||||||
|
# to CRLF when the file is checked out.
|
||||||
|
* text=auto eol=lf
|
||||||
32
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
32
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
@@ -11,9 +11,11 @@ Please fill in as much of the template below as you're able.
|
|||||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Describe the bug
|
### Description
|
||||||
|
|
||||||
<!-- A clear and concise description of what the bug is. -->
|
<!--
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
-->
|
||||||
|
|
||||||
### OS
|
### OS
|
||||||
|
|
||||||
@@ -23,14 +25,32 @@ 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 macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Screenshots
|
### Reproduction steps
|
||||||
|
|
||||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
<!--
|
||||||
|
How can the bug be recreated?
|
||||||
|
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
||||||
|
E.g.
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
-->
|
||||||
|
|
||||||
### Scripts
|
### Scripts
|
||||||
|
|
||||||
<!-- Which scripts did you execute? If applicable, please paste the executed scripts or attach the generated privacy.sexy file . -->
|
<!--
|
||||||
|
If applicable, please attach the generated privacy.sexy file instead of copy pasting which becomes too long.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
-->
|
||||||
|
|
||||||
### Additional information
|
### Additional information
|
||||||
|
|
||||||
<!-- Add any other context about the problem here. -->
|
<!--
|
||||||
|
If applicable, add any other context about the problem here.
|
||||||
|
-->
|
||||||
|
|||||||
19
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
19
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
@@ -11,13 +11,16 @@ Please fill in as much of the template below as you're able.
|
|||||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Describe the bug
|
### Description
|
||||||
|
|
||||||
<!-- A clear and concise description of what the bug is. -->
|
|
||||||
|
|
||||||
### To Reproduce
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Reproduction steps
|
||||||
|
|
||||||
|
<!--
|
||||||
|
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
@@ -41,12 +44,12 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
If applicable, mention how you were using privacy.sexy when the bug was encountered:
|
If applicable, mention how you were using privacy.sexy when the bug was encountered:
|
||||||
- Web (on Desktop or mobile?)
|
- Web (on Desktop or mobile?)
|
||||||
- Or desktop (Windows, macOS or Linux?)
|
- Or desktop (Windows, macOS or Linux?)
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Additional context
|
### Additional context
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Add any other context about the problem here.
|
If applicable, add any other context about the problem here.
|
||||||
-->
|
-->
|
||||||
|
|||||||
36
.github/ISSUE_TEMPLATE/3-feature-request.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/3-feature-request.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for privacy.sexy
|
||||||
|
labels: enhancement
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Thank you for suggesting an idea to improve privacy better 🤗.
|
||||||
|
Please fill in as much of the template below as you're able.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Problem description
|
||||||
|
|
||||||
|
<!--
|
||||||
|
What are we trying to solve?
|
||||||
|
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||||
|
E.g. I'm always frustrated when [...]
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Proposed solution
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe the solution you'd like in a clear and concise manner.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
|
||||||
|
<!--
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Additional information
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, add any other context or screenshots about the feature request here.
|
||||||
|
-->
|
||||||
27
.github/ISSUE_TEMPLATE/3-feature_request.md
vendored
27
.github/ISSUE_TEMPLATE/3-feature_request.md
vendored
@@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for privacy.sexy
|
|
||||||
labels: enhancement
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for suggesting an idea to make privacy better. 🤗
|
|
||||||
|
|
||||||
Please fill in as much of the template below as you're able.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Problem Description
|
|
||||||
|
|
||||||
<!-- Please add a clear and concise description of the problem you are seeking to solve with this feature request. Ex. I'm always frustrated when [...] -->
|
|
||||||
|
|
||||||
### Proposed solution
|
|
||||||
|
|
||||||
<!-- Describe the solution you'd like in a clear and concise manner. -->
|
|
||||||
|
|
||||||
### Alternatives considered
|
|
||||||
|
|
||||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
|
||||||
|
|
||||||
### Additional information
|
|
||||||
|
|
||||||
<!-- Add any other context or screenshots about the feature request here. -->
|
|
||||||
73
.github/ISSUE_TEMPLATE/4-new-script-suggestion.md
vendored
Normal file
73
.github/ISSUE_TEMPLATE/4-new-script-suggestion.md
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
name: New script suggestion
|
||||||
|
about: Suggest a new script for privacy.sexy
|
||||||
|
labels: enhancement
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Thank you for suggesting an script to make privacy better. 🤗
|
||||||
|
Please fill in as much of the template below as you're able.
|
||||||
|
You could alternatively send a PR directly (see CONTRIBUTING.md).
|
||||||
|
-->
|
||||||
|
|
||||||
|
### OS
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Which OS will the new script configure?
|
||||||
|
Either "Windows" or "macOS".
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Name
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The name of the script.
|
||||||
|
It should start with an imperative noun such as "disable", "turn off" , "clear"...
|
||||||
|
E.g. "Disable webcam telemetry"
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Script code
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Code that will be executed when script is selected.
|
||||||
|
Try to keep it as simple and backwards-compatible as possible.
|
||||||
|
Allowed languages:
|
||||||
|
- macOS: bash (sh)
|
||||||
|
- Windows: PowerShell (ps1) or batchfile
|
||||||
|
- 💡 Prioritize the one that's simpler, batchfile if similar.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Revert code
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, add code that will revert the script code to its original (OS default) state.
|
||||||
|
It may require additional time, but it's much appreciated by the community.
|
||||||
|
Leave blank if the script is nonreversible (e.g. when clearing data without backup).
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Suggested category
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, suggest one more multiple suitable parent category of script.
|
||||||
|
A category is the item where the script will be presented under.
|
||||||
|
Most likely there already is a category for the script, so check the existing categories.
|
||||||
|
If you're unsure, leave blank and maintainer(s) will choose one.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Suggested recommendation level
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, suggest recommending the script or not recommending at all.
|
||||||
|
A script should be only recommended if it'll be safe for your grandmother to run.
|
||||||
|
So you have three options here:
|
||||||
|
STANDARD: Non-breaking scripts that does not limit any functionality.
|
||||||
|
STRICT: Scripts that can break certain functionality but not intrusive to common daily OS usage.
|
||||||
|
NONE: Script is not recommended for newbies at all, only those who knows what's going on should select it.
|
||||||
|
If you're unsure, leave blank and maintainer(s) will choose one.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Additional documentation/references
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, refer to documentation that should show up on the script description.
|
||||||
|
Sources (URLs) should be as high quality as possible e.g. vendor documentation is favored over user forums.
|
||||||
|
-->
|
||||||
59
.github/workflows/checks.build.yaml
vendored
Normal file
59
.github/workflows/checks.build.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: build-checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-web:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
mode: [ development, test, production ]
|
||||||
|
fail-fast: false # Allows to see results from other combinations
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 15.x
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Build
|
||||||
|
run: npm run build -- --mode ${{ matrix.mode }}
|
||||||
|
|
||||||
|
# A new job is used due to environments/modes different from Vue CLI, https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
|
||||||
|
build-desktop:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
mode: [ development, production ] # "test" is not supported https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1627
|
||||||
|
fail-fast: false # Allows to see results from other combinations
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 15.x
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Install cross-env
|
||||||
|
# Used to set NODE_ENV due to https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
|
||||||
|
run: npm install --global cross-env
|
||||||
|
-
|
||||||
|
name: Build
|
||||||
|
run: |-
|
||||||
|
cross-env-shell NODE_ENV=${{ matrix.mode }}
|
||||||
|
npm run electron:build -- --publish never --mode ${{ matrix.mode }}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Quality checks
|
name: quality-checks
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
@@ -8,18 +8,20 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
lint-command:
|
lint-command:
|
||||||
- npm run lint:vue
|
- npm run lint:eslint
|
||||||
- npm run lint:yaml
|
- npm run lint:yaml
|
||||||
- npm run lint:md
|
- npm run lint:md
|
||||||
- npm run lint:md:relative-urls
|
- npm run lint:md:relative-urls
|
||||||
- npm run lint:md:consistency
|
- npm run lint:md:consistency
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 15.x
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Lint
|
- name: Lint
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
name: Security checks
|
name: security-checks
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths: [ '/package.json', '/package-lock.json' ] # Allow PRs to be green if they do not introduce dependency change
|
paths: [ '/package.json', '/package-lock.json' ] # Allow PRs to be green if they do not introduce dependency change
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * 0'
|
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
npm-audit:
|
npm-audit:
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 15.x
|
||||||
-
|
-
|
||||||
name: NPM audit
|
name: NPM audit
|
||||||
run: npm audit
|
run: exit "$(npm audit)" # Since node 15.x, it does not fail with error if we don't explicitly exit
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy desktop
|
name: release-desktop
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@@ -22,10 +22,10 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '14.x'
|
node-version: 15.x
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Run tests
|
- name: Run unit tests
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
- name: Publish desktop app
|
- name: Publish desktop app
|
||||||
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Bump & release
|
name: release-git
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push: # Ensure a new release is created for each new tag
|
push: # Ensure a new release is created for each new tag
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy site
|
name: release-site
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@@ -83,13 +83,13 @@ jobs:
|
|||||||
name: "App: Setup node"
|
name: "App: Setup node"
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '14.x'
|
node-version: 15.x
|
||||||
-
|
-
|
||||||
name: "App: Install dependencies"
|
name: "App: Install dependencies"
|
||||||
run: npm ci
|
run: npm ci
|
||||||
working-directory: site
|
working-directory: site
|
||||||
-
|
-
|
||||||
name: "App: Run tests"
|
name: "App: Run unit tests"
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
working-directory: site
|
working-directory: site
|
||||||
-
|
-
|
||||||
28
.github/workflows/tests.e2e.yaml
vendored
Normal file
28
.github/workflows/tests.e2e.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: e2e-tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos, ubuntu, windows]
|
||||||
|
fail-fast: false # So it still runs on other OSes if one of them fails
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 15.x
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Run e2e tests
|
||||||
|
run: npm run test:e2e -- --headless
|
||||||
30
.github/workflows/tests.integration.yaml
vendored
Normal file
30
.github/workflows/tests.integration.yaml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: integration-tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule: # To get notified about problems from third party dependencies
|
||||||
|
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos, ubuntu, windows]
|
||||||
|
fail-fast: false # So it still runs on other OSes if one of them fails
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 15.x
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Run integration tests
|
||||||
|
run: npm run test:integration
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
name: Test
|
name: unit-tests
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-tests:
|
run-tests:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos, ubuntu, windows]
|
os: [macos, ubuntu, windows]
|
||||||
|
fail-fast: false # So it still runs on other OSes if one of them fails
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
@@ -16,10 +19,10 @@ jobs:
|
|||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '14.x'
|
node-version: 15.x
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
-
|
-
|
||||||
name: Run tests
|
name: Run unit tests
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,3 +4,6 @@ dist/
|
|||||||
.vscode
|
.vscode
|
||||||
#Electron-builder output
|
#Electron-builder output
|
||||||
/dist_electron
|
/dist_electron
|
||||||
|
# Cypress
|
||||||
|
/tests/e2e/screenshots
|
||||||
|
/tests/e2e/videos
|
||||||
|
|||||||
109
CHANGELOG.md
109
CHANGELOG.md
@@ -1,5 +1,114 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.11.2 (2021-12-03)
|
||||||
|
|
||||||
|
* Fix Windows TrustedInstaller session errors | [20a0071](https://github.com/undergroundwires/privacy.sexy/commit/20a0071c0d3d769a8f31218abdbfc4cafa25c6ff)
|
||||||
|
* Improve tests for `UserSelection` | [2f90cac](https://github.com/undergroundwires/privacy.sexy/commit/2f90cac52ab9e57615aeec41f9daa842bce770a5)
|
||||||
|
* Fix disabling/enabling Defender on Windows #104 | [2e08293](https://github.com/undergroundwires/privacy.sexy/commit/2e082932c952b0849ab2b8709ff0c75293b88e95)
|
||||||
|
* Refactor Saas naming, structure and modules | [bf83c58](https://github.com/undergroundwires/privacy.sexy/commit/bf83c58982ffa178facc6d35e50c7f1eac7ff236)
|
||||||
|
* Fix Defender features errors in Windows #104 | [d7761ab](https://github.com/undergroundwires/privacy.sexy/commit/d7761ab30e7f1e10a2919c196804d67511d6163a)
|
||||||
|
* Fix unintendedly inlined Windows scripts | [f2d9881](https://github.com/undergroundwires/privacy.sexy/commit/f2d988138257ff184884e4adc83c39e3bc247e9b)
|
||||||
|
* Fix Defender error due to non-english Windows #104 | [7c02ffb](https://github.com/undergroundwires/privacy.sexy/commit/7c02ffb6c95382b94f0b05e6f259cc418ec91c93)
|
||||||
|
* Improve and unify disabling of Windows services | [70cdf38](https://github.com/undergroundwires/privacy.sexy/commit/70cdf3865a0de3214fc9e26fbdada4b0cb413c46)
|
||||||
|
* Improve Windows defender docs and errors #104 | [d2518b1](https://github.com/undergroundwires/privacy.sexy/commit/d2518b11a7774ec58b9b46a691e2f013855bf0f9)
|
||||||
|
* Unrecommend and complete Windows Push Notif. #101 | [c65209e](https://github.com/undergroundwires/privacy.sexy/commit/c65209e6a99230f15ace8955e8d5a6f3333d146b)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.1...0.11.2)
|
||||||
|
|
||||||
|
## 0.11.1 (2021-11-04)
|
||||||
|
|
||||||
|
* Update dependencies | [64631a4](https://github.com/undergroundwires/privacy.sexy/commit/64631a4552fad7f7b06286aba8d3ca2d731f9342)
|
||||||
|
* Fix, document, unrecommend Windows browser cleanup | [5ead1a0](https://github.com/undergroundwires/privacy.sexy/commit/5ead1a087d91948890bc4ae6fea176123f18c285)
|
||||||
|
* Fix failing URL status checking integration tests | [799fb09](https://github.com/undergroundwires/privacy.sexy/commit/799fb091b8eb06c70ac0c67f2ef5385dce73501f)
|
||||||
|
* Refactor to remove "Async" function name suffix | [82c43ba](https://github.com/undergroundwires/privacy.sexy/commit/82c43ba2e37fb6e7f62ccd9bec8c5f48575f0613)
|
||||||
|
* Fix dead URLs and use forks as GitHub references | [97ddc02](https://github.com/undergroundwires/privacy.sexy/commit/97ddc027cb5395a74991cabc1d8c875ee945636d)
|
||||||
|
* Fix website not loading on Safari | [0db8cc4](https://github.com/undergroundwires/privacy.sexy/commit/0db8cc420655e01cbbed57c4658489b761a15899)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.0...0.11.1)
|
||||||
|
|
||||||
|
## 0.11.0 (2021-10-21)
|
||||||
|
|
||||||
|
* Change "grouping" to "view" | [c0c475f](https://github.com/undergroundwires/privacy.sexy/commit/c0c475ff564b23a4dabcc03ac2909207a8eb61ce)
|
||||||
|
* Tighten parameter substitution tolerance | [dcccb61](https://github.com/undergroundwires/privacy.sexy/commit/dcccb617813625c224a28242c5b965bb4cd6f189)
|
||||||
|
* Add optionality for parameters | [6a89c62](https://github.com/undergroundwires/privacy.sexy/commit/6a89c6224bdef5eb96980471f3b3935b9351b197)
|
||||||
|
* Do not collapse cards on links and code area #88 | [e73c0ad](https://github.com/undergroundwires/privacy.sexy/commit/e73c0ad1bf922b1dd3360fc5aafc3434951fa63c)
|
||||||
|
* Add scripts to disable, hide and opt-out from Siri | [c92dc1e](https://github.com/undergroundwires/privacy.sexy/commit/c92dc1e25387c65a3a41ca64d2a23cf8131b4c86)
|
||||||
|
* Improve macOS scripts for cleaning OS logs | [6c3c2e6](https://github.com/undergroundwires/privacy.sexy/commit/6c3c2e6709ec84f8e0411f19c024bab2c7e5753b)
|
||||||
|
* Add "with" expression for templating #53 | [862914b](https://github.com/undergroundwires/privacy.sexy/commit/862914b06ea9ef74c4b58a9a4164a10a38273638)
|
||||||
|
* Add support for pipes in templates #53 | [4d7ff7e](https://github.com/undergroundwires/privacy.sexy/commit/4d7ff7edc5a96cc0d99d3c1ca4fdf9bbdace3fd2)
|
||||||
|
* Bump node environment to 15.x | [2f0321f](https://github.com/undergroundwires/privacy.sexy/commit/2f0321f315ac0da8c713dd50e37032f1de194942)
|
||||||
|
* Add new UX for optionally downloading updates | [ddf417a](https://github.com/undergroundwires/privacy.sexy/commit/ddf417a16a79551b43576befab0541ea08487969)
|
||||||
|
* Add pipes to write pretty PowerShell #53 | [5217b0b](https://github.com/undergroundwires/privacy.sexy/commit/5217b0b7587ccfe509ba8adc3a7748b9bae14d7a)
|
||||||
|
* Improve alignment, padding/margin issues on UI | [c8cb7a5](https://github.com/undergroundwires/privacy.sexy/commit/c8cb7a5c28420557319606da82f56b011e88f470)
|
||||||
|
* Support disabling per-user services in Windows #16 | [4b23907](https://github.com/undergroundwires/privacy.sexy/commit/4b2390736ac1f9de2d5176b7b07da0e827112f9a)
|
||||||
|
* Add script to remove Meet Now icon in Windows | [f39ee76](https://github.com/undergroundwires/privacy.sexy/commit/f39ee76c0cda95f54502b19d5c49390fd0f12b5e)
|
||||||
|
* Add support for more depth in function calls | [20b7d28](https://github.com/undergroundwires/privacy.sexy/commit/20b7d283b02dd751dfbde18ef1fe334c6bf76e2b)
|
||||||
|
* Increase default screen width on desktop app | [9942df1](https://github.com/undergroundwires/privacy.sexy/commit/9942df16c8334ff041fb92f432a3a29e351c88df)
|
||||||
|
* Improve disabling of SmartScreen #74 | [0696ed8](https://github.com/undergroundwires/privacy.sexy/commit/0696ed8396e298a358bec17adb91c9145dd90418)
|
||||||
|
* Remove integration tests from deployments #90 | [37ad26a](https://github.com/undergroundwires/privacy.sexy/commit/37ad26a082851c02497c36e7fce40555b9480e11)
|
||||||
|
* Use a consistent color system | [b08a6b5](https://github.com/undergroundwires/privacy.sexy/commit/b08a6b5cecf4a53023053695292146edbd24b960)
|
||||||
|
* Add semi-automatic update support for macOS | [410bcd8](https://github.com/undergroundwires/privacy.sexy/commit/410bcd82445097c29c9fcf0eabf7af9ebcb93c1e)
|
||||||
|
* Add more ways to disable and clean Defender #74 | [2492f2d](https://github.com/undergroundwires/privacy.sexy/commit/2492f2d8141b3abdf590ccad59680b1f50ecb59e)
|
||||||
|
* Add privacy over security scripts for macOS #83 | [236a0f6](https://github.com/undergroundwires/privacy.sexy/commit/236a0f6c8241294fc397194cd1b20bdeccbbb50b)
|
||||||
|
* Change PowerShell double quotes escape | [9aa8166](https://github.com/undergroundwires/privacy.sexy/commit/9aa816689146ee6cd86d8262112677c38651c6bd)
|
||||||
|
* Change theme colors | [a8031d1](https://github.com/undergroundwires/privacy.sexy/commit/a8031d18d520dd3b0567f7b8cfe2dcd694b65073)
|
||||||
|
* Improve security hardening for macOS | [e6152fa](https://github.com/undergroundwires/privacy.sexy/commit/e6152fa76f5e7d23b0f79d5dd98713daaecbff90)
|
||||||
|
* Support disabling of protected services #74 | [ab8bce7](https://github.com/undergroundwires/privacy.sexy/commit/ab8bce768650a10677f0a13b3a9fae93c83802ff)
|
||||||
|
* Fix minor issues with Defender scripts | [739287a](https://github.com/undergroundwires/privacy.sexy/commit/739287ac71b3f8b04348fc101f1fa06f2d7d86a2)
|
||||||
|
* Update screenshot | [504fa05](https://github.com/undergroundwires/privacy.sexy/commit/504fa056d7d8b17fc20afd398f9a557495fca7e8)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.3...0.11.0)
|
||||||
|
|
||||||
|
## 0.10.3 (2021-08-27)
|
||||||
|
|
||||||
|
* unrecommend VSS and document its breaking behavior | [7714898](https://github.com/undergroundwires/privacy.sexy/commit/77148980e08859f89c15c6604e55b56ce4f74358)
|
||||||
|
* fix incorrect modification of Desktop folder on ThisPC (#71) | [eb9ac35](https://github.com/undergroundwires/privacy.sexy/commit/eb9ac35a923325cc2c9983ef71c0d904337a58f5)
|
||||||
|
* add initial integration tests | [49600c5](https://github.com/undergroundwires/privacy.sexy/commit/49600c5f37ca33c1687885fdf02a71ef7d3e6e8c)
|
||||||
|
* unify usage of sleepAsync and add tests | [36f0805](https://github.com/undergroundwires/privacy.sexy/commit/36f08055909f371fd9cbe3480ea813b963aea22b)
|
||||||
|
* fix broken URLs and automate broken URL checks #70 | [db62ed7](https://github.com/undergroundwires/privacy.sexy/commit/db62ed7f3ac63e9f2d762eb946060595eb9f5626)
|
||||||
|
* fix hiding recent files in quick access | [b976b92](https://github.com/undergroundwires/privacy.sexy/commit/b976b920318dba55b32d39f148fdca4f6be3cce3)
|
||||||
|
* bump dependencies to latest #75, #69 | [0a857aa](https://github.com/undergroundwires/privacy.sexy/commit/0a857aa09ee703d34ad0422bd1731158017a9a58)
|
||||||
|
* Fix NTP configuration before running the service (#72) | [71e70e5](https://github.com/undergroundwires/privacy.sexy/commit/71e70e50c51249bb10f6203414948b325acc2b2a)
|
||||||
|
* Fix typo on main page (#82) | [487001a](https://github.com/undergroundwires/privacy.sexy/commit/487001af485fdbb958615d7b52c09c2e386ddaf2)
|
||||||
|
* Improve issue templates | [f2935e4](https://github.com/undergroundwires/privacy.sexy/commit/f2935e4008f1231ef174f8932290e11715564d20)
|
||||||
|
* Fix infinitely subscribing to state changes | [ea5f9ec](https://github.com/undergroundwires/privacy.sexy/commit/ea5f9ec27df7cec6ac575e23fef18948d2b8e68a)
|
||||||
|
* Fix select options being clickable when disabled | [1c6b305](https://github.com/undergroundwires/privacy.sexy/commit/1c6b3057ea6e45125cadf374f20a905712ccdf3c)
|
||||||
|
* Fix tests for `ParameterSubstitutionParser` | [2a08855](https://github.com/undergroundwires/privacy.sexy/commit/2a08855e5d1bdf74354fd692cbfebd1a48e495ac)
|
||||||
|
* Fix excessive highlighting on hover | [ec0c972](https://github.com/undergroundwires/privacy.sexy/commit/ec0c972d348ffd5897f115d201031b704875b56a)
|
||||||
|
* Fix dead URLs | [439cd30](https://github.com/undergroundwires/privacy.sexy/commit/439cd303ff3db96a53664e5f44fefe12b95c5e6c)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.2...0.10.3)
|
||||||
|
|
||||||
|
## 0.10.2 (2021-04-19)
|
||||||
|
|
||||||
|
* in CI/CD, run other tests/check even if one of them fails | [5c43965](https://github.com/undergroundwires/privacy.sexy/commit/5c43965f0bc44f991ada7d3bad68937a80665dc3)
|
||||||
|
* fix desktop initial window size being bigger than current display size on smaller Linux/Windows screens | [02bdc4c](https://github.com/undergroundwires/privacy.sexy/commit/02bdc4cf0426c452f3fc9af52b819ca9b0757290)
|
||||||
|
* refactor extra code, duplicates, complexity | [00d8e55](https://github.com/undergroundwires/privacy.sexy/commit/00d8e551db001247fadfb6f6af7a4c5ce19a9e64)
|
||||||
|
* improve disabling ads and marketing #65 | [040ed27](https://github.com/undergroundwires/privacy.sexy/commit/040ed2701c4a468749901f4c5369b221bc0973c4)
|
||||||
|
* document breaking behavior in script name #64 | [b1ed3ce](https://github.com/undergroundwires/privacy.sexy/commit/b1ed3ce55f2d003cad1ead23e674aa66d4eb5802)
|
||||||
|
* add module alias '@tests/' | [60c8061](https://github.com/undergroundwires/privacy.sexy/commit/60c80611eab227791fabb883caf93418cef5fd00)
|
||||||
|
* document chromium warning for policy changes | [aea04e5](https://github.com/undergroundwires/privacy.sexy/commit/aea04e5f7cd48fbb9b407b68ade75575a6064c82)
|
||||||
|
* fix script revert activating recommendation level | [a2f1085](https://github.com/undergroundwires/privacy.sexy/commit/a2f10857e2a8debb3ce01f79b0dfbe8649ea9a17)
|
||||||
|
* fix typo and dead URL in Windows scripts (#70) | [8141a01](https://github.com/undergroundwires/privacy.sexy/commit/8141a01ef798331b4d82f5ca95f7b18df4f6f912)
|
||||||
|
* fix vue warning for undefined property during render | [b25b8cc](https://github.com/undergroundwires/privacy.sexy/commit/b25b8cc8052655af70b0695c6c3085974d783bb6)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.1...0.10.2)
|
||||||
|
|
||||||
|
## 0.10.1 (2021-03-25)
|
||||||
|
|
||||||
|
* refactor script compilation to make it easy to add new expressions #41 #53 | [646db90](https://github.com/undergroundwires/privacy.sexy/commit/646db9058541cebd0af437554de04fdc6bb63a6e)
|
||||||
|
* restructure presentation layer | [f3c7413](https://github.com/undergroundwires/privacy.sexy/commit/f3c7413f529be4a00dba7b0ab23904b48ea13a35)
|
||||||
|
* fix a test where "it" is not used inside "describe" | [1a5f920](https://github.com/undergroundwires/privacy.sexy/commit/1a5f92021f7423cd039f8f5326cd6f99b355c962)
|
||||||
|
* bump dependencies to latest | [1f515e7](https://github.com/undergroundwires/privacy.sexy/commit/1f515e7be525291c960ccb71db05312db6da53f5)
|
||||||
|
* fix throttle function not being able to run with argument(s) | [1935db1](https://github.com/undergroundwires/privacy.sexy/commit/1935db10192051401ab00ca2cd767955d0d3b866)
|
||||||
|
* fix fs module hanging not allowing code to run | [5f527a0](https://github.com/undergroundwires/privacy.sexy/commit/5f527a00cf225d3e74b3f6577d6e2456e919de24)
|
||||||
|
* refactor all modals to use same dialog component | [6f46cdb](https://github.com/undergroundwires/privacy.sexy/commit/6f46cdb4ed49a8941c6c0dde5c5e2a816c06daef)
|
||||||
|
* fix safari cleanup scripts that are not working on modern versions | [05932c5](https://github.com/undergroundwires/privacy.sexy/commit/05932c5a36446d551c5bc811165e3295fbe15e3f)
|
||||||
|
* refactor features to use shared functions #41 | [ac2249f](https://github.com/undergroundwires/privacy.sexy/commit/ac2249f25664827d8a6d2c7ebd659ccf126b0cde)
|
||||||
|
* increase performance by polyfilling ResizeObserver only if required | [448e378](https://github.com/undergroundwires/privacy.sexy/commit/448e378dc4501f9de69af63634c87d0e5060bf52)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.0...0.10.1)
|
||||||
|
|
||||||
## 0.10.0 (2021-03-02)
|
## 0.10.0 (2021-03-02)
|
||||||
|
|
||||||
* allow functions to call other functions #53 | [7661575](https://github.com/undergroundwires/privacy.sexy/commit/7661575573c6d3e8f4bc28bfa7a124a764c72ef9)
|
* allow functions to call other functions #53 | [7661575](https://github.com/undergroundwires/privacy.sexy/commit/7661575573c6d3e8f4bc28bfa7a124a764c72ef9)
|
||||||
|
|||||||
123
README.md
123
README.md
@@ -2,21 +2,104 @@
|
|||||||
|
|
||||||
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
||||||
|
|
||||||
[](./CONTRIBUTING.md)
|
<!-- markdownlint-disable MD033 -->
|
||||||
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
<p align="center">
|
||||||
[](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
|
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md">
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
<img
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
alt="contributions are welcome"
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
/>
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
</a>
|
||||||
[](https://github.com/undergroundwires/bump-everywhere)
|
<!-- Code quality -->
|
||||||
|
<br />
|
||||||
|
<a href="https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript">
|
||||||
|
<img
|
||||||
|
alt="Language grade: JavaScript/TypeScript"
|
||||||
|
src="https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability">
|
||||||
|
<img
|
||||||
|
alt="Maintainability"
|
||||||
|
src="https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Tests -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.unit.yaml">
|
||||||
|
<img
|
||||||
|
alt="Unit tests status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/unit-tests/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.integration.yaml">
|
||||||
|
<img
|
||||||
|
alt="Integration tests status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/integration-tests/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.e2e.yaml">
|
||||||
|
<img
|
||||||
|
alt="E2E tests status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/e2e-tests/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Checks -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml">
|
||||||
|
<img
|
||||||
|
alt="Quality checks status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.yaml">
|
||||||
|
<img
|
||||||
|
alt="Security checks status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/security-checks/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml">
|
||||||
|
<img
|
||||||
|
alt="Build checks status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Release -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml">
|
||||||
|
<img
|
||||||
|
alt="Git release status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-git/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.site.yaml">
|
||||||
|
<img
|
||||||
|
alt="Site release status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-site/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.desktop.yaml">
|
||||||
|
<img
|
||||||
|
alt="Desktop application release status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-desktop/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Others -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/bump-everywhere">
|
||||||
|
<img
|
||||||
|
alt="Auto-versioned by bump-everywhere"
|
||||||
|
src="https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
||||||
- 💡 No need to run any compiled software on your computer.
|
- 💡 No need to run any compiled software on your computer.
|
||||||
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.0/privacy.sexy-Setup-0.10.0.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.0/privacy.sexy-0.10.0.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.0/privacy.sexy-0.10.0.AppImage).
|
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-Setup-0.11.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.AppImage).
|
||||||
- 💡 Single click to execute your script.
|
- 💡 Single click to execute your script.
|
||||||
- ❗ Come back regularly to apply latest version for stronger privacy and security.
|
- ❗ Come back regularly to apply latest version for stronger privacy and security.
|
||||||
|
|
||||||
@@ -30,21 +113,28 @@
|
|||||||
- Have full visibility into what the tweaks do as you enable them
|
- Have full visibility into what the tweaks do as you enable them
|
||||||
- Ability to revert (undo) applied scripts
|
- Ability to revert (undo) applied scripts
|
||||||
- Everything is transparent: both application and its infrastructure are open-source and automated
|
- Everything is transparent: both application and its infrastructure are open-source and automated
|
||||||
- Easily extendable
|
- Easily extendable with [own powerful templating language](./docs/templating.md)
|
||||||
|
- Each script is independently executable without cross-dependencies
|
||||||
|
|
||||||
## Extend scripts
|
## Extend scripts
|
||||||
|
|
||||||
1. Fork the repository
|
- You can either [create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose)
|
||||||
2. Add more scripts in respective script collection in [collections](src/application/collections/) folder.
|
- Or send a PR:
|
||||||
|
1. Fork the repository
|
||||||
|
2. Add more scripts in respective script collection in [collections](src/application/collections/) folder.
|
||||||
- 📖 If you're unsure about the syntax you can refer to the [collection files | documentation](docs/collection-files.md).
|
- 📖 If you're unsure about the syntax you can refer to the [collection files | documentation](docs/collection-files.md).
|
||||||
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||||
3. Send a pull request 👌
|
3. Send a pull request 👌
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- Project setup: `npm install`
|
- Project setup: `npm install`
|
||||||
- Testing
|
- Testing
|
||||||
- Run unit tests: `npm run test:unit`
|
- Run unit tests: `npm run test:unit`
|
||||||
|
- Run integration tests: `npm run test:integration`
|
||||||
|
- Run e2e (end-to-end) tests
|
||||||
|
- Interactive mode with GUI: `npm run test:e2e`
|
||||||
|
- Headless mode without GUI: `npm run test:e2e -- --headless`
|
||||||
- Lint: `npm run lint`
|
- Lint: `npm run lint`
|
||||||
- **Desktop app**
|
- **Desktop app**
|
||||||
- Development: `npm run electron:serve`
|
- Development: `npm run electron:serve`
|
||||||
@@ -53,8 +143,8 @@
|
|||||||
- Development: `npm run serve` to compile & hot-reload for development.
|
- Development: `npm run serve` to compile & hot-reload for development.
|
||||||
- Production: `npm run build` to prepare files for distribution.
|
- Production: `npm run build` to prepare files for distribution.
|
||||||
- Or run using Docker:
|
- Or run using Docker:
|
||||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.10.0 .`
|
1. Build: `docker build -t undergroundwires/privacy.sexy:0.11.2 .`
|
||||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.10.0 undergroundwires/privacy.sexy:0.10.0`
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.2 undergroundwires/privacy.sexy:0.11.2`
|
||||||
|
|
||||||
## Architecture overview
|
## Architecture overview
|
||||||
|
|
||||||
@@ -80,5 +170,6 @@
|
|||||||
- CI/CD is fully automated for this repo using different GIT events & GitHub actions.
|
- CI/CD is fully automated for this repo using different GIT events & GitHub actions.
|
||||||
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
|
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
|
||||||
- Everything that's merged in the master goes directly to production.
|
- Everything that's merged in the master goes directly to production.
|
||||||
|
- 📖 Read more on [CI/CD pipelines](./docs/ci-cd.md)
|
||||||
|
|
||||||
[](.github/workflows/)
|
[](.github/workflows/)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
'@vue/cli-plugin-babel/preset'
|
'@vue/cli-plugin-babel/preset',
|
||||||
]
|
],
|
||||||
}
|
};
|
||||||
|
|||||||
3
cypress.json
Normal file
3
cypress.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"pluginsFile": "tests/e2e/plugins/index.js"
|
||||||
|
}
|
||||||
@@ -3,6 +3,14 @@
|
|||||||
- It's mainly responsible for
|
- It's mainly responsible for
|
||||||
- creating and event based [application state](#application-state)
|
- creating and event based [application state](#application-state)
|
||||||
- [parsing](#parsing) and [compiling](#compiling) [application data](#application-data)
|
- [parsing](#parsing) and [compiling](#compiling) [application data](#application-data)
|
||||||
|
- Consumed by [presentation layer](./presentation.md)
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- [`/src/` **`application/`**](./../src/application/): Contains all application related code.
|
||||||
|
- [**`collections/`**](./../src/application/collections/): Holds [collection files](./collection-files.md)
|
||||||
|
- [**`Common/`**](./../src/application/Common/): Contains common functionality that is shared in application layer.
|
||||||
|
- `..`: other classes are categorized using folders-by-feature structure
|
||||||
|
|
||||||
## Application state
|
## Application state
|
||||||
|
|
||||||
|
|||||||
19
docs/ci-cd.md
Normal file
19
docs/ci-cd.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Pipelines
|
||||||
|
|
||||||
|
Pipelines are found under [`.github/workflows`](./../.github/workflows).
|
||||||
|
|
||||||
|
## Pipeline types
|
||||||
|
|
||||||
|
They are categorized based on their type:
|
||||||
|
|
||||||
|
- `tests`: Different types of tests to verify functionality.
|
||||||
|
- `checks`: Other controls such as vulnerability scans or styling checks.
|
||||||
|
- `release`: Pipelines used for release of deployment such as building and testing.
|
||||||
|
|
||||||
|
## Naming conventions
|
||||||
|
|
||||||
|
Pipeline files are named using: **`<type>.<name>.yaml`**.
|
||||||
|
|
||||||
|
**`type`**: Sub-folders do not work for GitHub workflows so that's why `<type>.` prefix is used. See also [pipeline types](#pipeline-types).
|
||||||
|
|
||||||
|
**`name`**: Pipeline themselves are named using kebab case. It allows for easier URL references for their status badges. E.g. file name `tests.unit.yaml`, pipeline name: `name: unit-tests`
|
||||||
@@ -45,9 +45,11 @@
|
|||||||
### `Script`
|
### `Script`
|
||||||
|
|
||||||
- Script represents a single tweak.
|
- Script represents a single tweak.
|
||||||
- A script must include either:
|
- A script can be of two different types (just like [functions](#function)):
|
||||||
- A `code` and `revertCode`
|
1. Inline script; a script with an inline code
|
||||||
- Or `call` to call YAML-defined functions
|
- Must define `code` property and optionally `revertCode` but not `call`
|
||||||
|
2. Caller script; a script that calls other functions
|
||||||
|
- Must define `call` property but not `code` or `revertCode`
|
||||||
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||||
|
|
||||||
#### `Script` syntax
|
#### `Script` syntax
|
||||||
@@ -80,7 +82,7 @@
|
|||||||
### `FunctionCall`
|
### `FunctionCall`
|
||||||
|
|
||||||
- Describes a single call to a function by optionally providing values to its parameters.
|
- Describes a single call to a function by optionally providing values to its parameters.
|
||||||
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
- 👀 See [parameter substitution](./templating.md#parameter-substitution) for an example usage
|
||||||
|
|
||||||
#### `FunctionCall` syntax
|
#### `FunctionCall` syntax
|
||||||
|
|
||||||
@@ -98,52 +100,18 @@
|
|||||||
appName: Microsoft.WindowsFeedbackHub
|
appName: Microsoft.WindowsFeedbackHub
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- 💡 [Expressions (templating)](./templating.md#expressions) can be used as parameter value
|
||||||
|
|
||||||
### `Function`
|
### `Function`
|
||||||
|
|
||||||
- Functions allow re-usable code throughout the defined scripts.
|
- Functions allow re-usable code throughout the defined scripts.
|
||||||
- Functions are templates compiled by privacy.sexy and uses special [expressions](#expressions).
|
- Functions are templates compiled by privacy.sexy and uses special expression expressions.
|
||||||
- Functions can call other functions by defining `call` property instead of `code`
|
- A function can be of two different types (just like [scripts](#script)):
|
||||||
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
1. Inline function: a function with an inline code.
|
||||||
|
- Must define `code` property and optionally `revertCode` but not `call`.
|
||||||
#### Expressions
|
2. Caller function: a function that calls other functions.
|
||||||
|
- Must define `call` property but not `code` or `revertCode`.
|
||||||
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
- 👀 Read more on [Templating](./templating.md) for function expressions and [example usages](./templating.md#parameter-substitution).
|
||||||
|
|
||||||
##### Parameter substitution
|
|
||||||
|
|
||||||
A simple function example
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
function: EchoArgument
|
|
||||||
parameters: [ 'argument' ]
|
|
||||||
code: Hello {{ $argument }} !
|
|
||||||
```
|
|
||||||
|
|
||||||
It would print "Hello world" if it's called in a [script](#script) as following:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
script: Echo script
|
|
||||||
call:
|
|
||||||
function: EchoArgument
|
|
||||||
parameters:
|
|
||||||
argument: World
|
|
||||||
```
|
|
||||||
|
|
||||||
A function can call other functions such as:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
-
|
|
||||||
function: CallerFunction
|
|
||||||
parameters: [ 'value' ]
|
|
||||||
call:
|
|
||||||
function: EchoArgument
|
|
||||||
parameters:
|
|
||||||
argument: {{ $value }}
|
|
||||||
-
|
|
||||||
function: EchoArgument
|
|
||||||
parameters: [ 'argument' ]
|
|
||||||
code: Hello {{ $argument }} !
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `Function` syntax
|
#### `Function` syntax
|
||||||
|
|
||||||
@@ -152,24 +120,43 @@ A function can call other functions such as:
|
|||||||
- Convention is to use camelCase, and be verbs.
|
- Convention is to use camelCase, and be verbs.
|
||||||
- E.g. `uninstallStoreApp`
|
- E.g. `uninstallStoreApp`
|
||||||
- ❗ Function names must be unique
|
- ❗ Function names must be unique
|
||||||
- `parameters`: `[` *`string`* `, ... ]`
|
- `parameters`: `[` ***[`FunctionParameter`](#FunctionParameter)*** `, ... ]`
|
||||||
- Name of the parameters that the function has.
|
- List of parameters that function code refers to.
|
||||||
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#FunctionCall)
|
- ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
|
||||||
- Parameter names must be defined to be used in [expressions](#expressions)
|
|
||||||
- ❗ Parameter names must be unique
|
|
||||||
`code`: *`string`* (**required** if `call` is undefined)
|
`code`: *`string`* (**required** if `call` is undefined)
|
||||||
- Batch file commands that will be executed
|
- Batch file commands that will be executed
|
||||||
|
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in its value
|
||||||
- 💡 If defined, best practice to also define `revertCode`
|
- 💡 If defined, best practice to also define `revertCode`
|
||||||
- ❗ If not defined `call` must be defined
|
- ❗ If not defined `call` must be defined
|
||||||
- `revertCode`: *`string`*
|
- `revertCode`: *`string`*
|
||||||
- Code that'll undo the change done by `code` property.
|
- Code that'll undo the change done by `code` property.
|
||||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
|
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in code
|
||||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
||||||
- A shared function or sequence of functions to call (called in order)
|
- A shared function or sequence of functions to call (called in order)
|
||||||
- The parameter values that are sent can use [expressions](#expressions)
|
- The parameter values that are sent can use [expressions (templating)](./templating.md#expressions)
|
||||||
- ❗ If not defined `code` must be defined
|
- ❗ If not defined `code` must be defined
|
||||||
|
|
||||||
|
### `FunctionParameter`
|
||||||
|
|
||||||
|
- Defines a parameter that function requires optionally or mandatory.
|
||||||
|
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#FunctionCall).
|
||||||
|
|
||||||
|
#### `FunctionParameter` syntax
|
||||||
|
|
||||||
|
- `name`: *`string`* (**required**)
|
||||||
|
- Name of the parameters that the function has.
|
||||||
|
- Parameter names must be defined to be used in [expressions (templating)](./templating.md#expressions).
|
||||||
|
- ❗ Parameter names must be unique and include alphanumeric characters only.
|
||||||
|
- `optional`: *`boolean`* (default: `false`)
|
||||||
|
- Specifies whether the caller [Script](#script) must provide any value for the parameter.
|
||||||
|
- If set to `false` i.e. an argument value is not optional then it expects a non-empty value for the variable;
|
||||||
|
- Otherwise it throws.
|
||||||
|
- 💡 Set it to `true` if a parameter is used conditionally;
|
||||||
|
- Or else set it to `false` for verbosity or do not define it as default value is `false` anyway.
|
||||||
|
- 💡 Can be used in conjunction with [`with` expression](./templating.md#with).
|
||||||
|
|
||||||
### `ScriptingDefinition`
|
### `ScriptingDefinition`
|
||||||
|
|
||||||
- Defines global properties for scripting that's used throughout its parent [Collection](#collection).
|
- Defines global properties for scripting that's used throughout its parent [Collection](#collection).
|
||||||
@@ -180,7 +167,7 @@ A function can call other functions such as:
|
|||||||
- 📖 See [ScriptingLanguage.ts](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values.
|
- 📖 See [ScriptingLanguage.ts](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values.
|
||||||
- `startCode:` *`string`* (**required**)
|
- `startCode:` *`string`* (**required**)
|
||||||
- Code that'll be inserted on top of user created script.
|
- Code that'll be inserted on top of user created script.
|
||||||
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||||
- `endCode:` *`string`* (**required**)
|
- `endCode:` *`string`* (**required**)
|
||||||
- Code that'll be inserted at the end of user created script.
|
- Code that'll be inserted at the end of user created script.
|
||||||
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||||
|
|||||||
@@ -10,10 +10,16 @@
|
|||||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
|
||||||
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
|
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
|
||||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components.
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components.
|
||||||
- [**`styles/`**](./../src/presentation/styles/): Contains shared styles used throughout different components.
|
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that will be processed by webpack.
|
||||||
|
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
|
||||||
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
|
||||||
|
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles that are reusable and tightly coupled a Vue/HTML component.
|
||||||
|
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles that override third-party components used.
|
||||||
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Primary Sass file, passes along all other styles, should be the only file used from other components.
|
||||||
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
|
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
|
||||||
- [**`background.ts`**](./../src/presentation/background.ts): Main process of Electron, started as first thing when app starts.
|
- [**`electron/`**](./../src/presentation/electron/): Electron configuration for the desktop application.
|
||||||
- [**`/public/`**](./../public/): Contains static assets that will simply be copied and not go through webpack.
|
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
|
||||||
|
- [**`/public/`**](./../public/): Contains static assets that will directly be copied and not go through webpack.
|
||||||
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`
|
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`
|
||||||
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally
|
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally
|
||||||
- [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`
|
- [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`
|
||||||
@@ -49,3 +55,18 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
<div @click="$refs.testDialog.show()">Show dialog</div>
|
<div @click="$refs.testDialog.show()">Show dialog</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Sass naming convention
|
||||||
|
|
||||||
|
- Use lowercase for variables/functions/mixins e.g.
|
||||||
|
- Variable: `$variable: value;`
|
||||||
|
- Function: `@function function() {}`
|
||||||
|
- Mixin: `@mixin mixin() {}`
|
||||||
|
- Use - for a phrase/compound word e.g.
|
||||||
|
- Variable: `$some-variable: value;`
|
||||||
|
- Function: `@function some-function() {}`
|
||||||
|
- Mixin: `@mixin some-mixin() {}`
|
||||||
|
- Grouping and name variables from generic to specific e.g.
|
||||||
|
- ✅ `$border-blue`, `$border-blue-light`, `$border-blue-lightest`, `$border-red`
|
||||||
|
- ❌ `$blue-border`, `$light-blue-border`, `$lightest-blue-border`, `$red-border`
|
||||||
|
|
||||||
88
docs/templating.md
Normal file
88
docs/templating.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Templating
|
||||||
|
|
||||||
|
## Benefits of templating
|
||||||
|
|
||||||
|
- Generating scripts by sharing code to increase best-practice usage and maintainability.
|
||||||
|
- Creating self-contained scripts without depending on each other that can be easily shared.
|
||||||
|
- Use of pipes for writing cleaner code and letting pipes do dirty work.
|
||||||
|
|
||||||
|
## Expressions
|
||||||
|
|
||||||
|
- Expressions in the language are defined inside mustaches (double brackets, `{{` and `}}`).
|
||||||
|
- Expression syntax is inspired mainly by [Go Templates](https://pkg.go.dev/text/template).
|
||||||
|
- Expressions are used in and enabled by functions where they can be used.
|
||||||
|
- 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).
|
||||||
|
|
||||||
|
### Parameter substitution
|
||||||
|
|
||||||
|
A simple function example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
- name: 'argument'
|
||||||
|
code: Hello {{ $argument }} !
|
||||||
|
```
|
||||||
|
|
||||||
|
It would print "Hello world" if it's called in a [script](./collection-files.md#script) as following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
script: Echo script
|
||||||
|
call:
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
argument: World
|
||||||
|
```
|
||||||
|
|
||||||
|
A function can call other functions such as:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
-
|
||||||
|
function: CallerFunction
|
||||||
|
parameters:
|
||||||
|
- name: 'value'
|
||||||
|
call:
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
argument: {{ $value }}
|
||||||
|
-
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
- name: 'argument'
|
||||||
|
code: Hello {{ $argument }} !
|
||||||
|
```
|
||||||
|
|
||||||
|
### with
|
||||||
|
|
||||||
|
- Skips the block if the variable is absent or empty.
|
||||||
|
- Binds its context (`.`) value of provided argument for the parameter if provided one.
|
||||||
|
- A block is defined as `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
|
||||||
|
- The parameters used for `with` condition should be declared as optional, otherwise `with` block becomes redundant.
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
function: FunctionThatOutputsConditionally
|
||||||
|
parameters:
|
||||||
|
- name: 'argument'
|
||||||
|
optional: true
|
||||||
|
code: |-
|
||||||
|
{{ with $argument }}
|
||||||
|
Value is: {{ . }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pipes
|
||||||
|
|
||||||
|
- Pipes are set of functions available for handling text in privacy.sexy.
|
||||||
|
- Allows stacking actions one after another also known as "chaining".
|
||||||
|
- Just like [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), the concept is simple: each pipeline's output becomes the input of the following pipe.
|
||||||
|
- Pipes are provided and defined by the compiler and consumed by collection files.
|
||||||
|
- Pipes can be combined with [parameter substitution](#parameter-substitution) and [with](#with).
|
||||||
|
- ❗ Pipe names must be camelCase without any space or special characters.
|
||||||
|
- **Existing pipes**
|
||||||
|
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
|
||||||
|
- `escapeDoubleQuotes`: Escapes `"` characters to be used inside double quotes (`"`)
|
||||||
|
- **Example usages**
|
||||||
|
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
|
||||||
|
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
|
||||||
@@ -1,29 +1,61 @@
|
|||||||
# Unit tests
|
# Tests
|
||||||
|
|
||||||
- Unit tests are defined in [`./tests`](./../tests)
|
- There are two different types of tests executed:
|
||||||
- They follow same folder structure as [`./src`](./../src)
|
1. [Unit tests](#unit-tests)
|
||||||
|
2. [Integration tests](#integration-tests)
|
||||||
|
3. [End-to-end (E2E) tests](#e2e-tests)
|
||||||
|
- All tests
|
||||||
|
- Uses [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/).
|
||||||
|
- Are written in files that includes `.spec` extension.
|
||||||
|
- 💡 You can use path/module alias `@/tests` in import statements.
|
||||||
|
|
||||||
## Naming
|
## Unit tests
|
||||||
|
|
||||||
- Each test suite first describe the system under test
|
- Tests each component in isolation.
|
||||||
- E.g. tests for class `Application` is categorized under `Application`
|
- Defined in [`./tests/unit`](./../tests/unit).
|
||||||
- Tests for specific methods are categorized under method name (if applicable)
|
- They follow same folder structure as [`./src`](./../src).
|
||||||
- E.g. test for `run()` is categorized under `run`
|
|
||||||
|
|
||||||
## Act, arrange, assert
|
### Naming
|
||||||
|
|
||||||
- Tests use act, arrange and assert (AAA) pattern when applicable
|
- Each test suite first describe the system under test.
|
||||||
|
- E.g. tests for class `Application` is categorized under `Application`.
|
||||||
|
- Tests for specific methods are categorized under method name (if applicable).
|
||||||
|
- E.g. test for `run()` is categorized under `run`.
|
||||||
|
|
||||||
|
### Act, arrange, assert
|
||||||
|
|
||||||
|
- Tests use act, arrange and assert (AAA) pattern when applicable.
|
||||||
- **Arrange**
|
- **Arrange**
|
||||||
- Should set up the test case
|
- Should set up the test case.
|
||||||
- Starts with comment line `// arrange`
|
- Starts with comment line `// arrange`.
|
||||||
- **Act**
|
- **Act**
|
||||||
- Should cover the main thing to be tested
|
- Should cover the main thing to be tested.
|
||||||
- Starts with comment line `// act`
|
- Starts with comment line `// act`.
|
||||||
- **Assert**
|
- **Assert**
|
||||||
- Should elicit some sort of response
|
- Should elicit some sort of response.
|
||||||
- Starts with comment line `// assert`
|
- Starts with comment line `// assert`.
|
||||||
|
|
||||||
## Stubs
|
### Stubs
|
||||||
|
|
||||||
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs)
|
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs).
|
||||||
- They implement dummy behavior to be functional
|
- They implement dummy behavior to be functional.
|
||||||
|
|
||||||
|
## Integration tests
|
||||||
|
|
||||||
|
- Tests functionality of a component in combination with others (not isolated).
|
||||||
|
- Ensure dependencies to third parties work as expected.
|
||||||
|
- Defined in [`./tests/integration`](./../tests/integration).
|
||||||
|
|
||||||
|
## E2E tests
|
||||||
|
|
||||||
|
- Test the functionality and performance of a running application.
|
||||||
|
- E2E tests are configured by vue plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) for Vue CLI.
|
||||||
|
- Names and folders are structured logically based on tests.
|
||||||
|
- The structure is following:
|
||||||
|
- [`cypress.json`](./../cypress.json): Cypress configuration file.
|
||||||
|
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
|
||||||
|
- [`/specs/`](./../tests/e2e/specs/): Test files, test are named with `.spec.js` extension.
|
||||||
|
- [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before project is loaded.
|
||||||
|
- [`/support/index.js`](./../tests/e2e/support/index.js): Support file, runs before every single spec file.
|
||||||
|
- *(Ignored)* `/videos`: Asset folder for videos taken during tests.
|
||||||
|
- *(Ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 483 KiB After Width: | Height: | Size: 255 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 98 KiB |
63037
package-lock.json
generated
63037
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
101
package.json
101
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.10.0",
|
"version": "0.11.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
@@ -8,65 +8,86 @@
|
|||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"test:unit": "vue-cli-service test:unit",
|
||||||
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
|
"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",
|
||||||
"electron:build": "vue-cli-service electron:build",
|
"electron:build": "vue-cli-service electron:build",
|
||||||
"electron:serve": "vue-cli-service electron:serve",
|
"electron:serve": "vue-cli-service electron:serve",
|
||||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||||
"lint:vue": "vue-cli-service lint --no-fix",
|
"lint:eslint": "vue-cli-service lint --no-fix --mode production",
|
||||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstall": "electron-builder install-app-deps",
|
||||||
|
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\""
|
||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "background.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.15.2",
|
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.15.2",
|
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "^2.0.2",
|
"@fortawesome/vue-fontawesome": "^2.0.6",
|
||||||
"@juggle/resize-observer": "^3.3.0",
|
"@juggle/resize-observer": "^3.3.1",
|
||||||
"ace-builds": "^1.4.12",
|
"ace-builds": "^1.4.13",
|
||||||
"core-js": "^3.9.1",
|
"core-js": "^3.18.3",
|
||||||
|
"cross-fetch": "^3.1.4",
|
||||||
|
"electron-progressbar": "^2.0.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"inversify": "^5.0.5",
|
"install": "^0.13.0",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
"v-tooltip": "2.1.2",
|
"npm": "^8.1.1",
|
||||||
"vue": "^2.6.12",
|
"v-tooltip": "2.1.3",
|
||||||
|
"vue": "^2.6.14",
|
||||||
"vue-class-component": "^7.2.6",
|
"vue-class-component": "^7.2.6",
|
||||||
"vue-js-modal": "^2.0.0-rc.6",
|
"vue-js-modal": "^2.0.1",
|
||||||
"vue-property-decorator": "^9.1.2"
|
"vue-property-decorator": "^9.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ace": "0.0.45",
|
"@types/ace": "0.0.47",
|
||||||
"@types/chai": "^4.2.15",
|
"@types/chai": "^4.2.22",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.3",
|
||||||
"@types/mocha": "^8.2.1",
|
"@types/mocha": "^9.0.0",
|
||||||
"@vue/cli-plugin-babel": "^4.5.11",
|
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||||
"@vue/cli-plugin-typescript": "^4.5.11",
|
"@typescript-eslint/parser": "^5.4.0",
|
||||||
"@vue/cli-plugin-unit-mocha": "^4.5.11",
|
"@vue/cli-plugin-babel": "~5.0.0-rc.1",
|
||||||
"@vue/cli-service": "^4.5.11",
|
"@vue/cli-plugin-e2e-cypress": "~5.0.0-rc.1",
|
||||||
"@vue/test-utils": "1.1.3",
|
"@vue/cli-plugin-eslint": "~5.0.0-rc.1",
|
||||||
"chai": "^4.3.3",
|
"@vue/cli-plugin-typescript": "~5.0.0-rc.1",
|
||||||
"electron": "^12.0.1",
|
"@vue/cli-plugin-unit-mocha": "~5.0.0-rc.1",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"@vue/cli-service": "~5.0.0-rc.1",
|
||||||
"electron-log": "^4.3.2",
|
"@vue/eslint-config-airbnb": "^6.0.0",
|
||||||
"electron-updater": "^4.3.8",
|
"@vue/eslint-config-typescript": "^9.1.0",
|
||||||
|
"@vue/test-utils": "1.2.2",
|
||||||
|
"chai": "^4.3.4",
|
||||||
|
"cypress": "^8.3.0",
|
||||||
|
"electron": "^15.3.0",
|
||||||
|
"electron-devtools-installer": "^3.2.0",
|
||||||
|
"electron-log": "^4.4.1",
|
||||||
|
"electron-updater": "^4.3.9",
|
||||||
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-plugin-import": "^2.25.3",
|
||||||
|
"eslint-plugin-vue": "^8.0.3",
|
||||||
|
"eslint-plugin-vuejs-accessibility": "^1.1.0",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"markdownlint-cli": "^0.27.1",
|
"markdownlint-cli": "^0.29.0",
|
||||||
"remark-cli": "^9.0.0",
|
"raw-loader": "^4.0.2",
|
||||||
|
"remark-cli": "^10.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^4.0.0",
|
"remark-preset-lint-consistent": "^5.1.0",
|
||||||
"remark-validate-links": "^10.0.3",
|
"remark-validate-links": "^11.0.1",
|
||||||
"sass": "^1.32.8",
|
"sass": "^1.43.3",
|
||||||
"sass-loader": "^10.1.1",
|
"sass-loader": "10.2.0",
|
||||||
"tslib": "^2.1.0",
|
"ts-loader": "9.0.1",
|
||||||
"typescript": "^4.2.3",
|
"tslib": "^2.3.1",
|
||||||
"vue-cli-plugin-electron-builder": "^2.0.0-rc.6",
|
"typescript": "^4.4.4",
|
||||||
"vue-template-compiler": "^2.6.12",
|
"vue-cli-plugin-electron-builder": "^2.1.1",
|
||||||
|
"vue-template-compiler": "^2.6.14",
|
||||||
"yaml-lint": "^1.2.4"
|
"yaml-lint": "^1.2.4"
|
||||||
},
|
},
|
||||||
|
"//devDependencies": {
|
||||||
|
"ts-loader": "Here as workaround for vue-cli-plugin-electron-builder using older webpack 4"
|
||||||
|
},
|
||||||
"homepage": "https://privacy.sexy",
|
"homepage": "https://privacy.sexy",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
autoprefixer: {}
|
autoprefixer: {},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -8,14 +8,17 @@ const ApplicationGetter: ApplicationGetter = parseApplication;
|
|||||||
|
|
||||||
export class ApplicationFactory implements IApplicationFactory {
|
export class ApplicationFactory implements IApplicationFactory {
|
||||||
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
||||||
|
|
||||||
private readonly getter: AsyncLazy<IApplication>;
|
private readonly getter: AsyncLazy<IApplication>;
|
||||||
|
|
||||||
protected constructor(costlyGetter: ApplicationGetter) {
|
protected constructor(costlyGetter: ApplicationGetter) {
|
||||||
if (!costlyGetter) {
|
if (!costlyGetter) {
|
||||||
throw new Error('undefined getter');
|
throw new Error('undefined getter');
|
||||||
}
|
}
|
||||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||||
}
|
}
|
||||||
public getAppAsync(): Promise<IApplication> {
|
|
||||||
return this.getter.getValueAsync();
|
public getApp(): Promise<IApplication> {
|
||||||
|
return this.getter.getValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/application/Common/Array.ts
Normal file
21
src/application/Common/Array.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Compares to Array<T> objects for equality, ignoring order
|
||||||
|
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
|
if (!array1) { throw new Error('undefined first array'); }
|
||||||
|
if (!array2) { throw new Error('undefined second array'); }
|
||||||
|
const sortedArray1 = sort(array1);
|
||||||
|
const sortedArray2 = sort(array2);
|
||||||
|
return sequenceEqual(sortedArray1, sortedArray2);
|
||||||
|
function sort(array: readonly T[]) {
|
||||||
|
return array.slice().sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares to Array<T> objects for equality in same order
|
||||||
|
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
|
if (!array1) { throw new Error('undefined first array'); }
|
||||||
|
if (!array2) { throw new Error('undefined second array'); }
|
||||||
|
if (array1.length !== array2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return array1.every((val, index) => val === array2[index]);
|
||||||
|
}
|
||||||
@@ -1,20 +1,25 @@
|
|||||||
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
||||||
type EnumType = number | string;
|
export type EnumType = number | string;
|
||||||
type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
|
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
||||||
|
= { [key in T]: TEnumValue };
|
||||||
|
|
||||||
export interface IEnumParser<TEnum> {
|
export interface IEnumParser<TEnum> {
|
||||||
parseEnum(value: string, propertyName: string): TEnum;
|
parseEnum(value: string, propertyName: string): TEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
): IEnumParser<TEnumValue> {
|
||||||
return {
|
return {
|
||||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
value: string,
|
value: string,
|
||||||
enumName: string,
|
enumName: string,
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
): TEnumValue {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new Error(`undefined ${enumName}`);
|
throw new Error(`undefined ${enumName}`);
|
||||||
}
|
}
|
||||||
@@ -29,15 +34,30 @@ function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
|||||||
return enumVariable[casedValue as keyof typeof enumVariable];
|
return enumVariable[casedValue as keyof typeof enumVariable];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
|
export function getEnumNames
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): string[] {
|
<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
): string[] {
|
||||||
return Object
|
return Object
|
||||||
.values(enumVariable)
|
.values(enumVariable)
|
||||||
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
): TEnumValue[] {
|
||||||
return getEnumNames(enumVariable)
|
return getEnumNames(enumVariable)
|
||||||
.map((level) => enumVariable[level]) as TEnumValue[];
|
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
value: TEnumValue,
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
) {
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error('undefined enum value');
|
||||||
|
}
|
||||||
|
if (!(value in enumVariable)) {
|
||||||
|
throw new RangeError(`enum value "${value}" is out of range`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
|
||||||
|
export interface IScriptingLanguageFactory<T> {
|
||||||
|
create(language: ScriptingLanguage): T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
|
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
||||||
|
|
||||||
|
type Getter<T> = () => T;
|
||||||
|
|
||||||
|
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<T> {
|
||||||
|
private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
|
||||||
|
|
||||||
|
public create(language: ScriptingLanguage): T {
|
||||||
|
assertInRange(language, ScriptingLanguage);
|
||||||
|
if (!this.getters.has(language)) {
|
||||||
|
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||||
|
}
|
||||||
|
const getter = this.getters.get(language);
|
||||||
|
const instance = getter();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||||
|
assertInRange(language, ScriptingLanguage);
|
||||||
|
if (!getter) {
|
||||||
|
throw new Error('undefined getter');
|
||||||
|
}
|
||||||
|
if (this.getters.has(language)) {
|
||||||
|
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
||||||
|
}
|
||||||
|
this.getters.set(language, getter);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
|
||||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
|
||||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
|
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||||
|
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||||
|
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
||||||
|
|
||||||
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
||||||
|
|
||||||
export class ApplicationContext implements IApplicationContext {
|
export class ApplicationContext implements IApplicationContext {
|
||||||
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
|
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
|
||||||
|
|
||||||
public collection: ICategoryCollection;
|
public collection: ICategoryCollection;
|
||||||
|
|
||||||
public currentOs: OperatingSystem;
|
public currentOs: OperatingSystem;
|
||||||
|
|
||||||
public get state(): ICategoryCollectionState {
|
public get state(): ICategoryCollectionState {
|
||||||
@@ -18,11 +21,13 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readonly states: StateMachine;
|
private readonly states: StateMachine;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly app: IApplication,
|
public readonly app: IApplication,
|
||||||
initialContext: OperatingSystem) {
|
initialContext: OperatingSystem,
|
||||||
|
) {
|
||||||
validateApp(app);
|
validateApp(app);
|
||||||
validateOs(initialContext);
|
assertInRange(initialContext, OperatingSystem);
|
||||||
this.states = initializeStates(app);
|
this.states = initializeStates(app);
|
||||||
this.changeContext(initialContext);
|
this.changeContext(initialContext);
|
||||||
}
|
}
|
||||||
@@ -50,18 +55,6 @@ function validateApp(app: IApplication) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateOs(os: OperatingSystem) {
|
|
||||||
if (os === undefined) {
|
|
||||||
throw new Error('undefined os');
|
|
||||||
}
|
|
||||||
if (os === OperatingSystem.Unknown) {
|
|
||||||
throw new Error('unknown os');
|
|
||||||
}
|
|
||||||
if (!(os in OperatingSystem)) {
|
|
||||||
throw new Error(`os "${os}" is out of range`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeStates(app: IApplication): StateMachine {
|
function initializeStates(app: IApplication): StateMachine {
|
||||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||||
for (const collection of app.collections) {
|
for (const collection of app.collections) {
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { ApplicationContext } from './ApplicationContext';
|
|
||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { Environment } from '../Environment/Environment';
|
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { Environment } from '../Environment/Environment';
|
||||||
import { IEnvironment } from '../Environment/IEnvironment';
|
import { IEnvironment } from '../Environment/IEnvironment';
|
||||||
import { IApplicationFactory } from '../IApplicationFactory';
|
import { IApplicationFactory } from '../IApplicationFactory';
|
||||||
import { ApplicationFactory } from '../ApplicationFactory';
|
import { ApplicationFactory } from '../ApplicationFactory';
|
||||||
|
import { ApplicationContext } from './ApplicationContext';
|
||||||
|
|
||||||
export async function buildContextAsync(
|
export async function buildContext(
|
||||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||||
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
|
environment = Environment.CurrentEnvironment,
|
||||||
|
): Promise<IApplicationContext> {
|
||||||
if (!factory) { throw new Error('undefined factory'); }
|
if (!factory) { throw new Error('undefined factory'); }
|
||||||
if (!environment) { throw new Error('undefined environment'); }
|
if (!environment) { throw new Error('undefined environment'); }
|
||||||
const app = await factory.getAppAsync();
|
const app = await factory.getApp();
|
||||||
const os = getInitialOs(app, environment);
|
const os = getInitialOs(app, environment);
|
||||||
return new ApplicationContext(app, os);
|
return new ApplicationContext(app, os);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
|
||||||
|
|
||||||
export interface IApplicationContext {
|
export interface IReadOnlyApplicationContext {
|
||||||
readonly app: IApplication;
|
readonly app: IApplication;
|
||||||
readonly state: ICategoryCollectionState;
|
readonly state: IReadOnlyCategoryCollectionState;
|
||||||
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApplicationContext extends IReadOnlyApplicationContext {
|
||||||
|
readonly state: ICategoryCollectionState;
|
||||||
changeContext(os: OperatingSystem): void;
|
changeContext(os: OperatingSystem): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { UserFilter } from './Filter/UserFilter';
|
import { UserFilter } from './Filter/UserFilter';
|
||||||
import { IUserFilter } from './Filter/IUserFilter';
|
import { IUserFilter } from './Filter/IUserFilter';
|
||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
@@ -5,13 +7,14 @@ import { UserSelection } from './Selection/UserSelection';
|
|||||||
import { IUserSelection } from './Selection/IUserSelection';
|
import { IUserSelection } from './Selection/IUserSelection';
|
||||||
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
import { ICategoryCollection } from '../../../domain/ICategoryCollection';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
|
|
||||||
export class CategoryCollectionState implements ICategoryCollectionState {
|
export class CategoryCollectionState implements ICategoryCollectionState {
|
||||||
public readonly os: OperatingSystem;
|
public readonly os: OperatingSystem;
|
||||||
|
|
||||||
public readonly code: IApplicationCode;
|
public readonly code: IApplicationCode;
|
||||||
|
|
||||||
public readonly selection: IUserSelection;
|
public readonly selection: IUserSelection;
|
||||||
|
|
||||||
public readonly filter: IUserFilter;
|
public readonly filter: IUserFilter;
|
||||||
|
|
||||||
public constructor(readonly collection: ICategoryCollection) {
|
public constructor(readonly collection: ICategoryCollection) {
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
|
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||||
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||||
import { CodePosition } from './Position/CodePosition';
|
import { CodePosition } from './Position/CodePosition';
|
||||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
|
||||||
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
|
||||||
import { IApplicationCode } from './IApplicationCode';
|
import { IApplicationCode } from './IApplicationCode';
|
||||||
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
|
||||||
|
|
||||||
export class ApplicationCode implements IApplicationCode {
|
export class ApplicationCode implements IApplicationCode {
|
||||||
public readonly changed = new EventSource<ICodeChangedEvent>();
|
public readonly changed = new EventSource<ICodeChangedEvent>();
|
||||||
|
|
||||||
public current: string;
|
public current: string;
|
||||||
|
|
||||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
userSelection: IUserSelection,
|
userSelection: IReadOnlyUserSelection,
|
||||||
private readonly scriptingDefinition: IScriptingDefinition,
|
private readonly scriptingDefinition: IScriptingDefinition,
|
||||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
||||||
|
) {
|
||||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||||
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
||||||
if (!generator) { throw new Error('generator is null or undefined'); }
|
if (!generator) { throw new Error('generator is null or undefined'); }
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
|
||||||
import { SelectedScript } from '../../Selection/SelectedScript';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||||
|
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
|
||||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
public readonly code: string;
|
public readonly code: string;
|
||||||
|
|
||||||
public readonly addedScripts: ReadonlyArray<IScript>;
|
public readonly addedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
public readonly removedScripts: ReadonlyArray<IScript>;
|
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
public readonly changedScripts: ReadonlyArray<IScript>;
|
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
private readonly scripts: Map<IScript, ICodePosition>;
|
private readonly scripts: Map<IScript, ICodePosition>;
|
||||||
@@ -14,7 +17,8 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
constructor(
|
constructor(
|
||||||
code: string,
|
code: string,
|
||||||
oldScripts: ReadonlyArray<SelectedScript>,
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
scripts: Map<SelectedScript, ICodePosition>) {
|
scripts: Map<SelectedScript, ICodePosition>,
|
||||||
|
) {
|
||||||
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
||||||
this.code = code;
|
this.code = code;
|
||||||
const newScripts = Array.from(scripts.keys());
|
const newScripts = Array.from(scripts.keys());
|
||||||
@@ -38,26 +42,29 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
|
|
||||||
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||||
const totalLines = script.split(/\r\n|\r|\n/).length;
|
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||||
for (const position of positions) {
|
const missingPositions = positions.filter((position) => position.endLine > totalLines);
|
||||||
if (position.endLine > totalLines) {
|
if (missingPositions.length > 0) {
|
||||||
throw new Error(`script end line (${position.endLine}) is out of range.` +
|
throw new Error(
|
||||||
`(total code lines: ${totalLines}`);
|
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
|
||||||
}
|
+ `(total code lines: ${totalLines}).`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChangedScripts(
|
function getChangedScripts(
|
||||||
oldScripts: ReadonlyArray<SelectedScript>,
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
|
newScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
): ReadonlyArray<IScript> {
|
||||||
return newScripts
|
return newScripts
|
||||||
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||||
&& oldScript.revert !== newScript.revert ))
|
&& oldScript.revert !== newScript.revert))
|
||||||
.map((selection) => selection.script);
|
.map((selection) => selection.script);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectIfNotExists(
|
function selectIfNotExists(
|
||||||
selectableContainer: ReadonlyArray<SelectedScript>,
|
selectableContainer: ReadonlyArray<SelectedScript>,
|
||||||
test: ReadonlyArray<SelectedScript>) {
|
test: ReadonlyArray<SelectedScript>,
|
||||||
|
) {
|
||||||
return selectableContainer
|
return selectableContainer
|
||||||
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
||||||
.map((selection) => selection.script);
|
.map((selection) => selection.script);
|
||||||
|
|||||||
@@ -17,14 +17,13 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
const lines = code.match(/[^\r\n]+/g);
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
for (const line of lines) {
|
this.lines.push(...lines);
|
||||||
this.lines.push(line);
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public appendTrailingHyphensCommentLine(
|
public appendTrailingHyphensCommentLine(
|
||||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
|
||||||
|
): CodeBuilder {
|
||||||
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
|
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +44,8 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
|
|
||||||
public appendCommentLineWithHyphensAround(
|
public appendCommentLineWithHyphensAround(
|
||||||
sectionName: string,
|
sectionName: string,
|
||||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
|
||||||
|
): CodeBuilder {
|
||||||
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
|
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
|
||||||
if (sectionName.length >= totalRepeatHyphens) {
|
if (sectionName.length >= totalRepeatHyphens) {
|
||||||
return this.appendCommentLine(sectionName);
|
return this.appendCommentLine(sectionName);
|
||||||
@@ -63,5 +63,6 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected abstract getCommentDelimiter(): string;
|
protected abstract getCommentDelimiter(): string;
|
||||||
|
|
||||||
protected abstract writeStandardOut(text: string): string;
|
protected abstract writeStandardOut(text: string): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
|
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ICodeBuilder } from './ICodeBuilder';
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
|
||||||
import { BatchBuilder } from './Languages/BatchBuilder';
|
import { BatchBuilder } from './Languages/BatchBuilder';
|
||||||
import { ShellBuilder } from './Languages/ShellBuilder';
|
import { ShellBuilder } from './Languages/ShellBuilder';
|
||||||
|
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||||
|
|
||||||
export class CodeBuilderFactory implements ICodeBuilderFactory {
|
export class CodeBuilderFactory
|
||||||
public create(language: ScriptingLanguage): ICodeBuilder {
|
extends ScriptingLanguageFactory<ICodeBuilder>
|
||||||
switch (language) {
|
implements ICodeBuilderFactory {
|
||||||
case ScriptingLanguage.shellscript: return new ShellBuilder();
|
constructor() {
|
||||||
case ScriptingLanguage.batchfile: return new BatchBuilder();
|
super();
|
||||||
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
|
||||||
}
|
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||||
import { ICodeBuilder } from './ICodeBuilder';
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
export interface ICodeBuilderFactory {
|
export type ICodeBuilderFactory = IScriptingLanguageFactory<ICodeBuilder>;
|
||||||
create(language: ScriptingLanguage): ICodeBuilder;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { IUserScript } from './IUserScript';
|
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
|
|
||||||
export interface IUserScriptGenerator {
|
export interface IUserScriptGenerator {
|
||||||
buildCode(
|
buildCode(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export class BatchBuilder extends CodeBuilder {
|
|||||||
protected getCommentDelimiter(): string {
|
protected getCommentDelimiter(): string {
|
||||||
return '::';
|
return '::';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected writeStandardOut(text: string): string {
|
protected writeStandardOut(text: string): string {
|
||||||
return `echo ${escapeForEcho(text)}`;
|
return `echo ${escapeForEcho(text)}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export class ShellBuilder extends CodeBuilder {
|
|||||||
protected getCommentDelimiter(): string {
|
protected getCommentDelimiter(): string {
|
||||||
return '#';
|
return '#';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected writeStandardOut(text: string): string {
|
protected writeStandardOut(text: string): string {
|
||||||
return `echo '${escapeForEcho(text)}'`;
|
return `echo '${escapeForEcho(text)}'`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { CodePosition } from '../Position/CodePosition';
|
|
||||||
import { IUserScript } from './IUserScript';
|
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { CodePosition } from '../Position/CodePosition';
|
||||||
|
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
import { ICodeBuilder } from './ICodeBuilder';
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||||
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
||||||
@@ -12,20 +12,21 @@ export class UserScriptGenerator implements IUserScriptGenerator {
|
|||||||
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildCode(
|
public buildCode(
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
scriptingDefinition: IScriptingDefinition): IUserScript {
|
scriptingDefinition: IScriptingDefinition,
|
||||||
|
): IUserScript {
|
||||||
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
||||||
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
||||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
|
||||||
if (!selectedScripts.length) {
|
if (!selectedScripts.length) {
|
||||||
return { code: '', scriptPositions };
|
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
||||||
}
|
}
|
||||||
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
||||||
builder = initializeCode(scriptingDefinition.startCode, builder);
|
builder = initializeCode(scriptingDefinition.startCode, builder);
|
||||||
for (const selection of selectedScripts) {
|
const scriptPositions = selectedScripts.reduce((result, selection) => {
|
||||||
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
return appendSelection(selection, result, builder);
|
||||||
}
|
}, new Map<SelectedScript, ICodePosition>());
|
||||||
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
||||||
return { code, scriptPositions };
|
return { code, scriptPositions };
|
||||||
}
|
}
|
||||||
@@ -52,9 +53,11 @@ function finalizeCode(builder: ICodeBuilder, endCode: string): string {
|
|||||||
function appendSelection(
|
function appendSelection(
|
||||||
selection: SelectedScript,
|
selection: SelectedScript,
|
||||||
scriptPositions: Map<SelectedScript, ICodePosition>,
|
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||||
builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
|
builder: ICodeBuilder,
|
||||||
const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts
|
): Map<SelectedScript, ICodePosition> {
|
||||||
builder = appendCode(selection, builder);
|
// Start from next line because first line will be empty to separate scripts
|
||||||
|
const startPosition = builder.currentLine + 1;
|
||||||
|
appendCode(selection, builder);
|
||||||
const endPosition = builder.currentLine - 1;
|
const endPosition = builder.currentLine - 1;
|
||||||
builder.appendLine();
|
builder.appendLine();
|
||||||
const position = new CodePosition(startPosition, endPosition);
|
const position = new CodePosition(startPosition, endPosition);
|
||||||
@@ -63,8 +66,9 @@ function appendSelection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
||||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
const { script } = selection;
|
||||||
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
|
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
||||||
|
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
|
||||||
return builder
|
return builder
|
||||||
.appendLine()
|
.appendLine()
|
||||||
.appendFunction(name, scriptCode);
|
.appendFunction(name, scriptCode);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
|
|
||||||
export interface IApplicationCode {
|
export interface IApplicationCode {
|
||||||
readonly changed: IEventSource<ICodeChangedEvent>;
|
readonly changed: IEventSource<ICodeChangedEvent>;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export class CodePosition implements ICodePosition {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly startLine: number,
|
public readonly startLine: number,
|
||||||
public readonly endLine: number) {
|
public readonly endLine: number,
|
||||||
|
) {
|
||||||
if (startLine < 0) {
|
if (startLine < 0) {
|
||||||
throw new Error('Code cannot start in a negative line');
|
throw new Error('Code cannot start in a negative line');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { IFilterResult } from './IFilterResult';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { IFilterResult } from './IFilterResult';
|
||||||
|
|
||||||
export class FilterResult implements IFilterResult {
|
export class FilterResult implements IFilterResult {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly scriptMatches: ReadonlyArray<IScript>,
|
public readonly scriptMatches: ReadonlyArray<IScript>,
|
||||||
public readonly categoryMatches: ReadonlyArray<ICategory>,
|
public readonly categoryMatches: ReadonlyArray<ICategory>,
|
||||||
public readonly query: string) {
|
public readonly query: string,
|
||||||
|
) {
|
||||||
if (!query) { throw new Error('Query is empty or undefined'); }
|
if (!query) { throw new Error('Query is empty or undefined'); }
|
||||||
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
|
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
|
||||||
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
|
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasAnyMatches(): boolean {
|
public hasAnyMatches(): boolean {
|
||||||
return this.scriptMatches.length > 0
|
return this.scriptMatches.length > 0
|
||||||
|| this.categoryMatches.length > 0;
|
|| this.categoryMatches.length > 0;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import { IFilterResult } from './IFilterResult';
|
import { IFilterResult } from './IFilterResult';
|
||||||
|
|
||||||
export interface IUserFilter {
|
export interface IReadOnlyUserFilter {
|
||||||
readonly currentFilter: IFilterResult | undefined;
|
readonly currentFilter: IFilterResult | undefined;
|
||||||
readonly filtered: IEventSource<IFilterResult>;
|
readonly filtered: IEventSource<IFilterResult>;
|
||||||
readonly filterRemoved: IEventSource<void>;
|
readonly filterRemoved: IEventSource<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserFilter extends IReadOnlyUserFilter {
|
||||||
setFilter(filter: string): void;
|
setFilter(filter: string): void;
|
||||||
removeFilter(): void;
|
removeFilter(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { FilterResult } from './FilterResult';
|
import { FilterResult } from './FilterResult';
|
||||||
import { IFilterResult } from './IFilterResult';
|
import { IFilterResult } from './IFilterResult';
|
||||||
import { IUserFilter } from './IUserFilter';
|
import { IUserFilter } from './IUserFilter';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
|
||||||
|
|
||||||
export class UserFilter implements IUserFilter {
|
export class UserFilter implements IUserFilter {
|
||||||
public readonly filtered = new EventSource<IFilterResult>();
|
public readonly filtered = new EventSource<IFilterResult>();
|
||||||
|
|
||||||
public readonly filterRemoved = new EventSource<void>();
|
public readonly filterRemoved = new EventSource<void>();
|
||||||
|
|
||||||
public currentFilter: IFilterResult | undefined;
|
public currentFilter: IFilterResult | undefined;
|
||||||
|
|
||||||
constructor(private collection: ICategoryCollection) {
|
constructor(private collection: ICategoryCollection) {
|
||||||
@@ -20,9 +22,11 @@ export class UserFilter implements IUserFilter {
|
|||||||
}
|
}
|
||||||
const filterLowercase = filter.toLocaleLowerCase();
|
const filterLowercase = filter.toLocaleLowerCase();
|
||||||
const filteredScripts = this.collection.getAllScripts().filter(
|
const filteredScripts = this.collection.getAllScripts().filter(
|
||||||
(script) => isScriptAMatch(script, filterLowercase));
|
(script) => isScriptAMatch(script, filterLowercase),
|
||||||
|
);
|
||||||
const filteredCategories = this.collection.getAllCategories().filter(
|
const filteredCategories = this.collection.getAllCategories().filter(
|
||||||
(category) => category.name.toLowerCase().includes(filterLowercase));
|
(category) => category.name.toLowerCase().includes(filterLowercase),
|
||||||
|
);
|
||||||
const matches = new FilterResult(
|
const matches = new FilterResult(
|
||||||
filteredScripts,
|
filteredScripts,
|
||||||
filteredCategories,
|
filteredCategories,
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { IUserFilter } from './Filter/IUserFilter';
|
|
||||||
import { IUserSelection } from './Selection/IUserSelection';
|
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
|
||||||
|
import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
|
||||||
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
|
||||||
export interface ICategoryCollectionState {
|
export interface IReadOnlyCategoryCollectionState {
|
||||||
readonly code: IApplicationCode;
|
readonly code: IApplicationCode;
|
||||||
|
readonly os: OperatingSystem;
|
||||||
|
readonly filter: IReadOnlyUserFilter;
|
||||||
|
readonly selection: IReadOnlyUserSelection;
|
||||||
|
readonly collection: ICategoryCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
|
||||||
readonly filter: IUserFilter;
|
readonly filter: IUserFilter;
|
||||||
readonly selection: IUserSelection;
|
readonly selection: IUserSelection;
|
||||||
readonly collection: ICategoryCollection;
|
|
||||||
readonly os: OperatingSystem;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
|
||||||
export interface IUserSelection {
|
export interface IReadOnlyUserSelection {
|
||||||
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
||||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||||
readonly totalSelected: number;
|
isSelected(scriptId: string): boolean;
|
||||||
areAllSelected(category: ICategory): boolean;
|
areAllSelected(category: ICategory): boolean;
|
||||||
isAnySelected(category: ICategory): boolean;
|
isAnySelected(category: ICategory): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserSelection extends IReadOnlyUserSelection {
|
||||||
removeAllInCategory(categoryId: number): void;
|
removeAllInCategory(categoryId: number): void;
|
||||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
||||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
removeSelectedScript(scriptId: string): void;
|
removeSelectedScript(scriptId: string): void;
|
||||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
||||||
isSelected(scriptId: string): boolean;
|
|
||||||
selectAll(): void;
|
selectAll(): void;
|
||||||
deselectAll(): void;
|
deselectAll(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
import { IUserSelection } from './IUserSelection';
|
|
||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { IUserSelection } from './IUserSelection';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
|
||||||
export class UserSelection implements IUserSelection {
|
export class UserSelection implements IUserSelection {
|
||||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||||
|
|
||||||
private readonly scripts: IRepository<string, SelectedScript>;
|
private readonly scripts: IRepository<string, SelectedScript>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly collection: ICategoryCollection,
|
private readonly collection: ICategoryCollection,
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>) {
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
) {
|
||||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
if (selectedScripts && selectedScripts.length > 0) {
|
|
||||||
for (const script of selectedScripts) {
|
for (const script of selectedScripts) {
|
||||||
this.scripts.addItem(script);
|
this.scripts.addItem(script);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public areAllSelected(category: ICategory): boolean {
|
public areAllSelected(category: ICategory): boolean {
|
||||||
if (this.selectedScripts.length === 0) {
|
if (this.selectedScripts.length === 0) {
|
||||||
@@ -30,7 +30,9 @@ export class UserSelection implements IUserSelection {
|
|||||||
if (this.selectedScripts.length < scripts.length) {
|
if (this.selectedScripts.length < scripts.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id));
|
return scripts.every(
|
||||||
|
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isAnySelected(category: ICategory): boolean {
|
public isAnySelected(category: ICategory): boolean {
|
||||||
@@ -53,19 +55,20 @@ export class UserSelection implements IUserSelection {
|
|||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
|
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
||||||
const category = this.collection.findCategory(categoryId);
|
const scriptsToAddOrUpdate = this.collection
|
||||||
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
.findCategory(categoryId)
|
||||||
.filter((script) =>
|
.getAllScriptsRecursively()
|
||||||
!this.scripts.exists(script.id)
|
.filter(
|
||||||
|
(script) => !this.scripts.exists(script.id)
|
||||||
|| this.scripts.getById(script.id).revert !== revert,
|
|| this.scripts.getById(script.id).revert !== revert,
|
||||||
);
|
)
|
||||||
|
.map((script) => new SelectedScript(script, revert));
|
||||||
if (!scriptsToAddOrUpdate.length) {
|
if (!scriptsToAddOrUpdate.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const script of scriptsToAddOrUpdate) {
|
for (const script of scriptsToAddOrUpdate) {
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
this.scripts.addOrUpdateItem(script);
|
||||||
this.scripts.addOrUpdateItem(selectedScript);
|
|
||||||
}
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
@@ -101,16 +104,13 @@ export class UserSelection implements IUserSelection {
|
|||||||
return this.scripts.getItems();
|
return this.scripts.getItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get totalSelected(): number {
|
|
||||||
return this.scripts.getItems().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectAll(): void {
|
public selectAll(): void {
|
||||||
for (const script of this.collection.getAllScripts()) {
|
const scriptsToSelect = this.collection
|
||||||
if (!this.scripts.exists(script.id)) {
|
.getAllScripts()
|
||||||
const selection = new SelectedScript(script, false);
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
this.scripts.addItem(selection);
|
.map((script) => new SelectedScript(script, false));
|
||||||
}
|
for (const script of scriptsToSelect) {
|
||||||
|
this.scripts.addItem(script);
|
||||||
}
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
@@ -135,10 +135,11 @@ export class UserSelection implements IUserSelection {
|
|||||||
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
||||||
}
|
}
|
||||||
// Select from unselected scripts
|
// Select from unselected scripts
|
||||||
const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
|
const unselectedScripts = scripts
|
||||||
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
|
.map((script) => new SelectedScript(script, false));
|
||||||
for (const toSelect of unselectedScripts) {
|
for (const toSelect of unselectedScripts) {
|
||||||
const selection = new SelectedScript(toSelect, false);
|
this.scripts.addItem(toSelect);
|
||||||
this.scripts.addItem(selection);
|
|
||||||
}
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,50 +4,53 @@ import { IBrowserOsDetector } from './IBrowserOsDetector';
|
|||||||
|
|
||||||
export class BrowserOsDetector implements IBrowserOsDetector {
|
export class BrowserOsDetector implements IBrowserOsDetector {
|
||||||
private readonly detectors = BrowserDetectors;
|
private readonly detectors = BrowserDetectors;
|
||||||
public detect(userAgent: string): OperatingSystem {
|
|
||||||
|
public detect(userAgent: string): OperatingSystem | undefined {
|
||||||
if (!userAgent) {
|
if (!userAgent) {
|
||||||
return OperatingSystem.Unknown;
|
return undefined;
|
||||||
}
|
}
|
||||||
for (const detector of this.detectors) {
|
for (const detector of this.detectors) {
|
||||||
const os = detector.detect(userAgent);
|
const os = detector.detect(userAgent);
|
||||||
if (os !== OperatingSystem.Unknown) {
|
if (os !== undefined) {
|
||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return OperatingSystem.Unknown;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
|
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
|
||||||
const BrowserDetectors =
|
const BrowserDetectors = [
|
||||||
[
|
define(OperatingSystem.KaiOS, (b) => b
|
||||||
define(OperatingSystem.KaiOS, (b) =>
|
.mustInclude('KAIOS')),
|
||||||
b.mustInclude('KAIOS')),
|
define(OperatingSystem.ChromeOS, (b) => b
|
||||||
define(OperatingSystem.ChromeOS, (b) =>
|
.mustInclude('CrOS')),
|
||||||
b.mustInclude('CrOS')),
|
define(OperatingSystem.BlackBerryOS, (b) => b
|
||||||
define(OperatingSystem.BlackBerryOS, (b) =>
|
.mustInclude('BlackBerry')),
|
||||||
b.mustInclude('BlackBerry')),
|
define(OperatingSystem.BlackBerryTabletOS, (b) => b
|
||||||
define(OperatingSystem.BlackBerryTabletOS, (b) =>
|
.mustInclude('RIM Tablet OS')),
|
||||||
b.mustInclude('RIM Tablet OS')),
|
define(OperatingSystem.BlackBerry, (b) => b
|
||||||
define(OperatingSystem.BlackBerry, (b) =>
|
.mustInclude('BB10')),
|
||||||
b.mustInclude('BB10')),
|
define(OperatingSystem.Android, (b) => b
|
||||||
define(OperatingSystem.Android, (b) =>
|
.mustInclude('Android').mustNotInclude('Windows Phone')),
|
||||||
b.mustInclude('Android').mustNotInclude('Windows Phone')),
|
define(OperatingSystem.Android, (b) => b
|
||||||
define(OperatingSystem.Android, (b) =>
|
.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
||||||
b.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
define(OperatingSystem.iOS, (b) => b
|
||||||
define(OperatingSystem.iOS, (b) =>
|
.mustInclude('like Mac OS X')),
|
||||||
b.mustInclude('like Mac OS X')),
|
define(OperatingSystem.Linux, (b) => b
|
||||||
define(OperatingSystem.Linux, (b) =>
|
.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
||||||
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
define(OperatingSystem.Windows, (b) => b
|
||||||
define(OperatingSystem.Windows, (b) =>
|
.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
||||||
b.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
define(OperatingSystem.WindowsPhone, (b) => b
|
||||||
define(OperatingSystem.WindowsPhone, (b) =>
|
.mustInclude('Windows Phone')),
|
||||||
b.mustInclude('Windows Phone')),
|
define(OperatingSystem.macOS, (b) => b
|
||||||
define(OperatingSystem.macOS, (b) =>
|
.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
||||||
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
|
function define(
|
||||||
|
os: OperatingSystem,
|
||||||
|
applyRules: (builder: DetectorBuilder) => DetectorBuilder,
|
||||||
|
): IBrowserOsDetector {
|
||||||
const builder = new DetectorBuilder(os);
|
const builder = new DetectorBuilder(os);
|
||||||
applyRules(builder);
|
applyRules(builder);
|
||||||
return builder.build();
|
return builder.build();
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
|
|
||||||
export class DetectorBuilder {
|
export class DetectorBuilder {
|
||||||
private readonly existingPartsInUserAgent = new Array<string>();
|
private readonly existingPartsInUserAgent = new Array<string>();
|
||||||
|
|
||||||
private readonly notExistingPartsInUserAgent = new Array<string>();
|
private readonly notExistingPartsInUserAgent = new Array<string>();
|
||||||
|
|
||||||
constructor(private readonly os: OperatingSystem) { }
|
constructor(private readonly os: OperatingSystem) { }
|
||||||
@@ -29,10 +30,10 @@ export class DetectorBuilder {
|
|||||||
throw new Error('User agent is null or undefined');
|
throw new Error('User agent is null or undefined');
|
||||||
}
|
}
|
||||||
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
||||||
return OperatingSystem.Unknown;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
|
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
|
||||||
return OperatingSystem.Unknown;
|
return undefined;
|
||||||
}
|
}
|
||||||
return this.os;
|
return this.os;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export interface IBrowserOsDetector {
|
export interface IBrowserOsDetector {
|
||||||
detect(userAgent: string): OperatingSystem;
|
detect(userAgent: string): OperatingSystem | undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||||
import { IEnvironment } from './IEnvironment';
|
import { IEnvironment } from './IEnvironment';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
|
|
||||||
interface IEnvironmentVariables {
|
export interface IEnvironmentVariables {
|
||||||
readonly window: Window & typeof globalThis;
|
readonly window: Window & typeof globalThis;
|
||||||
readonly process: NodeJS.Process;
|
readonly process: NodeJS.Process;
|
||||||
readonly navigator: Navigator;
|
readonly navigator: Navigator;
|
||||||
@@ -12,21 +12,28 @@ interface IEnvironmentVariables {
|
|||||||
export class Environment implements IEnvironment {
|
export class Environment implements IEnvironment {
|
||||||
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
||||||
window,
|
window,
|
||||||
process,
|
process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
|
||||||
navigator,
|
navigator,
|
||||||
});
|
});
|
||||||
|
|
||||||
public readonly isDesktop: boolean;
|
public readonly isDesktop: boolean;
|
||||||
|
|
||||||
public readonly os: OperatingSystem;
|
public readonly os: OperatingSystem;
|
||||||
|
|
||||||
protected constructor(
|
protected constructor(
|
||||||
variables: IEnvironmentVariables,
|
variables: IEnvironmentVariables,
|
||||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
|
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||||
|
) {
|
||||||
if (!variables) {
|
if (!variables) {
|
||||||
throw new Error('variables is null or empty');
|
throw new Error('variables is null or empty');
|
||||||
}
|
}
|
||||||
this.isDesktop = isDesktop(variables);
|
this.isDesktop = isDesktop(variables);
|
||||||
this.os = this.isDesktop ?
|
if (this.isDesktop) {
|
||||||
getDesktopOsType(getProcessPlatform(variables))
|
this.os = getDesktopOsType(getProcessPlatform(variables));
|
||||||
: browserOsDetector.detect(getUserAgent(variables));
|
} else {
|
||||||
|
const userAgent = getUserAgent(variables);
|
||||||
|
this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,16 +51,18 @@ function getProcessPlatform(variables: IEnvironmentVariables): string {
|
|||||||
return variables.process.platform;
|
return variables.process.platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDesktopOsType(processPlatform: string): OperatingSystem {
|
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
|
||||||
// https://nodejs.org/api/process.html#process_process_platform
|
// https://nodejs.org/api/process.html#process_process_platform
|
||||||
if (processPlatform === 'darwin') {
|
switch (processPlatform) {
|
||||||
|
case 'darwin':
|
||||||
return OperatingSystem.macOS;
|
return OperatingSystem.macOS;
|
||||||
} else if (processPlatform === 'win32') {
|
case 'win32':
|
||||||
return OperatingSystem.Windows;
|
return OperatingSystem.Windows;
|
||||||
} else if (processPlatform === 'linux') {
|
case 'linux':
|
||||||
return OperatingSystem.Linux;
|
return OperatingSystem.Linux;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
return OperatingSystem.Unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDesktop(variables: IEnvironmentVariables): boolean {
|
function isDesktop(variables: IEnvironmentVariables): boolean {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
export interface IApplicationFactory {
|
export interface IApplicationFactory {
|
||||||
getAppAsync(): Promise<IApplication>;
|
getApp(): Promise<IApplication>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
|
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
|
||||||
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
||||||
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
|
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
|
||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
|
|
||||||
export function parseApplication(
|
export function parseApplication(
|
||||||
parser = CategoryCollectionParser,
|
parser = CategoryCollectionParser,
|
||||||
processEnv: NodeJS.ProcessEnv = process.env,
|
processEnv: NodeJS.ProcessEnv = process.env,
|
||||||
collectionsData = PreParsedCollections): IApplication {
|
collectionsData = PreParsedCollections,
|
||||||
|
): IApplication {
|
||||||
validateCollectionsData(collectionsData);
|
validateCollectionsData(collectionsData);
|
||||||
const information = parseProjectInformation(processEnv);
|
const information = parseProjectInformation(processEnv);
|
||||||
const collections = collectionsData.map((collection) => parser(collection, information));
|
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||||
@@ -22,11 +23,13 @@ export function parseApplication(
|
|||||||
export type CategoryCollectionParserType
|
export type CategoryCollectionParserType
|
||||||
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
||||||
|
|
||||||
const CategoryCollectionParser: CategoryCollectionParserType
|
const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
|
||||||
= (file, info) => parseCategoryCollection(file, info);
|
return parseCategoryCollection(file, info);
|
||||||
|
};
|
||||||
|
|
||||||
const PreParsedCollections: readonly CollectionData []
|
const PreParsedCollections: readonly CollectionData [] = [
|
||||||
= [ WindowsData, MacOsData ];
|
WindowsData, MacOsData,
|
||||||
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
if (!collections.length) {
|
if (!collections.length) {
|
||||||
|
|||||||
@@ -1,32 +1,29 @@
|
|||||||
import { Category } from '@/domain/Category';
|
|
||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||||
import { parseCategory } from './CategoryParser';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { createEnumParser } from '../Common/Enum';
|
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
|
import { createEnumParser } from '../Common/Enum';
|
||||||
|
import { parseCategory } from './CategoryParser';
|
||||||
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||||
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
|
|
||||||
export function parseCategoryCollection(
|
export function parseCategoryCollection(
|
||||||
content: CollectionData,
|
content: CollectionData,
|
||||||
info: IProjectInformation,
|
info: IProjectInformation,
|
||||||
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
osParser = createEnumParser(OperatingSystem),
|
||||||
|
): ICategoryCollection {
|
||||||
validate(content);
|
validate(content);
|
||||||
const scripting = new ScriptingDefinitionParser()
|
const scripting = new ScriptingDefinitionParser()
|
||||||
.parse(content.scripting, info);
|
.parse(content.scripting, info);
|
||||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||||
const categories = new Array<Category>();
|
const categories = content.actions.map((action) => parseCategory(action, context));
|
||||||
for (const action of content.actions) {
|
|
||||||
const category = parseCategory(action, context);
|
|
||||||
categories.push(category);
|
|
||||||
}
|
|
||||||
const os = osParser.parseEnum(content.os, 'os');
|
const os = osParser.parseEnum(content.os, 'os');
|
||||||
const collection = new CategoryCollection(
|
const collection = new CategoryCollection(
|
||||||
os,
|
os,
|
||||||
categories,
|
categories,
|
||||||
scripting);
|
scripting,
|
||||||
|
);
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { CategoryData, ScriptData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
|
import {
|
||||||
|
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
|
||||||
|
} from 'js-yaml-loader!@/*';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||||
import { parseScript } from './Script/ScriptParser';
|
import { parseScript } from './Script/ScriptParser';
|
||||||
|
|
||||||
let categoryIdCounter: number = 0;
|
let categoryIdCounter = 0;
|
||||||
|
|
||||||
interface ICategoryChildren {
|
export function parseCategory(
|
||||||
subCategories: Category[];
|
category: CategoryData,
|
||||||
subScripts: Script[];
|
context: ICategoryCollectionParseContext,
|
||||||
}
|
): Category {
|
||||||
|
|
||||||
export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category {
|
|
||||||
if (!context) { throw new Error('undefined context'); }
|
if (!context) { throw new Error('undefined context'); }
|
||||||
ensureValid(category);
|
ensureValid(category);
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
@@ -23,11 +23,11 @@ export function parseCategory(category: CategoryData, context: ICategoryCollecti
|
|||||||
parseCategoryChild(data, children, category, context);
|
parseCategoryChild(data, children, category, context);
|
||||||
}
|
}
|
||||||
return new Category(
|
return new Category(
|
||||||
/*id*/ categoryIdCounter++,
|
/* id: */ categoryIdCounter++,
|
||||||
/*name*/ category.category,
|
/* name: */ category.category,
|
||||||
/*docs*/ parseDocUrls(category),
|
/* docs: */ parseDocUrls(category),
|
||||||
/*categories*/ children.subCategories,
|
/* categories: */ children.subCategories,
|
||||||
/*scripts*/ children.subScripts,
|
/* scripts: */ children.subScripts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,11 +43,17 @@ function ensureValid(category: CategoryData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ICategoryChildren {
|
||||||
|
subCategories: Category[];
|
||||||
|
subScripts: Script[];
|
||||||
|
}
|
||||||
|
|
||||||
function parseCategoryChild(
|
function parseCategoryChild(
|
||||||
data: CategoryOrScriptData,
|
data: CategoryOrScriptData,
|
||||||
children: ICategoryChildren,
|
children: ICategoryChildren,
|
||||||
parent: CategoryData,
|
parent: CategoryData,
|
||||||
context: ICategoryCollectionParseContext) {
|
context: ICategoryCollectionParseContext,
|
||||||
|
) {
|
||||||
if (isCategory(data)) {
|
if (isCategory(data)) {
|
||||||
const subCategory = parseCategory(data as CategoryData, context);
|
const subCategory = parseCategory(data as CategoryData, context);
|
||||||
children.subCategories.push(subCategory);
|
children.subCategories.push(subCategory);
|
||||||
@@ -61,11 +67,20 @@ function parseCategoryChild(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isScript(data: any): boolean {
|
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||||
return (data.code && data.code.length > 0)
|
const holder = (data as InstructionHolder);
|
||||||
|| data.call;
|
return hasCode(holder) || hasCall(holder);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCategory(data: any): boolean {
|
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||||
return data.category && data.category.length > 0;
|
const { category } = data as CategoryData;
|
||||||
|
return category && category.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCode(holder: InstructionHolder): boolean {
|
||||||
|
return holder.code && holder.code.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCall(holder: InstructionHolder) {
|
||||||
|
return holder.call !== undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<stri
|
|||||||
if (!documentable) {
|
if (!documentable) {
|
||||||
throw new Error('documentable is null or undefined');
|
throw new Error('documentable is null or undefined');
|
||||||
}
|
}
|
||||||
const docs = documentable.docs;
|
const { docs } = documentable;
|
||||||
if (!docs || !docs.length) {
|
if (!docs || !docs.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,10 @@ export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<stri
|
|||||||
return result.getAll();
|
return result.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer {
|
function addDocs(
|
||||||
|
docs: DocumentationUrlsData,
|
||||||
|
urls: DocumentationUrlContainer,
|
||||||
|
): DocumentationUrlContainer {
|
||||||
if (docs instanceof Array) {
|
if (docs instanceof Array) {
|
||||||
urls.addUrls(docs);
|
urls.addUrls(docs);
|
||||||
} else if (typeof docs === 'string') {
|
} else if (typeof docs === 'string') {
|
||||||
@@ -32,7 +35,7 @@ class DocumentationUrlContainer {
|
|||||||
this.urls.push(url);
|
this.urls.push(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addUrls(urls: readonly any[]) {
|
public addUrls(urls: readonly string[]) {
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
if (typeof url !== 'string') {
|
if (typeof url !== 'string') {
|
||||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
throw new Error('Docs field (documentation url) must be an array of strings');
|
||||||
@@ -53,8 +56,8 @@ function validateUrl(docUrl: string): void {
|
|||||||
if (docUrl.includes('\n')) {
|
if (docUrl.includes('\n')) {
|
||||||
throw new Error('Documentation url cannot be multi-lined.');
|
throw new Error('Documentation url cannot be multi-lined.');
|
||||||
}
|
}
|
||||||
const res = docUrl.match(
|
const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
|
||||||
/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g);
|
const res = docUrl.match(validUrlRegex);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
throw new Error(`Invalid documentation url: ${docUrl}`);
|
throw new Error(`Invalid documentation url: ${docUrl}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
|
|||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
|
|
||||||
export function parseProjectInformation(
|
export function parseProjectInformation(
|
||||||
environment: NodeJS.ProcessEnv): IProjectInformation {
|
environment: NodeJS.ProcessEnv,
|
||||||
|
): IProjectInformation {
|
||||||
return new ProjectInformation(
|
return new ProjectInformation(
|
||||||
environment.VUE_APP_NAME,
|
environment.VUE_APP_NAME,
|
||||||
environment.VUE_APP_VERSION,
|
environment.VUE_APP_VERSION,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { FunctionData } from 'js-yaml-loader!@/*';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
import { FunctionData } from 'js-yaml-loader!*';
|
|
||||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
@@ -9,12 +9,14 @@ import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
|||||||
|
|
||||||
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||||
public readonly compiler: IScriptCompiler;
|
public readonly compiler: IScriptCompiler;
|
||||||
|
|
||||||
public readonly syntax: ILanguageSyntax;
|
public readonly syntax: ILanguageSyntax;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||||
scripting: IScriptingDefinition,
|
scripting: IScriptingDefinition,
|
||||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) {
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||||
|
) {
|
||||||
if (!scripting) { throw new Error('undefined scripting'); }
|
if (!scripting) { throw new Error('undefined scripting'); }
|
||||||
this.syntax = syntaxFactory.create(scripting.language);
|
this.syntax = syntaxFactory.create(scripting.language);
|
||||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
|
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||||
|
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
|
import { IExpression } from './IExpression';
|
||||||
import { ExpressionPosition } from './ExpressionPosition';
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
import { ExpressionArguments, IExpression } from './IExpression';
|
import { ExpressionEvaluationContext, IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||||
|
|
||||||
export type ExpressionEvaluator = (args?: ExpressionArguments) => string;
|
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||||
export class Expression implements IExpression {
|
export class Expression implements IExpression {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly position: ExpressionPosition,
|
public readonly position: ExpressionPosition,
|
||||||
public readonly evaluator: ExpressionEvaluator,
|
public readonly evaluator: ExpressionEvaluator,
|
||||||
public readonly parameters: readonly string[] = new Array<string>()) {
|
public readonly parameters
|
||||||
|
: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(),
|
||||||
|
) {
|
||||||
if (!position) {
|
if (!position) {
|
||||||
throw new Error('undefined position');
|
throw new Error('undefined position');
|
||||||
}
|
}
|
||||||
@@ -14,22 +21,43 @@ export class Expression implements IExpression {
|
|||||||
throw new Error('undefined evaluator');
|
throw new Error('undefined evaluator');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public evaluate(args?: ExpressionArguments): string {
|
|
||||||
args = filterUnusedArguments(this.parameters, args);
|
public evaluate(context: IExpressionEvaluationContext): string {
|
||||||
return this.evaluator(args);
|
if (!context) {
|
||||||
|
throw new Error('undefined context');
|
||||||
|
}
|
||||||
|
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||||
|
const args = filterUnusedArguments(this.parameters, context.args);
|
||||||
|
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||||
|
return this.evaluator(filteredContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateThatAllRequiredParametersAreSatisfied(
|
||||||
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
) {
|
||||||
|
const requiredParameterNames = parameters
|
||||||
|
.all
|
||||||
|
.filter((parameter) => !parameter.isOptional)
|
||||||
|
.map((parameter) => parameter.name);
|
||||||
|
const missingParameterNames = requiredParameterNames
|
||||||
|
.filter((parameterName) => !args.hasArgument(parameterName));
|
||||||
|
if (missingParameterNames.length) {
|
||||||
|
throw new Error(
|
||||||
|
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterUnusedArguments(
|
function filterUnusedArguments(
|
||||||
parameters: readonly string[], args: ExpressionArguments): ExpressionArguments {
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
let result: ExpressionArguments = {};
|
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
for (const parameter of Object.keys(args)) {
|
): IReadOnlyFunctionCallArgumentCollection {
|
||||||
if (parameters.includes(parameter)) {
|
const specificCallArgs = new FunctionCallArgumentCollection();
|
||||||
result = {
|
parameters.all
|
||||||
...result,
|
.filter((parameter) => allFunctionArgs.hasArgument(parameter.name))
|
||||||
[parameter]: args[parameter],
|
.map((parameter) => allFunctionArgs.getArgument(parameter.name))
|
||||||
};
|
.forEach((argument) => specificCallArgs.addArgument(argument));
|
||||||
}
|
return specificCallArgs;
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
import { IPipelineCompiler } from '../Pipes/IPipelineCompiler';
|
||||||
|
import { PipelineCompiler } from '../Pipes/PipelineCompiler';
|
||||||
|
|
||||||
|
export interface IExpressionEvaluationContext {
|
||||||
|
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||||
|
readonly pipelineCompiler: IPipelineCompiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
|
||||||
|
constructor(
|
||||||
|
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
||||||
|
) {
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('undefined args, send empty collection instead');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
export class ExpressionPosition {
|
export class ExpressionPosition {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly start: number,
|
public readonly start: number,
|
||||||
public readonly end: number) {
|
public readonly end: number,
|
||||||
|
) {
|
||||||
if (start === end) {
|
if (start === end) {
|
||||||
throw new Error(`no length (start = end = ${start})`);
|
throw new Error(`no length (start = end = ${start})`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
|
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||||
import { ExpressionPosition } from './ExpressionPosition';
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
|
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||||
|
|
||||||
export interface IExpression {
|
export interface IExpression {
|
||||||
readonly position: ExpressionPosition;
|
readonly position: ExpressionPosition;
|
||||||
readonly parameters?: readonly string[];
|
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
evaluate(args?: ExpressionArguments): string;
|
evaluate(context: IExpressionEvaluationContext): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExpressionArguments {
|
|
||||||
readonly [parameter: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,48 @@
|
|||||||
import { IExpressionsCompiler, ParameterValueDictionary } from './IExpressionsCompiler';
|
import { IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
import { IExpressionsCompiler } from './IExpressionsCompiler';
|
||||||
import { IExpression } from './Expression/IExpression';
|
import { IExpression } from './Expression/IExpression';
|
||||||
import { IExpressionParser } from './Parser/IExpressionParser';
|
import { IExpressionParser } from './Parser/IExpressionParser';
|
||||||
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
||||||
|
|
||||||
export class ExpressionsCompiler implements IExpressionsCompiler {
|
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||||
public constructor(private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
|
public constructor(
|
||||||
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string {
|
private readonly extractor: IExpressionParser = new CompositeExpressionParser(),
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public compileExpressions(
|
||||||
|
code: string | undefined,
|
||||||
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
): string {
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('undefined args, send empty collection instead');
|
||||||
|
}
|
||||||
|
if (!code) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
const expressions = this.extractor.findExpressions(code);
|
const expressions = this.extractor.findExpressions(code);
|
||||||
const requiredParameterNames = expressions.map((e) => e.parameters).filter((p) => p).flat();
|
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
||||||
const uniqueParameterNames = Array.from(new Set(requiredParameterNames));
|
const context = new ExpressionEvaluationContext(args);
|
||||||
ensureRequiredArgsProvided(uniqueParameterNames, parameters);
|
const compiledCode = compileExpressions(expressions, code, context);
|
||||||
return compileExpressions(expressions, code, parameters);
|
return compiledCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileExpressions(expressions: IExpression[], code: string, parameters?: ParameterValueDictionary) {
|
function compileExpressions(
|
||||||
|
expressions: readonly IExpression[],
|
||||||
|
code: string,
|
||||||
|
context: IExpressionEvaluationContext,
|
||||||
|
) {
|
||||||
let compiledCode = '';
|
let compiledCode = '';
|
||||||
expressions = expressions
|
const sortedExpressions = expressions
|
||||||
.slice() // copy the array to not mutate the parameter
|
.slice() // copy the array to not mutate the parameter
|
||||||
.sort((a, b) => b.position.start - a.position.start);
|
.sort((a, b) => b.position.start - a.position.start);
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (index !== code.length) {
|
while (index !== code.length) {
|
||||||
const nextExpression = expressions.pop();
|
const nextExpression = sortedExpressions.pop();
|
||||||
if (nextExpression) {
|
if (nextExpression) {
|
||||||
compiledCode += code.substring(index, nextExpression.position.start);
|
compiledCode += code.substring(index, nextExpression.position.start);
|
||||||
const expressionCode = nextExpression.evaluate(parameters);
|
const expressionCode = nextExpression.evaluate(context);
|
||||||
compiledCode += expressionCode;
|
compiledCode += expressionCode;
|
||||||
index = nextExpression.position.end;
|
index = nextExpression.position.end;
|
||||||
} else {
|
} else {
|
||||||
@@ -35,15 +53,30 @@ function compileExpressions(expressions: IExpression[], code: string, parameters
|
|||||||
return compiledCode;
|
return compiledCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureRequiredArgsProvided(parameters: readonly string[], args: ParameterValueDictionary) {
|
function extractRequiredParameterNames(
|
||||||
parameters = parameters || [];
|
expressions: readonly IExpression[],
|
||||||
args = args || {};
|
): string[] {
|
||||||
if (!parameters.length) {
|
return expressions
|
||||||
|
.map((e) => e.parameters.all
|
||||||
|
.filter((p) => !p.isOptional)
|
||||||
|
.map((p) => p.name))
|
||||||
|
.filter(Boolean) // Remove empty or undefined
|
||||||
|
.flat()
|
||||||
|
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureParamsUsedInCodeHasArgsProvided(
|
||||||
|
expressions: readonly IExpression[],
|
||||||
|
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
): void {
|
||||||
|
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||||
|
if (!usedParameterNames?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const notProvidedParameters = parameters.filter((parameter) => !Boolean(args[parameter]));
|
const notProvidedParameters = usedParameterNames
|
||||||
|
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
|
||||||
if (notProvidedParameters.length) {
|
if (notProvidedParameters.length) {
|
||||||
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)}`);
|
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export interface ParameterValueDictionary { [parameterName: string]: string; }
|
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
export interface IExpressionsCompiler {
|
export interface IExpressionsCompiler {
|
||||||
compileExpressions(code: string, parameters?: ParameterValueDictionary): string;
|
compileExpressions(
|
||||||
|
code: string | undefined,
|
||||||
|
args: IReadOnlyFunctionCallArgumentCollection): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { IExpression } from '../Expression/IExpression';
|
import { IExpression } from '../Expression/IExpression';
|
||||||
import { IExpressionParser } from './IExpressionParser';
|
|
||||||
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
||||||
|
import { WithParser } from '../SyntaxParsers/WithParser';
|
||||||
|
import { IExpressionParser } from './IExpressionParser';
|
||||||
|
|
||||||
const parsers = [
|
const Parsers = [
|
||||||
new ParameterSubstitutionParser(),
|
new ParameterSubstitutionParser(),
|
||||||
|
new WithParser(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export class CompositeExpressionParser implements IExpressionParser {
|
export class CompositeExpressionParser implements IExpressionParser {
|
||||||
public constructor(private readonly leafs: readonly IExpressionParser[] = parsers) {
|
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||||
if (leafs.some((leaf) => !leaf)) { throw new Error('undefined leaf'); }
|
if (leafs.some((leaf) => !leaf)) {
|
||||||
|
throw new Error('undefined leaf');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
const expressions = new Array<IExpression>();
|
return this.leafs.flatMap(
|
||||||
for (const parser of this.leafs) {
|
(parser) => parser.findExpressions(code) || [],
|
||||||
const newExpressions = parser.findExpressions(code);
|
);
|
||||||
if (newExpressions && newExpressions.length) {
|
|
||||||
expressions.push(...newExpressions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return expressions;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
export class ExpressionRegexBuilder {
|
||||||
|
private readonly parts = new Array<string>();
|
||||||
|
|
||||||
|
public expectCharacters(characters: string) {
|
||||||
|
return this.addRawRegex(
|
||||||
|
characters
|
||||||
|
.replaceAll('$', '\\$')
|
||||||
|
.replaceAll('.', '\\.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public expectOneOrMoreWhitespaces() {
|
||||||
|
return this
|
||||||
|
.addRawRegex('\\s+');
|
||||||
|
}
|
||||||
|
|
||||||
|
public matchPipeline() {
|
||||||
|
return this
|
||||||
|
.expectZeroOrMoreWhitespaces()
|
||||||
|
.addRawRegex('(\\|\\s*.+?)?');
|
||||||
|
}
|
||||||
|
|
||||||
|
public matchUntilFirstWhitespace() {
|
||||||
|
return this
|
||||||
|
.addRawRegex('([^|\\s]+)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public matchAnythingExceptSurroundingWhitespaces() {
|
||||||
|
return this
|
||||||
|
.expectZeroOrMoreWhitespaces()
|
||||||
|
.addRawRegex('(.+?)')
|
||||||
|
.expectZeroOrMoreWhitespaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
public expectExpressionStart() {
|
||||||
|
return this
|
||||||
|
.expectCharacters('{{')
|
||||||
|
.expectZeroOrMoreWhitespaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
public expectExpressionEnd() {
|
||||||
|
return this
|
||||||
|
.expectZeroOrMoreWhitespaces()
|
||||||
|
.expectCharacters('}}');
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildRegExp(): RegExp {
|
||||||
|
return new RegExp(this.parts.join(''), 'g');
|
||||||
|
}
|
||||||
|
|
||||||
|
private expectZeroOrMoreWhitespaces() {
|
||||||
|
return this
|
||||||
|
.addRawRegex('\\s*');
|
||||||
|
}
|
||||||
|
|
||||||
|
private addRawRegex(regex: string) {
|
||||||
|
this.parts.push(regex);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { IExpressionParser } from '../IExpressionParser';
|
||||||
|
import { ExpressionPosition } from '../../Expression/ExpressionPosition';
|
||||||
|
import { IExpression } from '../../Expression/IExpression';
|
||||||
|
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
|
||||||
|
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
||||||
|
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||||
|
|
||||||
|
export abstract class RegexParser implements IExpressionParser {
|
||||||
|
protected abstract readonly regex: RegExp;
|
||||||
|
|
||||||
|
public findExpressions(code: string): IExpression[] {
|
||||||
|
return Array.from(this.findRegexExpressions(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
||||||
|
|
||||||
|
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('undefined code');
|
||||||
|
}
|
||||||
|
const matches = code.matchAll(this.regex);
|
||||||
|
for (const match of matches) {
|
||||||
|
const primitiveExpression = this.buildExpression(match);
|
||||||
|
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
|
||||||
|
const parameters = createParameters(primitiveExpression);
|
||||||
|
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||||
|
yield expression;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private doOrRethrow<T>(action: () => T, errorText: string, code: string): T {
|
||||||
|
try {
|
||||||
|
return action();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`[${this.constructor.name}] ${errorText}: ${error.message}\nRegex: ${this.regex}\nCode: ${code}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPosition(match: RegExpMatchArray): ExpressionPosition {
|
||||||
|
const startPos = match.index;
|
||||||
|
const endPos = startPos + match[0].length;
|
||||||
|
return new ExpressionPosition(startPos, endPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createParameters(
|
||||||
|
expression: IPrimitiveExpression,
|
||||||
|
): FunctionParameterCollection {
|
||||||
|
return (expression.parameters || [])
|
||||||
|
.reduce((parameters, parameter) => {
|
||||||
|
parameters.addParameter(parameter);
|
||||||
|
return parameters;
|
||||||
|
}, new FunctionParameterCollection());
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPrimitiveExpression {
|
||||||
|
evaluator: ExpressionEvaluator;
|
||||||
|
parameters?: readonly IFunctionParameter[];
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { IExpressionParser } from './IExpressionParser';
|
|
||||||
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
|
||||||
import { IExpression } from '../Expression/IExpression';
|
|
||||||
import { Expression, ExpressionEvaluator } from '../Expression/Expression';
|
|
||||||
|
|
||||||
export abstract class RegexParser implements IExpressionParser {
|
|
||||||
protected abstract readonly regex: RegExp;
|
|
||||||
public findExpressions(code: string): IExpression[] {
|
|
||||||
return Array.from(this.findRegexExpressions(code));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
|
||||||
|
|
||||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
|
||||||
const matches = Array.from(code.matchAll(this.regex));
|
|
||||||
for (const match of matches) {
|
|
||||||
const startPos = match.index;
|
|
||||||
const endPos = startPos + match[0].length;
|
|
||||||
let position: ExpressionPosition;
|
|
||||||
try {
|
|
||||||
position = new ExpressionPosition(startPos, endPos);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
|
|
||||||
}
|
|
||||||
const primitiveExpression = this.buildExpression(match);
|
|
||||||
const expression = new Expression(position, primitiveExpression.evaluator, primitiveExpression.parameters);
|
|
||||||
yield expression;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPrimitiveExpression {
|
|
||||||
evaluator: ExpressionEvaluator;
|
|
||||||
parameters?: readonly string[];
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IPipe {
|
||||||
|
readonly name: string;
|
||||||
|
apply(input: string): string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface IPipelineCompiler {
|
||||||
|
compile(value: string, pipeline: string): string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { IPipe } from '../IPipe';
|
||||||
|
|
||||||
|
export class EscapeDoubleQuotes implements IPipe {
|
||||||
|
public readonly name: string = 'escapeDoubleQuotes';
|
||||||
|
|
||||||
|
public apply(raw: string): string {
|
||||||
|
return raw?.replaceAll('"', '"^""');
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
/*
|
||||||
|
"^"" is the most robust and stable choice.
|
||||||
|
Other options:
|
||||||
|
""
|
||||||
|
Breaks, because it is fundamentally unsupported
|
||||||
|
""""
|
||||||
|
Does not work with consecutive double quotes.
|
||||||
|
E.g. `PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"`
|
||||||
|
Works when using: `PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"`
|
||||||
|
\"
|
||||||
|
May break as they are interpreted by cmd.exe as metacharacters breaking the command
|
||||||
|
E.g. `PowerShell -Command "Write-Host 'Hello \"w&orld\"'"` does not work due to unescaped "&"
|
||||||
|
Works when using: `PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"`
|
||||||
|
\""
|
||||||
|
Normalizes interior whitespace
|
||||||
|
E.g. `PowerShell -Command "\""a& c\"".length"`, outputs 4 and discards one of two whitespaces
|
||||||
|
Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"`
|
||||||
|
A good explanation: https://stackoverflow.com/a/31413730
|
||||||
|
*/
|
||||||
|
/* eslint-enable max-len */
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { IPipe } from '../IPipe';
|
||||||
|
|
||||||
|
export class InlinePowerShell implements IPipe {
|
||||||
|
public readonly name: string = 'inlinePowerShell';
|
||||||
|
|
||||||
|
public apply(code: string): string {
|
||||||
|
if (!code || !hasLines(code)) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
|
||||||
|
inlineComments,
|
||||||
|
mergeLinesWithBacktick,
|
||||||
|
mergeHereStrings,
|
||||||
|
mergeNewLines,
|
||||||
|
]).reduce((a, b) => (data) => b(a(data)));
|
||||||
|
const newCode = processor(code);
|
||||||
|
return newCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasLines(text: string) {
|
||||||
|
return text.includes('\n') || text.includes('\r');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
|
||||||
|
Otherwise single # comments out rest of the code
|
||||||
|
*/
|
||||||
|
function inlineComments(code: string): string {
|
||||||
|
const makeInlineComment = (comment: string) => {
|
||||||
|
const value = comment?.trim();
|
||||||
|
if (!value) {
|
||||||
|
return '<##>';
|
||||||
|
}
|
||||||
|
return `<# ${value} #>`;
|
||||||
|
};
|
||||||
|
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
||||||
|
if (captureComment === undefined) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
return makeInlineComment(captureComment);
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
Other alternatives considered:
|
||||||
|
--------------------------
|
||||||
|
/#(?<!<#)(?![<>])(.*)$/gm
|
||||||
|
-------------------------
|
||||||
|
✅ Simple, yet matches and captures only what's necessary
|
||||||
|
❌ Fails to match some cases
|
||||||
|
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
||||||
|
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
||||||
|
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one`
|
||||||
|
❌ Uses lookbehind
|
||||||
|
Safari does not yet support lookbehind and syntax, leading application to not
|
||||||
|
load and throw "Invalid regular expression: invalid group specifier name"
|
||||||
|
https://caniuse.com/js-regexp-lookbehind
|
||||||
|
⏩ Usage
|
||||||
|
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (match, captureComment) => {
|
||||||
|
return makeInlineComment(captureComment)
|
||||||
|
});
|
||||||
|
----------------
|
||||||
|
/<#.*?#>|#(.*)/g
|
||||||
|
----------------
|
||||||
|
✅ Simple yet affective
|
||||||
|
❌ Matches all comments, but only captures dash comments
|
||||||
|
❌ Fails to match some cases
|
||||||
|
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
||||||
|
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
||||||
|
⏩ Usage
|
||||||
|
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
||||||
|
if (captureComment === undefined) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
return makeInlineComment(captureComment);
|
||||||
|
});
|
||||||
|
------------------------------------
|
||||||
|
/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
|
||||||
|
------------------------------------
|
||||||
|
✅ Covers all cases
|
||||||
|
❌ Matches every line, three capture groups are used to build result
|
||||||
|
⏩ Usage
|
||||||
|
return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
|
||||||
|
(match, captureLeft, captureDash, captureComment) => {
|
||||||
|
if (!captureDash) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
return captureLeft + makeInlineComment(captureComment);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLines(code: string): string[] {
|
||||||
|
return (code?.split(/\r\n|\r|\n/) || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||||
|
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
|
||||||
|
*/
|
||||||
|
function mergeHereStrings(code: string) {
|
||||||
|
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
||||||
|
return code.replaceAll(regex, (_$, quotes, scope) => {
|
||||||
|
const newString = getHereStringHandler(quotes);
|
||||||
|
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
||||||
|
const lines = getLines(escaped);
|
||||||
|
const inlined = lines.join(newString.separator);
|
||||||
|
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
||||||
|
return quoted;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
interface IInlinedHereString {
|
||||||
|
readonly quotesAround: string;
|
||||||
|
readonly escapedQuotes: string;
|
||||||
|
readonly separator: string;
|
||||||
|
}
|
||||||
|
function getHereStringHandler(quotes: string): IInlinedHereString {
|
||||||
|
/*
|
||||||
|
We handle @' and @" differently.
|
||||||
|
Single quotes are interpreted literally and doubles are expandable.
|
||||||
|
*/
|
||||||
|
const expandableNewLine = '`r`n';
|
||||||
|
switch (quotes) {
|
||||||
|
case '\'':
|
||||||
|
return {
|
||||||
|
quotesAround: '\'',
|
||||||
|
escapedQuotes: '\'\'',
|
||||||
|
separator: `'+"${expandableNewLine}"+'`,
|
||||||
|
};
|
||||||
|
case '"':
|
||||||
|
return {
|
||||||
|
quotesAround: '"',
|
||||||
|
escapedQuotes: '`"',
|
||||||
|
separator: expandableNewLine,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`expected quotes: ${quotes}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Input ->
|
||||||
|
Get-Service * `
|
||||||
|
Sort-Object StartType `
|
||||||
|
Format-Table Name, ServiceType, Status -AutoSize
|
||||||
|
Output ->
|
||||||
|
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
|
||||||
|
*/
|
||||||
|
function mergeLinesWithBacktick(code: string) {
|
||||||
|
/*
|
||||||
|
The regex actually wraps any whitespace character after backtick and before newline
|
||||||
|
However, this is not always the case for PowerShell.
|
||||||
|
I see two behaviors:
|
||||||
|
1. If inside string, it's accepted (inside " or ')
|
||||||
|
2. If part of a command, PowerShell throws "An empty pipe element is not allowed"
|
||||||
|
However we don't need to be so robust and handle this complexity (yet), so for easier regex
|
||||||
|
we wrap it anyway
|
||||||
|
*/
|
||||||
|
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeNewLines(code: string) {
|
||||||
|
return getLines(code)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0)
|
||||||
|
.join('; ');
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { IPipe } from './IPipe';
|
||||||
|
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
||||||
|
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
||||||
|
|
||||||
|
const RegisteredPipes = [
|
||||||
|
new EscapeDoubleQuotes(),
|
||||||
|
new InlinePowerShell(),
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface IPipeFactory {
|
||||||
|
get(pipeName: string): IPipe;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PipeFactory implements IPipeFactory {
|
||||||
|
private readonly pipes = new Map<string, IPipe>();
|
||||||
|
|
||||||
|
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||||
|
if (pipes.some((pipe) => !pipe)) {
|
||||||
|
throw new Error('undefined pipe in list');
|
||||||
|
}
|
||||||
|
for (const pipe of pipes) {
|
||||||
|
this.registerPipe(pipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(pipeName: string): IPipe {
|
||||||
|
validatePipeName(pipeName);
|
||||||
|
if (!this.pipes.has(pipeName)) {
|
||||||
|
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||||
|
}
|
||||||
|
return this.pipes.get(pipeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerPipe(pipe: IPipe): void {
|
||||||
|
validatePipeName(pipe.name);
|
||||||
|
if (this.pipes.has(pipe.name)) {
|
||||||
|
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
||||||
|
}
|
||||||
|
this.pipes.set(pipe.name, pipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePipeName(name: string) {
|
||||||
|
if (!name) {
|
||||||
|
throw new Error('empty pipe name');
|
||||||
|
}
|
||||||
|
if (!/^[a-z][A-Za-z]*$/.test(name)) {
|
||||||
|
throw new Error(`Pipe name should be camelCase: "${name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { IPipeFactory, PipeFactory } from './PipeFactory';
|
||||||
|
import { IPipelineCompiler } from './IPipelineCompiler';
|
||||||
|
|
||||||
|
export class PipelineCompiler implements IPipelineCompiler {
|
||||||
|
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
||||||
|
|
||||||
|
public compile(value: string, pipeline: string): string {
|
||||||
|
ensureValidArguments(value, pipeline);
|
||||||
|
const pipeNames = extractPipeNames(pipeline);
|
||||||
|
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
||||||
|
return pipes.reduce((previousValue, pipe) => {
|
||||||
|
return pipe.apply(previousValue);
|
||||||
|
}, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPipeNames(pipeline: string): string[] {
|
||||||
|
return pipeline
|
||||||
|
.trim()
|
||||||
|
.split('|')
|
||||||
|
.slice(1)
|
||||||
|
.map((p) => p.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidArguments(value: string, pipeline: string) {
|
||||||
|
if (!value) { throw new Error('undefined value'); }
|
||||||
|
if (!pipeline) { throw new Error('undefined pipeline'); }
|
||||||
|
if (!pipeline.trimStart().startsWith('|')) {
|
||||||
|
throw new Error('pipeline does not start with pipe');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,28 @@
|
|||||||
import { RegexParser, IPrimitiveExpression } from '../Parser/RegexParser';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
|
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
export class ParameterSubstitutionParser extends RegexParser {
|
export class ParameterSubstitutionParser extends RegexParser {
|
||||||
protected readonly regex = /{{\s*\$\s*([^}| ]+)\s*}}/g;
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('$')
|
||||||
|
.matchUntilFirstWhitespace() // First match: Parameter name
|
||||||
|
.matchPipeline() // Second match: Pipeline
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.buildRegExp();
|
||||||
|
|
||||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||||
const parameterName = match[1];
|
const parameterName = match[1];
|
||||||
|
const pipeline = match[2];
|
||||||
return {
|
return {
|
||||||
parameters: [ parameterName ],
|
parameters: [new FunctionParameter(parameterName, false)],
|
||||||
evaluator: (args) => args[parameterName],
|
evaluator: (context) => {
|
||||||
|
const { argumentValue } = context.args.getArgument(parameterName);
|
||||||
|
if (!pipeline) {
|
||||||
|
return argumentValue;
|
||||||
|
}
|
||||||
|
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
|
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
|
export class WithParser extends RegexParser {
|
||||||
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
|
// {{ with $parameterName }}
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('with')
|
||||||
|
.expectOneOrMoreWhitespaces()
|
||||||
|
.expectCharacters('$')
|
||||||
|
.matchUntilFirstWhitespace() // First match: parameter name
|
||||||
|
.expectExpressionEnd()
|
||||||
|
// ...
|
||||||
|
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||||
|
// {{ end }}
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('end')
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.buildRegExp();
|
||||||
|
|
||||||
|
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||||
|
const parameterName = match[1];
|
||||||
|
const scopeText = match[2];
|
||||||
|
return {
|
||||||
|
parameters: [new FunctionParameter(parameterName, true)],
|
||||||
|
evaluator: (context) => {
|
||||||
|
const argumentValue = context.args.hasArgument(parameterName)
|
||||||
|
? context.args.getArgument(parameterName).argumentValue
|
||||||
|
: undefined;
|
||||||
|
if (!argumentValue) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
||||||
|
if (!pipeline) {
|
||||||
|
return argumentValue;
|
||||||
|
}
|
||||||
|
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
||||||
|
// {{ . | pipeName }}
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('.')
|
||||||
|
.matchPipeline() // First match: pipeline
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.buildRegExp();
|
||||||
|
|
||||||
|
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
||||||
|
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
|
||||||
|
// but instead letting the pipeline compiler to fail on those.
|
||||||
|
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
|
||||||
|
return replacer(match1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
|
||||||
|
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
|
||||||
|
export class FunctionCallArgument implements IFunctionCallArgument {
|
||||||
|
constructor(
|
||||||
|
public readonly parameterName: string,
|
||||||
|
public readonly argumentValue: string,
|
||||||
|
) {
|
||||||
|
ensureValidParameterName(parameterName);
|
||||||
|
if (!argumentValue) {
|
||||||
|
throw new Error(`undefined argument value for "${parameterName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
|
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
|
||||||
|
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||||
|
|
||||||
|
public addArgument(argument: IFunctionCallArgument): void {
|
||||||
|
if (!argument) {
|
||||||
|
throw new Error('undefined argument');
|
||||||
|
}
|
||||||
|
if (this.hasArgument(argument.parameterName)) {
|
||||||
|
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
||||||
|
}
|
||||||
|
this.arguments.set(argument.parameterName, argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllParameterNames(): string[] {
|
||||||
|
return Array.from(this.arguments.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasArgument(parameterName: string): boolean {
|
||||||
|
if (!parameterName) {
|
||||||
|
throw new Error('undefined parameter name');
|
||||||
|
}
|
||||||
|
return this.arguments.has(parameterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getArgument(parameterName: string): IFunctionCallArgument {
|
||||||
|
if (!parameterName) {
|
||||||
|
throw new Error('undefined parameter name');
|
||||||
|
}
|
||||||
|
const arg = this.arguments.get(parameterName);
|
||||||
|
if (!arg) {
|
||||||
|
throw new Error(`parameter does not exist: ${parameterName}`);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IFunctionCallArgument {
|
||||||
|
readonly parameterName: string;
|
||||||
|
readonly argumentValue: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
|
||||||
|
export interface IReadOnlyFunctionCallArgumentCollection {
|
||||||
|
getArgument(parameterName: string): IFunctionCallArgument;
|
||||||
|
getAllParameterNames(): string[];
|
||||||
|
hasArgument(parameterName: string): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
|
||||||
|
addArgument(argument: IFunctionCallArgument): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
|
||||||
|
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||||
|
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||||
|
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
|
||||||
|
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
|
||||||
|
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
|
||||||
|
import { FunctionCall } from '../FunctionCall';
|
||||||
|
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
|
||||||
|
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||||
|
import { ICompiledCode } from './ICompiledCode';
|
||||||
|
|
||||||
|
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||||
|
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public compileCall(
|
||||||
|
calls: IFunctionCall[],
|
||||||
|
functions: ISharedFunctionCollection,
|
||||||
|
): ICompiledCode {
|
||||||
|
if (!functions) { throw new Error('undefined functions'); }
|
||||||
|
if (!calls) { throw new Error('undefined calls'); }
|
||||||
|
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
|
||||||
|
const context: ICompilationContext = {
|
||||||
|
allFunctions: functions,
|
||||||
|
callSequence: calls,
|
||||||
|
expressionsCompiler: this.expressionsCompiler,
|
||||||
|
};
|
||||||
|
const code = compileCallSequence(context);
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICompilationContext {
|
||||||
|
allFunctions: ISharedFunctionCollection;
|
||||||
|
callSequence: readonly IFunctionCall[];
|
||||||
|
expressionsCompiler: IExpressionsCompiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICompiledFunctionCall {
|
||||||
|
readonly code: string;
|
||||||
|
readonly revertCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
|
||||||
|
const compiledFunctions = context.callSequence
|
||||||
|
.flatMap((call) => compileSingleCall(call, context));
|
||||||
|
return {
|
||||||
|
code: merge(compiledFunctions.map((f) => f.code)),
|
||||||
|
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileSingleCall(
|
||||||
|
call: IFunctionCall,
|
||||||
|
context: ICompilationContext,
|
||||||
|
): ICompiledFunctionCall[] {
|
||||||
|
const func = context.allFunctions.getFunctionByName(call.functionName);
|
||||||
|
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||||
|
if (func.body.code) { // Function with inline code
|
||||||
|
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
||||||
|
return [compiledCode];
|
||||||
|
}
|
||||||
|
// Function with inner calls
|
||||||
|
return func.body.calls
|
||||||
|
.map((innerCall) => {
|
||||||
|
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
||||||
|
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
|
||||||
|
return compileSingleCall(compiledCall, context);
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileCode(
|
||||||
|
code: IFunctionCode,
|
||||||
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
compiler: IExpressionsCompiler,
|
||||||
|
): ICompiledFunctionCall {
|
||||||
|
return {
|
||||||
|
code: compiler.compileExpressions(code.do, args),
|
||||||
|
revertCode: compiler.compileExpressions(code.revert, args),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileArgs(
|
||||||
|
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
compiler: IExpressionsCompiler,
|
||||||
|
): IReadOnlyFunctionCallArgumentCollection {
|
||||||
|
return argsToCompile
|
||||||
|
.getAllParameterNames()
|
||||||
|
.map((parameterName) => {
|
||||||
|
const { argumentValue } = argsToCompile.getArgument(parameterName);
|
||||||
|
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
||||||
|
return new FunctionCallArgument(parameterName, compiledValue);
|
||||||
|
})
|
||||||
|
.reduce((compiledArgs, arg) => {
|
||||||
|
compiledArgs.addArgument(arg);
|
||||||
|
return compiledArgs;
|
||||||
|
}, new FunctionCallArgumentCollection());
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge(codeParts: readonly string[]): string {
|
||||||
|
return codeParts
|
||||||
|
.filter((part) => part?.length > 0)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureThatCallArgumentsExistInParameterDefinition(
|
||||||
|
func: ISharedFunction,
|
||||||
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
): void {
|
||||||
|
const callArgumentNames = args.getAllParameterNames();
|
||||||
|
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||||
|
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||||
|
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findUnexpectedParameters(
|
||||||
|
callArgumentNames: string[],
|
||||||
|
functionParameterNames: string[],
|
||||||
|
): string[] {
|
||||||
|
if (!callArgumentNames.length && !functionParameterNames.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return callArgumentNames
|
||||||
|
.filter((callParam) => !functionParameterNames.includes(callParam));
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwIfNotEmpty(
|
||||||
|
functionName: string,
|
||||||
|
unexpectedParameters: string[],
|
||||||
|
expectedParameters: string[],
|
||||||
|
) {
|
||||||
|
if (!unexpectedParameters.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
// eslint-disable-next-line prefer-template
|
||||||
|
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||||
|
+ `"${unexpectedParameters.join('", "')}"`
|
||||||
|
+ '. Expected parameter(s): '
|
||||||
|
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ICompiledCode {
|
||||||
|
readonly code: string;
|
||||||
|
readonly revertCode?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
|
import { IFunctionCall } from '../IFunctionCall';
|
||||||
|
import { ICompiledCode } from './ICompiledCode';
|
||||||
|
|
||||||
|
export interface IFunctionCallCompiler {
|
||||||
|
compileCall(
|
||||||
|
calls: IFunctionCall[],
|
||||||
|
functions: ISharedFunctionCollection): ICompiledCode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||||
|
import { IFunctionCall } from './IFunctionCall';
|
||||||
|
|
||||||
|
export class FunctionCall implements IFunctionCall {
|
||||||
|
constructor(
|
||||||
|
public readonly functionName: string,
|
||||||
|
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
) {
|
||||||
|
if (!functionName) {
|
||||||
|
throw new Error('empty function name in function call');
|
||||||
|
}
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('undefined args');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
||||||
|
import { IFunctionCall } from './IFunctionCall';
|
||||||
|
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
||||||
|
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
||||||
|
import { FunctionCall } from './FunctionCall';
|
||||||
|
|
||||||
|
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
|
||||||
|
if (calls === undefined) {
|
||||||
|
throw new Error('undefined call data');
|
||||||
|
}
|
||||||
|
const sequence = getCallSequence(calls);
|
||||||
|
return sequence.map((call) => parseFunctionCall(call));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
||||||
|
if (typeof calls !== 'object') {
|
||||||
|
throw new Error('called function(s) must be an object');
|
||||||
|
}
|
||||||
|
if (calls instanceof Array) {
|
||||||
|
return calls as FunctionCallData[];
|
||||||
|
}
|
||||||
|
return [calls as FunctionCallData];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
|
||||||
|
if (!call) {
|
||||||
|
throw new Error('undefined function call');
|
||||||
|
}
|
||||||
|
const callArgs = parseArgs(call.parameters);
|
||||||
|
return new FunctionCall(call.function, callArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(
|
||||||
|
parameters: FunctionCallParametersData,
|
||||||
|
): FunctionCallArgumentCollection {
|
||||||
|
return Object.keys(parameters || {})
|
||||||
|
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
|
||||||
|
.reduce((args, arg) => {
|
||||||
|
args.addArgument(arg);
|
||||||
|
return args;
|
||||||
|
}, new FunctionCallArgumentCollection());
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
|
export interface IFunctionCall {
|
||||||
|
readonly functionName: string;
|
||||||
|
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||||
|
}
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import { FunctionData, InstructionHolder } from 'js-yaml-loader!*';
|
|
||||||
import { SharedFunction } from './SharedFunction';
|
|
||||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
|
||||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
|
||||||
import { IFunctionCompiler } from './IFunctionCompiler';
|
|
||||||
import { IFunctionCallCompiler } from '../FunctionCall/IFunctionCallCompiler';
|
|
||||||
import { FunctionCallCompiler } from '../FunctionCall/FunctionCallCompiler';
|
|
||||||
|
|
||||||
export class FunctionCompiler implements IFunctionCompiler {
|
|
||||||
public static readonly instance: IFunctionCompiler = new FunctionCompiler();
|
|
||||||
protected constructor(
|
|
||||||
private readonly functionCallCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance) {
|
|
||||||
}
|
|
||||||
public compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection {
|
|
||||||
const collection = new SharedFunctionCollection();
|
|
||||||
if (!functions || !functions.length) {
|
|
||||||
return collection;
|
|
||||||
}
|
|
||||||
ensureValidFunctions(functions);
|
|
||||||
functions
|
|
||||||
.filter((func) => hasCode(func))
|
|
||||||
.forEach((func) => {
|
|
||||||
const shared = new SharedFunction(func.name, func.parameters, func.code, func.revertCode);
|
|
||||||
collection.addFunction(shared);
|
|
||||||
});
|
|
||||||
functions
|
|
||||||
.filter((func) => hasCall(func))
|
|
||||||
.forEach((func) => {
|
|
||||||
const code = this.functionCallCompiler.compileCall(func.call, collection);
|
|
||||||
const shared = new SharedFunction(func.name, func.parameters, code.code, code.revertCode);
|
|
||||||
collection.addFunction(shared);
|
|
||||||
});
|
|
||||||
return collection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasCode(data: FunctionData): boolean {
|
|
||||||
return Boolean(data.code);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasCall(data: FunctionData): boolean {
|
|
||||||
return Boolean(data.call);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
|
||||||
ensureNoUndefinedItem(functions);
|
|
||||||
ensureNoDuplicatesInFunctionNames(functions);
|
|
||||||
ensureNoDuplicatesInParameterNames(functions);
|
|
||||||
ensureNoDuplicateCode(functions);
|
|
||||||
ensureEitherCallOrCodeIsDefined(functions);
|
|
||||||
ensureExpectedParameterNameTypes(functions);
|
|
||||||
}
|
|
||||||
|
|
||||||
function printList(list: readonly string[]): string {
|
|
||||||
return `"${list.join('","')}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
|
|
||||||
// Ensure functions do not define both call and code
|
|
||||||
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
|
||||||
if (withBothCallAndCode.length) {
|
|
||||||
throw new Error(`both "code" and "call" are defined in ${printNames(withBothCallAndCode)}`);
|
|
||||||
}
|
|
||||||
// Ensure functions have either code or call
|
|
||||||
const hasEitherCodeOrCall = holders.filter((holder) => !hasCode(holder) && !hasCall(holder));
|
|
||||||
if (hasEitherCodeOrCall.length) {
|
|
||||||
throw new Error(`neither "code" or "call" is defined in ${printNames(hasEitherCodeOrCall)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureExpectedParameterNameTypes(functions: readonly FunctionData[]) {
|
|
||||||
const unexpectedFunctions = functions.filter((func) => func.parameters && !isArrayOfStrings(func.parameters));
|
|
||||||
if (unexpectedFunctions.length) {
|
|
||||||
throw new Error(`unexpected parameter name type in ${printNames(unexpectedFunctions)}`);
|
|
||||||
}
|
|
||||||
function isArrayOfStrings(value: any): boolean {
|
|
||||||
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function printNames(holders: readonly InstructionHolder[]) {
|
|
||||||
return printList(holders.map((holder) => holder.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
|
||||||
const duplicateFunctionNames = getDuplicates(functions
|
|
||||||
.map((func) => func.name.toLowerCase()));
|
|
||||||
if (duplicateFunctionNames.length) {
|
|
||||||
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
|
||||||
if (functions.some((func) => !func)) {
|
|
||||||
throw new Error(`some functions are undefined`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
|
|
||||||
const functionsWithParameters = functions
|
|
||||||
.filter((func) => func.parameters && func.parameters.length > 0);
|
|
||||||
for (const func of functionsWithParameters) {
|
|
||||||
const duplicateParameterNames = getDuplicates(func.parameters);
|
|
||||||
if (duplicateParameterNames.length) {
|
|
||||||
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
|
||||||
const duplicateCodes = getDuplicates(functions
|
|
||||||
.map((func) => func.code)
|
|
||||||
.filter((code) => code),
|
|
||||||
);
|
|
||||||
if (duplicateCodes.length > 0) {
|
|
||||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
|
||||||
}
|
|
||||||
const duplicateRevertCodes = getDuplicates(functions
|
|
||||||
.filter((func) => func.revertCode)
|
|
||||||
.map((func) => func.revertCode));
|
|
||||||
if (duplicateRevertCodes.length > 0) {
|
|
||||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDuplicates(texts: readonly string[]): string[] {
|
|
||||||
return texts.filter((item, index) => texts.indexOf(item) !== index);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user