Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58f902216b | ||
|
|
48d6dbd700 |
12
.github/actions/install-imagemagick/action.yml
vendored
12
.github/actions/install-imagemagick/action.yml
vendored
@@ -1,12 +0,0 @@
|
|||||||
inputs:
|
|
||||||
project-root:
|
|
||||||
required: false
|
|
||||||
default: '.'
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
-
|
|
||||||
name: Install ImageMagick
|
|
||||||
shell: bash
|
|
||||||
run: ./.github/actions/install-imagemagick/install-imagemagick.sh
|
|
||||||
working-directory: ${{ inputs.project-root }}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
main() {
|
|
||||||
local install_command
|
|
||||||
if ! install_command=$(get_install_command); then
|
|
||||||
fatal_error 'Could not find available command to install'
|
|
||||||
fi
|
|
||||||
if ! eval "$install_command"; then
|
|
||||||
echo "Failed to install ImageMagick. Command: ${install_command}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo 'ImageMagick installation completed successfully'
|
|
||||||
}
|
|
||||||
|
|
||||||
get_install_command() {
|
|
||||||
case "$OSTYPE" in
|
|
||||||
darwin*)
|
|
||||||
ensure_command_exists 'brew'
|
|
||||||
echo 'brew install imagemagick'
|
|
||||||
;;
|
|
||||||
linux-gnu*)
|
|
||||||
if is_ubuntu; then
|
|
||||||
ensure_command_exists 'apt'
|
|
||||||
echo 'sudo apt install -y imagemagick'
|
|
||||||
else
|
|
||||||
fatal_error 'Unsupported Linux distribution'
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
msys*|cygwin*)
|
|
||||||
ensure_command_exists 'choco'
|
|
||||||
echo 'choco install -y imagemagick'
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
fatal_error "Unsupported operating system: $OSTYPE"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_command_exists() {
|
|
||||||
local -r command="$1"
|
|
||||||
if ! command -v "$command" >/dev/null 2>&1; then
|
|
||||||
fatal_error "Command missing: $command"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
fatal_error() {
|
|
||||||
local -r error_message="$1"
|
|
||||||
>&2 echo "❌ $error_message"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
is_ubuntu() {
|
|
||||||
[ -f /etc/os-release ] && grep -qi 'ubuntu' /etc/os-release
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
@@ -26,12 +26,9 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
uses: ./.github/actions/npm-install-dependencies
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
-
|
|
||||||
name: Install ImageMagick # For screenshots
|
|
||||||
uses: ./.github/actions/install-imagemagick
|
|
||||||
-
|
-
|
||||||
name: Configure Ubuntu
|
name: Configure Ubuntu
|
||||||
if: contains(matrix.os, 'ubuntu')
|
if: contains(matrix.os, 'ubuntu') # macOS runner is missing Docker
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |-
|
run: |-
|
||||||
sudo apt update
|
sudo apt update
|
||||||
@@ -59,20 +56,11 @@ jobs:
|
|||||||
sudo Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
sudo Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||||
echo "DISPLAY=:99" >> $GITHUB_ENV
|
echo "DISPLAY=:99" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Install ImageMagick for screenshots
|
||||||
|
sudo apt install -y imagemagick
|
||||||
|
|
||||||
# Install xdotool and xprop (from x11-utils) for window title capturing
|
# Install xdotool and xprop (from x11-utils) for window title capturing
|
||||||
sudo apt install -y xdotool x11-utils
|
sudo apt install -y xdotool x11-utils
|
||||||
|
|
||||||
# Workaround for Electron AppImage apps failing to initialize on Ubuntu 24.04 due to AppArmor restrictions
|
|
||||||
# Disables unprivileged user namespaces restriction to allow Electron apps to run
|
|
||||||
# Reference: https://github.com/electron/electron/issues/42510
|
|
||||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
|
||||||
|
|
||||||
# Workaround for Mesa driver issues on Ubuntu 24.04
|
|
||||||
# Installs latest Mesa drivers from Kisak PPA
|
|
||||||
# Reference: https://askubuntu.com/q/1516040
|
|
||||||
sudo add-apt-repository ppa:kisak/kisak-mesa
|
|
||||||
sudo apt update
|
|
||||||
sudo apt upgrade
|
|
||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
3
.github/workflows/checks.quality.yaml
vendored
3
.github/workflows/checks.quality.yaml
vendored
@@ -11,9 +11,8 @@ jobs:
|
|||||||
- npm run lint:eslint
|
- npm run lint:eslint
|
||||||
- npm run lint:yaml
|
- npm run lint:yaml
|
||||||
- npm run lint:md
|
- npm run lint:md
|
||||||
- npm run lint:md:consistency
|
|
||||||
- npm run lint:md:relative-urls
|
- npm run lint:md:relative-urls
|
||||||
- npm run lint:md:external-urls
|
- npm run lint:md:consistency
|
||||||
os: [ macos, ubuntu, windows ]
|
os: [ macos, ubuntu, windows ]
|
||||||
fail-fast: false # Still interested to see results from other combinations
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
7
.github/workflows/checks.scripts.yaml
vendored
7
.github/workflows/checks.scripts.yaml
vendored
@@ -9,15 +9,16 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos, ubuntu, windows]
|
os: [ macos, ubuntu, windows ]
|
||||||
fail-fast: false # Still interested to see results from other combinations
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Install ImageMagick
|
name: Install ImageMagick on macOS
|
||||||
uses: ./.github/actions/install-imagemagick
|
if: matrix.os == 'macos'
|
||||||
|
run: brew install imagemagick
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
|||||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -1,38 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.13.6 (2024-08-13)
|
|
||||||
|
|
||||||
* win: improve service disabling as TrustedInstaller | [5d365f6](https://github.com/undergroundwires/privacy.sexy/commit/5d365f65fa0e34925b16b2eac2af53c31e34e99a)
|
|
||||||
* Fix documentation button spacing on small screens | [70959cc](https://github.com/undergroundwires/privacy.sexy/commit/70959ccadafac5abcfa83e90cdb0537890b05f14)
|
|
||||||
* Fix close button overlap by scrollbar | [19ea8db](https://github.com/undergroundwires/privacy.sexy/commit/19ea8dbc5bc2dc436200cd40bf2a84c3fc3c6471)
|
|
||||||
* win: refactor version-specific actions | [0239b52](https://github.com/undergroundwires/privacy.sexy/commit/0239b523859d5c2b80033cc03f0248a9af35f28f)
|
|
||||||
* win: support Microsoft Store Firefox installations | [8d7a7eb](https://github.com/undergroundwires/privacy.sexy/commit/8d7a7eb434b2d83e32fa758db7e6798849bad41c)
|
|
||||||
* Refactor text utilities and expand their usage | [851917e](https://github.com/undergroundwires/privacy.sexy/commit/851917e049c41c679644ddbe8ad4b6e45e5c8f35)
|
|
||||||
* Bump dependencies to latest | [dd7239b](https://github.com/undergroundwires/privacy.sexy/commit/dd7239b8c14027274926279a4c8c7e5845b55558)
|
|
||||||
* Refactor styles to match new CSS nesting behavior | [abe03ce](https://github.com/undergroundwires/privacy.sexy/commit/abe03cef3f691f6e56faee991cd2da9c45244279)
|
|
||||||
* Improve compiler error display for latest Chromium | [b16e136](https://github.com/undergroundwires/privacy.sexy/commit/b16e13678ce1b8a6871eba8196e82bb321410067)
|
|
||||||
* Fix intermittent `ModalDialog` unit test failures | [a650558](https://github.com/undergroundwires/privacy.sexy/commit/a6505587bf4a448f5f3de930004a95ee203416b8)
|
|
||||||
* Ensure tests do not log warning or errors | [ae0165f](https://github.com/undergroundwires/privacy.sexy/commit/ae0165f1fe7dba9dd8ddaa1afa722a939772d3b6)
|
|
||||||
* win: improve disabling SmartScreen #385 | [11e566d](https://github.com/undergroundwires/privacy.sexy/commit/11e566d0e5177214a2600f3fd2097aea62373b24)
|
|
||||||
* win: unify registry setting as TrustedInstaller | [8526d25](https://github.com/undergroundwires/privacy.sexy/commit/8526d2510b34cbd7e79342f79d444419f601b186)
|
|
||||||
* win: improve, fix, restructure CEIP disabling | [c2d3cdd](https://github.com/undergroundwires/privacy.sexy/commit/c2d3cddc47d8d4b34bff63d959612919fa971012)
|
|
||||||
* win: centralize, improve Defender data collection | [b185255](https://github.com/undergroundwires/privacy.sexy/commit/b185255a0a72d5bfa96d6cf60f868ecc67149d68)
|
|
||||||
* win: fix and document VStudio license removal | [109fc01](https://github.com/undergroundwires/privacy.sexy/commit/109fc01c9a047002c4309e7f8a2ca4647c494a8a)
|
|
||||||
* win: improve registry/recent cleaning | [48d97af](https://github.com/undergroundwires/privacy.sexy/commit/48d97afdf6c2964cab7951208e1b0a02c3fd4c9b)
|
|
||||||
* Relax linting to allow null recommendation | [6fbc816](https://github.com/undergroundwires/privacy.sexy/commit/6fbc81675f7f063c4ee2502b8d9f169aacb39ae4)
|
|
||||||
* Refactor executable IDs to use strings #262 | [ded55a6](https://github.com/undergroundwires/privacy.sexy/commit/ded55a66d6044a03d4e18330e146b69d159509a3)
|
|
||||||
* win: fix, improve and unify Windows version logic | [f89c232](https://github.com/undergroundwires/privacy.sexy/commit/f89c2322b05d19b82914b20416ecefd7bc7e3702)
|
|
||||||
* Fix PowerShell code block inlining in compiler | [d77c3cb](https://github.com/undergroundwires/privacy.sexy/commit/d77c3cbbe212d9929e083181cc331b45d01e2883)
|
|
||||||
* win: improve registry value deletion #381 | [55c23e9](https://github.com/undergroundwires/privacy.sexy/commit/55c23e9d4cee3b7f74c26a4ac8516535048d67f2)
|
|
||||||
* win: improve folder hiding in "This PC" #16 | [e8add5e](https://github.com/undergroundwires/privacy.sexy/commit/e8add5ec08d2e8b7636cc9c8f0f9a33e4b004265)
|
|
||||||
* win: improve Microsoft Edge associations removal | [c2f4b68](https://github.com/undergroundwires/privacy.sexy/commit/c2f4b6878635e97f9c4be7bf2ee194a2deebb38a)
|
|
||||||
* win: unify registry data setting, fix #380 | [4cea6b2](https://github.com/undergroundwires/privacy.sexy/commit/4cea6b26ec2717c792c2471cc587f370274f90c4)
|
|
||||||
* win: improve disabling NCSI #189, #216, #279 | [c7e57b8](https://github.com/undergroundwires/privacy.sexy/commit/c7e57b8913f409a1c149ba598dc2f8786df0f9a9)
|
|
||||||
* win, mac: fix minor typos, formatting, dead URLs | [29e1069](https://github.com/undergroundwires/privacy.sexy/commit/29e1069bf2bc317e3c255b38c1ba0ab078b42d98)
|
|
||||||
* win: fix, constrain and document WNS #227 #314 | [50ba00b](https://github.com/undergroundwires/privacy.sexy/commit/50ba00b0af6232fc9187532635b04c4d9d9a68af)
|
|
||||||
|
|
||||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.5...0.13.6)
|
|
||||||
|
|
||||||
## 0.13.5 (2024-06-26)
|
## 0.13.5 (2024-06-26)
|
||||||
|
|
||||||
* ci/cd: centralize and bump artifact uploads | [22d6c79](https://github.com/undergroundwires/privacy.sexy/commit/22d6c7991eb2c138578a7d41950f301906dbf703)
|
* ci/cd: centralize and bump artifact uploads | [22d6c79](https://github.com/undergroundwires/privacy.sexy/commit/22d6c7991eb2c138578a7d41950f301906dbf703)
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.6/privacy.sexy-Setup-0.13.6.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.6/privacy.sexy-0.13.6.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.6/privacy.sexy-0.13.6.AppImage). For more options, see [here](#additional-install-options).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-Setup-0.13.5.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-0.13.5.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-0.13.5.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
See also:
|
See also:
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Related documentation:
|
|||||||
|
|
||||||
### Executables
|
### Executables
|
||||||
|
|
||||||
They represent independently executable actions with documentation and reversibility.
|
They represent independently executable tweaks with documentation and reversibility.
|
||||||
|
|
||||||
An Executable is a logical entity that can
|
An Executable is a logical entity that can
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,8 @@ The desktop version ensures secure delivery through cryptographic signatures and
|
|||||||
|
|
||||||
[Security is a top priority](./../../SECURITY.md#update-security-and-integrity) at privacy.sexy.
|
[Security is a top priority](./../../SECURITY.md#update-security-and-integrity) at privacy.sexy.
|
||||||
|
|
||||||
> **Note for macOS users:**
|
> **Note for macOS users:** On macOS, the desktop version's auto-update process involves manual steps due to Apple's code signing costs.
|
||||||
> On macOS, the desktop version's auto-update process involves manual steps due to Apple's code signing costs.
|
|
||||||
> Users get notified about updates but might need to complete the installation manually.
|
> Users get notified about updates but might need to complete the installation manually.
|
||||||
> Updater stores update installation files temporarily at `$HOME/Library/Application Support/privacy.sexy/updates`.
|
|
||||||
> Consider [donating](https://github.com/sponsors/undergroundwires) to help improve this process ❤️.
|
> Consider [donating](https://github.com/sponsors/undergroundwires) to help improve this process ❤️.
|
||||||
|
|
||||||
### Logging
|
### Logging
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ See [ci-cd.md](./ci-cd.md) for more information.
|
|||||||
- Markdown: `npm run lint:md`
|
- Markdown: `npm run lint:md`
|
||||||
- Markdown consistency `npm run lint:md:consistency`
|
- Markdown consistency `npm run lint:md:consistency`
|
||||||
- Markdown relative URLs: `npm run lint:md:relative-urls`
|
- Markdown relative URLs: `npm run lint:md:relative-urls`
|
||||||
- Markdown external URLs: `npm run lint:md:external-urls`
|
|
||||||
- JavaScript/TypeScript: `npm run lint:eslint`
|
- JavaScript/TypeScript: `npm run lint:eslint`
|
||||||
- Yaml: `npm run lint:yaml`
|
- Yaml: `npm run lint:yaml`
|
||||||
|
|
||||||
|
|||||||
10061
package-lock.json
generated
10061
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
63
package.json
63
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.13.6",
|
"version": "0.13.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Privacy is sexy",
|
"slogan": "Privacy is sexy",
|
||||||
"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.",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"test:integration": "vitest run --dir tests/integration",
|
"test:integration": "vitest run --dir tests/integration",
|
||||||
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
|
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
|
||||||
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
||||||
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:md:external-urls && npm run lint:eslint && npm run lint:yaml && npm run lint:pylint",
|
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml && npm run lint:pylint",
|
||||||
"install-deps": "node scripts/npm-install.js",
|
"install-deps": "node scripts/npm-install.js",
|
||||||
"icons:build": "node scripts/logo-update.js",
|
"icons:build": "node scripts/logo-update.js",
|
||||||
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
|
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
|
||||||
@@ -28,61 +28,60 @@
|
|||||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||||
"lint:md:external-urls": "remark . --frail --use remark-lint-no-dead-urls",
|
|
||||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||||
"lint:pylint": "pylint **/*.py",
|
"lint:pylint": "pylint **/*.py",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/vue": "^1.1.1",
|
"@floating-ui/vue": "^1.0.6",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"ace-builds": "^1.35.3",
|
"ace-builds": "^1.33.0",
|
||||||
"electron-log": "^5.1.6",
|
"electron-log": "^5.1.2",
|
||||||
"electron-progressbar": "^2.2.1",
|
"electron-progressbar": "^2.2.1",
|
||||||
"electron-updater": "^6.2.1",
|
"electron-updater": "^6.1.9",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"vue": "^3.4.32"
|
"vue": "^3.4.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||||
"@rushstack/eslint-patch": "^1.10.3",
|
"@rushstack/eslint-patch": "^1.10.2",
|
||||||
"@types/ace": "^0.0.52",
|
"@types/ace": "^0.0.52",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/markdown-it": "^14.1.1",
|
"@types/markdown-it": "^14.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||||
"@typescript-eslint/parser": "6.21.0",
|
"@typescript-eslint/parser": "6.21.0",
|
||||||
"@vitejs/plugin-legacy": "^5.4.1",
|
"@vitejs/plugin-legacy": "^5.3.2",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
|
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
|
||||||
"@vue/eslint-config-typescript": "12.0.0",
|
"@vue/eslint-config-typescript": "12.0.0",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.5",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"cypress": "^13.13.1",
|
"cypress": "^13.7.3",
|
||||||
"electron": "^31.2.1",
|
"electron": "^31.0.2",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "^24.13.3",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-vite": "^2.3.0",
|
"electron-vite": "^2.1.0",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-plugin-cypress": "^3.3.0",
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
"eslint-plugin-vue": "^9.25.0",
|
||||||
"eslint-plugin-vuejs-accessibility": "^2.4.0",
|
"eslint-plugin-vuejs-accessibility": "^2.2.1",
|
||||||
"jsdom": "^24.1.0",
|
"jsdom": "^24.0.0",
|
||||||
"markdownlint-cli": "^0.41.0",
|
"markdownlint-cli": "^0.39.0",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.38",
|
||||||
"remark-cli": "^12.0.1",
|
"remark-cli": "^12.0.0",
|
||||||
"remark-lint-no-dead-urls": "^2.0.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^6.0.0",
|
"remark-preset-lint-consistent": "^6.0.0",
|
||||||
"remark-validate-links": "^13.0.1",
|
"remark-validate-links": "^13.0.1",
|
||||||
"sass": "~1.79.4",
|
"sass": "^1.75.0",
|
||||||
"start-server-and-test": "^2.0.4",
|
"start-server-and-test": "^2.0.3",
|
||||||
"terser": "^5.31.3",
|
"terser": "^5.30.3",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "~5.5.4",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.4.8",
|
"vite": "^5.2.8",
|
||||||
"vitest": "^2.0.3",
|
"vitest": "^1.5.0",
|
||||||
"vue-tsc": "^2.0.26",
|
"vue-tsc": "^2.0.13",
|
||||||
"yaml-lint": "^1.7.0"
|
"yaml-lint": "^1.7.0"
|
||||||
},
|
},
|
||||||
"//devDependencies": {
|
"//devDependencies": {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ async function verifyFilesExist(directoryPath, filePatterns) {
|
|||||||
if (!match) {
|
if (!match) {
|
||||||
die(
|
die(
|
||||||
`No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``,
|
`No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``,
|
||||||
`\nFiles in directory:\n${files.map((file) => `- ${file}`).join('\n')}`,
|
`\nFiles in directory:\n${files.map((file) => `\t- ${file}`).join('\n')}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,25 +33,23 @@ function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
|||||||
if (!casedValue) {
|
if (!casedValue) {
|
||||||
throw new Error(`unknown ${enumName}: "${value}"`);
|
throw new Error(`unknown ${enumName}: "${value}"`);
|
||||||
}
|
}
|
||||||
return enumVariable[casedValue as keyof EnumVariable<T, TEnumValue>];
|
return enumVariable[casedValue as keyof typeof enumVariable];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEnumNames
|
export function getEnumNames
|
||||||
<T extends EnumType, TEnumValue extends EnumType>(
|
<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>,
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
): (string & keyof EnumVariable<T, TEnumValue>)[] {
|
): string[] {
|
||||||
return Object
|
return Object
|
||||||
.values(enumVariable)
|
.values(enumVariable)
|
||||||
.filter((
|
.filter((enumMember): enumMember is string => isString(enumMember));
|
||||||
enumMember,
|
|
||||||
): enumMember is string & (keyof EnumVariable<T, TEnumValue>) => isString(enumMember));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>,
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
): TEnumValue[] {
|
): TEnumValue[] {
|
||||||
return getEnumNames(enumVariable)
|
return getEnumNames(enumVariable)
|
||||||
.map((name) => enumVariable[name]) as TEnumValue[];
|
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { isArray } from '@/TypeHelpers';
|
|
||||||
|
|
||||||
export type OptionalString = string | undefined | null;
|
|
||||||
|
|
||||||
export function filterEmptyStrings(
|
|
||||||
texts: readonly OptionalString[],
|
|
||||||
isArrayType: typeof isArray = isArray,
|
|
||||||
): string[] {
|
|
||||||
if (!isArrayType(texts)) {
|
|
||||||
throw new Error(`Invalid input: Expected an array, but received type ${typeof texts}.`);
|
|
||||||
}
|
|
||||||
assertArrayItemsAreStringLike(texts);
|
|
||||||
return texts
|
|
||||||
.filter((title): title is string => Boolean(title));
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertArrayItemsAreStringLike(
|
|
||||||
texts: readonly unknown[],
|
|
||||||
): asserts texts is readonly OptionalString[] {
|
|
||||||
const invalidItems = texts.filter((item) => !(typeof item === 'string' || item === undefined || item === null));
|
|
||||||
if (invalidItems.length > 0) {
|
|
||||||
const invalidTypes = invalidItems.map((item) => typeof item).join(', ');
|
|
||||||
throw new Error(`Invalid array items: Expected items as string, undefined, or null. Received invalid types: ${invalidTypes}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { isString } from '@/TypeHelpers';
|
|
||||||
import { splitTextIntoLines } from './SplitTextIntoLines';
|
|
||||||
|
|
||||||
export function indentText(
|
|
||||||
text: string,
|
|
||||||
indentLevel = 1,
|
|
||||||
utilities: TextIndentationUtilities = DefaultUtilities,
|
|
||||||
): string {
|
|
||||||
if (!utilities.isStringType(text)) {
|
|
||||||
throw new Error(`Indentation error: The input must be a string. Received type: ${typeof text}.`);
|
|
||||||
}
|
|
||||||
if (indentLevel <= 0) {
|
|
||||||
throw new Error(`Indentation error: The indent level must be a positive integer. Received: ${indentLevel}.`);
|
|
||||||
}
|
|
||||||
const indentation = '\t'.repeat(indentLevel);
|
|
||||||
return utilities.splitIntoLines(text)
|
|
||||||
.map((line) => (line ? `${indentation}${line}` : line))
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TextIndentationUtilities {
|
|
||||||
readonly splitIntoLines: typeof splitTextIntoLines;
|
|
||||||
readonly isStringType: typeof isString;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultUtilities: TextIndentationUtilities = {
|
|
||||||
splitIntoLines: splitTextIntoLines,
|
|
||||||
isStringType: isString,
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { isString } from '@/TypeHelpers';
|
|
||||||
|
|
||||||
export function splitTextIntoLines(
|
|
||||||
text: string,
|
|
||||||
isStringType = isString,
|
|
||||||
): string[] {
|
|
||||||
if (!isStringType(text)) {
|
|
||||||
throw new Error(`Line splitting error: Expected a string but received type '${typeof text}'.`);
|
|
||||||
}
|
|
||||||
return text.split(/\r\n|\r|\n/);
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
public currentOs: OperatingSystem;
|
public currentOs: OperatingSystem;
|
||||||
|
|
||||||
public get state(): ICategoryCollectionState {
|
public get state(): ICategoryCollectionState {
|
||||||
return this.getState(this.collection.os);
|
return this.states[this.collection.os];
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly states: StateMachine;
|
private readonly states: StateMachine;
|
||||||
@@ -26,51 +26,30 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
public readonly app: IApplication,
|
public readonly app: IApplication,
|
||||||
initialContext: OperatingSystem,
|
initialContext: OperatingSystem,
|
||||||
) {
|
) {
|
||||||
this.setContext(initialContext);
|
|
||||||
this.states = initializeStates(app);
|
this.states = initializeStates(app);
|
||||||
|
this.changeContext(initialContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public changeContext(os: OperatingSystem): void {
|
public changeContext(os: OperatingSystem): void {
|
||||||
|
assertInRange(os, OperatingSystem);
|
||||||
if (this.currentOs === os) {
|
if (this.currentOs === os) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const collection = this.app.getCollection(os);
|
||||||
|
this.collection = collection;
|
||||||
const event: IApplicationContextChangedEvent = {
|
const event: IApplicationContextChangedEvent = {
|
||||||
newState: this.getState(os),
|
newState: this.states[os],
|
||||||
oldState: this.getState(this.currentOs),
|
oldState: this.states[this.currentOs],
|
||||||
};
|
};
|
||||||
this.setContext(os);
|
|
||||||
this.contextChanged.notify(event);
|
this.contextChanged.notify(event);
|
||||||
}
|
|
||||||
|
|
||||||
private setContext(os: OperatingSystem): void {
|
|
||||||
validateOperatingSystem(os, this.app);
|
|
||||||
this.collection = this.app.getCollection(os);
|
|
||||||
this.currentOs = os;
|
this.currentOs = os;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getState(os: OperatingSystem): ICategoryCollectionState {
|
|
||||||
const state = this.states.get(os);
|
|
||||||
if (!state) {
|
|
||||||
throw new Error(`Operating system "${OperatingSystem[os]}" state is unknown.`);
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateOperatingSystem(
|
|
||||||
os: OperatingSystem,
|
|
||||||
app: IApplication,
|
|
||||||
): void {
|
|
||||||
assertInRange(os, OperatingSystem);
|
|
||||||
if (!app.getSupportedOsList().includes(os)) {
|
|
||||||
throw new Error(`Operating system "${OperatingSystem[os]}" is not supported.`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeStates(app: IApplication): StateMachine {
|
function initializeStates(app: IApplication): StateMachine {
|
||||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||||
for (const collection of app.collections) {
|
for (const collection of app.collections) {
|
||||||
machine.set(collection.os, new CategoryCollectionState(collection));
|
machine[collection.os] = new CategoryCollectionState(collection);
|
||||||
}
|
}
|
||||||
return machine;
|
return machine;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
|
||||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
|
||||||
import type { ICodeChangedEvent } from './ICodeChangedEvent';
|
import type { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
|
||||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
@@ -41,7 +39,7 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
return this.getPositionById(script.executableId);
|
return this.getPositionById(script.executableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPositionById(scriptId: ExecutableId): ICodePosition {
|
private getPositionById(scriptId: string): ICodePosition {
|
||||||
const position = [...this.scripts.entries()]
|
const position = [...this.scripts.entries()]
|
||||||
.filter(([s]) => s.executableId === scriptId)
|
.filter(([s]) => s.executableId === scriptId)
|
||||||
.map(([, pos]) => pos)
|
.map(([, pos]) => pos)
|
||||||
@@ -54,12 +52,12 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||||
const totalLines = splitTextIntoLines(script).length;
|
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||||
const missingPositions = positions.filter((position) => position.endLine > totalLines);
|
const missingPositions = positions.filter((position) => position.endLine > totalLines);
|
||||||
if (missingPositions.length > 0) {
|
if (missingPositions.length > 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
|
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
|
||||||
+ ` (total code lines: ${totalLines}).`,
|
+ `(total code lines: ${totalLines}).`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
|
||||||
import type { ICodeBuilder } from './ICodeBuilder';
|
import type { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
const TotalFunctionSeparatorChars = 58;
|
const TotalFunctionSeparatorChars = 58;
|
||||||
@@ -16,7 +15,7 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
this.lines.push('');
|
this.lines.push('');
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
const lines = splitTextIntoLines(code);
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
if (lines) {
|
if (lines) {
|
||||||
this.lines.push(...lines);
|
this.lines.push(...lines);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { EventSource } from '@/infrastructure/Events/EventSource';
|
|||||||
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
||||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
||||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
|
||||||
import { UserSelectedScript } from './UserSelectedScript';
|
import { UserSelectedScript } from './UserSelectedScript';
|
||||||
import type { ScriptSelection } from './ScriptSelection';
|
import type { ScriptSelection } from './ScriptSelection';
|
||||||
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
@@ -39,8 +38,8 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isSelected(scriptExecutableId: ExecutableId): boolean {
|
public isSelected(scriptId: string): boolean {
|
||||||
return this.scripts.exists(scriptExecutableId);
|
return this.scripts.exists(scriptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get selectedScripts(): readonly SelectedScript[] {
|
public get selectedScripts(): readonly SelectedScript[] {
|
||||||
@@ -122,7 +121,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
return this.removeScript(script.executableId);
|
return this.removeScript(script.executableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addOrUpdateScript(scriptId: ExecutableId, revert: boolean): number {
|
private addOrUpdateScript(scriptId: string, revert: boolean): number {
|
||||||
const script = this.collection.getScript(scriptId);
|
const script = this.collection.getScript(scriptId);
|
||||||
const selectedScript = new UserSelectedScript(script, revert);
|
const selectedScript = new UserSelectedScript(script, revert);
|
||||||
if (!this.scripts.exists(selectedScript.id)) {
|
if (!this.scripts.exists(selectedScript.id)) {
|
||||||
@@ -137,7 +136,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeScript(scriptId: ExecutableId): number {
|
private removeScript(scriptId: string): number {
|
||||||
if (!this.scripts.exists(scriptId)) {
|
if (!this.scripts.exists(scriptId)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
|
||||||
import type { SelectedScript } from './SelectedScript';
|
import type { SelectedScript } from './SelectedScript';
|
||||||
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
|
|
||||||
export interface ReadonlyScriptSelection {
|
export interface ReadonlyScriptSelection {
|
||||||
readonly changed: IEventSource<readonly SelectedScript[]>;
|
readonly changed: IEventSource<readonly SelectedScript[]>;
|
||||||
readonly selectedScripts: readonly SelectedScript[];
|
readonly selectedScripts: readonly SelectedScript[];
|
||||||
isSelected(scriptExecutableId: ExecutableId): boolean;
|
isSelected(scriptId: string): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptSelection extends ReadonlyScriptSelection {
|
export interface ScriptSelection extends ReadonlyScriptSelection {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
|
||||||
|
|
||||||
export type ScriptSelectionStatus = {
|
export type ScriptSelectionStatus = {
|
||||||
readonly isSelected: true;
|
readonly isSelected: true;
|
||||||
readonly isReverted: boolean;
|
readonly isReverted: boolean;
|
||||||
@@ -9,7 +7,7 @@ export type ScriptSelectionStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface ScriptSelectionChange {
|
export interface ScriptSelectionChange {
|
||||||
readonly scriptId: ExecutableId;
|
readonly scriptId: string;
|
||||||
readonly newStatus: ScriptSelectionStatus;
|
readonly newStatus: ScriptSelectionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function validateCollectionsData(
|
|||||||
) {
|
) {
|
||||||
validator.assertNonEmptyCollection({
|
validator.assertNonEmptyCollection({
|
||||||
value: collections,
|
value: collections,
|
||||||
valueName: 'Collections',
|
valueName: 'collections',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { createEnumParser, type EnumParser } from '../Common/Enum';
|
|||||||
import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
|
import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
|
||||||
import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
|
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
|
||||||
import { createCategoryCollectionContext, type CategoryCollectionContextFactory } from './Executable/CategoryCollectionContext';
|
import { createCollectionUtilities, type CategoryCollectionSpecificUtilitiesFactory } from './Executable/CategoryCollectionSpecificUtilities';
|
||||||
|
|
||||||
export const parseCategoryCollection: CategoryCollectionParser = (
|
export const parseCategoryCollection: CategoryCollectionParser = (
|
||||||
content,
|
content,
|
||||||
@@ -16,9 +16,9 @@ export const parseCategoryCollection: CategoryCollectionParser = (
|
|||||||
) => {
|
) => {
|
||||||
validateCollection(content, utilities.validator);
|
validateCollection(content, utilities.validator);
|
||||||
const scripting = utilities.parseScriptingDefinition(content.scripting, projectDetails);
|
const scripting = utilities.parseScriptingDefinition(content.scripting, projectDetails);
|
||||||
const collectionContext = utilities.createContext(content.functions, scripting.language);
|
const collectionUtilities = utilities.createUtilities(content.functions, scripting);
|
||||||
const categories = content.actions.map(
|
const categories = content.actions.map(
|
||||||
(action) => utilities.parseCategory(action, collectionContext),
|
(action) => utilities.parseCategory(action, collectionUtilities),
|
||||||
);
|
);
|
||||||
const os = utilities.osParser.parseEnum(content.os, 'os');
|
const os = utilities.osParser.parseEnum(content.os, 'os');
|
||||||
const collection = utilities.createCategoryCollection({
|
const collection = utilities.createCategoryCollection({
|
||||||
@@ -45,14 +45,14 @@ function validateCollection(
|
|||||||
): void {
|
): void {
|
||||||
validator.assertObject({
|
validator.assertObject({
|
||||||
value: content,
|
value: content,
|
||||||
valueName: 'Collection',
|
valueName: 'collection',
|
||||||
allowedProperties: [
|
allowedProperties: [
|
||||||
'os', 'scripting', 'actions', 'functions',
|
'os', 'scripting', 'actions', 'functions',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
validator.assertNonEmptyCollection({
|
validator.assertNonEmptyCollection({
|
||||||
value: content.actions,
|
value: content.actions,
|
||||||
valueName: '\'actions\' in collection',
|
valueName: '"actions" in collection',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ interface CategoryCollectionParserUtilities {
|
|||||||
readonly osParser: EnumParser<OperatingSystem>;
|
readonly osParser: EnumParser<OperatingSystem>;
|
||||||
readonly validator: TypeValidator;
|
readonly validator: TypeValidator;
|
||||||
readonly parseScriptingDefinition: ScriptingDefinitionParser;
|
readonly parseScriptingDefinition: ScriptingDefinitionParser;
|
||||||
readonly createContext: CategoryCollectionContextFactory;
|
readonly createUtilities: CategoryCollectionSpecificUtilitiesFactory;
|
||||||
readonly parseCategory: CategoryParser;
|
readonly parseCategory: CategoryParser;
|
||||||
readonly createCategoryCollection: CategoryCollectionFactory;
|
readonly createCategoryCollection: CategoryCollectionFactory;
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ const DefaultUtilities: CategoryCollectionParserUtilities = {
|
|||||||
osParser: createEnumParser(OperatingSystem),
|
osParser: createEnumParser(OperatingSystem),
|
||||||
validator: createTypeValidator(),
|
validator: createTypeValidator(),
|
||||||
parseScriptingDefinition,
|
parseScriptingDefinition,
|
||||||
createContext: createCategoryCollectionContext,
|
createUtilities: createCollectionUtilities,
|
||||||
parseCategory,
|
parseCategory,
|
||||||
createCategoryCollection: (...args) => new CategoryCollection(...args),
|
createCategoryCollection: (...args) => new CategoryCollection(...args),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,116 +1,42 @@
|
|||||||
import { CustomError } from '@/application/Common/CustomError';
|
import { CustomError } from '@/application/Common/CustomError';
|
||||||
import { indentText } from '@/application/Common/Text/IndentText';
|
|
||||||
|
|
||||||
export interface ErrorWithContextWrapper {
|
export interface ErrorWithContextWrapper {
|
||||||
(
|
(
|
||||||
innerError: Error,
|
error: Error,
|
||||||
additionalContext: string,
|
additionalContext: string,
|
||||||
): Error;
|
): Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
||||||
innerError,
|
error: Error,
|
||||||
additionalContext,
|
additionalContext: string,
|
||||||
) => {
|
) => {
|
||||||
if (!additionalContext) {
|
return (error instanceof ContextualError ? error : new ContextualError(error))
|
||||||
throw new Error('Missing additional context');
|
.withAdditionalContext(additionalContext);
|
||||||
}
|
|
||||||
return new ContextualError({
|
|
||||||
innerError,
|
|
||||||
additionalContext,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/* AggregateError is similar but isn't well-serialized or displayed by browsers */
|
||||||
* Class for building a detailed error trace.
|
|
||||||
*
|
|
||||||
* Alternatives considered:
|
|
||||||
* - `AggregateError`:
|
|
||||||
* Similar but not well-serialized or displayed by browsers such as Chromium (last tested v126).
|
|
||||||
* - `cause` property:
|
|
||||||
* Not displayed by all browsers (last tested v126).
|
|
||||||
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
|
|
||||||
*
|
|
||||||
* This is immutable where the constructor sets the values because using getter functions such as
|
|
||||||
* `get cause()`, `get message()` does not work on Chromium (last tested v126), but works fine on
|
|
||||||
* Firefox (last tested v127).
|
|
||||||
*/
|
|
||||||
class ContextualError extends CustomError {
|
class ContextualError extends CustomError {
|
||||||
constructor(public readonly context: ErrorContext) {
|
private readonly additionalContext = new Array<string>();
|
||||||
super(
|
|
||||||
generateDetailedErrorMessageWithContext(context),
|
constructor(
|
||||||
{
|
public readonly innerError: Error,
|
||||||
cause: context.innerError,
|
) {
|
||||||
},
|
super();
|
||||||
);
|
}
|
||||||
|
|
||||||
|
public withAdditionalContext(additionalContext: string): this {
|
||||||
|
this.additionalContext.push(additionalContext);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get message(): string { // toString() is not used when Chromium logs it on console
|
||||||
|
return [
|
||||||
|
'\n',
|
||||||
|
this.innerError.message,
|
||||||
|
'\n',
|
||||||
|
'Additional context:',
|
||||||
|
...this.additionalContext.map((context, index) => `${index + 1}: ${context}`),
|
||||||
|
].join('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorContext {
|
|
||||||
readonly innerError: Error;
|
|
||||||
readonly additionalContext: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateDetailedErrorMessageWithContext(
|
|
||||||
context: ErrorContext,
|
|
||||||
): string {
|
|
||||||
return [
|
|
||||||
'\n',
|
|
||||||
// Display the current error message first, then the root cause.
|
|
||||||
// This prevents repetitive main messages for errors with a `cause:` chain,
|
|
||||||
// aligning with browser error display conventions.
|
|
||||||
context.additionalContext,
|
|
||||||
'\n',
|
|
||||||
'Error Trace (starting from root cause):',
|
|
||||||
indentText(
|
|
||||||
formatErrorTrace(
|
|
||||||
// Displaying contexts from the top frame (deepest, most recent) aligns with
|
|
||||||
// common debugger/compiler standard.
|
|
||||||
extractErrorTraceAscendingFromDeepest(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
'\n',
|
|
||||||
].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractErrorTraceAscendingFromDeepest(
|
|
||||||
context: ErrorContext,
|
|
||||||
): string[] {
|
|
||||||
const originalError = findRootError(context.innerError);
|
|
||||||
const contextsDescendingFromMostRecent: string[] = [
|
|
||||||
context.additionalContext,
|
|
||||||
...gatherContextsFromErrorChain(context.innerError),
|
|
||||||
originalError.toString(),
|
|
||||||
];
|
|
||||||
const contextsAscendingFromDeepest = contextsDescendingFromMostRecent.reverse();
|
|
||||||
return contextsAscendingFromDeepest;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findRootError(error: Error): Error {
|
|
||||||
if (error instanceof ContextualError) {
|
|
||||||
return findRootError(error.context.innerError);
|
|
||||||
}
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
function gatherContextsFromErrorChain(
|
|
||||||
error: Error,
|
|
||||||
accumulatedContexts: string[] = [],
|
|
||||||
): string[] {
|
|
||||||
if (error instanceof ContextualError) {
|
|
||||||
accumulatedContexts.push(error.context.additionalContext);
|
|
||||||
return gatherContextsFromErrorChain(error.context.innerError, accumulatedContexts);
|
|
||||||
}
|
|
||||||
return accumulatedContexts;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatErrorTrace(
|
|
||||||
errorMessages: readonly string[],
|
|
||||||
): string {
|
|
||||||
if (errorMessages.length === 1) {
|
|
||||||
return errorMessages[0];
|
|
||||||
}
|
|
||||||
return errorMessages
|
|
||||||
.map((context, index) => `${index + 1}.${indentText(context)}`)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ function assertArray(
|
|||||||
valueName: string,
|
valueName: string,
|
||||||
): asserts value is Array<unknown> {
|
): asserts value is Array<unknown> {
|
||||||
if (!isArray(value)) {
|
if (!isArray(value)) {
|
||||||
throw new Error(`${valueName} should be of type 'array', but is of type '${typeof value}'.`);
|
throw new Error(`'${valueName}' should be of type 'array', but is of type '${typeof value}'.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ function assertString(
|
|||||||
valueName: string,
|
valueName: string,
|
||||||
): asserts value is string {
|
): asserts value is string {
|
||||||
if (!isString(value)) {
|
if (!isString(value)) {
|
||||||
throw new Error(`${valueName} should be of type 'string', but is of type '${typeof value}'.`);
|
throw new Error(`'${valueName}' should be of type 'string', but is of type '${typeof value}'.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import type { FunctionData } from '@/application/collections/';
|
|
||||||
import type { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
import { createScriptCompiler, type ScriptCompilerFactory } from './Script/Compiler/ScriptCompilerFactory';
|
|
||||||
import type { ScriptCompiler } from './Script/Compiler/ScriptCompiler';
|
|
||||||
|
|
||||||
export interface CategoryCollectionContext {
|
|
||||||
readonly compiler: ScriptCompiler;
|
|
||||||
readonly language: ScriptingLanguage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CategoryCollectionContextFactory {
|
|
||||||
(
|
|
||||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
|
||||||
language: ScriptingLanguage,
|
|
||||||
compilerFactory?: ScriptCompilerFactory,
|
|
||||||
): CategoryCollectionContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createCategoryCollectionContext: CategoryCollectionContextFactory = (
|
|
||||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
|
||||||
language: ScriptingLanguage,
|
|
||||||
compilerFactory: ScriptCompilerFactory = createScriptCompiler,
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
compiler: compilerFactory({
|
|
||||||
categoryContext: {
|
|
||||||
functions: functionsData ?? [],
|
|
||||||
language,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
language,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import type { FunctionData } from '@/application/collections/';
|
||||||
|
import { ScriptCompiler } from './Script/Compiler/ScriptCompiler';
|
||||||
|
import { SyntaxFactory } from './Script/Validation/Syntax/SyntaxFactory';
|
||||||
|
import type { IScriptCompiler } from './Script/Compiler/IScriptCompiler';
|
||||||
|
import type { ILanguageSyntax } from './Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import type { ISyntaxFactory } from './Script/Validation/Syntax/ISyntaxFactory';
|
||||||
|
|
||||||
|
export interface CategoryCollectionSpecificUtilities {
|
||||||
|
readonly compiler: IScriptCompiler;
|
||||||
|
readonly syntax: ILanguageSyntax;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createCollectionUtilities: CategoryCollectionSpecificUtilitiesFactory = (
|
||||||
|
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||||
|
scripting: IScriptingDefinition,
|
||||||
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||||
|
) => {
|
||||||
|
const syntax = syntaxFactory.create(scripting.language);
|
||||||
|
return {
|
||||||
|
compiler: new ScriptCompiler({
|
||||||
|
functions: functionsData ?? [],
|
||||||
|
syntax,
|
||||||
|
}),
|
||||||
|
syntax,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CategoryCollectionSpecificUtilitiesFactory {
|
||||||
|
(
|
||||||
|
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||||
|
scripting: IScriptingDefinition,
|
||||||
|
syntaxFactory?: ISyntaxFactory,
|
||||||
|
): CategoryCollectionSpecificUtilities;
|
||||||
|
}
|
||||||
@@ -9,16 +9,16 @@ import { parseDocs, type DocsParser } from './DocumentationParser';
|
|||||||
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
||||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
|
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
|
||||||
import { ExecutableType } from './Validation/ExecutableType';
|
import { ExecutableType } from './Validation/ExecutableType';
|
||||||
import type { CategoryCollectionContext } from './CategoryCollectionContext';
|
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
|
||||||
|
|
||||||
export const parseCategory: CategoryParser = (
|
export const parseCategory: CategoryParser = (
|
||||||
category: CategoryData,
|
category: CategoryData,
|
||||||
collectionContext: CategoryCollectionContext,
|
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||||
categoryUtilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
|
categoryUtilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
|
||||||
) => {
|
) => {
|
||||||
return parseCategoryRecursively({
|
return parseCategoryRecursively({
|
||||||
categoryData: category,
|
categoryData: category,
|
||||||
collectionContext,
|
collectionUtilities,
|
||||||
categoryUtilities,
|
categoryUtilities,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -26,14 +26,14 @@ export const parseCategory: CategoryParser = (
|
|||||||
export interface CategoryParser {
|
export interface CategoryParser {
|
||||||
(
|
(
|
||||||
category: CategoryData,
|
category: CategoryData,
|
||||||
collectionContext: CategoryCollectionContext,
|
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||||
categoryUtilities?: CategoryParserUtilities,
|
categoryUtilities?: CategoryParserUtilities,
|
||||||
): Category;
|
): Category;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CategoryParseContext {
|
interface CategoryParseContext {
|
||||||
readonly categoryData: CategoryData;
|
readonly categoryData: CategoryData;
|
||||||
readonly collectionContext: CategoryCollectionContext;
|
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
|
||||||
readonly parentCategory?: CategoryData;
|
readonly parentCategory?: CategoryData;
|
||||||
readonly categoryUtilities: CategoryParserUtilities;
|
readonly categoryUtilities: CategoryParserUtilities;
|
||||||
}
|
}
|
||||||
@@ -52,12 +52,12 @@ function parseCategoryRecursively(
|
|||||||
children,
|
children,
|
||||||
parent: context.categoryData,
|
parent: context.categoryData,
|
||||||
categoryUtilities: context.categoryUtilities,
|
categoryUtilities: context.categoryUtilities,
|
||||||
collectionContext: context.collectionContext,
|
collectionUtilities: context.collectionUtilities,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return context.categoryUtilities.createCategory({
|
return context.categoryUtilities.createCategory({
|
||||||
executableId: context.categoryData.category, // Pseudo-ID for uniqueness until real ID support
|
executableId: context.categoryData.category, // arbitrary ID
|
||||||
name: context.categoryData.category,
|
name: context.categoryData.category,
|
||||||
docs: context.categoryUtilities.parseDocs(context.categoryData),
|
docs: context.categoryUtilities.parseDocs(context.categoryData),
|
||||||
subcategories: children.subcategories,
|
subcategories: children.subcategories,
|
||||||
@@ -82,7 +82,7 @@ function ensureValidCategory(
|
|||||||
});
|
});
|
||||||
validator.assertType((v) => v.assertObject({
|
validator.assertType((v) => v.assertObject({
|
||||||
value: category,
|
value: category,
|
||||||
valueName: category.category ? `Category '${category.category}'` : 'Category',
|
valueName: category.category ?? 'category',
|
||||||
allowedProperties: [
|
allowedProperties: [
|
||||||
'docs', 'children', 'category',
|
'docs', 'children', 'category',
|
||||||
],
|
],
|
||||||
@@ -104,7 +104,7 @@ interface ExecutableParseContext {
|
|||||||
readonly data: ExecutableData;
|
readonly data: ExecutableData;
|
||||||
readonly children: CategoryChildren;
|
readonly children: CategoryChildren;
|
||||||
readonly parent: CategoryData;
|
readonly parent: CategoryData;
|
||||||
readonly collectionContext: CategoryCollectionContext;
|
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
|
||||||
readonly categoryUtilities: CategoryParserUtilities;
|
readonly categoryUtilities: CategoryParserUtilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,13 +124,13 @@ function parseUnknownExecutable(context: ExecutableParseContext) {
|
|||||||
if (isCategory(context.data)) {
|
if (isCategory(context.data)) {
|
||||||
const subCategory = parseCategoryRecursively({
|
const subCategory = parseCategoryRecursively({
|
||||||
categoryData: context.data,
|
categoryData: context.data,
|
||||||
collectionContext: context.collectionContext,
|
collectionUtilities: context.collectionUtilities,
|
||||||
parentCategory: context.parent,
|
parentCategory: context.parent,
|
||||||
categoryUtilities: context.categoryUtilities,
|
categoryUtilities: context.categoryUtilities,
|
||||||
});
|
});
|
||||||
context.children.subcategories.push(subCategory);
|
context.children.subcategories.push(subCategory);
|
||||||
} else { // A script
|
} else { // A script
|
||||||
const script = context.categoryUtilities.parseScript(context.data, context.collectionContext);
|
const script = context.categoryUtilities.parseScript(context.data, context.collectionUtilities);
|
||||||
context.children.subscripts.push(script);
|
context.children.subscripts.push(script);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface Pipe {
|
export interface IPipe {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
apply(input: string): string;
|
apply(input: string): string;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Pipe } from '../Pipe';
|
import type { IPipe } from '../IPipe';
|
||||||
|
|
||||||
export class EscapeDoubleQuotes implements Pipe {
|
export class EscapeDoubleQuotes implements IPipe {
|
||||||
public readonly name: string = 'escapeDoubleQuotes';
|
public readonly name: string = 'escapeDoubleQuotes';
|
||||||
|
|
||||||
public apply(raw: string): string {
|
public apply(raw: string): string {
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return '';
|
return raw;
|
||||||
}
|
}
|
||||||
return raw.replaceAll('"', '"^""');
|
return raw.replaceAll('"', '"^""');
|
||||||
/* eslint-disable vue/max-len */
|
/* eslint-disable vue/max-len */
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
import type { IPipe } from '../IPipe';
|
||||||
import type { Pipe } from '../Pipe';
|
|
||||||
|
|
||||||
export class InlinePowerShell implements Pipe {
|
export class InlinePowerShell implements IPipe {
|
||||||
public readonly name: string = 'inlinePowerShell';
|
public readonly name: string = 'inlinePowerShell';
|
||||||
|
|
||||||
public apply(code: string): string {
|
public apply(code: string): string {
|
||||||
@@ -9,11 +8,9 @@ export class InlinePowerShell implements Pipe {
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
|
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
|
||||||
// Order is important
|
|
||||||
inlineComments,
|
inlineComments,
|
||||||
mergeHereStrings,
|
|
||||||
mergeLinesWithBacktick,
|
mergeLinesWithBacktick,
|
||||||
mergeLinesWithBracketCodeBlocks,
|
mergeHereStrings,
|
||||||
mergeNewLines,
|
mergeNewLines,
|
||||||
]).reduce((a, b) => (data) => b(a(data)));
|
]).reduce((a, b) => (data) => b(a(data)));
|
||||||
const newCode = processor(code);
|
const newCode = processor(code);
|
||||||
@@ -92,6 +89,10 @@ function inlineComments(code: string): string {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLines(code: string): string[] {
|
||||||
|
return (code?.split(/\r\n|\r|\n/) || []);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||||
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings
|
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings
|
||||||
@@ -101,18 +102,18 @@ function mergeHereStrings(code: string) {
|
|||||||
return code.replaceAll(regex, (_$, quotes, scope) => {
|
return code.replaceAll(regex, (_$, quotes, scope) => {
|
||||||
const newString = getHereStringHandler(quotes);
|
const newString = getHereStringHandler(quotes);
|
||||||
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
||||||
const lines = splitTextIntoLines(escaped);
|
const lines = getLines(escaped);
|
||||||
const inlined = lines.join(newString.separator);
|
const inlined = lines.join(newString.separator);
|
||||||
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
||||||
return quoted;
|
return quoted;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
interface InlinedHereString {
|
interface IInlinedHereString {
|
||||||
readonly quotesAround: string;
|
readonly quotesAround: string;
|
||||||
readonly escapedQuotes: string;
|
readonly escapedQuotes: string;
|
||||||
readonly separator: string;
|
readonly separator: string;
|
||||||
}
|
}
|
||||||
function getHereStringHandler(quotes: string): InlinedHereString {
|
function getHereStringHandler(quotes: string): IInlinedHereString {
|
||||||
/*
|
/*
|
||||||
We handle @' and @" differently.
|
We handle @' and @" differently.
|
||||||
Single quotes are interpreted literally and doubles are expandable.
|
Single quotes are interpreted literally and doubles are expandable.
|
||||||
@@ -157,33 +158,9 @@ function mergeLinesWithBacktick(code: string) {
|
|||||||
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Inlines code blocks in PowerShell scripts while preserving correct syntax.
|
|
||||||
* It removes unnecessary newlines and spaces around brackets,
|
|
||||||
* inlining the code where possible.
|
|
||||||
* This prevents syntax errors like "Unexpected token '}'" when inlining brackets.
|
|
||||||
*/
|
|
||||||
function mergeLinesWithBracketCodeBlocks(code: string): string {
|
|
||||||
return code
|
|
||||||
// Opening bracket: [whitespace] Opening bracket (newline)
|
|
||||||
.replace(/(?<=.*)\s*{[\r\n][\s\r\n]*/g, ' { ')
|
|
||||||
// Closing bracket: [whitespace] Closing bracket (newline) (continuation keyword)
|
|
||||||
.replace(/\s*}[\r\n][\s\r\n]*(?=elseif|else|catch|finally|until)/g, ' } ')
|
|
||||||
.replace(/(?<=do\s*{.*)[\r\n\s]*}[\r\n][\r\n\s]*(?=while)/g, ' } '); // Do-While
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeNewLines(code: string) {
|
function mergeNewLines(code: string) {
|
||||||
const nonEmptyLines = splitTextIntoLines(code)
|
return getLines(code)
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter((line) => line.length > 0);
|
.filter((line) => line.length > 0)
|
||||||
|
.join('; ');
|
||||||
return nonEmptyLines
|
|
||||||
.map((line, index) => {
|
|
||||||
const isLastLine = index === nonEmptyLines.length - 1;
|
|
||||||
if (isLastLine) {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
return line.endsWith(';') ? line : `${line};`;
|
|
||||||
})
|
|
||||||
.join(' ');
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
||||||
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
||||||
import type { Pipe } from './Pipe';
|
import type { IPipe } from './IPipe';
|
||||||
|
|
||||||
const RegisteredPipes = [
|
const RegisteredPipes = [
|
||||||
new EscapeDoubleQuotes(),
|
new EscapeDoubleQuotes(),
|
||||||
@@ -8,19 +8,19 @@ const RegisteredPipes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export interface IPipeFactory {
|
export interface IPipeFactory {
|
||||||
get(pipeName: string): Pipe;
|
get(pipeName: string): IPipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PipeFactory implements IPipeFactory {
|
export class PipeFactory implements IPipeFactory {
|
||||||
private readonly pipes = new Map<string, Pipe>();
|
private readonly pipes = new Map<string, IPipe>();
|
||||||
|
|
||||||
constructor(pipes: readonly Pipe[] = RegisteredPipes) {
|
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||||
for (const pipe of pipes) {
|
for (const pipe of pipes) {
|
||||||
this.registerPipe(pipe);
|
this.registerPipe(pipe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(pipeName: string): Pipe {
|
public get(pipeName: string): IPipe {
|
||||||
validatePipeName(pipeName);
|
validatePipeName(pipeName);
|
||||||
const pipe = this.pipes.get(pipeName);
|
const pipe = this.pipes.get(pipeName);
|
||||||
if (!pipe) {
|
if (!pipe) {
|
||||||
@@ -29,7 +29,7 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
return pipe;
|
return pipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerPipe(pipe: Pipe): void {
|
private registerPipe(pipe: IPipe): void {
|
||||||
validatePipeName(pipe.name);
|
validatePipeName(pipe.name);
|
||||||
if (this.pipes.has(pipe.name)) {
|
if (this.pipes.has(pipe.name)) {
|
||||||
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const createFunctionCallArgument: FunctionCallArgumentFactory = (
|
|||||||
utilities.validateParameterName(parameterName);
|
utilities.validateParameterName(parameterName);
|
||||||
utilities.typeValidator.assertNonEmptyString({
|
utilities.typeValidator.assertNonEmptyString({
|
||||||
value: argumentValue,
|
value: argumentValue,
|
||||||
valueName: `Function parameter '${parameterName}'`,
|
valueName: `Missing argument value for the parameter "${parameterName}".`,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
parameterName,
|
parameterName,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
|
||||||
import type { CompiledCode } from '../CompiledCode';
|
import type { CompiledCode } from '../CompiledCode';
|
||||||
import type { CodeSegmentMerger } from './CodeSegmentMerger';
|
import type { CodeSegmentMerger } from './CodeSegmentMerger';
|
||||||
|
|
||||||
@@ -9,9 +8,11 @@ export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
||||||
revertCode: joinCodeParts(filterEmptyStrings(
|
revertCode: joinCodeParts(
|
||||||
codeSegments.map((f) => f.revertCode),
|
codeSegments
|
||||||
)),
|
.map((f) => f.revertCode)
|
||||||
|
.filter((code): code is string => Boolean(code)),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { IExpressionsCompiler } from '@/application/Parser/Executable/Scrip
|
|||||||
import { FunctionBodyType, type ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
|
import { FunctionBodyType, type ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
|
||||||
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
import { indentText } from '@/application/Common/Text/IndentText';
|
|
||||||
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||||
|
|
||||||
export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
|
export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||||
@@ -23,12 +22,10 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
|
|||||||
if (calledFunction.body.type !== FunctionBodyType.Code) {
|
if (calledFunction.body.type !== FunctionBodyType.Code) {
|
||||||
throw new Error([
|
throw new Error([
|
||||||
'Unexpected function body type.',
|
'Unexpected function body type.',
|
||||||
indentText([
|
`\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
|
||||||
`Expected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
|
`\tActual: "${FunctionBodyType[calledFunction.body.type]}"`,
|
||||||
`Actual: "${FunctionBodyType[calledFunction.body.type]}"`,
|
|
||||||
].join('\n')),
|
|
||||||
'Function:',
|
'Function:',
|
||||||
indentText(JSON.stringify(callToFunction)),
|
`\t${JSON.stringify(callToFunction)}`,
|
||||||
].join('\n'));
|
].join('\n'));
|
||||||
}
|
}
|
||||||
const { code } = calledFunction.body;
|
const { code } = calledFunction.body;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function getCallSequence(calls: FunctionCallsData, validator: TypeValidator): Fu
|
|||||||
if (isArray(calls)) {
|
if (isArray(calls)) {
|
||||||
validator.assertNonEmptyCollection({
|
validator.assertNonEmptyCollection({
|
||||||
value: calls,
|
value: calls,
|
||||||
valueName: 'Function call sequence',
|
valueName: 'function call sequence',
|
||||||
});
|
});
|
||||||
return calls as FunctionCallData[];
|
return calls as FunctionCallData[];
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ function parseFunctionCall(
|
|||||||
): FunctionCall {
|
): FunctionCall {
|
||||||
utilities.typeValidator.assertObject({
|
utilities.typeValidator.assertObject({
|
||||||
value: call,
|
value: call,
|
||||||
valueName: 'Function call',
|
valueName: 'function call',
|
||||||
allowedProperties: ['function', 'parameters'],
|
allowedProperties: ['function', 'parameters'],
|
||||||
});
|
});
|
||||||
const callArgs = parseArgs(call.parameters, utilities.createCallArgument);
|
const callArgs = parseArgs(call.parameters, utilities.createCallArgument);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const validateParameterName = (
|
|||||||
) => {
|
) => {
|
||||||
typeValidator.assertNonEmptyString({
|
typeValidator.assertNonEmptyString({
|
||||||
value: parameterName,
|
value: parameterName,
|
||||||
valueName: 'Parameter name',
|
valueName: 'parameter name',
|
||||||
rule: {
|
rule: {
|
||||||
expectedMatch: /^[0-9a-zA-Z]+$/,
|
expectedMatch: /^[0-9a-zA-Z]+$/,
|
||||||
errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`,
|
errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`,
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import type {
|
|||||||
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData,
|
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData,
|
||||||
CallInstruction, ParameterDefinitionData,
|
CallInstruction, ParameterDefinitionData,
|
||||||
} from '@/application/collections/';
|
} from '@/application/collections/';
|
||||||
import { validateCode, type CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
|
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||||
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
|
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
|
||||||
import type { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
import { CodeValidationRule } from '@/application/Parser/Executable/Script/Validation/CodeValidationRule';
|
|
||||||
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
||||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||||
import { parseFunctionCalls, type FunctionCallsParser } from './Call/FunctionCallsParser';
|
import { parseFunctionCalls, type FunctionCallsParser } from './Call/FunctionCallsParser';
|
||||||
@@ -21,14 +22,14 @@ import type { ISharedFunction } from './ISharedFunction';
|
|||||||
export interface SharedFunctionsParser {
|
export interface SharedFunctionsParser {
|
||||||
(
|
(
|
||||||
functions: readonly FunctionData[],
|
functions: readonly FunctionData[],
|
||||||
language: ScriptingLanguage,
|
syntax: ILanguageSyntax,
|
||||||
utilities?: SharedFunctionsParsingUtilities,
|
utilities?: SharedFunctionsParsingUtilities,
|
||||||
): ISharedFunctionCollection;
|
): ISharedFunctionCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseSharedFunctions: SharedFunctionsParser = (
|
export const parseSharedFunctions: SharedFunctionsParser = (
|
||||||
functions: readonly FunctionData[],
|
functions: readonly FunctionData[],
|
||||||
language: ScriptingLanguage,
|
syntax: ILanguageSyntax,
|
||||||
utilities = DefaultUtilities,
|
utilities = DefaultUtilities,
|
||||||
) => {
|
) => {
|
||||||
const collection = new SharedFunctionCollection();
|
const collection = new SharedFunctionCollection();
|
||||||
@@ -37,7 +38,7 @@ export const parseSharedFunctions: SharedFunctionsParser = (
|
|||||||
}
|
}
|
||||||
ensureValidFunctions(functions);
|
ensureValidFunctions(functions);
|
||||||
return functions
|
return functions
|
||||||
.map((func) => parseFunction(func, language, utilities))
|
.map((func) => parseFunction(func, syntax, utilities))
|
||||||
.reduce((acc, func) => {
|
.reduce((acc, func) => {
|
||||||
acc.addFunction(func);
|
acc.addFunction(func);
|
||||||
return acc;
|
return acc;
|
||||||
@@ -47,7 +48,7 @@ export const parseSharedFunctions: SharedFunctionsParser = (
|
|||||||
const DefaultUtilities: SharedFunctionsParsingUtilities = {
|
const DefaultUtilities: SharedFunctionsParsingUtilities = {
|
||||||
wrapError: wrapErrorWithAdditionalContext,
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
parseParameter: parseFunctionParameter,
|
parseParameter: parseFunctionParameter,
|
||||||
codeValidator: validateCode,
|
codeValidator: CodeValidator.instance,
|
||||||
createParameterCollection: createFunctionParameterCollection,
|
createParameterCollection: createFunctionParameterCollection,
|
||||||
parseFunctionCalls,
|
parseFunctionCalls,
|
||||||
};
|
};
|
||||||
@@ -55,20 +56,20 @@ const DefaultUtilities: SharedFunctionsParsingUtilities = {
|
|||||||
interface SharedFunctionsParsingUtilities {
|
interface SharedFunctionsParsingUtilities {
|
||||||
readonly wrapError: ErrorWithContextWrapper;
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
readonly parseParameter: FunctionParameterParser;
|
readonly parseParameter: FunctionParameterParser;
|
||||||
readonly codeValidator: CodeValidator;
|
readonly codeValidator: ICodeValidator;
|
||||||
readonly createParameterCollection: FunctionParameterCollectionFactory;
|
readonly createParameterCollection: FunctionParameterCollectionFactory;
|
||||||
readonly parseFunctionCalls: FunctionCallsParser;
|
readonly parseFunctionCalls: FunctionCallsParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFunction(
|
function parseFunction(
|
||||||
data: FunctionData,
|
data: FunctionData,
|
||||||
language: ScriptingLanguage,
|
syntax: ILanguageSyntax,
|
||||||
utilities: SharedFunctionsParsingUtilities,
|
utilities: SharedFunctionsParsingUtilities,
|
||||||
): ISharedFunction {
|
): ISharedFunction {
|
||||||
const { name } = data;
|
const { name } = data;
|
||||||
const parameters = parseParameters(data, utilities);
|
const parameters = parseParameters(data, utilities);
|
||||||
if (hasCode(data)) {
|
if (hasCode(data)) {
|
||||||
validateNonEmptyCode(data, language, utilities.codeValidator);
|
validateCode(data, syntax, utilities.codeValidator);
|
||||||
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||||
}
|
}
|
||||||
// Has call
|
// Has call
|
||||||
@@ -76,20 +77,17 @@ function parseFunction(
|
|||||||
return createCallerFunction(name, parameters, calls);
|
return createCallerFunction(name, parameters, calls);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateNonEmptyCode(
|
function validateCode(
|
||||||
data: CodeFunctionData,
|
data: CodeFunctionData,
|
||||||
language: ScriptingLanguage,
|
syntax: ILanguageSyntax,
|
||||||
validate: CodeValidator,
|
validator: ICodeValidator,
|
||||||
): void {
|
): void {
|
||||||
filterEmptyStrings([data.code, data.revertCode])
|
[data.code, data.revertCode]
|
||||||
|
.filter((code): code is string => Boolean(code))
|
||||||
.forEach(
|
.forEach(
|
||||||
(code) => validate(
|
(code) => validator.throwIfInvalid(
|
||||||
code,
|
code,
|
||||||
language,
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
[
|
|
||||||
CodeValidationRule.NoEmptyLines,
|
|
||||||
CodeValidationRule.NoDuplicatedLines,
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -206,9 +204,9 @@ function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
|||||||
if (duplicateCodes.length > 0) {
|
if (duplicateCodes.length > 0) {
|
||||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||||
}
|
}
|
||||||
const duplicateRevertCodes = getDuplicates(filterEmptyStrings(
|
const duplicateRevertCodes = getDuplicates(callFunctions
|
||||||
callFunctions.map((func) => func.revertCode),
|
.map((func) => func.revertCode)
|
||||||
));
|
.filter((code): code is string => Boolean(code)));
|
||||||
if (duplicateRevertCodes.length > 0) {
|
if (duplicateRevertCodes.length > 0) {
|
||||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ScriptData } from '@/application/collections/';
|
||||||
|
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||||
|
|
||||||
|
export interface IScriptCompiler {
|
||||||
|
canCompile(script: ScriptData): boolean;
|
||||||
|
compile(script: ScriptData): ScriptCode;
|
||||||
|
}
|
||||||
@@ -1,7 +1,87 @@
|
|||||||
import type { ScriptData } from '@/application/collections/';
|
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
||||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||||
|
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
|
import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||||
|
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
||||||
|
import { parseFunctionCalls } from './Function/Call/FunctionCallsParser';
|
||||||
|
import { parseSharedFunctions, type SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||||
|
import type { CompiledCode } from './Function/Call/Compiler/CompiledCode';
|
||||||
|
import type { IScriptCompiler } from './IScriptCompiler';
|
||||||
|
import type { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||||
|
import type { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';
|
||||||
|
|
||||||
export interface ScriptCompiler {
|
interface ScriptCompilerUtilities {
|
||||||
canCompile(script: ScriptData): boolean;
|
readonly sharedFunctionsParser: SharedFunctionsParser;
|
||||||
compile(script: ScriptData): ScriptCode;
|
readonly callCompiler: FunctionCallCompiler;
|
||||||
|
readonly codeValidator: ICodeValidator;
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
readonly scriptCodeFactory: ScriptCodeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultUtilities: ScriptCompilerUtilities = {
|
||||||
|
sharedFunctionsParser: parseSharedFunctions,
|
||||||
|
callCompiler: FunctionCallSequenceCompiler.instance,
|
||||||
|
codeValidator: CodeValidator.instance,
|
||||||
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
|
scriptCodeFactory: createScriptCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CategoryCollectionDataContext {
|
||||||
|
readonly functions: readonly FunctionData[];
|
||||||
|
readonly syntax: ILanguageSyntax;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScriptCompiler implements IScriptCompiler {
|
||||||
|
private readonly functions: ISharedFunctionCollection;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
categoryContext: CategoryCollectionDataContext,
|
||||||
|
private readonly utilities: ScriptCompilerUtilities = DefaultUtilities,
|
||||||
|
) {
|
||||||
|
this.functions = this.utilities.sharedFunctionsParser(
|
||||||
|
categoryContext.functions,
|
||||||
|
categoryContext.syntax,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public canCompile(script: ScriptData): boolean {
|
||||||
|
return hasCall(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
public compile(script: ScriptData): ScriptCode {
|
||||||
|
try {
|
||||||
|
if (!hasCall(script)) {
|
||||||
|
throw new Error('Script does include any calls.');
|
||||||
|
}
|
||||||
|
const calls = parseFunctionCalls(script.call);
|
||||||
|
const compiledCode = this.utilities.callCompiler.compileFunctionCalls(calls, this.functions);
|
||||||
|
validateCompiledCode(compiledCode, this.utilities.codeValidator);
|
||||||
|
return this.utilities.scriptCodeFactory(
|
||||||
|
compiledCode.code,
|
||||||
|
compiledCode.revertCode,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw this.utilities.wrapError(error, `Failed to compile script: ${script.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
||||||
|
[compiledCode.code, compiledCode.revertCode]
|
||||||
|
.filter((code): code is string => Boolean(code))
|
||||||
|
.map((code) => code as string)
|
||||||
|
.forEach(
|
||||||
|
(code) => validator.throwIfInvalid(
|
||||||
|
code,
|
||||||
|
[new NoEmptyLines()],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
|
||||||
|
return (data as CallInstruction).call !== undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
|
||||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
|
||||||
import { validateCode, type CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
|
||||||
import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
|
||||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
|
||||||
import type { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
import { CodeValidationRule } from '@/application/Parser/Executable/Script/Validation/CodeValidationRule';
|
|
||||||
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
|
||||||
import { parseFunctionCalls } from './Function/Call/FunctionCallsParser';
|
|
||||||
import { parseSharedFunctions, type SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
|
||||||
import type { CompiledCode } from './Function/Call/Compiler/CompiledCode';
|
|
||||||
import type { ScriptCompiler } from './ScriptCompiler';
|
|
||||||
import type { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
|
||||||
import type { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';
|
|
||||||
|
|
||||||
export interface ScriptCompilerInitParameters {
|
|
||||||
readonly categoryContext: CategoryCollectionDataContext;
|
|
||||||
readonly utilities?: ScriptCompilerUtilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScriptCompilerFactory {
|
|
||||||
(parameters: ScriptCompilerInitParameters): ScriptCompiler;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createScriptCompiler: ScriptCompilerFactory = (
|
|
||||||
parameters,
|
|
||||||
) => {
|
|
||||||
return new FunctionCallScriptCompiler(
|
|
||||||
parameters.categoryContext,
|
|
||||||
parameters.utilities ?? DefaultUtilities,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ScriptCompilerUtilities {
|
|
||||||
readonly sharedFunctionsParser: SharedFunctionsParser;
|
|
||||||
readonly callCompiler: FunctionCallCompiler;
|
|
||||||
readonly codeValidator: CodeValidator;
|
|
||||||
readonly wrapError: ErrorWithContextWrapper;
|
|
||||||
readonly scriptCodeFactory: ScriptCodeFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultUtilities: ScriptCompilerUtilities = {
|
|
||||||
sharedFunctionsParser: parseSharedFunctions,
|
|
||||||
callCompiler: FunctionCallSequenceCompiler.instance,
|
|
||||||
codeValidator: validateCode,
|
|
||||||
wrapError: wrapErrorWithAdditionalContext,
|
|
||||||
scriptCodeFactory: createScriptCode,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CategoryCollectionDataContext {
|
|
||||||
readonly functions: readonly FunctionData[];
|
|
||||||
readonly language: ScriptingLanguage;
|
|
||||||
}
|
|
||||||
|
|
||||||
class FunctionCallScriptCompiler implements ScriptCompiler {
|
|
||||||
private readonly functions: ISharedFunctionCollection;
|
|
||||||
|
|
||||||
private readonly language: ScriptingLanguage;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
categoryContext: CategoryCollectionDataContext,
|
|
||||||
private readonly utilities: ScriptCompilerUtilities = DefaultUtilities,
|
|
||||||
) {
|
|
||||||
this.functions = this.utilities.sharedFunctionsParser(
|
|
||||||
categoryContext.functions,
|
|
||||||
categoryContext.language,
|
|
||||||
);
|
|
||||||
this.language = categoryContext.language;
|
|
||||||
}
|
|
||||||
|
|
||||||
public canCompile(script: ScriptData): boolean {
|
|
||||||
return hasCall(script);
|
|
||||||
}
|
|
||||||
|
|
||||||
public compile(script: ScriptData): ScriptCode {
|
|
||||||
try {
|
|
||||||
if (!hasCall(script)) {
|
|
||||||
throw new Error('Script does include any calls.');
|
|
||||||
}
|
|
||||||
const calls = parseFunctionCalls(script.call);
|
|
||||||
const compiledCode = this.utilities.callCompiler.compileFunctionCalls(calls, this.functions);
|
|
||||||
validateCompiledCode(
|
|
||||||
compiledCode,
|
|
||||||
this.language,
|
|
||||||
this.utilities.codeValidator,
|
|
||||||
);
|
|
||||||
return this.utilities.scriptCodeFactory(
|
|
||||||
compiledCode.code,
|
|
||||||
compiledCode.revertCode,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
throw this.utilities.wrapError(error, `Failed to compile script: ${script.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateCompiledCode(
|
|
||||||
compiledCode: CompiledCode,
|
|
||||||
language: ScriptingLanguage,
|
|
||||||
validate: CodeValidator,
|
|
||||||
): void {
|
|
||||||
filterEmptyStrings([compiledCode.code, compiledCode.revertCode])
|
|
||||||
.forEach(
|
|
||||||
(code) => validate(
|
|
||||||
code,
|
|
||||||
language,
|
|
||||||
[
|
|
||||||
CodeValidationRule.NoEmptyLines,
|
|
||||||
CodeValidationRule.NoTooLongLines,
|
|
||||||
// Allow duplicated lines to enable calling same function multiple times
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
|
|
||||||
return (data as CallInstruction).call !== undefined;
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,33 @@
|
|||||||
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
|
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||||
import { validateCode, type CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||||
import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
|
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
|
||||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
|
||||||
import { createScript, type ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
|
import { createScript, type ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
|
||||||
import type { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
import { CodeValidationRule } from '@/application/Parser/Executable/Script/Validation/CodeValidationRule';
|
|
||||||
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
||||||
import { ExecutableType } from '../Validation/ExecutableType';
|
import { ExecutableType } from '../Validation/ExecutableType';
|
||||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
|
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
|
||||||
import type { CategoryCollectionContext } from '../CategoryCollectionContext';
|
import { CodeValidator } from './Validation/CodeValidator';
|
||||||
|
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||||
|
import type { CategoryCollectionSpecificUtilities } from '../CategoryCollectionSpecificUtilities';
|
||||||
|
|
||||||
export interface ScriptParser {
|
export interface ScriptParser {
|
||||||
(
|
(
|
||||||
data: ScriptData,
|
data: ScriptData,
|
||||||
collectionContext: CategoryCollectionContext,
|
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||||
scriptUtilities?: ScriptParserUtilities,
|
scriptUtilities?: ScriptParserUtilities,
|
||||||
): Script;
|
): Script;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseScript: ScriptParser = (
|
export const parseScript: ScriptParser = (
|
||||||
data,
|
data,
|
||||||
collectionContext,
|
collectionUtilities,
|
||||||
scriptUtilities = DefaultUtilities,
|
scriptUtilities = DefaultUtilities,
|
||||||
) => {
|
) => {
|
||||||
const validator = scriptUtilities.createValidator({
|
const validator = scriptUtilities.createValidator({
|
||||||
@@ -36,11 +37,11 @@ export const parseScript: ScriptParser = (
|
|||||||
validateScript(data, validator);
|
validateScript(data, validator);
|
||||||
try {
|
try {
|
||||||
const script = scriptUtilities.createScript({
|
const script = scriptUtilities.createScript({
|
||||||
executableId: data.name, // Pseudo-ID for uniqueness until real ID support
|
executableId: data.name, // arbitrary ID
|
||||||
name: data.name,
|
name: data.name,
|
||||||
code: parseCode(
|
code: parseCode(
|
||||||
data,
|
data,
|
||||||
collectionContext,
|
collectionUtilities,
|
||||||
scriptUtilities.codeValidator,
|
scriptUtilities.codeValidator,
|
||||||
scriptUtilities.createCode,
|
scriptUtilities.createCode,
|
||||||
),
|
),
|
||||||
@@ -68,34 +69,30 @@ function parseLevel(
|
|||||||
|
|
||||||
function parseCode(
|
function parseCode(
|
||||||
script: ScriptData,
|
script: ScriptData,
|
||||||
collectionContext: CategoryCollectionContext,
|
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||||
codeValidator: CodeValidator,
|
codeValidator: ICodeValidator,
|
||||||
createCode: ScriptCodeFactory,
|
createCode: ScriptCodeFactory,
|
||||||
): ScriptCode {
|
): ScriptCode {
|
||||||
if (collectionContext.compiler.canCompile(script)) {
|
if (collectionUtilities.compiler.canCompile(script)) {
|
||||||
return collectionContext.compiler.compile(script);
|
return collectionUtilities.compiler.compile(script);
|
||||||
}
|
}
|
||||||
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
||||||
const code = createCode(codeScript.code, codeScript.revertCode);
|
const code = createCode(codeScript.code, codeScript.revertCode);
|
||||||
validateHardcodedCodeWithoutCalls(code, codeValidator, collectionContext.language);
|
validateHardcodedCodeWithoutCalls(code, codeValidator, collectionUtilities.syntax);
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateHardcodedCodeWithoutCalls(
|
function validateHardcodedCodeWithoutCalls(
|
||||||
scriptCode: ScriptCode,
|
scriptCode: ScriptCode,
|
||||||
validate: CodeValidator,
|
validator: ICodeValidator,
|
||||||
language: ScriptingLanguage,
|
syntax: ILanguageSyntax,
|
||||||
) {
|
) {
|
||||||
filterEmptyStrings([scriptCode.execute, scriptCode.revert])
|
[scriptCode.execute, scriptCode.revert]
|
||||||
|
.filter((code): code is string => Boolean(code))
|
||||||
.forEach(
|
.forEach(
|
||||||
(code) => validate(
|
(code) => validator.throwIfInvalid(
|
||||||
code,
|
code,
|
||||||
language,
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
[
|
|
||||||
CodeValidationRule.NoEmptyLines,
|
|
||||||
CodeValidationRule.NoDuplicatedLines,
|
|
||||||
CodeValidationRule.NoTooLongLines,
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -106,7 +103,7 @@ function validateScript(
|
|||||||
): asserts script is NonNullable<ScriptData> {
|
): asserts script is NonNullable<ScriptData> {
|
||||||
validator.assertType((v) => v.assertObject<CallScriptData & CodeScriptData>({
|
validator.assertType((v) => v.assertObject<CallScriptData & CodeScriptData>({
|
||||||
value: script,
|
value: script,
|
||||||
valueName: script.name ? `Script '${script.name}'` : 'Script',
|
valueName: script.name ?? 'script',
|
||||||
allowedProperties: [
|
allowedProperties: [
|
||||||
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
||||||
],
|
],
|
||||||
@@ -129,7 +126,7 @@ function validateScript(
|
|||||||
interface ScriptParserUtilities {
|
interface ScriptParserUtilities {
|
||||||
readonly levelParser: EnumParser<RecommendationLevel>;
|
readonly levelParser: EnumParser<RecommendationLevel>;
|
||||||
readonly createScript: ScriptFactory;
|
readonly createScript: ScriptFactory;
|
||||||
readonly codeValidator: CodeValidator;
|
readonly codeValidator: ICodeValidator;
|
||||||
readonly wrapError: ErrorWithContextWrapper;
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
readonly createValidator: ExecutableValidatorFactory;
|
readonly createValidator: ExecutableValidatorFactory;
|
||||||
readonly createCode: ScriptCodeFactory;
|
readonly createCode: ScriptCodeFactory;
|
||||||
@@ -139,7 +136,7 @@ interface ScriptParserUtilities {
|
|||||||
const DefaultUtilities: ScriptParserUtilities = {
|
const DefaultUtilities: ScriptParserUtilities = {
|
||||||
levelParser: createEnumParser(RecommendationLevel),
|
levelParser: createEnumParser(RecommendationLevel),
|
||||||
createScript,
|
createScript,
|
||||||
codeValidator: validateCode,
|
codeValidator: CodeValidator.instance,
|
||||||
wrapError: wrapErrorWithAdditionalContext,
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
createValidator: createExecutableDataValidator,
|
createValidator: createExecutableDataValidator,
|
||||||
createCode: createScriptCode,
|
createCode: createScriptCode,
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import type { LanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/LanguageSyntax';
|
|
||||||
import type { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
import { createSyntax, type SyntaxFactory } from './Syntax/SyntaxFactory';
|
|
||||||
import type { CodeLine, CodeValidationAnalyzer, InvalidCodeLine } from './CodeValidationAnalyzer';
|
|
||||||
|
|
||||||
export type DuplicateLinesAnalyzer = CodeValidationAnalyzer & {
|
|
||||||
(
|
|
||||||
...args: [
|
|
||||||
...Parameters<CodeValidationAnalyzer>,
|
|
||||||
syntaxFactory?: SyntaxFactory,
|
|
||||||
]
|
|
||||||
): ReturnType<CodeValidationAnalyzer>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const analyzeDuplicateLines: DuplicateLinesAnalyzer = (
|
|
||||||
lines: readonly CodeLine[],
|
|
||||||
language: ScriptingLanguage,
|
|
||||||
syntaxFactory: SyntaxFactory = createSyntax,
|
|
||||||
) => {
|
|
||||||
const syntax = syntaxFactory(language);
|
|
||||||
return lines
|
|
||||||
.map((line): CodeLineWithDuplicateOccurrences => ({
|
|
||||||
lineNumber: line.lineNumber,
|
|
||||||
shouldBeIgnoredInAnalysis: shouldIgnoreLine(line.text, syntax),
|
|
||||||
duplicateLineNumbers: lines
|
|
||||||
.filter((other) => other.text === line.text)
|
|
||||||
.map((duplicatedLine) => duplicatedLine.lineNumber),
|
|
||||||
}))
|
|
||||||
.filter((line) => isNonIgnorableDuplicateLine(line))
|
|
||||||
.map((line): InvalidCodeLine => ({
|
|
||||||
lineNumber: line.lineNumber,
|
|
||||||
error: `Line is duplicated at line numbers ${line.duplicateLineNumbers.join(',')}.`,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CodeLineWithDuplicateOccurrences {
|
|
||||||
readonly lineNumber: number;
|
|
||||||
readonly duplicateLineNumbers: readonly number[];
|
|
||||||
readonly shouldBeIgnoredInAnalysis: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNonIgnorableDuplicateLine(line: CodeLineWithDuplicateOccurrences): boolean {
|
|
||||||
return !line.shouldBeIgnoredInAnalysis && line.duplicateLineNumbers.length > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldIgnoreLine(codeLine: string, syntax: LanguageSyntax): boolean {
|
|
||||||
return isCommentLine(codeLine, syntax)
|
|
||||||
|| isLineComposedEntirelyOfCommonCodeParts(codeLine, syntax);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCommentLine(codeLine: string, syntax: LanguageSyntax): boolean {
|
|
||||||
return syntax.commentDelimiters.some(
|
|
||||||
(delimiter) => codeLine.startsWith(delimiter),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLineComposedEntirelyOfCommonCodeParts(
|
|
||||||
codeLine: string,
|
|
||||||
syntax: LanguageSyntax,
|
|
||||||
): boolean {
|
|
||||||
const codeLineParts = codeLine.toLowerCase().trim().split(' ');
|
|
||||||
return codeLineParts.every((part) => syntax.commonCodeParts.includes(part));
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { CodeValidationAnalyzer, InvalidCodeLine } from './CodeValidationAnalyzer';
|
|
||||||
|
|
||||||
export const analyzeEmptyLines: CodeValidationAnalyzer = (
|
|
||||||
lines,
|
|
||||||
) => {
|
|
||||||
return lines
|
|
||||||
.filter((line) => isEmptyLine(line.text))
|
|
||||||
.map((line): InvalidCodeLine => ({
|
|
||||||
lineNumber: line.lineNumber,
|
|
||||||
error: (() => {
|
|
||||||
if (!line.text) {
|
|
||||||
return 'Empty line';
|
|
||||||
}
|
|
||||||
const markedText = line.text
|
|
||||||
.replaceAll(' ', '{whitespace}')
|
|
||||||
.replaceAll('\t', '{tab}');
|
|
||||||
return `Empty line: "${markedText}"`;
|
|
||||||
})(),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
function isEmptyLine(line: string): boolean {
|
|
||||||
return line.trim().length === 0;
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
import type { CodeValidationAnalyzer, InvalidCodeLine } from './CodeValidationAnalyzer';
|
|
||||||
|
|
||||||
export const analyzeTooLongLines: CodeValidationAnalyzer = (
|
|
||||||
lines,
|
|
||||||
language,
|
|
||||||
) => {
|
|
||||||
const maxLineLength = getMaxAllowedLineLength(language);
|
|
||||||
return lines
|
|
||||||
.filter((line) => line.text.length > maxLineLength)
|
|
||||||
.map((line): InvalidCodeLine => ({
|
|
||||||
lineNumber: line.lineNumber,
|
|
||||||
error: [
|
|
||||||
`Line is too long (${line.text.length}).`,
|
|
||||||
`It exceed maximum allowed length ${maxLineLength} by ${line.text.length - maxLineLength} characters.`,
|
|
||||||
'This may cause bugs due to unintended trimming by operating system, shells or terminal emulators.',
|
|
||||||
].join(' '),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
function getMaxAllowedLineLength(language: ScriptingLanguage): number {
|
|
||||||
switch (language) {
|
|
||||||
case ScriptingLanguage.batchfile:
|
|
||||||
/*
|
|
||||||
The maximum length of the string that you can use at the command prompt is 8191 characters.
|
|
||||||
https://web.archive.org/web/20240815120224/https://learn.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/command-line-string-limitation
|
|
||||||
*/
|
|
||||||
return 8191;
|
|
||||||
case ScriptingLanguage.shellscript:
|
|
||||||
/*
|
|
||||||
Tests show:
|
|
||||||
|
|
||||||
| OS | Command | Value |
|
|
||||||
| --- | ------- | ----- |
|
|
||||||
| Pop!_OS 22.04 | xargs --show-limits | 2088784 |
|
|
||||||
| macOS Sonoma 14.3 on Intel | getconf ARG_MAX | 1048576 |
|
|
||||||
| macOS Sonoma 14.3 on Apple Silicon M1 | getconf ARG_MAX | 1048576 |
|
|
||||||
| Android 12 (4.14.180) with Termux | xargs --show-limits | 2087244 |
|
|
||||||
*/
|
|
||||||
return 1048576; // Minimum value for reliability
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported language: ${ScriptingLanguage[language]} (${language})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
|
|
||||||
export interface CodeValidationAnalyzer {
|
|
||||||
(
|
|
||||||
lines: readonly CodeLine[],
|
|
||||||
language: ScriptingLanguage,
|
|
||||||
): InvalidCodeLine[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InvalidCodeLine {
|
|
||||||
readonly lineNumber: number;
|
|
||||||
readonly error: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CodeLine {
|
|
||||||
readonly lineNumber: number;
|
|
||||||
readonly text: string;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export interface LanguageSyntax {
|
|
||||||
readonly commentDelimiters: readonly string[];
|
|
||||||
readonly commonCodeParts: readonly string[];
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { LanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/LanguageSyntax';
|
|
||||||
|
|
||||||
export class ShellScriptSyntax implements LanguageSyntax {
|
|
||||||
public readonly commentDelimiters = ['#'];
|
|
||||||
|
|
||||||
public readonly commonCodeParts = ['(', ')', 'else', 'fi', 'done'];
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
import type { LanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/LanguageSyntax';
|
|
||||||
import { BatchFileSyntax } from './BatchFileSyntax';
|
|
||||||
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
|
||||||
|
|
||||||
export interface SyntaxFactory {
|
|
||||||
(language: ScriptingLanguage): LanguageSyntax;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createSyntax: SyntaxFactory = (language: ScriptingLanguage): LanguageSyntax => {
|
|
||||||
switch (language) {
|
|
||||||
case ScriptingLanguage.batchfile:
|
|
||||||
return new BatchFileSyntax();
|
|
||||||
case ScriptingLanguage.shellscript:
|
|
||||||
return new ShellScriptSyntax();
|
|
||||||
default:
|
|
||||||
throw new RangeError(`Invalid language: "${ScriptingLanguage[language]}"`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export enum CodeValidationRule {
|
|
||||||
NoEmptyLines,
|
|
||||||
NoDuplicatedLines,
|
|
||||||
NoTooLongLines,
|
|
||||||
}
|
|
||||||
@@ -1,78 +1,46 @@
|
|||||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
import type { ICodeLine } from './ICodeLine';
|
||||||
import type { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import type { ICodeValidationRule, IInvalidCodeLine } from './ICodeValidationRule';
|
||||||
import { createValidationAnalyzers, type ValidationRuleAnalyzerFactory } from './ValidationRuleAnalyzerFactory';
|
import type { ICodeValidator } from './ICodeValidator';
|
||||||
import type { CodeLine, InvalidCodeLine } from './Analyzers/CodeValidationAnalyzer';
|
|
||||||
import type { CodeValidationRule } from './CodeValidationRule';
|
|
||||||
|
|
||||||
export interface CodeValidator {
|
export class CodeValidator implements ICodeValidator {
|
||||||
(
|
public static readonly instance: ICodeValidator = new CodeValidator();
|
||||||
|
|
||||||
|
public throwIfInvalid(
|
||||||
code: string,
|
code: string,
|
||||||
language: ScriptingLanguage,
|
rules: readonly ICodeValidationRule[],
|
||||||
rules: readonly CodeValidationRule[],
|
): void {
|
||||||
analyzerFactory?: ValidationRuleAnalyzerFactory,
|
if (rules.length === 0) { throw new Error('missing rules'); }
|
||||||
): void;
|
if (!code) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = extractLines(code);
|
||||||
|
const invalidLines = rules.flatMap((rule) => rule.analyze(lines));
|
||||||
|
if (invalidLines.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const errorText = `Errors with the code.\n${printLines(lines, invalidLines)}`;
|
||||||
|
throw new Error(errorText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validateCode: CodeValidator = (
|
function extractLines(code: string): ICodeLine[] {
|
||||||
code,
|
return code
|
||||||
language,
|
.split(/\r\n|\r|\n/)
|
||||||
rules,
|
.map((lineText, lineIndex): ICodeLine => ({
|
||||||
analyzerFactory = createValidationAnalyzers,
|
index: lineIndex + 1,
|
||||||
) => {
|
text: lineText,
|
||||||
const analyzers = analyzerFactory(rules);
|
}));
|
||||||
if (!code) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const lines = extractLines(code);
|
|
||||||
const invalidLines = analyzers.flatMap((analyze) => analyze(lines, language));
|
|
||||||
if (invalidLines.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const errorText = `Errors with the code.\n${formatLines(lines, invalidLines)}`;
|
|
||||||
throw new Error(errorText);
|
|
||||||
};
|
|
||||||
|
|
||||||
function extractLines(code: string): CodeLine[] {
|
|
||||||
const lines = splitTextIntoLines(code);
|
|
||||||
return lines.map((lineText, lineIndex): CodeLine => ({
|
|
||||||
lineNumber: lineIndex + 1,
|
|
||||||
text: lineText,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLines(
|
function printLines(
|
||||||
lines: readonly CodeLine[],
|
lines: readonly ICodeLine[],
|
||||||
invalidLines: readonly InvalidCodeLine[],
|
invalidLines: readonly IInvalidCodeLine[],
|
||||||
): string {
|
): string {
|
||||||
return lines.map((line) => {
|
return lines.map((line) => {
|
||||||
const badLine = invalidLines.find((invalidLine) => invalidLine.lineNumber === line.lineNumber);
|
const badLine = invalidLines.find((invalidLine) => invalidLine.index === line.index);
|
||||||
return formatLine({
|
if (!badLine) {
|
||||||
lineNumber: line.lineNumber,
|
return `[${line.index}] ✅ ${line.text}`;
|
||||||
text: line.text,
|
}
|
||||||
error: badLine?.error,
|
return `[${badLine.index}] ❌ ${line.text}\n\t⟶ ${badLine.error}`;
|
||||||
});
|
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
}
|
}
|
||||||
function formatLine(
|
|
||||||
line: {
|
|
||||||
readonly lineNumber: number;
|
|
||||||
readonly text: string;
|
|
||||||
readonly error?: string;
|
|
||||||
},
|
|
||||||
): string {
|
|
||||||
let text = `[${line.lineNumber}] `;
|
|
||||||
text += line.error ? '❌' : '✅';
|
|
||||||
text += ` ${trimLine(line.text)}`;
|
|
||||||
if (line.error) {
|
|
||||||
text += `\n\t⟶ ${line.error}`;
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function trimLine(line: string) {
|
|
||||||
const maxLength = 500;
|
|
||||||
if (line.length > maxLength) {
|
|
||||||
line = `${line.substring(0, maxLength)}... [Rest of the line trimmed]`;
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ICodeLine {
|
||||||
|
readonly index: number;
|
||||||
|
readonly text: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { ICodeLine } from './ICodeLine';
|
||||||
|
|
||||||
|
export interface IInvalidCodeLine {
|
||||||
|
readonly index: number;
|
||||||
|
readonly error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICodeValidationRule {
|
||||||
|
analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { ICodeValidationRule } from './ICodeValidationRule';
|
||||||
|
|
||||||
|
export interface ICodeValidator {
|
||||||
|
throwIfInvalid(
|
||||||
|
code: string,
|
||||||
|
rules: readonly ICodeValidationRule[],
|
||||||
|
): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import type { ICodeLine } from '../ICodeLine';
|
||||||
|
import type { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||||
|
|
||||||
|
export class NoDuplicatedLines implements ICodeValidationRule {
|
||||||
|
constructor(private readonly syntax: ILanguageSyntax) { }
|
||||||
|
|
||||||
|
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||||
|
return lines
|
||||||
|
.map((line): IDuplicateAnalyzedLine => ({
|
||||||
|
index: line.index,
|
||||||
|
isIgnored: shouldIgnoreLine(line.text, this.syntax),
|
||||||
|
occurrenceIndices: lines
|
||||||
|
.filter((other) => other.text === line.text)
|
||||||
|
.map((duplicatedLine) => duplicatedLine.index),
|
||||||
|
}))
|
||||||
|
.filter((line) => hasInvalidDuplicates(line))
|
||||||
|
.map((line): IInvalidCodeLine => ({
|
||||||
|
index: line.index,
|
||||||
|
error: `Line is duplicated at line numbers ${line.occurrenceIndices.join(',')}.`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDuplicateAnalyzedLine {
|
||||||
|
readonly index: number;
|
||||||
|
readonly occurrenceIndices: readonly number[];
|
||||||
|
readonly isIgnored: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasInvalidDuplicates(line: IDuplicateAnalyzedLine): boolean {
|
||||||
|
return !line.isIgnored && line.occurrenceIndices.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
||||||
|
const lowerCaseCodeLine = codeLine.toLowerCase();
|
||||||
|
const isCommentLine = () => syntax.commentDelimiters.some(
|
||||||
|
(delimiter) => lowerCaseCodeLine.startsWith(delimiter),
|
||||||
|
);
|
||||||
|
const consistsOfFrequentCommands = () => {
|
||||||
|
const trimmed = lowerCaseCodeLine.trim().split(' ');
|
||||||
|
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
|
||||||
|
};
|
||||||
|
return isCommentLine() || consistsOfFrequentCommands();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||||
|
import type { ICodeLine } from '../ICodeLine';
|
||||||
|
|
||||||
|
export class NoEmptyLines implements ICodeValidationRule {
|
||||||
|
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||||
|
return lines
|
||||||
|
.filter((line) => (line.text?.trim().length ?? 0) === 0)
|
||||||
|
.map((line): IInvalidCodeLine => ({
|
||||||
|
index: line.index,
|
||||||
|
error: (() => {
|
||||||
|
if (!line.text) {
|
||||||
|
return 'Empty line';
|
||||||
|
}
|
||||||
|
const markedText = line.text
|
||||||
|
.replaceAll(' ', '{whitespace}')
|
||||||
|
.replaceAll('\t', '{tab}');
|
||||||
|
return `Empty line: "${markedText}"`;
|
||||||
|
})(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { LanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/LanguageSyntax';
|
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
|
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
|
||||||
const PowerShellCommonCodeParts = ['{', '}'];
|
const PowerShellCommonCodeParts = ['{', '}'];
|
||||||
|
|
||||||
export class BatchFileSyntax implements LanguageSyntax {
|
export class BatchFileSyntax implements ILanguageSyntax {
|
||||||
public readonly commentDelimiters = ['REM', '::'];
|
public readonly commentDelimiters = ['REM', '::'];
|
||||||
|
|
||||||
public readonly commonCodeParts = [...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts];
|
public readonly commonCodeParts = [...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts];
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ILanguageSyntax {
|
||||||
|
readonly commentDelimiters: string[];
|
||||||
|
readonly commonCodeParts: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import type { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||||
|
import type { ILanguageSyntax } from './ILanguageSyntax';
|
||||||
|
|
||||||
|
export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
|
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||||
|
public readonly commentDelimiters = ['#'];
|
||||||
|
|
||||||
|
public readonly commonCodeParts = ['(', ')', 'else', 'fi', 'done'];
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
||||||
|
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { BatchFileSyntax } from './BatchFileSyntax';
|
||||||
|
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||||
|
import type { ISyntaxFactory } from './ISyntaxFactory';
|
||||||
|
|
||||||
|
export class SyntaxFactory
|
||||||
|
extends ScriptingLanguageFactory<ILanguageSyntax>
|
||||||
|
implements ISyntaxFactory {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());
|
||||||
|
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellScriptSyntax());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { CodeValidationRule } from './CodeValidationRule';
|
|
||||||
import { analyzeDuplicateLines } from './Analyzers/AnalyzeDuplicateLines';
|
|
||||||
import { analyzeEmptyLines } from './Analyzers/AnalyzeEmptyLines';
|
|
||||||
import { analyzeTooLongLines } from './Analyzers/AnalyzeTooLongLines';
|
|
||||||
import type { CodeValidationAnalyzer } from './Analyzers/CodeValidationAnalyzer';
|
|
||||||
|
|
||||||
export interface ValidationRuleAnalyzerFactory {
|
|
||||||
(
|
|
||||||
rules: readonly CodeValidationRule[],
|
|
||||||
): CodeValidationAnalyzer[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createValidationAnalyzers: ValidationRuleAnalyzerFactory = (
|
|
||||||
rules,
|
|
||||||
): CodeValidationAnalyzer[] => {
|
|
||||||
if (rules.length === 0) { throw new Error('missing rules'); }
|
|
||||||
validateUniqueRules(rules);
|
|
||||||
return rules.map((rule) => createValidationRule(rule));
|
|
||||||
};
|
|
||||||
|
|
||||||
function createValidationRule(rule: CodeValidationRule): CodeValidationAnalyzer {
|
|
||||||
switch (rule) {
|
|
||||||
case CodeValidationRule.NoEmptyLines:
|
|
||||||
return analyzeEmptyLines;
|
|
||||||
case CodeValidationRule.NoDuplicatedLines:
|
|
||||||
return analyzeDuplicateLines;
|
|
||||||
case CodeValidationRule.NoTooLongLines:
|
|
||||||
return analyzeTooLongLines;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown rule: ${rule}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateUniqueRules(
|
|
||||||
rules: readonly CodeValidationRule[],
|
|
||||||
): void {
|
|
||||||
const ruleCounts = new Map<CodeValidationRule, number>();
|
|
||||||
rules.forEach((rule) => {
|
|
||||||
ruleCounts.set(rule, (ruleCounts.get(rule) || 0) + 1);
|
|
||||||
});
|
|
||||||
const duplicates = Array.from(ruleCounts.entries())
|
|
||||||
.filter(([, count]) => count > 1)
|
|
||||||
.map(([rule, count]) => `${CodeValidationRule[rule]} (${count} times)`);
|
|
||||||
if (duplicates.length > 0) {
|
|
||||||
throw new Error(`Duplicate rules are not allowed. Duplicates found: ${duplicates.join(', ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,7 +37,7 @@ function validateData(
|
|||||||
): void {
|
): void {
|
||||||
validator.assertObject({
|
validator.assertObject({
|
||||||
value: data,
|
value: data,
|
||||||
valueName: 'Scripting definition',
|
valueName: 'scripting definition',
|
||||||
allowedProperties: ['language', 'fileExtension', 'startCode', 'endCode'],
|
allowedProperties: ['language', 'fileExtension', 'startCode', 'endCode'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,12 +69,6 @@ definitions:
|
|||||||
- $ref: '#/definitions/CodeScript'
|
- $ref: '#/definitions/CodeScript'
|
||||||
- $ref: '#/definitions/CallScript'
|
- $ref: '#/definitions/CallScript'
|
||||||
|
|
||||||
RecommendationLevel:
|
|
||||||
oneOf:
|
|
||||||
- type: string
|
|
||||||
enum: [standard, strict]
|
|
||||||
- type: 'null'
|
|
||||||
|
|
||||||
ScriptDefinition:
|
ScriptDefinition:
|
||||||
type: object
|
type: object
|
||||||
allOf:
|
allOf:
|
||||||
@@ -84,7 +78,8 @@ definitions:
|
|||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
recommend:
|
recommend:
|
||||||
$ref: '#/definitions/RecommendationLevel'
|
type: string
|
||||||
|
enum: [standard, strict]
|
||||||
|
|
||||||
CodeScript:
|
CodeScript:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -1800,7 +1800,7 @@ actions:
|
|||||||
# References for spctl --master-disable
|
# References for spctl --master-disable
|
||||||
- https://web.archive.org/web/20240523173608/https://www.manpagez.com/man/8/spctl/
|
- https://web.archive.org/web/20240523173608/https://www.manpagez.com/man/8/spctl/
|
||||||
# References for /var/db/SystemPolicy-prefs.plist
|
# References for /var/db/SystemPolicy-prefs.plist
|
||||||
- https://web.archive.org/web/20240810103202/https://krypted.com/mac-security/manage-gatekeeper-from-the-command-line-in-mountain-lion/
|
- https://krypted.com/mac-security/manage-gatekeeper-from-the-command-line-in-mountain-lion/
|
||||||
- https://community.jamf.com/t5/jamf-pro/users-can-t-change-password-greyed-out/m-p/54228
|
- https://community.jamf.com/t5/jamf-pro/users-can-t-change-password-greyed-out/m-p/54228
|
||||||
code: |-
|
code: |-
|
||||||
os_major_ver=$(sw_vers -productVersion | awk -F "." '{print $1}')
|
os_major_ver=$(sw_vers -productVersion | awk -F "." '{print $1}')
|
||||||
@@ -1842,10 +1842,10 @@ actions:
|
|||||||
fi
|
fi
|
||||||
-
|
-
|
||||||
name: Disable library validation entitlement (library signature validation)
|
name: Disable library validation entitlement (library signature validation)
|
||||||
docs: |-
|
docs:
|
||||||
- [Disable Library Validation Entitlement | Apple Developer Documentation | developer.apple.com](https://archive.ph/2024.07.19-101811/https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_disable-library-validation)
|
- https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_disable-library-validation
|
||||||
- [Forbidden Commands to Speed Up macOS | www.naut.ca](https://web.archive.org/web/20240625020749/https://www.naut.ca/blog/2020/11/13/forbidden-commands-to-liberate-macos/)
|
- https://www.macenhance.com/docs/general/sip-library-validation.html
|
||||||
- [macEnhance | macEnhance.com](https://web.archive.org/web/20220622212008/https://www.macenhance.com/docs/general/sip-library-validation.html)
|
- https://www.naut.ca/blog/2020/11/13/forbidden-commands-to-liberate-macos/
|
||||||
code: sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist 'DisableLibraryValidation' -bool true
|
code: sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist 'DisableLibraryValidation' -bool true
|
||||||
revertCode: sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist 'DisableLibraryValidation' -bool false
|
revertCode: sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist 'DisableLibraryValidation' -bool false
|
||||||
-
|
-
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,11 @@
|
|||||||
import { getEnumValues, assertInRange } from '@/application/Common/Enum';
|
import { getEnumValues, assertInRange } from '@/application/Common/Enum';
|
||||||
import { RecommendationLevel } from '../Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '../Executables/Script/RecommendationLevel';
|
||||||
import { OperatingSystem } from '../OperatingSystem';
|
import { OperatingSystem } from '../OperatingSystem';
|
||||||
import { validateCategoryCollection } from './Validation/CompositeCategoryCollectionValidator';
|
import type { ExecutableId, Identifiable } from '../Executables/Identifiable';
|
||||||
import type { ExecutableId } from '../Executables/Identifiable';
|
|
||||||
import type { Category } from '../Executables/Category/Category';
|
import type { Category } from '../Executables/Category/Category';
|
||||||
import type { Script } from '../Executables/Script/Script';
|
import type { Script } from '../Executables/Script/Script';
|
||||||
import type { IScriptingDefinition } from '../IScriptingDefinition';
|
import type { IScriptingDefinition } from '../IScriptingDefinition';
|
||||||
import type { ICategoryCollection } from './ICategoryCollection';
|
import type { ICategoryCollection } from './ICategoryCollection';
|
||||||
import type { CategoryCollectionValidator } from './Validation/CategoryCollectionValidator';
|
|
||||||
|
|
||||||
export class CategoryCollection implements ICategoryCollection {
|
export class CategoryCollection implements ICategoryCollection {
|
||||||
public readonly os: OperatingSystem;
|
public readonly os: OperatingSystem;
|
||||||
@@ -24,18 +22,16 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
parameters: CategoryCollectionInitParameters,
|
parameters: CategoryCollectionInitParameters,
|
||||||
validate: CategoryCollectionValidator = validateCategoryCollection,
|
|
||||||
) {
|
) {
|
||||||
this.os = parameters.os;
|
this.os = parameters.os;
|
||||||
this.actions = parameters.actions;
|
this.actions = parameters.actions;
|
||||||
this.scripting = parameters.scripting;
|
this.scripting = parameters.scripting;
|
||||||
|
|
||||||
this.queryable = makeQueryable(this.actions);
|
this.queryable = makeQueryable(this.actions);
|
||||||
validate({
|
assertInRange(this.os, OperatingSystem);
|
||||||
allScripts: this.queryable.allScripts,
|
ensureValid(this.queryable);
|
||||||
allCategories: this.queryable.allCategories,
|
ensureNoDuplicateIds(this.queryable.allCategories);
|
||||||
operatingSystem: this.os,
|
ensureNoDuplicateIds(this.queryable.allScripts);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCategory(executableId: ExecutableId): Category {
|
public getCategory(executableId: ExecutableId): Category {
|
||||||
@@ -52,10 +48,10 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
return scripts ?? [];
|
return scripts ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public getScript(executableId: ExecutableId): Script {
|
public getScript(executableId: string): Script {
|
||||||
const script = this.queryable.allScripts.find((s) => s.executableId === executableId);
|
const script = this.queryable.allScripts.find((s) => s.executableId === executableId);
|
||||||
if (!script) {
|
if (!script) {
|
||||||
throw new Error(`Missing script: ${executableId}`);
|
throw new Error(`missing script: ${executableId}`);
|
||||||
}
|
}
|
||||||
return script;
|
return script;
|
||||||
}
|
}
|
||||||
@@ -69,6 +65,18 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureNoDuplicateIds(executables: ReadonlyArray<Identifiable>) { // TODO: Unit test this
|
||||||
|
const duplicatedIds = executables
|
||||||
|
.map((e) => e.executableId)
|
||||||
|
.filter((id, index, array) => array.findIndex((otherId) => otherId === id) !== index);
|
||||||
|
if (duplicatedIds.length > 0) {
|
||||||
|
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
||||||
|
throw new Error(
|
||||||
|
`Duplicate executables are detected with following id(s): ${duplicatedIdsText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface CategoryCollectionInitParameters {
|
export interface CategoryCollectionInitParameters {
|
||||||
readonly os: OperatingSystem;
|
readonly os: OperatingSystem;
|
||||||
readonly actions: ReadonlyArray<Category>;
|
readonly actions: ReadonlyArray<Category>;
|
||||||
@@ -81,12 +89,35 @@ interface QueryableCollection {
|
|||||||
readonly scriptsByLevel: Map<RecommendationLevel, readonly Script[]>;
|
readonly scriptsByLevel: Map<RecommendationLevel, readonly Script[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenCategoryHierarchy(
|
function ensureValid(application: QueryableCollection) {
|
||||||
|
ensureValidCategories(application.allCategories);
|
||||||
|
ensureValidScripts(application.allScripts);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidCategories(allCategories: readonly Category[]) {
|
||||||
|
if (!allCategories.length) {
|
||||||
|
throw new Error('must consist of at least one category');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidScripts(allScripts: readonly Script[]) {
|
||||||
|
if (!allScripts.length) {
|
||||||
|
throw new Error('must consist of at least one script');
|
||||||
|
}
|
||||||
|
const missingRecommendationLevels = getEnumValues(RecommendationLevel)
|
||||||
|
.filter((level) => allScripts.every((script) => script.level !== level));
|
||||||
|
if (missingRecommendationLevels.length > 0) {
|
||||||
|
throw new Error('none of the scripts are recommended as'
|
||||||
|
+ ` "${missingRecommendationLevels.map((level) => RecommendationLevel[level]).join(', "')}".`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenApplication(
|
||||||
categories: ReadonlyArray<Category>,
|
categories: ReadonlyArray<Category>,
|
||||||
): [Category[], Script[]] {
|
): [Category[], Script[]] {
|
||||||
const [subCategories, subScripts] = (categories || [])
|
const [subCategories, subScripts] = (categories || [])
|
||||||
// Parse children
|
// Parse children
|
||||||
.map((category) => flattenCategoryHierarchy(category.subcategories))
|
.map((category) => flattenApplication(category.subcategories))
|
||||||
// Flatten results
|
// Flatten results
|
||||||
.reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => {
|
.reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => {
|
||||||
return [
|
return [
|
||||||
@@ -109,7 +140,7 @@ function flattenCategoryHierarchy(
|
|||||||
function makeQueryable(
|
function makeQueryable(
|
||||||
actions: ReadonlyArray<Category>,
|
actions: ReadonlyArray<Category>,
|
||||||
): QueryableCollection {
|
): QueryableCollection {
|
||||||
const flattened = flattenCategoryHierarchy(actions);
|
const flattened = flattenApplication(actions);
|
||||||
return {
|
return {
|
||||||
allCategories: flattened[0],
|
allCategories: flattened[0],
|
||||||
allScripts: flattened[1],
|
allScripts: flattened[1],
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
|
||||||
import type { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
|
|
||||||
export interface CategoryCollectionValidationContext {
|
|
||||||
readonly allScripts: readonly Script[];
|
|
||||||
readonly allCategories: readonly Category[];
|
|
||||||
readonly operatingSystem: OperatingSystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CategoryCollectionValidator {
|
|
||||||
(
|
|
||||||
context: CategoryCollectionValidationContext,
|
|
||||||
): void;
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { ensurePresenceOfAtLeastOneScript } from './Rules/EnsurePresenceOfAtLeastOneScript';
|
|
||||||
import { ensurePresenceOfAtLeastOneCategory } from './Rules/EnsurePresenceOfAtLeastOneCategory';
|
|
||||||
import { ensureUniqueIdsAcrossExecutables } from './Rules/EnsureUniqueIdsAcrossExecutables';
|
|
||||||
import { ensureKnownOperatingSystem } from './Rules/EnsureKnownOperatingSystem';
|
|
||||||
import type { CategoryCollectionValidationContext, CategoryCollectionValidator } from './CategoryCollectionValidator';
|
|
||||||
|
|
||||||
export type CompositeCategoryCollectionValidator = CategoryCollectionValidator & {
|
|
||||||
(
|
|
||||||
...args: [
|
|
||||||
...Parameters<CategoryCollectionValidator>,
|
|
||||||
(readonly CategoryCollectionValidator[])?,
|
|
||||||
]
|
|
||||||
): ReturnType<CategoryCollectionValidator>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateCategoryCollection: CompositeCategoryCollectionValidator = (
|
|
||||||
context: CategoryCollectionValidationContext,
|
|
||||||
validators: readonly CategoryCollectionValidator[] = DefaultValidators,
|
|
||||||
) => {
|
|
||||||
if (!validators.length) {
|
|
||||||
throw new Error('No validators provided.');
|
|
||||||
}
|
|
||||||
for (const validate of validators) {
|
|
||||||
validate(context);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const DefaultValidators: readonly CategoryCollectionValidator[] = [
|
|
||||||
ensureKnownOperatingSystem,
|
|
||||||
ensurePresenceOfAtLeastOneScript,
|
|
||||||
ensurePresenceOfAtLeastOneCategory,
|
|
||||||
ensureUniqueIdsAcrossExecutables,
|
|
||||||
];
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { assertInRange } from '@/application/Common/Enum';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
import type { CategoryCollectionValidator } from '../CategoryCollectionValidator';
|
|
||||||
|
|
||||||
export const ensureKnownOperatingSystem: CategoryCollectionValidator = (
|
|
||||||
context,
|
|
||||||
) => {
|
|
||||||
assertInRange(context.operatingSystem, OperatingSystem);
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { getEnumValues } from '@/application/Common/Enum';
|
|
||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
|
||||||
import type { CategoryCollectionValidator } from '../CategoryCollectionValidator';
|
|
||||||
|
|
||||||
export const ensurePresenceOfAllRecommendationLevels: CategoryCollectionValidator = (
|
|
||||||
context,
|
|
||||||
) => {
|
|
||||||
const unrepresentedRecommendationLevels = getUnrepresentedRecommendationLevels(
|
|
||||||
context.allScripts,
|
|
||||||
);
|
|
||||||
if (unrepresentedRecommendationLevels.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const formattedRecommendationLevels = unrepresentedRecommendationLevels
|
|
||||||
.map((level) => getDisplayName(level))
|
|
||||||
.join(', ');
|
|
||||||
throw new Error(`Missing recommendation levels: ${formattedRecommendationLevels}.`);
|
|
||||||
};
|
|
||||||
|
|
||||||
function getUnrepresentedRecommendationLevels(
|
|
||||||
scripts: readonly Script[],
|
|
||||||
): (RecommendationLevel | undefined)[] {
|
|
||||||
const expectedLevels = [
|
|
||||||
undefined,
|
|
||||||
...getEnumValues(RecommendationLevel),
|
|
||||||
];
|
|
||||||
return expectedLevels.filter(
|
|
||||||
(level) => scripts.every((script) => script.level !== level),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisplayName(level: RecommendationLevel | undefined): string {
|
|
||||||
return level === undefined ? 'None' : RecommendationLevel[level];
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { CategoryCollectionValidator } from '../CategoryCollectionValidator';
|
|
||||||
|
|
||||||
export const ensurePresenceOfAtLeastOneCategory: CategoryCollectionValidator = (
|
|
||||||
context,
|
|
||||||
) => {
|
|
||||||
if (!context.allCategories.length) {
|
|
||||||
throw new Error('Collection must have at least one category');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { CategoryCollectionValidator } from '../CategoryCollectionValidator';
|
|
||||||
|
|
||||||
export const ensurePresenceOfAtLeastOneScript: CategoryCollectionValidator = (
|
|
||||||
context,
|
|
||||||
) => {
|
|
||||||
if (!context.allScripts.length) {
|
|
||||||
throw new Error('Collection must have at least one script');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import type { Identifiable } from '@/domain/Executables/Identifiable';
|
|
||||||
import type { CategoryCollectionValidator } from '../CategoryCollectionValidator';
|
|
||||||
|
|
||||||
export const ensureUniqueIdsAcrossExecutables: CategoryCollectionValidator = (
|
|
||||||
context,
|
|
||||||
) => {
|
|
||||||
const allExecutables: readonly Identifiable[] = [
|
|
||||||
...context.allCategories,
|
|
||||||
...context.allScripts,
|
|
||||||
];
|
|
||||||
ensureNoDuplicateIds(allExecutables);
|
|
||||||
};
|
|
||||||
|
|
||||||
function ensureNoDuplicateIds(
|
|
||||||
executables: readonly Identifiable[],
|
|
||||||
) {
|
|
||||||
const duplicateExecutables = getExecutablesWithDuplicateIds(executables);
|
|
||||||
if (duplicateExecutables.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const formattedDuplicateIds = duplicateExecutables.map(
|
|
||||||
(executable) => `"${executable.executableId}"`,
|
|
||||||
).join(', ');
|
|
||||||
throw new Error(`Duplicate executable IDs found: ${formattedDuplicateIds}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getExecutablesWithDuplicateIds(
|
|
||||||
executables: readonly Identifiable[],
|
|
||||||
): Identifiable[] {
|
|
||||||
return executables
|
|
||||||
.filter(
|
|
||||||
(executable, index, array) => {
|
|
||||||
const otherIndex = array.findIndex(
|
|
||||||
(otherExecutable) => haveIdenticalIds(executable, otherExecutable),
|
|
||||||
);
|
|
||||||
return otherIndex !== index;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function haveIdenticalIds(first: Identifiable, second: Identifiable): boolean {
|
|
||||||
return first.executableId === second.executableId;
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { RecommendationLevel } from './RecommendationLevel';
|
import { RecommendationLevel } from './RecommendationLevel';
|
||||||
import type { ScriptCode } from './Code/ScriptCode';
|
import type { ScriptCode } from './Code/ScriptCode';
|
||||||
import type { Script } from './Script';
|
import type { Script } from './Script';
|
||||||
import type { ExecutableId } from '../Identifiable';
|
|
||||||
|
|
||||||
export interface ScriptInitParameters {
|
export interface ScriptInitParameters {
|
||||||
readonly executableId: ExecutableId;
|
readonly executableId: string;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly code: ScriptCode;
|
readonly code: ScriptCode;
|
||||||
readonly docs: ReadonlyArray<string>;
|
readonly docs: ReadonlyArray<string>;
|
||||||
@@ -20,7 +19,7 @@ export const createScript: ScriptFactory = (parameters) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class CollectionScript implements Script {
|
class CollectionScript implements Script {
|
||||||
public readonly executableId: ExecutableId;
|
public readonly executableId: string;
|
||||||
|
|
||||||
public readonly name: string;
|
public readonly name: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,32 @@
|
|||||||
import type { Logger } from '@/application/Common/Log/Logger';
|
import type { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
import { NodeElectronFileSystemOperations } from '@/infrastructure/FileSystem/NodeElectronFileSystemOperations';
|
import type { CodeRunError, CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
||||||
import type {
|
import { NodeElectronSystemOperations } from '../../System/NodeElectronSystemOperations';
|
||||||
DirectoryCreationOutcome, ApplicationDirectoryProvider, DirectoryType,
|
import type { SystemOperations } from '../../System/SystemOperations';
|
||||||
DirectoryCreationError, DirectoryCreationErrorType,
|
import type { ScriptDirectoryOutcome, ScriptDirectoryProvider } from './ScriptDirectoryProvider';
|
||||||
} from './ApplicationDirectoryProvider';
|
|
||||||
import type { FileSystemOperations } from '../FileSystemOperations';
|
|
||||||
|
|
||||||
export const SubdirectoryNames: Record<DirectoryType, string> = {
|
export const ExecutionSubdirectory = 'runs';
|
||||||
'script-runs': 'runs',
|
|
||||||
'update-installation-files': 'updates',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides persistent directories.
|
* Provides a dedicated directory for script execution.
|
||||||
* Benefits of using a persistent directory:
|
* Benefits of using a persistent directory:
|
||||||
* - Antivirus Exclusions: Easier antivirus configuration.
|
* - Antivirus Exclusions: Easier antivirus configuration.
|
||||||
* - Auditability: Stores script execution history for troubleshooting.
|
* - Auditability: Stores script execution history for troubleshooting.
|
||||||
* - Reliability: Avoids issues with directory clean-ups during execution,
|
* - Reliability: Avoids issues with directory clean-ups during execution,
|
||||||
* seen in Windows Pro Azure VMs when stored on Windows temporary directory.
|
* seen in Windows Pro Azure VMs when stored on Windows temporary directory.
|
||||||
*/
|
*/
|
||||||
export class PersistentApplicationDirectoryProvider implements ApplicationDirectoryProvider {
|
export class PersistentDirectoryProvider implements ScriptDirectoryProvider {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly fileSystem: FileSystemOperations = NodeElectronFileSystemOperations,
|
private readonly system: SystemOperations = new NodeElectronSystemOperations(),
|
||||||
private readonly logger: Logger = ElectronLogger,
|
private readonly logger: Logger = ElectronLogger,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public async provideDirectory(type: DirectoryType): Promise<DirectoryCreationOutcome> {
|
public async provideScriptDirectory(): Promise<ScriptDirectoryOutcome> {
|
||||||
const {
|
const {
|
||||||
success: isPathConstructed,
|
success: isPathConstructed,
|
||||||
error: pathConstructionError,
|
error: pathConstructionError,
|
||||||
directoryPath,
|
directoryPath,
|
||||||
} = this.constructScriptDirectoryPath(type);
|
} = this.constructScriptDirectoryPath();
|
||||||
if (!isPathConstructed) {
|
if (!isPathConstructed) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -57,7 +52,7 @@ export class PersistentApplicationDirectoryProvider implements ApplicationDirect
|
|||||||
private async createDirectory(directoryPath: string): Promise<DirectoryPathCreationOutcome> {
|
private async createDirectory(directoryPath: string): Promise<DirectoryPathCreationOutcome> {
|
||||||
try {
|
try {
|
||||||
this.logger.info(`Attempting to create script directory at path: ${directoryPath}`);
|
this.logger.info(`Attempting to create script directory at path: ${directoryPath}`);
|
||||||
await this.fileSystem.createDirectory(directoryPath, true);
|
await this.system.fileSystem.createDirectory(directoryPath, true);
|
||||||
this.logger.info(`Script directory successfully created at: ${directoryPath}`);
|
this.logger.info(`Script directory successfully created at: ${directoryPath}`);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -65,26 +60,17 @@ export class PersistentApplicationDirectoryProvider implements ApplicationDirect
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: this.handleError(error, 'DirectoryWriteError'),
|
error: this.handleException(error, 'DirectoryCreationError'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructScriptDirectoryPath(type: DirectoryType): DirectoryPathConstructionOutcome {
|
private constructScriptDirectoryPath(): DirectoryPathConstructionOutcome {
|
||||||
let parentDirectory: string;
|
|
||||||
try {
|
try {
|
||||||
parentDirectory = this.fileSystem.getUserDataDirectory();
|
const parentDirectory = this.system.operatingSystem.getUserDataDirectory();
|
||||||
} catch (error) {
|
const scriptDirectory = this.system.location.combinePaths(
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: this.handleError(error, 'UserDataFolderRetrievalError'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const subdirectoryName = SubdirectoryNames[type];
|
|
||||||
const scriptDirectory = this.fileSystem.combinePaths(
|
|
||||||
parentDirectory,
|
parentDirectory,
|
||||||
subdirectoryName,
|
ExecutionSubdirectory,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -93,15 +79,15 @@ export class PersistentApplicationDirectoryProvider implements ApplicationDirect
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: this.handleError(error, 'PathConstructionError'),
|
error: this.handleException(error, 'DirectoryCreationError'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleError(
|
private handleException(
|
||||||
exception: Error,
|
exception: Error,
|
||||||
errorType: DirectoryCreationErrorType,
|
errorType: CodeRunErrorType,
|
||||||
): DirectoryCreationError {
|
): CodeRunError {
|
||||||
const errorMessage = 'Error during script directory creation';
|
const errorMessage = 'Error during script directory creation';
|
||||||
this.logger.error(errorType, errorMessage, exception);
|
this.logger.error(errorType, errorMessage, exception);
|
||||||
return {
|
return {
|
||||||
@@ -113,7 +99,7 @@ export class PersistentApplicationDirectoryProvider implements ApplicationDirect
|
|||||||
|
|
||||||
type DirectoryPathConstructionOutcome = {
|
type DirectoryPathConstructionOutcome = {
|
||||||
readonly success: false;
|
readonly success: false;
|
||||||
readonly error: DirectoryCreationError;
|
readonly error: CodeRunError;
|
||||||
readonly directoryPath?: undefined;
|
readonly directoryPath?: undefined;
|
||||||
} | {
|
} | {
|
||||||
readonly success: true;
|
readonly success: true;
|
||||||
@@ -123,7 +109,7 @@ type DirectoryPathConstructionOutcome = {
|
|||||||
|
|
||||||
type DirectoryPathCreationOutcome = {
|
type DirectoryPathCreationOutcome = {
|
||||||
readonly success: false;
|
readonly success: false;
|
||||||
readonly error: DirectoryCreationError;
|
readonly error: CodeRunError;
|
||||||
} | {
|
} | {
|
||||||
readonly success: true;
|
readonly success: true;
|
||||||
readonly error?: undefined;
|
readonly error?: undefined;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { CodeRunError } from '@/application/CodeRunner/CodeRunner';
|
||||||
|
|
||||||
|
export interface ScriptDirectoryProvider {
|
||||||
|
provideScriptDirectory(): Promise<ScriptDirectoryOutcome>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScriptDirectoryOutcome = SuccessfulDirectoryCreation | FailedDirectoryCreation;
|
||||||
|
|
||||||
|
interface ScriptDirectoryCreationStatus {
|
||||||
|
readonly success: boolean;
|
||||||
|
readonly directoryAbsolutePath?: string;
|
||||||
|
readonly error?: CodeRunError;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SuccessfulDirectoryCreation extends ScriptDirectoryCreationStatus {
|
||||||
|
readonly success: true;
|
||||||
|
readonly directoryAbsolutePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FailedDirectoryCreation extends ScriptDirectoryCreationStatus {
|
||||||
|
readonly success: false;
|
||||||
|
readonly error: CodeRunError;
|
||||||
|
}
|
||||||
@@ -1,22 +1,21 @@
|
|||||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
import type { Logger } from '@/application/Common/Log/Logger';
|
import type { Logger } from '@/application/Common/Log/Logger';
|
||||||
import type { CodeRunError, CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
import type { CodeRunError, CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
||||||
import { FileReadbackVerificationErrors, type ReadbackFileWriter } from '@/infrastructure/FileSystem/ReadbackFileWriter/ReadbackFileWriter';
|
import { FileReadbackVerificationErrors, type ReadbackFileWriter } from '@/infrastructure/ReadbackFileWriter/ReadbackFileWriter';
|
||||||
import { NodeReadbackFileWriter } from '@/infrastructure/FileSystem/ReadbackFileWriter/NodeReadbackFileWriter';
|
import { NodeReadbackFileWriter } from '@/infrastructure/ReadbackFileWriter/NodeReadbackFileWriter';
|
||||||
import { NodeElectronFileSystemOperations } from '@/infrastructure/FileSystem/NodeElectronFileSystemOperations';
|
import { NodeElectronSystemOperations } from '../System/NodeElectronSystemOperations';
|
||||||
import { PersistentApplicationDirectoryProvider } from '@/infrastructure/FileSystem/Directory/PersistentApplicationDirectoryProvider';
|
|
||||||
import type { FileSystemOperations } from '@/infrastructure/FileSystem/FileSystemOperations';
|
|
||||||
import type { ApplicationDirectoryProvider } from '@/infrastructure/FileSystem/Directory/ApplicationDirectoryProvider';
|
|
||||||
import { TimestampedFilenameGenerator } from './Filename/TimestampedFilenameGenerator';
|
import { TimestampedFilenameGenerator } from './Filename/TimestampedFilenameGenerator';
|
||||||
|
import { PersistentDirectoryProvider } from './Directory/PersistentDirectoryProvider';
|
||||||
|
import type { SystemOperations } from '../System/SystemOperations';
|
||||||
import type { FilenameGenerator } from './Filename/FilenameGenerator';
|
import type { FilenameGenerator } from './Filename/FilenameGenerator';
|
||||||
import type { ScriptFilenameParts, ScriptFileCreator, ScriptFileCreationOutcome } from './ScriptFileCreator';
|
import type { ScriptFilenameParts, ScriptFileCreator, ScriptFileCreationOutcome } from './ScriptFileCreator';
|
||||||
|
import type { ScriptDirectoryProvider } from './Directory/ScriptDirectoryProvider';
|
||||||
|
|
||||||
export class ScriptFileCreationOrchestrator implements ScriptFileCreator {
|
export class ScriptFileCreationOrchestrator implements ScriptFileCreator {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly fileSystem: FileSystemOperations = NodeElectronFileSystemOperations,
|
private readonly system: SystemOperations = new NodeElectronSystemOperations(),
|
||||||
private readonly filenameGenerator: FilenameGenerator = new TimestampedFilenameGenerator(),
|
private readonly filenameGenerator: FilenameGenerator = new TimestampedFilenameGenerator(),
|
||||||
private readonly directoryProvider: ApplicationDirectoryProvider
|
private readonly directoryProvider: ScriptDirectoryProvider = new PersistentDirectoryProvider(),
|
||||||
= new PersistentApplicationDirectoryProvider(),
|
|
||||||
private readonly fileWriter: ReadbackFileWriter = new NodeReadbackFileWriter(),
|
private readonly fileWriter: ReadbackFileWriter = new NodeReadbackFileWriter(),
|
||||||
private readonly logger: Logger = ElectronLogger,
|
private readonly logger: Logger = ElectronLogger,
|
||||||
) { }
|
) { }
|
||||||
@@ -27,12 +26,9 @@ export class ScriptFileCreationOrchestrator implements ScriptFileCreator {
|
|||||||
): Promise<ScriptFileCreationOutcome> {
|
): Promise<ScriptFileCreationOutcome> {
|
||||||
const {
|
const {
|
||||||
success: isDirectoryCreated, error: directoryCreationError, directoryAbsolutePath,
|
success: isDirectoryCreated, error: directoryCreationError, directoryAbsolutePath,
|
||||||
} = await this.directoryProvider.provideDirectory('script-runs');
|
} = await this.directoryProvider.provideScriptDirectory();
|
||||||
if (!isDirectoryCreated) {
|
if (!isDirectoryCreated) {
|
||||||
return createFailure({
|
return createFailure(directoryCreationError);
|
||||||
type: 'DirectoryCreationError',
|
|
||||||
message: `[${directoryCreationError.type}] ${directoryCreationError.message}`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
success: isFilePathConstructed, error: filePathGenerationError, filePath,
|
success: isFilePathConstructed, error: filePathGenerationError, filePath,
|
||||||
@@ -58,7 +54,7 @@ export class ScriptFileCreationOrchestrator implements ScriptFileCreator {
|
|||||||
): FilePathConstructionOutcome {
|
): FilePathConstructionOutcome {
|
||||||
try {
|
try {
|
||||||
const filename = this.filenameGenerator.generateFilename(scriptFilenameParts);
|
const filename = this.filenameGenerator.generateFilename(scriptFilenameParts);
|
||||||
const filePath = this.fileSystem.combinePaths(directoryPath, filename);
|
const filePath = this.system.location.combinePaths(directoryPath, filename);
|
||||||
return { success: true, filePath };
|
return { success: true, filePath };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { ExecutablePermissionSetter } from './ExecutablePermissionSetter';
|
|||||||
|
|
||||||
export class FileSystemExecutablePermissionSetter implements ExecutablePermissionSetter {
|
export class FileSystemExecutablePermissionSetter implements ExecutablePermissionSetter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly system: SystemOperations = NodeElectronSystemOperations,
|
private readonly system: SystemOperations = new NodeElectronSystemOperations(),
|
||||||
private readonly logger: Logger = ElectronLogger,
|
private readonly logger: Logger = ElectronLogger,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { ShellCommandOutcome, ShellCommandRunner } from './ShellCommandRunn
|
|||||||
export class LoggingNodeShellCommandRunner implements ShellCommandRunner {
|
export class LoggingNodeShellCommandRunner implements ShellCommandRunner {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger = ElectronLogger,
|
private readonly logger: Logger = ElectronLogger,
|
||||||
private readonly systemOps: SystemOperations = NodeElectronSystemOperations,
|
private readonly systemOps: SystemOperations = new NodeElectronSystemOperations(),
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,57 @@
|
|||||||
|
import { join } from 'node:path';
|
||||||
|
import { chmod, mkdir } from 'node:fs/promises';
|
||||||
import { exec } from 'node:child_process';
|
import { exec } from 'node:child_process';
|
||||||
import { NodeElectronFileSystemOperations } from '@/infrastructure/FileSystem/NodeElectronFileSystemOperations';
|
import { app } from 'electron/main';
|
||||||
import type { SystemOperations } from './SystemOperations';
|
import type {
|
||||||
|
CommandOps, FileSystemOps, LocationOps, OperatingSystemOps, SystemOperations,
|
||||||
|
} from './SystemOperations';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thin wrapper for Node and Electron APIs.
|
* Thin wrapper for Node and Electron APIs.
|
||||||
*/
|
*/
|
||||||
export const NodeElectronSystemOperations: SystemOperations = {
|
export class NodeElectronSystemOperations implements SystemOperations {
|
||||||
fileSystem: NodeElectronFileSystemOperations,
|
public readonly operatingSystem: OperatingSystemOps = {
|
||||||
command: {
|
/*
|
||||||
|
This method returns the directory for storing app's configuration files.
|
||||||
|
It appends your app's name to the default appData directory.
|
||||||
|
Conventionally, you should store user data files in this directory.
|
||||||
|
However, avoid writing large files here as some environments might back up this directory
|
||||||
|
to cloud storage, potentially causing issues with file size.
|
||||||
|
|
||||||
|
Based on tests it returns:
|
||||||
|
|
||||||
|
- Windows: `%APPDATA%\privacy.sexy`
|
||||||
|
- Linux: `$HOME/.config/privacy.sexy/runs`
|
||||||
|
- macOS: `$HOME/Library/Application Support/privacy.sexy/runs`
|
||||||
|
|
||||||
|
For more details, refer to the Electron documentation: https://web.archive.org/web/20240104154857/https://www.electronjs.org/docs/latest/api/app#appgetpathname
|
||||||
|
*/
|
||||||
|
getUserDataDirectory: () => {
|
||||||
|
return app.getPath('userData');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public readonly location: LocationOps = {
|
||||||
|
combinePaths: (...pathSegments) => join(...pathSegments),
|
||||||
|
};
|
||||||
|
|
||||||
|
public readonly fileSystem: FileSystemOps = {
|
||||||
|
setFilePermissions: (
|
||||||
|
filePath: string,
|
||||||
|
mode: string | number,
|
||||||
|
) => chmod(filePath, mode),
|
||||||
|
createDirectory: async (
|
||||||
|
directoryPath: string,
|
||||||
|
isRecursive?: boolean,
|
||||||
|
) => {
|
||||||
|
await mkdir(directoryPath, { recursive: isRecursive });
|
||||||
|
// Ignoring the return value from `mkdir`, which is the first directory created
|
||||||
|
// when `recursive` is true, or empty return value.
|
||||||
|
// See https://github.com/nodejs/node/pull/31530
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public readonly command: CommandOps = {
|
||||||
exec,
|
exec,
|
||||||
},
|
};
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
import type { FileSystemOperations } from '@/infrastructure/FileSystem/FileSystemOperations';
|
|
||||||
import type { exec } from 'node:child_process';
|
import type { exec } from 'node:child_process';
|
||||||
|
|
||||||
export interface SystemOperations {
|
export interface SystemOperations {
|
||||||
readonly fileSystem: FileSystemOperations;
|
readonly operatingSystem: OperatingSystemOps;
|
||||||
|
readonly location: LocationOps;
|
||||||
|
readonly fileSystem: FileSystemOps;
|
||||||
readonly command: CommandOps;
|
readonly command: CommandOps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OperatingSystemOps {
|
||||||
|
getUserDataDirectory(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationOps {
|
||||||
|
combinePaths(...pathSegments: string[]): string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CommandOps {
|
export interface CommandOps {
|
||||||
exec(command: string): ReturnType<typeof exec>;
|
exec(command: string): ReturnType<typeof exec>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileSystemOps {
|
||||||
|
setFilePermissions(filePath: string, mode: string | number): Promise<void>;
|
||||||
|
createDirectory(directoryPath: string, isRecursive?: boolean): Promise<void>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
|||||||
import {
|
import {
|
||||||
FileType, type SaveFileError, type SaveFileErrorType, type SaveFileOutcome,
|
FileType, type SaveFileError, type SaveFileErrorType, type SaveFileOutcome,
|
||||||
} from '@/presentation/common/Dialog';
|
} from '@/presentation/common/Dialog';
|
||||||
import { FileReadbackVerificationErrors, type ReadbackFileWriter } from '@/infrastructure/FileSystem/ReadbackFileWriter/ReadbackFileWriter';
|
import { FileReadbackVerificationErrors, type ReadbackFileWriter } from '@/infrastructure/ReadbackFileWriter/ReadbackFileWriter';
|
||||||
import { NodeReadbackFileWriter } from '@/infrastructure/FileSystem/ReadbackFileWriter/NodeReadbackFileWriter';
|
import { NodeReadbackFileWriter } from '@/infrastructure/ReadbackFileWriter/NodeReadbackFileWriter';
|
||||||
import type { ElectronSaveFileDialog } from './ElectronSaveFileDialog';
|
import type { ElectronSaveFileDialog } from './ElectronSaveFileDialog';
|
||||||
|
|
||||||
export class NodeElectronSaveFileDialog implements ElectronSaveFileDialog {
|
export class NodeElectronSaveFileDialog implements ElectronSaveFileDialog {
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
export interface ApplicationDirectoryProvider {
|
|
||||||
provideDirectory(type: DirectoryType): Promise<DirectoryCreationOutcome>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DirectoryType = 'update-installation-files' | 'script-runs';
|
|
||||||
|
|
||||||
export type DirectoryCreationOutcome = SuccessfulDirectoryCreation | FailedDirectoryCreation;
|
|
||||||
|
|
||||||
export type DirectoryCreationErrorType = 'PathConstructionError' | 'DirectoryWriteError' | 'UserDataFolderRetrievalError';
|
|
||||||
|
|
||||||
export interface DirectoryCreationError {
|
|
||||||
readonly type: DirectoryCreationErrorType;
|
|
||||||
readonly message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DirectoryCreationStatus {
|
|
||||||
readonly success: boolean;
|
|
||||||
readonly directoryAbsolutePath?: string;
|
|
||||||
readonly error?: DirectoryCreationError;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SuccessfulDirectoryCreation extends DirectoryCreationStatus {
|
|
||||||
readonly success: true;
|
|
||||||
readonly directoryAbsolutePath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FailedDirectoryCreation extends DirectoryCreationStatus {
|
|
||||||
readonly success: false;
|
|
||||||
readonly error: DirectoryCreationError;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
export interface FileSystemOperations {
|
|
||||||
getUserDataDirectory(): string;
|
|
||||||
|
|
||||||
setFilePermissions(filePath: string, mode: string | number): Promise<void>;
|
|
||||||
createDirectory(directoryPath: string, isRecursive?: boolean): Promise<void>;
|
|
||||||
|
|
||||||
isFileAvailable(filePath: string): Promise<boolean>;
|
|
||||||
isDirectoryAvailable(filePath: string): Promise<boolean>;
|
|
||||||
deletePath(filePath: string): Promise<void>;
|
|
||||||
listDirectoryContents(directoryPath: string): Promise<string[]>;
|
|
||||||
|
|
||||||
combinePaths(...pathSegments: string[]): string;
|
|
||||||
|
|
||||||
readFile: (filePath: string, encoding: NodeJS.BufferEncoding) => Promise<string>;
|
|
||||||
writeFile: (
|
|
||||||
filePath: string,
|
|
||||||
fileContents: string,
|
|
||||||
encoding: NodeJS.BufferEncoding,
|
|
||||||
) => Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { join } from 'node:path';
|
|
||||||
import {
|
|
||||||
chmod, mkdir,
|
|
||||||
readdir, rm, stat,
|
|
||||||
readFile, writeFile,
|
|
||||||
} from 'node:fs/promises';
|
|
||||||
import { app } from 'electron/main';
|
|
||||||
import type { FileSystemOperations } from './FileSystemOperations';
|
|
||||||
import type { Stats } from 'node:original-fs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thin wrapper for Node and Electron APIs.
|
|
||||||
*/
|
|
||||||
export const NodeElectronFileSystemOperations: FileSystemOperations = {
|
|
||||||
combinePaths: (...pathSegments) => join(...pathSegments),
|
|
||||||
setFilePermissions: (
|
|
||||||
filePath: string,
|
|
||||||
mode: string | number,
|
|
||||||
) => chmod(filePath, mode),
|
|
||||||
createDirectory: async (
|
|
||||||
directoryPath: string,
|
|
||||||
isRecursive?: boolean,
|
|
||||||
) => {
|
|
||||||
await mkdir(directoryPath, { recursive: isRecursive });
|
|
||||||
// Ignoring the return value from `mkdir`, which is the first directory created
|
|
||||||
// when `recursive` is true, or empty return value.
|
|
||||||
// See https://github.com/nodejs/node/pull/31530
|
|
||||||
},
|
|
||||||
isFileAvailable: async (path) => isPathAvailable(path, (stats) => stats.isFile()),
|
|
||||||
isDirectoryAvailable: async (path) => isPathAvailable(path, (stats) => stats.isDirectory()),
|
|
||||||
deletePath: (path) => rm(path, { recursive: true, force: true }),
|
|
||||||
listDirectoryContents: (directoryPath) => readdir(directoryPath),
|
|
||||||
getUserDataDirectory: () => {
|
|
||||||
/*
|
|
||||||
This method returns the directory for storing app's configuration files.
|
|
||||||
It appends your app's name to the default appData directory.
|
|
||||||
Conventionally, you should store user data files in this directory.
|
|
||||||
However, avoid writing large files here as some environments might back up this directory
|
|
||||||
to cloud storage, potentially causing issues with file size.
|
|
||||||
|
|
||||||
Based on tests it returns:
|
|
||||||
|
|
||||||
- Windows: `%APPDATA%\privacy.sexy`
|
|
||||||
- Linux: `$HOME/.config/privacy.sexy/runs`
|
|
||||||
- macOS: `$HOME/Library/Application Support/privacy.sexy/runs`
|
|
||||||
|
|
||||||
For more details, refer to the Electron documentation: https://web.archive.org/web/20240104154857/https://www.electronjs.org/docs/latest/api/app#appgetpathname
|
|
||||||
*/
|
|
||||||
return app.getPath('userData');
|
|
||||||
},
|
|
||||||
writeFile,
|
|
||||||
readFile,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function isPathAvailable(
|
|
||||||
path: string,
|
|
||||||
condition: (stats: Stats) => boolean,
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const stats = await stat(path);
|
|
||||||
return condition(stats);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
return false; // path does not exist
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
|
import { writeFile, access, readFile } from 'node:fs/promises';
|
||||||
|
import { constants } from 'node:fs';
|
||||||
import type { Logger } from '@/application/Common/Log/Logger';
|
import type { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { ElectronLogger } from '../../Log/ElectronLogger';
|
import { ElectronLogger } from '../Log/ElectronLogger';
|
||||||
import { NodeElectronFileSystemOperations } from '../NodeElectronFileSystemOperations';
|
|
||||||
import type {
|
import type {
|
||||||
FailedFileWrite, ReadbackFileWriter, FileWriteErrorType,
|
FailedFileWrite, ReadbackFileWriter, FileWriteErrorType,
|
||||||
FileWriteOutcome, SuccessfulFileWrite,
|
FileWriteOutcome, SuccessfulFileWrite,
|
||||||
} from './ReadbackFileWriter';
|
} from './ReadbackFileWriter';
|
||||||
import type { FileSystemOperations } from '../FileSystemOperations';
|
|
||||||
|
|
||||||
const FILE_ENCODING: NodeJS.BufferEncoding = 'utf-8';
|
const FILE_ENCODING: NodeJS.BufferEncoding = 'utf-8';
|
||||||
|
|
||||||
export class NodeReadbackFileWriter implements ReadbackFileWriter {
|
export class NodeReadbackFileWriter implements ReadbackFileWriter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger = ElectronLogger,
|
private readonly logger: Logger = ElectronLogger,
|
||||||
private readonly fileSystem: FileSystemOperations = NodeElectronFileSystemOperations,
|
private readonly fileSystem: FileReadWriteOperations = {
|
||||||
|
writeFile,
|
||||||
|
readFile: (path, encoding) => readFile(path, encoding),
|
||||||
|
access,
|
||||||
|
},
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public async writeAndVerifyFile(
|
public async writeAndVerifyFile(
|
||||||
@@ -51,9 +55,7 @@ export class NodeReadbackFileWriter implements ReadbackFileWriter {
|
|||||||
filePath: string,
|
filePath: string,
|
||||||
): Promise<FileWriteOutcome> {
|
): Promise<FileWriteOutcome> {
|
||||||
try {
|
try {
|
||||||
if (!(await this.fileSystem.isFileAvailable(filePath))) {
|
await this.fileSystem.access(filePath, constants.F_OK);
|
||||||
return this.reportFailure('FileExistenceVerificationFailed', 'File does not exist.');
|
|
||||||
}
|
|
||||||
return this.reportSuccess('Verified file existence without reading.');
|
return this.reportSuccess('Verified file existence without reading.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return this.reportFailure('FileExistenceVerificationFailed', error);
|
return this.reportFailure('FileExistenceVerificationFailed', error);
|
||||||
@@ -105,3 +107,9 @@ export class NodeReadbackFileWriter implements ReadbackFileWriter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileReadWriteOperations {
|
||||||
|
readonly writeFile: typeof writeFile;
|
||||||
|
readonly access: typeof access;
|
||||||
|
readFile: (filePath: string, encoding: NodeJS.BufferEncoding) => Promise<string>;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Repository } from '../../application/Repository/Repository';
|
import type { Repository } from '../../application/Repository/Repository';
|
||||||
import type { RepositoryEntity, RepositoryEntityId } from '../../application/Repository/RepositoryEntity';
|
import type { RepositoryEntity } from '../../application/Repository/RepositoryEntity';
|
||||||
|
|
||||||
export class InMemoryRepository<TEntity extends RepositoryEntity>
|
export class InMemoryRepository<TEntity extends RepositoryEntity>
|
||||||
implements Repository<TEntity> {
|
implements Repository<TEntity> {
|
||||||
@@ -20,7 +20,7 @@ implements Repository<TEntity> {
|
|||||||
return predicate ? this.items.filter(predicate) : this.items;
|
return predicate ? this.items.filter(predicate) : this.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getById(id: RepositoryEntityId): TEntity {
|
public getById(id: string): TEntity {
|
||||||
const items = this.getItems((entity) => entity.id === id);
|
const items = this.getItems((entity) => entity.id === id);
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
throw new Error(`missing item: ${id}`);
|
throw new Error(`missing item: ${id}`);
|
||||||
@@ -42,7 +42,7 @@ implements Repository<TEntity> {
|
|||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeItem(id: RepositoryEntityId): void {
|
public removeItem(id: string): void {
|
||||||
const index = this.items.findIndex((item) => item.id === id);
|
const index = this.items.findIndex((item) => item.id === id);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
throw new Error(`Cannot remove (id: ${id}) as it does not exist`);
|
throw new Error(`Cannot remove (id: ${id}) as it does not exist`);
|
||||||
@@ -50,7 +50,7 @@ implements Repository<TEntity> {
|
|||||||
this.items.splice(index, 1);
|
this.items.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public exists(id: RepositoryEntityId): boolean {
|
public exists(id: string): boolean {
|
||||||
const index = this.items.findIndex((item) => item.id === id);
|
const index = this.items.findIndex((item) => item.id === id);
|
||||||
return index !== -1;
|
return index !== -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import type { SanityValidator } from './SanityValidator';
|
import type { ISanityValidator } from './ISanityValidator';
|
||||||
import type { SanityCheckOptions } from './SanityCheckOptions';
|
import type { ISanityCheckOptions } from './ISanityCheckOptions';
|
||||||
|
|
||||||
export type FactoryFunction<T> = () => T;
|
export type FactoryFunction<T> = () => T;
|
||||||
|
|
||||||
export abstract class FactoryValidator<T> implements SanityValidator {
|
export abstract class FactoryValidator<T> implements ISanityValidator {
|
||||||
private readonly factory: FactoryFunction<T>;
|
private readonly factory: FactoryFunction<T>;
|
||||||
|
|
||||||
protected constructor(factory: FactoryFunction<T>) {
|
protected constructor(factory: FactoryFunction<T>) {
|
||||||
this.factory = factory;
|
this.factory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract shouldValidate(options: SanityCheckOptions): boolean;
|
public abstract shouldValidate(options: ISanityCheckOptions): boolean;
|
||||||
|
|
||||||
public abstract name: string;
|
public abstract name: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface SanityCheckOptions {
|
export interface ISanityCheckOptions {
|
||||||
readonly validateEnvironmentVariables: boolean;
|
readonly validateEnvironmentVariables: boolean;
|
||||||
readonly validateWindowVariables: boolean;
|
readonly validateWindowVariables: boolean;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ISanityCheckOptions } from './ISanityCheckOptions';
|
||||||
|
|
||||||
|
export interface ISanityValidator {
|
||||||
|
readonly name: string;
|
||||||
|
shouldValidate(options: ISanityCheckOptions): boolean;
|
||||||
|
collectErrors(): Iterable<string>;
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { SanityCheckOptions } from './SanityCheckOptions';
|
|
||||||
|
|
||||||
export interface SanityValidator {
|
|
||||||
readonly name: string;
|
|
||||||
shouldValidate(options: SanityCheckOptions): boolean;
|
|
||||||
collectErrors(): Iterable<string>;
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,16 @@
|
|||||||
import { EnvironmentVariablesValidator } from './Validators/EnvironmentVariablesValidator';
|
import { EnvironmentVariablesValidator } from './Validators/EnvironmentVariablesValidator';
|
||||||
import type { SanityCheckOptions } from './Common/SanityCheckOptions';
|
import type { ISanityCheckOptions } from './Common/ISanityCheckOptions';
|
||||||
import type { SanityValidator } from './Common/SanityValidator';
|
import type { ISanityValidator } from './Common/ISanityValidator';
|
||||||
|
|
||||||
const DefaultSanityValidators: SanityValidator[] = [
|
const DefaultSanityValidators: ISanityValidator[] = [
|
||||||
new EnvironmentVariablesValidator(),
|
new EnvironmentVariablesValidator(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface RuntimeSanityValidator {
|
|
||||||
(
|
|
||||||
options: SanityCheckOptions,
|
|
||||||
validators?: readonly SanityValidator[],
|
|
||||||
): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Helps to fail-fast on errors */
|
/* Helps to fail-fast on errors */
|
||||||
export const validateRuntimeSanity: RuntimeSanityValidator = (
|
export function validateRuntimeSanity(
|
||||||
options: SanityCheckOptions,
|
options: ISanityCheckOptions,
|
||||||
validators: readonly SanityValidator[] = DefaultSanityValidators,
|
validators: readonly ISanityValidator[] = DefaultSanityValidators,
|
||||||
) => {
|
): void {
|
||||||
if (!validators.length) {
|
if (!validators.length) {
|
||||||
throw new Error('missing validators');
|
throw new Error('missing validators');
|
||||||
}
|
}
|
||||||
@@ -33,9 +26,9 @@ export const validateRuntimeSanity: RuntimeSanityValidator = (
|
|||||||
if (errorMessages.length > 0) {
|
if (errorMessages.length > 0) {
|
||||||
throw new Error(`Sanity check failed.\n${errorMessages.join('\n---\n')}`);
|
throw new Error(`Sanity check failed.\n${errorMessages.join('\n---\n')}`);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
function getErrorMessage(validator: SanityValidator): string | undefined {
|
function getErrorMessage(validator: ISanityValidator): string | undefined {
|
||||||
const errorMessages = [...validator.collectErrors()];
|
const errorMessages = [...validator.collectErrors()];
|
||||||
if (!errorMessages.length) {
|
if (!errorMessages.length) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
import type { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
import { FactoryValidator, type FactoryFunction } from '../Common/FactoryValidator';
|
import { FactoryValidator, type FactoryFunction } from '../Common/FactoryValidator';
|
||||||
import type { SanityCheckOptions } from '../Common/SanityCheckOptions';
|
import type { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
|
||||||
|
|
||||||
export class EnvironmentVariablesValidator extends FactoryValidator<IEnvironmentVariables> {
|
export class EnvironmentVariablesValidator extends FactoryValidator<IEnvironmentVariables> {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -14,7 +14,7 @@ export class EnvironmentVariablesValidator extends FactoryValidator<IEnvironment
|
|||||||
|
|
||||||
public override name = 'environment variables';
|
public override name = 'environment variables';
|
||||||
|
|
||||||
public override shouldValidate(options: SanityCheckOptions): boolean {
|
public override shouldValidate(options: ISanityCheckOptions): boolean {
|
||||||
return options.validateEnvironmentVariables;
|
return options.validateEnvironmentVariables;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
import type { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||||
import { FactoryValidator, type FactoryFunction } from '../Common/FactoryValidator';
|
import { FactoryValidator, type FactoryFunction } from '../Common/FactoryValidator';
|
||||||
import type { SanityCheckOptions } from '../Common/SanityCheckOptions';
|
import type { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
|
||||||
|
|
||||||
export class WindowVariablesValidator extends FactoryValidator<WindowVariables> {
|
export class WindowVariablesValidator extends FactoryValidator<WindowVariables> {
|
||||||
constructor(factory: FactoryFunction<WindowVariables> = () => window) {
|
constructor(factory: FactoryFunction<WindowVariables> = () => window) {
|
||||||
@@ -9,7 +9,7 @@ export class WindowVariablesValidator extends FactoryValidator<WindowVariables>
|
|||||||
|
|
||||||
public override name = 'window variables';
|
public override name = 'window variables';
|
||||||
|
|
||||||
public override shouldValidate(options: SanityCheckOptions): boolean {
|
public override shouldValidate(options: ISanityCheckOptions): boolean {
|
||||||
return options.validateWindowVariables;
|
return options.validateWindowVariables;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import type { ScriptDiagnosticData, ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
import type { ScriptDiagnosticData, ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||||
import type { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
import type { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||||
import type { ApplicationDirectoryProvider } from '@/infrastructure/FileSystem/Directory/ApplicationDirectoryProvider';
|
import { PersistentDirectoryProvider } from '@/infrastructure/CodeRunner/Creation/Directory/PersistentDirectoryProvider';
|
||||||
import { PersistentApplicationDirectoryProvider } from '@/infrastructure/FileSystem/Directory/PersistentApplicationDirectoryProvider';
|
import type { ScriptDirectoryProvider } from '../CodeRunner/Creation/Directory/ScriptDirectoryProvider';
|
||||||
|
|
||||||
export class ScriptEnvironmentDiagnosticsCollector implements ScriptDiagnosticsCollector {
|
export class ScriptEnvironmentDiagnosticsCollector implements ScriptDiagnosticsCollector {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly directoryProvider: ApplicationDirectoryProvider
|
private readonly directoryProvider: ScriptDirectoryProvider = new PersistentDirectoryProvider(),
|
||||||
= new PersistentApplicationDirectoryProvider(),
|
|
||||||
private readonly environment: RuntimeEnvironment = CurrentEnvironment,
|
private readonly environment: RuntimeEnvironment = CurrentEnvironment,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public async collectDiagnosticInformation(): Promise<ScriptDiagnosticData> {
|
public async collectDiagnosticInformation(): Promise<ScriptDiagnosticData> {
|
||||||
const {
|
const { directoryAbsolutePath } = await this.directoryProvider.provideScriptDirectory();
|
||||||
directoryAbsolutePath: scriptsDirectory,
|
|
||||||
} = await this.directoryProvider.provideDirectory('script-runs');
|
|
||||||
return {
|
return {
|
||||||
scriptsDirectoryAbsolutePath: scriptsDirectory,
|
scriptsDirectoryAbsolutePath: directoryAbsolutePath,
|
||||||
currentOperatingSystem: this.environment.os,
|
currentOperatingSystem: this.environment.os,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,19 @@
|
|||||||
- Darker than default: Shades are named as `color-{name}-dark`, `color-{name}-darker`, or `color-{name}-darkest`.
|
- Darker than default: Shades are named as `color-{name}-dark`, `color-{name}-darker`, or `color-{name}-darkest`.
|
||||||
- Lighter than default: Tints are named as `color-{name}-light`, `color-{name}-lighter`, or `color-{name}-lightest`.
|
- Lighter than default: Tints are named as `color-{name}-light`, `color-{name}-lighter`, or `color-{name}-lightest`.
|
||||||
*/
|
*/
|
||||||
@use 'sass:color';
|
|
||||||
|
|
||||||
// --- Primary | The color displayed most frequently across screens and components
|
// --- Primary | The color displayed most frequently across screens and components
|
||||||
$color-primary : #3a65ab;
|
$color-primary : #3a65ab;
|
||||||
$color-primary-light : color.adjust($color-primary, $lightness: 30%);
|
$color-primary-light : lighten($color-primary, 30%);
|
||||||
$color-primary-dark : color.adjust($color-primary, $lightness: -18%);
|
$color-primary-dark : darken($color-primary, 18%);
|
||||||
$color-primary-darker : color.adjust($color-primary, $lightness: -32%);
|
$color-primary-darker : darken($color-primary, 32%);
|
||||||
$color-primary-darkest : color.adjust($color-primary, $lightness: -44%);
|
$color-primary-darkest : darken($color-primary, 44%);
|
||||||
// Text/iconography color that is usable on top of primary color
|
// Text/iconography color that is usable on top of primary color
|
||||||
$color-on-primary : #e4f1fe;
|
$color-on-primary : #e4f1fe;
|
||||||
|
|
||||||
// --- Secondary | Accent color, should be applied sparingly to accent select parts of UI
|
// --- Secondary | Accent color, should be applied sparingly to accent select parts of UI
|
||||||
$color-secondary : #00D1AD;
|
$color-secondary : #00D1AD;
|
||||||
$color-secondary-light : color.adjust($color-secondary, $lightness: 48%);
|
$color-secondary-light : lighten($color-secondary, 48%);
|
||||||
// Text/iconography color that is usable on top of secondary color
|
// Text/iconography color that is usable on top of secondary color
|
||||||
$color-on-secondary : #005051;
|
$color-on-secondary : #005051;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user