Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fd193e676 | ||
|
|
52a4730073 | ||
|
|
bc4879cfe9 | ||
|
|
dd71536316 | ||
|
|
a3343205b1 | ||
|
|
1d7cafc831 | ||
|
|
c75df1c8c1 | ||
|
|
ab25e0a066 | ||
|
|
813d820b85 | ||
|
|
66a56888a4 | ||
|
|
4ef16cea56 | ||
|
|
8c17396285 | ||
|
|
694bf1a74d | ||
|
|
0fc2ffc1ea | ||
|
|
d19dde603d | ||
|
|
23bac0fc76 | ||
|
|
e18907ca91 | ||
|
|
4e21f05031 | ||
|
|
8b224eefe7 | ||
|
|
f261ab4cd9 | ||
|
|
f584fabb50 | ||
|
|
2eed6f4afb | ||
|
|
1c9dc93246 | ||
|
|
cb144ae472 | ||
|
|
f3571abeaf | ||
|
|
b87b7aac7d | ||
|
|
ae172000a6 | ||
|
|
ffd647d152 | ||
|
|
4142d084f6 | ||
|
|
b7a20d9d41 | ||
|
|
b68711ef88 | ||
|
|
7b546c567c | ||
|
|
49f22f048f | ||
|
|
4472c2852e | ||
|
|
5d940b57ef | ||
|
|
bc7e1faa1c | ||
|
|
557cea3f48 | ||
|
|
4fb6302c67 | ||
|
|
59decd17e2 | ||
|
|
52fadcd617 | ||
|
|
8a5592f92b | ||
|
|
79183d6417 | ||
|
|
89243371fa | ||
|
|
4a9b430702 | ||
|
|
ac176935f5 | ||
|
|
abec9def07 | ||
|
|
b71ad797a3 | ||
|
|
ec34ac1124 | ||
|
|
840adf9429 | ||
|
|
5eff3a0488 | ||
|
|
5abf8ff216 | ||
|
|
e7218850ba | ||
|
|
adc2089887 | ||
|
|
4ac1425f76 | ||
|
|
a721e82a4f | ||
|
|
98845e6cae | ||
|
|
19645248ab | ||
|
|
255c51c8a0 | ||
|
|
bbf3490e9c |
32
.github/actions/force-ipv4/README.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# force-ipv4
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This GitHub action enforces IPv4 for all outgoing network requests. It addresses connectivity issues encountered in GitHub runners, where IPv6 requests may lead to timeouts due to the lack of IPv6 support [1] [2].
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Some applications attempt network connections over IPv6.
|
||||||
|
Such as requests made by Node's `fetch` API causes `UND_ERR_CONNECT_TIMEOUT` [3] [4] and similar issues [5].
|
||||||
|
This happens when the software cannot handle this such as by using Happy Eyeballs [6] [7].
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To use this action in your GitHub workflow, add the following step before any job that requires network access:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Enforce IPv4 Connectivity
|
||||||
|
uses: ./.github/actions/force-ipv4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
This action is a workaround addressing specific IPv6-related connectivity issues on GitHub runners and may not be necessary if GitHub's infrastructure evolves to fully support IPv6 in the future.
|
||||||
|
|
||||||
|
[1]: https://archive.ph/2024.03.28-185829/https://github.com/actions/runner/issues/3138 "Actions Runner fails on IPv6 only host · Issue #3138 · actions/runner · GitHub | github.com"
|
||||||
|
[2]: https://archive.ph/2024.03.28-185838/https://github.com/actions/runner-images/issues/668 "IPv6 on GitHub-hosted runners · Issue #668 · actions/runner-images · GitHub | github.com"
|
||||||
|
[3]: https://archive.ph/2024.03.28-185847/https://github.com/actions/runner/issues/3213 "GitHub runner cannot send `fetch` with `node`, failing with IPv6 DNS error `UND_ERR_CONNECT_TIMEOUT` · Issue #3213 · actions/runner · GitHub | github.com"
|
||||||
|
[4]: https://archive.ph/2024.03.28-185853/https://github.com/actions/runner-images/issues/9540 "Cannot send outbound requests using node fetch, failing with IPv6 DNS error UND_ERR_CONNECT_TIMEOUT · Issue #9540 · actions/runner-images · GitHub | github.com"
|
||||||
|
[5]: https://archive.today/2024.03.30-113315/https://github.com/nodejs/node/issues/40537 "\"localhost\" favours IPv6 in node v17, used to favour IPv4 · Issue #40537 · nodejs/node · GitHub"
|
||||||
|
[6]: https://archive.ph/2024.03.28-185900/https://github.com/nodejs/node/issues/41625 "Happy Eyeballs support (address IPv6 issues in Node 17) · Issue #41625 · nodejs/node · GitHub | github.com"
|
||||||
|
[7]: https://archive.ph/2024.03.28-185910/https://github.com/nodejs/undici/issues/1531 "fetch times out in under 5 seconds · Issue #1531 · nodejs/undici · GitHub | github.com"
|
||||||
12
.github/actions/force-ipv4/action.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
inputs:
|
||||||
|
project-root:
|
||||||
|
required: false
|
||||||
|
default: '.'
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Run prefer IPv4 script
|
||||||
|
shell: bash
|
||||||
|
run: ./.github/actions/force-ipv4/force-ipv4.sh
|
||||||
|
working-directory: ${{ inputs.project-root }}
|
||||||
80
.github/actions/force-ipv4/force-ipv4.sh
vendored
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if is_linux; then
|
||||||
|
echo 'Configuring Linux...'
|
||||||
|
|
||||||
|
configure_warp_with_doh_and_ipv6_exclusion_on_linux # [WORKS] Resolves the issue when run independently on GitHub runners lacking IPv6 support.
|
||||||
|
prefer_ipv4_on_linux # [DOES NOT WORK] It does not resolve the issue when run independently on GitHub runners without IPv6 support.
|
||||||
|
|
||||||
|
# Considered alternatives:
|
||||||
|
# - `sysctl` commands, and direct changes to `/proc/sys/net/` and `/etc/sysctl.conf` led to silent
|
||||||
|
# Node 18 exits (code: 13) when using `fetch`.
|
||||||
|
elif is_macos; then
|
||||||
|
echo 'Configuring macOS...'
|
||||||
|
|
||||||
|
configure_warp_with_doh_and_ipv6_exclusion_on_macos # [WORKS] Resolves the issue when run independently on GitHub runners lacking IPv6 support.
|
||||||
|
disable_ipv6_on_macos # [WORKS INCONSISTENTLY] Resolves the issue inconsistently when run independently on GitHub runners without IPv6 support.
|
||||||
|
fi
|
||||||
|
echo "IPv4: $(curl --ipv4 --silent --max-time 15 --retry 3 --user-agent Mozilla https://api.ip.sb/geoip)"
|
||||||
|
echo "IPv6: $(curl --ipv6 --silent --max-time 15 --retry 3 --user-agent Mozilla https://api.ip.sb/geoip)"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_linux() {
|
||||||
|
[[ "$(uname -s)" == "Linux" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
is_macos() {
|
||||||
|
[[ "$(uname -s)" == "Darwin" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_warp_with_doh_and_ipv6_exclusion_on_linux() {
|
||||||
|
install_warp_on_debian
|
||||||
|
configure_warp_doh_and_exclude_ipv6
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_warp_with_doh_and_ipv6_exclusion_on_macos() {
|
||||||
|
brew install cloudflare-warp
|
||||||
|
configure_warp_doh_and_exclude_ipv6
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_warp_doh_and_exclude_ipv6() {
|
||||||
|
echo 'Beginning configuration of the Cloudflare WARP client with DNS-over-HTTPS and IPv6 exclusion...'
|
||||||
|
echo 'Initiating client registration with Cloudflare...'
|
||||||
|
warp-cli --accept-tos registration new
|
||||||
|
echo 'Configuring WARP to operate in DNS-over-HTTPS mode (warp+doh)...'
|
||||||
|
warp-cli --accept-tos mode warp+doh
|
||||||
|
echo 'Excluding IPv6 traffic from WARP by configuring it as a split tunnel...'
|
||||||
|
warp-cli --accept-tos add-excluded-route '::/0' # Exclude IPv6, forcing IPv4 resolution
|
||||||
|
# `tunnel ip add` does not work with IP ranges, see https://community.cloudflare.com/t/cant-cidr-for-split-tunnling/630834
|
||||||
|
echo 'Establishing WARP connection...'
|
||||||
|
warp-cli --accept-tos connect
|
||||||
|
}
|
||||||
|
|
||||||
|
install_warp_on_debian() {
|
||||||
|
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | sudo gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
|
||||||
|
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflare-client.list
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y cloudflare-warp
|
||||||
|
}
|
||||||
|
|
||||||
|
disable_ipv6_on_macos() {
|
||||||
|
networksetup -listallnetworkservices \
|
||||||
|
| tail -n +2 \
|
||||||
|
| while IFS= read -r interface; do
|
||||||
|
echo "Disabling IPv6 on: $interface..."
|
||||||
|
networksetup -setv6off "$interface"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
prefer_ipv4_on_linux() {
|
||||||
|
local -r gai_config_file_path='/etc/gai.conf'
|
||||||
|
if [ ! -f "$gai_config_file_path" ]; then
|
||||||
|
echo "Creating $gai_config_file_path since it doesn't exist..."
|
||||||
|
touch "$gai_config_file_path"
|
||||||
|
fi
|
||||||
|
echo "precedence ::ffff:0:0/96 100" | sudo tee -a "$gai_config_file_path" > /dev/null
|
||||||
|
echo "Configuration complete."
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
3
.github/actions/setup-node/action.yml
vendored
@@ -5,4 +5,5 @@ runs:
|
|||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
|
# check-latest: true # Newest versions can potentially have undiscovered bugs or regressions
|
||||||
|
|||||||
8
.github/workflows/checks.build.yaml
vendored
@@ -95,6 +95,12 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Run Docker image on port 8080
|
name: Run Docker image on port 8080
|
||||||
run: docker run -d -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest
|
run: docker run -d -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest
|
||||||
|
-
|
||||||
|
name: Enforce IPv4 Connectivity # Used due to GitHub runners' lack of IPv6 support, preventing request timeouts.
|
||||||
|
uses: ./.github/actions/force-ipv4
|
||||||
-
|
-
|
||||||
name: Check server is up and returns HTTP 200
|
name: Check server is up and returns HTTP 200
|
||||||
run: node ./scripts/verify-web-server-status.js --url http://localhost:8080
|
run: >-
|
||||||
|
node ./scripts/verify-web-server-status.js \
|
||||||
|
--url http://localhost:8080 \
|
||||||
|
--max-retries ${{ matrix.os == 'macos' && '90' || '30' }}
|
||||||
|
|||||||
8
.github/workflows/checks.external-urls.yaml
vendored
@@ -1,6 +1,7 @@
|
|||||||
name: checks.external-urls
|
name: checks.external-urls
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||||
|
|
||||||
@@ -17,6 +18,13 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
uses: ./.github/actions/npm-install-dependencies
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
|
-
|
||||||
|
name: Enforce IPv4 Connectivity # Used due to GitHub runners' lack of IPv6 support, preventing request timeouts.
|
||||||
|
uses: ./.github/actions/force-ipv4
|
||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
run: npm run check:external-urls
|
run: npm run check:external-urls
|
||||||
|
env:
|
||||||
|
RANDOMIZED_URL_CHECK_LIMIT: "${{ github.event_name == 'push' && '100' || '3000' }}"
|
||||||
|
# - Scheduled checks has high limit for thorough testing.
|
||||||
|
# - For push events, triggered by code changes, the amount of URLs are limited to provide quick feedback.
|
||||||
|
|||||||
48
.github/workflows/checks.quality.yaml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: quality-checks
|
name: checks.quality
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
@@ -28,3 +28,49 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Lint
|
name: Lint
|
||||||
run: ${{ matrix.lint-command }}
|
run: ${{ matrix.lint-command }}
|
||||||
|
|
||||||
|
todo-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Scan latest commit for TODO comments
|
||||||
|
shell: bash
|
||||||
|
run: |-
|
||||||
|
readonly todo_comment_search_pattern='TODO'':' # Define search pattern in parts to prevent IDE from flagging this script line as a TODO item
|
||||||
|
if git grep "$todo_comment_search_pattern" HEAD; then
|
||||||
|
echo 'TODO comments found in the latest commit.'
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo 'No TODO comments found in the latest commit.'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
pylint:
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pylint
|
||||||
|
-
|
||||||
|
name: Analyzing the code with pylint
|
||||||
|
run: npm run lint:pylint
|
||||||
|
|||||||
32
.github/workflows/checks.scripts.yaml
vendored
@@ -15,6 +15,10 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Install ImageMagick on macOS
|
||||||
|
if: matrix.os == 'macos'
|
||||||
|
run: brew install imagemagick
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
@@ -53,3 +57,31 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Run install-deps
|
name: Run install-deps
|
||||||
run: ${{ matrix.install-command }}
|
run: ${{ matrix.install-command }}
|
||||||
|
|
||||||
|
configure-vscode:
|
||||||
|
runs-on: ${{ matrix.os.name }}-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- name: macos
|
||||||
|
install-vscode-command: brew install --cask visual-studio-code
|
||||||
|
- name: ubuntu
|
||||||
|
install-vscode-command: sudo snap install code --classic
|
||||||
|
- name: windows
|
||||||
|
install-vscode-command: choco install vscode
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
-
|
||||||
|
name: Install VSCode
|
||||||
|
run: ${{ matrix.os.install-vscode-command }}
|
||||||
|
-
|
||||||
|
name: Configure VSCode
|
||||||
|
run: python3 ./scripts/configure_vscode.py
|
||||||
|
|||||||
1
.github/workflows/release.desktop.yaml
vendored
@@ -20,6 +20,7 @@ jobs:
|
|||||||
fetch-depth: 0 # fetch all history
|
fetch-depth: 0 # fetch all history
|
||||||
-
|
-
|
||||||
name: Checkout to bump commit
|
name: Checkout to bump commit
|
||||||
|
shell: bash
|
||||||
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
|
|||||||
73
CHANGELOG.md
@@ -1,5 +1,78 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.13.2 (2024-04-15)
|
||||||
|
|
||||||
|
* Update documentation for `logo-update.js` script | [4a9b430](https://github.com/undergroundwires/privacy.sexy/commit/4a9b430702bc6082426b50ecc3a06362b5720796)
|
||||||
|
* win: improve and document removing Phone apps #279 | [8924337](https://github.com/undergroundwires/privacy.sexy/commit/89243371faa5d6aef5fce52b0d54a442143cdd39)
|
||||||
|
* Fix bottom gap in card expansion panel | [79183d6](https://github.com/undergroundwires/privacy.sexy/commit/79183d64173e588d88bf074d5b50a52a71c2d885)
|
||||||
|
* ci/cd: Fix macOS Docker build reliability issues | [8a5592f](https://github.com/undergroundwires/privacy.sexy/commit/8a5592f92be4366a806afc9eee9135696a1dd993)
|
||||||
|
* ci/cd: fix IPv6 timeouts with `force-ipv4` action | [52fadcd](https://github.com/undergroundwires/privacy.sexy/commit/52fadcd6177ed06216be9c67dad57192ae02a4f9)
|
||||||
|
* ci/cd: bump Node.js environment to 20.x | [59decd1](https://github.com/undergroundwires/privacy.sexy/commit/59decd17e273bada1493eaa855c43cbabf90308f)
|
||||||
|
* ci/cd: trigger URL checks more, and limit amount | [4fb6302](https://github.com/undergroundwires/privacy.sexy/commit/4fb6302c67f2a3fedff419e8c22872593cf800ef)
|
||||||
|
* Fix overflow in tree node content on small screens | [557cea3](https://github.com/undergroundwires/privacy.sexy/commit/557cea3f4866dc33236874f5fe4d2d69ee963dae)
|
||||||
|
* Fix horizontal layout shift after script selection | [bc7e1fa](https://github.com/undergroundwires/privacy.sexy/commit/bc7e1faa1c3f2b61bf2046fdd6d6a4141b484662)
|
||||||
|
* Fix card header expansion glitch on card collapse | [5d940b5](https://github.com/undergroundwires/privacy.sexy/commit/5d940b57ef2a4c219932cd15201401f8550cfb41)
|
||||||
|
* Ignore `ResizeObserver` errors in Cypress tests | [4472c28](https://github.com/undergroundwires/privacy.sexy/commit/4472c2852e4b87083bda7979471ab9f377d17a01)
|
||||||
|
* win: improve and document secret key scripts | [49f22f0](https://github.com/undergroundwires/privacy.sexy/commit/49f22f048f39e7388633c488b5fe59101b831984)
|
||||||
|
* Fix card arrow not being animated in sync | [7b546c5](https://github.com/undergroundwires/privacy.sexy/commit/7b546c567c4683a37fe94595362f4c2bf92ffd59)
|
||||||
|
* win: improve Windows feature disablement scripts | [b68711e](https://github.com/undergroundwires/privacy.sexy/commit/b68711ef88982c0ee2b1d41b4452e899821adc64)
|
||||||
|
* Fix top script menu overflow on small screens | [b7a20d9](https://github.com/undergroundwires/privacy.sexy/commit/b7a20d9d41ea8bcefdd553b87641f3c22b4cde97)
|
||||||
|
* win: fix Visual Studio remote analysis script #327 | [4142d08](https://github.com/undergroundwires/privacy.sexy/commit/4142d084f64a3b540487ff68b28032977d12006d)
|
||||||
|
* win: improve firewall docs /w `winget` impact #142 | [ffd647d](https://github.com/undergroundwires/privacy.sexy/commit/ffd647d1529375474b81900cc7bee4c32fbf861f)
|
||||||
|
* Centralize and use global spacing variables | [ae17200](https://github.com/undergroundwires/privacy.sexy/commit/ae172000a64416e5a3e2b2e32b7846f039f445f0)
|
||||||
|
* win: improve service revert and docs | [b87b7aa](https://github.com/undergroundwires/privacy.sexy/commit/b87b7aac7d118a23a0d1bfb881e385347de4adb7)
|
||||||
|
* Bump dependencies to latest, hold ESLint | [f3571ab](https://github.com/undergroundwires/privacy.sexy/commit/f3571abeafdbe1e6d152958fab26de91a9c08bc3)
|
||||||
|
* Fix inability to tap outside modal on mobile | [cb144ae](https://github.com/undergroundwires/privacy.sexy/commit/cb144ae47273deeb7058d4b1380e480ebccdaf81)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.1...0.13.2)
|
||||||
|
|
||||||
|
## 0.13.1 (2024-03-22)
|
||||||
|
|
||||||
|
* ci/cd: Fix cross-platform git command compability | [255c51c](https://github.com/undergroundwires/privacy.sexy/commit/255c51c8a0524d3ea8a3b16ffc1b178650525010)
|
||||||
|
* Fix tooltip falling behind elements on fade out | [1964524](https://github.com/undergroundwires/privacy.sexy/commit/19645248ab7bc78dc872fa176c1a3650d7d6d644)
|
||||||
|
* Improve VSCode detection in `configure_vscode.py` | [98845e6](https://github.com/undergroundwires/privacy.sexy/commit/98845e6caee168db131aaf0736533e450827a52c)
|
||||||
|
* Bump TypeScript to 5.3 with `verbatimModuleSyntax` | [a721e82](https://github.com/undergroundwires/privacy.sexy/commit/a721e82a4fb603c0732ccfdffc87396c2a01363e)
|
||||||
|
* Migrate to Vite 5 and adjust configurations | [4ac1425](https://github.com/undergroundwires/privacy.sexy/commit/4ac1425f76079352268c488f3ff607d1fdc1beb2)
|
||||||
|
* win: improve and unify service start/stop logic | [adc2089](https://github.com/undergroundwires/privacy.sexy/commit/adc20898873d50a8873ffc74c48257e69a45d367)
|
||||||
|
* Upgrade vitest to v1 and fix test definitions | [e721885](https://github.com/undergroundwires/privacy.sexy/commit/e7218850ba62a7bebaf4768b13e46cba0dedd906)
|
||||||
|
* Improve URL checks to reduce false-negatives | [5abf8ff](https://github.com/undergroundwires/privacy.sexy/commit/5abf8ff216a1da737fd489864eeee880f78d6601)
|
||||||
|
* win: improve OneDrive data deletion safety | [5eff3a0](https://github.com/undergroundwires/privacy.sexy/commit/5eff3a04886d0d23a6e4c13a0178bb247105c5cb)
|
||||||
|
* Bump Electron to latest and use native ESM | [840adf9](https://github.com/undergroundwires/privacy.sexy/commit/840adf9429ed47f9e88c05e90f1d3ab930c2dfc4)
|
||||||
|
* Fix tooltip styling inconsistency | [ec34ac1](https://github.com/undergroundwires/privacy.sexy/commit/ec34ac1124e8b8ae53bf31a4dbdc88bb078b3d4e)
|
||||||
|
* win: fix VSCode manual update switch script #312 | [b71ad79](https://github.com/undergroundwires/privacy.sexy/commit/b71ad797a3af0db45143249903cb5e178692de7c)
|
||||||
|
* mac, linux, win: fix dead URLs and improve docs | [abec9de](https://github.com/undergroundwires/privacy.sexy/commit/abec9def075d82fdaee9663ef8fe1a488911f45b)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.0...0.13.1)
|
||||||
|
|
||||||
|
## 0.13.0 (2024-02-11)
|
||||||
|
|
||||||
|
* win: add disabling clipboard features #251, #247 | [c6ebba8](https://github.com/undergroundwires/privacy.sexy/commit/c6ebba85fb1b362be0d81d3078f19db71e0528b2)
|
||||||
|
* win: improve search privacy scripts #117 | [541f9aa](https://github.com/undergroundwires/privacy.sexy/commit/541f9aa5ee1b5f4885063b65beaf6cd873f0d786)
|
||||||
|
* win: add disabling Windows Copilot #263, #266 | [cd42550](https://github.com/undergroundwires/privacy.sexy/commit/cd425502ae882bba9642dc2171c2b5771946b5a9)
|
||||||
|
* win: add Dropbox telemetry blocking #125, #118 | [10829d6](https://github.com/undergroundwires/privacy.sexy/commit/10829d65aa3fb0df937bb8829244e6290bb748c7)
|
||||||
|
* Improve selection type documentation | [7af8daa](https://github.com/undergroundwires/privacy.sexy/commit/7af8daa3411b24efb6385c7876a49bd372753f38)
|
||||||
|
* Expand script names to take full available width | [d277139](https://github.com/undergroundwires/privacy.sexy/commit/d277139dd50eeb4e4057b0a7d8fc4ac2d70785de)
|
||||||
|
* Limit tooltip width for improved readability | [6ab6dac](https://github.com/undergroundwires/privacy.sexy/commit/6ab6dacd1be2d7bf1863b07b121d86f2a379ac67)
|
||||||
|
* Add markdown support for script/category names | [a5ffed4](https://github.com/undergroundwires/privacy.sexy/commit/a5ffed4cd60d9d058d5374145c1176b10fad1660)
|
||||||
|
* Normalize and improve font sizes | [4da306b](https://github.com/undergroundwires/privacy.sexy/commit/4da306b9f79b0bb7a64bb197fb246258cf435b8d)
|
||||||
|
* Change 'revert' button to title case | [937f459](https://github.com/undergroundwires/privacy.sexy/commit/937f4593d1a91081ab6b1bcb8f85d03879d7cf07)
|
||||||
|
* Remove playful emojis (🍑🍆) | [aa4205f](https://github.com/undergroundwires/privacy.sexy/commit/aa4205ff7af7d05cfb5e82bf541b521d49bbd1c8)
|
||||||
|
* Improve UI code styling for all platforms | [311fcb1](https://github.com/undergroundwires/privacy.sexy/commit/311fcb18133d1343f6a9ae5bd7a25795a1d12c49)
|
||||||
|
* Render bracket references as superscript text | [b9c89b7](https://github.com/undergroundwires/privacy.sexy/commit/b9c89b701fc77d20dcc706419a8659ad156c4fc2)
|
||||||
|
* Change slogan and refactor project info naming | [a54e164](https://github.com/undergroundwires/privacy.sexy/commit/a54e16488ce32219bcf811b5da85f06584b293fb)
|
||||||
|
* Add 'Revert All Selection' feature #68 | [55fa7ea](https://github.com/undergroundwires/privacy.sexy/commit/55fa7eae71031357d6f03f0d349a09cd446270d3)
|
||||||
|
* win, mac, linux: add privacy.sexy cleanup scripts | [63366a4](https://github.com/undergroundwires/privacy.sexy/commit/63366a4ec2533a376849d692211e9972b56ab4a8)
|
||||||
|
* Extend search by including documentation content | [6142f3a](https://github.com/undergroundwires/privacy.sexy/commit/6142f3a2973d20493f784f323f3be57fa8deaeef)
|
||||||
|
* Remove 'preview' label from Linux options | [ebd8285](https://github.com/undergroundwires/privacy.sexy/commit/ebd82853ddc56f1cc2fc9be3fe0b3001b07f0186)
|
||||||
|
* Change fonts for improved readability | [d5bbc32](https://github.com/undergroundwires/privacy.sexy/commit/d5bbc321f902dc60618ffdfda0d583a4a433f7af)
|
||||||
|
* Apply global styles for visual consistency | [faa7a38](https://github.com/undergroundwires/privacy.sexy/commit/faa7a38a7d16390f27e4a3e51017b81665cf85ca)
|
||||||
|
* Add UI animations for expand/collapse actions | [fb08f03](https://github.com/undergroundwires/privacy.sexy/commit/fb08f037651e1a7d453b9a6af724cbccecc5b903)
|
||||||
|
* win: relocate service disabling and improve docs | [894687c](https://github.com/undergroundwires/privacy.sexy/commit/894687c0e0375a24f40bcd720ea69c9b2aa62a58)
|
||||||
|
* win: add host blocking category #26 | [17152c8](https://github.com/undergroundwires/privacy.sexy/commit/17152c84dc639e75560998a6feddfd46e0f713ce)
|
||||||
|
* Update meta title and description | [c7fa4b6](https://github.com/undergroundwires/privacy.sexy/commit/c7fa4b6d020ac6fd3bf72bb4e57022dffb1ba921)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.10...0.13.0)
|
||||||
|
|
||||||
## 0.12.10 (2024-01-17)
|
## 0.12.10 (2024-01-17)
|
||||||
|
|
||||||
* Fix CSP for Vue, Ace, Vite, Safari compatibility | [940febc](https://github.com/undergroundwires/privacy.sexy/commit/940febc3e80cfd0c01b5cc8282ebaab6b024d1b5)
|
* Fix CSP for Vue, Ace, Vite, Safari compatibility | [940febc](https://github.com/undergroundwires/privacy.sexy/commit/940febc3e80cfd0c01b5cc8282ebaab6b024d1b5)
|
||||||
|
|||||||
11
README.md
@@ -60,8 +60,8 @@
|
|||||||
<br />
|
<br />
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
<img
|
<img
|
||||||
alt="Quality checks status"
|
alt="Status of quality checks"
|
||||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.quality/badge.svg"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
@@ -122,9 +122,12 @@
|
|||||||
## 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.12.10/privacy.sexy-Setup-0.12.10.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.10/privacy.sexy-0.12.10.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.10/privacy.sexy-0.12.10.AppImage). For more options, see [here](#additional-install-options).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.2/privacy.sexy-Setup-0.13.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.2/privacy.sexy-0.13.2.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.2/privacy.sexy-0.13.2.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
For a detailed comparison of features between the desktop and web versions of privacy.sexy, see [Desktop vs. Web Features](./docs/desktop-vs-web-features.md).
|
See also:
|
||||||
|
|
||||||
|
- [Desktop vs. Web Features](./docs/desktop/desktop-vs-web-features.md): Differences and unique aspects of desktop and web versions.
|
||||||
|
- [System Requirements](./docs/desktop/system-requirements.md): Hardware and software requirements for the desktop version.
|
||||||
|
|
||||||
💡 Regularly applying your configuration with privacy.sexy is recommended, especially after each new release and major operating system updates. Each version updates scripts to enhance stability, privacy, and security.
|
💡 Regularly applying your configuration with privacy.sexy is recommended, especially after each new release and major operating system updates. Each version updates scripts to enhance stability, privacy, and security.
|
||||||
|
|
||||||
|
|||||||
15
SECURITY.md
@@ -43,10 +43,17 @@ privacy.sexy adopts a defense in depth strategy to protect users on multiple lay
|
|||||||
elevation of privileges for system modifications with explicit user consent and logs every action taken with high privileges. This
|
elevation of privileges for system modifications with explicit user consent and logs every action taken with high privileges. This
|
||||||
approach actively minimizes potential security risks by limiting privileged operations and aligning with the principle of least privilege.
|
approach actively minimizes potential security risks by limiting privileged operations and aligning with the principle of least privilege.
|
||||||
- **Secure Script Execution/Storage:**
|
- **Secure Script Execution/Storage:**
|
||||||
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans. This safeguards against
|
- **Antivirus scans:**
|
||||||
any unwanted modifications. Furthermore, the application incorporates integrity checks for tamper protection. If the script file differs from
|
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans.
|
||||||
the user's selected script, the application will not execute or save the script, ensuring the processing of authentic scripts.
|
This step allows confirming that the scripts are secure and safe to use.
|
||||||
Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these scripts.
|
- **Tamper protection:**
|
||||||
|
The application incorporates integrity checks for tamper protection.
|
||||||
|
If the script file differs from the user's selected script, the application will not execute or save the script, ensuring the processing
|
||||||
|
of authentic scripts.
|
||||||
|
This safeguards against any unwanted modifications.
|
||||||
|
- **Clean-up:**
|
||||||
|
Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these scripts.
|
||||||
|
This allows users to maintain their privacy by removing traces of their usage patterns or script preferences.
|
||||||
|
|
||||||
### Update Security and Integrity
|
### Update Security and Integrity
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# build
|
|
||||||
|
|
||||||
This folder contains files that are used by Electron to serve the desktop version.
|
|
||||||
|
|
||||||
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md).
|
|
||||||
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,6 +1,6 @@
|
|||||||
# Desktop vs. Web Features
|
# Desktop vs. Web Features
|
||||||
|
|
||||||
This table highlights differences between the desktop and web versions of `privacy.sexy`.
|
This table outlines the differences between the desktop and web versions of `privacy.sexy`.
|
||||||
|
|
||||||
| Feature | Desktop | Web |
|
| Feature | Desktop | Web |
|
||||||
| ------- | ------- | --- |
|
| ------- | ------- | --- |
|
||||||
@@ -8,10 +8,8 @@ This table highlights differences between the desktop and web versions of `priva
|
|||||||
| [Offline usage](#offline-usage) | 🟢 Available | 🟡 Partially available |
|
| [Offline usage](#offline-usage) | 🟢 Available | 🟡 Partially available |
|
||||||
| [Auto-updates](#auto-updates) | 🟢 Available | 🟢 Available |
|
| [Auto-updates](#auto-updates) | 🟢 Available | 🟢 Available |
|
||||||
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
|
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
|
||||||
| [Script execution](#script-execution) | 🟢 Available | 🔴 Not available |
|
|
||||||
| [Error handling](#error-handling) | 🟢 Advanced | 🟡 Limited |
|
|
||||||
| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available |
|
|
||||||
| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available |
|
| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available |
|
||||||
|
| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available |
|
||||||
|
|
||||||
## Feature descriptions
|
## Feature descriptions
|
||||||
|
|
||||||
@@ -30,11 +28,11 @@ Desktop version inherently allows offline usage.
|
|||||||
|
|
||||||
### Auto-updates
|
### Auto-updates
|
||||||
|
|
||||||
Both the desktop and web versions of privacy.sexy provide timely access to the latest features and security improvements. The updates are automatically deployed from source code, reflecting the latest changes for enhanced security and reliability. For more details, see [CI/CD documentation](./ci-cd.md).
|
Both the desktop and web versions of privacy.sexy provide timely access to the latest features and security improvements. The updates are automatically deployed from source code, reflecting the latest changes for enhanced security and reliability. For more details, see [CI/CD documentation](./../ci-cd.md).
|
||||||
|
|
||||||
The desktop version ensures secure delivery through cryptographic signatures and version checks.
|
The desktop version ensures secure delivery through cryptographic signatures and version checks.
|
||||||
|
|
||||||
[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:** On macOS, the desktop version's auto-update process involves manual steps due to Apple's code signing costs.
|
> **Note for macOS users:** 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.
|
||||||
@@ -53,7 +51,7 @@ Log file locations vary by operating system:
|
|||||||
|
|
||||||
> 💡 privacy.sexy provides scripts to securely erase these logs.
|
> 💡 privacy.sexy provides scripts to securely erase these logs.
|
||||||
|
|
||||||
### Script execution
|
### Secure script execution/storage
|
||||||
|
|
||||||
The desktop version of privacy.sexy enables direct script execution, providing a seamless and integrated experience.
|
The desktop version of privacy.sexy enables direct script execution, providing a seamless and integrated experience.
|
||||||
This direct execution capability isn't available in the web version due to inherent browser restrictions.
|
This direct execution capability isn't available in the web version due to inherent browser restrictions.
|
||||||
@@ -69,31 +67,27 @@ These locations vary based on the operating system:
|
|||||||
|
|
||||||
> 💡 privacy.sexy provides scripts to securely erase your script execution history.
|
> 💡 privacy.sexy provides scripts to securely erase your script execution history.
|
||||||
|
|
||||||
### Error handling
|
**Script antivirus scans:**
|
||||||
|
|
||||||
The desktop version of privacy.sexy features advanced error handling capabilities.
|
To enhance system protection, the desktop version of privacy.sexy automatically verifies the security of script
|
||||||
It employs robust and reliable execution strategies, including self-healing mechanisms, and provides guidance and troubleshooting information to resolve issues effectively.
|
execution files by reading them back.
|
||||||
In contrast, the web version has more basic error handling due to browser limitations and the nature of web applications.
|
This process triggers antivirus scans to verify that scripts are safe before the execution.
|
||||||
|
|
||||||
### Native dialogs
|
**Script integrity checks:**
|
||||||
|
|
||||||
The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs.
|
|
||||||
These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities.
|
|
||||||
|
|
||||||
### Secure script execution/storage
|
|
||||||
|
|
||||||
**Integrity checks:**
|
|
||||||
|
|
||||||
The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage.
|
The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage.
|
||||||
Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them.
|
Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them.
|
||||||
If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script.
|
If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script.
|
||||||
This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability.
|
This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability.
|
||||||
Due to browser constraints, this feature is absent in the web version.
|
|
||||||
|
|
||||||
**Error handling:**
|
**Error handling:**
|
||||||
|
|
||||||
|
The desktop version of privacy.sexy features advanced error handling capabilities.
|
||||||
In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes.
|
In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes.
|
||||||
It also guides users through potential issues with filesystem or third-party software, such as antivirus interventions.
|
It employs robust and reliable execution strategies, including self-healing mechanisms, and provides guidance and troubleshooting information to resolve issues effectively.
|
||||||
Specifically, the application is capable of identifying when antivirus software blocks or removes a script, providing users with tailored error messages
|
This proactive error handling and user guidance enhances the application's security and reliability.
|
||||||
and detailed resolution steps. This level of proactive error handling and user guidance enhances the application's security and reliability,
|
|
||||||
offering a feature not achievable in the web version due to browser limitations.
|
### Native dialogs
|
||||||
|
|
||||||
|
The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs.
|
||||||
|
These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities.
|
||||||
40
docs/desktop/system-requirements.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# System Requirements for the Desktop Version
|
||||||
|
|
||||||
|
The following system requirements are the official ones for the desktop version.
|
||||||
|
While we have tested and confirmed these requirements, the application might also work on other
|
||||||
|
systems or configurations that haven't undergone official testing.
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
- **Version:** Windows 10 and later.
|
||||||
|
- **Processor:** Intel Pentium 4 or later.
|
||||||
|
- **Architecture:** 64-bit (x64), ARM.
|
||||||
|
|
||||||
|
> **⚠️ Compatibility Note:**
|
||||||
|
> ARM version is only compatible with Windows 11 and later.
|
||||||
|
> It runs non-natively, leading to slower performance due to emulation [1].
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
|
||||||
|
- **Version:** macOS Catalina (10.15) and later.
|
||||||
|
- **Architecture:** Intel-based (64-bit), Apple Silicon (ARM).
|
||||||
|
|
||||||
|
> **⚠️ Compatibility Note:**
|
||||||
|
> Apple Silicon version runs non-natively, leading to slower performance due to emulation [2].
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
- **Version:** Ubuntu 18.04 and later, Fedora 32 and later, and Debian 10 and later.
|
||||||
|
- **Processor:** Intel Pentium 4 or later.
|
||||||
|
- **Architecture:** 64-bit (x64).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
System requirements reflect Electron's platform capabilities [3] and Chromium's recommended configurations [4].
|
||||||
|
|
||||||
|
For details on the build process, see [electron-builder configuration file](./../../electron-builder.cjs).
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20240428082726/https://learn.microsoft.com/en-us/windows/arm/add-arm-support#emulation-on-arm-based-devices-for-x86-or-x64-windows-apps "Add support Arm devices to your Windows app | Microsoft Learn | learn.microsoft.com"
|
||||||
|
[2]: https://archive.today/2024.04.28-082901/https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary%23overview "Building a universal macOS binary | Apple Developer Documentation | developer.apple.com"
|
||||||
|
[3]: https://archive.ph/2024.04.28-082958/https://github.com/electron/electron/blob/main/README.md#platform-support "Platform Support | electron/README.md at main · electron/electron · GitHub | github.com"
|
||||||
|
[4]: https://web.archive.org/web/20240428082945/https://support.google.com/chrome/a/answer/7100626?hl=en "Chrome browser system requirements - Chrome Enterprise and Education Help | support.google.com"
|
||||||
@@ -14,18 +14,19 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
||||||
- [**`index.html`**](./../src/presentation/index.html): The `index.html` entry file, located at the root of the project as required by Vite
|
- [**`index.html`**](./../src/presentation/index.html): The `index.html` entry file, located at the root of the project as required by Vite
|
||||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
||||||
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
- [**`components/`**](./../src/presentation/components/): Contains Vue components, helpers and styles coupled to Vue components.
|
||||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
||||||
- [**`Hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections).
|
- [**`Hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections).
|
||||||
- [**`/public/`**](../src/presentation/public/): Contains static assets.
|
- [**`/public/`**](../src/presentation/public/): Contains static assets.
|
||||||
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
|
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
|
||||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
||||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
|
|
||||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
||||||
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||||
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
- [`/main/` **`index.ts`**](./../src/presentation/electron/main/index.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||||
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
- [`/preload/` **`index.ts`**](./../src/presentation/electron/preload/index.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||||
|
- [**`/shared/`**](./../src/presentation/electron/shared/): Shared logic between different Electron processes.
|
||||||
|
- [**`/build/`**](./../src/presentation/electron/build/): `electron-builder` build resources directory, [README.md](./../src/presentation/electron/build/README.md).
|
||||||
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
||||||
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
||||||
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
||||||
@@ -38,6 +39,13 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility.
|
They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility.
|
||||||
- **Borders**:
|
- **Borders**:
|
||||||
privacy.sexy prefers sharper edges in its design language.
|
privacy.sexy prefers sharper edges in its design language.
|
||||||
|
- **Fonts**:
|
||||||
|
- Use the primary font for regular text and monospace font for code or specific data.
|
||||||
|
- Use cursive and logo fonts solely for branding.
|
||||||
|
- Refer to [standardized font size variables](../src/presentation/assets/styles/_typography.scss) for font sizing, avoiding arbitrary `px`, `em`, `rem`, or percentage values.
|
||||||
|
- **Spacing**:
|
||||||
|
Use [global spacing variables](../src/presentation/assets/styles/_spacing.scss) for consistent margin, padding, and gap definitions.
|
||||||
|
This provides uniform spatial distribution and alignment of elements, enhancing visual harmony and making the UI more scalable and maintainable.
|
||||||
|
|
||||||
## Application data
|
## Application data
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ Key attributes of a good script:
|
|||||||
- `Minimize` over `Limit`, `Reduce`
|
- `Minimize` over `Limit`, `Reduce`
|
||||||
- `Maximize` over `Extend`, `Delay`, `Postpone`, `Prolong`
|
- `Maximize` over `Extend`, `Delay`, `Postpone`, `Prolong`
|
||||||
- `Remove` over `Uninstall`
|
- `Remove` over `Uninstall`
|
||||||
|
- `Improve` over `Increase`
|
||||||
- Structure your phrases for clarity, examples:
|
- Structure your phrases for clarity, examples:
|
||||||
- Prefer `Disable XX telemetry` over `Disable telemetry in XX`
|
- Prefer `Disable XX telemetry` over `Disable telemetry in XX`
|
||||||
- Prefer `Clear XX data` over `Clear data from XX`, or `Clear data of XX`.
|
- Prefer `Clear XX data` over `Clear data from XX`, or `Clear data of XX`.
|
||||||
@@ -35,8 +36,8 @@ Key attributes of a good script:
|
|||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- Use credible and reputable sources for references.
|
- Use credible and reputable sources for references.
|
||||||
- Use archived links by using [archive.org](https://archive.org) or [archive.today](https://archive.today).
|
- Use archived links by using [archive.org](https://archive.org) or [archive.ph](https://archive.ph).
|
||||||
- Format archive.today links fully, for example: `https://archive.today/YYYYMMDDhhmmss/https://privacy.sexy`.
|
- Format archive.today links fully, for example: `https://archive.ph/YYYYMMDDhhmmss/https://privacy.sexy`.
|
||||||
- Explain the default behavior if the script is not executed.
|
- Explain the default behavior if the script is not executed.
|
||||||
|
|
||||||
## Shared functions
|
## Shared functions
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
/* eslint-disable no-template-curly-in-string */
|
/* eslint-disable no-template-curly-in-string */
|
||||||
|
|
||||||
const { join } = require('node:path');
|
const { join, resolve } = require('node:path');
|
||||||
|
const { readdirSync, existsSync } = require('node:fs');
|
||||||
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('electron-builder').Configuration}
|
||||||
|
* @see https://www.electron.build/configuration/configuration
|
||||||
|
*/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Common options
|
// Common options
|
||||||
publish: {
|
publish: {
|
||||||
@@ -12,9 +17,12 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
directories: {
|
directories: {
|
||||||
output: electronBundled,
|
output: electronBundled,
|
||||||
|
buildResources: resolvePathFromProjectRoot('src/presentation/electron/build'),
|
||||||
},
|
},
|
||||||
extraMetadata: {
|
extraMetadata: {
|
||||||
main: join(electronUnbundled, 'main/index.cjs'), // do not `path.resolve`, it expects a relative path
|
main: findMainEntryFile(
|
||||||
|
join(electronUnbundled, 'main'), // do not `path.resolve`, it expects a relative path
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Windows
|
// Windows
|
||||||
@@ -41,3 +49,23 @@ module.exports = {
|
|||||||
artifactName: '${name}-${version}.${ext}',
|
artifactName: '${name}-${version}.${ext}',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds by accommodating different JS file extensions and module formats.
|
||||||
|
*/
|
||||||
|
function findMainEntryFile(parentDirectory) {
|
||||||
|
const absoluteParentDirectory = resolvePathFromProjectRoot(parentDirectory);
|
||||||
|
if (!existsSync(absoluteParentDirectory)) {
|
||||||
|
return null; // Avoid disrupting other processes such `npm install`.
|
||||||
|
}
|
||||||
|
const files = readdirSync(absoluteParentDirectory);
|
||||||
|
const entryFile = files.find((file) => /^index\.(cjs|mjs|js)$/.test(file));
|
||||||
|
if (!entryFile) {
|
||||||
|
throw new Error(`Main entry file not found in ${absoluteParentDirectory}.`);
|
||||||
|
}
|
||||||
|
return join(parentDirectory, entryFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePathFromProjectRoot(pathSegment) {
|
||||||
|
return resolve(__dirname, pathSegment);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { mergeConfig, UserConfig } from 'vite';
|
import { mergeConfig, type UserConfig } from 'vite';
|
||||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||||
import { getAliases, getClientEnvironmentVariables } from './vite-config-helper';
|
import { getAliases, getClientEnvironmentVariables } from './vite-config-helper';
|
||||||
import { createVueConfig } from './vite.config';
|
import { createVueConfig } from './vite.config';
|
||||||
@@ -14,7 +14,7 @@ const ELECTRON_DIST_SUBDIRECTORIES = {
|
|||||||
renderer: resolveElectronDistSubdirectory('renderer'),
|
renderer: resolveElectronDistSubdirectory('renderer'),
|
||||||
};
|
};
|
||||||
|
|
||||||
process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.cjs');
|
process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.mjs');
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: getSharedElectronConfig({
|
main: getSharedElectronConfig({
|
||||||
@@ -54,13 +54,23 @@ function getSharedElectronConfig(options: {
|
|||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
// Mark: electron-esm-support
|
format: 'es',
|
||||||
// This is needed so `type="module"` works
|
|
||||||
entryFileNames: '[name].cjs',
|
// Ensure all generated files use '.mjs' for module consistency.
|
||||||
|
// Otherwise, preloader process get `.mjs` extension but main process get `.js` extension, see https://github.com/alex8088/electron-vite/issues/397.
|
||||||
|
entryFileNames: '[name].mjs',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [externalizeDepsPlugin()],
|
plugins: [externalizeDepsPlugin({
|
||||||
|
exclude: [
|
||||||
|
// Keep 'electron-log' in bundling process.
|
||||||
|
// This is a workaround for inability of Electron's ESM loader to resolve subpath imports.
|
||||||
|
// Do not externalize `electron-log` so subpath imports such as `electron-log/main` works.
|
||||||
|
// See https://github.com/electron/electron/issues/41241, https://github.com/alex8088/electron-vite/issues/401
|
||||||
|
'electron-log',
|
||||||
|
],
|
||||||
|
})],
|
||||||
define: {
|
define: {
|
||||||
...getClientEnvironmentVariables(),
|
...getClientEnvironmentVariables(),
|
||||||
},
|
},
|
||||||
|
|||||||
16190
package-lock.json
generated
90
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.10",
|
"version": "0.13.2",
|
||||||
"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:eslint && npm run lint:yaml",
|
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml && 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",
|
||||||
@@ -29,66 +29,68 @@
|
|||||||
"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:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||||
|
"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.0.2",
|
"@floating-ui/vue": "^1.0.6",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"@types/markdown-it": "^13.0.7",
|
"ace-builds": "^1.33.0",
|
||||||
"ace-builds": "^1.30.0",
|
"electron-log": "^5.1.2",
|
||||||
"electron-log": "^5.0.1",
|
"electron-progressbar": "^2.2.1",
|
||||||
"electron-progressbar": "^2.1.0",
|
"electron-updater": "^6.1.9",
|
||||||
"electron-updater": "^6.1.4",
|
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"markdown-it": "^13.0.2",
|
"markdown-it": "^14.1.0",
|
||||||
"vue": "^3.3.7"
|
"vue": "^3.4.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||||
"@rushstack/eslint-patch": "^1.6.1",
|
"@rushstack/eslint-patch": "^1.10.2",
|
||||||
"@types/ace": "^0.0.49",
|
"@types/ace": "^0.0.52",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
"@types/markdown-it": "^14.0.1",
|
||||||
"@typescript-eslint/parser": "^6.17.0",
|
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||||
"@vitejs/plugin-legacy": "^4.1.1",
|
"@typescript-eslint/parser": "6.21.0",
|
||||||
"@vitejs/plugin-vue": "^4.4.0",
|
"@vitejs/plugin-legacy": "^5.3.2",
|
||||||
|
"@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.1",
|
"@vue/test-utils": "^2.4.5",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.19",
|
||||||
"cypress": "^13.3.1",
|
"cypress": "^13.7.3",
|
||||||
"electron": "^27.0.0",
|
"electron": "^29.3.0",
|
||||||
"electron-builder": "^24.6.4",
|
"electron-builder": "^24.13.3",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-icon-builder": "^2.0.1",
|
"electron-vite": "^2.1.0",
|
||||||
"electron-vite": "^1.0.28",
|
"eslint": "8.57.0",
|
||||||
"eslint": "^8.56.0",
|
|
||||||
"eslint-plugin-cypress": "^2.15.1",
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"eslint-plugin-vue": "^9.19.2",
|
"eslint-plugin-vue": "^9.25.0",
|
||||||
"eslint-plugin-vuejs-accessibility": "^2.2.0",
|
"eslint-plugin-vuejs-accessibility": "^2.2.1",
|
||||||
"icon-gen": "^4.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"jsdom": "^22.1.0",
|
"markdownlint-cli": "^0.39.0",
|
||||||
"markdownlint-cli": "^0.37.0",
|
"postcss": "^8.4.38",
|
||||||
"postcss": "^8.4.31",
|
|
||||||
"remark-cli": "^12.0.0",
|
"remark-cli": "^12.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^5.1.2",
|
"remark-preset-lint-consistent": "^6.0.0",
|
||||||
"remark-validate-links": "^13.0.0",
|
"remark-validate-links": "^13.0.1",
|
||||||
"sass": "^1.69.3",
|
"sass": "^1.75.0",
|
||||||
"start-server-and-test": "^2.0.1",
|
"start-server-and-test": "^2.0.3",
|
||||||
"svgexport": "^0.4.2",
|
"terser": "^5.30.3",
|
||||||
"terser": "^5.21.0",
|
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^4.4.11",
|
"vite": "^5.2.8",
|
||||||
"vitest": "^0.34.6",
|
"vitest": "^1.5.0",
|
||||||
"vue-tsc": "^1.8.19",
|
"vue-tsc": "^2.0.13",
|
||||||
"yaml-lint": "^1.7.0"
|
"yaml-lint": "^1.7.0"
|
||||||
},
|
},
|
||||||
"//devDependencies": {
|
"//devDependencies": {
|
||||||
"terser": "Used by `@vitejs/plugin-legacy` for minification",
|
"terser": "Used by `@vitejs/plugin-legacy` for minification",
|
||||||
"@rushstack/eslint-patch": "Needed by `@vue/eslint-config-typescript` and `@vue/eslint-config-airbnb-with-typescript`"
|
"@rushstack/eslint-patch": "Needed by `@vue/eslint-config-typescript` and `@vue/eslint-config-airbnb-with-typescript`",
|
||||||
|
"@typescript-eslint/eslint-plugin": "Cannot migrate to v7 because of `@vue/eslint-config-airbnb-with-typescript`, see https://github.com/vuejs/eslint-config-airbnb/issues/63",
|
||||||
|
"@typescript-eslint/parser": "Cannot migrate to v7 because of `@vue/eslint-config-airbnb-with-typescript`, see https://github.com/vuejs/eslint-config-airbnb/issues/63",
|
||||||
|
"@vue/eslint-config-typescript": "Cannot migrate to v13 because of `@vue/eslint-config-airbnb-with-typescript`, see https://github.com/vuejs/eslint-config-airbnb/issues/63",
|
||||||
|
"eslint": "Cannot migrate to v9 `@typescript-eslint/eslint-plugin` (≤ v7), `@typescript-eslint/parser` (≤ v7), `@vue/eslint-config-airbnb-with-typescript@` (≤ v8) requires `eslint` ≤ v8, see https://github.com/vuejs/eslint-config-airbnb/issues/65, https://github.com/typescript-eslint/typescript-eslint/issues/8211"
|
||||||
},
|
},
|
||||||
"homepage": "https://privacy.sexy",
|
"homepage": "https://privacy.sexy",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
"""
|
"""
|
||||||
This script configures project-level VSCode settings in '.vscode/settings.json' for
|
Description:
|
||||||
development and installs recommended extensions from '.vscode/extensions.json'.
|
This script configures project-level VSCode settings in '.vscode/settings.json' for
|
||||||
|
development and installs recommended extensions from '.vscode/extensions.json'.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 ./scripts/configure_vscode.py
|
||||||
"""
|
"""
|
||||||
# pylint: disable=missing-function-docstring
|
# pylint: disable=missing-function-docstring
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
from shutil import which
|
from shutil import which
|
||||||
|
|
||||||
VSCODE_SETTINGS_JSON_FILE: str = '.vscode/settings.json'
|
VSCODE_SETTINGS_JSON_FILE: str = '.vscode/settings.json'
|
||||||
@@ -39,7 +44,7 @@ def ensure_setting_file_exists() -> None:
|
|||||||
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||||
except IOError as error:
|
except IOError as error:
|
||||||
print_error(f"Error creating file {VSCODE_SETTINGS_JSON_FILE}: {error}")
|
print_error(f"Error creating file {VSCODE_SETTINGS_JSON_FILE}: {error}")
|
||||||
print(f"📄 Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||||
|
|
||||||
def add_or_update_settings() -> None:
|
def add_or_update_settings() -> None:
|
||||||
configure_setting_key('eslint.validate', ['vue', 'javascript', 'typescript'])
|
configure_setting_key('eslint.validate', ['vue', 'javascript', 'typescript'])
|
||||||
@@ -84,7 +89,7 @@ def install_recommended_extensions() -> None:
|
|||||||
if not extensions:
|
if not extensions:
|
||||||
print_skip(f"No recommendations found in the {VSCODE_EXTENSIONS_JSON_FILE} file.")
|
print_skip(f"No recommendations found in the {VSCODE_EXTENSIONS_JSON_FILE} file.")
|
||||||
return
|
return
|
||||||
vscode_cli_path = which('code') # More reliable than using `code`, especially on Windows.
|
vscode_cli_path = locate_vscode_cli()
|
||||||
if vscode_cli_path is None:
|
if vscode_cli_path is None:
|
||||||
print_error('Visual Studio Code CLI (`code`) tool not found.')
|
print_error('Visual Studio Code CLI (`code`) tool not found.')
|
||||||
return
|
return
|
||||||
@@ -92,6 +97,19 @@ def install_recommended_extensions() -> None:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print_error(f"Invalid JSON in {VSCODE_EXTENSIONS_JSON_FILE}")
|
print_error(f"Invalid JSON in {VSCODE_EXTENSIONS_JSON_FILE}")
|
||||||
|
|
||||||
|
def locate_vscode_cli() -> Optional[str]:
|
||||||
|
vscode_alias = which('code') # More reliable than using `code`, especially on Windows.
|
||||||
|
if vscode_alias:
|
||||||
|
return vscode_alias
|
||||||
|
potential_vscode_cli_paths = [
|
||||||
|
# VS Code on macOS may not register 'code' command in PATH
|
||||||
|
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code'
|
||||||
|
]
|
||||||
|
for vscode_cli_candidate_path in potential_vscode_cli_paths:
|
||||||
|
if Path(vscode_cli_candidate_path).is_file():
|
||||||
|
return vscode_cli_candidate_path
|
||||||
|
return None
|
||||||
|
|
||||||
def remove_json_comments(json_like: str) -> str:
|
def remove_json_comments(json_like: str) -> str:
|
||||||
pattern: str = r'(?:"(?:\\.|[^"\\])*"|/\*[\s\S]*?\*/|//.*)|([^:]//.*$)'
|
pattern: str = r'(?:"(?:\\.|[^"\\])*"|/\*[\s\S]*?\*/|//.*)|([^:]//.*$)'
|
||||||
return re.sub(
|
return re.sub(
|
||||||
@@ -123,6 +141,12 @@ def install_vscode_extensions(vscode_cli_path: str, extensions: list[str]) -> No
|
|||||||
f"Visual Studio Code CLI tool not found: {vscode_cli_path}."
|
f"Visual Studio Code CLI tool not found: {vscode_cli_path}."
|
||||||
f"Could not install extension: {ext}",
|
f"Could not install extension: {ext}",
|
||||||
]))
|
]))
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
print_error(' '.join([
|
||||||
|
f"Failed to install extension '{ext}'.",
|
||||||
|
f"Attempted using Visual Studio Code CLI at: '{vscode_cli_path}'.",
|
||||||
|
f"Encountered error: {e}",
|
||||||
|
]))
|
||||||
total_extensions = len(extensions)
|
total_extensions = len(extensions)
|
||||||
print_installation_results(successful_installations, total_extensions)
|
print_installation_results(successful_installations, total_extensions)
|
||||||
|
|
||||||
@@ -147,16 +171,16 @@ def print_installation_results(successful_installations: int, total_extensions:
|
|||||||
print_error("Failed to install any of the recommended extensions.")
|
print_error("Failed to install any of the recommended extensions.")
|
||||||
|
|
||||||
def print_error(message: str) -> None:
|
def print_error(message: str) -> None:
|
||||||
print(f"💀 Error: {message}", file=sys.stderr)
|
print(f"[ERROR] {message}", file=sys.stderr)
|
||||||
|
|
||||||
def print_success(message: str) -> None:
|
def print_success(message: str) -> None:
|
||||||
print(f"✅ Success: {message}")
|
print(f"[SUCCESS] {message}")
|
||||||
|
|
||||||
def print_skip(message: str) -> None:
|
def print_skip(message: str) -> None:
|
||||||
print(f"⏩ Skipped: {message}")
|
print(f"[SKIPPED] {message}")
|
||||||
|
|
||||||
def print_warning(message: str) -> None:
|
def print_warning(message: str) -> None:
|
||||||
print(f"⚠️ Warning: {message}", file=sys.stderr)
|
print(f"[WARNING] {message}", file=sys.stderr)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,84 +1,120 @@
|
|||||||
#!/usr/bin/env bash
|
/**
|
||||||
import { resolve, join } from 'node:path';
|
* Description:
|
||||||
import { rm, mkdtemp, stat } from 'node:fs/promises';
|
* This script updates the logo images across the project based on the primary
|
||||||
|
* logo file ('img/logo.svg' file).
|
||||||
|
*
|
||||||
|
* It handles the creation and update of various icon sizes for different purposes,
|
||||||
|
* including desktop launcher icons, tray icons, and web favicons from a single source
|
||||||
|
* SVG logo file.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node ./scripts/logo-update.js
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* ImageMagick must be installed and accessible in the system's PATH
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { resolve, join, dirname } from 'node:path';
|
||||||
|
import { stat } from 'node:fs/promises';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { URL, fileURLToPath } from 'node:url';
|
import { URL, fileURLToPath } from 'node:url';
|
||||||
|
import electronBuilderConfig from '../electron-builder.cjs';
|
||||||
|
|
||||||
class Paths {
|
class ImageAssetPaths {
|
||||||
constructor(selfDirectory) {
|
constructor(currentScriptDirectory) {
|
||||||
const projectRoot = resolve(selfDirectory, '../');
|
const projectRoot = resolve(currentScriptDirectory, '../');
|
||||||
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
||||||
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
||||||
this.electronBuildDirectory = join(projectRoot, 'build');
|
this.electronBuildResourcesDirectory = electronBuilderConfig.directories.buildResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
get electronTrayIconFile() {
|
||||||
|
return join(this.publicDirectory, 'icon.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
get webFaviconFile() {
|
||||||
|
return join(this.publicDirectory, 'favicon.ico');
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return `Source image: ${this.sourceImage}\n`
|
return `Source image: ${this.sourceImage}`
|
||||||
+ `Public directory: ${this.publicDirectory}\n`
|
+ `\nPublic directory: ${this.publicDirectory}`
|
||||||
+ `Electron build directory: ${this.electronBuildDirectory}`;
|
+ `\n\t Electron tray icon file: ${this.electronTrayIconFile}`
|
||||||
|
+ `\n\t Web favicon file: ${this.webFaviconFile}`
|
||||||
|
+ `\nElectron build directory: ${this.electronBuildResourcesDirectory}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const paths = new Paths(getCurrentScriptDirectory());
|
const paths = new ImageAssetPaths(getCurrentScriptDirectory());
|
||||||
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
||||||
await updateDesktopLauncherAndTrayIcon(paths.sourceImage, paths.publicDirectory);
|
const convertCommand = await findAvailableImageMagickCommand();
|
||||||
await updateWebFavicon(paths.sourceImage, paths.publicDirectory);
|
await generateDesktopAndTrayIcons(
|
||||||
await updateDesktopIcons(paths.sourceImage, paths.electronBuildDirectory);
|
paths.sourceImage,
|
||||||
|
paths.electronTrayIconFile,
|
||||||
|
convertCommand,
|
||||||
|
);
|
||||||
|
await generateWebFavicon(
|
||||||
|
paths.sourceImage,
|
||||||
|
paths.webFaviconFile,
|
||||||
|
convertCommand,
|
||||||
|
);
|
||||||
|
await generateDesktopIcons(
|
||||||
|
paths.sourceImage,
|
||||||
|
paths.electronBuildResourcesDirectory,
|
||||||
|
convertCommand,
|
||||||
|
);
|
||||||
console.log('🎉 (Re)created icons successfully.');
|
console.log('🎉 (Re)created icons successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDesktopLauncherAndTrayIcon(sourceImage, publicFolder) {
|
async function generateDesktopAndTrayIcons(sourceImage, targetFile, convertCommand) {
|
||||||
|
// Reference: https://web.archive.org/web/20240502124306/https://www.electronjs.org/docs/latest/api/tray
|
||||||
|
console.log(`Updating desktop launcher and tray icon at ${targetFile}.`);
|
||||||
await ensureFileExists(sourceImage);
|
await ensureFileExists(sourceImage);
|
||||||
await ensureFolderExists(publicFolder);
|
await ensureParentFolderExists(targetFile);
|
||||||
const electronTrayIconFile = join(publicFolder, 'icon.png');
|
await convertFromSvgToPng(
|
||||||
console.log(`Updating desktop launcher and tray icon at ${electronTrayIconFile}.`);
|
convertCommand,
|
||||||
await runCommand(
|
|
||||||
'npx',
|
|
||||||
'svgexport',
|
|
||||||
sourceImage,
|
sourceImage,
|
||||||
electronTrayIconFile,
|
targetFile,
|
||||||
|
'512x512',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateWebFavicon(sourceImage, faviconFolder) {
|
async function generateWebFavicon(sourceImage, faviconFilePath, convertCommand) {
|
||||||
console.log('Updating favicon');
|
console.log(`Updating favicon at ${faviconFilePath}.`);
|
||||||
await ensureFileExists(sourceImage);
|
await ensureFileExists(sourceImage);
|
||||||
await ensureFolderExists(faviconFolder);
|
await ensureParentFolderExists(faviconFilePath);
|
||||||
await runCommand(
|
await convertFromSvgToIco(
|
||||||
'npx',
|
convertCommand,
|
||||||
'icon-gen',
|
sourceImage,
|
||||||
`--input ${sourceImage}`,
|
faviconFilePath,
|
||||||
`--output ${faviconFolder}`,
|
[16, 24, 32, 48, 64, 128, 256],
|
||||||
'--ico',
|
|
||||||
'--ico-name \'favicon\'',
|
|
||||||
'--report',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDesktopIcons(sourceImage, electronIconsDir) {
|
async function generateDesktopIcons(sourceImage, electronBuildResourcesDirectory, convertCommand) {
|
||||||
|
console.log(`Creating Electron icon files to ${electronBuildResourcesDirectory}.`);
|
||||||
|
// Reference: https://web.archive.org/web/20240501103645/https://www.electron.build/icons.html
|
||||||
|
await ensureFolderExists(electronBuildResourcesDirectory);
|
||||||
await ensureFileExists(sourceImage);
|
await ensureFileExists(sourceImage);
|
||||||
await ensureFolderExists(electronIconsDir);
|
const electronMainIconFile = join(electronBuildResourcesDirectory, 'icon.png');
|
||||||
const temporaryDir = await mkdtemp('icon-');
|
await convertFromSvgToPng(
|
||||||
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
convertCommand,
|
||||||
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by `icon-builder`
|
|
||||||
await runCommand(
|
|
||||||
'npx',
|
|
||||||
'svgexport',
|
|
||||||
sourceImage,
|
sourceImage,
|
||||||
temporaryPngFile,
|
electronMainIconFile,
|
||||||
'1024:1024',
|
'1024x1024', // Should be at least 512x512
|
||||||
);
|
);
|
||||||
console.log(`Creating electron icons to ${electronIconsDir}.`);
|
// Relying on `electron-builder`s conversion from png to ico results in pixelated look on Windows
|
||||||
await runCommand(
|
// 10 and 11 according to tests, see:
|
||||||
'npx',
|
// - https://web.archive.org/web/20240502114650/https://github.com/electron-userland/electron-builder/issues/7328
|
||||||
'electron-icon-builder',
|
// - https://web.archive.org/web/20240502115448/https://github.com/electron-userland/electron-builder/issues/3867
|
||||||
`--input="${temporaryPngFile}"`,
|
const electronWindowsIconFile = join(electronBuildResourcesDirectory, 'icon.ico');
|
||||||
`--output="${electronIconsDir}"`,
|
await convertFromSvgToIco(
|
||||||
'--flatten',
|
convertCommand,
|
||||||
|
sourceImage,
|
||||||
|
electronWindowsIconFile,
|
||||||
|
[16, 24, 32, 48, 64, 128, 256],
|
||||||
);
|
);
|
||||||
console.log('Cleaning up temporary directory.');
|
|
||||||
await rm(temporaryDir, { recursive: true, force: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureFileExists(filePath) {
|
async function ensureFileExists(filePath) {
|
||||||
@@ -89,12 +125,60 @@ async function ensureFileExists(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureFolderExists(folderPath) {
|
async function ensureFolderExists(folderPath) {
|
||||||
|
if (!folderPath) {
|
||||||
|
throw new Error('Path is missing');
|
||||||
|
}
|
||||||
const path = await stat(folderPath);
|
const path = await stat(folderPath);
|
||||||
if (!path.isDirectory()) {
|
if (!path.isDirectory()) {
|
||||||
throw new Error(`Not a directory: ${folderPath}`);
|
throw new Error(`Not a directory: ${folderPath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureParentFolderExists(filePath) {
|
||||||
|
return ensureFolderExists(dirname(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseImageMagickConvertArguments = Object.freeze([
|
||||||
|
'-background none', // Transparent, so they do not get filled with white.
|
||||||
|
'-strip', // Strip metadata.
|
||||||
|
'-gravity Center', // Center the image when there's empty space
|
||||||
|
]);
|
||||||
|
|
||||||
|
async function convertFromSvgToIco(
|
||||||
|
convertCommand,
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
sizes,
|
||||||
|
) {
|
||||||
|
await runCommand(
|
||||||
|
convertCommand,
|
||||||
|
...BaseImageMagickConvertArguments,
|
||||||
|
`-density ${Math.max(...sizes).toString()}`, // High enough for sharpness
|
||||||
|
`-define icon:auto-resize=${sizes.map((s) => s.toString()).join(',')}`, // Automatically store multiple sizes in an ico image
|
||||||
|
'-compress None',
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertFromSvgToPng(
|
||||||
|
convertCommand,
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
size = undefined,
|
||||||
|
) {
|
||||||
|
await runCommand(
|
||||||
|
convertCommand,
|
||||||
|
...BaseImageMagickConvertArguments,
|
||||||
|
...(size === undefined ? [] : [
|
||||||
|
`-resize ${size}`,
|
||||||
|
`-density ${size}`, // High enough for sharpness
|
||||||
|
]),
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function runCommand(...args) {
|
async function runCommand(...args) {
|
||||||
const command = args.join(' ');
|
const command = args.join(' ');
|
||||||
console.log(`Running command: ${command}`);
|
console.log(`Running command: ${command}`);
|
||||||
@@ -124,4 +208,27 @@ function getCurrentScriptDirectory() {
|
|||||||
return fileURLToPath(new URL('.', import.meta.url));
|
return fileURLToPath(new URL('.', import.meta.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findAvailableImageMagickCommand() {
|
||||||
|
// Reference: https://web.archive.org/web/20240502120041/https://imagemagick.org/script/convert.php
|
||||||
|
const potentialBaseCommands = [
|
||||||
|
'convert', // Legacy command, usually available on Linux/macOS installations
|
||||||
|
'magick convert', // Newer command, available on Windows installations
|
||||||
|
];
|
||||||
|
for (const baseCommand of potentialBaseCommands) {
|
||||||
|
const testCommand = `${baseCommand} -version`;
|
||||||
|
try {
|
||||||
|
await runCommand(testCommand); // eslint-disable-line no-await-in-loop
|
||||||
|
console.log(`Confirmed: ImageMagick command '${baseCommand}' is available and operational.`);
|
||||||
|
return baseCommand;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error: The command '${baseCommand}' is not found or failed to execute. Detailed error: ${err.message}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error([
|
||||||
|
'Unable to locate any operational ImageMagick command.',
|
||||||
|
`Attempted commands were: ${potentialBaseCommands.join(', ')}.`,
|
||||||
|
'Please ensure ImageMagick is correctly installed and accessible.',
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
await main();
|
await main();
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ function getBuildVerificationConfigs() {
|
|||||||
'--electron-unbundled': {
|
'--electron-unbundled': {
|
||||||
printDistDirScriptArgument: '--electron-unbundled',
|
printDistDirScriptArgument: '--electron-unbundled',
|
||||||
filePatterns: [
|
filePatterns: [
|
||||||
/main[/\\]index\.cjs/,
|
/main[/\\]index\.(cjs|mjs|js)/,
|
||||||
/preload[/\\]index\.cjs/,
|
/preload[/\\]index\.(cjs|mjs|js)/,
|
||||||
/renderer[/\\]index\.htm(l)?/,
|
/renderer[/\\]index\.htm(l)?/,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,62 +1,87 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Description:
|
* Description:
|
||||||
* This script checks if a server, provided as a CLI argument, is up
|
* This script checks if a server, provided as a CLI argument, is up
|
||||||
* and returns an HTTP 200 status code.
|
* and returns an HTTP 200 status code.
|
||||||
* It is designed to provide easy verification of server availability
|
* It is designed to provide easy verification of server availability
|
||||||
* and will retry a specified number of times.
|
* and will retry a specified number of times.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node ./scripts/verify-web-server-status.js --url [URL]
|
* node ./scripts/verify-web-server-status.js --url [URL] [--max-retries NUMBER]
|
||||||
*
|
*
|
||||||
* Options:
|
* Options:
|
||||||
* --url URL of the server to check
|
* --url URL of the server to check
|
||||||
|
* --max-retries Maximum number of retry attempts (default: 30)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { get } from 'http';
|
const DEFAULT_MAX_RETRIES = 30;
|
||||||
|
|
||||||
const MAX_RETRIES = 30;
|
|
||||||
const RETRY_DELAY_IN_SECONDS = 3;
|
const RETRY_DELAY_IN_SECONDS = 3;
|
||||||
const URL_PARAMETER_NAME = '--url';
|
const PARAMETER_NAME_URL = '--url';
|
||||||
|
const PARAMETER_NAME_MAX_RETRIES = '--max-retries';
|
||||||
|
|
||||||
function checkServer(currentRetryCount = 1) {
|
async function checkServer(currentRetryCount = 1) {
|
||||||
const serverUrl = getServerUrl();
|
const serverUrl = readRequiredParameterValue(PARAMETER_NAME_URL);
|
||||||
console.log(`Requesting ${serverUrl}...`);
|
const maxRetries = parseNumber(
|
||||||
get(serverUrl, (res) => {
|
readOptionalParameterValue(PARAMETER_NAME_MAX_RETRIES, DEFAULT_MAX_RETRIES),
|
||||||
if (res.statusCode === 200) {
|
);
|
||||||
|
console.log(`🌐 Requesting ${serverUrl}...`);
|
||||||
|
try {
|
||||||
|
const response = await fetch(serverUrl);
|
||||||
|
if (response.status === 200) {
|
||||||
console.log('🎊 Success: The server is up and returned HTTP 200.');
|
console.log('🎊 Success: The server is up and returned HTTP 200.');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Server returned HTTP status code ${res.statusCode}.`);
|
exitWithError(`Server returned unexpected HTTP status code ${response.statusCode}.`);
|
||||||
retry(currentRetryCount);
|
|
||||||
}
|
}
|
||||||
}).on('error', (err) => {
|
} catch (error) {
|
||||||
console.error('Error making the request:', err);
|
console.error('Error making the request:', error);
|
||||||
retry(currentRetryCount);
|
scheduleNextRetry(maxRetries, currentRetryCount);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function retry(currentRetryCount) {
|
function scheduleNextRetry(maxRetries, currentRetryCount) {
|
||||||
console.log(`Attempt ${currentRetryCount}/${MAX_RETRIES}:`);
|
console.log(`Attempt ${currentRetryCount}/${maxRetries}:`);
|
||||||
console.log(`Retrying in ${RETRY_DELAY_IN_SECONDS} seconds.`);
|
console.log(`Retrying in ${RETRY_DELAY_IN_SECONDS} seconds.`);
|
||||||
|
|
||||||
const remainingTime = (MAX_RETRIES - currentRetryCount) * RETRY_DELAY_IN_SECONDS;
|
const remainingTime = (maxRetries - currentRetryCount) * RETRY_DELAY_IN_SECONDS;
|
||||||
console.log(`Time remaining before timeout: ${remainingTime}s`);
|
console.log(`Time remaining before timeout: ${remainingTime}s`);
|
||||||
|
|
||||||
if (currentRetryCount < MAX_RETRIES) {
|
if (currentRetryCount < maxRetries) {
|
||||||
setTimeout(() => checkServer(currentRetryCount + 1), RETRY_DELAY_IN_SECONDS * 1000);
|
setTimeout(() => checkServer(currentRetryCount + 1), RETRY_DELAY_IN_SECONDS * 1000);
|
||||||
} else {
|
} else {
|
||||||
console.log('Failure: The server at did not return HTTP 200 within the allocated time. Exiting.');
|
exitWithError('The server at did not return HTTP 200 within the allocated time.');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getServerUrl() {
|
function readRequiredParameterValue(parameterName) {
|
||||||
const urlIndex = process.argv.indexOf(URL_PARAMETER_NAME);
|
const parameterValue = readOptionalParameterValue(parameterName);
|
||||||
if (urlIndex === -1 || urlIndex === process.argv.length - 1) {
|
if (parameterValue === undefined) {
|
||||||
console.error(`Parameter "${URL_PARAMETER_NAME}" is not provided.`);
|
exitWithError(`Parameter "${parameterName}" is required but not provided.`);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
return process.argv[urlIndex + 1];
|
return parameterValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkServer();
|
function readOptionalParameterValue(parameterName, defaultValue) {
|
||||||
|
const index = process.argv.indexOf(parameterName);
|
||||||
|
if (index === -1 || index === process.argv.length - 1) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return process.argv[index + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNumber(numberLike) {
|
||||||
|
const number = parseInt(numberLike, 10);
|
||||||
|
if (Number.isNaN(number)) {
|
||||||
|
exitWithError(`Invalid number: ${numberLike}`);
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitWithError(message) {
|
||||||
|
console.error(`Failure: ${message}`);
|
||||||
|
console.log('Exiting');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkServer();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||||
import { IApplicationFactory } from './IApplicationFactory';
|
|
||||||
import { parseApplication } from './Parser/ApplicationParser';
|
import { parseApplication } from './Parser/ApplicationParser';
|
||||||
|
import type { IApplicationFactory } from './IApplicationFactory';
|
||||||
|
|
||||||
export type ApplicationGetterType = () => IApplication;
|
export type ApplicationGetterType = () => IApplication;
|
||||||
const ApplicationGetter: ApplicationGetterType = parseApplication;
|
const ApplicationGetter: ApplicationGetterType = parseApplication;
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ export type CodeRunErrorType =
|
|||||||
| 'FileWriteError'
|
| 'FileWriteError'
|
||||||
| 'FileReadbackVerificationError'
|
| 'FileReadbackVerificationError'
|
||||||
| 'FilePathGenerationError'
|
| 'FilePathGenerationError'
|
||||||
| 'UnsupportedOperatingSystem'
|
| 'UnsupportedPlatform'
|
||||||
| 'FileExecutionError'
|
|
||||||
| 'DirectoryCreationError'
|
| 'DirectoryCreationError'
|
||||||
| 'UnexpectedError';
|
| 'FilePermissionChangeError'
|
||||||
|
| 'FileExecutionError'
|
||||||
|
| 'ExternalProcessTermination';
|
||||||
|
|
||||||
interface CodeRunStatus {
|
interface CodeRunStatus {
|
||||||
readonly success: boolean;
|
readonly success: boolean;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { assertInRange } from '@/application/Common/Enum';
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
import type { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
||||||
|
|
||||||
type Getter<T> = () => T;
|
type Getter<T> = () => T;
|
||||||
|
|
||||||
|
|||||||
12
src/application/Common/Shuffle.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Shuffle an array of strings, returning a new array with elements in random order.
|
||||||
|
Uses the Fisher-Yates (or Durstenfeld) algorithm.
|
||||||
|
*/
|
||||||
|
export function shuffle<T>(array: readonly T[]): T[] {
|
||||||
|
const shuffledArray = [...array];
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
|
||||||
|
}
|
||||||
|
return shuffledArray;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PlatformTimer } from './PlatformTimer';
|
import { PlatformTimer } from './PlatformTimer';
|
||||||
import { TimeoutType, Timer } from './Timer';
|
import type { TimeoutType, Timer } from './Timer';
|
||||||
|
|
||||||
export function batchedDebounce<T>(
|
export function batchedDebounce<T>(
|
||||||
callback: (batches: readonly T[]) => void,
|
callback: (batches: readonly T[]) => void,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Timer } from './Timer';
|
import type { Timer } from './Timer';
|
||||||
|
|
||||||
export const PlatformTimer: Timer = {
|
export const PlatformTimer: Timer = {
|
||||||
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
||||||
|
|||||||
@@ -1,44 +1,156 @@
|
|||||||
import { Timer, TimeoutType } from './Timer';
|
/* eslint-disable max-classes-per-file */
|
||||||
import { PlatformTimer } from './PlatformTimer';
|
import { PlatformTimer } from './PlatformTimer';
|
||||||
|
import type { Timer, TimeoutType } from './Timer';
|
||||||
|
|
||||||
export type CallbackType = (..._: readonly unknown[]) => void;
|
export type CallbackType = (..._: readonly unknown[]) => void;
|
||||||
|
|
||||||
|
export interface ThrottleOptions {
|
||||||
|
/** Skip the immediate execution of the callback on the first invoke */
|
||||||
|
readonly excludeLeadingCall: boolean;
|
||||||
|
readonly timer: Timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultOptions: ThrottleOptions = {
|
||||||
|
excludeLeadingCall: false,
|
||||||
|
timer: PlatformTimer,
|
||||||
|
};
|
||||||
|
|
||||||
export function throttle(
|
export function throttle(
|
||||||
callback: CallbackType,
|
callback: CallbackType,
|
||||||
waitInMs: number,
|
waitInMs: number,
|
||||||
timer: Timer = PlatformTimer,
|
options: Partial<ThrottleOptions> = DefaultOptions,
|
||||||
): CallbackType {
|
): CallbackType {
|
||||||
const throttler = new Throttler(timer, waitInMs, callback);
|
const defaultedOptions: ThrottleOptions = {
|
||||||
|
...DefaultOptions,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
const throttler = new Throttler(waitInMs, callback, defaultedOptions);
|
||||||
return (...args: unknown[]) => throttler.invoke(...args);
|
return (...args: unknown[]) => throttler.invoke(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Throttler {
|
class Throttler {
|
||||||
private queuedExecutionId: TimeoutType | undefined;
|
private lastExecutionTime: number | null = null;
|
||||||
|
|
||||||
private previouslyRun: number;
|
private executionScheduler: DelayedCallbackScheduler;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly timer: Timer,
|
|
||||||
private readonly waitInMs: number,
|
private readonly waitInMs: number,
|
||||||
private readonly callback: CallbackType,
|
private readonly callback: CallbackType,
|
||||||
|
private readonly options: ThrottleOptions,
|
||||||
) {
|
) {
|
||||||
if (!waitInMs) { throw new Error('missing delay'); }
|
if (!waitInMs) { throw new Error('missing delay'); }
|
||||||
if (waitInMs < 0) { throw new Error('negative delay'); }
|
if (waitInMs < 0) { throw new Error('negative delay'); }
|
||||||
|
this.executionScheduler = new DelayedCallbackScheduler(options.timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public invoke(...args: unknown[]): void {
|
public invoke(...args: unknown[]): void {
|
||||||
const now = this.timer.dateNow();
|
switch (true) {
|
||||||
if (this.queuedExecutionId !== undefined) {
|
case this.isLeadingCallWithinThrottlePeriod(): {
|
||||||
this.timer.clearTimeout(this.queuedExecutionId);
|
if (this.options.excludeLeadingCall) {
|
||||||
this.queuedExecutionId = undefined;
|
this.scheduleNext(args);
|
||||||
}
|
return;
|
||||||
if (!this.previouslyRun || (now - this.previouslyRun >= this.waitInMs)) {
|
}
|
||||||
this.callback(...args);
|
this.executeNow(args);
|
||||||
this.previouslyRun = now;
|
return;
|
||||||
} else {
|
}
|
||||||
const nextCall = () => this.invoke(...args);
|
case this.isAlreadyScheduled(): {
|
||||||
const nextCallDelayInMs = this.waitInMs - (now - this.previouslyRun);
|
this.updateNextScheduled(args);
|
||||||
this.queuedExecutionId = this.timer.setTimeout(nextCall, nextCallDelayInMs);
|
return;
|
||||||
|
}
|
||||||
|
case !this.isThrottlePeriodPassed(): {
|
||||||
|
this.scheduleNext(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Throttle logical error: no conditions for execution or scheduling were met.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isLeadingCallWithinThrottlePeriod(): boolean {
|
||||||
|
return this.isThrottlePeriodPassed()
|
||||||
|
&& !this.isAlreadyScheduled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private isThrottlePeriodPassed(): boolean {
|
||||||
|
if (this.lastExecutionTime === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const timeSinceLastExecution = this.options.timer.dateNow() - this.lastExecutionTime;
|
||||||
|
const isThrottleTimePassed = timeSinceLastExecution >= this.waitInMs;
|
||||||
|
return isThrottleTimePassed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isAlreadyScheduled(): boolean {
|
||||||
|
return this.executionScheduler.getNext() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleNext(args: unknown[]): void {
|
||||||
|
if (this.executionScheduler.getNext()) {
|
||||||
|
throw new Error('An execution is already scheduled.');
|
||||||
|
}
|
||||||
|
this.executionScheduler.resetNext(
|
||||||
|
() => this.executeNow(args),
|
||||||
|
this.waitInMs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateNextScheduled(args: unknown[]): void {
|
||||||
|
const nextScheduled = this.executionScheduler.getNext();
|
||||||
|
if (!nextScheduled) {
|
||||||
|
throw new Error('A non-existent scheduled execution cannot be updated.');
|
||||||
|
}
|
||||||
|
const nextDelay = nextScheduled.scheduledTime - this.dateNow();
|
||||||
|
this.executionScheduler.resetNext(
|
||||||
|
() => this.executeNow(args),
|
||||||
|
nextDelay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private executeNow(args: unknown[]): void {
|
||||||
|
this.callback(...args);
|
||||||
|
this.lastExecutionTime = this.dateNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private dateNow(): number {
|
||||||
|
return this.options.timer.dateNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScheduledCallback {
|
||||||
|
readonly scheduleTimeoutId: TimeoutType;
|
||||||
|
readonly scheduledTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DelayedCallbackScheduler {
|
||||||
|
private scheduledCallback: ScheduledCallback | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly timer: Timer,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public getNext(): ScheduledCallback | null {
|
||||||
|
return this.scheduledCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetNext(
|
||||||
|
callback: () => void,
|
||||||
|
delayInMs: number,
|
||||||
|
) {
|
||||||
|
this.clear();
|
||||||
|
this.scheduledCallback = {
|
||||||
|
scheduledTime: this.timer.dateNow() + delayInMs,
|
||||||
|
scheduleTimeoutId: this.timer.setTimeout(() => {
|
||||||
|
this.clear();
|
||||||
|
callback();
|
||||||
|
}, delayInMs),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private clear() {
|
||||||
|
if (this.scheduledCallback === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.timer.clearTimeout(this.scheduledCallback.scheduleTimeoutId);
|
||||||
|
this.scheduledCallback = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { assertInRange } from '@/application/Common/Enum';
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
import type { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
||||||
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
import type { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||||
|
|
||||||
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import type { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||||
import { IApplicationFactory } from '../IApplicationFactory';
|
|
||||||
import { ApplicationFactory } from '../ApplicationFactory';
|
import { ApplicationFactory } from '../ApplicationFactory';
|
||||||
import { ApplicationContext } from './ApplicationContext';
|
import { ApplicationContext } from './ApplicationContext';
|
||||||
|
import type { IApplicationFactory } from '../IApplicationFactory';
|
||||||
|
|
||||||
export async function buildContext(
|
export async function buildContext(
|
||||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
|
import type { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
|
||||||
|
|
||||||
export interface IReadOnlyApplicationContext {
|
export interface IReadOnlyApplicationContext {
|
||||||
readonly app: IApplication;
|
readonly app: IApplication;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
|
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
|
||||||
import { FilterContext } from './Filter/FilterContext';
|
|
||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
import { UserSelection } from './Selection/UserSelection';
|
|
||||||
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
|
||||||
import { UserSelectionFacade } from './Selection/UserSelectionFacade';
|
import { UserSelectionFacade } from './Selection/UserSelectionFacade';
|
||||||
|
import type { FilterContext } from './Filter/FilterContext';
|
||||||
|
import type { UserSelection } from './Selection/UserSelection';
|
||||||
|
import type { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||||
|
import type { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
|
||||||
export class CategoryCollectionState implements ICategoryCollectionState {
|
export class CategoryCollectionState implements ICategoryCollectionState {
|
||||||
public readonly os: OperatingSystem;
|
public readonly os: OperatingSystem;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { ReadonlyScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
|
import type { ReadonlyScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
|
||||||
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||||
import { CodePosition } from './Position/CodePosition';
|
import { CodePosition } from './Position/CodePosition';
|
||||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
|
||||||
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||||
import { IApplicationCode } from './IApplicationCode';
|
import type { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||||
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
import type { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
|
import type { IApplicationCode } from './IApplicationCode';
|
||||||
|
|
||||||
export class ApplicationCode implements IApplicationCode {
|
export class ApplicationCode implements IApplicationCode {
|
||||||
public readonly changed = new EventSource<ICodeChangedEvent>();
|
public readonly changed = new EventSource<ICodeChangedEvent>();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
import type { IScript } from '@/domain/IScript';
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
import type { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
|
||||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
public readonly code: string;
|
public readonly code: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
import type { IScript } from '@/domain/IScript';
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
export interface ICodeChangedEvent {
|
export interface ICodeChangedEvent {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ICodeBuilder } from './ICodeBuilder';
|
import type { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
const TotalFunctionSeparatorChars = 58;
|
const TotalFunctionSeparatorChars = 58;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ICodeBuilder } from './ICodeBuilder';
|
|
||||||
import { BatchBuilder } from './Languages/BatchBuilder';
|
import { BatchBuilder } from './Languages/BatchBuilder';
|
||||||
import { ShellBuilder } from './Languages/ShellBuilder';
|
import { ShellBuilder } from './Languages/ShellBuilder';
|
||||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
import type { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
import type { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||||
|
|
||||||
export class CodeBuilderFactory
|
export class CodeBuilderFactory
|
||||||
extends ScriptingLanguageFactory<ICodeBuilder>
|
extends ScriptingLanguageFactory<ICodeBuilder>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
import type { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||||
import { ICodeBuilder } from './ICodeBuilder';
|
import type { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
export type ICodeBuilderFactory = IScriptingLanguageFactory<ICodeBuilder>;
|
export type ICodeBuilderFactory = IScriptingLanguageFactory<ICodeBuilder>;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
|
|
||||||
export interface IUserScript {
|
export interface IUserScript {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { IUserScript } from './IUserScript';
|
import type { IUserScript } from './IUserScript';
|
||||||
|
|
||||||
export interface IUserScriptGenerator {
|
export interface IUserScriptGenerator {
|
||||||
buildCode(
|
buildCode(
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { CodePosition } from '../Position/CodePosition';
|
import { CodePosition } from '../Position/CodePosition';
|
||||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
|
||||||
import { IUserScript } from './IUserScript';
|
|
||||||
import { ICodeBuilder } from './ICodeBuilder';
|
|
||||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
|
||||||
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
||||||
|
import type { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||||
|
import type { IUserScript } from './IUserScript';
|
||||||
|
import type { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
import type { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||||
|
|
||||||
export class UserScriptGenerator implements IUserScriptGenerator {
|
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||||
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
import type { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
|
|
||||||
export interface IApplicationCode {
|
export interface IApplicationCode {
|
||||||
readonly changed: IEventSource<ICodeChangedEvent>;
|
readonly changed: IEventSource<ICodeChangedEvent>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ICodePosition } from './ICodePosition';
|
import type { ICodePosition } from './ICodePosition';
|
||||||
|
|
||||||
export class CodePosition implements ICodePosition {
|
export class CodePosition implements ICodePosition {
|
||||||
public get totalLines(): number {
|
public get totalLines(): number {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { FilterResult } from './Result/FilterResult';
|
|
||||||
import { FilterContext } from './FilterContext';
|
|
||||||
import { FilterChangeDetails } from './Event/FilterChangeDetails';
|
|
||||||
import { FilterChange } from './Event/FilterChange';
|
import { FilterChange } from './Event/FilterChange';
|
||||||
import { FilterStrategy } from './Strategy/FilterStrategy';
|
|
||||||
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
|
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
|
||||||
|
import type { FilterResult } from './Result/FilterResult';
|
||||||
|
import type { FilterContext } from './FilterContext';
|
||||||
|
import type { FilterChangeDetails } from './Event/FilterChangeDetails';
|
||||||
|
import type { FilterStrategy } from './Strategy/FilterStrategy';
|
||||||
|
|
||||||
export class AdaptiveFilterContext implements FilterContext {
|
export class AdaptiveFilterContext implements FilterContext {
|
||||||
public readonly filterChanged = new EventSource<FilterChangeDetails>();
|
public readonly filterChanged = new EventSource<FilterChangeDetails>();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
||||||
import { FilterActionType } from './FilterActionType';
|
import { FilterActionType } from './FilterActionType';
|
||||||
import {
|
import type {
|
||||||
FilterChangeDetails, FilterChangeDetailsVisitor,
|
FilterChangeDetails, FilterChangeDetailsVisitor,
|
||||||
ApplyFilterAction, ClearFilterAction,
|
ApplyFilterAction, ClearFilterAction,
|
||||||
} from './FilterChangeDetails';
|
} from './FilterChangeDetails';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import { FilterResult } from './Result/FilterResult';
|
import type { FilterResult } from './Result/FilterResult';
|
||||||
import { FilterChangeDetails } from './Event/FilterChangeDetails';
|
import type { FilterChangeDetails } from './Event/FilterChangeDetails';
|
||||||
|
|
||||||
export interface ReadonlyFilterContext {
|
export interface ReadonlyFilterContext {
|
||||||
readonly currentFilter: FilterResult | undefined;
|
readonly currentFilter: FilterResult | undefined;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
import type { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import type { ICategory } from '@/domain/ICategory';
|
||||||
import { FilterResult } from './FilterResult';
|
import type { FilterResult } from './FilterResult';
|
||||||
|
|
||||||
export class AppliedFilterResult implements FilterResult {
|
export class AppliedFilterResult implements FilterResult {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IScript, ICategory } from '@/domain/ICategory';
|
import type { IScript, ICategory } from '@/domain/ICategory';
|
||||||
|
|
||||||
export interface FilterResult {
|
export interface FilterResult {
|
||||||
readonly categoryMatches: ReadonlyArray<ICategory>;
|
readonly categoryMatches: ReadonlyArray<ICategory>;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';
|
import type { IApplicationCode } from './Code/IApplicationCode';
|
||||||
import { ReadonlyUserSelection, UserSelection } from './Selection/UserSelection';
|
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import type { ReadonlyUserSelection, UserSelection } from './Selection/UserSelection';
|
||||||
|
|
||||||
export interface IReadOnlyCategoryCollectionState {
|
export interface IReadOnlyCategoryCollectionState {
|
||||||
readonly code: IApplicationCode;
|
readonly code: IApplicationCode;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ICategory } from '@/domain/ICategory';
|
import type { ICategory } from '@/domain/ICategory';
|
||||||
import { CategorySelectionChangeCommand } from './CategorySelectionChange';
|
import type { CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
|
|
||||||
export interface ReadonlyCategorySelection {
|
export interface ReadonlyCategorySelection {
|
||||||
areAllScriptsSelected(category: ICategory): boolean;
|
areAllScriptsSelected(category: ICategory): boolean;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ICategory } from '@/domain/ICategory';
|
import type { ICategory } from '@/domain/ICategory';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { ScriptSelection } from '../Script/ScriptSelection';
|
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
import { ScriptSelectionChange } from '../Script/ScriptSelectionChange';
|
import type { CategorySelection } from './CategorySelection';
|
||||||
import { CategorySelection } from './CategorySelection';
|
import type { ScriptSelection } from '../Script/ScriptSelection';
|
||||||
import { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
import type { ScriptSelectionChange } from '../Script/ScriptSelectionChange';
|
||||||
|
|
||||||
export class ScriptToCategorySelectionMapper implements CategorySelection {
|
export class ScriptToCategorySelectionMapper implements CategorySelection {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
import { IScript } from '@/domain/IScript';
|
import type { IScript } from '@/domain/IScript';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
||||||
import { ScriptSelection } from './ScriptSelection';
|
|
||||||
import { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
|
||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
import { UserSelectedScript } from './UserSelectedScript';
|
import { UserSelectedScript } from './UserSelectedScript';
|
||||||
|
import type { ScriptSelection } from './ScriptSelection';
|
||||||
|
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
|
import type { SelectedScript } from './SelectedScript';
|
||||||
|
|
||||||
const DEBOUNCE_DELAY_IN_MS = 100;
|
const DEBOUNCE_DELAY_IN_MS = 100;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import { IScript } from '@/domain/IScript';
|
import type { IScript } from '@/domain/IScript';
|
||||||
import { SelectedScript } from './SelectedScript';
|
import type { SelectedScript } from './SelectedScript';
|
||||||
import { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
|
|
||||||
export interface ReadonlyScriptSelection {
|
export interface ReadonlyScriptSelection {
|
||||||
readonly changed: IEventSource<readonly SelectedScript[]>;
|
readonly changed: IEventSource<readonly SelectedScript[]>;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IEntity } from '@/infrastructure/Entity/IEntity';
|
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||||
import { IScript } from '@/domain/IScript';
|
import type { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
type ScriptId = IScript['id'];
|
type ScriptId = IScript['id'];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||||
import { IScript } from '@/domain/IScript';
|
import type { IScript } from '@/domain/IScript';
|
||||||
import { SelectedScript } from './SelectedScript';
|
import type { SelectedScript } from './SelectedScript';
|
||||||
|
|
||||||
type SelectedScriptId = SelectedScript['id'];
|
type SelectedScriptId = SelectedScript['id'];
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CategorySelection, ReadonlyCategorySelection } from './Category/CategorySelection';
|
import type { CategorySelection, ReadonlyCategorySelection } from './Category/CategorySelection';
|
||||||
import { ReadonlyScriptSelection, ScriptSelection } from './Script/ScriptSelection';
|
import type { ReadonlyScriptSelection, ScriptSelection } from './Script/ScriptSelection';
|
||||||
|
|
||||||
export interface ReadonlyUserSelection {
|
export interface ReadonlyUserSelection {
|
||||||
readonly categories: ReadonlyCategorySelection;
|
readonly categories: ReadonlyCategorySelection;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CategorySelection } from './Category/CategorySelection';
|
|
||||||
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
||||||
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
||||||
import { ScriptSelection } from './Script/ScriptSelection';
|
import type { CategorySelection } from './Category/CategorySelection';
|
||||||
import { UserSelection } from './UserSelection';
|
import type { ScriptSelection } from './Script/ScriptSelection';
|
||||||
import { SelectedScript } from './Script/SelectedScript';
|
import type { UserSelection } from './UserSelection';
|
||||||
|
import type { SelectedScript } from './Script/SelectedScript';
|
||||||
|
|
||||||
export class UserSelectionFacade implements UserSelection {
|
export class UserSelectionFacade implements UserSelection {
|
||||||
public readonly categories: CategorySelection;
|
public readonly categories: CategorySelection;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
export interface IApplicationFactory {
|
export interface IApplicationFactory {
|
||||||
getApp(): Promise<IApplication>;
|
getApp(): Promise<IApplication>;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { CollectionData } from '@/application/collections/';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import WindowsData from '@/application/collections/windows.yaml';
|
import WindowsData from '@/application/collections/windows.yaml';
|
||||||
import MacOsData from '@/application/collections/macos.yaml';
|
import MacOsData from '@/application/collections/macos.yaml';
|
||||||
import LinuxData from '@/application/collections/linux.yaml';
|
import LinuxData from '@/application/collections/linux.yaml';
|
||||||
import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser';
|
import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { CollectionData } from '@/application/collections/';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import { createEnumParser } from '../Common/Enum';
|
import { createEnumParser } from '../Common/Enum';
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { Category } from '@/domain/Category';
|
|||||||
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
||||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||||
import { parseDocs } from './DocumentationParser';
|
import { parseDocs } from './DocumentationParser';
|
||||||
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
|
||||||
import { parseScript } from './Script/ScriptParser';
|
import { parseScript } from './Script/ScriptParser';
|
||||||
|
import type { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||||
|
|
||||||
let categoryIdCounter = 0;
|
let categoryIdCounter = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CustomError } from '@/application/Common/CustomError';
|
import { CustomError } from '@/application/Common/CustomError';
|
||||||
import { NodeType } from './NodeType';
|
import { NodeType } from './NodeType';
|
||||||
import { NodeData } from './NodeData';
|
import type { NodeData } from './NodeData';
|
||||||
|
|
||||||
export class NodeDataError extends CustomError {
|
export class NodeDataError extends CustomError {
|
||||||
constructor(message: string, public readonly context: INodeDataErrorContext) {
|
constructor(message: string, public readonly context: INodeDataErrorContext) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { isString } from '@/TypeHelpers';
|
import { isString } from '@/TypeHelpers';
|
||||||
import { INodeDataErrorContext, NodeDataError } from './NodeDataError';
|
import { type INodeDataErrorContext, NodeDataError } from './NodeDataError';
|
||||||
import { NodeData } from './NodeData';
|
import type { NodeData } from './NodeData';
|
||||||
|
|
||||||
export class NodeValidator {
|
export class NodeValidator {
|
||||||
constructor(private readonly context: INodeDataErrorContext) {
|
constructor(private readonly context: INodeDataErrorContext) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import { GitHubProjectDetails } from '@/domain/Project/GitHubProjectDetails';
|
import { GitHubProjectDetails } from '@/domain/Project/GitHubProjectDetails';
|
||||||
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||||
import { Version } from '@/domain/Version';
|
import { Version } from '@/domain/Version';
|
||||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
import { ConstructorArguments } from '@/TypeHelpers';
|
import type { ConstructorArguments } from '@/TypeHelpers';
|
||||||
|
|
||||||
export function
|
export function
|
||||||
parseProjectDetails(
|
parseProjectDetails(
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { FunctionData } from '@/application/collections/';
|
import type { FunctionData } from '@/application/collections/';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
|
||||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
|
||||||
import { SyntaxFactory } from './Validation/Syntax/SyntaxFactory';
|
import { SyntaxFactory } from './Validation/Syntax/SyntaxFactory';
|
||||||
import { ISyntaxFactory } from './Validation/Syntax/ISyntaxFactory';
|
import type { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
|
||||||
import { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
|
import type { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
|
import type { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
import type { ISyntaxFactory } from './Validation/Syntax/ISyntaxFactory';
|
||||||
|
|
||||||
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||||
public readonly compiler: IScriptCompiler;
|
public readonly compiler: IScriptCompiler;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||||
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
|
||||||
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
|
||||||
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
import { IExpression } from './IExpression';
|
import { ExpressionEvaluationContext, type IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||||
import { ExpressionPosition } from './ExpressionPosition';
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
import { ExpressionEvaluationContext, IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
import type { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
import type { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||||
|
import type { IExpression } from './IExpression';
|
||||||
|
|
||||||
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||||
export class Expression implements IExpression {
|
export class Expression implements IExpression {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
|
||||||
import { IPipelineCompiler } from '../Pipes/IPipelineCompiler';
|
|
||||||
import { PipelineCompiler } from '../Pipes/PipelineCompiler';
|
import { PipelineCompiler } from '../Pipes/PipelineCompiler';
|
||||||
|
import type { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
import type { IPipelineCompiler } from '../Pipes/IPipelineCompiler';
|
||||||
|
|
||||||
export interface IExpressionEvaluationContext {
|
export interface IExpressionEvaluationContext {
|
||||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
|
||||||
import { ExpressionPosition } from './ExpressionPosition';
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
import type { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||||
|
import type { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||||
|
|
||||||
export interface IExpression {
|
export interface IExpression {
|
||||||
readonly position: ExpressionPosition;
|
readonly position: ExpressionPosition;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||||
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
|
||||||
import { IExpressionsCompiler } from './IExpressionsCompiler';
|
|
||||||
import { IExpression } from './Expression/IExpression';
|
|
||||||
import { IExpressionParser } from './Parser/IExpressionParser';
|
|
||||||
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
||||||
|
import type { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
import type { IExpressionsCompiler } from './IExpressionsCompiler';
|
||||||
|
import type { IExpression } from './Expression/IExpression';
|
||||||
|
import type { IExpressionParser } from './Parser/IExpressionParser';
|
||||||
|
|
||||||
export class ExpressionsCompiler implements IExpressionsCompiler {
|
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
import type { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
export interface IExpressionsCompiler {
|
export interface IExpressionsCompiler {
|
||||||
compileExpressions(
|
compileExpressions(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IExpression } from '../Expression/IExpression';
|
|
||||||
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
||||||
import { WithParser } from '../SyntaxParsers/WithParser';
|
import { WithParser } from '../SyntaxParsers/WithParser';
|
||||||
import { IExpressionParser } from './IExpressionParser';
|
import type { IExpression } from '../Expression/IExpression';
|
||||||
|
import type { IExpressionParser } from './IExpressionParser';
|
||||||
|
|
||||||
const Parsers = [
|
const Parsers = [
|
||||||
new ParameterSubstitutionParser(),
|
new ParameterSubstitutionParser(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IExpression } from '../Expression/IExpression';
|
import type { IExpression } from '../Expression/IExpression';
|
||||||
|
|
||||||
export interface IExpressionParser {
|
export interface IExpressionParser {
|
||||||
findExpressions(code: string): IExpression[];
|
findExpressions(code: string): IExpression[];
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IExpressionParser } from '../IExpressionParser';
|
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
|
||||||
import { IExpression } from '../../Expression/IExpression';
|
|
||||||
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
|
|
||||||
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
|
||||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||||
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
|
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
|
||||||
|
import type { IExpressionParser } from '../IExpressionParser';
|
||||||
|
import type { IExpression } from '../../Expression/IExpression';
|
||||||
|
import type { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
||||||
|
|
||||||
export abstract class RegexParser implements IExpressionParser {
|
export abstract class RegexParser implements IExpressionParser {
|
||||||
protected abstract readonly regex: RegExp;
|
protected abstract readonly regex: RegExp;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IPipe } from '../IPipe';
|
import type { IPipe } from '../IPipe';
|
||||||
|
|
||||||
export class EscapeDoubleQuotes implements IPipe {
|
export class EscapeDoubleQuotes implements IPipe {
|
||||||
public readonly name: string = 'escapeDoubleQuotes';
|
public readonly name: string = 'escapeDoubleQuotes';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IPipe } from '../IPipe';
|
import type { IPipe } from '../IPipe';
|
||||||
|
|
||||||
export class InlinePowerShell implements IPipe {
|
export class InlinePowerShell implements IPipe {
|
||||||
public readonly name: string = 'inlinePowerShell';
|
public readonly name: string = 'inlinePowerShell';
|
||||||
@@ -95,7 +95,7 @@ function getLines(code: string): string[] {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
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://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
|
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings
|
||||||
*/
|
*/
|
||||||
function mergeHereStrings(code: string) {
|
function mergeHereStrings(code: string) {
|
||||||
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IPipe } from './IPipe';
|
|
||||||
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
||||||
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
||||||
|
import type { IPipe } from './IPipe';
|
||||||
|
|
||||||
const RegisteredPipes = [
|
const RegisteredPipes = [
|
||||||
new EscapeDoubleQuotes(),
|
new EscapeDoubleQuotes(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IPipeFactory, PipeFactory } from './PipeFactory';
|
import { type IPipeFactory, PipeFactory } from './PipeFactory';
|
||||||
import { IPipelineCompiler } from './IPipelineCompiler';
|
import type { IPipelineCompiler } from './IPipelineCompiler';
|
||||||
|
|
||||||
export class PipelineCompiler implements IPipelineCompiler {
|
export class PipelineCompiler implements IPipelineCompiler {
|
||||||
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
import { RegexParser, type IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
export class ParameterSubstitutionParser extends RegexParser {
|
export class ParameterSubstitutionParser extends RegexParser {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// eslint-disable-next-line max-classes-per-file
|
// eslint-disable-next-line max-classes-per-file
|
||||||
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
|
import type { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
|
||||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
import { IExpression } from '../Expression/IExpression';
|
|
||||||
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
|
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
|
||||||
|
import type { IExpression } from '../Expression/IExpression';
|
||||||
|
|
||||||
export class WithParser implements IExpressionParser {
|
export class WithParser implements IExpressionParser {
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
|
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
|
||||||
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
import type { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
|
||||||
export class FunctionCallArgument implements IFunctionCallArgument {
|
export class FunctionCallArgument implements IFunctionCallArgument {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
import type { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
|
import type { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
|
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
|
||||||
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
import type { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
|
||||||
export interface IReadOnlyFunctionCallArgumentCollection {
|
export interface IReadOnlyFunctionCallArgumentCollection {
|
||||||
getArgument(parameterName: string): IFunctionCallArgument;
|
getArgument(parameterName: string): IFunctionCallArgument;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CompiledCode } from '../CompiledCode';
|
import type { CompiledCode } from '../CompiledCode';
|
||||||
|
|
||||||
export interface CodeSegmentMerger {
|
export interface CodeSegmentMerger {
|
||||||
mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode;
|
mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CompiledCode } from '../CompiledCode';
|
import type { CompiledCode } from '../CompiledCode';
|
||||||
import { CodeSegmentMerger } from './CodeSegmentMerger';
|
import type { CodeSegmentMerger } from './CodeSegmentMerger';
|
||||||
|
|
||||||
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
||||||
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
import type { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
import { FunctionCall } from '../FunctionCall';
|
import type { FunctionCall } from '../FunctionCall';
|
||||||
import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
|
import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
|
||||||
|
|
||||||
export interface FunctionCallCompilationContext {
|
export interface FunctionCallCompilationContext {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
import type { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
import { FunctionCall } from '../FunctionCall';
|
import type { CompiledCode } from './CompiledCode';
|
||||||
import { CompiledCode } from './CompiledCode';
|
import type { FunctionCall } from '../FunctionCall';
|
||||||
|
|
||||||
export interface FunctionCallCompiler {
|
export interface FunctionCallCompiler {
|
||||||
compileFunctionCalls(
|
compileFunctionCalls(
|
||||||
|
|||||||