Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d328f08952 | ||
|
|
bcad357017 | ||
|
|
9845a7cd68 | ||
|
|
7c632f7388 | ||
|
|
1442f62633 | ||
|
|
7f7a84e3ba | ||
|
|
dee3279f85 | ||
|
|
094dbb01b8 | ||
|
|
e299d40fa1 | ||
|
|
cb42f11b97 | ||
|
|
4531645b4c | ||
|
|
bf3426f91b | ||
|
|
3864f04218 | ||
|
|
e541a35e86 | ||
|
|
bd383ed273 | ||
|
|
949fac1a7c | ||
|
|
7ab16ecccb | ||
|
|
58cd551a30 | ||
|
|
7770a9b521 | ||
|
|
aab0f7ea46 | ||
|
|
ea41f4f503 | ||
|
|
af7219f6e1 | ||
|
|
8ccaec7af6 | ||
|
|
b2ffc90da7 | ||
|
|
72e4d0b896 | ||
|
|
5bb13e34f8 | ||
|
|
0466b86f10 | ||
|
|
ca81f68ff1 | ||
|
|
4995e49c46 | ||
|
|
77123d8c92 | ||
|
|
e72c1c13ea | ||
|
|
e775d68a9b | ||
|
|
f8e5f1a5a2 | ||
|
|
f4a74f058d | ||
|
|
80821fca07 | ||
|
|
dfd4451561 | ||
|
|
8570b02dde | ||
|
|
d6da406c61 | ||
|
|
060e789662 | ||
|
|
e40b9a3cf5 | ||
|
|
237d9944f9 | ||
|
|
79b46bf210 | ||
|
|
98a26f9ae4 | ||
|
|
dbe3c5cfb9 | ||
|
|
25d7f7b2a4 | ||
|
|
b76e99ac0f | ||
|
|
67c3677621 | ||
|
|
bab6316e76 | ||
|
|
48730bca05 | ||
|
|
698b570ee6 | ||
|
|
a3f11dff18 | ||
|
|
5e359c2fb8 | ||
|
|
2147eae687 | ||
|
|
286295128d | ||
|
|
8501495c17 | ||
|
|
888c9166fc | ||
|
|
e5f6edf405 | ||
|
|
e8a52f717d | ||
|
|
d45750428c | ||
|
|
cf55ca9e28 | ||
|
|
3e5239f7d3 | ||
|
|
7669985f8e | ||
|
|
5047c9b6e7 | ||
|
|
bd2082e8c5 | ||
|
|
8f188acd3c | ||
|
|
0303ef2fd9 | ||
|
|
cb21a970b6 | ||
|
|
203daeb4a2 | ||
|
|
60dde11311 | ||
|
|
8b930fc57c | ||
|
|
f810ed0c14 | ||
|
|
53222fd83c | ||
|
|
a1f2497381 | ||
|
|
c27172c32e | ||
|
|
6e9b65d8b1 | ||
|
|
6d301f9961 | ||
|
|
659fea7afc | ||
|
|
e0303058a3 |
@@ -5,3 +5,7 @@ end_of_line = lf
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
max_line_length = 100
|
max_line_length = 100
|
||||||
|
|
||||||
|
[{Dockerfile}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
// Vue specific rules, eslint-plugin-vue
|
// Vue specific rules, eslint-plugin-vue
|
||||||
'plugin:vue/essential',
|
'plugin:vue/vue3-recommended',
|
||||||
|
|
||||||
// Extends eslint-config-airbnb
|
// Extends eslint-config-airbnb
|
||||||
'@vue/eslint-config-airbnb-with-typescript',
|
'@vue/eslint-config-airbnb-with-typescript',
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ runs:
|
|||||||
-
|
-
|
||||||
name: Run `npm ci` with retries
|
name: Run `npm ci` with retries
|
||||||
shell: bash
|
shell: bash
|
||||||
run: npm run install-deps -- --ci --root-directory "${{ inputs.working-directory }}"
|
run: npm run install-deps -- --ci
|
||||||
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
|||||||
2
.github/actions/setup-node/action.yml
vendored
2
.github/actions/setup-node/action.yml
vendored
@@ -3,6 +3,6 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 16.x
|
||||||
|
|||||||
36
.github/workflows/checks.build.yaml
vendored
36
.github/workflows/checks.build.yaml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: build-checks
|
name: checks.build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
@@ -68,3 +68,33 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Verify bundled desktop build artifacts
|
name: Verify bundled desktop build artifacts
|
||||||
run: npm run check:verify-build-artifacts -- --electron-bundled
|
run: npm run check:verify-build-artifacts -- --electron-bundled
|
||||||
|
|
||||||
|
build-docker:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu ] # Windows runners do not support Linux containers
|
||||||
|
fail-fast: false # Allows to see results from other combinations
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Install Docker on macOS
|
||||||
|
if: matrix.os == 'macos' # macOS runner is missing Docker
|
||||||
|
run: |-
|
||||||
|
# Install Docker
|
||||||
|
brew install docker
|
||||||
|
# Docker on macOS misses daemon due to licensing, so install colima as runtime
|
||||||
|
brew install colima
|
||||||
|
# Start the daemon
|
||||||
|
colima start
|
||||||
|
-
|
||||||
|
name: Build Docker image
|
||||||
|
run: docker build -t undergroundwires/privacy.sexy:latest .
|
||||||
|
-
|
||||||
|
name: Run Docker image on port 8080
|
||||||
|
run: docker run -d -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest
|
||||||
|
-
|
||||||
|
name: Check server is up and returns HTTP 200
|
||||||
|
run: node ./scripts/verify-web-server-status.js --url http://localhost:8080
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
|||||||
2
.github/workflows/checks.external-urls.yaml
vendored
2
.github/workflows/checks.external-urls.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
|||||||
2
.github/workflows/checks.quality.yaml
vendored
2
.github/workflows/checks.quality.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
|||||||
4
.github/workflows/checks.scripts.yaml
vendored
4
.github/workflows/checks.scripts.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: security-checks
|
name: checks.security.dependencies
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
42
.github/workflows/checks.security.sast.yaml
vendored
Normal file
42
.github/workflows/checks.security.sast.yaml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: checks.security.sast
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [
|
||||||
|
javascript # analyzes code written in JavaScript, TypeScript and both.
|
||||||
|
]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
queries: +security-and-quality
|
||||||
|
-
|
||||||
|
name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
-
|
||||||
|
name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
|
with:
|
||||||
|
category: "/language:${{ matrix.language }}"
|
||||||
2
.github/workflows/release.desktop.yaml
vendored
2
.github/workflows/release.desktop.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: master # otherwise it defaults to the version tag missing bump commit
|
ref: master # otherwise it defaults to the version tag missing bump commit
|
||||||
fetch-depth: 0 # fetch all history
|
fetch-depth: 0 # fetch all history
|
||||||
|
|||||||
8
.github/workflows/release.site.yaml
vendored
8
.github/workflows/release.site.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: "Infrastructure: Checkout"
|
name: "Infrastructure: Checkout"
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
path: aws
|
path: aws
|
||||||
repository: undergroundwires/aws-static-site-with-cd
|
repository: undergroundwires/aws-static-site-with-cd
|
||||||
@@ -75,7 +75,7 @@ jobs:
|
|||||||
working-directory: aws
|
working-directory: aws
|
||||||
-
|
-
|
||||||
name: "App: Checkout"
|
name: "App: Checkout"
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
path: app
|
path: app
|
||||||
ref: master # otherwise we don't get version bump commit
|
ref: master # otherwise we don't get version bump commit
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
uses: ./app/.github/actions/setup-node
|
uses: ./app/.github/actions/setup-node
|
||||||
-
|
-
|
||||||
name: "App: Install dependencies"
|
name: "App: Install dependencies"
|
||||||
uses: ./.github/actions/npm-install-dependencies
|
uses: ./app/.github/actions/npm-install-dependencies
|
||||||
with:
|
with:
|
||||||
working-directory: app
|
working-directory: app
|
||||||
-
|
-
|
||||||
@@ -102,7 +102,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: "App: Deploy to S3"
|
name: "App: Deploy to S3"
|
||||||
shell: bash
|
shell: bash
|
||||||
run: >-
|
run: |-
|
||||||
declare web_output_dir
|
declare web_output_dir
|
||||||
if ! web_output_dir=$(cd app && node scripts/print-dist-dir.js --web); then
|
if ! web_output_dir=$(cd app && node scripts/print-dist-dir.js --web); then
|
||||||
echo 'Error: Could not determine distribution directory.'
|
echo 'Error: Could not determine distribution directory.'
|
||||||
|
|||||||
40
.github/workflows/tests.e2e.yaml
vendored
40
.github/workflows/tests.e2e.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
@@ -24,3 +24,41 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Run e2e tests
|
name: Run e2e tests
|
||||||
run: npm run test:cy:run
|
run: npm run test:cy:run
|
||||||
|
-
|
||||||
|
name: Output artifact directories
|
||||||
|
id: artifacts
|
||||||
|
if: always() # Run even if previous steps fail because test run video is always captured
|
||||||
|
shell: bash
|
||||||
|
run: |-
|
||||||
|
declare -r dirs_json_file='cypress-dirs.json'
|
||||||
|
if [ ! -f "${dirs_json_file}" ]; then
|
||||||
|
echo "${dirs_json_file} does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCREENSHOTS_DIR=$(jq -r '.screenshots' "${dirs_json_file}")
|
||||||
|
VIDEOS_DIR=$(jq -r '.videos' "${dirs_json_file}")
|
||||||
|
|
||||||
|
for dir in "${SCREENSHOTS_DIR}" "${VIDEOS_DIR}"; do
|
||||||
|
if [ "${dir}" = 'null' ] || [ -z "${dir}" ]; then
|
||||||
|
echo "One or more directories are null or not specified in cypress-dirs.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "SCREENSHOTS_DIR=${SCREENSHOTS_DIR}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "VIDEOS_DIR=${VIDEOS_DIR}" >> "${GITHUB_OUTPUT}"
|
||||||
|
-
|
||||||
|
name: Upload screenshots
|
||||||
|
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: e2e-screenshots-${{ matrix.os }}
|
||||||
|
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
|
||||||
|
-
|
||||||
|
name: Upload videos
|
||||||
|
if: always() # Run even if previous steps fail because test run video is always captured
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: e2e-videos-${{ matrix.os }}
|
||||||
|
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}
|
||||||
|
|||||||
2
.github/workflows/tests.integration.yaml
vendored
2
.github/workflows/tests.integration.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
|||||||
2
.github/workflows/tests.unit.yaml
vendored
2
.github/workflows/tests.unit.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Set-up node
|
name: Set-up node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
|||||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,5 +1,13 @@
|
|||||||
node_modules
|
# Application build artifacts
|
||||||
/dist-*/
|
/dist-*/
|
||||||
.vs
|
|
||||||
|
# npm
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
.vscode/**/*
|
.vscode/**/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# draw.io
|
||||||
|
*.bkp
|
||||||
|
*.dtmp
|
||||||
|
|||||||
89
CHANGELOG.md
89
CHANGELOG.md
@@ -1,5 +1,94 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.12.7 (2023-11-07)
|
||||||
|
|
||||||
|
* Add winget download instructions | [b2ffc90](https://github.com/undergroundwires/privacy.sexy/commit/b2ffc90da70367b9e65c82556e8f440f865ceb98)
|
||||||
|
* Fix unresponsive copy button on instructions modal | [8ccaec7](https://github.com/undergroundwires/privacy.sexy/commit/8ccaec7af6ea3ecfd46bab5c13b90f71d55e32c1)
|
||||||
|
* Fix tree node check states not being updated | [af7219f](https://github.com/undergroundwires/privacy.sexy/commit/af7219f6e12ab4a65ce07190f691cf3234e87e35)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.6...0.12.7)
|
||||||
|
|
||||||
|
## 0.12.6 (2023-11-03)
|
||||||
|
|
||||||
|
* Bump dependencies to latest | [25d7f7b](https://github.com/undergroundwires/privacy.sexy/commit/25d7f7b2a479e51e092881cc2751e67a7d3f179f)
|
||||||
|
* win: improve system app uninstall cleanup #73 | [dbe3c5c](https://github.com/undergroundwires/privacy.sexy/commit/dbe3c5cfb91ba8a1657838b69117858843c8fbc8)
|
||||||
|
* win: improve system app uninstall /w fallback #260 | [98a26f9](https://github.com/undergroundwires/privacy.sexy/commit/98a26f9ae47af2668aa53f39d1768983036048ce)
|
||||||
|
* Improve performance of rendering during search | [79b46bf](https://github.com/undergroundwires/privacy.sexy/commit/79b46bf21004d96d31551439e5db5d698a3f71f3)
|
||||||
|
* Fix YAML error for site release in CI/CD | [237d994](https://github.com/undergroundwires/privacy.sexy/commit/237d9944f900f5172366868d75219224ff0542b0)
|
||||||
|
* win: fix Microsoft Advertising app removal #200 | [e40b9a3](https://github.com/undergroundwires/privacy.sexy/commit/e40b9a3cf53c341f2e84023a9f0e9680ac08f3fa)
|
||||||
|
* win: improve directory cleanup security | [060e789](https://github.com/undergroundwires/privacy.sexy/commit/060e7896624309aebd25e8b190c127282de177e8)
|
||||||
|
* Centralize Electron entry file path configuration | [d6da406](https://github.com/undergroundwires/privacy.sexy/commit/d6da406c61e5b9f5408851d1302d6d7398157a2e)
|
||||||
|
* win: prevent updates from reinstalling apps #260 | [8570b02](https://github.com/undergroundwires/privacy.sexy/commit/8570b02dde14ffad64863f614682c3fc1f87b6c2)
|
||||||
|
* win: improve script environment robustness #221 | [dfd4451](https://github.com/undergroundwires/privacy.sexy/commit/dfd44515613f38abe5a806bda36f44e7b715b50b)
|
||||||
|
* Fix compiler failing with nested `with` expression | [80821fc](https://github.com/undergroundwires/privacy.sexy/commit/80821fca0769e5fd2c6338918fbdcea12fbe83d2)
|
||||||
|
* win: improve soft file/app delete security #260 | [f4a74f0](https://github.com/undergroundwires/privacy.sexy/commit/f4a74f058db9b5bcbcbe438785db5ec88ecc1657)
|
||||||
|
* Fix incorrect tooltip position after window resize | [f8e5f1a](https://github.com/undergroundwires/privacy.sexy/commit/f8e5f1a5a2afa1f18567e6d965359b6a1f082367)
|
||||||
|
* linux: fix string formatting of Firefox configs | [e775d68](https://github.com/undergroundwires/privacy.sexy/commit/e775d68a9b4a5f9e893ff0e3500dade036185193)
|
||||||
|
* win: improve file delete | [e72c1c1](https://github.com/undergroundwires/privacy.sexy/commit/e72c1c13ea2d73ebfc7a8da5a21254fdfc0e5b59)
|
||||||
|
* win: change system app removal to hard delete #260 | [77123d8](https://github.com/undergroundwires/privacy.sexy/commit/77123d8c929d23676a9cb21d7b697703fd1b6e82)
|
||||||
|
* Improve UI performance by optimizing reactivity | [4995e49](https://github.com/undergroundwires/privacy.sexy/commit/4995e49c469211404dac9fcb79b75eb121f80bce)
|
||||||
|
* Migrate to Vue 3.0 #230 | [ca81f68](https://github.com/undergroundwires/privacy.sexy/commit/ca81f68ff1c3bbe5b22981096ae9220b0b5851c7)
|
||||||
|
* win, linux: unify & improve Firefox clean-up #273 | [0466b86](https://github.com/undergroundwires/privacy.sexy/commit/0466b86f1013341c966a9bbf6513990337b16598)
|
||||||
|
* win: fix store revert for multiple installs #260 | [5bb13e3](https://github.com/undergroundwires/privacy.sexy/commit/5bb13e34f8de2e2a7ba943ff72b12c0569435e62)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.5...0.12.6)
|
||||||
|
|
||||||
|
## 0.12.5 (2023-10-13)
|
||||||
|
|
||||||
|
* Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16)
|
||||||
|
* Add SAST security checks with SECURITY.md #178 | [3e5239f](https://github.com/undergroundwires/privacy.sexy/commit/3e5239f7d35e57749c01adf3dbbcd365aebb39c8)
|
||||||
|
* Add Scoop download instructions #174 | [cf55ca9](https://github.com/undergroundwires/privacy.sexy/commit/cf55ca9e28b064fa7a516077a9da23e3a8e3f534)
|
||||||
|
* win: fix and improve temp dir cleanup #176, #89 | [d457504](https://github.com/undergroundwires/privacy.sexy/commit/d45750428cca010daf2721b33a8ae3a01b28813b)
|
||||||
|
* win, linux: improve VSCode setting robustness #196 | [e8a52f7](https://github.com/undergroundwires/privacy.sexy/commit/e8a52f717dc799b34ceeb1c27c2b8219391dff6a)
|
||||||
|
* linux: fix obsolete Firefox DPI script #239 | [e5f6edf](https://github.com/undergroundwires/privacy.sexy/commit/e5f6edf405bcec7c29ea4d7932d1910620fa15f8)
|
||||||
|
* win: add removal of Edge assocations #64 | [888c916](https://github.com/undergroundwires/privacy.sexy/commit/888c9166fc66a2094137fa8be739cc21bafef5f6)
|
||||||
|
* win: improve Edge & OneDrive shortcut removal #73 | [8501495](https://github.com/undergroundwires/privacy.sexy/commit/8501495c170af61913288a63dbd369db5bbc5003)
|
||||||
|
* win: relocate and document SecHealthUI #190 | [2862951](https://github.com/undergroundwires/privacy.sexy/commit/286295128d0179358e0c6b7b6415d752175a1aed)
|
||||||
|
* Add developer toolkit UI component | [2147eae](https://github.com/undergroundwires/privacy.sexy/commit/2147eae687b82d05bc43bb4605d9068f148bb92a)
|
||||||
|
* win: fix and improve network data usage reset #265 | [5e359c2](https://github.com/undergroundwires/privacy.sexy/commit/5e359c2fb82a08e6acf7159b70ca86a8234b359b)
|
||||||
|
* win: improve app reversion and docs #260 | [a3f11df](https://github.com/undergroundwires/privacy.sexy/commit/a3f11dff187c821a00910c20dac05e285cda9073)
|
||||||
|
* Fix working directory in CI/CD web release | [698b570](https://github.com/undergroundwires/privacy.sexy/commit/698b570ee6e300d6703015464f4345b5e706f1cb)
|
||||||
|
* Implement new UI component for icons #230 | [48730bc](https://github.com/undergroundwires/privacy.sexy/commit/48730bca0506120bca4bf3a23545d59f2b1a9009)
|
||||||
|
* win: fix and improve AppCompat disabling #255 | [bab6316](https://github.com/undergroundwires/privacy.sexy/commit/bab6316e7625230cf4a4cf67c3aca417347db75c)
|
||||||
|
* win, linux, mac: fix typos and improve naming | [67c3677](https://github.com/undergroundwires/privacy.sexy/commit/67c3677621b201525a813e8a26f07d607176e89b)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.4...0.12.5)
|
||||||
|
|
||||||
|
## 0.12.4 (2023-09-25)
|
||||||
|
|
||||||
|
* win: fix Windows spotlight revert, docs, recommend | [659fea7](https://github.com/undergroundwires/privacy.sexy/commit/659fea7afcabcd0ea273cfdcc8c4bae190c126f3)
|
||||||
|
* win: fix Edge telemetry disabling for v116+ #242 | [6d301f9](https://github.com/undergroundwires/privacy.sexy/commit/6d301f99616ed49975876803d0098eafe4d3cb2e)
|
||||||
|
* win: fix, improve disabling automatic updates #252 | [6e9b65d](https://github.com/undergroundwires/privacy.sexy/commit/6e9b65d8b1b481c1471dde90876c37838b4ac4e5)
|
||||||
|
* win: refactor `update.mode` key for VSCode #215 | [c27172c](https://github.com/undergroundwires/privacy.sexy/commit/c27172c32e7c316b7cb0f44cab611eed89ca034e)
|
||||||
|
* Fix wrong action path in website CI deployment | [a1f2497](https://github.com/undergroundwires/privacy.sexy/commit/a1f24973813ccbdd7e1f06c64e1912a991a6bb64)
|
||||||
|
* Fix compiler bug with nested optional arguments | [53222fd](https://github.com/undergroundwires/privacy.sexy/commit/53222fd83c2846089746a217482195806f960d18)
|
||||||
|
* Fix no spacing after lists in documentation text | [f810ed0](https://github.com/undergroundwires/privacy.sexy/commit/f810ed0c147c2a46cae3b70b635ed81128646fff)
|
||||||
|
* Rewrite tooltip UI for efficiency and Vue 3.0 #230 | [8b930fc](https://github.com/undergroundwires/privacy.sexy/commit/8b930fc57c8ee6691ed6165bcb27d97e64a1a0c0)
|
||||||
|
* win: fix uninstallation of newer Edge #236 | [60dde11](https://github.com/undergroundwires/privacy.sexy/commit/60dde11311a2409537f5965f370b0daaaec53339)
|
||||||
|
* win: fix delivery optimization side-effects #173 | [203daeb](https://github.com/undergroundwires/privacy.sexy/commit/203daeb4a2fca0a0295cbc2a736394f9f87725e6)
|
||||||
|
* win: fix Defender scan artifacts removal #246 | [cb21a97](https://github.com/undergroundwires/privacy.sexy/commit/cb21a970b6b867e1476a5eb8a72b9a7fdd53a744)
|
||||||
|
* Fix outdated and broken links in README #161 | [0303ef2](https://github.com/undergroundwires/privacy.sexy/commit/0303ef2fd98b36306523e2a0c5f5ae812a4c6c99)
|
||||||
|
* Fix loss of tree node state when switching views | [8f188ac](https://github.com/undergroundwires/privacy.sexy/commit/8f188acd3c2d93e40c89569c74bc5cff992f0052)
|
||||||
|
* Fix slow appearance of nodes on tree view | [bd2082e](https://github.com/undergroundwires/privacy.sexy/commit/bd2082e8c574db065bb4462f30ea3ace2cb028cb)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.3...0.12.4)
|
||||||
|
|
||||||
|
## 0.12.3 (2023-09-09)
|
||||||
|
|
||||||
|
* linux: use user.js over prefs.js for Firefox #232 | [dae6d11](https://github.com/undergroundwires/privacy.sexy/commit/dae6d114daab6857d773071211eb57619b136281)
|
||||||
|
* win: fix typo in Defender retention script #213 | [35be05d](https://github.com/undergroundwires/privacy.sexy/commit/35be05df2094ea8bba4ee4725e6fa4956a79493d)
|
||||||
|
* Improve desktop runtime execution tests | [ad0576a](https://github.com/undergroundwires/privacy.sexy/commit/ad0576a752f8fd6ea2f917a59173fe61f9951246)
|
||||||
|
* Fix Windows artifact naming in desktop packaging | [f4d86fc](https://github.com/undergroundwires/privacy.sexy/commit/f4d86fccfd0e73e94c8c6e400a33514900bc5abe)
|
||||||
|
* Refactor and improve external URL checks | [19e42c9](https://github.com/undergroundwires/privacy.sexy/commit/19e42c9c52a18c813ded4265e687e01032cdd4c8)
|
||||||
|
* Fix memory leaks via auto-unsubscribing and DI | [eb096d0](https://github.com/undergroundwires/privacy.sexy/commit/eb096d07e276e1b4c8040220c47f186d02841e14)
|
||||||
|
* Refactor build configs and improve CI/CD checks | [0a2a1a0](https://github.com/undergroundwires/privacy.sexy/commit/0a2a1a026b0efb29624be82b06536c518c1ea439)
|
||||||
|
* Introduce retry mechanism for npm install in CI/CD | [4beb1bb](https://github.com/undergroundwires/privacy.sexy/commit/4beb1bb5748a60886210187ca3cdc7f4b41067c0)
|
||||||
|
* win: fix disable recent apps revert #211, #248 | [4ce327e](https://github.com/undergroundwires/privacy.sexy/commit/4ce327eb6af542ed2916d649553e5e1ba5833882)
|
||||||
|
* Change license to AGPLv3 | [821cc62](https://github.com/undergroundwires/privacy.sexy/commit/821cc62c4c8347cb76d041f82f574754e4d948c5)
|
||||||
|
* Introduce new TreeView UI component | [65f121c](https://github.com/undergroundwires/privacy.sexy/commit/65f121c451af87315e1c91df4198562e0445b2c2)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.2...0.12.3)
|
||||||
|
|
||||||
## 0.12.2 (2023-08-25)
|
## 0.12.2 (2023-08-25)
|
||||||
|
|
||||||
* Add automated checks for desktop app runtime #233 | [04b3133](https://github.com/undergroundwires/privacy.sexy/commit/04b3133500485d0d278a81a177a1677134131405)
|
* Add automated checks for desktop app runtime #233 | [04b3133](https://github.com/undergroundwires/privacy.sexy/commit/04b3133500485d0d278a81a177a1677134131405)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ You have two alternatives:
|
|||||||
|
|
||||||
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) and ask for someone else to add the script for you.
|
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) and ask for someone else to add the script for you.
|
||||||
2. Or send a PR yourself. This would make it faster to get your code into the project. You need to add scripts to related OS in [collections](src/application/collections/) folder. Then you'd sent a pull request, see [pull request process](#pull-request-process).
|
2. Or send a PR yourself. This would make it faster to get your code into the project. You need to add scripts to related OS in [collections](src/application/collections/) folder. Then you'd sent a pull request, see [pull request process](#pull-request-process).
|
||||||
|
- 💡 You should use existing shared functions for most of the operations, like `DisableService` for disabling services, to maintain code consistency and efficiency.
|
||||||
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md).
|
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md).
|
||||||
- 📖 If you wish to use templates, use [templating.md](./docs/templating.md).
|
- 📖 If you wish to use templates, use [templating.md](./docs/templating.md).
|
||||||
|
|
||||||
|
|||||||
15
Dockerfile
15
Dockerfile
@@ -1,13 +1,16 @@
|
|||||||
# Build
|
# Build
|
||||||
FROM node:lts-alpine as build-stage
|
FROM node:lts-alpine AS build-stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm install
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run install-deps
|
||||||
|
RUN npm run build \
|
||||||
|
&& npm run check:verify-build-artifacts -- --web
|
||||||
|
RUN mkdir /dist \
|
||||||
|
&& dist_directory=$(node 'scripts/print-dist-dir.js' --web) \
|
||||||
|
&& cp -a "${dist_directory}/." '/dist'
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM nginx:stable-alpine as production-stage
|
FROM nginx:stable-alpine AS production-stage
|
||||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
COPY --from=build-stage /dist /usr/share/nginx/html
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
57
README.md
57
README.md
@@ -16,14 +16,6 @@
|
|||||||
src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"
|
src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<!-- Code quality -->
|
|
||||||
<br />
|
|
||||||
<a href="https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript" target="_blank" rel="noopener noreferrer">
|
|
||||||
<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" target="_blank" rel="noopener noreferrer">
|
<a href="https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability" target="_blank" rel="noopener noreferrer">
|
||||||
<img
|
<img
|
||||||
alt="Maintainability"
|
alt="Maintainability"
|
||||||
@@ -50,6 +42,20 @@
|
|||||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/e2e-tests/badge.svg"
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/e2e-tests/badge.svg"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
<!-- Security checks -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.sast.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Status of dependency security checks"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.security.sast/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.dependencies.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Status of Static Analysis Security Testing (SAST)"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.security.dependencies/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
<!-- Checks -->
|
<!-- Checks -->
|
||||||
<br />
|
<br />
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
@@ -58,16 +64,10 @@
|
|||||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.yaml" target="_blank" rel="noopener noreferrer">
|
|
||||||
<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" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
<img
|
<img
|
||||||
alt="Build checks status"
|
alt="Status of build checks"
|
||||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.build/badge.svg"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.desktop-runtime-errors.yaml" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.desktop-runtime-errors.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 🖥️ **Offline**: Check [releases page](https://github.com/undergroundwires/privacy.sexy/releases), or download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-Setup-0.11.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.dmg), [Linux](https://github.com/undergroundwires/pr.vacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.AppImage).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-Setup-0.12.7.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-0.12.7.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-0.12.7.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
||||||
|
|
||||||
@@ -150,6 +150,25 @@ Online version does not require to run any software on your computer. Offline ve
|
|||||||
|
|
||||||
**Contribute 👷**. Contributions of any type are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) as the starting point. It includes useful information like [how to add new scripts](./CONTRIBUTING.md#extend-scripts).
|
**Contribute 👷**. Contributions of any type are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) as the starting point. It includes useful information like [how to add new scripts](./CONTRIBUTING.md#extend-scripts).
|
||||||
|
|
||||||
|
## Additional Install Options
|
||||||
|
|
||||||
|
- Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions.
|
||||||
|
- Other unofficial channels (not maintained by privacy.sexy) for Windows include:
|
||||||
|
- [Scoop 🥄](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) (latest version):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
scoop bucket add extras
|
||||||
|
scoop install privacy.sexy
|
||||||
|
```
|
||||||
|
|
||||||
|
- [winget 🪟](https://winget.run/pkg/undergroundwires/privacy.sexy) (may be outdated):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
winget install -e --id undergroundwires.privacy.sexy
|
||||||
|
```
|
||||||
|
|
||||||
|
With winget, updates require manual submission; the auto-update feature within privacy.sexy will notify you of new releases post-installation.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Refer to [development.md](./docs/development.md) for Docker usage and reading more about setting up your development environment.
|
Refer to [development.md](./docs/development.md) for Docker usage and reading more about setting up your development environment.
|
||||||
@@ -157,3 +176,7 @@ Refer to [development.md](./docs/development.md) for Docker usage and reading mo
|
|||||||
Check [architecture.md](./docs/architecture.md) for an overview of design and how different parts and layers work together. You can refer to [application.md](./docs/application.md) for a closer look at application layer codebase and [presentation.md](./docs/presentation.md) for code related to GUI layer. [collection-files.md](./docs/collection-files.md) explains the YAML files that are the core of the application and [templating.md](./docs/templating.md) documents how to use templating language in those files. In [ci-cd.md](./docs/ci-cd.md), you can read more about the pipelines that automates maintenance tasks and ensures you get what see.
|
Check [architecture.md](./docs/architecture.md) for an overview of design and how different parts and layers work together. You can refer to [application.md](./docs/application.md) for a closer look at application layer codebase and [presentation.md](./docs/presentation.md) for code related to GUI layer. [collection-files.md](./docs/collection-files.md) explains the YAML files that are the core of the application and [templating.md](./docs/templating.md) documents how to use templating language in those files. In [ci-cd.md](./docs/ci-cd.md), you can read more about the pipelines that automates maintenance tasks and ensures you get what see.
|
||||||
|
|
||||||
[docs/](./docs/) folder includes all other documentation.
|
[docs/](./docs/) folder includes all other documentation.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Security is a top priority at privacy.sexy. An extensive commitment to security verification ensures this priority. For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md).
|
||||||
|
|||||||
31
SECURITY.md
Normal file
31
SECURITY.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
privacy.sexy takes security seriously. Commitment is made to address all security issues with urgency. Responsible reporting of any discovered vulnerabilities in the project is highly encouraged.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Efforts to responsibly disclose findings are greatly appreciated. To report a security vulnerability, follow these steps:
|
||||||
|
|
||||||
|
- For general vulnerabilities, [open an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) using the bug report template.
|
||||||
|
- For sensitive matters, [contact the developer directly](https://undergroundwires.dev).
|
||||||
|
|
||||||
|
## Security Report Handling
|
||||||
|
|
||||||
|
Upon receipt of a security report, the following actions will be taken:
|
||||||
|
|
||||||
|
- The report will be confirmed, identifying the affected components.
|
||||||
|
- The impact and severity of the issue will be assessed.
|
||||||
|
- Work on a fix and plan a release to address the vulnerability will be initiated.
|
||||||
|
- The reporter will be kept updated about the progress.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Regular and extensive testing is conducted to ensure robust security in the project. Information about testing practices can be found in the [Testing Documentation](./docs/tests.md).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For additional assistance or any unanswered questions, [submit a GitHub issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose). Security concerns are a priority, and necessary support to address them is assured.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Active contribution to the safety and security of privacy.sexy is thanked. This collaborative effort keeps the project resilient and trustworthy for all.
|
||||||
5
cypress-dirs.json
Normal file
5
cypress-dirs.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"base": "tests/e2e",
|
||||||
|
"videos": "tests/e2e/videos",
|
||||||
|
"screenshots": "tests/e2e/videos"
|
||||||
|
}
|
||||||
@@ -1,15 +1,31 @@
|
|||||||
import { defineConfig } from 'cypress';
|
import { defineConfig } from 'cypress';
|
||||||
import ViteConfig from './vite.config';
|
import ViteConfig from './vite.config';
|
||||||
|
import cypressDirs from './cypress-dirs.json' assert { type: 'json' };
|
||||||
const CYPRESS_BASE_DIR = 'tests/e2e/';
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
fixturesFolder: `${CYPRESS_BASE_DIR}/fixtures`,
|
fixturesFolder: `${cypressDirs.base}/fixtures`,
|
||||||
screenshotsFolder: `${CYPRESS_BASE_DIR}/screenshots`,
|
screenshotsFolder: cypressDirs.screenshots,
|
||||||
videosFolder: `${CYPRESS_BASE_DIR}/videos`,
|
|
||||||
|
video: true,
|
||||||
|
videosFolder: cypressDirs.videos,
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: `http://localhost:${ViteConfig.server.port}/`,
|
baseUrl: `http://localhost:${getApplicationPort()}/`,
|
||||||
specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
|
specPattern: `${cypressDirs.base}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
|
||||||
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
|
supportFile: `${cypressDirs.base}/support/e2e.ts`,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
Disabling Chrome's web security to allow for faster DOM queries to access DOM earlier than
|
||||||
|
`cy.get()`. It bypasses the usual same-origin policy constraints
|
||||||
|
*/
|
||||||
|
chromeWebSecurity: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getApplicationPort(): number {
|
||||||
|
const port = ViteConfig.server?.port;
|
||||||
|
if (port === undefined) {
|
||||||
|
throw new Error('Unknown application port');
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|||||||
@@ -174,3 +174,19 @@
|
|||||||
- `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](./templating.md#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 }}!`
|
||||||
|
|
||||||
|
## Naming guidelines
|
||||||
|
|
||||||
|
- Prioritize consistency throughout all names.
|
||||||
|
- Use an instruction format like "do this, do that" for clear, direct guidance. This approach reduces potential confusion and offers easy-to-follow steps. It provides specific, unambiguous instructions.
|
||||||
|
- Ensure brand names adhere to their official casing.
|
||||||
|
- Choose clear and uncomplicated language.
|
||||||
|
- Favor the terms:
|
||||||
|
- "Disable" over "Turn off"
|
||||||
|
- "Configure" over "Set up"
|
||||||
|
- "Clear" over "Erase" or "Clean"
|
||||||
|
- "Minimize" over "Limit" or "Reduce" (when it enhances clarity)
|
||||||
|
- "Remove" over "Uninstall"
|
||||||
|
- Structure your phrases for clarity.
|
||||||
|
- For instance, "Disable XX telemetry" or "Clear XX data" are preferred over "Clear data from XX", "Disable telemetry in XX", or "Clear data of XX".
|
||||||
|
- Use sentence case rather than Title Case.
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ See [ci-cd.md](./ci-cd.md) for more information.
|
|||||||
|
|
||||||
1. Build: `docker build -t undergroundwires/privacy.sexy:latest .`
|
1. Build: `docker build -t undergroundwires/privacy.sexy:latest .`
|
||||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest`
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest`
|
||||||
|
3. Application should be available at [`http://localhost:8080`](http://localhost:8080)
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
@@ -81,11 +82,12 @@ See [ci-cd.md](./ci-cd.md) for more information.
|
|||||||
|
|
||||||
#### Automation scripts
|
#### Automation scripts
|
||||||
|
|
||||||
- [**`node scripts/print-dist-dir.js [-- <options>]`**](../scripts/print-dist-dir.js):
|
- [**`node scripts/print-dist-dir.js [<options>]`**](../scripts/print-dist-dir.js):
|
||||||
- Determines the absolute path of a distribution directory based on CLI arguments and outputs its absolute path.
|
- Determines the absolute path of a distribution directory based on CLI arguments and outputs its absolute path.
|
||||||
- Primarily used by automation scripts.
|
|
||||||
- [**`npm run check:verify-build-artifacts [-- <options>]`**](../scripts/verify-build-artifacts.js):
|
- [**`npm run check:verify-build-artifacts [-- <options>]`**](../scripts/verify-build-artifacts.js):
|
||||||
- Verifies the existence and content of build artifacts. Useful for ensuring that the build process is generating the expected output.
|
- Verifies the existence and content of build artifacts. Useful for ensuring that the build process is generating the expected output.
|
||||||
|
- [**`node scripts/verify-web-server-status.js --url [URL]`**](../scripts/verify-web-server-status.js):
|
||||||
|
- Checks if a specified server is up with retries and returns an HTTP 200 status code.
|
||||||
|
|
||||||
## Recommended extensions
|
## Recommended extensions
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code.
|
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code.
|
||||||
|
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
||||||
|
- [**`index.html`**](./../src/presentation/index.html): The `index.html` entry file, located at the root of the project as required by Vite
|
||||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
||||||
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
||||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
||||||
@@ -20,9 +22,7 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
||||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
|
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
|
||||||
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles for third-party components.
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
||||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
|
|
||||||
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
|
||||||
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||||
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||||
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||||
@@ -71,10 +71,11 @@ To add a new dependency:
|
|||||||
1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into:
|
1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into:
|
||||||
- **Singletons**: Shared across components, instantiated once.
|
- **Singletons**: Shared across components, instantiated once.
|
||||||
- **Transients**: Factories yielding a new instance on every access.
|
- **Transients**: Factories yielding a new instance on every access.
|
||||||
2. **Provide the dependency**: Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency. [`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
|
2. **Provide the dependency**:
|
||||||
3. **Inject the dependency**: Use Vue's `inject` method alongside the defined symbol to incorporate the dependency into components.
|
Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency.
|
||||||
- For singletons, invoke the factory method: `inject(symbolKey)()`.
|
[`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
|
||||||
- For transients, directly inject: `inject(symbolKey)`.
|
3. **Inject the dependency**: Use `injectKey` to inject a dependency. Pass a selector function to `injectKey` that retrieves the appropriate symbol from the provided dependencies.
|
||||||
|
- Example usage: `injectKey((keys) => keys.useCollectionState)`;
|
||||||
|
|
||||||
## Shared UI components
|
## Shared UI components
|
||||||
|
|
||||||
|
|||||||
@@ -2,79 +2,142 @@
|
|||||||
|
|
||||||
## Benefits of templating
|
## Benefits of templating
|
||||||
|
|
||||||
- Generating scripts by sharing code to increase best-practice usage and maintainability.
|
- **Code sharing:** Share code across scripts for consistent practices and easier maintenance.
|
||||||
- Creating self-contained scripts without cross-dependencies.
|
- **Script independence:** Generate self-contained scripts, eliminating the need for external code.
|
||||||
- Use of pipes for writing cleaner code and letting pipes do dirty work.
|
- **Cleaner code:** Use pipes for complex operations, resulting in more readable and streamlined code.
|
||||||
|
|
||||||
## Expressions
|
## Expressions
|
||||||
|
|
||||||
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
|
**Syntax:**
|
||||||
- E.g. `Hello {{ $name }} !`
|
|
||||||
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
|
|
||||||
- Functions enables usage of expressions.
|
|
||||||
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
|
||||||
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
|
||||||
- Expressions inside expressions (nested templates) are supported.
|
|
||||||
- An expression can output another expression that will also be compiled.
|
|
||||||
- E.g. following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output.
|
|
||||||
|
|
||||||
```go
|
Expressions are enclosed within `{{` and `}}`.
|
||||||
{{ with $condition }}
|
Example: `Hello {{ $name }}!`.
|
||||||
echo {{ $text }}
|
They are a core component of templating, enhancing scripts with dynamic capabilities and functionality.
|
||||||
{{ end }}
|
|
||||||
```
|
**Syntax similarity:**
|
||||||
|
|
||||||
|
The syntax shares similarities with [Go Templates ❤️](https://pkg.go.dev/text/template), but with some differences:
|
||||||
|
|
||||||
|
**Function definitions:**
|
||||||
|
|
||||||
|
You can use expressions in function definition.
|
||||||
|
Refer to [Function](./collection-files.md#function) for more details.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: GreetFunction
|
||||||
|
parameters:
|
||||||
|
- name: name
|
||||||
|
code: Hello {{ $name }}!
|
||||||
|
```
|
||||||
|
|
||||||
|
If you assign `name` the value `world`, invoking `GreetFunction` would result in `Hello world!`.
|
||||||
|
|
||||||
|
**Function arguments:**
|
||||||
|
|
||||||
|
You can also use expressions in arguments in nested function calls.
|
||||||
|
Refer to [`Function | collection-files.md`](./collection-files.md#functioncall) for more details.
|
||||||
|
|
||||||
|
Example with nested function calls:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
-
|
||||||
|
name: PrintMessageFunction
|
||||||
|
parameters:
|
||||||
|
- name: message
|
||||||
|
code: echo "{{ $message }}"
|
||||||
|
-
|
||||||
|
name: GreetUserFunction
|
||||||
|
parameters:
|
||||||
|
- name: userName
|
||||||
|
call:
|
||||||
|
name: PrintMessageFunction
|
||||||
|
parameters:
|
||||||
|
argument: 'Hello, {{ $userName }}!'
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, if `userName` is `Alice`, invoking `GreetUserFunction` would execute `echo "Hello, Alice!"`.
|
||||||
|
|
||||||
|
**Nested templates:**
|
||||||
|
|
||||||
|
You can nest expressions inside expressions (also called "nested templates").
|
||||||
|
This means that an expression can output another expression where compiler will compile both.
|
||||||
|
|
||||||
|
For example, following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $condition }}
|
||||||
|
echo {{ $text }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
### Parameter substitution
|
### Parameter substitution
|
||||||
|
|
||||||
A simple function example:
|
Parameter substitution dynamically replaces variable references with their corresponding values in the script.
|
||||||
|
|
||||||
|
**Example function:**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
function: EchoArgument
|
name: DisplayTextFunction
|
||||||
parameters:
|
parameters:
|
||||||
- name: 'argument'
|
- name: 'text'
|
||||||
code: Hello {{ $argument }} !
|
code: echo {{ $text }}
|
||||||
```
|
```
|
||||||
|
|
||||||
It would print "Hello world" if it's called in a [script](./collection-files.md#script) as following:
|
Invoking `DisplayTextFunction` with `text` set to `"Hello, world!"` would result in `echo "Hello, World!"`.
|
||||||
|
|
||||||
```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
|
### with
|
||||||
|
|
||||||
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
|
The `with` expression enables conditional rendering and provides a context variable for simpler code.
|
||||||
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
|
|
||||||
|
|
||||||
It binds its context (value of the provided parameter value) as arbitrary `.` value. It allows you to use the argument value of the given parameter when it is provided and not empty such as:
|
**Optional block rendering:**
|
||||||
|
|
||||||
|
If the provided variable is falsy (`false`, `null`, or empty), the compiler skips the enclosed block of code.
|
||||||
|
A "block" lies between the with start (`{{ with .. }}`) and end (`{{ end }}`) expressions, defining its boundaries.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $optionalVariable }}
|
||||||
|
Hello
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
This would display `Hello` if `$optionalVariable` is truthy.
|
||||||
|
|
||||||
|
**Parameter declaration:**
|
||||||
|
|
||||||
|
You should set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
||||||
|
|
||||||
|
Declare parameters used for `with` condition as optional such as:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: ConditionalOutputFunction
|
||||||
|
parameters:
|
||||||
|
- name: 'data'
|
||||||
|
optional: true
|
||||||
|
code: |-
|
||||||
|
{{ with $data }}
|
||||||
|
Data is: {{ . }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Context variable:**
|
||||||
|
|
||||||
|
`with` statement binds its context (value of the provided parameter value) as arbitrary `.` value.
|
||||||
|
`{{ . }}` syntax gives you access to the context variable.
|
||||||
|
This is optional to use, and not required to use `with` expressions.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
|
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
It supports multiline text inside the block. You can have something like:
|
**Multiline text:**
|
||||||
|
|
||||||
|
It supports multiline text inside the block. You can write something like:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ with $argument }}
|
{{ with $argument }}
|
||||||
@@ -83,7 +146,9 @@ It supports multiline text inside the block. You can have something like:
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
**Inner expressions:**
|
||||||
|
|
||||||
|
You can also embed other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ with $condition }}
|
{{ with $condition }}
|
||||||
@@ -91,32 +156,44 @@ You can also use other expressions inside its block, such as [parameter substitu
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
This also includes nesting `with` statements:
|
||||||
|
|
||||||
Example:
|
```go
|
||||||
|
{{ with $condition1 }}
|
||||||
```yaml
|
Value of $condition1: {{ . }}
|
||||||
function: FunctionThatOutputsConditionally
|
{{ with $condition2 }}
|
||||||
parameters:
|
Value of $condition2: {{ . }}
|
||||||
- name: 'argument'
|
|
||||||
optional: true
|
|
||||||
code: |-
|
|
||||||
{{ with $argument }}
|
|
||||||
Value is: {{ . }}
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pipes
|
### Pipes
|
||||||
|
|
||||||
- Pipes are functions available for handling text.
|
Pipes are functions designed for text manipulation.
|
||||||
- Allows stacking actions one after another also known as "chaining".
|
They allow for a sequential application of operations resembling [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), also known as "chaining".
|
||||||
- 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.
|
Each pipeline's output becomes the input of the following pipe.
|
||||||
- You cannot create pipes. [A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
|
|
||||||
- You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
|
**Pre-defined**:
|
||||||
- ❗ Pipe names must be camelCase without any space or special characters.
|
|
||||||
- **Existing pipes**
|
Pipes are pre-defined by the system.
|
||||||
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
|
You cannot create pipes in [collection files](./collection-files.md).
|
||||||
- `escapeDoubleQuotes`: Escapes `"` characters, allows you to use them inside double quotes (`"`).
|
[A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
|
||||||
- **Example usages**
|
|
||||||
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
|
**Compatibility:**
|
||||||
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
|
|
||||||
|
You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $script }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Naming:**
|
||||||
|
|
||||||
|
❗ Pipe names must be camelCase without any space or special characters.
|
||||||
|
|
||||||
|
**Available pipes:**
|
||||||
|
|
||||||
|
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
|
||||||
|
- `escapeDoubleQuotes`: Escapes `"` characters for batch command execution, allows you to use them inside double quotes (`"`).
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ These checks validate various qualities like runtime execution, building process
|
|||||||
- Use [various tools](./../package.json) and [scripts](./../scripts).
|
- Use [various tools](./../package.json) and [scripts](./../scripts).
|
||||||
- Are automatically executed as [GitHub workflows](./../.github/workflows).
|
- Are automatically executed as [GitHub workflows](./../.github/workflows).
|
||||||
|
|
||||||
|
### Security checks
|
||||||
|
|
||||||
|
- [`checks.security.sast`](./../.github/workflows/checks.security.sast.yaml): Utilizes CodeQL to conduct Static Analysis Security Testing (SAST) to ensure the secure integrity of the codebase.
|
||||||
|
- [`checks.security.dependencies`](./../.github/workflows/checks.security.dependencies.yaml): Performs audits on third-party dependencies to identify and mitigate potential vulnerabilities, safeguarding the project from exploitable weaknesses.
|
||||||
|
|
||||||
## Tests structure
|
## Tests structure
|
||||||
|
|
||||||
- [`package.json`](./../package.json): Defines test commands and includes tools used in tests.
|
- [`package.json`](./../package.json): Defines test commands and includes tools used in tests.
|
||||||
@@ -63,21 +68,23 @@ These checks validate various qualities like runtime execution, building process
|
|||||||
- [`./src/`](./../src/): Contains the code subject to testing.
|
- [`./src/`](./../src/): Contains the code subject to testing.
|
||||||
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
|
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
|
||||||
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
|
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
|
||||||
|
- [`Assertions/`](./../tests/shared/Assertions/): Contains common assertion functions, prefixed with `expect`.
|
||||||
- [`./tests/unit/`](./../tests/unit/)
|
- [`./tests/unit/`](./../tests/unit/)
|
||||||
- Stores unit test code.
|
- Stores unit test code.
|
||||||
- The directory structure mirrors [`./src/`](./../src).
|
- The directory structure mirrors [`./src/`](./../src).
|
||||||
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
||||||
- [`shared/`](./../tests/unit/shared/)
|
- [`shared/`](./../tests/unit/shared/)
|
||||||
- Contains shared unit test functionalities.
|
- Contains shared unit test functionalities.
|
||||||
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
|
|
||||||
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
||||||
- Shared test cases.
|
- Shared test cases.
|
||||||
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
|
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
|
||||||
- [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities.
|
- [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities.
|
||||||
- [`./tests/integration/`](./../tests/integration/): Contains integration test files.
|
- [`./tests/integration/`](./../tests/integration/): Contains integration test files.
|
||||||
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
|
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
|
||||||
|
- [`cypress-dirs.json`](./../cypress-dirs.json): A central definition of directories used by Cypress, designed for reuse across different configurations.
|
||||||
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension.
|
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension.
|
||||||
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single spec file.
|
|
||||||
- [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation.
|
- [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation.
|
||||||
- *(git ignored)* `/videos`: Asset folder for videos taken during tests.
|
- *(git ignored)* `/videos`: Asset folder for videos taken during tests.
|
||||||
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
||||||
|
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single test file.
|
||||||
|
- [`/support/interactions/`](./../tests/e2e/support/interactions/): Contains reusable functions for simulating user interactions, enhancing test readability and maintainability.
|
||||||
|
|||||||
@@ -8,15 +8,21 @@ import distDirs from './dist-dirs.json' assert { type: 'json' };
|
|||||||
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
|
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
|
||||||
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
|
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
|
||||||
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
|
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
|
||||||
const DIST_DIR = resolvePathFromProjectRoot(distDirs.electronUnbundled);
|
const ELECTRON_DIST_SUBDIRECTORIES = {
|
||||||
|
main: resolveElectronDistSubdirectory('main'),
|
||||||
|
preload: resolveElectronDistSubdirectory('preload'),
|
||||||
|
renderer: resolveElectronDistSubdirectory('renderer'),
|
||||||
|
};
|
||||||
|
|
||||||
|
process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.cjs');
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: getSharedElectronConfig({
|
main: getSharedElectronConfig({
|
||||||
distDirSubfolder: 'main',
|
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.main,
|
||||||
entryFilePath: MAIN_ENTRY_FILE,
|
entryFilePath: MAIN_ENTRY_FILE,
|
||||||
}),
|
}),
|
||||||
preload: getSharedElectronConfig({
|
preload: getSharedElectronConfig({
|
||||||
distDirSubfolder: 'preload',
|
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.preload,
|
||||||
entryFilePath: PRELOAD_ENTRY_FILE,
|
entryFilePath: PRELOAD_ENTRY_FILE,
|
||||||
}),
|
}),
|
||||||
renderer: mergeConfig(
|
renderer: mergeConfig(
|
||||||
@@ -25,7 +31,7 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
build: {
|
build: {
|
||||||
outDir: resolve(DIST_DIR, 'renderer'),
|
outDir: ELECTRON_DIST_SUBDIRECTORIES.renderer,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
index: WEB_INDEX_HTML_PATH,
|
index: WEB_INDEX_HTML_PATH,
|
||||||
@@ -42,7 +48,7 @@ function getSharedElectronConfig(options: {
|
|||||||
}): UserConfig {
|
}): UserConfig {
|
||||||
return {
|
return {
|
||||||
build: {
|
build: {
|
||||||
outDir: resolve(DIST_DIR, options.distDirSubfolder),
|
outDir: options.distDirSubfolder,
|
||||||
lib: {
|
lib: {
|
||||||
entry: options.entryFilePath,
|
entry: options.entryFilePath,
|
||||||
},
|
},
|
||||||
@@ -64,6 +70,11 @@ function getSharedElectronConfig(options: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePathFromProjectRoot(pathSegment: string) {
|
function resolvePathFromProjectRoot(pathSegment: string): string {
|
||||||
return resolve(__dirname, pathSegment);
|
return resolve(__dirname, pathSegment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveElectronDistSubdirectory(subDirectory: string): string {
|
||||||
|
const electronDistDir = resolvePathFromProjectRoot(distDirs.electronUnbundled);
|
||||||
|
return resolve(electronDistDir, subDirectory);
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
10680
package-lock.json
generated
10680
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
92
package.json
92
package.json
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.2",
|
"version": "0.12.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Now you have the choice",
|
"slogan": "Now you have the choice",
|
||||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist-electron-unbundled/main/index.cjs",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
@@ -25,7 +24,7 @@
|
|||||||
"electron:preview": "electron-vite preview",
|
"electron:preview": "electron-vite preview",
|
||||||
"electron:prebuild": "electron-vite build",
|
"electron:prebuild": "electron-vite build",
|
||||||
"electron:build": "electron-builder",
|
"electron:build": "electron-builder",
|
||||||
"lint:eslint": "eslint . --ignore-path .gitignore",
|
"lint:eslint": "eslint . --max-warnings=0 --ignore-path .gitignore",
|
||||||
"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",
|
||||||
@@ -34,78 +33,75 @@
|
|||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@floating-ui/vue": "^1.0.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
|
||||||
"@fortawesome/vue-fontawesome": "^2.0.9",
|
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"ace-builds": "^1.23.4",
|
"ace-builds": "^1.30.0",
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
|
"electron-log": "^4.4.8",
|
||||||
"electron-progressbar": "^2.1.0",
|
"electron-progressbar": "^2.1.0",
|
||||||
|
"electron-updater": "^6.1.4",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.2",
|
||||||
"npm": "^9.8.1",
|
"vue": "^3.3.7"
|
||||||
"v-tooltip": "2.1.3",
|
|
||||||
"vue": "^2.7.14"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||||
"@rushstack/eslint-patch": "^1.3.2",
|
"@rushstack/eslint-patch": "^1.5.1",
|
||||||
"@types/ace": "^0.0.48",
|
"@types/ace": "^0.0.49",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vitejs/plugin-legacy": "^4.1.1",
|
"@vitejs/plugin-legacy": "^4.1.1",
|
||||||
"@vitejs/plugin-vue2": "^2.2.0",
|
"@vitejs/plugin-vue": "^4.4.0",
|
||||||
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"@vue/test-utils": "^1.3.6",
|
"@vue/test-utils": "^2.4.1",
|
||||||
"autoprefixer": "^10.4.15",
|
"autoprefixer": "^10.4.16",
|
||||||
"cypress": "^12.17.2",
|
"cypress": "^13.3.1",
|
||||||
"electron": "^25.3.2",
|
"electron": "^27.0.0",
|
||||||
"electron-builder": "^24.6.3",
|
"electron-builder": "^24.6.4",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-icon-builder": "^2.0.1",
|
"electron-icon-builder": "^2.0.1",
|
||||||
"electron-log": "^4.4.8",
|
"electron-vite": "^1.0.28",
|
||||||
"electron-updater": "^6.1.4",
|
"eslint": "^8.51.0",
|
||||||
"electron-vite": "^1.0.27",
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"eslint": "^8.46.0",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
"eslint-plugin-cypress": "^2.14.0",
|
"eslint-plugin-vuejs-accessibility": "^2.2.0",
|
||||||
"eslint-plugin-vue": "^9.6.0",
|
"icon-gen": "^4.0.0",
|
||||||
"eslint-plugin-vuejs-accessibility": "^1.2.0",
|
|
||||||
"icon-gen": "^3.0.1",
|
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
"markdownlint-cli": "^0.35.0",
|
"markdownlint-cli": "^0.37.0",
|
||||||
"postcss": "^8.4.28",
|
"postcss": "^8.4.31",
|
||||||
"remark-cli": "^11.0.0",
|
"remark-cli": "^12.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^5.1.2",
|
"remark-preset-lint-consistent": "^5.1.2",
|
||||||
"remark-validate-links": "^12.1.1",
|
"remark-validate-links": "^13.0.0",
|
||||||
"sass": "^1.64.1",
|
"sass": "^1.69.3",
|
||||||
"start-server-and-test": "^2.0.0",
|
"start-server-and-test": "^2.0.1",
|
||||||
"svgexport": "^0.4.2",
|
"svgexport": "^0.4.2",
|
||||||
"terser": "^5.19.2",
|
"terser": "^5.21.0",
|
||||||
"tslib": "~2.4.0",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "~4.6.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.11",
|
||||||
"vitest": "^0.34.2",
|
"vitest": "^0.34.6",
|
||||||
"vue-tsc": "^1.8.8",
|
"vue-tsc": "^1.8.19",
|
||||||
"yaml-lint": "^1.7.0"
|
"yaml-lint": "^1.7.0"
|
||||||
},
|
},
|
||||||
"//devDependencies": {
|
"//devDependencies": {
|
||||||
"terser": "Used by @vitejs/plugin-legacy for minification",
|
"terser": "Used by @vitejs/plugin-legacy for minification",
|
||||||
"typescript": [
|
"@rushstack/eslint-patch": "Needed by @vue/eslint-config-typescript",
|
||||||
"Cannot upgrade to 5.X.X due to unmaintained @vue/cli-plugin-typescript, https://github.com/vuejs/vue-cli/issues/7401",
|
"@vue/eslint-config-typescript": "Cannot upgrade to 12.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58",
|
||||||
"Cannot upgrade to > 4.6.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252"
|
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58",
|
||||||
],
|
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58"
|
||||||
"tslib": "Cannot upgrade to > 2.4.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252",
|
|
||||||
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60",
|
|
||||||
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60"
|
|
||||||
},
|
},
|
||||||
"homepage": "https://privacy.sexy",
|
"homepage": "https://privacy.sexy",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/undergroundwires/privacy.sexy.git"
|
"url": "https://github.com/undergroundwires/privacy.sexy.git"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"dmg-license": "^1.0.11"
|
||||||
|
},
|
||||||
|
"//optionalDependencies": {
|
||||||
|
"dmg-license": "Required by `electron-builder` for DMG builds on macOS, https://github.com/electron-userland/electron-builder/issues/6489, https://github.com/electron-userland/electron-builder/issues/6520"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
scripts/verify-web-server-status.js
Normal file
62
scripts/verify-web-server-status.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Description:
|
||||||
|
* This script checks if a server, provided as a CLI argument, is up
|
||||||
|
* and returns an HTTP 200 status code.
|
||||||
|
* It is designed to provide easy verification of server availability
|
||||||
|
* and will retry a specified number of times.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node ./scripts/verify-web-server-status.js --url [URL]
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
* --url URL of the server to check
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { get } from 'http';
|
||||||
|
|
||||||
|
const MAX_RETRIES = 30;
|
||||||
|
const RETRY_DELAY_IN_SECONDS = 3;
|
||||||
|
const URL_PARAMETER_NAME = '--url';
|
||||||
|
|
||||||
|
function checkServer(currentRetryCount = 1) {
|
||||||
|
const serverUrl = getServerUrl();
|
||||||
|
console.log(`Requesting ${serverUrl}...`);
|
||||||
|
get(serverUrl, (res) => {
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
console.log('🎊 Success: The server is up and returned HTTP 200.');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log(`Server returned HTTP status code ${res.statusCode}.`);
|
||||||
|
retry(currentRetryCount);
|
||||||
|
}
|
||||||
|
}).on('error', (err) => {
|
||||||
|
console.error('Error making the request:', err);
|
||||||
|
retry(currentRetryCount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function retry(currentRetryCount) {
|
||||||
|
console.log(`Attempt ${currentRetryCount}/${MAX_RETRIES}:`);
|
||||||
|
console.log(`Retrying in ${RETRY_DELAY_IN_SECONDS} seconds.`);
|
||||||
|
|
||||||
|
const remainingTime = (MAX_RETRIES - currentRetryCount) * RETRY_DELAY_IN_SECONDS;
|
||||||
|
console.log(`Time remaining before timeout: ${remainingTime}s`);
|
||||||
|
|
||||||
|
if (currentRetryCount < MAX_RETRIES) {
|
||||||
|
setTimeout(() => checkServer(currentRetryCount + 1), RETRY_DELAY_IN_SECONDS * 1000);
|
||||||
|
} else {
|
||||||
|
console.log('Failure: The server at did not return HTTP 200 within the allocated time. Exiting.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServerUrl() {
|
||||||
|
const urlIndex = process.argv.indexOf(URL_PARAMETER_NAME);
|
||||||
|
if (urlIndex === -1 || urlIndex === process.argv.length - 1) {
|
||||||
|
console.error(`Parameter "${URL_PARAMETER_NAME}" is not provided.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
return process.argv[urlIndex + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
checkServer();
|
||||||
@@ -12,9 +12,6 @@ export class ApplicationFactory implements IApplicationFactory {
|
|||||||
private readonly getter: AsyncLazy<IApplication>;
|
private readonly getter: AsyncLazy<IApplication>;
|
||||||
|
|
||||||
protected constructor(costlyGetter: ApplicationGetterType) {
|
protected constructor(costlyGetter: ApplicationGetterType) {
|
||||||
if (!costlyGetter) {
|
|
||||||
throw new Error('missing getter');
|
|
||||||
}
|
|
||||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
// Compares to Array<T> objects for equality, ignoring order
|
// Compares to Array<T> objects for equality, ignoring order
|
||||||
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('missing first array'); }
|
|
||||||
if (!array2) { throw new Error('missing second array'); }
|
|
||||||
const sortedArray1 = sort(array1);
|
const sortedArray1 = sort(array1);
|
||||||
const sortedArray2 = sort(array2);
|
const sortedArray2 = sort(array2);
|
||||||
return sequenceEqual(sortedArray1, sortedArray2);
|
return sequenceEqual(sortedArray1, sortedArray2);
|
||||||
@@ -12,8 +10,6 @@ export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
|||||||
|
|
||||||
// Compares to Array<T> objects for equality in same order
|
// Compares to Array<T> objects for equality in same order
|
||||||
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('missing first array'); }
|
|
||||||
if (!array2) { throw new Error('missing second array'); }
|
|
||||||
if (array1.length !== array2.length) {
|
if (array1.length !== array2.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,23 +20,30 @@ export abstract class CustomError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Environment = {
|
interface ErrorPrototypeManipulation {
|
||||||
|
getSetPrototypeOf: () => (typeof Object.setPrototypeOf | undefined);
|
||||||
|
getCaptureStackTrace: () => (typeof Error.captureStackTrace | undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlatformErrorPrototypeManipulation: ErrorPrototypeManipulation = {
|
||||||
getSetPrototypeOf: () => Object.setPrototypeOf,
|
getSetPrototypeOf: () => Object.setPrototypeOf,
|
||||||
getCaptureStackTrace: () => Error.captureStackTrace,
|
getCaptureStackTrace: () => Error.captureStackTrace,
|
||||||
};
|
};
|
||||||
|
|
||||||
function fixPrototype(target: Error, prototype: CustomError) {
|
function fixPrototype(target: Error, prototype: CustomError) {
|
||||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
// This is recommended by TypeScript guidelines.
|
||||||
const setPrototypeOf = Environment.getSetPrototypeOf();
|
// Source: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
||||||
if (!functionExists(setPrototypeOf)) {
|
// Snapshots: https://web.archive.org/web/20231111234849/https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget, https://archive.ph/tr7cX#support-for-newtarget
|
||||||
|
const setPrototypeOf = PlatformErrorPrototypeManipulation.getSetPrototypeOf();
|
||||||
|
if (!isFunction(setPrototypeOf)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPrototypeOf(target, prototype);
|
setPrototypeOf(target, prototype);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureStackTrace(target: Error) {
|
function ensureStackTrace(target: Error) {
|
||||||
const captureStackTrace = Environment.getCaptureStackTrace();
|
const captureStackTrace = PlatformErrorPrototypeManipulation.getCaptureStackTrace();
|
||||||
if (!functionExists(captureStackTrace)) {
|
if (!isFunction(captureStackTrace)) {
|
||||||
// captureStackTrace is only available on V8, if it's not available
|
// captureStackTrace is only available on V8, if it's not available
|
||||||
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
|
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
|
||||||
return;
|
return;
|
||||||
@@ -44,7 +51,7 @@ function ensureStackTrace(target: Error) {
|
|||||||
captureStackTrace(target, target.constructor);
|
captureStackTrace(target, target.constructor);
|
||||||
}
|
}
|
||||||
|
|
||||||
function functionExists(func: unknown): boolean {
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
// Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness
|
function isFunction(func: unknown): func is Function {
|
||||||
return typeof func === 'function';
|
return typeof func === 'function';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
|||||||
value: TEnumValue,
|
value: TEnumValue,
|
||||||
enumVariable: EnumVariable<T, TEnumValue>,
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
) {
|
) {
|
||||||
if (value === undefined || value === null) {
|
|
||||||
throw new Error('absent enum value');
|
|
||||||
}
|
|
||||||
if (!(value in enumVariable)) {
|
if (!(value in enumVariable)) {
|
||||||
throw new RangeError(`enum value "${value}" is out of range`);
|
throw new RangeError(`enum value "${value}" is out of range`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,16 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
|
|||||||
|
|
||||||
public create(language: ScriptingLanguage): T {
|
public create(language: ScriptingLanguage): T {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!this.getters.has(language)) {
|
const getter = this.getters.get(language);
|
||||||
|
if (!getter) {
|
||||||
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||||
}
|
}
|
||||||
const getter = this.getters.get(language);
|
|
||||||
const instance = getter();
|
const instance = getter();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!getter) {
|
|
||||||
throw new Error('missing getter');
|
|
||||||
}
|
|
||||||
if (this.getters.has(language)) {
|
if (this.getters.has(language)) {
|
||||||
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/application/Common/Timing/BatchedDebounce.ts
Normal file
27
src/application/Common/Timing/BatchedDebounce.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { PlatformTimer } from './PlatformTimer';
|
||||||
|
import { TimeoutType, Timer } from './Timer';
|
||||||
|
|
||||||
|
export function batchedDebounce<T>(
|
||||||
|
callback: (batches: readonly T[]) => void,
|
||||||
|
waitInMs: number,
|
||||||
|
timer: Timer = PlatformTimer,
|
||||||
|
): (arg: T) => void {
|
||||||
|
let lastTimeoutId: TimeoutType | undefined;
|
||||||
|
let batches: Array<T> = [];
|
||||||
|
|
||||||
|
return (arg: T) => {
|
||||||
|
batches.push(arg);
|
||||||
|
|
||||||
|
const later = () => {
|
||||||
|
callback(batches);
|
||||||
|
batches = [];
|
||||||
|
lastTimeoutId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lastTimeoutId !== undefined) {
|
||||||
|
timer.clearTimeout(lastTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTimeoutId = timer.setTimeout(later, waitInMs);
|
||||||
|
};
|
||||||
|
}
|
||||||
7
src/application/Common/Timing/PlatformTimer.ts
Normal file
7
src/application/Common/Timing/PlatformTimer.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Timer } from './Timer';
|
||||||
|
|
||||||
|
export const PlatformTimer: Timer = {
|
||||||
|
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
||||||
|
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
|
||||||
|
dateNow: () => Date.now(),
|
||||||
|
};
|
||||||
@@ -1,47 +1,29 @@
|
|||||||
|
import { Timer, TimeoutType } from './Timer';
|
||||||
|
import { PlatformTimer } from './PlatformTimer';
|
||||||
|
|
||||||
export type CallbackType = (..._: unknown[]) => void;
|
export type CallbackType = (..._: unknown[]) => void;
|
||||||
|
|
||||||
export function throttle(
|
export function throttle(
|
||||||
callback: CallbackType,
|
callback: CallbackType,
|
||||||
waitInMs: number,
|
waitInMs: number,
|
||||||
timer: ITimer = NodeTimer,
|
timer: Timer = PlatformTimer,
|
||||||
): CallbackType {
|
): CallbackType {
|
||||||
const throttler = new Throttler(timer, waitInMs, callback);
|
const throttler = new Throttler(timer, waitInMs, callback);
|
||||||
return (...args: unknown[]) => throttler.invoke(...args);
|
return (...args: unknown[]) => throttler.invoke(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
|
class Throttler {
|
||||||
export type TimeoutType = ReturnType<typeof setTimeout>;
|
private queuedExecutionId: TimeoutType | undefined;
|
||||||
|
|
||||||
export interface ITimer {
|
|
||||||
setTimeout: (callback: () => void, ms: number) => TimeoutType;
|
|
||||||
clearTimeout: (timeoutId: TimeoutType) => void;
|
|
||||||
dateNow(): number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NodeTimer: ITimer = {
|
|
||||||
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
|
||||||
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
|
|
||||||
dateNow: () => Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IThrottler {
|
|
||||||
invoke: CallbackType;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Throttler implements IThrottler {
|
|
||||||
private queuedExecutionId: TimeoutType;
|
|
||||||
|
|
||||||
private previouslyRun: number;
|
private previouslyRun: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly timer: ITimer,
|
private readonly timer: Timer,
|
||||||
private readonly waitInMs: number,
|
private readonly waitInMs: number,
|
||||||
private readonly callback: CallbackType,
|
private readonly callback: CallbackType,
|
||||||
) {
|
) {
|
||||||
if (!timer) { throw new Error('missing timer'); }
|
|
||||||
if (!waitInMs) { throw new Error('missing delay'); }
|
if (!waitInMs) { throw new Error('missing delay'); }
|
||||||
if (waitInMs < 0) { throw new Error('negative delay'); }
|
if (waitInMs < 0) { throw new Error('negative delay'); }
|
||||||
if (!callback) { throw new Error('missing callback'); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public invoke(...args: unknown[]): void {
|
public invoke(...args: unknown[]): void {
|
||||||
8
src/application/Common/Timing/Timer.ts
Normal file
8
src/application/Common/Timing/Timer.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
|
||||||
|
export type TimeoutType = ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
export interface Timer {
|
||||||
|
setTimeout: (callback: () => void, ms: number) => TimeoutType;
|
||||||
|
clearTimeout: (timeoutId: TimeoutType) => void;
|
||||||
|
dateNow(): number;
|
||||||
|
}
|
||||||
@@ -26,7 +26,6 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
public readonly app: IApplication,
|
public readonly app: IApplication,
|
||||||
initialContext: OperatingSystem,
|
initialContext: OperatingSystem,
|
||||||
) {
|
) {
|
||||||
validateApp(app);
|
|
||||||
this.states = initializeStates(app);
|
this.states = initializeStates(app);
|
||||||
this.changeContext(initialContext);
|
this.changeContext(initialContext);
|
||||||
}
|
}
|
||||||
@@ -36,10 +35,8 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
if (this.currentOs === os) {
|
if (this.currentOs === os) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.collection = this.app.getCollection(os);
|
const collection = this.app.getCollection(os);
|
||||||
if (!this.collection) {
|
this.collection = collection;
|
||||||
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
|
||||||
}
|
|
||||||
const event: IApplicationContextChangedEvent = {
|
const event: IApplicationContextChangedEvent = {
|
||||||
newState: this.states[os],
|
newState: this.states[os],
|
||||||
oldState: this.states[this.currentOs],
|
oldState: this.states[this.currentOs],
|
||||||
@@ -49,12 +46,6 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateApp(app: IApplication) {
|
|
||||||
if (!app) {
|
|
||||||
throw new Error('missing app');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeStates(app: IApplication): StateMachine {
|
function initializeStates(app: IApplication): StateMachine {
|
||||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||||
for (const collection of app.collections) {
|
for (const collection of app.collections) {
|
||||||
|
|||||||
@@ -10,18 +10,23 @@ export async function buildContext(
|
|||||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||||
environment = RuntimeEnvironment.CurrentEnvironment,
|
environment = RuntimeEnvironment.CurrentEnvironment,
|
||||||
): Promise<IApplicationContext> {
|
): Promise<IApplicationContext> {
|
||||||
if (!factory) { throw new Error('missing factory'); }
|
|
||||||
if (!environment) { throw new Error('missing environment'); }
|
|
||||||
const app = await factory.getApp();
|
const app = await factory.getApp();
|
||||||
const os = getInitialOs(app, environment.os);
|
const os = getInitialOs(app, environment.os);
|
||||||
return new ApplicationContext(app, os);
|
return new ApplicationContext(app, os);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
|
function getInitialOs(
|
||||||
|
app: IApplication,
|
||||||
|
currentOs: OperatingSystem | undefined,
|
||||||
|
): OperatingSystem {
|
||||||
const supportedOsList = app.getSupportedOsList();
|
const supportedOsList = app.getSupportedOsList();
|
||||||
if (supportedOsList.includes(currentOs)) {
|
if (currentOs !== undefined && supportedOsList.includes(currentOs)) {
|
||||||
return currentOs;
|
return currentOs;
|
||||||
}
|
}
|
||||||
|
return getMostSupportedOs(supportedOsList, app);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMostSupportedOs(supportedOsList: OperatingSystem[], app: IApplication) {
|
||||||
supportedOsList.sort((os1, os2) => {
|
supportedOsList.sort((os1, os2) => {
|
||||||
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||||
return getPriority(os2) - getPriority(os1);
|
return getPriority(os2) - getPriority(os1);
|
||||||
|
|||||||
@@ -4,23 +4,48 @@ 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';
|
||||||
import { UserSelection } from './Selection/UserSelection';
|
import { UserSelection } from './Selection/UserSelection';
|
||||||
import { IUserSelection } from './Selection/IUserSelection';
|
|
||||||
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
import { UserSelectionFacade } from './Selection/UserSelectionFacade';
|
||||||
|
|
||||||
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: UserSelection;
|
||||||
|
|
||||||
public readonly filter: IUserFilter;
|
public readonly filter: IUserFilter;
|
||||||
|
|
||||||
public constructor(readonly collection: ICategoryCollection) {
|
public constructor(
|
||||||
this.selection = new UserSelection(collection, []);
|
public readonly collection: ICategoryCollection,
|
||||||
this.code = new ApplicationCode(this.selection, collection.scripting);
|
selectionFactory = DefaultSelectionFactory,
|
||||||
this.filter = new UserFilter(collection);
|
codeFactory = DefaultCodeFactory,
|
||||||
|
filterFactory = DefaultFilterFactory,
|
||||||
|
) {
|
||||||
|
this.selection = selectionFactory(collection, []);
|
||||||
|
this.code = codeFactory(this.selection.scripts, collection.scripting);
|
||||||
|
this.filter = filterFactory(collection);
|
||||||
this.os = collection.os;
|
this.os = collection.os;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CodeFactory = (
|
||||||
|
...params: ConstructorParameters<typeof ApplicationCode>
|
||||||
|
) => IApplicationCode;
|
||||||
|
|
||||||
|
const DefaultCodeFactory: CodeFactory = (...params) => new ApplicationCode(...params);
|
||||||
|
|
||||||
|
export type SelectionFactory = (
|
||||||
|
...params: ConstructorParameters<typeof UserSelectionFacade>
|
||||||
|
) => UserSelection;
|
||||||
|
|
||||||
|
const DefaultSelectionFactory: SelectionFactory = (
|
||||||
|
...params
|
||||||
|
) => new UserSelectionFacade(...params);
|
||||||
|
|
||||||
|
export type FilterFactory = (
|
||||||
|
...params: ConstructorParameters<typeof UserFilter>
|
||||||
|
) => IUserFilter;
|
||||||
|
|
||||||
|
const DefaultFilterFactory: FilterFactory = (...params) => new UserFilter(...params);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
|
import { ReadonlyScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
|
||||||
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';
|
||||||
@@ -17,15 +17,12 @@ export class ApplicationCode implements IApplicationCode {
|
|||||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
userSelection: IReadOnlyUserSelection,
|
selection: ReadonlyScriptSelection,
|
||||||
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('missing userSelection'); }
|
this.setCode(selection.selectedScripts);
|
||||||
if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); }
|
selection.changed.on((scripts) => {
|
||||||
if (!generator) { throw new Error('missing generator'); }
|
|
||||||
this.setCode(userSelection.selectedScripts);
|
|
||||||
userSelection.changed.on((scripts) => {
|
|
||||||
this.setCode(scripts);
|
this.setCode(scripts);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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 { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
|
||||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
@@ -36,7 +36,18 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getScriptPositionInCode(script: IScript): ICodePosition {
|
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||||
return this.scripts.get(script);
|
return this.getPositionById(script.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPositionById(scriptId: string): ICodePosition {
|
||||||
|
const position = [...this.scripts.entries()]
|
||||||
|
.filter(([s]) => s.id === scriptId)
|
||||||
|
.map(([, pos]) => pos)
|
||||||
|
.at(0);
|
||||||
|
if (!position) {
|
||||||
|
throw new Error('Unknown script: Position could not be found for the script');
|
||||||
|
}
|
||||||
|
return position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePo
|
|||||||
|
|
||||||
export interface ICodeChangedEvent {
|
export interface ICodeChangedEvent {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
addedScripts: ReadonlyArray<IScript>;
|
readonly addedScripts: ReadonlyArray<IScript>;
|
||||||
removedScripts: ReadonlyArray<IScript>;
|
readonly removedScripts: ReadonlyArray<IScript>;
|
||||||
changedScripts: ReadonlyArray<IScript>;
|
readonly changedScripts: ReadonlyArray<IScript>;
|
||||||
isEmpty(): boolean;
|
isEmpty(): boolean;
|
||||||
getScriptPositionInCode(script: IScript): ICodePosition;
|
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
const lines = code.match(/[^\r\n]+/g);
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
this.lines.push(...lines);
|
if (lines) {
|
||||||
|
this.lines.push(...lines);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
|
|
||||||
export interface IUserScript {
|
export interface IUserScript {
|
||||||
code: string;
|
readonly code: string;
|
||||||
scriptPositions: Map<SelectedScript, ICodePosition>;
|
readonly scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { IUserScript } from './IUserScript';
|
import { IUserScript } from './IUserScript';
|
||||||
|
|
||||||
export interface IUserScriptGenerator {
|
export interface IUserScriptGenerator {
|
||||||
buildCode(
|
buildCode(
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
scriptingDefinition: IScriptingDefinition): IUserScript;
|
scriptingDefinition: IScriptingDefinition,
|
||||||
|
): IUserScript;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { CodePosition } from '../Position/CodePosition';
|
import { CodePosition } from '../Position/CodePosition';
|
||||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||||
import { IUserScript } from './IUserScript';
|
import { IUserScript } from './IUserScript';
|
||||||
@@ -17,8 +17,6 @@ export class UserScriptGenerator implements IUserScriptGenerator {
|
|||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
scriptingDefinition: IScriptingDefinition,
|
scriptingDefinition: IScriptingDefinition,
|
||||||
): IUserScript {
|
): IUserScript {
|
||||||
if (!selectedScripts) { throw new Error('missing scripts'); }
|
|
||||||
if (!scriptingDefinition) { throw new Error('missing definition'); }
|
|
||||||
if (!selectedScripts.length) {
|
if (!selectedScripts.length) {
|
||||||
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
||||||
}
|
}
|
||||||
@@ -68,8 +66,19 @@ function appendSelection(
|
|||||||
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
||||||
const { script } = selection;
|
const { script } = selection;
|
||||||
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
||||||
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
|
const scriptCode = getSelectedCode(selection);
|
||||||
return builder
|
return builder
|
||||||
.appendLine()
|
.appendLine()
|
||||||
.appendFunction(name, scriptCode);
|
.appendFunction(name, scriptCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSelectedCode(selection: SelectedScript): string {
|
||||||
|
const { code } = selection.script;
|
||||||
|
if (!selection.revert) {
|
||||||
|
return code.execute;
|
||||||
|
}
|
||||||
|
if (!code.revert) {
|
||||||
|
throw new Error('Reverted script lacks revert code.');
|
||||||
|
}
|
||||||
|
return code.revert;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { FilterActionType } from './FilterActionType';
|
import { FilterActionType } from './FilterActionType';
|
||||||
import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from './IFilterChangeDetails';
|
import {
|
||||||
|
IFilterChangeDetails, IFilterChangeDetailsVisitor,
|
||||||
|
ApplyFilterAction, ClearFilterAction,
|
||||||
|
} from './IFilterChangeDetails';
|
||||||
|
|
||||||
export class FilterChange implements IFilterChangeDetails {
|
export class FilterChange implements IFilterChangeDetails {
|
||||||
public static forApply(filter: IFilterResult) {
|
public static forApply(
|
||||||
if (!filter) {
|
filter: IFilterResult,
|
||||||
throw new Error('missing filter');
|
): IFilterChangeDetails {
|
||||||
}
|
return new FilterChange({ type: FilterActionType.Apply, filter });
|
||||||
return new FilterChange(FilterActionType.Apply, filter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static forClear() {
|
public static forClear(): IFilterChangeDetails {
|
||||||
return new FilterChange(FilterActionType.Clear);
|
return new FilterChange({ type: FilterActionType.Clear });
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(
|
private constructor(public readonly action: ApplyFilterAction | ClearFilterAction) { }
|
||||||
public readonly actionType: FilterActionType,
|
|
||||||
public readonly filter?: IFilterResult,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
public visit(visitor: IFilterChangeDetailsVisitor): void {
|
public visit(visitor: IFilterChangeDetailsVisitor): void {
|
||||||
if (!visitor) {
|
switch (this.action.type) {
|
||||||
throw new Error('missing visitor');
|
|
||||||
}
|
|
||||||
switch (this.actionType) {
|
|
||||||
case FilterActionType.Apply:
|
case FilterActionType.Apply:
|
||||||
visitor.onApply(this.filter);
|
if (visitor.onApply) {
|
||||||
|
visitor.onApply(this.action.filter);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case FilterActionType.Clear:
|
case FilterActionType.Clear:
|
||||||
visitor.onClear();
|
if (visitor.onClear) {
|
||||||
|
visitor.onClear();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown action type: ${this.actionType}`);
|
throw new Error(`Unknown action: ${this.action}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,22 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
|
|||||||
import { FilterActionType } from './FilterActionType';
|
import { FilterActionType } from './FilterActionType';
|
||||||
|
|
||||||
export interface IFilterChangeDetails {
|
export interface IFilterChangeDetails {
|
||||||
readonly actionType: FilterActionType;
|
readonly action: FilterAction;
|
||||||
readonly filter?: IFilterResult;
|
|
||||||
|
|
||||||
visit(visitor: IFilterChangeDetailsVisitor): void;
|
visit(visitor: IFilterChangeDetailsVisitor): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFilterChangeDetailsVisitor {
|
export interface IFilterChangeDetailsVisitor {
|
||||||
onClear(): void;
|
readonly onClear?: () => void;
|
||||||
onApply(filter: IFilterResult): void;
|
readonly onApply?: (filter: IFilterResult) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ApplyFilterAction = {
|
||||||
|
readonly type: FilterActionType.Apply,
|
||||||
|
readonly filter: IFilterResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClearFilterAction = {
|
||||||
|
readonly type: FilterActionType.Clear,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FilterAction = ApplyFilterAction | ClearFilterAction;
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ export class FilterResult implements IFilterResult {
|
|||||||
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 (!categoryMatches) { throw new Error('Category matches is undefined'); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasAnyMatches(): boolean {
|
public hasAnyMatches(): boolean {
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
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 { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
|
||||||
import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
|
import { ReadonlyUserSelection, UserSelection } from './Selection/UserSelection';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
|
||||||
export interface IReadOnlyCategoryCollectionState {
|
export interface IReadOnlyCategoryCollectionState {
|
||||||
readonly code: IApplicationCode;
|
readonly code: IApplicationCode;
|
||||||
readonly os: OperatingSystem;
|
readonly os: OperatingSystem;
|
||||||
readonly filter: IReadOnlyUserFilter;
|
readonly filter: IReadOnlyUserFilter;
|
||||||
readonly selection: IReadOnlyUserSelection;
|
readonly selection: ReadonlyUserSelection;
|
||||||
readonly collection: ICategoryCollection;
|
readonly collection: ICategoryCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
|
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
|
||||||
readonly filter: IUserFilter;
|
readonly filter: IUserFilter;
|
||||||
readonly selection: IUserSelection;
|
readonly selection: UserSelection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
|
|
||||||
|
export interface ReadonlyCategorySelection {
|
||||||
|
areAllScriptsSelected(category: ICategory): boolean;
|
||||||
|
isAnyScriptSelected(category: ICategory): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategorySelection extends ReadonlyCategorySelection {
|
||||||
|
processChanges(action: CategorySelectionChangeCommand): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
type CategorySelectionStatus = {
|
||||||
|
readonly isSelected: true;
|
||||||
|
readonly isReverted: boolean;
|
||||||
|
} | {
|
||||||
|
readonly isSelected: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CategorySelectionChange {
|
||||||
|
readonly categoryId: number;
|
||||||
|
readonly newStatus: CategorySelectionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategorySelectionChangeCommand {
|
||||||
|
readonly changes: readonly CategorySelectionChange[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { ScriptSelection } from '../Script/ScriptSelection';
|
||||||
|
import { ScriptSelectionChange } from '../Script/ScriptSelectionChange';
|
||||||
|
import { CategorySelection } from './CategorySelection';
|
||||||
|
import { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
|
|
||||||
|
export class ScriptToCategorySelectionMapper implements CategorySelection {
|
||||||
|
constructor(
|
||||||
|
private readonly scriptSelection: ScriptSelection,
|
||||||
|
private readonly collection: ICategoryCollection,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public areAllScriptsSelected(category: ICategory): boolean {
|
||||||
|
const { selectedScripts } = this.scriptSelection;
|
||||||
|
if (selectedScripts.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const scripts = category.getAllScriptsRecursively();
|
||||||
|
if (selectedScripts.length < scripts.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return scripts.every(
|
||||||
|
(script) => selectedScripts.some((selected) => selected.id === script.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAnyScriptSelected(category: ICategory): boolean {
|
||||||
|
const { selectedScripts } = this.scriptSelection;
|
||||||
|
if (selectedScripts.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return selectedScripts.some((s) => category.includes(s.script));
|
||||||
|
}
|
||||||
|
|
||||||
|
public processChanges(action: CategorySelectionChangeCommand): void {
|
||||||
|
const scriptChanges = action.changes.reduce((changes, change) => {
|
||||||
|
changes.push(...this.collectScriptChanges(change));
|
||||||
|
return changes;
|
||||||
|
}, new Array<ScriptSelectionChange>());
|
||||||
|
this.scriptSelection.processChanges({
|
||||||
|
changes: scriptChanges,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private collectScriptChanges(change: CategorySelectionChange): ScriptSelectionChange[] {
|
||||||
|
const category = this.collection.getCategory(change.categoryId);
|
||||||
|
const scripts = category.getAllScriptsRecursively();
|
||||||
|
const scriptsChangesInCategory = scripts
|
||||||
|
.map((script): ScriptSelectionChange => ({
|
||||||
|
scriptId: script.id,
|
||||||
|
newStatus: {
|
||||||
|
...change.newStatus,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return scriptsChangesInCategory;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
|
||||||
import { ICategory } from '@/domain/ICategory';
|
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
|
||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
|
|
||||||
export interface IReadOnlyUserSelection {
|
|
||||||
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
|
||||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
|
||||||
isSelected(scriptId: string): boolean;
|
|
||||||
areAllSelected(category: ICategory): boolean;
|
|
||||||
isAnySelected(category: ICategory): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IUserSelection extends IReadOnlyUserSelection {
|
|
||||||
removeAllInCategory(categoryId: number): void;
|
|
||||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
|
||||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
|
||||||
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
|
||||||
removeSelectedScript(scriptId: string): void;
|
|
||||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
|
||||||
selectAll(): void;
|
|
||||||
deselectAll(): void;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
||||||
|
import { ScriptSelection } from './ScriptSelection';
|
||||||
|
import { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
import { UserSelectedScript } from './UserSelectedScript';
|
||||||
|
|
||||||
|
const DEBOUNCE_DELAY_IN_MS = 100;
|
||||||
|
|
||||||
|
export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeCommand>;
|
||||||
|
|
||||||
|
export class DebouncedScriptSelection implements ScriptSelection {
|
||||||
|
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||||
|
|
||||||
|
private readonly scripts: Repository<string, SelectedScript>;
|
||||||
|
|
||||||
|
public readonly processChanges: ScriptSelection['processChanges'];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly collection: ICategoryCollection,
|
||||||
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
debounce: DebounceFunction = batchedDebounce,
|
||||||
|
) {
|
||||||
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
|
for (const script of selectedScripts) {
|
||||||
|
this.scripts.addItem(script);
|
||||||
|
}
|
||||||
|
this.processChanges = debounce(
|
||||||
|
(batchedRequests: readonly ScriptSelectionChangeCommand[]) => {
|
||||||
|
const consolidatedChanges = batchedRequests.flatMap((request) => request.changes);
|
||||||
|
this.processScriptChanges(consolidatedChanges);
|
||||||
|
},
|
||||||
|
DEBOUNCE_DELAY_IN_MS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isSelected(scriptId: string): boolean {
|
||||||
|
return this.scripts.exists(scriptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get selectedScripts(): readonly SelectedScript[] {
|
||||||
|
return this.scripts.getItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectAll(): void {
|
||||||
|
const scriptsToSelect = this.collection
|
||||||
|
.getAllScripts()
|
||||||
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
|
.map((script) => new UserSelectedScript(script, false));
|
||||||
|
if (scriptsToSelect.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.processChanges({
|
||||||
|
changes: scriptsToSelect.map((script): ScriptSelectionChange => ({
|
||||||
|
scriptId: script.id,
|
||||||
|
newStatus: {
|
||||||
|
isSelected: true,
|
||||||
|
isReverted: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public deselectAll(): void {
|
||||||
|
if (this.scripts.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
||||||
|
this.processChanges({
|
||||||
|
changes: selectedScriptIds.map((scriptId): ScriptSelectionChange => ({
|
||||||
|
scriptId,
|
||||||
|
newStatus: {
|
||||||
|
isSelected: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectOnly(scripts: readonly IScript[]): void {
|
||||||
|
if (scripts.length === 0) {
|
||||||
|
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
|
||||||
|
}
|
||||||
|
this.processChanges({
|
||||||
|
changes: [
|
||||||
|
...getScriptIdsToBeDeselected(this.scripts, scripts)
|
||||||
|
.map((scriptId): ScriptSelectionChange => ({
|
||||||
|
scriptId,
|
||||||
|
newStatus: {
|
||||||
|
isSelected: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...getScriptIdsToBeSelected(this.scripts, scripts)
|
||||||
|
.map((scriptId): ScriptSelectionChange => ({
|
||||||
|
scriptId,
|
||||||
|
newStatus: {
|
||||||
|
isSelected: true,
|
||||||
|
isReverted: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processScriptChanges(changes: readonly ScriptSelectionChange[]): void {
|
||||||
|
let totalChanged = 0;
|
||||||
|
for (const change of changes) {
|
||||||
|
totalChanged += this.applyChange(change);
|
||||||
|
}
|
||||||
|
if (totalChanged > 0) {
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyChange(change: ScriptSelectionChange): number {
|
||||||
|
const script = this.collection.getScript(change.scriptId);
|
||||||
|
if (change.newStatus.isSelected) {
|
||||||
|
return this.addOrUpdateScript(script.id, change.newStatus.isReverted);
|
||||||
|
}
|
||||||
|
return this.removeScript(script.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addOrUpdateScript(scriptId: string, revert: boolean): number {
|
||||||
|
const script = this.collection.getScript(scriptId);
|
||||||
|
const selectedScript = new UserSelectedScript(script, revert);
|
||||||
|
if (!this.scripts.exists(selectedScript.id)) {
|
||||||
|
this.scripts.addItem(selectedScript);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const existingSelectedScript = this.scripts.getById(selectedScript.id);
|
||||||
|
if (equals(selectedScript, existingSelectedScript)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
this.scripts.addOrUpdateItem(selectedScript);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeScript(scriptId: string): number {
|
||||||
|
if (!this.scripts.exists(scriptId)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
this.scripts.removeItem(scriptId);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScriptIdsToBeSelected(
|
||||||
|
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||||
|
desiredScripts: readonly IScript[],
|
||||||
|
): string[] {
|
||||||
|
return desiredScripts
|
||||||
|
.filter((script) => !existingItems.exists(script.id))
|
||||||
|
.map((script) => script.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScriptIdsToBeDeselected(
|
||||||
|
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||||
|
desiredScripts: readonly IScript[],
|
||||||
|
): string[] {
|
||||||
|
return existingItems
|
||||||
|
.getItems()
|
||||||
|
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
|
||||||
|
.map((script) => script.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function equals(a: SelectedScript, b: SelectedScript): boolean {
|
||||||
|
return a.script.equals(b.script.id) && a.revert === b.revert;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
import { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
|
|
||||||
|
export interface ReadonlyScriptSelection {
|
||||||
|
readonly changed: IEventSource<readonly SelectedScript[]>;
|
||||||
|
readonly selectedScripts: readonly SelectedScript[];
|
||||||
|
isSelected(scriptId: string): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptSelection extends ReadonlyScriptSelection {
|
||||||
|
selectOnly(scripts: readonly IScript[]): void;
|
||||||
|
selectAll(): void;
|
||||||
|
deselectAll(): void;
|
||||||
|
processChanges(action: ScriptSelectionChangeCommand): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export type ScriptSelectionStatus = {
|
||||||
|
readonly isSelected: true;
|
||||||
|
readonly isReverted: boolean;
|
||||||
|
} | {
|
||||||
|
readonly isSelected: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ScriptSelectionChange {
|
||||||
|
readonly scriptId: string;
|
||||||
|
readonly newStatus: ScriptSelectionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptSelectionChangeCommand {
|
||||||
|
readonly changes: ReadonlyArray<ScriptSelectionChange>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
|
type ScriptId = IScript['id'];
|
||||||
|
|
||||||
|
export interface SelectedScript extends IEntity<ScriptId> {
|
||||||
|
readonly script: IScript;
|
||||||
|
readonly revert: boolean;
|
||||||
|
}
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
|
||||||
export class SelectedScript extends BaseEntity<string> {
|
type SelectedScriptId = SelectedScript['id'];
|
||||||
|
|
||||||
|
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly script: IScript,
|
public readonly script: IScript,
|
||||||
public readonly revert: boolean,
|
public readonly revert: boolean,
|
||||||
) {
|
) {
|
||||||
super(script.id);
|
super(script.id);
|
||||||
if (revert && !script.canRevert()) {
|
if (revert && !script.canRevert()) {
|
||||||
throw new Error('cannot revert an irreversible script');
|
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,167 +1,12 @@
|
|||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { CategorySelection, ReadonlyCategorySelection } from './Category/CategorySelection';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { ReadonlyScriptSelection, ScriptSelection } from './Script/ScriptSelection';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
|
||||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
|
||||||
import { ICategory } from '@/domain/ICategory';
|
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
|
||||||
import { IUserSelection } from './IUserSelection';
|
|
||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
|
|
||||||
export class UserSelection implements IUserSelection {
|
export interface ReadonlyUserSelection {
|
||||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
readonly categories: ReadonlyCategorySelection;
|
||||||
|
readonly scripts: ReadonlyScriptSelection;
|
||||||
private readonly scripts: IRepository<string, SelectedScript>;
|
}
|
||||||
|
|
||||||
constructor(
|
export interface UserSelection extends ReadonlyUserSelection {
|
||||||
private readonly collection: ICategoryCollection,
|
readonly categories: CategorySelection;
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
readonly scripts: ScriptSelection;
|
||||||
) {
|
|
||||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
|
||||||
for (const script of selectedScripts) {
|
|
||||||
this.scripts.addItem(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public areAllSelected(category: ICategory): boolean {
|
|
||||||
if (this.selectedScripts.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const scripts = category.getAllScriptsRecursively();
|
|
||||||
if (this.selectedScripts.length < scripts.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return scripts.every(
|
|
||||||
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public isAnySelected(category: ICategory): boolean {
|
|
||||||
if (this.selectedScripts.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.selectedScripts.some((s) => category.includes(s.script));
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeAllInCategory(categoryId: number): void {
|
|
||||||
const category = this.collection.findCategory(categoryId);
|
|
||||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
|
||||||
.filter((script) => this.scripts.exists(script.id));
|
|
||||||
if (!scriptsToRemove.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const script of scriptsToRemove) {
|
|
||||||
this.scripts.removeItem(script.id);
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
|
||||||
const scriptsToAddOrUpdate = this.collection
|
|
||||||
.findCategory(categoryId)
|
|
||||||
.getAllScriptsRecursively()
|
|
||||||
.filter(
|
|
||||||
(script) => !this.scripts.exists(script.id)
|
|
||||||
|| this.scripts.getById(script.id).revert !== revert,
|
|
||||||
)
|
|
||||||
.map((script) => new SelectedScript(script, revert));
|
|
||||||
if (!scriptsToAddOrUpdate.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const script of scriptsToAddOrUpdate) {
|
|
||||||
this.scripts.addOrUpdateItem(script);
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
|
||||||
const script = this.collection.findScript(scriptId);
|
|
||||||
if (!script) {
|
|
||||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
|
||||||
}
|
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
|
||||||
this.scripts.addItem(selectedScript);
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
|
||||||
const script = this.collection.findScript(scriptId);
|
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
|
||||||
this.scripts.addOrUpdateItem(selectedScript);
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeSelectedScript(scriptId: string): void {
|
|
||||||
this.scripts.removeItem(scriptId);
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public isSelected(scriptId: string): boolean {
|
|
||||||
return this.scripts.exists(scriptId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get users scripts based on his/her selections */
|
|
||||||
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
|
||||||
return this.scripts.getItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectAll(): void {
|
|
||||||
const scriptsToSelect = this.collection
|
|
||||||
.getAllScripts()
|
|
||||||
.filter((script) => !this.scripts.exists(script.id))
|
|
||||||
.map((script) => new SelectedScript(script, false));
|
|
||||||
if (scriptsToSelect.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const script of scriptsToSelect) {
|
|
||||||
this.scripts.addItem(script);
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public deselectAll(): void {
|
|
||||||
if (this.scripts.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
|
||||||
for (const scriptId of selectedScriptIds) {
|
|
||||||
this.scripts.removeItem(scriptId);
|
|
||||||
}
|
|
||||||
this.changed.notify([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectOnly(scripts: readonly IScript[]): void {
|
|
||||||
if (!scripts || scripts.length === 0) {
|
|
||||||
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
|
||||||
}
|
|
||||||
let totalChanged = 0;
|
|
||||||
totalChanged += this.unselectMissingWithoutNotifying(scripts);
|
|
||||||
totalChanged += this.selectNewWithoutNotifying(scripts);
|
|
||||||
if (totalChanged > 0) {
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unselectMissingWithoutNotifying(scripts: readonly IScript[]): number {
|
|
||||||
if (this.scripts.length === 0 || scripts.length === 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const existingItems = this.scripts.getItems();
|
|
||||||
const missingIds = existingItems
|
|
||||||
.filter((existing) => !scripts.some((script) => existing.id === script.id))
|
|
||||||
.map((script) => script.id);
|
|
||||||
for (const id of missingIds) {
|
|
||||||
this.scripts.removeItem(id);
|
|
||||||
}
|
|
||||||
return missingIds.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private selectNewWithoutNotifying(scripts: readonly IScript[]): number {
|
|
||||||
const unselectedScripts = scripts
|
|
||||||
.filter((script) => !this.scripts.exists(script.id))
|
|
||||||
.map((script) => new SelectedScript(script, false));
|
|
||||||
for (const newScript of unselectedScripts) {
|
|
||||||
this.scripts.addItem(newScript);
|
|
||||||
}
|
|
||||||
return unselectedScripts.length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { CategorySelection } from './Category/CategorySelection';
|
||||||
|
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
||||||
|
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
||||||
|
import { ScriptSelection } from './Script/ScriptSelection';
|
||||||
|
import { UserSelection } from './UserSelection';
|
||||||
|
import { SelectedScript } from './Script/SelectedScript';
|
||||||
|
|
||||||
|
export class UserSelectionFacade implements UserSelection {
|
||||||
|
public readonly categories: CategorySelection;
|
||||||
|
|
||||||
|
public readonly scripts: ScriptSelection;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
collection: ICategoryCollection,
|
||||||
|
selectedScripts: readonly SelectedScript[],
|
||||||
|
scriptsFactory = DefaultScriptsFactory,
|
||||||
|
categoriesFactory = DefaultCategoriesFactory,
|
||||||
|
) {
|
||||||
|
this.scripts = scriptsFactory(collection, selectedScripts);
|
||||||
|
this.categories = categoriesFactory(this.scripts, collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScriptsFactory = (
|
||||||
|
...params: ConstructorParameters<typeof DebouncedScriptSelection>
|
||||||
|
) => ScriptSelection;
|
||||||
|
|
||||||
|
const DefaultScriptsFactory: ScriptsFactory = (
|
||||||
|
...params
|
||||||
|
) => new DebouncedScriptSelection(...params);
|
||||||
|
|
||||||
|
export type CategoriesFactory = (
|
||||||
|
...params: ConstructorParameters<typeof ScriptToCategorySelectionMapper>
|
||||||
|
) => CategorySelection;
|
||||||
|
|
||||||
|
const DefaultCategoriesFactory: CategoriesFactory = (
|
||||||
|
...params
|
||||||
|
) => new ScriptToCategorySelectionMapper(...params);
|
||||||
@@ -32,10 +32,7 @@ const PreParsedCollections: readonly CollectionData [] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
if (!collections?.length) {
|
if (!collections.length) {
|
||||||
throw new Error('missing collections');
|
throw new Error('missing collections');
|
||||||
}
|
}
|
||||||
if (collections.some((collection) => !collection)) {
|
|
||||||
throw new Error('missing collection provided');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ export function parseCategoryCollection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validate(content: CollectionData): void {
|
function validate(content: CollectionData): void {
|
||||||
if (!content) {
|
if (!content.actions.length) {
|
||||||
throw new Error('missing content');
|
|
||||||
}
|
|
||||||
if (!content.actions || content.actions.length <= 0) {
|
|
||||||
throw new Error('content does not define any action');
|
throw new Error('content does not define any action');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
|
CategoryData, ScriptData, CategoryOrScriptData,
|
||||||
} from '@/application/collections/';
|
} from '@/application/collections/';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
@@ -16,7 +16,6 @@ export function parseCategory(
|
|||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
factory: CategoryFactoryType = CategoryFactory,
|
factory: CategoryFactoryType = CategoryFactory,
|
||||||
): Category {
|
): Category {
|
||||||
if (!context) { throw new Error('missing context'); }
|
|
||||||
return parseCategoryRecursively({
|
return parseCategoryRecursively({
|
||||||
categoryData: category,
|
categoryData: category,
|
||||||
context,
|
context,
|
||||||
@@ -30,8 +29,8 @@ interface ICategoryParseContext {
|
|||||||
readonly factory: CategoryFactoryType,
|
readonly factory: CategoryFactoryType,
|
||||||
readonly parentCategory?: CategoryData,
|
readonly parentCategory?: CategoryData,
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
|
||||||
ensureValidCategory(context.categoryData, context.parentCategory);
|
ensureValidCategory(context.categoryData, context.parentCategory);
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
subCategories: new Array<Category>(),
|
subCategories: new Array<Category>(),
|
||||||
@@ -55,7 +54,7 @@ function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
|||||||
/* scripts: */ children.subScripts,
|
/* scripts: */ children.subScripts,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
new NodeValidator({
|
return new NodeValidator({
|
||||||
type: NodeType.Category,
|
type: NodeType.Category,
|
||||||
selfNode: context.categoryData,
|
selfNode: context.categoryData,
|
||||||
parentNode: context.parentCategory,
|
parentNode: context.parentCategory,
|
||||||
@@ -72,7 +71,7 @@ function ensureValidCategory(category: CategoryData, parentCategory?: CategoryDa
|
|||||||
.assertDefined(category)
|
.assertDefined(category)
|
||||||
.assertValidName(category.category)
|
.assertValidName(category.category)
|
||||||
.assert(
|
.assert(
|
||||||
() => category.children && category.children.length > 0,
|
() => category.children.length > 0,
|
||||||
`"${category.category}" has no children.`,
|
`"${category.category}" has no children.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -94,14 +93,14 @@ function parseNode(context: INodeParseContext) {
|
|||||||
validator.assertDefined(context.nodeData);
|
validator.assertDefined(context.nodeData);
|
||||||
if (isCategory(context.nodeData)) {
|
if (isCategory(context.nodeData)) {
|
||||||
const subCategory = parseCategoryRecursively({
|
const subCategory = parseCategoryRecursively({
|
||||||
categoryData: context.nodeData as CategoryData,
|
categoryData: context.nodeData,
|
||||||
context: context.context,
|
context: context.context,
|
||||||
factory: context.factory,
|
factory: context.factory,
|
||||||
parentCategory: context.parent,
|
parentCategory: context.parent,
|
||||||
});
|
});
|
||||||
context.children.subCategories.push(subCategory);
|
context.children.subCategories.push(subCategory);
|
||||||
} else if (isScript(context.nodeData)) {
|
} else if (isScript(context.nodeData)) {
|
||||||
const script = parseScript(context.nodeData as ScriptData, context.context);
|
const script = parseScript(context.nodeData, context.context);
|
||||||
context.children.subScripts.push(script);
|
context.children.subScripts.push(script);
|
||||||
} else {
|
} else {
|
||||||
validator.throw('Node is neither a category or a script.');
|
validator.throw('Node is neither a category or a script.');
|
||||||
@@ -109,19 +108,18 @@ function parseNode(context: INodeParseContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||||
const holder = (data as InstructionHolder);
|
return hasCode(data) || hasCall(data);
|
||||||
return hasCode(holder) || hasCall(holder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||||
return hasProperty(data, 'category');
|
return hasProperty(data, 'category');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(data: InstructionHolder): boolean {
|
function hasCode(data: unknown): boolean {
|
||||||
return hasProperty(data, 'code');
|
return hasProperty(data, 'code');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCall(data: InstructionHolder) {
|
function hasCall(data: unknown) {
|
||||||
return hasProperty(data, 'call');
|
return hasProperty(data, 'call');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||||
|
|
||||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||||
if (!documentable) {
|
|
||||||
throw new Error('missing documentable');
|
|
||||||
}
|
|
||||||
const { docs } = documentable;
|
const { docs } = documentable;
|
||||||
if (!docs) {
|
if (!docs) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class NodeValidator {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public throw(errorMessage: string) {
|
public throw(errorMessage: string): never {
|
||||||
throw new NodeDataError(errorMessage, this.context);
|
throw new NodeDataError(errorMessage, this.context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ export class CategoryCollectionParseContext implements ICategoryCollectionParseC
|
|||||||
scripting: IScriptingDefinition,
|
scripting: IScriptingDefinition,
|
||||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||||
) {
|
) {
|
||||||
if (!scripting) { throw new Error('missing 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,19 +15,10 @@ export class Expression implements IExpression {
|
|||||||
public readonly evaluator: ExpressionEvaluator,
|
public readonly evaluator: ExpressionEvaluator,
|
||||||
parameters?: IReadOnlyFunctionParameterCollection,
|
parameters?: IReadOnlyFunctionParameterCollection,
|
||||||
) {
|
) {
|
||||||
if (!position) {
|
|
||||||
throw new Error('missing position');
|
|
||||||
}
|
|
||||||
if (!evaluator) {
|
|
||||||
throw new Error('missing evaluator');
|
|
||||||
}
|
|
||||||
this.parameters = parameters ?? new FunctionParameterCollection();
|
this.parameters = parameters ?? new FunctionParameterCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(context: IExpressionEvaluationContext): string {
|
public evaluate(context: IExpressionEvaluationContext): string {
|
||||||
if (!context) {
|
|
||||||
throw new Error('missing context');
|
|
||||||
}
|
|
||||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||||
const args = filterUnusedArguments(this.parameters, context.args);
|
const args = filterUnusedArguments(this.parameters, context.args);
|
||||||
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||||
|
|||||||
@@ -12,8 +12,5 @@ export class ExpressionEvaluationContext implements IExpressionEvaluationContext
|
|||||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
||||||
) {
|
) {
|
||||||
if (!args) {
|
|
||||||
throw new Error('missing args, send empty collection instead.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
|
|
||||||
|
export function createPositionFromRegexFullMatch(
|
||||||
|
match: RegExpMatchArray,
|
||||||
|
): ExpressionPosition {
|
||||||
|
const startPos = match.index;
|
||||||
|
if (startPos === undefined) {
|
||||||
|
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
|
||||||
|
}
|
||||||
|
const fullMatch = match[0];
|
||||||
|
if (!fullMatch.length) {
|
||||||
|
throw new Error(`Regex match is empty: ${JSON.stringify(match)}`);
|
||||||
|
}
|
||||||
|
const endPos = startPos + fullMatch.length;
|
||||||
|
return new ExpressionPosition(startPos, endPos);
|
||||||
|
}
|
||||||
@@ -11,14 +11,11 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
public compileExpressions(
|
public compileExpressions(
|
||||||
code: string | undefined,
|
code: string,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
): string {
|
): string {
|
||||||
if (!args) {
|
|
||||||
throw new Error('missing args, send empty collection instead.');
|
|
||||||
}
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return code;
|
return '';
|
||||||
}
|
}
|
||||||
const context = new ExpressionEvaluationContext(args);
|
const context = new ExpressionEvaluationContext(args);
|
||||||
const compiledCode = compileRecursively(code, context, this.extractor);
|
const compiledCode = compileRecursively(code, context, this.extractor);
|
||||||
@@ -145,7 +142,7 @@ function ensureParamsUsedInCodeHasArgsProvided(
|
|||||||
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
): void {
|
): void {
|
||||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||||
if (!usedParameterNames?.length) {
|
if (!usedParameterNames.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const notProvidedParameters = usedParameterNames
|
const notProvidedParameters = usedParameterNames
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argume
|
|||||||
|
|
||||||
export interface IExpressionsCompiler {
|
export interface IExpressionsCompiler {
|
||||||
compileExpressions(
|
compileExpressions(
|
||||||
code: string | undefined,
|
code: string,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection): string;
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,9 @@ const Parsers = [
|
|||||||
|
|
||||||
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) {
|
if (!leafs.length) {
|
||||||
throw new Error('missing leafs');
|
throw new Error('missing leafs');
|
||||||
}
|
}
|
||||||
if (leafs.some((leaf) => !leaf)) {
|
|
||||||
throw new Error('missing leaf');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
|
|||||||
@@ -14,45 +14,44 @@ export class ExpressionRegexBuilder {
|
|||||||
.addRawRegex('\\s+');
|
.addRawRegex('\\s+');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchPipeline() {
|
public captureOptionalPipeline() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.addRawRegex('((?:\\|\\s*\\b[a-zA-Z]+\\b\\s*)*)');
|
||||||
.addRawRegex('(\\|\\s*.+?)?');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchUntilFirstWhitespace() {
|
public captureUntilWhitespaceOrPipe() {
|
||||||
return this
|
return this
|
||||||
.addRawRegex('([^|\\s]+)');
|
.addRawRegex('([^|\\s]+)');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchMultilineAnythingExceptSurroundingWhitespaces() {
|
public captureMultilineAnythingExceptSurroundingWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectOptionalWhitespaces()
|
||||||
.addRawRegex('([\\S\\s]+?)')
|
.addRawRegex('([\\s\\S]*\\S)')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectOptionalWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectExpressionStart() {
|
public expectExpressionStart() {
|
||||||
return this
|
return this
|
||||||
.expectCharacters('{{')
|
.expectCharacters('{{')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectOptionalWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectExpressionEnd() {
|
public expectExpressionEnd() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectOptionalWhitespaces()
|
||||||
.expectCharacters('}}');
|
.expectCharacters('}}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public expectOptionalWhitespaces() {
|
||||||
|
return this
|
||||||
|
.addRawRegex('\\s*');
|
||||||
|
}
|
||||||
|
|
||||||
public buildRegExp(): RegExp {
|
public buildRegExp(): RegExp {
|
||||||
return new RegExp(this.parts.join(''), 'g');
|
return new RegExp(this.parts.join(''), 'g');
|
||||||
}
|
}
|
||||||
|
|
||||||
private expectZeroOrMoreWhitespaces() {
|
|
||||||
return this
|
|
||||||
.addRawRegex('\\s*');
|
|
||||||
}
|
|
||||||
|
|
||||||
private addRawRegex(regex: string) {
|
private addRawRegex(regex: string) {
|
||||||
this.parts.push(regex);
|
this.parts.push(regex);
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IExpressionParser } from '../IExpressionParser';
|
import { IExpressionParser } from '../IExpressionParser';
|
||||||
import { ExpressionPosition } from '../../Expression/ExpressionPosition';
|
|
||||||
import { IExpression } from '../../Expression/IExpression';
|
import { IExpression } from '../../Expression/IExpression';
|
||||||
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
|
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
|
||||||
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
||||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||||
|
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
|
||||||
|
|
||||||
export abstract class RegexParser implements IExpressionParser {
|
export abstract class RegexParser implements IExpressionParser {
|
||||||
protected abstract readonly regex: RegExp;
|
protected abstract readonly regex: RegExp;
|
||||||
@@ -21,7 +21,7 @@ export abstract class RegexParser implements IExpressionParser {
|
|||||||
const matches = code.matchAll(this.regex);
|
const matches = code.matchAll(this.regex);
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const primitiveExpression = this.buildExpression(match);
|
const primitiveExpression = this.buildExpression(match);
|
||||||
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
|
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code);
|
||||||
const parameters = createParameters(primitiveExpression);
|
const parameters = createParameters(primitiveExpression);
|
||||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||||
yield expression;
|
yield expression;
|
||||||
@@ -37,12 +37,6 @@ export abstract class RegexParser implements IExpressionParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPosition(match: RegExpMatchArray): ExpressionPosition {
|
|
||||||
const startPos = match.index;
|
|
||||||
const endPos = startPos + match[0].length;
|
|
||||||
return new ExpressionPosition(startPos, endPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createParameters(
|
function createParameters(
|
||||||
expression: IPrimitiveExpression,
|
expression: IPrimitiveExpression,
|
||||||
): FunctionParameterCollection {
|
): FunctionParameterCollection {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function hasLines(text: string) {
|
|||||||
*/
|
*/
|
||||||
function inlineComments(code: string): string {
|
function inlineComments(code: string): string {
|
||||||
const makeInlineComment = (comment: string) => {
|
const makeInlineComment = (comment: string) => {
|
||||||
const value = comment?.trim();
|
const value = comment.trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '<##>';
|
return '<##>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
private readonly pipes = new Map<string, IPipe>();
|
private readonly pipes = new Map<string, IPipe>();
|
||||||
|
|
||||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||||
if (!pipes) {
|
|
||||||
throw new Error('missing pipes');
|
|
||||||
}
|
|
||||||
if (pipes.some((pipe) => !pipe)) {
|
|
||||||
throw new Error('missing pipe in list');
|
|
||||||
}
|
|
||||||
for (const pipe of pipes) {
|
for (const pipe of pipes) {
|
||||||
this.registerPipe(pipe);
|
this.registerPipe(pipe);
|
||||||
}
|
}
|
||||||
@@ -28,10 +22,11 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
|
|
||||||
public get(pipeName: string): IPipe {
|
public get(pipeName: string): IPipe {
|
||||||
validatePipeName(pipeName);
|
validatePipeName(pipeName);
|
||||||
if (!this.pipes.has(pipeName)) {
|
const pipe = this.pipes.get(pipeName);
|
||||||
|
if (!pipe) {
|
||||||
throw new Error(`Unknown pipe: "${pipeName}"`);
|
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||||
}
|
}
|
||||||
return this.pipes.get(pipeName);
|
return pipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerPipe(pipe: IPipe): void {
|
private registerPipe(pipe: IPipe): void {
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ export class ParameterSubstitutionParser extends RegexParser {
|
|||||||
protected readonly regex = new ExpressionRegexBuilder()
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('$')
|
.expectCharacters('$')
|
||||||
.matchUntilFirstWhitespace() // First match: Parameter name
|
.captureUntilWhitespaceOrPipe() // First capture: Parameter name
|
||||||
.matchPipeline() // Second match: Pipeline
|
.expectOptionalWhitespaces()
|
||||||
|
.captureOptionalPipeline() // Second capture: Pipeline
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,220 @@
|
|||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
|
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
|
||||||
|
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
import { IExpression } from '../Expression/IExpression';
|
||||||
|
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
|
||||||
|
|
||||||
export class WithParser extends RegexParser {
|
export class WithParser implements IExpressionParser {
|
||||||
protected readonly regex = new ExpressionRegexBuilder()
|
public findExpressions(code: string): IExpression[] {
|
||||||
// {{ with $parameterName }}
|
if (!code) {
|
||||||
.expectExpressionStart()
|
throw new Error('missing code');
|
||||||
.expectCharacters('with')
|
}
|
||||||
.expectOneOrMoreWhitespaces()
|
return parseWithExpressions(code);
|
||||||
.expectCharacters('$')
|
}
|
||||||
.matchUntilFirstWhitespace() // First match: parameter name
|
}
|
||||||
.expectExpressionEnd()
|
|
||||||
// ...
|
|
||||||
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
|
||||||
// {{ end }}
|
|
||||||
.expectExpressionStart()
|
|
||||||
.expectCharacters('end')
|
|
||||||
.expectExpressionEnd()
|
|
||||||
.buildRegExp();
|
|
||||||
|
|
||||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
enum WithStatementType {
|
||||||
const parameterName = match[1];
|
Start,
|
||||||
const scopeText = match[2];
|
End,
|
||||||
|
ContextVariable,
|
||||||
|
}
|
||||||
|
|
||||||
|
type WithStatement = {
|
||||||
|
readonly type: WithStatementType.Start;
|
||||||
|
readonly parameterName: string;
|
||||||
|
readonly position: ExpressionPosition;
|
||||||
|
} | {
|
||||||
|
readonly type: WithStatementType.End;
|
||||||
|
readonly position: ExpressionPosition;
|
||||||
|
} | {
|
||||||
|
readonly type: WithStatementType.ContextVariable;
|
||||||
|
readonly position: ExpressionPosition;
|
||||||
|
readonly pipeline: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseAllWithExpressions(
|
||||||
|
input: string,
|
||||||
|
): WithStatement[] {
|
||||||
|
const expressions = new Array<WithStatement>();
|
||||||
|
for (const match of input.matchAll(WithStatementStartRegEx)) {
|
||||||
|
expressions.push({
|
||||||
|
type: WithStatementType.Start,
|
||||||
|
parameterName: match[1],
|
||||||
|
position: createPositionFromRegexFullMatch(match),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const match of input.matchAll(WithStatementEndRegEx)) {
|
||||||
|
expressions.push({
|
||||||
|
type: WithStatementType.End,
|
||||||
|
position: createPositionFromRegexFullMatch(match),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) {
|
||||||
|
expressions.push({
|
||||||
|
type: WithStatementType.ContextVariable,
|
||||||
|
position: createPositionFromRegexFullMatch(match),
|
||||||
|
pipeline: match[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return expressions;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WithStatementBuilder {
|
||||||
|
private readonly contextVariables = new Array<{
|
||||||
|
readonly positionInScope: ExpressionPosition;
|
||||||
|
readonly pipeline: string | undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
public addContextVariable(
|
||||||
|
absolutePosition: ExpressionPosition,
|
||||||
|
pipeline: string | undefined,
|
||||||
|
): void {
|
||||||
|
const positionInScope = new ExpressionPosition(
|
||||||
|
absolutePosition.start - this.startExpressionPosition.end,
|
||||||
|
absolutePosition.end - this.startExpressionPosition.end,
|
||||||
|
);
|
||||||
|
this.contextVariables.push({
|
||||||
|
positionInScope,
|
||||||
|
pipeline,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression {
|
||||||
|
const parameters = new FunctionParameterCollection();
|
||||||
|
parameters.addParameter(new FunctionParameter(this.parameterName, true));
|
||||||
|
const position = new ExpressionPosition(
|
||||||
|
this.startExpressionPosition.start,
|
||||||
|
endExpressionPosition.end,
|
||||||
|
);
|
||||||
|
const scope = input.substring(this.startExpressionPosition.end, endExpressionPosition.start);
|
||||||
return {
|
return {
|
||||||
parameters: [new FunctionParameter(parameterName, true)],
|
parameters,
|
||||||
evaluator: (context) => {
|
position,
|
||||||
const argumentValue = context.args.hasArgument(parameterName)
|
evaluate: (context) => {
|
||||||
? context.args.getArgument(parameterName).argumentValue
|
const argumentValue = context.args.hasArgument(this.parameterName)
|
||||||
|
? context.args.getArgument(this.parameterName).argumentValue
|
||||||
: undefined;
|
: undefined;
|
||||||
if (!argumentValue) {
|
if (!argumentValue) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
const substitutedScope = this.substituteContextVariables(scope, (pipeline) => {
|
||||||
if (!pipeline) {
|
if (!pipeline) {
|
||||||
return argumentValue;
|
return argumentValue;
|
||||||
}
|
}
|
||||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||||
});
|
});
|
||||||
|
return substitutedScope;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly startExpressionPosition: ExpressionPosition,
|
||||||
|
private readonly parameterName: string,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private substituteContextVariables(
|
||||||
|
scope: string,
|
||||||
|
substituter: (pipeline?: string) => string,
|
||||||
|
): string {
|
||||||
|
if (!this.contextVariables.length) {
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
let substitutedScope = '';
|
||||||
|
let scopeSubstrIndex = 0;
|
||||||
|
for (const contextVariable of this.contextVariables) {
|
||||||
|
substitutedScope += scope.substring(scopeSubstrIndex, contextVariable.positionInScope.start);
|
||||||
|
substitutedScope += substituter(contextVariable.pipeline);
|
||||||
|
scopeSubstrIndex = contextVariable.positionInScope.end;
|
||||||
|
}
|
||||||
|
substitutedScope += scope.substring(scopeSubstrIndex, scope.length);
|
||||||
|
return substitutedScope;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
function buildErrorContext(code: string, statements: readonly WithStatement[]): string {
|
||||||
|
const formattedStatements = statements.map((s) => `- [${s.position.start}, ${s.position.end}] ${WithStatementType[s.type]}`).join('\n');
|
||||||
|
return [
|
||||||
|
'Code:', '---', code, '---',
|
||||||
|
'nStatements:', '---', formattedStatements, '---',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWithExpressions(input: string): IExpression[] {
|
||||||
|
const allStatements = parseAllWithExpressions(input);
|
||||||
|
const sortedStatements = allStatements
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => b.position.start - a.position.start);
|
||||||
|
const expressions = new Array<IExpression>();
|
||||||
|
const builders = new Array<WithStatementBuilder>();
|
||||||
|
const throwWithContext = (message: string): never => {
|
||||||
|
throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`);
|
||||||
|
};
|
||||||
|
while (sortedStatements.length > 0) {
|
||||||
|
const statement = sortedStatements.pop();
|
||||||
|
if (!statement) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch (statement.type) { // eslint-disable-line default-case
|
||||||
|
case WithStatementType.Start:
|
||||||
|
builders.push(new WithStatementBuilder(
|
||||||
|
statement.position,
|
||||||
|
statement.parameterName,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case WithStatementType.ContextVariable:
|
||||||
|
if (builders.length === 0) {
|
||||||
|
throwWithContext('Context variable before `with` statement.');
|
||||||
|
}
|
||||||
|
builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline);
|
||||||
|
break;
|
||||||
|
case WithStatementType.End: {
|
||||||
|
const builder = builders.pop();
|
||||||
|
if (!builder) {
|
||||||
|
throwWithContext('Redundant `end` statement, missing `with`?');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
expressions.push(builder.buildExpression(statement.position, input));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (builders.length > 0) {
|
||||||
|
throwWithContext('Missing `end` statement, forgot `{{ end }}?');
|
||||||
|
}
|
||||||
|
return expressions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContextVariableWithPipelineRegEx = new ExpressionRegexBuilder()
|
||||||
// {{ . | pipeName }}
|
// {{ . | pipeName }}
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('.')
|
.expectCharacters('.')
|
||||||
.matchPipeline() // First match: pipeline
|
.expectOptionalWhitespaces()
|
||||||
|
.captureOptionalPipeline() // First capture: pipeline
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
const WithStatementStartRegEx = new ExpressionRegexBuilder()
|
||||||
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
|
// {{ with $parameterName }}
|
||||||
// but instead letting the pipeline compiler to fail on those.
|
.expectExpressionStart()
|
||||||
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
|
.expectCharacters('with')
|
||||||
return replacer(match1);
|
.expectOneOrMoreWhitespaces()
|
||||||
});
|
.expectCharacters('$')
|
||||||
}
|
.captureUntilWhitespaceOrPipe() // First capture: parameter name
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.expectOptionalWhitespaces()
|
||||||
|
.buildRegExp();
|
||||||
|
|
||||||
|
const WithStatementEndRegEx = new ExpressionRegexBuilder()
|
||||||
|
// {{ end }}
|
||||||
|
.expectOptionalWhitespaces()
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('end')
|
||||||
|
.expectOptionalWhitespaces()
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.buildRegExp();
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
|
|||||||
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||||
|
|
||||||
public addArgument(argument: IFunctionCallArgument): void {
|
public addArgument(argument: IFunctionCallArgument): void {
|
||||||
if (!argument) {
|
|
||||||
throw new Error('missing argument');
|
|
||||||
}
|
|
||||||
if (this.hasArgument(argument.parameterName)) {
|
if (this.hasArgument(argument.parameterName)) {
|
||||||
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { CompiledCode } from '../CompiledCode';
|
||||||
|
|
||||||
|
export interface CodeSegmentMerger {
|
||||||
|
mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { CompiledCode } from '../CompiledCode';
|
||||||
|
import { CodeSegmentMerger } from './CodeSegmentMerger';
|
||||||
|
|
||||||
|
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
||||||
|
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
||||||
|
if (!codeSegments.length) {
|
||||||
|
throw new Error('missing segments');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
||||||
|
revertCode: joinCodeParts(
|
||||||
|
codeSegments
|
||||||
|
.map((f) => f.revertCode)
|
||||||
|
.filter((code): code is string => Boolean(code)),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinCodeParts(codeSegments: readonly string[]): string {
|
||||||
|
return codeSegments
|
||||||
|
.filter((segment) => segment.length > 0)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface ICompiledCode {
|
export interface CompiledCode {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
readonly revertCode?: string;
|
readonly revertCode?: string;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
|
import { FunctionCall } from '../FunctionCall';
|
||||||
|
import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
|
||||||
|
|
||||||
|
export interface FunctionCallCompilationContext {
|
||||||
|
readonly allFunctions: ISharedFunctionCollection;
|
||||||
|
readonly rootCallSequence: readonly FunctionCall[];
|
||||||
|
readonly singleCallCompiler: SingleCallCompiler;
|
||||||
|
}
|
||||||
@@ -1,149 +1,10 @@
|
|||||||
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
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 { FunctionCall } from '../FunctionCall';
|
||||||
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
|
import { CompiledCode } from './CompiledCode';
|
||||||
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
|
||||||
import { ICompiledCode } from './ICompiledCode';
|
|
||||||
|
|
||||||
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
export interface FunctionCallCompiler {
|
||||||
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
compileFunctionCalls(
|
||||||
|
calls: readonly FunctionCall[],
|
||||||
protected constructor(
|
|
||||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public compileCall(
|
|
||||||
calls: IFunctionCall[],
|
|
||||||
functions: ISharedFunctionCollection,
|
functions: ISharedFunctionCollection,
|
||||||
): ICompiledCode {
|
): CompiledCode;
|
||||||
if (!functions) { throw new Error('missing functions'); }
|
|
||||||
if (!calls) { throw new Error('missing calls'); }
|
|
||||||
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
|
|
||||||
const context: ICompilationContext = {
|
|
||||||
allFunctions: functions,
|
|
||||||
callSequence: calls,
|
|
||||||
expressionsCompiler: this.expressionsCompiler,
|
|
||||||
};
|
|
||||||
const code = compileCallSequence(context);
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ICompilationContext {
|
|
||||||
allFunctions: ISharedFunctionCollection;
|
|
||||||
callSequence: readonly IFunctionCall[];
|
|
||||||
expressionsCompiler: IExpressionsCompiler;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ICompiledFunctionCall {
|
|
||||||
readonly code: string;
|
|
||||||
readonly revertCode: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
|
|
||||||
const compiledFunctions = context.callSequence
|
|
||||||
.flatMap((call) => compileSingleCall(call, context));
|
|
||||||
return {
|
|
||||||
code: merge(compiledFunctions.map((f) => f.code)),
|
|
||||||
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function compileSingleCall(
|
|
||||||
call: IFunctionCall,
|
|
||||||
context: ICompilationContext,
|
|
||||||
): ICompiledFunctionCall[] {
|
|
||||||
const func = context.allFunctions.getFunctionByName(call.functionName);
|
|
||||||
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
|
||||||
if (func.body.code) { // Function with inline code
|
|
||||||
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
|
||||||
return [compiledCode];
|
|
||||||
}
|
|
||||||
// Function with inner calls
|
|
||||||
return func.body.calls
|
|
||||||
.map((innerCall) => {
|
|
||||||
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
|
||||||
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
|
|
||||||
return compileSingleCall(compiledCall, context);
|
|
||||||
})
|
|
||||||
.flat();
|
|
||||||
}
|
|
||||||
|
|
||||||
function compileCode(
|
|
||||||
code: IFunctionCode,
|
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
|
||||||
compiler: IExpressionsCompiler,
|
|
||||||
): ICompiledFunctionCall {
|
|
||||||
return {
|
|
||||||
code: compiler.compileExpressions(code.execute, 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,34 @@
|
|||||||
|
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
|
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||||
|
import { FunctionCallCompiler } from './FunctionCallCompiler';
|
||||||
|
import { CompiledCode } from './CompiledCode';
|
||||||
|
import { FunctionCallCompilationContext } from './FunctionCallCompilationContext';
|
||||||
|
import { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
|
||||||
|
import { AdaptiveFunctionCallCompiler } from './SingleCall/AdaptiveFunctionCallCompiler';
|
||||||
|
import { CodeSegmentMerger } from './CodeSegmentJoin/CodeSegmentMerger';
|
||||||
|
import { NewlineCodeSegmentMerger } from './CodeSegmentJoin/NewlineCodeSegmentMerger';
|
||||||
|
|
||||||
|
export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
|
||||||
|
public static readonly instance: FunctionCallCompiler = new FunctionCallSequenceCompiler();
|
||||||
|
|
||||||
|
/* The constructor is protected to enforce the singleton pattern. */
|
||||||
|
protected constructor(
|
||||||
|
private readonly singleCallCompiler: SingleCallCompiler = new AdaptiveFunctionCallCompiler(),
|
||||||
|
private readonly codeSegmentMerger: CodeSegmentMerger = new NewlineCodeSegmentMerger(),
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public compileFunctionCalls(
|
||||||
|
calls: readonly FunctionCall[],
|
||||||
|
functions: ISharedFunctionCollection,
|
||||||
|
): CompiledCode {
|
||||||
|
if (!calls.length) { throw new Error('missing calls'); }
|
||||||
|
const context: FunctionCallCompilationContext = {
|
||||||
|
allFunctions: functions,
|
||||||
|
rootCallSequence: calls,
|
||||||
|
singleCallCompiler: this.singleCallCompiler,
|
||||||
|
};
|
||||||
|
const codeSegments = context.rootCallSequence
|
||||||
|
.flatMap((call) => this.singleCallCompiler.compileSingleCall(call, context));
|
||||||
|
return this.codeSegmentMerger.mergeCodeParts(codeSegments);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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,78 @@
|
|||||||
|
import { FunctionCall } from '../../FunctionCall';
|
||||||
|
import { CompiledCode } from '../CompiledCode';
|
||||||
|
import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
|
||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '../../Argument/IFunctionCallArgumentCollection';
|
||||||
|
import { ISharedFunction } from '../../../ISharedFunction';
|
||||||
|
import { SingleCallCompiler } from './SingleCallCompiler';
|
||||||
|
import { SingleCallCompilerStrategy } from './SingleCallCompilerStrategy';
|
||||||
|
import { InlineFunctionCallCompiler } from './Strategies/InlineFunctionCallCompiler';
|
||||||
|
import { NestedFunctionCallCompiler } from './Strategies/NestedFunctionCallCompiler';
|
||||||
|
|
||||||
|
export class AdaptiveFunctionCallCompiler implements SingleCallCompiler {
|
||||||
|
public constructor(
|
||||||
|
private readonly strategies: SingleCallCompilerStrategy[] = [
|
||||||
|
new InlineFunctionCallCompiler(),
|
||||||
|
new NestedFunctionCallCompiler(),
|
||||||
|
],
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public compileSingleCall(
|
||||||
|
call: FunctionCall,
|
||||||
|
context: FunctionCallCompilationContext,
|
||||||
|
): CompiledCode[] {
|
||||||
|
const func = context.allFunctions.getFunctionByName(call.functionName);
|
||||||
|
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||||
|
const strategy = this.findStrategy(func);
|
||||||
|
return strategy.compileFunction(func, call, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private findStrategy(func: ISharedFunction): SingleCallCompilerStrategy {
|
||||||
|
const strategies = this.strategies.filter((strategy) => strategy.canCompile(func));
|
||||||
|
if (strategies.length > 1) {
|
||||||
|
throw new Error('Multiple strategies found to compile the function call.');
|
||||||
|
}
|
||||||
|
if (strategies.length === 0) {
|
||||||
|
throw new Error('No strategies found to compile the function call.');
|
||||||
|
}
|
||||||
|
return strategies[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureThatCallArgumentsExistInParameterDefinition(
|
||||||
|
func: ISharedFunction,
|
||||||
|
callArguments: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
): void {
|
||||||
|
const callArgumentNames = callArguments.getAllParameterNames();
|
||||||
|
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||||
|
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||||
|
throwIfUnexpectedParametersExist(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 throwIfUnexpectedParametersExist(
|
||||||
|
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,10 @@
|
|||||||
|
import { FunctionCall } from '../../FunctionCall';
|
||||||
|
import { CompiledCode } from '../CompiledCode';
|
||||||
|
import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
|
||||||
|
|
||||||
|
export interface SingleCallCompiler {
|
||||||
|
compileSingleCall(
|
||||||
|
call: FunctionCall,
|
||||||
|
context: FunctionCallCompilationContext,
|
||||||
|
): CompiledCode[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
|
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
|
import { CompiledCode } from '../CompiledCode';
|
||||||
|
import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
|
||||||
|
|
||||||
|
export interface SingleCallCompilerStrategy {
|
||||||
|
canCompile(func: ISharedFunction): boolean;
|
||||||
|
compileFunction(
|
||||||
|
calledFunction: ISharedFunction,
|
||||||
|
callToFunction: FunctionCall,
|
||||||
|
context: FunctionCallCompilationContext,
|
||||||
|
): CompiledCode[],
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
|
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
|
|
||||||
|
export interface ArgumentCompiler {
|
||||||
|
createCompiledNestedCall(
|
||||||
|
nestedFunctionCall: FunctionCall,
|
||||||
|
parentFunctionCall: FunctionCall,
|
||||||
|
context: FunctionCallCompilationContext,
|
||||||
|
): FunctionCall;
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||||
|
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
|
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||||
|
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||||
|
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
|
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
|
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
|
||||||
|
import { ArgumentCompiler } from './ArgumentCompiler';
|
||||||
|
|
||||||
|
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
||||||
|
constructor(
|
||||||
|
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public createCompiledNestedCall(
|
||||||
|
nestedFunction: FunctionCall,
|
||||||
|
parentFunction: FunctionCall,
|
||||||
|
context: FunctionCallCompilationContext,
|
||||||
|
): FunctionCall {
|
||||||
|
const compiledArgs = compileNestedFunctionArguments(
|
||||||
|
nestedFunction,
|
||||||
|
parentFunction.args,
|
||||||
|
context,
|
||||||
|
this.expressionsCompiler,
|
||||||
|
);
|
||||||
|
const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs);
|
||||||
|
return compiledCall;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileNestedFunctionArguments(
|
||||||
|
nestedFunction: FunctionCall,
|
||||||
|
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
context: FunctionCallCompilationContext,
|
||||||
|
expressionsCompiler: IExpressionsCompiler,
|
||||||
|
): IReadOnlyFunctionCallArgumentCollection {
|
||||||
|
const requiredParameterNames = context
|
||||||
|
.allFunctions
|
||||||
|
.getRequiredParameterNames(nestedFunction.functionName);
|
||||||
|
const compiledArguments = nestedFunction.args
|
||||||
|
.getAllParameterNames()
|
||||||
|
// Compile each argument value
|
||||||
|
.map((paramName) => ({
|
||||||
|
parameterName: paramName,
|
||||||
|
compiledArgumentValue: compileArgument(
|
||||||
|
paramName,
|
||||||
|
nestedFunction,
|
||||||
|
parentFunctionArgs,
|
||||||
|
expressionsCompiler,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
// Filter out arguments with absent values
|
||||||
|
.filter(({
|
||||||
|
parameterName,
|
||||||
|
compiledArgumentValue,
|
||||||
|
}) => isValidNonAbsentArgumentValue(
|
||||||
|
parameterName,
|
||||||
|
compiledArgumentValue,
|
||||||
|
requiredParameterNames,
|
||||||
|
))
|
||||||
|
/*
|
||||||
|
Create argument object with non-absent values.
|
||||||
|
This is done after eliminating absent values because otherwise creating argument object
|
||||||
|
with absent values throws error.
|
||||||
|
*/
|
||||||
|
.map(({
|
||||||
|
parameterName,
|
||||||
|
compiledArgumentValue,
|
||||||
|
}) => new FunctionCallArgument(parameterName, compiledArgumentValue));
|
||||||
|
return buildArgumentCollectionFromArguments(compiledArguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidNonAbsentArgumentValue(
|
||||||
|
parameterName: string,
|
||||||
|
argumentValue: string | undefined,
|
||||||
|
requiredParameterNames: string[],
|
||||||
|
): boolean {
|
||||||
|
if (argumentValue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!requiredParameterNames.includes(parameterName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw new Error(`Compilation resulted in empty value for required parameter: "${parameterName}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileArgument(
|
||||||
|
parameterName: string,
|
||||||
|
nestedFunction: FunctionCall,
|
||||||
|
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
expressionsCompiler: IExpressionsCompiler,
|
||||||
|
): string {
|
||||||
|
try {
|
||||||
|
const { argumentValue: codeInArgument } = nestedFunction.args.getArgument(parameterName);
|
||||||
|
return expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs);
|
||||||
|
} catch (err) {
|
||||||
|
throw new AggregateError([err], `Error when compiling argument for "${parameterName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildArgumentCollectionFromArguments(
|
||||||
|
args: FunctionCallArgument[],
|
||||||
|
): FunctionCallArgumentCollection {
|
||||||
|
return args.reduce((compiledArgs, arg) => {
|
||||||
|
compiledArgs.addArgument(arg);
|
||||||
|
return compiledArgs;
|
||||||
|
}, new FunctionCallArgumentCollection());
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||||
|
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||||
|
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
|
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
|
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
|
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||||
|
|
||||||
|
export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||||
|
public constructor(
|
||||||
|
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public canCompile(func: ISharedFunction): boolean {
|
||||||
|
return func.body.type === FunctionBodyType.Code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public compileFunction(
|
||||||
|
calledFunction: ISharedFunction,
|
||||||
|
callToFunction: FunctionCall,
|
||||||
|
): CompiledCode[] {
|
||||||
|
if (calledFunction.body.type !== FunctionBodyType.Code) {
|
||||||
|
throw new Error([
|
||||||
|
'Unexpected function body type.',
|
||||||
|
`\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
|
||||||
|
`\tActual: "${FunctionBodyType[calledFunction.body.type]}"`,
|
||||||
|
'Function:',
|
||||||
|
`\t${JSON.stringify(callToFunction)}`,
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
const { code } = calledFunction.body;
|
||||||
|
const { args } = callToFunction;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
code: this.expressionsCompiler.compileExpressions(code.execute, args),
|
||||||
|
revertCode: (() => {
|
||||||
|
if (!code.revert) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.expressionsCompiler.compileExpressions(code.revert, args);
|
||||||
|
})(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user