Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd2082e8c5 | ||
|
|
8f188acd3c | ||
|
|
0303ef2fd9 | ||
|
|
cb21a970b6 | ||
|
|
203daeb4a2 | ||
|
|
60dde11311 | ||
|
|
8b930fc57c | ||
|
|
f810ed0c14 | ||
|
|
53222fd83c | ||
|
|
a1f2497381 | ||
|
|
c27172c32e | ||
|
|
6e9b65d8b1 | ||
|
|
6d301f9961 | ||
|
|
659fea7afc | ||
|
|
e0303058a3 | ||
|
|
65f121c451 | ||
|
|
821cc62c4c | ||
|
|
4ce327eb6a | ||
|
|
4beb1bb574 | ||
|
|
0a2a1a026b | ||
|
|
eb096d07e2 | ||
|
|
19e42c9c52 | ||
|
|
f4d86fccfd | ||
|
|
ad0576a752 | ||
|
|
35be05df20 | ||
|
|
dae6d114da | ||
|
|
ecce47fdcd | ||
|
|
e9e0001ef8 | ||
|
|
62f8bfac2f | ||
|
|
75c9b51bf2 | ||
|
|
ec98d8417f | ||
|
|
736590558b | ||
|
|
6e40edd3f8 | ||
|
|
5f11c8d98f | ||
|
|
08737698c2 | ||
|
|
04b3133500 | ||
|
|
0d15992d56 |
@@ -1 +0,0 @@
|
||||
dist/
|
||||
@@ -6,10 +6,10 @@ module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
es2022: true, // add globals and sets parserOptions.ecmaVersion to 2022
|
||||
},
|
||||
extends: [
|
||||
// Vue specific rules, eslint-plugin-vue
|
||||
// Added by Vue CLI
|
||||
'plugin:vue/essential',
|
||||
|
||||
// Extends eslint-config-airbnb
|
||||
@@ -17,42 +17,14 @@ module.exports = {
|
||||
|
||||
// Extends @typescript-eslint/recommended
|
||||
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||
// Added by Vue CLI
|
||||
'@vue/typescript/recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 12, // ECMA 2021
|
||||
/*
|
||||
Having 'latest' leads to:
|
||||
```
|
||||
Parsing error: ecmaVersion must be a number. Received value of type string instead
|
||||
```
|
||||
For .js files in the project
|
||||
*/
|
||||
},
|
||||
rules: {
|
||||
...getOwnRules(),
|
||||
...getTurnedOffBrokenRules(),
|
||||
...getOpinionatedRuleOverrides(),
|
||||
...getTodoRules(),
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/*.{j,t}s?(x)',
|
||||
'**/tests/unit/**/*.spec.{j,t}s?(x)',
|
||||
],
|
||||
env: {
|
||||
mocha: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/tests/**/*.{j,t}s?(x)'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function getOwnRules() {
|
||||
|
||||
11
.github/actions/npm-install-dependencies/action.yml
vendored
Normal file
11
.github/actions/npm-install-dependencies/action.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
inputs:
|
||||
working-directory:
|
||||
required: false
|
||||
default: '.'
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
-
|
||||
name: Run `npm ci` with retries
|
||||
shell: bash
|
||||
run: npm run install-deps -- --ci --root-directory "${{ inputs.working-directory }}"
|
||||
57
.github/workflows/checks.build.yaml
vendored
57
.github/workflows/checks.build.yaml
vendored
@@ -9,7 +9,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
mode: [ development, test, production ]
|
||||
mode: [
|
||||
# Vite mode: https://vitejs.dev/guide/env-and-mode.html
|
||||
development, # Used by `dev` command
|
||||
production, # Used by `build` command
|
||||
# Vitest mode: https://vitest.dev/guide/cli.html
|
||||
test, # Used by Vitest
|
||||
]
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
@@ -21,17 +27,23 @@ jobs:
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Build
|
||||
name: Build web
|
||||
run: npm run build -- --mode ${{ matrix.mode }}
|
||||
-
|
||||
name: Verify web build artifacts
|
||||
run: npm run check:verify-build-artifacts -- --web
|
||||
|
||||
# A new job is used due to environments/modes different from Vue CLI, https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
|
||||
build-desktop:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
mode: [ development, production ] # "test" is not supported https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1627
|
||||
mode: [
|
||||
# electron-vite modes: https://electron-vite.org/guide/env-and-mode.html#global-env-variables
|
||||
development, # Used by `dev` command
|
||||
production, # Used by `build` and `preview` commands
|
||||
]
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
@@ -43,33 +55,16 @@ jobs:
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Install cross-env
|
||||
# Used to set NODE_ENV due to https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
|
||||
run: npm install --global cross-env
|
||||
name: Prebuild desktop
|
||||
run: npm run electron:prebuild -- --mode ${{ matrix.mode }}
|
||||
-
|
||||
name: Build
|
||||
run: |-
|
||||
cross-env-shell NODE_ENV=${{ matrix.mode }}
|
||||
npm run electron:build -- --publish never --mode ${{ matrix.mode }}
|
||||
|
||||
create-icons:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
name: Verify unbundled desktop build artifacts
|
||||
run: npm run check:verify-build-artifacts -- --electron-unbundled
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
name: Build (bundle and package) desktop application
|
||||
run: npm run electron:build -- --publish never
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
-
|
||||
name: Create icons
|
||||
run: npm run create-icons
|
||||
name: Verify bundled desktop build artifacts
|
||||
run: npm run check:verify-build-artifacts -- --electron-bundled
|
||||
|
||||
72
.github/workflows/checks.desktop-runtime-errors.yaml
vendored
Normal file
72
.github/workflows/checks.desktop-runtime-errors.yaml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: checks.desktop-runtime-errors
|
||||
# Verifies desktop builds for Electron applications across multiple OS platforms (macOS ,Ubuntu, and Windows).
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
run-check:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Configure Ubuntu
|
||||
if: matrix.os == 'ubuntu'
|
||||
shell: bash
|
||||
run: |-
|
||||
sudo apt update
|
||||
|
||||
# Configure AppImage dependencies
|
||||
sudo apt install -y libfuse2
|
||||
|
||||
# Configure DBUS (fixes `Failed to connect to the bus: Could not parse server address: Unknown address type`)
|
||||
if ! command -v 'dbus-launch' &> /dev/null; then
|
||||
echo 'DBUS does not exist, installing...'
|
||||
sudo apt install -y dbus-x11 # Gives both dbus and dbus-launch utility
|
||||
fi
|
||||
sudo systemctl start dbus
|
||||
DBUS_LAUNCH_OUTPUT=$(dbus-launch)
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "${DBUS_LAUNCH_OUTPUT}" >> $GITHUB_ENV
|
||||
else
|
||||
echo 'Error: dbus-launch command did not execute successfully. Exiting.' >&2
|
||||
echo "${DBUS_LAUNCH_OUTPUT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configure fake (virtual) display
|
||||
sudo apt install -y xvfb
|
||||
sudo Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
echo "DISPLAY=:99" >> $GITHUB_ENV
|
||||
|
||||
# Install ImageMagick for screenshots
|
||||
sudo apt install -y imagemagick
|
||||
|
||||
# Install xdotool and xprop (from x11-utils) for window title capturing
|
||||
sudo apt install -y xdotool x11-utils
|
||||
-
|
||||
name: Test
|
||||
shell: bash
|
||||
run: |-
|
||||
export SCREENSHOT=true
|
||||
npm run check:desktop
|
||||
-
|
||||
name: Upload screenshot
|
||||
if: always() # Run even if previous step fails
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: screenshot-${{ matrix.os }}
|
||||
path: screenshot.png
|
||||
22
.github/workflows/checks.external-urls.yaml
vendored
Normal file
22
.github/workflows/checks.external-urls.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: checks.external-urls
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||
|
||||
jobs:
|
||||
run-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Test
|
||||
run: npm run check:external-urls
|
||||
14
.github/workflows/checks.quality.yaml
vendored
14
.github/workflows/checks.quality.yaml
vendored
@@ -16,11 +16,15 @@ jobs:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Still interested to see results from other combinations
|
||||
steps:
|
||||
- name: Checkout
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Lint
|
||||
run: ${{ matrix.lint-command }}
|
||||
|
||||
55
.github/workflows/checks.scripts.yaml
vendored
Normal file
55
.github/workflows/checks.scripts.yaml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: checks.scripts
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
icons-build:
|
||||
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@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Create icons
|
||||
run: npm run icons:build
|
||||
|
||||
install-deps:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
matrix:
|
||||
install-deps-before: [true, false]
|
||||
install-command:
|
||||
- npm run install-deps
|
||||
- npm run install-deps -- --no-errors
|
||||
- npm run install-deps -- --ci
|
||||
- npm run install-deps -- --fresh --non-deterministic
|
||||
- npm run install-deps -- --fresh
|
||||
- npm run install-deps -- --non-deterministic
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Still interested to see results from other combinations
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
if: matrix.install-deps-before == true
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run install-deps
|
||||
run: ${{ matrix.install-command }}
|
||||
25
.github/workflows/release.desktop.yaml
vendored
25
.github/workflows/release.desktop.yaml
vendored
@@ -13,20 +13,29 @@ jobs:
|
||||
fail-fast: false # So publish runs for other OSes if one fails
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
-
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master # otherwise it defaults to the version tag missing bump commit
|
||||
fetch-depth: 0 # fetch all history
|
||||
- name: Checkout to bump commit
|
||||
-
|
||||
name: Checkout to bump commit
|
||||
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
||||
- name: Setup node
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run unit tests
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run unit tests
|
||||
run: npm run test:unit
|
||||
- name: Publish desktop app
|
||||
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
||||
-
|
||||
name: Prebuild
|
||||
run: npm run electron:prebuild
|
||||
-
|
||||
name: Build and publish
|
||||
run: npm run electron:build -- --publish always
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074
|
||||
|
||||
17
.github/workflows/release.site.yaml
vendored
17
.github/workflows/release.site.yaml
vendored
@@ -84,8 +84,9 @@ jobs:
|
||||
uses: ./app/.github/actions/setup-node
|
||||
-
|
||||
name: "App: Install dependencies"
|
||||
run: npm ci
|
||||
working-directory: app
|
||||
uses: ./app/.github/actions/npm-install-dependencies
|
||||
with:
|
||||
working-directory: app
|
||||
-
|
||||
name: "App: Run unit tests"
|
||||
run: npm run test:unit
|
||||
@@ -94,11 +95,21 @@ jobs:
|
||||
name: "App: Build"
|
||||
run: npm run build
|
||||
working-directory: app
|
||||
-
|
||||
name: "App: Verify web build artifacts"
|
||||
run: npm run check:verify-build-artifacts -- --web
|
||||
working-directory: app
|
||||
-
|
||||
name: "App: Deploy to S3"
|
||||
shell: bash
|
||||
run: >-
|
||||
declare web_output_dir
|
||||
if ! web_output_dir=$(cd app && node scripts/print-dist-dir.js --web); then
|
||||
echo 'Error: Could not determine distribution directory.'
|
||||
exit 1
|
||||
fi
|
||||
bash "aws/scripts/deploy/deploy-to-s3.sh" \
|
||||
--folder app/dist \
|
||||
--folder "${web_output_dir}" \
|
||||
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
|
||||
--storage-class ONEZONE_IA \
|
||||
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \
|
||||
|
||||
4
.github/workflows/tests.e2e.yaml
vendored
4
.github/workflows/tests.e2e.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run e2e tests
|
||||
run: npm run test:e2e -- --headless
|
||||
run: npm run test:cy:run
|
||||
|
||||
2
.github/workflows/tests.integration.yaml
vendored
2
.github/workflows/tests.integration.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run integration tests
|
||||
run: npm run test:integration
|
||||
|
||||
2
.github/workflows/tests.unit.yaml
vendored
2
.github/workflows/tests.unit.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run unit tests
|
||||
run: npm run test:unit
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,10 +1,5 @@
|
||||
node_modules
|
||||
dist/
|
||||
/dist-*/
|
||||
.vs
|
||||
.vscode/**/*
|
||||
!.vscode/extensions.json
|
||||
#Electron-builder output
|
||||
/dist_electron
|
||||
# Cypress
|
||||
/tests/e2e/screenshots
|
||||
/tests/e2e/videos
|
||||
!.vscode/extensions.json
|
||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -11,8 +11,8 @@
|
||||
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
|
||||
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.
|
||||
// Vue
|
||||
"jcbuisson.vue", // Highlights syntax.
|
||||
"octref.vetur", // Adds Vetur, Vue tooling support.
|
||||
"Vue.volar", // Official Vue extensions
|
||||
"Vue.vscode-typescript-vue-plugin", // Official TypeScript Vue Plugin
|
||||
// Scripting
|
||||
"timonwong.shellcheck", // Lints bash files.
|
||||
"ms-vscode.powershell", // Lints PowerShell files.
|
||||
|
||||
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,5 +1,52 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.3 (2023-09-09)
|
||||
|
||||
* linux: use user.js over prefs.js for Firefox #232 | [dae6d11](https://github.com/undergroundwires/privacy.sexy/commit/dae6d114daab6857d773071211eb57619b136281)
|
||||
* win: fix typo in Defender retention script #213 | [35be05d](https://github.com/undergroundwires/privacy.sexy/commit/35be05df2094ea8bba4ee4725e6fa4956a79493d)
|
||||
* Improve desktop runtime execution tests | [ad0576a](https://github.com/undergroundwires/privacy.sexy/commit/ad0576a752f8fd6ea2f917a59173fe61f9951246)
|
||||
* Fix Windows artifact naming in desktop packaging | [f4d86fc](https://github.com/undergroundwires/privacy.sexy/commit/f4d86fccfd0e73e94c8c6e400a33514900bc5abe)
|
||||
* Refactor and improve external URL checks | [19e42c9](https://github.com/undergroundwires/privacy.sexy/commit/19e42c9c52a18c813ded4265e687e01032cdd4c8)
|
||||
* Fix memory leaks via auto-unsubscribing and DI | [eb096d0](https://github.com/undergroundwires/privacy.sexy/commit/eb096d07e276e1b4c8040220c47f186d02841e14)
|
||||
* Refactor build configs and improve CI/CD checks | [0a2a1a0](https://github.com/undergroundwires/privacy.sexy/commit/0a2a1a026b0efb29624be82b06536c518c1ea439)
|
||||
* Introduce retry mechanism for npm install in CI/CD | [4beb1bb](https://github.com/undergroundwires/privacy.sexy/commit/4beb1bb5748a60886210187ca3cdc7f4b41067c0)
|
||||
* win: fix disable recent apps revert #211, #248 | [4ce327e](https://github.com/undergroundwires/privacy.sexy/commit/4ce327eb6af542ed2916d649553e5e1ba5833882)
|
||||
* Change license to AGPLv3 | [821cc62](https://github.com/undergroundwires/privacy.sexy/commit/821cc62c4c8347cb76d041f82f574754e4d948c5)
|
||||
* Introduce new TreeView UI component | [65f121c](https://github.com/undergroundwires/privacy.sexy/commit/65f121c451af87315e1c91df4198562e0445b2c2)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.2...0.12.3)
|
||||
|
||||
## 0.12.2 (2023-08-25)
|
||||
|
||||
* Add automated checks for desktop app runtime #233 | [04b3133](https://github.com/undergroundwires/privacy.sexy/commit/04b3133500485d0d278a81a177a1677134131405)
|
||||
* win: fix automatic updates revert #234 | [0873769](https://github.com/undergroundwires/privacy.sexy/commit/08737698c2283bdf535d1611a730031ebfc7c0df)
|
||||
* Migrate unit/integration tests to Vitest with Vite | [5f11c8d](https://github.com/undergroundwires/privacy.sexy/commit/5f11c8d98f782dd7c77f27649a1685fb7bd06e13)
|
||||
* Remove Vue ESLint plugin for Vite compatibility | [6e40edd](https://github.com/undergroundwires/privacy.sexy/commit/6e40edd3f8a063c1b7482c27d8368e14c2fbcfbf)
|
||||
* Migrate web builds from Vue CLI to Vite | [7365905](https://github.com/undergroundwires/privacy.sexy/commit/736590558be51a09435bb87e78b6655e8533bc2e)
|
||||
* Migrate Cypress (E2E) tests to Vite and TypeScript | [ec98d84](https://github.com/undergroundwires/privacy.sexy/commit/ec98d8417f779fa818ccdda6bb90f521e1738002)
|
||||
* Migrate to `electron-vite` and `electron-builder` | [75c9b51](https://github.com/undergroundwires/privacy.sexy/commit/75c9b51bf2d1dc7269adfd7b5ed71acfb5031299)
|
||||
* Fix searching/filtering bugs #235 | [62f8bfa](https://github.com/undergroundwires/privacy.sexy/commit/62f8bfac2f481c93598fe19a51594769f522d684)
|
||||
* Improve desktop security by isolating Electron | [e9e0001](https://github.com/undergroundwires/privacy.sexy/commit/e9e0001ef845fa6935c59a4e20a89aac9e71756a)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.1...0.12.2)
|
||||
|
||||
## 0.12.1 (2023-08-17)
|
||||
|
||||
* Transition to eslint-config-airbnb-with-typescript | [ff84f56](https://github.com/undergroundwires/privacy.sexy/commit/ff84f5676e496dd7ec5b3599e34ec9627d181ea2)
|
||||
* Improve user privacy with secure outbound links | [3a594ac](https://github.com/undergroundwires/privacy.sexy/commit/3a594ac7fd708dc1e98155ffb9b21acd4e1fcf2d)
|
||||
* Refactor Vue components using Composition API #230 | [1b9be8f](https://github.com/undergroundwires/privacy.sexy/commit/1b9be8fe2d72d8fb5cf1fed6dcc0b9777171aa98)
|
||||
* Fix failing security tests | [3bc8da4](https://github.com/undergroundwires/privacy.sexy/commit/3bc8da4cbf1e2bd758dc3fffe4b1e62dc3beb7b3)
|
||||
* Improve Defender scripts #201 | [061afad](https://github.com/undergroundwires/privacy.sexy/commit/061afad9673a41454c2421c318898f2b4f4cf504)
|
||||
* Fix failing tests due to failed error logging | [986ba07](https://github.com/undergroundwires/privacy.sexy/commit/986ba078a643de6acbee50fff9cf77494ca7ea7f)
|
||||
* Implement custom lightweight modal #230 | [9e5491f](https://github.com/undergroundwires/privacy.sexy/commit/9e5491fdbf2d9d40d974f5ad0e879a6d5c6d1e55)
|
||||
* Refactor usage of tooltips for flexibility | [bc91237](https://github.com/undergroundwires/privacy.sexy/commit/bc91237d7c54bdcd15c5c39a55def50d172bb659)
|
||||
* Fix revert toggle partial rendering | [39e650c](https://github.com/undergroundwires/privacy.sexy/commit/39e650cf110bee6b1b21d9b2902b36b0e2568d54)
|
||||
* Increase testability through dependency injection | [ae75059](https://github.com/undergroundwires/privacy.sexy/commit/ae75059cc14db41f55dd2056f528442c7d319dd2)
|
||||
* Refactor filter (search query) event handling | [6a20d80](https://github.com/undergroundwires/privacy.sexy/commit/6a20d804dc365d22c1248d787f9912271f508eeb)
|
||||
* Migrate to ES6 modules | [a14929a](https://github.com/undergroundwires/privacy.sexy/commit/a14929a13cc6260b514692d9b4f1cdf5fb85d8b2)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.0...0.12.1)
|
||||
|
||||
## 0.12.0 (2023-08-03)
|
||||
|
||||
* Improve script/category name validation | [b210aad](https://github.com/undergroundwires/privacy.sexy/commit/b210aaddf26629179f77fe19f62f65d8a0ca2b87)
|
||||
|
||||
141
LICENSE
141
LICENSE
@@ -1,5 +1,5 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@@ -7,17 +7,15 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
20
README.md
20
README.md
@@ -70,6 +70,24 @@
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.desktop-runtime-errors.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Status of runtime error checks for the desktop application"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.desktop-runtime-errors/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.scripts.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Status of script checks"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.scripts/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.external-urls.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Status of external URL checks"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.external-urls/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<!-- Release -->
|
||||
<br />
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml" target="_blank" rel="noopener noreferrer">
|
||||
@@ -104,7 +122,7 @@
|
||||
## Get started
|
||||
|
||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||
- 🖥️ **Offline**: Check [releases page](https://github.com/undergroundwires/privacy.sexy/releases), or download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-Setup-0.11.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.dmg), [Linux](https://github.com/undergroundwires/pr.vacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.AppImage).
|
||||
- 🖥️ **Offline**: Check [releases page](https://github.com/undergroundwires/privacy.sexy/releases), or download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.3/privacy.sexy-Setup-0.12.3.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.3/privacy.sexy-0.12.3.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.3/privacy.sexy-0.12.3.AppImage).
|
||||
|
||||
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset',
|
||||
],
|
||||
};
|
||||
@@ -1,15 +1,15 @@
|
||||
import { defineConfig } from 'cypress';
|
||||
import setupPlugins from './tests/e2e/plugins/index.js';
|
||||
import ViteConfig from './vite.config';
|
||||
|
||||
const CYPRESS_BASE_DIR = 'tests/e2e/';
|
||||
|
||||
export default defineConfig({
|
||||
fixturesFolder: 'tests/e2e/fixtures',
|
||||
screenshotsFolder: 'tests/e2e/screenshots',
|
||||
videosFolder: 'tests/e2e/videos',
|
||||
fixturesFolder: `${CYPRESS_BASE_DIR}/fixtures`,
|
||||
screenshotsFolder: `${CYPRESS_BASE_DIR}/screenshots`,
|
||||
videosFolder: `${CYPRESS_BASE_DIR}/videos`,
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
return setupPlugins(on, config);
|
||||
},
|
||||
specPattern: 'tests/e2e/specs/**/*.cy.{js,jsx,ts,tsx}',
|
||||
supportFile: 'tests/e2e/support/index.js',
|
||||
baseUrl: `http://localhost:${ViteConfig.server.port}/`,
|
||||
specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
|
||||
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
|
||||
},
|
||||
});
|
||||
|
||||
5
dist-dirs.json
Normal file
5
dist-dirs.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"electronUnbundled": "dist-electron-unbundled",
|
||||
"electronBundled": "dist-electron-bundled",
|
||||
"web": "dist-web"
|
||||
}
|
||||
@@ -35,7 +35,7 @@ Application layer enables [data-driven programming](https://en.wikipedia.org/wik
|
||||
|
||||
Application layer parses the application data to compile the domain object [`Application.ts`](./../src/domain/Application.ts).
|
||||
|
||||
A webpack loader loads (or injects) application data ([collection yaml files](./../src/application/collections/)) into the application layer in compile time. Application layer ([`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)) parses and compiles this data in runtime.
|
||||
The build tool loads (or injects) application data ([collection yaml files](./../src/application/collections/)) into the application layer in compile time. Application layer ([`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)) parses and compiles this data in runtime.
|
||||
|
||||
Application layer compiles templating syntax during parsing to create the end scripts. You can read more about templating syntax in [templating.md](./templating.md) and how application data uses them through functions in [collection-files.md | Function](./collection-files.md#function).
|
||||
|
||||
|
||||
@@ -15,11 +15,23 @@ Application is
|
||||
|
||||
Application uses highly decoupled models & services in different DDD layers:
|
||||
|
||||
- presentation layer (see [presentation.md](./presentation.md)),
|
||||
- application layer (see [application.md](./application.md)),
|
||||
- and domain layer.
|
||||
**Application layer** (see [application.md](./application.md)):
|
||||
|
||||
Application layer depends on and consumes domain layer. [Presentation layer](./presentation.md) consumes and depends on application layer along with domain layer. Application and presentation layers can communicate through domain model.
|
||||
- Coordinates application activities and consumes the domain layer.
|
||||
|
||||
**Presentation layer** (see [presentation.md](./presentation.md)):
|
||||
|
||||
- Handles UI/UX, consumes both the application and domain layers.
|
||||
- May communicate directly with the infrastructure layer for technical needs, but avoids domain logic.
|
||||
|
||||
**Domain layer**:
|
||||
|
||||
- Serves as the system's core and central truth.
|
||||
- Facilitates communication between the application and presentation layers through the domain model.
|
||||
|
||||
**Infrastructure layer**:
|
||||
|
||||
- Manages technical implementations without dependencies on other layers or domain knowledge.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -5,22 +5,28 @@ Before your commit, a good practice is to:
|
||||
1. [Run unit tests](#testing)
|
||||
2. [Lint your code](#linting)
|
||||
|
||||
You could run other types of tests as well, but they may take longer time and overkill for your changes. Automated actions executes the tests for a pull request or change in the main branch. See [ci-cd.md](./ci-cd.md) for more information.
|
||||
You could run other types of tests as well, but they may take longer time and overkill for your changes.
|
||||
Automated actions are set up to execute these tests as necessary.
|
||||
See [ci-cd.md](./ci-cd.md) for more information.
|
||||
|
||||
## Commands
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Install node >15.x.
|
||||
- Install dependencies using `npm install`.
|
||||
- Install Node >16.x.
|
||||
- Install dependencies using `npm install` (or [`npm run install-deps`](#utility-scripts) for more options).
|
||||
|
||||
### Testing
|
||||
|
||||
- Run unit tests: `npm run test:unit`
|
||||
- Run integration tests: `npm run test:integration`
|
||||
- Run e2e (end-to-end) tests
|
||||
- Interactive mode with GUI: `npm run test:e2e`
|
||||
- Headless mode without GUI: `npm run test:e2e -- --headless`
|
||||
- Run end-to-end (e2e) tests:
|
||||
- `npm run test:cy:open`: Run tests interactively using the development server with hot-reloading.
|
||||
- `npm run test:cy:run`: Run tests on the production build in a headless mode.
|
||||
- Run checks:
|
||||
- `npm run check:desktop`: Run runtime checks for packaged desktop applications ([README.md](./../tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/README.md)).
|
||||
- You can set environment variables active its flags such as `BUILD=true SCREENSHOT=true npm run check:desktop`
|
||||
- `npm run check:external-urls`: Test whether external URLs used in applications are alive.
|
||||
|
||||
📖 Read more about testing in [tests](./tests.md).
|
||||
|
||||
@@ -35,11 +41,25 @@ You could run other types of tests as well, but they may take longer time and ov
|
||||
|
||||
### Running
|
||||
|
||||
- Run in local server: `npm run serve`
|
||||
**Web:**
|
||||
|
||||
- Run in local server: `npm run dev`
|
||||
- 💡 Meant for local development with features such as hot-reloading.
|
||||
- Run using Docker:
|
||||
1. Build: `docker build -t undergroundwires/privacy.sexy:latest .`
|
||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest`
|
||||
- Preview production build: `npm run preview`
|
||||
- Start a local web server that serves the built solution from `./dist`.
|
||||
- 💡 Run `npm run build` before `npm run preview`.
|
||||
|
||||
**Desktop apps:**
|
||||
|
||||
- `npm run electron:dev`: The command will build the main process and preload scripts source code, and start a dev server for the renderer, and start the Electron app.
|
||||
- `npm run electron:preview`: The command will build the main process, preload scripts and renderer source code, and start the Electron app to preview.
|
||||
- `npm run electron:prebuild`: The command will build the main process, preload scripts and renderer source code. Usually before packaging the Electron application, you need to execute this command.
|
||||
- `npm run electron:build`: Prebuilds the Electron application, packages and publishes it through `electron-builder`.
|
||||
|
||||
**Docker:**
|
||||
|
||||
1. Build: `docker build -t undergroundwires/privacy.sexy:latest .`
|
||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest`
|
||||
|
||||
### Building
|
||||
|
||||
@@ -47,13 +67,26 @@ You could run other types of tests as well, but they may take longer time and ov
|
||||
- Build desktop application: `npm run electron:build`
|
||||
- (Re)create icons (see [documentation](../img/README.md)): `npm run create-icons`
|
||||
|
||||
### Utility Scripts
|
||||
### Scripts
|
||||
|
||||
- Run fresh NPM install: [`./scripts/fresh-npm-install.sh`](../scripts/fresh-npm-install.sh)
|
||||
- This script provides a clean NPM install, removing existing node modules and optionally the package-lock.json (when run with -n), then installs dependencies and runs unit tests.
|
||||
- Configure VSCode: [`./scripts/configure-vscode.sh`](../scripts/configure-vscode.sh)
|
||||
📖 For detailed options and behavior for any of the following scripts, please refer to the script file itself.
|
||||
|
||||
#### Utility scripts
|
||||
|
||||
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
|
||||
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
|
||||
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
|
||||
- [**`./scripts/configure-vscode.sh`**](../scripts/configure-vscode.sh):
|
||||
- This script checks and sets the necessary configurations for VSCode in `settings.json` file.
|
||||
|
||||
#### Automation scripts
|
||||
|
||||
- [**`node scripts/print-dist-dir.js [-- <options>]`**](../scripts/print-dist-dir.js):
|
||||
- Determines the absolute path of a distribution directory based on CLI arguments and outputs its absolute path.
|
||||
- Primarily used by automation scripts.
|
||||
- [**`npm run check:verify-build-artifacts [-- <options>]`**](../scripts/verify-build-artifacts.js):
|
||||
- Verifies the existence and content of build artifacts. Useful for ensuring that the build process is generating the expected output.
|
||||
|
||||
## Recommended extensions
|
||||
|
||||
You should use EditorConfig to follow project style.
|
||||
|
||||
@@ -10,24 +10,24 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
||||
|
||||
## Structure
|
||||
|
||||
- [`/src/` **`presentation/`**](./../src/presentation/): Contains all presentation related code including Vue and Electron configurations
|
||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
|
||||
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
|
||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that other components share.
|
||||
- [**`hooks`**](../src/presentation/components/Shared/Hooks): Shared hooks for state access
|
||||
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that webpack will process.
|
||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
|
||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
|
||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains reusable styles coupled to a Vue/HTML component.
|
||||
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles that override third-party components used.
|
||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Primary Sass file, passes along all other styles, should be the single file used from other components.
|
||||
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
|
||||
- [**`electron/`**](./../src/presentation/electron/): Electron configuration for the desktop application.
|
||||
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
|
||||
- [**`/public/`**](./../public/): Contains static assets that are directly copied and do not go through webpack.
|
||||
- [**`/vue.config.cjs`**](./../vue.config.cjs): Global Vue CLI configurations loaded by `@vue/cli-service`.
|
||||
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): PostCSS configurations used by Vue CLI internally.
|
||||
- [**`/babel.config.cjs`**](./../babel.config.cjs): Babel configurations for polyfills used by `@vue/cli-plugin-babel`.
|
||||
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code.
|
||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
||||
- [**`components/`**](./../src/presentation/components/): Contains 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).
|
||||
- [**`/public/`**](../src/presentation/public/): Contains static assets.
|
||||
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
|
||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
||||
- [**`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.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
||||
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||
- [**`/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.
|
||||
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
||||
|
||||
## Visual design best-practices
|
||||
|
||||
@@ -86,7 +86,18 @@ Shared components include:
|
||||
- [ModalDialog.vue](./../src/presentation/components/Shared/Modal/ModalDialog.vue) is utilized for rendering modal windows.
|
||||
- [TooltipWrapper.vue](./../src/presentation/components/Shared/TooltipWrapper.vue) acts as a wrapper for rendering tooltips.
|
||||
|
||||
## Sass naming convention
|
||||
## Desktop builds
|
||||
|
||||
Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages.
|
||||
|
||||
## Styles
|
||||
|
||||
### Style location
|
||||
|
||||
- **Global styles**: The [`assets/styles/`](#structure) directory is reserved for styles that have a broader scope, affecting multiple components or entire layouts. They are generic and should not be tightly coupled to a specific component's functionality.
|
||||
- **Component-specific styles**: Styles closely tied to a particular component's functionality or appearance should reside near the component they are used by. This makes it easier to locate and modify styles when working on a specific component.
|
||||
|
||||
### Sass naming convention
|
||||
|
||||
- Use lowercase for variables/functions/mixins, e.g.:
|
||||
- Variable: `$variable: value;`
|
||||
|
||||
123
docs/tests.md
123
docs/tests.md
@@ -5,78 +5,79 @@ There are different types of tests executed:
|
||||
1. [Unit tests](#unit-tests)
|
||||
2. [Integration tests](#integration-tests)
|
||||
3. [End-to-end (E2E) tests](#e2e-tests)
|
||||
4. [Automated checks](#automated-checks)
|
||||
|
||||
Common aspects for all tests:
|
||||
## Unit and integration tests
|
||||
|
||||
- They use [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/).
|
||||
- Their files end with `.spec.{ts|js}` suffix.
|
||||
|
||||
💡 You can use path/module alias `@/tests` in import statements.
|
||||
|
||||
## Unit tests
|
||||
|
||||
- Unit tests test each component in isolation.
|
||||
- All unit tests goes under [`./tests/unit`](./../tests/unit).
|
||||
- They rely on [stubs](./../tests/unit/shared/Stubs) for isolation.
|
||||
- Unit tests include also Vue component tests using `@vue/test-utils`.
|
||||
|
||||
### Unit tests structure
|
||||
|
||||
- [`./src/`](./../src/)
|
||||
- Includes source code that unit tests will test.
|
||||
- [`./tests/unit/`](./../tests/unit/)
|
||||
- Includes test code.
|
||||
- Tests follow same folder structure as [`./src/`](./../src).
|
||||
- E.g. if system under test lies in [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) then its tests would be in test would be at [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
||||
- [`shared/`](./../tests/unit/shared/)
|
||||
- Includes common functionality that's shared across unit tests.
|
||||
- [`Assertions/`](./../tests/unit/shared/Assertions):
|
||||
- Common assertions that extend [Chai Assertion Library](https://www.chaijs.com/).
|
||||
- Asserting functions should start with `expect` prefix.
|
||||
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
||||
- Shared test cases.
|
||||
- Functions that calls `it()` from [Mocha test framework](https://mochajs.org/) should have `it` prefix.
|
||||
- E.g. `itEachAbsentCollectionValue()`.
|
||||
- [`Stubs/`](./../tests/unit/shared/Stubs)
|
||||
- Includes stubs to be able to test components in isolation.
|
||||
- Stubs have minimal and dummy behavior to be functional, they may also have spying or mocking functions.
|
||||
|
||||
### Unit tests naming
|
||||
|
||||
- Each test suite first describe the system under test.
|
||||
- E.g. tests for class `Application.ts` are all inside `Application.spec.ts`.
|
||||
- `describe` blocks tests for same function (if applicable).
|
||||
- E.g. test for `run()` are inside `describe('run', () => ..)`.
|
||||
- They utilize [Vitest](https://vitest.dev/).
|
||||
- Test files are suffixed with `.spec.ts`.
|
||||
|
||||
### Act, arrange, assert
|
||||
|
||||
- Tests use act, arrange and assert (AAA) pattern when applicable.
|
||||
- Tests implement the act, arrange, and assert (AAA) pattern.
|
||||
- **Arrange**
|
||||
- Sets up the test case.
|
||||
- Starts with comment line `// arrange`.
|
||||
- Sets up the test scenario and environment.
|
||||
- Begins with comment line `// arrange`.
|
||||
- **Act**
|
||||
- Executes the actual test.
|
||||
- Starts with comment line `// act`.
|
||||
- Begins with comment line `// act`.
|
||||
- **Assert**
|
||||
- Elicit some sort of expectation.
|
||||
- Starts with comment line `// assert`.
|
||||
- Sets an expectation for the test's outcome.
|
||||
- Begins with comment line `// assert`.
|
||||
|
||||
## Integration tests
|
||||
### Unit tests
|
||||
|
||||
- Tests functionality of a component in combination with others (not isolated).
|
||||
- Ensure dependencies to third parties work as expected.
|
||||
- Defined in [./tests/integration](./../tests/integration).
|
||||
- Evaluate individual components in isolation.
|
||||
- Located in [`./tests/unit`](./../tests/unit).
|
||||
- Achieve isolation using [stubs](./../tests/unit/shared/Stubs).
|
||||
- Include Vue component tests, enabled by `@vue/test-utils`.
|
||||
|
||||
#### Unit tests naming
|
||||
|
||||
- Test suites start with a description of the component or system under test.
|
||||
- E.g., tests for `Application.ts` are contained in `Application.spec.ts`.
|
||||
- Whenever possible, `describe` blocks group tests of the same function.
|
||||
- E.g., tests for `run()` are inside `describe('run', () => ...)`.
|
||||
|
||||
### Integration tests
|
||||
|
||||
- Assess the combined functionality of components.
|
||||
- They verify that third-party dependencies function as anticipated.
|
||||
|
||||
## E2E tests
|
||||
|
||||
- Test the functionality and performance of a running application.
|
||||
- Vue CLI plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) configures E2E tests.
|
||||
- Test names and folders have logical structure based on tests executed.
|
||||
- The structure is following:
|
||||
- [`cypress.config.ts`](./../cypress.config.ts): Cypress configuration file.
|
||||
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
|
||||
- [`/specs/`](./../tests/e2e/specs/): Test files named with `.spec.js` extension.
|
||||
- [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before loading project.
|
||||
- [`/support/index.js`](./../tests/e2e/support/index.js): Support file, runs before every single spec file.
|
||||
- *(Ignored)* `/videos`: Asset folder for videos taken during tests.
|
||||
- *(Ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
||||
- Examine the live web application's functionality and performance.
|
||||
- Uses Cypress to run the tests.
|
||||
|
||||
## Automated checks
|
||||
|
||||
These checks validate various qualities like runtime execution, building process, security testing, etc.
|
||||
|
||||
- Use [various tools](./../package.json) and [scripts](./../scripts).
|
||||
- Are automatically executed as [GitHub workflows](./../.github/workflows).
|
||||
|
||||
## Tests structure
|
||||
|
||||
- [`package.json`](./../package.json): Defines test commands and includes tools used in tests.
|
||||
- [`vite.config.ts`](./../vite.config.ts): Configures `vitest` for unit and integration tests.
|
||||
- [`./src/`](./../src/): Contains the code subject to testing.
|
||||
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
|
||||
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
|
||||
- [`./tests/unit/`](./../tests/unit/)
|
||||
- Stores unit test code.
|
||||
- The directory structure mirrors [`./src/`](./../src).
|
||||
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
||||
- [`shared/`](./../tests/unit/shared/)
|
||||
- Contains shared unit test functionalities.
|
||||
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
|
||||
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
||||
- Shared test cases.
|
||||
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
|
||||
- [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities.
|
||||
- [`./tests/integration/`](./../tests/integration/): Contains integration test files.
|
||||
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
|
||||
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension.
|
||||
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single spec file.
|
||||
- [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation.
|
||||
- *(git ignored)* `/videos`: Asset folder for videos taken during tests.
|
||||
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
||||
|
||||
43
electron-builder.cjs
Normal file
43
electron-builder.cjs
Normal file
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
|
||||
const { join } = require('path');
|
||||
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
||||
|
||||
module.exports = {
|
||||
// Common options
|
||||
publish: {
|
||||
provider: 'github',
|
||||
vPrefixedTagName: false, // default: true
|
||||
releaseType: 'release', // default: draft
|
||||
},
|
||||
directories: {
|
||||
output: electronBundled,
|
||||
},
|
||||
extraMetadata: {
|
||||
main: join(electronUnbundled, 'main/index.cjs'), // do not `path.resolve`, it expects a relative path
|
||||
},
|
||||
|
||||
// Windows
|
||||
win: {
|
||||
target: 'nsis',
|
||||
},
|
||||
nsis: {
|
||||
artifactName: '${name}-Setup-${version}.${ext}',
|
||||
},
|
||||
|
||||
// Linux
|
||||
linux: {
|
||||
target: 'AppImage',
|
||||
},
|
||||
appImage: {
|
||||
artifactName: '${name}-${version}.${ext}',
|
||||
},
|
||||
|
||||
// macOS
|
||||
mac: {
|
||||
target: 'dmg',
|
||||
},
|
||||
dmg: {
|
||||
artifactName: '${name}-${version}.${ext}',
|
||||
},
|
||||
};
|
||||
69
electron.vite.config.ts
Normal file
69
electron.vite.config.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { resolve } from 'path';
|
||||
import { mergeConfig, UserConfig } from 'vite';
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||
import { getAliasesFromTsConfig, getClientEnvironmentVariables } from './vite-config-helper';
|
||||
import { createVueConfig } from './vite.config';
|
||||
import distDirs from './dist-dirs.json' assert { type: 'json' };
|
||||
|
||||
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
|
||||
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
|
||||
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
|
||||
const DIST_DIR = resolvePathFromProjectRoot(distDirs.electronUnbundled);
|
||||
|
||||
export default defineConfig({
|
||||
main: getSharedElectronConfig({
|
||||
distDirSubfolder: 'main',
|
||||
entryFilePath: MAIN_ENTRY_FILE,
|
||||
}),
|
||||
preload: getSharedElectronConfig({
|
||||
distDirSubfolder: 'preload',
|
||||
entryFilePath: PRELOAD_ENTRY_FILE,
|
||||
}),
|
||||
renderer: mergeConfig(
|
||||
createVueConfig({
|
||||
supportLegacyBrowsers: false,
|
||||
}),
|
||||
{
|
||||
build: {
|
||||
outDir: resolve(DIST_DIR, 'renderer'),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: WEB_INDEX_HTML_PATH,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
function getSharedElectronConfig(options: {
|
||||
readonly distDirSubfolder: string;
|
||||
readonly entryFilePath: string;
|
||||
}): UserConfig {
|
||||
return {
|
||||
build: {
|
||||
outDir: resolve(DIST_DIR, options.distDirSubfolder),
|
||||
lib: {
|
||||
entry: options.entryFilePath,
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: '[name].cjs', // This is needed so `type="module"` works
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
define: {
|
||||
...getClientEnvironmentVariables(),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
...getAliasesFromTsConfig(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePathFromProjectRoot(pathSegment: string) {
|
||||
return resolve(__dirname, pathSegment);
|
||||
}
|
||||
@@ -4,9 +4,6 @@ This folder contains image files and other resources related to images.
|
||||
|
||||
## logo.svg
|
||||
|
||||
[logo.svg](./logo.svg) is the master logo from which all other icons or images are created from.
|
||||
It should be the only file that will be changed manually.
|
||||
|
||||
[`logo-update.mjs`](./logo-update.mjs) script in this folder updates all the logo files.
|
||||
It should be executed everytime the logo is changed.
|
||||
It automates recreation of logo files in different formats.
|
||||
[`logo.svg`](./logo.svg) serves as the primary logo from which all other icons and images are derived.
|
||||
Only modify this file manually.
|
||||
After making changes, execute `npm run build:icons` to regenerate logo files in various formats.
|
||||
|
||||
25813
package-lock.json
generated
25813
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
70
package.json
70
package.json
@@ -1,31 +1,40 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.3",
|
||||
"private": true,
|
||||
"slogan": "Now you have the choice",
|
||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||
"author": "undergroundwires",
|
||||
"type": "module",
|
||||
"main": "./dist-electron-unbundled/main/index.cjs",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit --include ./tests/bootstrap/setup.ts",
|
||||
"test:e2e": "vue-cli-service test:e2e",
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest run --dir tests/unit",
|
||||
"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: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",
|
||||
"create-icons": "node img/logo-update.mjs",
|
||||
"electron:build": "vue-cli-service electron:build",
|
||||
"electron:serve": "vue-cli-service electron:serve",
|
||||
"lint:eslint": "vue-cli-service lint --no-fix --mode production",
|
||||
"install-deps": "node scripts/npm-install.js",
|
||||
"icons:build": "node scripts/logo-update.js",
|
||||
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
|
||||
"check:external-urls": "vitest run --dir tests/checks/external-urls --environment node",
|
||||
"check:verify-build-artifacts": "node scripts/verify-build-artifacts",
|
||||
"electron:dev": "electron-vite dev",
|
||||
"electron:preview": "electron-vite preview",
|
||||
"electron:prebuild": "electron-vite build",
|
||||
"electron:build": "electron-builder",
|
||||
"lint:eslint": "eslint . --ignore-path .gitignore",
|
||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstall": "electron-builder install-app-deps",
|
||||
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\" --include ./tests/bootstrap/setup.ts"
|
||||
"postuninstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@floating-ui/vue": "^1.0.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||
@@ -33,35 +42,26 @@
|
||||
"@fortawesome/vue-fontawesome": "^2.0.9",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"ace-builds": "^1.23.4",
|
||||
"core-js": "^3.32.0",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"electron-progressbar": "^2.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"install": "^0.13.0",
|
||||
"liquor-tree": "^0.2.70",
|
||||
"markdown-it": "^13.0.1",
|
||||
"npm": "^9.8.1",
|
||||
"v-tooltip": "2.1.3",
|
||||
"vue": "^2.7.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@types/ace": "^0.0.48",
|
||||
"@types/chai": "^4.3.5",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.8",
|
||||
"@vue/cli-plugin-e2e-cypress": "~5.0.8",
|
||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||
"@vue/cli-plugin-typescript": "~5.0.8",
|
||||
"@vue/cli-plugin-unit-mocha": "~5.0.8",
|
||||
"@vue/cli-service": "~5.0.8",
|
||||
"@vitejs/plugin-legacy": "^4.1.1",
|
||||
"@vitejs/plugin-vue2": "^2.2.0",
|
||||
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/test-utils": "^1.3.6",
|
||||
"chai": "^4.3.7",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"cypress": "^12.17.2",
|
||||
"electron": "^25.3.2",
|
||||
"electron-builder": "^24.6.3",
|
||||
@@ -69,32 +69,32 @@
|
||||
"electron-icon-builder": "^2.0.1",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-updater": "^6.1.4",
|
||||
"electron-vite": "^1.0.27",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-cypress": "^2.14.0",
|
||||
"eslint-plugin-vue": "^9.6.0",
|
||||
"eslint-plugin-vuejs-accessibility": "^1.2.0",
|
||||
"icon-gen": "^3.0.1",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdom": "^22.1.0",
|
||||
"markdownlint-cli": "^0.35.0",
|
||||
"postcss": "^8.4.28",
|
||||
"remark-cli": "^11.0.0",
|
||||
"remark-lint-no-dead-urls": "^1.1.0",
|
||||
"remark-preset-lint-consistent": "^5.1.2",
|
||||
"remark-validate-links": "^12.1.1",
|
||||
"sass": "^1.64.1",
|
||||
"sass-loader": "^13.3.2",
|
||||
"start-server-and-test": "^2.0.0",
|
||||
"svgexport": "^0.4.2",
|
||||
"ts-loader": "^9.4.4",
|
||||
"terser": "^5.19.2",
|
||||
"tslib": "~2.4.0",
|
||||
"typescript": "~4.6.2",
|
||||
"vue-cli-plugin-electron-builder": "^3.0.0-alpha.4",
|
||||
"vite": "^4.4.9",
|
||||
"vitest": "^0.34.2",
|
||||
"vue-tsc": "^1.8.8",
|
||||
"yaml-lint": "^1.7.0"
|
||||
},
|
||||
"overrides": {
|
||||
"vue-cli-plugin-electron-builder": {
|
||||
"electron-builder": "^24.6.3"
|
||||
}
|
||||
},
|
||||
"//devDependencies": {
|
||||
"terser": "Used by @vitejs/plugin-legacy for minification",
|
||||
"typescript": [
|
||||
"Cannot upgrade to 5.X.X due to unmaintained @vue/cli-plugin-typescript, https://github.com/vuejs/vue-cli/issues/7401",
|
||||
"Cannot upgrade to > 4.6.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
},
|
||||
const autoprefixer = require('autoprefixer');
|
||||
|
||||
module.exports = () => {
|
||||
return {
|
||||
plugins: [
|
||||
autoprefixer(),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Description:
|
||||
# This script ensures npm is available, removes existing node modules, optionally
|
||||
# removes package-lock.json (when -n flag is used), installs dependencies and runs unit tests.
|
||||
# Usage:
|
||||
# ./fresh-npm-install.sh # Regular execution
|
||||
# ./fresh-npm-install.sh -n # Non-deterministic mode (removes package-lock.json)
|
||||
|
||||
declare NON_DETERMINISTIC_FLAG=0
|
||||
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
ensure_npm_is_available
|
||||
ensure_npm_root
|
||||
remove_existing_modules
|
||||
if [[ $NON_DETERMINISTIC_FLAG -eq 1 ]]; then
|
||||
remove_package_lock_json
|
||||
fi
|
||||
install_dependencies
|
||||
run_unit_tests
|
||||
}
|
||||
|
||||
ensure_npm_is_available() {
|
||||
if ! command -v npm &> /dev/null; then
|
||||
log::fatal 'npm could not be found, please install it first.'
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_npm_root() {
|
||||
if [ ! -f package.json ]; then
|
||||
log::fatal 'Current directory is not a npm root. Please run the script in a npm root directory.'
|
||||
fi
|
||||
}
|
||||
|
||||
remove_existing_modules() {
|
||||
if [ -d ./node_modules ]; then
|
||||
log::info 'Removing existing node modules...'
|
||||
if ! rm -rf ./node_modules; then
|
||||
log::fatal 'Could not remove existing node modules.'
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
log::info 'Installing dependencies...'
|
||||
if ! npm install; then
|
||||
log::fatal 'Failed to install dependencies.'
|
||||
fi
|
||||
}
|
||||
|
||||
remove_package_lock_json() {
|
||||
if [ -f ./package-lock.json ]; then
|
||||
log::info 'Removing package-lock.json...'
|
||||
if ! rm -rf ./package-lock.json; then
|
||||
log::fatal 'Could not remove package-lock.json.'
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
run_unit_tests() {
|
||||
log::info 'Running unit tests...'
|
||||
if ! npm run test:unit; then
|
||||
pwd
|
||||
log::fatal 'Failed to run unit tests.'
|
||||
fi
|
||||
}
|
||||
|
||||
log::info() {
|
||||
local -r message="$1"
|
||||
echo "📣 ${message}"
|
||||
}
|
||||
|
||||
log::fatal() {
|
||||
local -r message="$1"
|
||||
echo "❌ ${message}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while getopts "n" opt; do
|
||||
case ${opt} in
|
||||
n)
|
||||
NON_DETERMINISTIC_FLAG=1
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: $OPTARG" 1>&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
main "$1"
|
||||
@@ -8,7 +8,7 @@ class Paths {
|
||||
constructor(selfDirectory) {
|
||||
const projectRoot = resolve(selfDirectory, '../');
|
||||
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
||||
this.publicDirectory = join(projectRoot, 'public');
|
||||
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
||||
this.electronBuildDirectory = join(projectRoot, 'build');
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ async function updateDesktopIcons(sourceImage, electronIconsDir) {
|
||||
await ensureFolderExists(electronIconsDir);
|
||||
const temporaryDir = await mkdtemp('icon-');
|
||||
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
||||
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by icon-builder
|
||||
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by `icon-builder`
|
||||
await runCommand(
|
||||
'npx',
|
||||
'svgexport',
|
||||
199
scripts/npm-install.js
Normal file
199
scripts/npm-install.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
Description:
|
||||
This script manages NPM dependencies for a project.
|
||||
It offers capabilities like doing a fresh install, retries on network errors, and other features.
|
||||
|
||||
Usage:
|
||||
npm run install-deps [-- <options>]
|
||||
node scripts/npm-install.js [options]
|
||||
|
||||
Options:
|
||||
--root-directory <path>
|
||||
Specifies the root directory where package.json resides
|
||||
Defaults to the current working directory.
|
||||
Example: npm run install-deps -- --root-directory /your/path/here
|
||||
|
||||
--no-errors
|
||||
Ignores errors and continues the execution.
|
||||
Example: npm run install-deps -- --no-errors
|
||||
|
||||
--ci
|
||||
Uses 'npm ci' for dependency installation instead of 'npm install'.
|
||||
Example: npm run install-deps -- --ci
|
||||
|
||||
--fresh
|
||||
Removes the existing node_modules directory before installing dependencies.
|
||||
Example: npm run install-deps -- --fresh
|
||||
|
||||
--non-deterministic
|
||||
Removes package-lock.json for a non-deterministic installation.
|
||||
Example: npm run install-deps -- --non-deterministic
|
||||
|
||||
Note:
|
||||
|
||||
Flags can be combined as needed.
|
||||
Example: npm run install-deps -- --fresh --non-deterministic
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { resolve } from 'path';
|
||||
import { access, rm, unlink } from 'fs/promises';
|
||||
import { constants } from 'fs';
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const RETRY_DELAY_IN_MS = 5 /* seconds */ * 1000;
|
||||
const ARG_NAMES = {
|
||||
rootDirectory: '--root-directory',
|
||||
ignoreErrors: '--no-errors',
|
||||
ci: '--ci',
|
||||
fresh: '--fresh',
|
||||
nonDeterministic: '--non-deterministic',
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const options = getOptions();
|
||||
console.log('Options:', options);
|
||||
await ensureNpmRootDirectory(options.rootDirectory);
|
||||
await ensureNpmIsAvailable();
|
||||
if (options.fresh) {
|
||||
await removeNodeModules(options.rootDirectory);
|
||||
}
|
||||
if (options.nonDeterministic) {
|
||||
await removePackageLockJson(options.rootDirectory);
|
||||
}
|
||||
const command = buildCommand(options.ci, options.outputErrors);
|
||||
console.log('Starting dependency installation...');
|
||||
const exitCode = await executeWithRetry(
|
||||
command,
|
||||
options.workingDirectory,
|
||||
MAX_RETRIES,
|
||||
RETRY_DELAY_IN_MS,
|
||||
);
|
||||
if (exitCode === 0) {
|
||||
console.log('🎊 Installed dependencies...');
|
||||
} else {
|
||||
console.error(`💀 Failed to install dependencies, exit code: ${exitCode}`);
|
||||
}
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
async function removeNodeModules(workingDirectory) {
|
||||
const nodeModulesDirectory = resolve(workingDirectory, 'node_modules');
|
||||
if (await exists('./node_modules')) {
|
||||
console.log('Removing node_modules...');
|
||||
await rm(nodeModulesDirectory, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function removePackageLockJson(workingDirectory) {
|
||||
const packageLockJsonFile = resolve(workingDirectory, 'package-lock.json');
|
||||
if (await exists(packageLockJsonFile)) {
|
||||
console.log('Removing package-lock.json...');
|
||||
await unlink(packageLockJsonFile);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureNpmIsAvailable() {
|
||||
const exitCode = await executeCommand('npm --version');
|
||||
if (exitCode !== 0) {
|
||||
throw new Error('`npm` in not available!');
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureNpmRootDirectory(workingDirectory) {
|
||||
const packageJsonPath = resolve(workingDirectory, 'package.json');
|
||||
if (!await exists(packageJsonPath)) {
|
||||
throw new Error(`Not an NPM project root: ${workingDirectory}`);
|
||||
}
|
||||
}
|
||||
|
||||
function buildCommand(ci, outputErrors) {
|
||||
const baseCommand = ci ? 'npm ci' : 'npm install';
|
||||
if (!outputErrors) {
|
||||
return `${baseCommand} --loglevel=error`;
|
||||
}
|
||||
return baseCommand;
|
||||
}
|
||||
|
||||
function getOptions() {
|
||||
const processArgs = process.argv.slice(2); // Slice off the node and script name
|
||||
return {
|
||||
rootDirectory: processArgs.includes('--root-directory') ? processArgs[processArgs.indexOf('--root-directory') + 1] : process.cwd(),
|
||||
outputErrors: !processArgs.includes(ARG_NAMES.ignoreErrors),
|
||||
ci: processArgs.includes(ARG_NAMES.ci),
|
||||
fresh: processArgs.includes(ARG_NAMES.fresh),
|
||||
nonDeterministic: processArgs.includes(ARG_NAMES.nonDeterministic),
|
||||
};
|
||||
}
|
||||
|
||||
async function executeWithRetry(
|
||||
command,
|
||||
workingDirectory,
|
||||
maxRetries,
|
||||
retryDelayInMs,
|
||||
currentAttempt = 1,
|
||||
) {
|
||||
const statusCode = await executeCommand(command, workingDirectory, true, true);
|
||||
if (statusCode === 0 || currentAttempt >= maxRetries) {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
console.log(`⚠️🔄 Attempt ${currentAttempt} failed. Retrying in ${retryDelayInMs / 1000} seconds...`);
|
||||
await sleep(retryDelayInMs);
|
||||
|
||||
const retryResult = await executeWithRetry(
|
||||
command,
|
||||
workingDirectory,
|
||||
maxRetries,
|
||||
retryDelayInMs,
|
||||
currentAttempt + 1,
|
||||
);
|
||||
return retryResult;
|
||||
}
|
||||
|
||||
async function executeCommand(
|
||||
command,
|
||||
workingDirectory = process.cwd(),
|
||||
logStdout = false,
|
||||
logCommand = false,
|
||||
) {
|
||||
if (logCommand) {
|
||||
console.log(`▶️ Executing command "${command}" at "${workingDirectory}"`);
|
||||
}
|
||||
const process = exec(
|
||||
command,
|
||||
{
|
||||
cwd: workingDirectory,
|
||||
},
|
||||
);
|
||||
if (logStdout) {
|
||||
process.stdout.on('data', (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
}
|
||||
process.stderr.on('data', (data) => {
|
||||
console.error(data.toString());
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
process.on('exit', (code) => {
|
||||
resolve(code);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sleep(milliseconds) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, milliseconds);
|
||||
});
|
||||
}
|
||||
|
||||
async function exists(path) {
|
||||
try {
|
||||
await access(path, constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
58
scripts/print-dist-dir.js
Normal file
58
scripts/print-dist-dir.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Description:
|
||||
* This script determines the absolute path of a distribution directory based on CLI arguments
|
||||
* and outputs its absolute path. It is designed to be run programmatically by other scripts.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/print-dist-dir.js [options]
|
||||
*
|
||||
* Options:
|
||||
* --electron-unbundled Path for the unbundled Electron application
|
||||
* --electron-bundled Path for the bundled Electron application
|
||||
* --web Path for the web application
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
const DIST_DIRS_JSON_FILE_PATH = resolve(process.cwd(), 'dist-dirs.json'); // cannot statically import because ESLint does not support it https://github.com/eslint/eslint/discussions/15305
|
||||
const CLI_ARGUMENTS = process.argv.slice(2);
|
||||
|
||||
async function main() {
|
||||
const distDirs = await readDistDirsJsonFile(DIST_DIRS_JSON_FILE_PATH);
|
||||
const relativeDistDir = determineRelativeDistDir(distDirs, CLI_ARGUMENTS);
|
||||
const absoluteDistDir = resolve(process.cwd(), relativeDistDir);
|
||||
console.log(absoluteDistDir);
|
||||
}
|
||||
|
||||
function mapCliFlagsToDistDirs(distDirs) {
|
||||
return {
|
||||
'--electron-unbundled': distDirs.electronUnbundled,
|
||||
'--electron-bundled': distDirs.electronBundled,
|
||||
'--web': distDirs.web,
|
||||
};
|
||||
}
|
||||
|
||||
function determineRelativeDistDir(distDirsJsonObject, cliArguments) {
|
||||
const cliFlagDistDirMap = mapCliFlagsToDistDirs(distDirsJsonObject);
|
||||
const availableCliFlags = Object.keys(cliFlagDistDirMap);
|
||||
const requestedCliFlags = cliArguments.filter((arg) => {
|
||||
return availableCliFlags.includes(arg);
|
||||
});
|
||||
if (!requestedCliFlags.length) {
|
||||
throw new Error(`No distribution directory was requested. Please use one of these flags: ${availableCliFlags.join(', ')}`);
|
||||
}
|
||||
if (requestedCliFlags.length > 1) {
|
||||
throw new Error(`Multiple distribution directories were requested, but this script only supports one: ${requestedCliFlags.join(', ')}`);
|
||||
}
|
||||
const selectedCliFlag = requestedCliFlags[0];
|
||||
return cliFlagDistDirMap[selectedCliFlag];
|
||||
}
|
||||
|
||||
async function readDistDirsJsonFile(absoluteConfigJsonFilePath) {
|
||||
const fileContentAsText = await readFile(absoluteConfigJsonFilePath, 'utf8');
|
||||
const parsedJsonData = JSON.parse(fileContentAsText);
|
||||
return parsedJsonData;
|
||||
}
|
||||
|
||||
await main();
|
||||
133
scripts/verify-build-artifacts.js
Normal file
133
scripts/verify-build-artifacts.js
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Description:
|
||||
* This script verifies the existence and content of build artifacts based on the
|
||||
* provided CLI flags. It exists with exit code `0` if all verifications pass, otherwise
|
||||
* with exit code `1`.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/verify-build-artifacts.js [options]
|
||||
*
|
||||
* Options:
|
||||
* --electron-unbundled Verify artifacts for the unbundled Electron application.
|
||||
* --electron-bundled Verify artifacts for the bundled Electron application.
|
||||
* --web Verify artifacts for the web application.
|
||||
*/
|
||||
|
||||
import { access, readdir } from 'fs/promises';
|
||||
import { exec } from 'child_process';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const PROCESS_ARGUMENTS = process.argv.slice(2);
|
||||
const PRINT_DIST_DIR_SCRIPT_BASE_COMMAND = 'node scripts/print-dist-dir';
|
||||
|
||||
async function main() {
|
||||
const buildConfigs = getBuildVerificationConfigs();
|
||||
if (!anyCommandsFound(Object.keys(buildConfigs))) {
|
||||
die(`No valid command found in process arguments. Expected one of: ${Object.keys(buildConfigs).join(', ')}`);
|
||||
}
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const [command, config] of Object.entries(buildConfigs)) {
|
||||
if (PROCESS_ARGUMENTS.includes(command)) {
|
||||
const distDir = await executePrintDistDirScript(config.printDistDirScriptArgument);
|
||||
await verifyDirectoryExists(distDir);
|
||||
await verifyNonEmptyDirectory(distDir);
|
||||
await verifyFilesExist(distDir, config.filePatterns);
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
console.log('✅ Build completed successfully and all expected artifacts are in place.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function getBuildVerificationConfigs() {
|
||||
return {
|
||||
'--electron-unbundled': {
|
||||
printDistDirScriptArgument: '--electron-unbundled',
|
||||
filePatterns: [
|
||||
/main[/\\]index\.cjs/,
|
||||
/preload[/\\]index\.cjs/,
|
||||
/renderer[/\\]index\.htm(l)?/,
|
||||
],
|
||||
},
|
||||
'--electron-bundled': {
|
||||
printDistDirScriptArgument: '--electron-bundled',
|
||||
filePatterns: [
|
||||
/latest.*\.yml/, // generates latest.yml for auto-updates
|
||||
/.*-\d+\.\d+\.\d+\..*/, // a file with extension and semantic version (packaged application)
|
||||
],
|
||||
},
|
||||
'--web': {
|
||||
printDistDirScriptArgument: '--web',
|
||||
filePatterns: [
|
||||
/index\.htm(l)?/,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function anyCommandsFound(commands) {
|
||||
return PROCESS_ARGUMENTS.some((arg) => commands.includes(arg));
|
||||
}
|
||||
|
||||
async function verifyDirectoryExists(directoryPath) {
|
||||
try {
|
||||
await access(directoryPath);
|
||||
} catch (error) {
|
||||
die(`Directory does not exist at \`${directoryPath}\`:\n\t${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyNonEmptyDirectory(directoryPath) {
|
||||
const files = await readdir(directoryPath);
|
||||
if (files.length === 0) {
|
||||
die(`Directory is empty at \`${directoryPath}\``);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyFilesExist(directoryPath, filePatterns) {
|
||||
const files = await listAllFilesRecursively(directoryPath);
|
||||
for (const pattern of filePatterns) {
|
||||
const match = files.some((file) => pattern.test(file));
|
||||
if (!match) {
|
||||
die(
|
||||
`No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``,
|
||||
`\nFiles in directory:\n${files.map((file) => `\t- ${file}`).join('\n')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function listAllFilesRecursively(directoryPath) {
|
||||
const dir = await readdir(directoryPath, { withFileTypes: true });
|
||||
const files = await Promise.all(dir.map(async (dirent) => {
|
||||
const absolutePath = resolve(directoryPath, dirent.name);
|
||||
if (dirent.isDirectory()) {
|
||||
return listAllFilesRecursively(absolutePath);
|
||||
}
|
||||
return absolutePath;
|
||||
}));
|
||||
return files.flat();
|
||||
}
|
||||
|
||||
async function executePrintDistDirScript(flag) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const commandToRun = `${PRINT_DIST_DIR_SCRIPT_BASE_COMMAND} ${flag}`;
|
||||
|
||||
exec(commandToRun, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`Execution failed with error: ${error}`));
|
||||
} else if (stderr) {
|
||||
reject(new Error(`Execution failed with stderr: ${stderr}`));
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function die(...message) {
|
||||
console.error(...message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await main();
|
||||
16
src/TypeHelpers.ts
Normal file
16
src/TypeHelpers.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export type Constructible<T, TArgs extends unknown[] = never> = {
|
||||
prototype: T;
|
||||
apply: (this: unknown, args: TArgs) => void;
|
||||
readonly name: string;
|
||||
};
|
||||
|
||||
export type PropertyKeys<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? never : K;
|
||||
}[keyof T];
|
||||
|
||||
export type ConstructorArguments<T> =
|
||||
T extends new (...args: infer U) => unknown ? U : never;
|
||||
|
||||
export type FunctionKeys<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never;
|
||||
}[keyof T];
|
||||
@@ -1,25 +1,23 @@
|
||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { Environment } from '../Environment/Environment';
|
||||
import { IEnvironment } from '../Environment/IEnvironment';
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { IApplicationFactory } from '../IApplicationFactory';
|
||||
import { ApplicationFactory } from '../ApplicationFactory';
|
||||
import { ApplicationContext } from './ApplicationContext';
|
||||
|
||||
export async function buildContext(
|
||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||
environment = Environment.CurrentEnvironment,
|
||||
environment = RuntimeEnvironment.CurrentEnvironment,
|
||||
): Promise<IApplicationContext> {
|
||||
if (!factory) { throw new Error('missing factory'); }
|
||||
if (!environment) { throw new Error('missing environment'); }
|
||||
const app = await factory.getApp();
|
||||
const os = getInitialOs(app, environment);
|
||||
const os = getInitialOs(app, environment.os);
|
||||
return new ApplicationContext(app, os);
|
||||
}
|
||||
|
||||
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
||||
const currentOs = environment.os;
|
||||
function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
|
||||
const supportedOsList = app.getSupportedOsList();
|
||||
if (supportedOsList.includes(currentOs)) {
|
||||
return currentOs;
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||
import { IEnvironment } from './IEnvironment';
|
||||
|
||||
export interface IEnvironmentVariables {
|
||||
readonly window: Window & typeof globalThis;
|
||||
readonly process: NodeJS.Process;
|
||||
readonly navigator: Navigator;
|
||||
}
|
||||
|
||||
export class Environment implements IEnvironment {
|
||||
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
||||
window,
|
||||
process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
|
||||
navigator,
|
||||
});
|
||||
|
||||
public readonly isDesktop: boolean;
|
||||
|
||||
public readonly os: OperatingSystem;
|
||||
|
||||
protected constructor(
|
||||
variables: IEnvironmentVariables,
|
||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||
) {
|
||||
if (!variables) {
|
||||
throw new Error('variables is null or empty');
|
||||
}
|
||||
this.isDesktop = isDesktop(variables);
|
||||
if (this.isDesktop) {
|
||||
this.os = getDesktopOsType(getProcessPlatform(variables));
|
||||
} else {
|
||||
const userAgent = getUserAgent(variables);
|
||||
this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getUserAgent(variables: IEnvironmentVariables): string {
|
||||
if (!variables.window || !variables.window.navigator) {
|
||||
return undefined;
|
||||
}
|
||||
return variables.window.navigator.userAgent;
|
||||
}
|
||||
|
||||
function getProcessPlatform(variables: IEnvironmentVariables): string {
|
||||
if (!variables.process || !variables.process.platform) {
|
||||
return undefined;
|
||||
}
|
||||
return variables.process.platform;
|
||||
}
|
||||
|
||||
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
|
||||
// https://nodejs.org/api/process.html#process_process_platform
|
||||
switch (processPlatform) {
|
||||
case 'darwin':
|
||||
return OperatingSystem.macOS;
|
||||
case 'win32':
|
||||
return OperatingSystem.Windows;
|
||||
case 'linux':
|
||||
return OperatingSystem.Linux;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isDesktop(variables: IEnvironmentVariables): boolean {
|
||||
// More: https://github.com/electron/electron/issues/2288
|
||||
// Renderer process
|
||||
if (variables.window
|
||||
&& variables.window.process
|
||||
&& variables.window.process.type === 'renderer') {
|
||||
return true;
|
||||
}
|
||||
// Main process
|
||||
if (variables.process
|
||||
&& variables.process.versions
|
||||
&& Boolean(variables.process.versions.electron)) {
|
||||
return true;
|
||||
}
|
||||
// Detect the user agent when the `nodeIntegration` option is set to true
|
||||
if (variables.navigator
|
||||
&& variables.navigator.userAgent
|
||||
&& variables.navigator.userAgent.includes('Electron')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IEnvironment {
|
||||
readonly isDesktop: boolean;
|
||||
readonly os: OperatingSystem;
|
||||
}
|
||||
@@ -7,16 +7,19 @@ import MacOsData from '@/application/collections/macos.yaml';
|
||||
import LinuxData from '@/application/collections/linux.yaml';
|
||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||
import { Application } from '@/domain/Application';
|
||||
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
|
||||
export function parseApplication(
|
||||
parser = CategoryCollectionParser,
|
||||
processEnv: NodeJS.ProcessEnv = process.env,
|
||||
categoryParser = parseCategoryCollection,
|
||||
informationParser = parseProjectInformation,
|
||||
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
||||
collectionsData = PreParsedCollections,
|
||||
): IApplication {
|
||||
validateCollectionsData(collectionsData);
|
||||
const information = parseProjectInformation(processEnv);
|
||||
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||
const information = informationParser(metadata);
|
||||
const collections = collectionsData.map((collection) => categoryParser(collection, information));
|
||||
const app = new Application(information, collections);
|
||||
return app;
|
||||
}
|
||||
@@ -24,16 +27,12 @@ export function parseApplication(
|
||||
export type CategoryCollectionParserType
|
||||
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
||||
|
||||
const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
|
||||
return parseCategoryCollection(file, info);
|
||||
};
|
||||
|
||||
const PreParsedCollections: readonly CollectionData [] = [
|
||||
WindowsData, MacOsData, LinuxData,
|
||||
];
|
||||
|
||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||
if (!collections || !collections.length) {
|
||||
if (!collections?.length) {
|
||||
throw new Error('missing collections');
|
||||
}
|
||||
if (collections.some((collection) => !collection)) {
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||
import { Version } from '@/domain/Version';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { ConstructorArguments } from '@/TypeHelpers';
|
||||
|
||||
export function parseProjectInformation(
|
||||
environment: NodeJS.ProcessEnv | VueAppEnvironment,
|
||||
export function
|
||||
parseProjectInformation(
|
||||
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
||||
createProjectInformation: ProjectInformationFactory = (
|
||||
...args
|
||||
) => new ProjectInformation(...args),
|
||||
): IProjectInformation {
|
||||
const version = new Version(environment[VueAppEnvironmentKeys.VUE_APP_VERSION]);
|
||||
return new ProjectInformation(
|
||||
environment[VueAppEnvironmentKeys.VUE_APP_NAME],
|
||||
const version = new Version(
|
||||
metadata.version,
|
||||
);
|
||||
return createProjectInformation(
|
||||
metadata.name,
|
||||
version,
|
||||
environment[VueAppEnvironmentKeys.VUE_APP_SLOGAN],
|
||||
environment[VueAppEnvironmentKeys.VUE_APP_REPOSITORY_URL],
|
||||
environment[VueAppEnvironmentKeys.VUE_APP_HOMEPAGE_URL],
|
||||
metadata.slogan,
|
||||
metadata.repositoryUrl,
|
||||
metadata.homepageUrl,
|
||||
);
|
||||
}
|
||||
|
||||
export const VueAppEnvironmentKeys = {
|
||||
VUE_APP_VERSION: 'VUE_APP_VERSION',
|
||||
VUE_APP_NAME: 'VUE_APP_NAME',
|
||||
VUE_APP_SLOGAN: 'VUE_APP_SLOGAN',
|
||||
VUE_APP_REPOSITORY_URL: 'VUE_APP_REPOSITORY_URL',
|
||||
VUE_APP_HOMEPAGE_URL: 'VUE_APP_HOMEPAGE_URL',
|
||||
} as const;
|
||||
|
||||
export type VueAppEnvironment = {
|
||||
[K in keyof typeof VueAppEnvironmentKeys]: string;
|
||||
};
|
||||
export type ProjectInformationFactory = (
|
||||
...args: ConstructorArguments<typeof ProjectInformation>
|
||||
) => IProjectInformation;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
|
||||
export interface CodeSegmentMerger {
|
||||
mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
import { CodeSegmentMerger } from './CodeSegmentMerger';
|
||||
|
||||
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
||||
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
||||
if (!codeSegments?.length) {
|
||||
throw new Error('missing segments');
|
||||
}
|
||||
return {
|
||||
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
||||
revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function joinCodeParts(codeSegments: readonly string[]): string {
|
||||
return codeSegments
|
||||
.filter((segment) => segment?.length > 0)
|
||||
.join('\n');
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ICompiledCode {
|
||||
export interface CompiledCode {
|
||||
readonly code: string;
|
||||
readonly revertCode?: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { FunctionCall } from '../FunctionCall';
|
||||
import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
|
||||
|
||||
export interface FunctionCallCompilationContext {
|
||||
readonly allFunctions: ISharedFunctionCollection;
|
||||
readonly rootCallSequence: readonly FunctionCall[];
|
||||
readonly singleCallCompiler: SingleCallCompiler;
|
||||
}
|
||||
@@ -1,149 +1,10 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
|
||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
|
||||
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
|
||||
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { FunctionCall } from '../FunctionCall';
|
||||
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
|
||||
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
import { CompiledCode } from './CompiledCode';
|
||||
|
||||
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||
|
||||
protected constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||
) {
|
||||
}
|
||||
|
||||
public compileCall(
|
||||
calls: IFunctionCall[],
|
||||
export interface FunctionCallCompiler {
|
||||
compileFunctionCalls(
|
||||
calls: readonly FunctionCall[],
|
||||
functions: ISharedFunctionCollection,
|
||||
): ICompiledCode {
|
||||
if (!functions) { throw new Error('missing functions'); }
|
||||
if (!calls) { throw new Error('missing calls'); }
|
||||
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
|
||||
const context: ICompilationContext = {
|
||||
allFunctions: functions,
|
||||
callSequence: calls,
|
||||
expressionsCompiler: this.expressionsCompiler,
|
||||
};
|
||||
const code = compileCallSequence(context);
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
interface ICompilationContext {
|
||||
allFunctions: ISharedFunctionCollection;
|
||||
callSequence: readonly IFunctionCall[];
|
||||
expressionsCompiler: IExpressionsCompiler;
|
||||
}
|
||||
|
||||
interface ICompiledFunctionCall {
|
||||
readonly code: string;
|
||||
readonly revertCode: string;
|
||||
}
|
||||
|
||||
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
|
||||
const compiledFunctions = context.callSequence
|
||||
.flatMap((call) => compileSingleCall(call, context));
|
||||
return {
|
||||
code: merge(compiledFunctions.map((f) => f.code)),
|
||||
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
||||
};
|
||||
}
|
||||
|
||||
function compileSingleCall(
|
||||
call: IFunctionCall,
|
||||
context: ICompilationContext,
|
||||
): ICompiledFunctionCall[] {
|
||||
const func = context.allFunctions.getFunctionByName(call.functionName);
|
||||
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||
if (func.body.code) { // Function with inline code
|
||||
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
||||
return [compiledCode];
|
||||
}
|
||||
// Function with inner calls
|
||||
return func.body.calls
|
||||
.map((innerCall) => {
|
||||
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
||||
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
|
||||
return compileSingleCall(compiledCall, context);
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function compileCode(
|
||||
code: IFunctionCode,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
compiler: IExpressionsCompiler,
|
||||
): ICompiledFunctionCall {
|
||||
return {
|
||||
code: compiler.compileExpressions(code.execute, args),
|
||||
revertCode: compiler.compileExpressions(code.revert, args),
|
||||
};
|
||||
}
|
||||
|
||||
function compileArgs(
|
||||
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
compiler: IExpressionsCompiler,
|
||||
): IReadOnlyFunctionCallArgumentCollection {
|
||||
return argsToCompile
|
||||
.getAllParameterNames()
|
||||
.map((parameterName) => {
|
||||
const { argumentValue } = argsToCompile.getArgument(parameterName);
|
||||
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
||||
return new FunctionCallArgument(parameterName, compiledValue);
|
||||
})
|
||||
.reduce((compiledArgs, arg) => {
|
||||
compiledArgs.addArgument(arg);
|
||||
return compiledArgs;
|
||||
}, new FunctionCallArgumentCollection());
|
||||
}
|
||||
|
||||
function merge(codeParts: readonly string[]): string {
|
||||
return codeParts
|
||||
.filter((part) => part?.length > 0)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function ensureThatCallArgumentsExistInParameterDefinition(
|
||||
func: ISharedFunction,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
): void {
|
||||
const callArgumentNames = args.getAllParameterNames();
|
||||
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
|
||||
}
|
||||
|
||||
function findUnexpectedParameters(
|
||||
callArgumentNames: string[],
|
||||
functionParameterNames: string[],
|
||||
): string[] {
|
||||
if (!callArgumentNames.length && !functionParameterNames.length) {
|
||||
return [];
|
||||
}
|
||||
return callArgumentNames
|
||||
.filter((callParam) => !functionParameterNames.includes(callParam));
|
||||
}
|
||||
|
||||
function throwIfNotEmpty(
|
||||
functionName: string,
|
||||
unexpectedParameters: string[],
|
||||
expectedParameters: string[],
|
||||
) {
|
||||
if (!unexpectedParameters.length) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
// eslint-disable-next-line prefer-template
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||
+ `"${unexpectedParameters.join('", "')}"`
|
||||
+ '. Expected parameter(s): '
|
||||
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||
);
|
||||
): CompiledCode;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||
import { FunctionCallCompiler } from './FunctionCallCompiler';
|
||||
import { CompiledCode } from './CompiledCode';
|
||||
import { FunctionCallCompilationContext } from './FunctionCallCompilationContext';
|
||||
import { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
|
||||
import { AdaptiveFunctionCallCompiler } from './SingleCall/AdaptiveFunctionCallCompiler';
|
||||
import { CodeSegmentMerger } from './CodeSegmentJoin/CodeSegmentMerger';
|
||||
import { NewlineCodeSegmentMerger } from './CodeSegmentJoin/NewlineCodeSegmentMerger';
|
||||
|
||||
export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
|
||||
public static readonly instance: FunctionCallCompiler = new FunctionCallSequenceCompiler();
|
||||
|
||||
/* The constructor is protected to enforce the singleton pattern. */
|
||||
protected constructor(
|
||||
private readonly singleCallCompiler: SingleCallCompiler = new AdaptiveFunctionCallCompiler(),
|
||||
private readonly codeSegmentMerger: CodeSegmentMerger = new NewlineCodeSegmentMerger(),
|
||||
) { }
|
||||
|
||||
public compileFunctionCalls(
|
||||
calls: readonly FunctionCall[],
|
||||
functions: ISharedFunctionCollection,
|
||||
): CompiledCode {
|
||||
if (!functions) { throw new Error('missing functions'); }
|
||||
if (!calls?.length) { throw new Error('missing calls'); }
|
||||
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
|
||||
const context: FunctionCallCompilationContext = {
|
||||
allFunctions: functions,
|
||||
rootCallSequence: calls,
|
||||
singleCallCompiler: this.singleCallCompiler,
|
||||
};
|
||||
const codeSegments = context.rootCallSequence
|
||||
.flatMap((call) => this.singleCallCompiler.compileSingleCall(call, context));
|
||||
return this.codeSegmentMerger.mergeCodeParts(codeSegments);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { IFunctionCall } from '../IFunctionCall';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
|
||||
export interface IFunctionCallCompiler {
|
||||
compileCall(
|
||||
calls: IFunctionCall[],
|
||||
functions: ISharedFunctionCollection): ICompiledCode;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { FunctionCall } from '../../FunctionCall';
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '../../Argument/IFunctionCallArgumentCollection';
|
||||
import { ISharedFunction } from '../../../ISharedFunction';
|
||||
import { SingleCallCompiler } from './SingleCallCompiler';
|
||||
import { SingleCallCompilerStrategy } from './SingleCallCompilerStrategy';
|
||||
import { InlineFunctionCallCompiler } from './Strategies/InlineFunctionCallCompiler';
|
||||
import { NestedFunctionCallCompiler } from './Strategies/NestedFunctionCallCompiler';
|
||||
|
||||
export class AdaptiveFunctionCallCompiler implements SingleCallCompiler {
|
||||
public constructor(
|
||||
private readonly strategies: SingleCallCompilerStrategy[] = [
|
||||
new InlineFunctionCallCompiler(),
|
||||
new NestedFunctionCallCompiler(),
|
||||
],
|
||||
) {
|
||||
}
|
||||
|
||||
public compileSingleCall(
|
||||
call: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): CompiledCode[] {
|
||||
const func = context.allFunctions.getFunctionByName(call.functionName);
|
||||
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||
const strategy = this.findStrategy(func);
|
||||
return strategy.compileFunction(func, call, context);
|
||||
}
|
||||
|
||||
private findStrategy(func: ISharedFunction): SingleCallCompilerStrategy {
|
||||
const strategies = this.strategies.filter((strategy) => strategy.canCompile(func));
|
||||
if (strategies.length > 1) {
|
||||
throw new Error('Multiple strategies found to compile the function call.');
|
||||
}
|
||||
if (strategies.length === 0) {
|
||||
throw new Error('No strategies found to compile the function call.');
|
||||
}
|
||||
return strategies[0];
|
||||
}
|
||||
}
|
||||
|
||||
function ensureThatCallArgumentsExistInParameterDefinition(
|
||||
func: ISharedFunction,
|
||||
callArguments: IReadOnlyFunctionCallArgumentCollection,
|
||||
): void {
|
||||
const callArgumentNames = callArguments.getAllParameterNames();
|
||||
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||
throwIfUnexpectedParametersExist(func.name, unexpectedParameters, functionParameterNames);
|
||||
}
|
||||
|
||||
function findUnexpectedParameters(
|
||||
callArgumentNames: string[],
|
||||
functionParameterNames: string[],
|
||||
): string[] {
|
||||
if (!callArgumentNames.length && !functionParameterNames.length) {
|
||||
return [];
|
||||
}
|
||||
return callArgumentNames
|
||||
.filter((callParam) => !functionParameterNames.includes(callParam));
|
||||
}
|
||||
|
||||
function throwIfUnexpectedParametersExist(
|
||||
functionName: string,
|
||||
unexpectedParameters: string[],
|
||||
expectedParameters: string[],
|
||||
) {
|
||||
if (!unexpectedParameters.length) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
// eslint-disable-next-line prefer-template
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||
+ `"${unexpectedParameters.join('", "')}"`
|
||||
+ '. Expected parameter(s): '
|
||||
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FunctionCall } from '../../FunctionCall';
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
|
||||
|
||||
export interface SingleCallCompiler {
|
||||
compileSingleCall(
|
||||
call: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): CompiledCode[];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
|
||||
|
||||
export interface SingleCallCompilerStrategy {
|
||||
canCompile(func: ISharedFunction): boolean;
|
||||
compileFunction(
|
||||
calledFunction: ISharedFunction,
|
||||
callToFunction: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): CompiledCode[],
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||
|
||||
export interface ArgumentCompiler {
|
||||
createCompiledNestedCall(
|
||||
nestedFunctionCall: FunctionCall,
|
||||
parentFunctionCall: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): FunctionCall;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
|
||||
import { ArgumentCompiler } from './ArgumentCompiler';
|
||||
|
||||
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
||||
constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||
) { }
|
||||
|
||||
public createCompiledNestedCall(
|
||||
nestedFunction: FunctionCall,
|
||||
parentFunction: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): FunctionCall {
|
||||
const compiledArgs = compileNestedFunctionArguments(
|
||||
nestedFunction,
|
||||
parentFunction.args,
|
||||
context,
|
||||
this.expressionsCompiler,
|
||||
);
|
||||
const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs);
|
||||
return compiledCall;
|
||||
}
|
||||
}
|
||||
|
||||
function compileNestedFunctionArguments(
|
||||
nestedFunction: FunctionCall,
|
||||
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
context: FunctionCallCompilationContext,
|
||||
expressionsCompiler: IExpressionsCompiler,
|
||||
): IReadOnlyFunctionCallArgumentCollection {
|
||||
const requiredParameterNames = context
|
||||
.allFunctions
|
||||
.getRequiredParameterNames(nestedFunction.functionName);
|
||||
const compiledArguments = nestedFunction.args
|
||||
.getAllParameterNames()
|
||||
// Compile each argument value
|
||||
.map((paramName) => ({
|
||||
parameterName: paramName,
|
||||
compiledArgumentValue: compileArgument(
|
||||
paramName,
|
||||
nestedFunction,
|
||||
parentFunctionArgs,
|
||||
expressionsCompiler,
|
||||
),
|
||||
}))
|
||||
// Filter out arguments with absent values
|
||||
.filter(({
|
||||
parameterName,
|
||||
compiledArgumentValue,
|
||||
}) => isValidNonAbsentArgumentValue(
|
||||
parameterName,
|
||||
compiledArgumentValue,
|
||||
requiredParameterNames,
|
||||
))
|
||||
/*
|
||||
Create argument object with non-absent values.
|
||||
This is done after eliminating absent values because otherwise creating argument object
|
||||
with absent values throws error.
|
||||
*/
|
||||
.map(({
|
||||
parameterName,
|
||||
compiledArgumentValue,
|
||||
}) => new FunctionCallArgument(parameterName, compiledArgumentValue));
|
||||
return buildArgumentCollectionFromArguments(compiledArguments);
|
||||
}
|
||||
|
||||
function isValidNonAbsentArgumentValue(
|
||||
parameterName: string,
|
||||
argumentValue: string | undefined,
|
||||
requiredParameterNames: string[],
|
||||
): boolean {
|
||||
if (argumentValue) {
|
||||
return true;
|
||||
}
|
||||
if (!requiredParameterNames.includes(parameterName)) {
|
||||
return false;
|
||||
}
|
||||
throw new Error(`Compilation resulted in empty value for required parameter: "${parameterName}"`);
|
||||
}
|
||||
|
||||
function compileArgument(
|
||||
parameterName: string,
|
||||
nestedFunction: FunctionCall,
|
||||
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
expressionsCompiler: IExpressionsCompiler,
|
||||
): string {
|
||||
try {
|
||||
const { argumentValue: codeInArgument } = nestedFunction.args.getArgument(parameterName);
|
||||
return expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs);
|
||||
} catch (err) {
|
||||
throw new AggregateError([err], `Error when compiling argument for "${parameterName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function buildArgumentCollectionFromArguments(
|
||||
args: FunctionCallArgument[],
|
||||
): FunctionCallArgumentCollection {
|
||||
return args.reduce((compiledArgs, arg) => {
|
||||
compiledArgs.addArgument(arg);
|
||||
return compiledArgs;
|
||||
}, new FunctionCallArgumentCollection());
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||
|
||||
export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||
public constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||
) {
|
||||
}
|
||||
|
||||
public canCompile(func: ISharedFunction): boolean {
|
||||
return func.body.code !== undefined;
|
||||
}
|
||||
|
||||
public compileFunction(
|
||||
calledFunction: ISharedFunction,
|
||||
callToFunction: FunctionCall,
|
||||
): CompiledCode[] {
|
||||
const { code } = calledFunction.body;
|
||||
const { args } = callToFunction;
|
||||
return [
|
||||
{
|
||||
code: this.expressionsCompiler.compileExpressions(code.execute, args),
|
||||
revertCode: this.expressionsCompiler.compileExpressions(code.revert, args),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||
import { ArgumentCompiler } from './Argument/ArgumentCompiler';
|
||||
import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler';
|
||||
|
||||
export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||
public constructor(
|
||||
private readonly argumentCompiler: ArgumentCompiler = new NestedFunctionArgumentCompiler(),
|
||||
) {
|
||||
}
|
||||
|
||||
public canCompile(func: ISharedFunction): boolean {
|
||||
return func.body.calls !== undefined;
|
||||
}
|
||||
|
||||
public compileFunction(
|
||||
calledFunction: ISharedFunction,
|
||||
callToFunction: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): CompiledCode[] {
|
||||
const nestedCalls = calledFunction.body.calls;
|
||||
return nestedCalls.map((nestedCall) => {
|
||||
try {
|
||||
const compiledParentCall = this.argumentCompiler
|
||||
.createCompiledNestedCall(nestedCall, callToFunction, context);
|
||||
const compiledNestedCall = context.singleCallCompiler
|
||||
.compileSingleCall(compiledParentCall, context);
|
||||
return compiledNestedCall;
|
||||
} catch (err) {
|
||||
throw new AggregateError([err], `Error with call to "${nestedCall.functionName}" function from "${callToFunction.functionName}" function`);
|
||||
}
|
||||
}).flat();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,6 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||
import { IFunctionCall } from './IFunctionCall';
|
||||
|
||||
export class FunctionCall implements IFunctionCall {
|
||||
constructor(
|
||||
public readonly functionName: string,
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||
) {
|
||||
if (!functionName) {
|
||||
throw new Error('missing function name in function call');
|
||||
}
|
||||
if (!args) {
|
||||
throw new Error('missing args');
|
||||
}
|
||||
}
|
||||
export interface FunctionCall {
|
||||
readonly functionName: string;
|
||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from '@/application/collections/';
|
||||
import { IFunctionCall } from './IFunctionCall';
|
||||
import { FunctionCall } from './FunctionCall';
|
||||
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
||||
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
||||
import { FunctionCall } from './FunctionCall';
|
||||
import { ParsedFunctionCall } from './ParsedFunctionCall';
|
||||
|
||||
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
|
||||
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
|
||||
if (calls === undefined) {
|
||||
throw new Error('missing call data');
|
||||
}
|
||||
@@ -22,12 +22,12 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
||||
return [calls as FunctionCallData];
|
||||
}
|
||||
|
||||
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
|
||||
function parseFunctionCall(call: FunctionCallData): FunctionCall {
|
||||
if (!call) {
|
||||
throw new Error('missing call data');
|
||||
}
|
||||
const callArgs = parseArgs(call.parameters);
|
||||
return new FunctionCall(call.function, callArgs);
|
||||
return new ParsedFunctionCall(call.function, callArgs);
|
||||
}
|
||||
|
||||
function parseArgs(
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||
|
||||
export interface IFunctionCall {
|
||||
readonly functionName: string;
|
||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||
import { FunctionCall } from './FunctionCall';
|
||||
|
||||
export class ParsedFunctionCall implements FunctionCall {
|
||||
constructor(
|
||||
public readonly functionName: string,
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||
) {
|
||||
if (!functionName) {
|
||||
throw new Error('missing function name in function call');
|
||||
}
|
||||
if (!args) {
|
||||
throw new Error('missing args');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||
import { IFunctionCall } from './Call/IFunctionCall';
|
||||
import { FunctionCall } from './Call/FunctionCall';
|
||||
|
||||
export interface ISharedFunction {
|
||||
readonly name: string;
|
||||
@@ -9,8 +9,8 @@ export interface ISharedFunction {
|
||||
|
||||
export interface ISharedFunctionBody {
|
||||
readonly type: FunctionBodyType;
|
||||
readonly code: IFunctionCode;
|
||||
readonly calls: readonly IFunctionCall[];
|
||||
readonly code: IFunctionCode | undefined;
|
||||
readonly calls: readonly FunctionCall[] | undefined;
|
||||
}
|
||||
|
||||
export enum FunctionBodyType {
|
||||
|
||||
@@ -2,4 +2,5 @@ import { ISharedFunction } from './ISharedFunction';
|
||||
|
||||
export interface ISharedFunctionCollection {
|
||||
getFunctionByName(name: string): ISharedFunction;
|
||||
getRequiredParameterNames(functionName: string): string[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IFunctionCall } from './Call/IFunctionCall';
|
||||
import { FunctionCall } from './Call/FunctionCall';
|
||||
|
||||
import {
|
||||
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
||||
@@ -8,7 +8,7 @@ import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParam
|
||||
export function createCallerFunction(
|
||||
name: string,
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
callSequence: readonly IFunctionCall[],
|
||||
callSequence: readonly FunctionCall[],
|
||||
): ISharedFunction {
|
||||
if (!callSequence || !callSequence.length) {
|
||||
throw new Error(`missing call sequence in function "${name}"`);
|
||||
@@ -38,7 +38,7 @@ class SharedFunction implements ISharedFunction {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly parameters: IReadOnlyFunctionParameterCollection,
|
||||
content: IFunctionCode | readonly IFunctionCall[],
|
||||
content: IFunctionCode | readonly FunctionCall[],
|
||||
bodyType: FunctionBodyType,
|
||||
) {
|
||||
if (!name) { throw new Error('missing function name'); }
|
||||
@@ -46,7 +46,7 @@ class SharedFunction implements ISharedFunction {
|
||||
this.body = {
|
||||
type: bodyType,
|
||||
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
||||
calls: bodyType === FunctionBodyType.Calls ? content as readonly IFunctionCall[] : undefined,
|
||||
calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,15 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
|
||||
return func;
|
||||
}
|
||||
|
||||
public getRequiredParameterNames(functionName: string): string[] {
|
||||
return this
|
||||
.getFunctionByName(functionName)
|
||||
.parameters
|
||||
.all
|
||||
.filter((parameter) => !parameter.isOptional)
|
||||
.map((parameter) => parameter.name);
|
||||
}
|
||||
|
||||
private has(functionName: string) {
|
||||
return this.functionsByName.has(functionName);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmp
|
||||
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||
import { IScriptCompiler } from './IScriptCompiler';
|
||||
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
|
||||
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
||||
import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';
|
||||
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
|
||||
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
||||
import { ICompiledCode } from './Function/Call/Compiler/ICompiledCode';
|
||||
import { CompiledCode } from './Function/Call/Compiler/CompiledCode';
|
||||
|
||||
export class ScriptCompiler implements IScriptCompiler {
|
||||
private readonly functions: ISharedFunctionCollection;
|
||||
@@ -21,7 +21,7 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
functions: readonly FunctionData[] | undefined,
|
||||
syntax: ILanguageSyntax,
|
||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
||||
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||
) {
|
||||
if (!syntax) { throw new Error('missing syntax'); }
|
||||
@@ -40,7 +40,7 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
if (!script) { throw new Error('missing script'); }
|
||||
try {
|
||||
const calls = parseFunctionCalls(script.call);
|
||||
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
||||
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
||||
validateCompiledCode(compiledCode, this.codeValidator);
|
||||
return new ScriptCode(
|
||||
compiledCode.code,
|
||||
@@ -52,7 +52,7 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
}
|
||||
}
|
||||
|
||||
function validateCompiledCode(compiledCode: ICompiledCode, validator: ICodeValidator): void {
|
||||
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
||||
[compiledCode.code, compiledCode.revertCode].forEach(
|
||||
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
|
||||
);
|
||||
|
||||
@@ -3558,82 +3558,98 @@ functions:
|
||||
parameters:
|
||||
- name: prefName
|
||||
- name: jsonValue
|
||||
# prefs.js file (https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file) exists at
|
||||
# - Default installation:
|
||||
# ~/.mozilla/firefox/<profile-name>/prefs.js
|
||||
# - Flatpak installation:
|
||||
# ~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/prefs.js
|
||||
# - Snap installation:
|
||||
# ~/snap/firefox/common/.mozilla/firefox/<profile-name>/prefs.js
|
||||
docs: |-
|
||||
This script either creates or updates the `user.js` file to set specific Mozilla Firefox preferences.
|
||||
|
||||
The `user.js` file can be found in a Firefox profile folder [1] and its location depends on the type of installation:
|
||||
|
||||
- Default: `~/.mozilla/firefox/<profile-name>/user.js`
|
||||
- Flatpak: `~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/user.js`
|
||||
- Snap: `~/snap/firefox/common/.mozilla/firefox/<profile-name>/user.js`
|
||||
|
||||
While the `user.js` file is optional [2], if it's present, the Firefox application will prioritize its settings over
|
||||
those in `prefs.js` upon startup [1][2]. To prevent potential profile corruption, Mozilla advises against editing
|
||||
`prefs.js` directly [2].
|
||||
|
||||
[1]: https://web.archive.org/web/20230811005205/https://kb.mozillazine.org/User.js_file "User.js file - MozillaZine Knowledge Base"
|
||||
[2]: https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file "Prefs.js file - MozillaZine Knowledge Base"
|
||||
code: |-
|
||||
pref_name='{{ $prefName }}'
|
||||
pref_value='{{ $jsonValue }}'
|
||||
echo "Setting preference \"$pref_name\" to \"$pref_value\"."
|
||||
pref_file_paths=(
|
||||
~/.mozilla/firefox/*/prefs.js
|
||||
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/prefs.js
|
||||
~/snap/firefox/common/.mozilla/firefox/*/prefs.js
|
||||
declare -a profile_paths=(
|
||||
~/.mozilla/firefox/*/
|
||||
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
|
||||
~/snap/firefox/common/.mozilla/firefox/*/
|
||||
)
|
||||
declare -i total_files_found=0
|
||||
for pref_file in "${pref_file_paths[@]}"; do
|
||||
if [ -f "$pref_file" ]; then
|
||||
((total_files_found++))
|
||||
echo "$pref_file:"
|
||||
pref_start="user_pref(\"$pref_name\","
|
||||
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
||||
if ! grep --quiet "^$pref_start" "${pref_file}"; then
|
||||
echo $'\t'"Preference is not configured before."
|
||||
echo -n $'\n'"$pref_line" >> "$pref_file"
|
||||
echo $'\t'"Successfully configured."
|
||||
else
|
||||
if grep --quiet "^$pref_line$" "${pref_file}"; then
|
||||
echo $'\t'"Skipping. Preference is already configured as expected."
|
||||
else
|
||||
sed --in-place "/^$pref_start/d" "$pref_file"
|
||||
echo $'\t'"Deleted assignment with unexpected value."
|
||||
echo -n $'\n'"$pref_line" >> "$pref_file"
|
||||
echo $'\t'"Successfully reconfigured with expected value."
|
||||
fi
|
||||
fi
|
||||
declare -i total_profiles_found=0
|
||||
for profile_dir in "${profile_paths[@]}"; do
|
||||
if [ ! -d "$profile_dir" ]; then
|
||||
continue
|
||||
fi
|
||||
if [[ ! "$(basename "$profile_dir")" =~ ^[a-z0-9]{8}\..+ ]]; then
|
||||
continue # Not a profile folder
|
||||
fi
|
||||
((total_profiles_found++))
|
||||
user_js_file="${profile_dir}user.js"
|
||||
echo "$user_js_file:"
|
||||
if [ ! -f "$user_js_file" ]; then
|
||||
touch "$user_js_file"
|
||||
echo $'\t''Created new user.js file'
|
||||
fi
|
||||
pref_start="user_pref(\"$pref_name\","
|
||||
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
||||
if ! grep --quiet "^$pref_start" "${user_js_file}"; then
|
||||
echo -n $'\n'"$pref_line" >> "$user_js_file"
|
||||
echo $'\t'"Successfully added a new preference in $user_js_file."
|
||||
elif grep --quiet "^$pref_line$" "$user_js_file"; then
|
||||
echo $'\t'"Skipping, preference is already set as expected in $user_js_file."
|
||||
else
|
||||
sed --in-place "/^$pref_start/c\\$pref_line" "$user_js_file"
|
||||
echo $'\t'"Successfully replaced the existing incorrect preference in $user_js_file."
|
||||
fi
|
||||
done
|
||||
if [ "$total_files_found" -eq 0 ]; then
|
||||
echo "No changes, no preference file is found."
|
||||
if [ "$total_profiles_found" -eq 0 ]; then
|
||||
echo 'No profile folders are found, no changes are made.'
|
||||
else
|
||||
echo "Ensured that $total_files_found profiles are compilant."
|
||||
echo "Preferences verified in $total_profiles_found profiles."
|
||||
fi
|
||||
revertCode: |-
|
||||
pref_name='{{ $prefName }}'
|
||||
pref_value='{{ $jsonValue }}'
|
||||
echo "Restoring \"$pref_name\" to its default."
|
||||
pref_file_paths=(
|
||||
~/.mozilla/firefox/*/prefs.js
|
||||
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/prefs.js
|
||||
~/snap/firefox/common/.mozilla/firefox/*/prefs.js
|
||||
echo "Reverting preference: \"$pref_name\" to its default."
|
||||
declare -a profile_paths=(
|
||||
~/.mozilla/firefox/*/
|
||||
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
|
||||
~/snap/firefox/common/.mozilla/firefox/*/
|
||||
)
|
||||
declare -i total_files_found=0
|
||||
for pref_file in "${pref_file_paths[@]}"; do
|
||||
if [ -f "$pref_file" ]; then
|
||||
((total_files_found++))
|
||||
echo "$pref_file:"
|
||||
pref_start="user_pref(\"$pref_name\","
|
||||
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
||||
if ! grep --quiet "^$pref_start" "${pref_file}"; then
|
||||
echo $'\t'"Skipping. Preference is not configured before."
|
||||
else
|
||||
if grep --quiet "^$pref_line$" "${pref_file}"; then
|
||||
sed --in-place "/^$pref_line/d" "$pref_file"
|
||||
echo $'\t'"Successfully restored preference value to its default."
|
||||
else
|
||||
echo $'\t'"Skipping, the preference has value that is not configured by privacy.sexy."
|
||||
fi
|
||||
declare -i total_profiles_found=0
|
||||
for profile_dir in "${profile_paths[@]}"; do
|
||||
user_js_file="${profile_dir}user.js"
|
||||
if [ ! -f "$user_js_file" ]; then
|
||||
continue
|
||||
fi
|
||||
((total_profiles_found++))
|
||||
echo "$user_js_file:"
|
||||
pref_start="user_pref(\"$pref_name\","
|
||||
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
||||
if ! grep --quiet "^$pref_start" "${user_js_file}"; then
|
||||
echo $'\t''Skipping, preference was not configured before.'
|
||||
elif grep --quiet "^$pref_line$" "${user_js_file}"; then
|
||||
sed --in-place "/^$pref_line/d" "$user_js_file"
|
||||
echo $'\t''Successfully reverted preference to default.'
|
||||
if ! grep --quiet '[^[:space:]]' "$user_js_file"; then
|
||||
rm "$user_js_file"
|
||||
echo $'\t''Removed user.js file as it became empty.'
|
||||
fi
|
||||
else
|
||||
echo $'\t''Skipping, the preference has value that is not configured by privacy.sexy.'
|
||||
fi
|
||||
done
|
||||
if [ "$total_files_found" -eq 0 ]; then
|
||||
echo "No changes, no preference file is found."
|
||||
if [ "$total_profiles_found" -eq 0 ]; then
|
||||
echo 'No reversion was necessary.'
|
||||
else
|
||||
echo "Ensured that $total_files_found profiles are compilant."
|
||||
echo "Preferences verified in $total_profiles_found profiles."
|
||||
fi
|
||||
-
|
||||
name: RenameFile
|
||||
|
||||
@@ -606,9 +606,46 @@ actions:
|
||||
wevtutil.exe cl %1 "%%i"
|
||||
)
|
||||
-
|
||||
name: Clean Windows Defender scan history
|
||||
docs: https://www.thewindowsclub.com/clear-windows-defender-protection-history
|
||||
code: del "%ProgramData%\Microsoft\Windows Defender\Scans\History\" /s /f /q
|
||||
name: Clear Defender scan (protection) history
|
||||
docs: |-
|
||||
This script deletes the scan history kept by Windows Defender on your computer. Windows Defender logs detected threats but also gathers
|
||||
and stores data about various other files it scans [1] [2]. While removing this history enhances your privacy, it might decrease security,
|
||||
as these logs assist in monitoring threats. By eliminating traces of your system's files, activities and any threats detected, you ensure
|
||||
no residual data can be utilized to study or analyze your computer's activities, thus protecting your privacy.
|
||||
|
||||
Defender keeps a log of various details whenever it scans your computer for threats. This includes [3] [4]:
|
||||
|
||||
- **Time**: The moment the threat was discovered.
|
||||
- **Threat Status**: The action carried out against the threat.
|
||||
- **Virus Type**: The type or category of the virus.
|
||||
- **Threat ID**: A unique identifier for the threat.
|
||||
- **Virus Name**: The name of the virus.
|
||||
- **File Path**: The location of the threat on your computer.
|
||||
- **File Hash**: A unique code representing the file.
|
||||
- **Quarantine File Name (GUID)**: The name given to the quarantined threat.
|
||||
- **File Size**: The size of the file.
|
||||
|
||||
When you first set up Windows, it conducts an initial scan [1]. This scan identifies system files that won't require future
|
||||
scans [1]. These 'safe' files are saved in a unique folder, which becomes a part of the scan history [1].
|
||||
|
||||
If a threat is recognized, Windows Defender will notify you [4]. Regardless of whether you choose to run the file or not, a
|
||||
`DetectionHistory` file is created [2]. This file is stored in a specific folder
|
||||
(`%ProgramData%\Microsoft\Windows Defender\Scans\History\Service\DetectionHistory\[numbered folder]\`), and it contains a
|
||||
system-generated ID for the event [2].
|
||||
|
||||
> **Caution**: Deleting these logs may decrease your security. These logs help in keeping track of potential threats and their sources,
|
||||
allowing for a more proactive response in future encounters. Without this history, Windows Defender might not recognize recurring threats
|
||||
as quickly, possibly leaving your system more vulnerable. It's essential to understand that you're making a trade-off between enhanced
|
||||
privacy and potentially reduced security.
|
||||
|
||||
[1]: https://web.archive.org/web/20230829142700/https://download.microsoft.com/download/7/e/7/7e7662cf-cbea-470b-a97e-ce7ce0d98dc2/win7perf.docx "Performance Testing Guide for Windows | Microsoft"
|
||||
[2]: https://web.archive.org/web/20230829143754/https://www.sans.org/blog/uncovering-windows-defender-real-time-protection-history-with-dhparser/ "Uncovering Windows Defender Real-time Protection History with DHParser | SANS Alumni Blog"
|
||||
[3]: https://web.archive.org/web/20230829144957/https://learn.microsoft.com/en-us/previous-versions/windows/desktop/defender/msft-mpthreatdetection "MSFT\_MpThreatDetection class | Microsoft Learn"
|
||||
[4]: https://web.archive.org/web/20230829144434/https://forensafe.com/blogs/windows_defender.html "Windows Defender | Forensafe"
|
||||
call:
|
||||
function: RunInlineCodeAsTrustedInstaller # Otherwise it cannot access/delete files under `Scans\History`, see https://github.com/undergroundwires/privacy.sexy/issues/246
|
||||
parameters:
|
||||
code: del "%ProgramData%\Microsoft\Windows Defender\Scans\History" /s /f /q
|
||||
-
|
||||
name: Clear credentials from Windows Credential Manager
|
||||
code: |-
|
||||
@@ -1094,32 +1131,130 @@ actions:
|
||||
serviceName: wercplsupport # Check: (Get-Service -Name wercplsupport).StartType
|
||||
defaultStartupMode: Manual # Allowed values: Automatic | Manual
|
||||
-
|
||||
category: Disable automatic driver updates by Windows Update
|
||||
category: Disable Windows Update data collection
|
||||
children:
|
||||
-
|
||||
name: Disable device metadata retrieval (breaks auto updates)
|
||||
recommend: strict
|
||||
docs:
|
||||
- https://www.stigviewer.com/stig/windows_server_2012_member_server/2014-01-07/finding/V-21964
|
||||
- https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-deviceinstallation#deviceinstallation-preventdevicemetadatafromnetwork
|
||||
code: |-
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f
|
||||
revertCode: |-
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 0 /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 0 /f
|
||||
-
|
||||
name: Do not include drivers with Windows Updates
|
||||
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsUpdate::ExcludeWUDriversInQualityUpdate
|
||||
recommend: strict
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "ExcludeWUDriversInQualityUpdate" /t REG_DWORD /d 1 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "ExcludeWUDriversInQualityUpdate" /t REG_DWORD /d 0 /f
|
||||
category: Disable automatic driver updates by Windows Update
|
||||
children:
|
||||
-
|
||||
name: Disable device metadata retrieval (breaks auto updates)
|
||||
recommend: strict
|
||||
docs:
|
||||
- https://www.stigviewer.com/stig/windows_server_2012_member_server/2014-01-07/finding/V-21964
|
||||
- https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-deviceinstallation#deviceinstallation-preventdevicemetadatafromnetwork
|
||||
code: |-
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f
|
||||
revertCode: |-
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 0 /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 0 /f
|
||||
-
|
||||
name: Do not include drivers with Windows Updates
|
||||
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsUpdate::ExcludeWUDriversInQualityUpdate
|
||||
recommend: strict
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "ExcludeWUDriversInQualityUpdate" /t REG_DWORD /d 1 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "ExcludeWUDriversInQualityUpdate" /t REG_DWORD /d 0 /f
|
||||
-
|
||||
name: Prevent Windows Update for device driver search
|
||||
docs: https://www.stigviewer.com/stig/windows_7/2018-02-12/finding/V-21965
|
||||
recommend: strict
|
||||
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DriverSearching" /v "SearchOrderConfig" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DriverSearching" /v "SearchOrderConfig" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
name: Prevent Windows Update for device driver search
|
||||
docs: https://www.stigviewer.com/stig/windows_7/2018-02-12/finding/V-21965
|
||||
recommend: strict
|
||||
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DriverSearching" /v "SearchOrderConfig" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DriverSearching" /v "SearchOrderConfig" /t REG_DWORD /d 1 /f
|
||||
category: Disable obtaining updates from other PCs on the Internet (delivery optimization)
|
||||
docs: |-
|
||||
Windows Delivery Optimization is a feature introduced by Microsoft to facilitate a more efficient downloading process for Windows
|
||||
updates, upgrades, and applications [1] [2]. Instead of exclusively relying on Microsoft's servers, this feature identifies other
|
||||
PCs on a user's local network or even across the internet that already possess the desired updates or applications [2]. By breaking
|
||||
the download into smaller segments and fetching each from the fastest and most reliable source, which can include other PCs, the
|
||||
system ensures more efficient downloads [2]. To support this process, Delivery Optimization uses a local cache to temporarily store
|
||||
downloaded files [2].
|
||||
|
||||
While Delivery Optimization is designed for speed and reliability, its operation raises privacy concerns. Specifically, when enabled,
|
||||
it can distribute updates and applications from one user's PC to others [2], sharing users' data such as their IP addresses [3].
|
||||
|
||||
Benefits of disabling Delivery Optimization for privacy:
|
||||
|
||||
- **Minimizing Data Sharing**: By turning off Delivery Optimization, users ensure that updates and apps are neither downloaded from nor sent
|
||||
to other devices [2]. This guarantees that all data remains strictly on the user's device [2] and the user IP is not shared [3].
|
||||
- **Storage Conservation**: Users can save storage space by eliminating the local cache utilized by Delivery Optimization.
|
||||
- **Guaranteed Source Authenticity**: Although Microsoft ensures the authenticity of updates and apps shared via Delivery Optimization [2],
|
||||
disabling the feature guarantees that all updates and apps come directly from Microsoft's servers, eliminating potential intermediaries.
|
||||
- **Bandwidth Conservation**: With the feature off, updates are restricted to direct downloads from Microsoft [1]. This is beneficial
|
||||
for users on metered or capped internet connections, as it allows for more effective bandwidth monitoring [2].
|
||||
- **Enhanced Security**: Devices using Delivery Optimization open port 7680 to accept peer requests [4]. Disabling the feature avoids this,
|
||||
ensuring users are not exposed to unwanted inbound traffic and enhancing security [5].
|
||||
- **VPN Protection**: Although Delivery Optimization attempts to detect VPNs and halts uploads when a VPN connection is detected [4], disabling
|
||||
it removes any risk of unintended data sharing over a VPN.
|
||||
|
||||
Notably, the USA government [5] and Department of Defense (DoD) in the USA [6] recommends disabling this feature.
|
||||
|
||||
[1]: https://web.archive.org/web/20230914164204/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization "What is Delivery Optimization? - Windows Deployment | Microsoft Learn"
|
||||
[2]: https://web.archive.org/web/20230914164355/https://support.microsoft.com/en-us/windows/windows-update-delivery-optimization-and-privacy-bf86a244-8f26-a3c7-a137-a43bfbe688e8 "Windows Update Delivery Optimization and privacy - Microsoft Support"
|
||||
[3]: https://web.archive.org/web/20230914164646/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization-monitor "Monitor Delivery Optimization - Windows Deployment | Microsoft Learn"
|
||||
[4]: https://web.archive.org/web/20230905120220/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization-faq "Delivery Optimization Frequently Asked Questions - Windows Deployment | Microsoft Learn"
|
||||
[5]: https://web.archive.org/web/20230914171139/https://www.irs.gov/pub/irs-utl/win10.xlsx "Internal Revenue Service Office of Safeguards - Windows 10 | irs.gov"
|
||||
[6]: https://web.archive.org/web/20230914171410/https://www.stigviewer.com/stig/windows_10/2019-01-04/finding/V-65681 "Windows Update must not obtain updates from other PCs on the Internet | stigviewer.com"
|
||||
children:
|
||||
-
|
||||
name: Disable peering download method for Windows Updates
|
||||
recommend: standard
|
||||
docs: |-
|
||||
This script modifies Delivery Optimization's download method for Windows Updates [1] to disable peering. When this script is run, it sets the
|
||||
download method to `0`, which means "HTTP only, no peering" [1] [2]. As a result, Windows Updates are downloaded solely from the internet and
|
||||
not from other computers on the network (referred to as "peer-to-peer") [3].
|
||||
|
||||
Peer-to-peer is a method where multiple computers share data amongst themselves. For Windows Updates, the default setting is for computers
|
||||
within a network to share updates (called LAN mode, represented by the value `1`) [1] [2].
|
||||
|
||||
Changing the setting to "HTTP only" reduces potential vulnerabilities [3]. When updates are fetched only from official servers, there's
|
||||
less chance of unwanted or malicious data entering the system. This is why the Department of Defense (DoD) in the USA [4] and USA government [3]
|
||||
recommends this setting. They assert that leaving it in its default configuration could expose the system to additional risks [3].
|
||||
|
||||
[1]: https://web.archive.org/web/20230914171524/https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-deliveryoptimization "DeliveryOptimization Policy CSP - Windows Client Management | Microsoft Learn"
|
||||
[2]: https://web.archive.org/web/20230914171842/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization-reference "Delivery Optimization reference - Windows Deployment | Microsoft Learn"
|
||||
[3]: https://web.archive.org/web/20230914171139/https://www.irs.gov/pub/irs-utl/win10.xlsx "Internal Revenue Service Office of Safeguards - Windows 10 | irs.gov"
|
||||
[4]: https://web.archive.org/web/20230914171410/https://www.stigviewer.com/stig/windows_10/2019-01-04/finding/V-65681 "Windows Update must not obtain updates from other PCs on the Internet | stigviewer.com"
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\DeliveryOptimization" /v "DODownloadMode" /t "REG_DWORD" /d 0 /f
|
||||
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\DeliveryOptimization" /v "DODownloadMode" /f 2>nul # Key does not exists since Windows 10 21H2, Windows 11 22H2
|
||||
-
|
||||
name: Disable "Delivery Optimization" service (breaks Microsoft Store downloads)
|
||||
recommend: strict
|
||||
docs: |-
|
||||
Delivery Optimization is a Windows feature that provides the Windows Updates through peer-to-peer sharing [1]. In simple terms, instead of solely
|
||||
relying on Microsoft's servers for updates, your computer can also fetch them from other devices that already possess the necessary files.
|
||||
|
||||
The "Delivery Optimization" service manages these content delivery tasks [2] [3]. It orchestrates the retrieval of updates both from other Windows users [3].
|
||||
In doing so, it connects to various Microsoft service points to collect data, such as policies, content details, device specifications, and information about
|
||||
other Windows users [3]. This data sharing raises privacy concerns.
|
||||
|
||||
This service also logs IP addresses [4] of peers which can be considered personal data. It listens on port 7680 for TCP/UDP traffic [5] that may expose the user
|
||||
to unwanted inbound traffic and enhancing security [6].
|
||||
|
||||
By default, the "Delivery Optimization" service is set to start automatically when Windows boots up [2]. This script alters that behavior, ensuring
|
||||
it doesn't run unless explicitly started by the user.
|
||||
|
||||
Taking control of this service prevents Microsoft from activating peer-to-peer sharing, enhancing user privacy. It ensures your device doesn't share update data
|
||||
or fetch it from arbitrary peers.
|
||||
|
||||
> **Caution**: Disabling this service affects the functionality of Windows Store. It plays a role not just in Windows Updates but also in Microsoft Store app
|
||||
downloads, especially since Windows 11 [7]. There have been reported issues with some app downloads on Windows 10 [8].
|
||||
|
||||
[1]: https://web.archive.org/web/20230914164204/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization "What is Delivery Optimization? - Windows Deployment | Microsoft Learn"
|
||||
[2]: https://web.archive.org/web/20230905120815/https://learn.microsoft.com/en-us/windows/iot/iot-enterprise/optimize/services#delivery-optimization "Guidance on disabling system services on Windows IoT Enterprise | Microsoft Learn"
|
||||
[3]: https://web.archive.org/web/20230914172129/https://learn.microsoft.com/en-us/windows/deployment/do/delivery-optimization-workflow "Delivery Optimization client-service communication explained - Windows Deployment | Microsoft Learn"
|
||||
[4]: https://web.archive.org/web/20230914164646/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization-monitor "Monitor Delivery Optimization - Windows Deployment | Microsoft Learn"
|
||||
[5]: https://web.archive.org/web/20230914172319/https://learn.microsoft.com/en-us/security/privileged-access-workstations/privileged-access-deployment "Deploying a privileged access solution | Microsoft Learn"
|
||||
[6]: https://web.archive.org/web/20230914171139/https://www.irs.gov/pub/irs-utl/win10.xlsx "Internal Revenue Service Office of Safeguards - Windows 10 | irs.gov"
|
||||
[7]: https://web.archive.org/web/20230914164355/https://support.microsoft.com/en-us/windows/windows-update-delivery-optimization-and-privacy-bf86a244-8f26-a3c7-a137-a43bfbe688e8 "Windows Update Delivery Optimization and privacy - Microsoft Support"
|
||||
[8]: https://github.com/undergroundwires/privacy.sexy/issues/173 "[BUG] Error 0x80004002 on Microsoft Store when attempting to download an app · Issue #173 · undergroundwires/privacy.sexy"
|
||||
call:
|
||||
function: DisableServiceInRegistry
|
||||
# Using registry way because because other options such as "sc config" or
|
||||
# "Set-Service" returns "Access is denied" since Windows 10 1809.
|
||||
parameters:
|
||||
serviceName: DoSvc # Check: (Get-Service -Name 'DoSvc').StartType
|
||||
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
|
||||
-
|
||||
name: Disable cloud speech recognition
|
||||
recommend: standard
|
||||
@@ -1776,12 +1911,26 @@ actions:
|
||||
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" /v "DisableSoftLanding" /t REG_DWORD /d "0" /f
|
||||
-
|
||||
name: Disable Windows Spotlight (random wallpaper on lock screen)
|
||||
recommend: standard
|
||||
docs:
|
||||
- https://docs.microsoft.com/en-us/windows/configuration/windows-spotlight
|
||||
- https://docs.microsoft.com/en-us/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services#25-windows-spotlight
|
||||
recommend: strict
|
||||
docs: |-
|
||||
The script disables the Windows Spotlight feature. Windows Spotlight is a feature in Windows 10 and Windows 11 [1] that automatically downloads
|
||||
and displays random wallpapers on the lock screen [1] [2]. These images are sourced from the internet [1] [2] [3]. At times, it might also promote
|
||||
various Microsoft products, services [1] [2], or even third-party apps and content [4].
|
||||
|
||||
When the lock screen fetches images from the internet, there's a silent data exchange happening. This can inadvertently reveal details about the
|
||||
user's device or their preferences.
|
||||
|
||||
To mitigate this potential privacy risk, the script makes a change to a key (`DisableWindowsSpotlightFeatures`) in the Windows operating system [3].
|
||||
Originally, Windows Spotlight is turned on unless the user decides otherwise [2].
|
||||
By applying this script, users can be sure their lock screen remains private and doesn't retrieve wallpapers from the internet, eliminating potential
|
||||
data leaks.
|
||||
|
||||
[1]: https://web.archive.org/web/20230911110727/https://support.microsoft.com/en-us/windows/personalize-your-lock-screen-81dab9b0-35cf-887c-84a0-6de8ef72bea0 "Personalize your lock screen - Microsoft Support"
|
||||
[2]: https://web.archive.org/web/20230911110748/https://learn.microsoft.com/en-us/windows/configuration/windows-spotlight "Configure Windows Spotlight on the lock screen - Configure Windows | Microsoft Learn"
|
||||
[3]: https://web.archive.org/web/20230911110911/https://learn.microsoft.com/en-us/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services#25-windows-spotlight "Manage connections from Windows 10 and Windows 11 Server/Enterprise editions operating system components to Microsoft services - Windows Privacy | Microsoft Learn"
|
||||
[4]: https://web.archive.org/web/20230911110921/https://download.microsoft.com/download/8/F/B/8FBD2E85-8852-45EC-8465-92756EBD9365/Windows10andWindowsServer2016PolicySettings.xlsx "Group Policy Settings Reference - Microsoft"
|
||||
code: reg add "HKLM\Software\Policies\Microsoft\Windows\CloudContent" /v "DisableWindowsSpotlightFeatures" /t "REG_DWORD" /d "1" /f
|
||||
revertCode: reg add "HKLM\Software\Policies\Microsoft\Windows\CloudContent" /v "DisableWindowsSpotlightFeatures" /t "REG_DWORD" /d "0" /f
|
||||
revertCode: reg delete "HKLM\Software\Policies\Microsoft\Windows\CloudContent" /v "DisableWindowsSpotlightFeatures" /f 2>nul # Key does not exists since Windows 10 21H2, Windows 11 22H2
|
||||
-
|
||||
name: Disable Microsoft consumer experiences
|
||||
recommend: standard
|
||||
@@ -2272,8 +2421,7 @@ actions:
|
||||
function: SetVsCodeSetting
|
||||
parameters:
|
||||
setting: update.mode
|
||||
powerShellValue: >-
|
||||
'manual'
|
||||
powerShellValue: manual
|
||||
-
|
||||
name: Show Release Notes from Microsoft online service after an update
|
||||
call:
|
||||
@@ -2415,21 +2563,25 @@ actions:
|
||||
category: Chromium Edge settings
|
||||
children:
|
||||
-
|
||||
name: Disable Edge usage and crash-related data reporting (shows "Your browser is managed") # Obselete since Microsoft Edge version 89
|
||||
recommend: standard
|
||||
docs:
|
||||
- https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::MetricsReportingEnabled
|
||||
- https://docs.microsoft.com/en-us/DeployEdge/microsoft-edge-policies#metricsreportingenabled
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /f
|
||||
-
|
||||
name: Disable sending site information (shows "Your browser is managed") # Obselete since Microsoft Edge version 89
|
||||
name: Disable Edge diagnostic data sending (shows "Your browser is managed")
|
||||
recommend: standard
|
||||
docs:
|
||||
- https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::SendSiteInfoToImproveServices
|
||||
- https://docs.microsoft.com/en-us/DeployEdge/microsoft-edge-policies#sendsiteinfotoimproveservices
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "SendSiteInfoToImproveServices" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "SendSiteInfoToImproveServices" /f
|
||||
- http://archive.today/2023.08.26-152941/https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::DiagnosticData
|
||||
- https://learn.microsoft.com/DeployEdge/microsoft-edge-policies#diagnosticdata
|
||||
- http://archive.today/2023.08.26-152952/https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::MetricsReportingEnabled
|
||||
- https://learn.microsoft.com/en-gb/DeployEdge/microsoft-edge-policies#metricsreportingenabled
|
||||
- http://archive.today/2023.08.26-153019/https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::SendSiteInfoToImproveServices
|
||||
- https://learn.microsoft.com/DeployEdge/microsoft-edge-policies#sendsiteinfotoimproveservices
|
||||
code: |-
|
||||
:: Disabling metrics and site info sending for Edge v88 ≥
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /t REG_DWORD /d 0 /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "SendSiteInfoToImproveServices" /t REG_DWORD /d 0 /f
|
||||
:: Disabling diagnostic data (replacing metrics and site info sending since Edge v89 ≤)
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "DiagnosticData" /t REG_DWORD /d 0 /f
|
||||
revertCode: |-
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /f 2>nul
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "SendSiteInfoToImproveServices" /f 2>nul
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "DiagnosticData" /f 2>nul
|
||||
-
|
||||
name: Disable Automatic Installation of Microsoft Edge Chromium
|
||||
docs:
|
||||
@@ -2456,7 +2608,7 @@ actions:
|
||||
recommend: standard
|
||||
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.EdgeUI::DisableRecentApps
|
||||
code: reg add "HKCU\Software\Policies\Microsoft\Windows\EdgeUI" /v "DisableRecentApps" /t REG_DWORD /d 1 /f
|
||||
revertCode: reg add "HKCU\Software\Policies\Microsoft\Windows\EdgeUI" /v "DisableRecentApps" /t REG_DWORD /d 0/f
|
||||
revertCode: reg add "HKCU\Software\Policies\Microsoft\Windows\EdgeUI" /v "DisableRecentApps" /t REG_DWORD /d 0 /f
|
||||
-
|
||||
name: Turn off backtracking
|
||||
recommend: standard
|
||||
@@ -3871,7 +4023,7 @@ actions:
|
||||
code: reg add "HKLM\Software\Policies\Microsoft\Windows Defender\Scan" /v "DisableRestorePoint" /t REG_DWORD /d "1" /f
|
||||
revertCode: reg delete "HKLM\Software\Policies\Microsoft\Windows Defender\Scan" /v "DisableRestorePoint" /f 2>nul
|
||||
-
|
||||
name: Set minumum time for keeping files in scan history folder
|
||||
name: Set minimum time for keeping files in scan history folder
|
||||
docs:
|
||||
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::Scan_PurgeItemsAfterDelay
|
||||
# Managing with MpPreference module:
|
||||
@@ -4891,29 +5043,233 @@ actions:
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost" /v "EnableWebContentEvaluation" /t REG_DWORD /d "1" /f
|
||||
reg delete "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost" /v "EnableWebContentEvaluation" /f 2>nul
|
||||
-
|
||||
name: Disable automatic updates
|
||||
docs:
|
||||
- https://docs.microsoft.com/fr-fr/security-updates/windowsupdateservices/18127152
|
||||
- http://batcmd.com/windows/10/services/usosvc/
|
||||
call:
|
||||
category: Disable automatic updates
|
||||
docs: |-
|
||||
Disabling automatic updates is often considered counterintuitive when it comes to securing your system. However, there are substantial arguments
|
||||
to consider this option if you're privacy-centric:
|
||||
|
||||
1. **Patching and Pre-Approval**: Manual control over update deployment allows for pre-emptive approval of patches. This strategy is useful
|
||||
in environments requiring the highest level of security. For instance, military agencies frequently employ air-gapped systems that mandate
|
||||
careful review of each update to mitigate risks such as potential backdoors or data leaks. Similarly, financial institutions often
|
||||
resort to staged rollouts of updates, subjecting them to an in-depth analysis of their implications on security and privacy before broad
|
||||
implementation.
|
||||
|
||||
2. **Telemetry and Data Transmission**: Automatic updates often come embedded with telemetry data collection mechanisms. Disabling these
|
||||
updates facilitates granular control over the data transmitted back to Microsoft servers. Thus, the decision to disable automatic updates
|
||||
allows you to control the timing and nature of information relayed to these servers.
|
||||
|
||||
3. **Peer-to-Peer Data Exposure**: Windows employs a Peer-to-Peer (P2P) approach to facilitate update distribution, which can
|
||||
reveal your IP address and some system details to peer systems [1].
|
||||
|
||||
4. **Configurational integrity**: Updates have the capacity to change pre-configured settings without explicit user consent. This could
|
||||
result in unintended alteration of your privacy settings, leaving you exposed until you realize the change.
|
||||
|
||||
**Security implications**: While controlling updates enhances your privacy, it can leave your system vulnerable to unpatched exploits.
|
||||
Ensure that you manually review and apply updates on a regular basis. You're essentially trading off some security for a heightened level of
|
||||
privacy.
|
||||
|
||||
[1]: https://web.archive.org/web/20230905120220/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization-faq "Delivery Optimization Frequently Asked Questions - Windows Deployment | Microsoft Learn"
|
||||
children:
|
||||
-
|
||||
function: RunInlineCode
|
||||
parameters:
|
||||
code: |-
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "0" /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "2" /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /t "REG_DWORD" /d "0" /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime" /t "REG_DWORD" /d "3" /f
|
||||
revertCode: |-
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "1" /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "3" /f
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /f 2>nul
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime /f 2>nul
|
||||
name: Disable Automatic Updates (AU) feature
|
||||
docs: |-
|
||||
This script deactivates the Automatic Updates feature in Windows. By disabling Automatic Updates,
|
||||
you gain control over when your system is updated, which may be preferable in specific
|
||||
privacy-sensitive environments.
|
||||
|
||||
The script changes a specific setting in your computer's registry, with a key called `NoAutoUpdate`, which has
|
||||
two possible states [1] [2]:
|
||||
|
||||
- `0`: Automatic Updates are enabled.
|
||||
- `1`: Automatic Updates are disabled.
|
||||
|
||||
By default, Windows comes with Automatic Updates enabled, meaning the `NoAutoUpdate` is set to `0` [3].
|
||||
|
||||
Running this script will set `NoAutoUpdate` to `1`, turning off Automatic Updates [1] [2] [3].
|
||||
In doing so, you prevent your computer from automatically receiving updates, which is a feature
|
||||
that could be considered intrusive or unwanted in some privacy-conscious settings.
|
||||
|
||||
It configure your computer to not automatically download and install updates without your explicit permission.
|
||||
|
||||
[1]: https://web.archive.org/web/20230807165936/https://learn.microsoft.com/de-de/security-updates/windowsupdateservices/18127499 "Configure Automatic Updates in a Non–Active Directory Environment | Microsoft Learn"
|
||||
[2]: https://web.archive.org/web/20221001051250/https://support.microsoft.com/en-us/topic/incorrect-automatic-updates-notification-is-received-even-though-au-options-are-disabled-in-windows-8-1-and-windows-server-2012-r2-18b4b73a-3910-9408-809c-7eaad0e1fbc7 "Incorrect Automatic Updates notification is received even though AU options are disabled in Windows 8.1 and Windows Server 2012 R2 - Microsoft Support"
|
||||
[3]: https://web.archive.org/web/20230711172555/https://learn.microsoft.com/en-us/windows/deployment/update/waas-wu-settings#configuring-automatic-updates-by-editing-the-registry "Manage additional Windows Update settings - Windows Deployment | Microsoft Learn"
|
||||
call:
|
||||
function: RunInlineCode
|
||||
parameters:
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "1" /f
|
||||
# Default value is `0` since Windows 10 21H2 and Windows 11 21H2
|
||||
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "0" /f
|
||||
-
|
||||
function: DisableService
|
||||
parameters:
|
||||
serviceName: UsoSvc # Check: (Get-Service -Name 'UsoSvc').StartType
|
||||
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
|
||||
name: Disable installing Windows updates without user approval
|
||||
docs: |-
|
||||
This script changes how your Windows computer handles automatic updates by modifying the `AUOptions` registry key.
|
||||
After running this script, your computer will notify you before downloading any updates [1] [2] [3].
|
||||
|
||||
In the default setup, your Windows system is configured to download and install updates automatically without notifying you [4].
|
||||
This means that new updates could be installed on your system without your explicit approval.
|
||||
|
||||
By forcing Windows to notify you before downloading updates, this script hands back control over your system to you.
|
||||
This feature enhances your privacy and minimizes risks because you get to manually review and approve each update before it's installed.
|
||||
|
||||
To explain the technical aspect, the `AUOptions` registry key is a setting stored under
|
||||
`HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU` in your computer's registry [1] [3].
|
||||
A value of `2` for `AUOptions` means that you will be notified before any updates are downloaded and installed [1] [2].
|
||||
On older versions of Windows, setting this key to `1` would prevent the system from even checking for updates [5].
|
||||
However, starting from Windows 10, the key `1` has a different meaning [2][3].
|
||||
|
||||
Running this script doesn't disable updates; it just ensures that you are informed and have the final say on
|
||||
whether to download them or not.
|
||||
|
||||
[1]: https://web.archive.org/web/20230807165936/https://learn.microsoft.com/de-de/security-updates/windowsupdateservices/18127499 "Configure Automatic Updates in a Non–Active Directory Environment | Microsoft Learn"
|
||||
[2]: https://web.archive.org/web/20230711172555/https://learn.microsoft.com/en-us/windows/deployment/update/waas-wu-settings#configuring-automatic-updates-by-editing-the-registry "Manage additional Windows Update settings - Windows Deployment | Microsoft Learn"
|
||||
[3]: https://web.archive.org/web/20230815051303/https://learn.microsoft.com/en-us/windows/deployment/update/waas-restart#registry-keys-used-to-manage-restart "Manage device restarts after updates - Windows Deployment | Microsoft Learn"
|
||||
[4]: https://web.archive.org/web/20230826081345/https://learn.microsoft.com/en-US/troubleshoot/windows-client/deployment/update-windows-update-agent "Update Windows Update Agent to latest version - Windows Client | Microsoft Learn"
|
||||
[5]: https://web.archive.org/web/20221001051250/https://support.microsoft.com/en-us/topic/incorrect-automatic-updates-notification-is-received-even-though-au-options-are-disabled-in-windows-8-1-and-windows-server-2012-r2-18b4b73a-3910-9408-809c-7eaad0e1fbc7 "Incorrect Automatic Updates notification is received even though AU options are disabled in Windows 8.1 and Windows Server 2012 R2 - Microsoft Support"
|
||||
call:
|
||||
function: RunInlineCode
|
||||
parameters:
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "2" /f
|
||||
# Default value is `4` since Windows 10 21H2 and Windows 11 21H2
|
||||
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "4" /f
|
||||
-
|
||||
name: Disable automatic daily installation of Windows updates
|
||||
docs: |-
|
||||
This script stops Windows from automatically installing updates every day. By doing so, you gain control over when update
|
||||
happen on your computer [1] [2].
|
||||
|
||||
By default, Windows is set to automatically update every day [2]. Having control over the update timing allows you to review
|
||||
what is being changed, thereby protecting your privacy and enhancing your system's security.
|
||||
|
||||
Technically, what the script does is remove a specific setting in the computer's system registry, the `ScheduledInstallDay` key
|
||||
from `HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU` [1] [2].
|
||||
|
||||
Disabling the scheduled install day ensures that updates won't be forcibly applied on a specific day of the week.
|
||||
|
||||
[1]: https://web.archive.org/web/20230711172555/https://learn.microsoft.com/en-us/windows/deployment/update/waas-wu-settings#configuring-automatic-updates-by-editing-the-registry "Manage additional Windows Update settings - Windows Deployment | Microsoft Learn"
|
||||
[2]: https://web.archive.org/web/20230708165017/https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-update#scheduledinstallday "Update Policy CSP - Windows Client Management | Microsoft Learn"
|
||||
call:
|
||||
function: RunInlineCode
|
||||
parameters:
|
||||
code: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /f 2>nul
|
||||
revertCode: >-
|
||||
:: This key does not exist by default since Windows 10 21H2 and Windows 11 21H2
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /f 2>nul
|
||||
-
|
||||
name: Disable scheduled automatic updates
|
||||
docs: |-
|
||||
This script turns off the automatic installation of Windows updates that are set to occur at a specific time.
|
||||
By doing this, you take back control over when your computer updates itself [1] [2] [3].
|
||||
The default behavior is to install updates at 3 AM [3].
|
||||
|
||||
Windows updates can be important for system security, but automatic installation could occur at inconvenient times and may even
|
||||
restart your computer without prior warning. This could interrupt your tasks and may send data about your system to external servers.
|
||||
By disabling the automatic scheduled installation time, you can manually control when updates are installed [3], ensuring that you're
|
||||
aware of any changes to your system.
|
||||
|
||||
The script works by removing a specific registry key called `ScheduledInstallTime` under
|
||||
`HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU` [2] [3]. This is the system setting that controls the scheduled update time.
|
||||
|
||||
[1]: https://web.archive.org/web/20230813094618/https://learn.microsoft.com/fr-fr/security-updates/windowsupdateservices/18127152 "Configure Automatic Updates in a Non–Active Directory Environment | Microsoft Learn"
|
||||
[2]: https://web.archive.org/web/20230711172555/https://learn.microsoft.com/en-us/windows/deployment/update/waas-wu-settings#configuring-automatic-updates-by-editing-the-registry "Manage additional Windows Update settings - Windows Deployment | Microsoft Learn"
|
||||
[3]: https://web.archive.org/web/20230708165017/https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-update#scheduledinstalltime "Update Policy CSP - Windows Client Management | Microsoft Learn"
|
||||
call:
|
||||
function: RunInlineCode
|
||||
parameters:
|
||||
code: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime" /f 2>nul
|
||||
revertCode: >-
|
||||
:: This key does not exist by default since Windows 10 21H2 and Windows 11 21H2
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime" /f 2>nul
|
||||
-
|
||||
category: Disable Windows update services
|
||||
docs: |-
|
||||
The scripts in this category offer users the ability to control Windows services related to system updates.
|
||||
These services manage how and when your system receives updates from Microsoft. By limiting or disabling these services,
|
||||
users can decide when to update their system, reducing unexpected changes. Moreover, a system with fewer running
|
||||
services uses fewer resources, which can improve overall performance.
|
||||
|
||||
Disabling these update services is also a privacy measure. Some updates can change privacy settings or add features that
|
||||
collect user data. By controlling update services, users can review and approve any changes before they take effect.
|
||||
children:
|
||||
-
|
||||
name: Disable "Windows Update" (`wuauserv`) service
|
||||
docs: |-
|
||||
This script turns off the Windows Update service, which is technically known as Windows Update Agent [1] [2].
|
||||
By disabling this service, the automatic detection, download, and installation of updates for both Windows and other
|
||||
installed programs are halted [3] [4].
|
||||
|
||||
Update can often come bundled with changes that could affect your privacy settings or introduce features that collect
|
||||
more of your data. Taking control of when and how updates are applied provides you with the opportunity to review any changes
|
||||
before they take effect.
|
||||
|
||||
By default, the service is enabled and set to start up manually [5].
|
||||
|
||||
If you disable this service, you won't be able to use the Windows Update feature for automatic updates [5]. Additionally,
|
||||
other software on your c omputer won't be able to access the functionalities provided by the Windows Update Agent,
|
||||
commonly known as WUA API [5].
|
||||
|
||||
[1]: https://web.archive.org/web/20230902020255/https://learn.microsoft.com/en-us/troubleshoot/windows-client/deployment/additional-resources-for-windows-update "Additional resources for Windows Update - Windows Client | Microsoft Learn"
|
||||
[2]: https://web.archive.org/web/20230711221240/https://learn.microsoft.com/en-us/troubleshoot/mem/configmgr/update-management/troubleshoot-software-update-scan-failures "Troubleshoot software update scan failures - Configuration Manager | Microsoft Learn"
|
||||
[3]: https://web.archive.org/web/20230905120348/https://learn.microsoft.com/en-us/troubleshoot/windows-client/performance/windows-devices-fail-boot-after-installing-kb4041676-kb4041691 "Windows devices may fail to boot after installing October 10 version of KB 4041676 or 4041691 that contained a publishing issue - Windows Client | Microsoft Learn"
|
||||
[4]: https://web.archive.org/web/20230905120345/https://learn.microsoft.com/en-us/windows-server/administration/server-core/server-core-servicing "Patching Server Core | Microsoft Learn"
|
||||
[5]: https://web.archive.org/web/20230905120445/https://learn.microsoft.com/en-us/windows/deployment/update/prepare-deploy-windows "Security guidelines for system services in Windows Server 2016 | Microsoft Learn"
|
||||
call:
|
||||
function: DisableService
|
||||
parameters:
|
||||
serviceName: wuauserv # Check: (Get-Service -Name 'wuauserv').StartType
|
||||
defaultStartupMode: Manual # Allowed values: Automatic | Manual
|
||||
-
|
||||
name: Disable "Update Orchestrator Service" (`UsoSvc`)
|
||||
docs: |-
|
||||
This script disables the Update Orchestrator Service, also known as "Update Orchestrator Service for Windows Update" [1].
|
||||
This service is in charge of managing the download and installation of Windows updates [1] [2].
|
||||
|
||||
By default, the service is enabled and set to start up manually [1].
|
||||
|
||||
While updates can be crucial for the security of your system, this service can sometimes install them without your approval.
|
||||
This lack of control can pose risks to your privacy, as data might be sent from your system without your knowledge.
|
||||
|
||||
Windows updates relies on this service [1] [3].
|
||||
If stopped, your devices will not be able to download and install latest updates [1].
|
||||
|
||||
Turning off this service can affect the update process and might cause issues like freezing during update scanning [3].
|
||||
|
||||
[1]: https://web.archive.org/web/20230905120757/https://learn.microsoft.com/en-us/windows-server/security/windows-services/security-guidelines-for-disabling-system-services-in-windows-server "Security guidelines for system services in Windows Server 2016 | Microsoft Learn"
|
||||
[2]: https://web.archive.org/web/20230905120348/https://learn.microsoft.com/en-us/troubleshoot/windows-client/performance/windows-devices-fail-boot-after-installing-kb4041676-kb4041691 "Windows devices may fail to boot after installing October 10 version of KB 4041676 or 4041691 that contained a publishing issue - Windows Client | Microsoft Learn"
|
||||
[3]: https://web.archive.org/web/20230905120445/https://learn.microsoft.com/en-us/windows/deployment/update/prepare-deploy-windows "Security guidelines for system services in Windows Server 2016 | Microsoft Learn"
|
||||
call:
|
||||
function: DisableService
|
||||
parameters:
|
||||
serviceName: UsoSvc # Check: (Get-Service -Name 'UsoSvc').StartType
|
||||
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
|
||||
-
|
||||
name: Disable "Windows Update Medic Service" (`WaaSMedicSvc`)
|
||||
docs: |-
|
||||
This script disables the Windows Update Medic Service. This service runs quietly in the background [1],
|
||||
making sure that parts related to Windows updates are working as they should [1] [2].
|
||||
|
||||
By default, the service is enabled and its startup setting is set to manual [3].
|
||||
|
||||
This service can undo any adjustments you've made to your Windows Update settings without your consent.
|
||||
For example, it can re-enable automatic Windows updates [4].
|
||||
That can interfere if you've tailored these settings for better privacy or security.
|
||||
|
||||
When you disable this service using our script, you're taking back control. You get to choose how your system
|
||||
handles updates and data transfers, ensuring that your privacy settings stay as you intended. This is a reliable
|
||||
way to strengthen both your privacy and your control over your computer.
|
||||
|
||||
[1]: https://web.archive.org/web/20230905120805/https://support.microsoft.com/en-us/topic/kb5005322-some-devices-cannot-install-new-updates-after-installing-kb5003214-may-25-2021-and-kb5003690-june-21-2021-66edf7cf-5d3c-401f-bd32-49865343144f "KB5005322—Some devices cannot install new updates after installing KB5003214 (May 25, 2021) and KB5003690 (June 21, 2021) - Microsoft Support"
|
||||
[2]: https://web.archive.org/web/20230905120445/https://learn.microsoft.com/en-us/windows/deployment/update/prepare-deploy-windows "Security guidelines for system services in Windows Server 2016 | Microsoft Learn"
|
||||
[3]: https://web.archive.org/web/20230905120815/https://learn.microsoft.com/en-us/windows/iot/iot-enterprise/optimize/services "Guidance on disabling system services on Windows IoT Enterprise | Microsoft Learn"
|
||||
[4]: https://github.com/undergroundwires/privacy.sexy/issues/252
|
||||
call:
|
||||
function: DisableServiceInRegistry
|
||||
# Since Windows 10 21H2 and Windows 11 21H2:
|
||||
# - Using `sc config` resulsts in "Access in denied", so registry should be used to disable the service.
|
||||
# - Default startup mode is Manual
|
||||
parameters:
|
||||
serviceName: WaaSMedicSvc # Check: (Get-Service -Name 'WaaSMedicSvc').StartType
|
||||
defaultStartupMode: Manual # Allowed values: Automatic | Manual
|
||||
-
|
||||
category: Configure handling of downloaded files
|
||||
docs: |-
|
||||
@@ -5169,25 +5525,6 @@ actions:
|
||||
-
|
||||
category: Disable OS services
|
||||
children:
|
||||
-
|
||||
name: Delivery Optimization (P2P Windows Updates)
|
||||
recommend: standard
|
||||
docs:
|
||||
# Delivery Optimization is a cloud-managed solution to offer Windows updates through
|
||||
# other users' network (peer-to-peer).
|
||||
- https://docs.microsoft.com/en-us/windows/deployment/update/waas-delivery-optimization
|
||||
# Delivery Optimization service performs content delivery optimization tasks.
|
||||
- http://batcmd.com/windows/10/services/dosvc/
|
||||
# Connects to various Microsoft service endpoints to get metadata, policies, content, device information
|
||||
# and information of other peers (Windows users).
|
||||
- https://docs.microsoft.com/en-us/windows/deployment/update/delivery-optimization-workflow
|
||||
call:
|
||||
function: DisableServiceInRegistry
|
||||
# Using registry way because because other options such as "sc config" or
|
||||
# "Set-Service" returns "Access is denied" since Windows 10 1809.
|
||||
parameters:
|
||||
serviceName: DoSvc # Check: (Get-Service -Name 'DoSvc').StartType
|
||||
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
|
||||
-
|
||||
name: Microsoft Account Sign-in Assistant (breaks Microsoft Store and Microsoft Account sign-in)
|
||||
recommend: strict
|
||||
@@ -6613,16 +6950,75 @@ actions:
|
||||
code: reg delete "HKCU\Environment" /v "OneDrive" /f 2>nul
|
||||
-
|
||||
name: Uninstall Edge (chromium-based)
|
||||
docs: |-
|
||||
This script automates the uninstallation of Microsoft Edge (also known as "Chromium Edge" or "New Edge" [1]), the web browser that comes
|
||||
pre-installed with many versions of Windows.
|
||||
|
||||
Microsoft Edge collects various types of data, some of which pertain to your browsing habits, such as the websites you visit, your search
|
||||
queries, and the data you enter into forms [2]. Additionally, it tracks usage metrics and diagnostic data about your device data and
|
||||
how the browser is functioning [2]. These pieces of information could be used for targeted advertising or profiling. Removing Microsoft
|
||||
Edge ensures that it is not silently accumulating this data in the background, thereby improving your overall privacy.
|
||||
|
||||
By default, Microsoft Edge doesn't allow easy uninstallation and has officially declared Microsoft Edge as uninstallable on Windows [3].
|
||||
|
||||
This scripts uses two steps to achieve this:
|
||||
|
||||
1. **Enable Uninstallation**: The script modifies a specific registry key to allow the uninstallation of Microsoft Edge. This step is crucial
|
||||
because, starting from version 116 of Edge, you cannot uninstall it unless this registry key is set.
|
||||
2. **Run Uninstaller**: The script then finds the Microsoft Edge installer (`setup.exe`) for every Microsoft Edge installation (it is possible
|
||||
to have multiple versions) and executes it to perform a system-level uninstall.
|
||||
|
||||
There's no official documentation for the Edge installer or registry keys codes, which this script relies on. However, these have been verified
|
||||
through testing and community support to work as expected.
|
||||
|
||||
[1]: https://en.wikipedia.org/w/index.php?title=Microsoft_Edge&oldid=1174053020#New_Edge_(2019%E2%80%93present) "Microsoft Edge - Wikipedia"
|
||||
[2]: https://web.archive.org/web/20230907002709/https://support.microsoft.com/en-us/microsoft-edge/learn-more-about-diagnostic-data-collection-in-microsoft-edge-7fcee15b-39f7-ba02-bc59-9eef622c1a9f "Learn more about diagnostic data collection in Microsoft Edge - Microsoft Support"
|
||||
[3]: https://web.archive.org/web/20230907002011/https://support.microsoft.com/en-us/microsoft-edge/why-can-t-i-uninstall-microsoft-edge-ee150b3b-7d7a-9984-6d83-eb36683d526d "Why can't I uninstall Microsoft Edge? - Microsoft Support"
|
||||
call:
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
code: |-
|
||||
$installer = (Get-ChildItem "$env:ProgramFiles*\Microsoft\Edge\Application\*\Installer\setup.exe")
|
||||
if (!$installer) {
|
||||
Write-Host 'Could not find the installer'
|
||||
} else {
|
||||
& $installer.FullName -Uninstall -System-Level -Verbose-Logging -Force-Uninstall
|
||||
}
|
||||
-
|
||||
function: RunInlineCode
|
||||
parameters:
|
||||
code: reg add "HKLM\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdateDev" /v "AllowUninstall" /t REG_DWORD /d "1" /f
|
||||
revertCode: reg delete "HKLM\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdateDev" /v "AllowUninstall" /f 2>nul # It does not exists since Windows 10 21H2 and Windows 11 21H2
|
||||
-
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
code: |-
|
||||
$installer = (Get-ChildItem "$($env:ProgramFiles)*\Microsoft\Edge\Application\*\Installer\setup.exe")
|
||||
if (!$installer) {
|
||||
Write-Host 'Installer not found. Microsoft Edge may already be uninstalled.'
|
||||
} else {
|
||||
$installer | ForEach-Object {
|
||||
$uninstallerPath = $_.FullName
|
||||
$installerArguments = @("--uninstall", "--system-level", "--verbose-logging", "--force-uninstall")
|
||||
Write-Output "Uninstalling through uninstaller: $uninstallerPath"
|
||||
$process = Start-Process -FilePath "$uninstallerPath" -ArgumentList $installerArguments -Wait -PassThru
|
||||
if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 19) {
|
||||
Write-Host "Successfully uninstalled Edge."
|
||||
} else {
|
||||
Write-Error "Failed to uninstall, uninstaller failed with exit code $($process.ExitCode)."
|
||||
}
|
||||
}
|
||||
}
|
||||
revertCode: |-
|
||||
$edgeExePath = Get-ChildItem -Path "$($env:ProgramFiles)*\Microsoft\Edge\Application" -Filter 'msedge.exe' -Recurse
|
||||
if ($edgeExePath) {
|
||||
Write-Host 'Microsoft Edge is already installed. Skipping reinstallation.'
|
||||
Exit 0
|
||||
}
|
||||
Write-Host 'Downloading Microsoft Edge...'
|
||||
$edgeInstallerUrl = 'https://c2rsetup.officeapps.live.com/c2r/downloadEdge.aspx?platform=Default&Channel=Stable&language=en'
|
||||
$downloadPath = "$($env:TEMP)\MicrosoftEdgeSetup.exe"
|
||||
Invoke-WebRequest -Uri "$edgeInstallerUrl" -OutFile "$downloadPath"
|
||||
$installerArguments = @('/install', '/silent')
|
||||
Write-Host 'Installing Microsoft Edge...'
|
||||
$process = Start-Process -FilePath "$downloadPath" -ArgumentList "$installerArguments" -Wait -PassThru
|
||||
Remove-Item -Path $downloadPath -Force
|
||||
if ($process.ExitCode -eq 0) {
|
||||
Write-Host 'Successfully reinstalled Microsoft Edge.'
|
||||
} else {
|
||||
Write-Error "Failed to reinstall Microsoft Edge. Installer failed with exit code $($process.ExitCode)."
|
||||
}
|
||||
-
|
||||
category: Disable built-in Windows features
|
||||
children:
|
||||
@@ -7473,6 +7869,7 @@ functions:
|
||||
parameters:
|
||||
- name: code
|
||||
- name: revertCode
|
||||
optional: true
|
||||
call:
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
@@ -7540,7 +7937,8 @@ functions:
|
||||
Remove-Item $streamOutFile, $batchFile
|
||||
}
|
||||
revertCode: |- # Duplicated until custom pipes are implemented
|
||||
$command = '{{ $revertCode }}'
|
||||
{{ with $revertCode }}
|
||||
$command = '{{ . }}'
|
||||
$trustedInstallerSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464')
|
||||
$trustedInstallerName = $trustedInstallerSid.Translate([System.Security.Principal.NTAccount])
|
||||
$streamOutFile = New-TemporaryFile
|
||||
@@ -7583,6 +7981,7 @@ functions:
|
||||
} finally {
|
||||
Remove-Item $streamOutFile, $batchFile
|
||||
}
|
||||
{{ end }}
|
||||
-
|
||||
name: DisableServiceInRegistry
|
||||
parameters:
|
||||
|
||||
@@ -1,31 +1,37 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
// eslint-disable-next-line camelcase
|
||||
import child_process from 'child_process';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { getWindowInjectedSystemOperations } from './SystemOperations/WindowInjectedSystemOperations';
|
||||
|
||||
export class CodeRunner {
|
||||
constructor(
|
||||
private readonly node = getNodeJs(),
|
||||
private readonly environment = Environment.CurrentEnvironment,
|
||||
private readonly system = getWindowInjectedSystemOperations(),
|
||||
private readonly environment = RuntimeEnvironment.CurrentEnvironment,
|
||||
) {
|
||||
if (!system) {
|
||||
throw new Error('missing system operations');
|
||||
}
|
||||
}
|
||||
|
||||
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
||||
const dir = this.node.path.join(this.node.os.tmpdir(), folderName);
|
||||
await this.node.fs.promises.mkdir(dir, { recursive: true });
|
||||
const filePath = this.node.path.join(dir, `run.${fileExtension}`);
|
||||
await this.node.fs.promises.writeFile(filePath, code);
|
||||
await this.node.fs.promises.chmod(filePath, '755');
|
||||
const command = getExecuteCommand(filePath, this.environment);
|
||||
this.node.child_process.exec(command);
|
||||
const { os } = this.environment;
|
||||
const dir = this.system.location.combinePaths(
|
||||
this.system.operatingSystem.getTempDirectory(),
|
||||
folderName,
|
||||
);
|
||||
await this.system.fileSystem.createDirectory(dir, true);
|
||||
const filePath = this.system.location.combinePaths(dir, `run.${fileExtension}`);
|
||||
await this.system.fileSystem.writeToFile(filePath, code);
|
||||
await this.system.fileSystem.setFilePermissions(filePath, '755');
|
||||
const command = getExecuteCommand(filePath, os);
|
||||
this.system.command.execute(command);
|
||||
}
|
||||
}
|
||||
|
||||
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
||||
switch (environment.os) {
|
||||
function getExecuteCommand(
|
||||
scriptPath: string,
|
||||
currentOperatingSystem: OperatingSystem,
|
||||
): string {
|
||||
switch (currentOperatingSystem) {
|
||||
case OperatingSystem.Linux:
|
||||
return `x-terminal-emulator -e '${scriptPath}'`;
|
||||
case OperatingSystem.macOS:
|
||||
@@ -36,46 +42,6 @@ function getExecuteCommand(scriptPath: string, environment: Environment): string
|
||||
case OperatingSystem.Windows:
|
||||
return scriptPath;
|
||||
default:
|
||||
throw Error(`unsupported os: ${OperatingSystem[environment.os]}`);
|
||||
throw Error(`unsupported os: ${OperatingSystem[currentOperatingSystem]}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeJs(): INodeJs {
|
||||
return {
|
||||
os, path, fs, child_process,
|
||||
};
|
||||
}
|
||||
|
||||
export interface INodeJs {
|
||||
os: INodeOs;
|
||||
path: INodePath;
|
||||
fs: INodeFs;
|
||||
// eslint-disable-next-line camelcase
|
||||
child_process: INodeChildProcess;
|
||||
}
|
||||
|
||||
export interface INodeOs {
|
||||
tmpdir(): string;
|
||||
}
|
||||
|
||||
export interface INodePath {
|
||||
join(...paths: string[]): string;
|
||||
}
|
||||
|
||||
export interface INodeChildProcess {
|
||||
exec(command: string): void;
|
||||
}
|
||||
|
||||
export interface INodeFs {
|
||||
readonly promises: INodeFsPromises;
|
||||
}
|
||||
|
||||
interface INodeFsPromisesMakeDirectoryOptions {
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
interface INodeFsPromises { // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v13/fs.d.ts
|
||||
chmod(path: string, mode: string | number): Promise<void>;
|
||||
mkdir(path: string, options: INodeFsPromisesMakeDirectoryOptions): Promise<string>;
|
||||
writeFile(path: string, data: string): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { IEnvironmentVariablesFactory } from './IEnvironmentVariablesFactory';
|
||||
import { validateEnvironmentVariables } from './EnvironmentVariablesValidator';
|
||||
import { ViteEnvironmentVariables } from './Vite/ViteEnvironmentVariables';
|
||||
import { IEnvironmentVariables } from './IEnvironmentVariables';
|
||||
|
||||
export class EnvironmentVariablesFactory implements IEnvironmentVariablesFactory {
|
||||
public static readonly Current = new EnvironmentVariablesFactory();
|
||||
|
||||
public readonly instance: IEnvironmentVariables;
|
||||
|
||||
protected constructor(validator: EnvironmentVariablesValidator = validateEnvironmentVariables) {
|
||||
const environment = new ViteEnvironmentVariables();
|
||||
validator(environment);
|
||||
this.instance = environment;
|
||||
}
|
||||
}
|
||||
|
||||
export type EnvironmentVariablesValidator = typeof validateEnvironmentVariables;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { IEnvironmentVariables } from './IEnvironmentVariables';
|
||||
|
||||
/* Validation is externalized to keep the environment objects simple */
|
||||
export function validateEnvironmentVariables(environment: IEnvironmentVariables): void {
|
||||
if (!environment) {
|
||||
throw new Error('missing environment');
|
||||
}
|
||||
const keyValues = capturePropertyValues(environment);
|
||||
if (!Object.keys(keyValues).length) {
|
||||
throw new Error('Unable to capture key/value pairs');
|
||||
}
|
||||
const keysMissingValue = getKeysMissingValues(keyValues);
|
||||
if (keysMissingValue.length > 0) {
|
||||
throw new Error(`Environment keys missing: ${keysMissingValue.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getKeysMissingValues(keyValuePairs: Record<string, unknown>): string[] {
|
||||
return Object.entries(keyValuePairs)
|
||||
.reduce((acc, [key, value]) => {
|
||||
if (!value && typeof value !== 'boolean') {
|
||||
acc.push(key);
|
||||
}
|
||||
return acc;
|
||||
}, new Array<string>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures values of properties and getters from the provided instance.
|
||||
* Necessary because code transformations can make class getters non-enumerable during bundling.
|
||||
* This ensures that even if getters are non-enumerable, their values are still captured and used.
|
||||
*/
|
||||
function capturePropertyValues(instance: unknown): Record<string, unknown> {
|
||||
const obj: Record<string, unknown> = {};
|
||||
const descriptors = Object.getOwnPropertyDescriptors(instance.constructor.prototype);
|
||||
|
||||
// Capture regular properties from the instance
|
||||
for (const [key, value] of Object.entries(instance)) {
|
||||
obj[key] = value;
|
||||
}
|
||||
|
||||
// Capture getter properties from the instance's prototype
|
||||
for (const [key, descriptor] of Object.entries(descriptors)) {
|
||||
if (typeof descriptor.get === 'function') {
|
||||
obj[key] = descriptor.get.call(instance);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
10
src/infrastructure/EnvironmentVariables/IAppMetadata.ts
Normal file
10
src/infrastructure/EnvironmentVariables/IAppMetadata.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Represents essential metadata about the application.
|
||||
*/
|
||||
export interface IAppMetadata {
|
||||
readonly version: string;
|
||||
readonly name: string;
|
||||
readonly slogan: string;
|
||||
readonly repositoryUrl: string;
|
||||
readonly homepageUrl: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IAppMetadata } from './IAppMetadata';
|
||||
|
||||
/**
|
||||
* Designed to decouple the process of retrieving environment variables
|
||||
* (e.g., from the build environment) from the rest of the application.
|
||||
*/
|
||||
export interface IEnvironmentVariables extends IAppMetadata {
|
||||
readonly isNonProduction: boolean;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { IEnvironmentVariables } from './IEnvironmentVariables';
|
||||
|
||||
export interface IEnvironmentVariablesFactory {
|
||||
readonly instance: IEnvironmentVariables;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Only variables prefixed with VITE_ are exposed to Vite-processed code
|
||||
export const VITE_USER_DEFINED_ENVIRONMENT_KEYS = {
|
||||
VERSION: 'VITE_APP_VERSION',
|
||||
NAME: 'VITE_APP_NAME',
|
||||
SLOGAN: 'VITE_APP_SLOGAN',
|
||||
REPOSITORY_URL: 'VITE_APP_REPOSITORY_URL',
|
||||
HOMEPAGE_URL: 'VITE_APP_HOMEPAGE_URL',
|
||||
} as const;
|
||||
|
||||
export const VITE_ENVIRONMENT_KEYS = {
|
||||
...VITE_USER_DEFINED_ENVIRONMENT_KEYS,
|
||||
DEV: 'DEV',
|
||||
} as const;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IEnvironmentVariables } from '../IEnvironmentVariables';
|
||||
|
||||
/**
|
||||
* Provides the application's environment variables.
|
||||
*/
|
||||
export class ViteEnvironmentVariables implements IEnvironmentVariables {
|
||||
// Ensure the use of import.meta.env prefix for the following properties.
|
||||
// Vue will replace these statically during production builds.
|
||||
|
||||
public get version(): string {
|
||||
return import.meta.env.VITE_APP_VERSION;
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
return import.meta.env.VITE_APP_NAME;
|
||||
}
|
||||
|
||||
public get slogan(): string {
|
||||
return import.meta.env.VITE_APP_SLOGAN;
|
||||
}
|
||||
|
||||
public get repositoryUrl(): string {
|
||||
return import.meta.env.VITE_APP_REPOSITORY_URL;
|
||||
}
|
||||
|
||||
public get homepageUrl(): string {
|
||||
return import.meta.env.VITE_APP_HOMEPAGE_URL;
|
||||
}
|
||||
|
||||
public get isNonProduction(): boolean {
|
||||
return import.meta.env.DEV;
|
||||
}
|
||||
}
|
||||
11
src/infrastructure/EnvironmentVariables/Vite/vite-env.d.ts
vendored
Normal file
11
src/infrastructure/EnvironmentVariables/Vite/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import { VITE_ENVIRONMENT_KEYS } from './ViteEnvironmentKeys';
|
||||
|
||||
export type ViteEnvironmentVariables = {
|
||||
readonly [K in keyof typeof VITE_ENVIRONMENT_KEYS]: string;
|
||||
};
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ViteEnvironmentVariables
|
||||
}
|
||||
@@ -1,9 +1,20 @@
|
||||
import { IEventSubscriptionCollection } from './IEventSubscriptionCollection';
|
||||
import { IEventSubscription } from './IEventSource';
|
||||
|
||||
export class EventSubscriptionCollection {
|
||||
export class EventSubscriptionCollection implements IEventSubscriptionCollection {
|
||||
private readonly subscriptions = new Array<IEventSubscription>();
|
||||
|
||||
public register(...subscriptions: IEventSubscription[]) {
|
||||
public get subscriptionCount() {
|
||||
return this.subscriptions.length;
|
||||
}
|
||||
|
||||
public register(subscriptions: IEventSubscription[]) {
|
||||
if (!subscriptions || subscriptions.length === 0) {
|
||||
throw new Error('missing subscriptions');
|
||||
}
|
||||
if (subscriptions.some((subscription) => !subscription)) {
|
||||
throw new Error('missing subscription in list');
|
||||
}
|
||||
this.subscriptions.push(...subscriptions);
|
||||
}
|
||||
|
||||
@@ -11,4 +22,9 @@ export class EventSubscriptionCollection {
|
||||
this.subscriptions.forEach((listener) => listener.unsubscribe());
|
||||
this.subscriptions.splice(0, this.subscriptions.length);
|
||||
}
|
||||
|
||||
public unsubscribeAllAndRegister(subscriptions: IEventSubscription[]) {
|
||||
this.unsubscribeAll();
|
||||
this.register(subscriptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||
|
||||
export interface IEventSubscriptionCollection {
|
||||
readonly subscriptionCount: number;
|
||||
|
||||
register(subscriptions: IEventSubscription[]): void;
|
||||
unsubscribeAll(): void;
|
||||
unsubscribeAllAndRegister(subscriptions: IEventSubscription[]);
|
||||
}
|
||||
13
src/infrastructure/Log/ConsoleLogger.ts
Normal file
13
src/infrastructure/Log/ConsoleLogger.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
export class ConsoleLogger implements ILogger {
|
||||
constructor(private readonly globalConsole: Partial<Console> = console) {
|
||||
if (!globalConsole) {
|
||||
throw new Error('missing console');
|
||||
}
|
||||
}
|
||||
|
||||
public info(...params: unknown[]): void {
|
||||
this.globalConsole.info(...params);
|
||||
}
|
||||
}
|
||||
12
src/infrastructure/Log/ElectronLogger.ts
Normal file
12
src/infrastructure/Log/ElectronLogger.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ElectronLog } from 'electron-log';
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
// Using plain-function rather than class so it can be used in Electron's context-bridging.
|
||||
export function createElectronLogger(logger: Partial<ElectronLog>): ILogger {
|
||||
if (!logger) {
|
||||
throw new Error('missing logger');
|
||||
}
|
||||
return {
|
||||
info: (...params) => logger.info(...params),
|
||||
};
|
||||
}
|
||||
3
src/infrastructure/Log/ILogger.ts
Normal file
3
src/infrastructure/Log/ILogger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface ILogger {
|
||||
info (...params: unknown[]): void;
|
||||
}
|
||||
5
src/infrastructure/Log/ILoggerFactory.ts
Normal file
5
src/infrastructure/Log/ILoggerFactory.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
export interface ILoggerFactory {
|
||||
readonly logger: ILogger;
|
||||
}
|
||||
5
src/infrastructure/Log/NoopLogger.ts
Normal file
5
src/infrastructure/Log/NoopLogger.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
export class NoopLogger implements ILogger {
|
||||
public info(): void { /* NOOP */ }
|
||||
}
|
||||
20
src/infrastructure/Log/WindowInjectedLogger.ts
Normal file
20
src/infrastructure/Log/WindowInjectedLogger.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { WindowVariables } from '../WindowVariables/WindowVariables';
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
export class WindowInjectedLogger implements ILogger {
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(windowVariables: WindowVariables = window) {
|
||||
if (!windowVariables) {
|
||||
throw new Error('missing window');
|
||||
}
|
||||
if (!windowVariables.log) {
|
||||
throw new Error('missing log');
|
||||
}
|
||||
this.logger = windowVariables.log;
|
||||
}
|
||||
|
||||
public info(...params: unknown[]): void {
|
||||
this.logger.info(...params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IRuntimeEnvironment {
|
||||
readonly isDesktop: boolean;
|
||||
readonly os: OperatingSystem | undefined;
|
||||
readonly isNonProduction: boolean;
|
||||
}
|
||||
46
src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts
Normal file
46
src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||
import { IRuntimeEnvironment } from './IRuntimeEnvironment';
|
||||
|
||||
export class RuntimeEnvironment implements IRuntimeEnvironment {
|
||||
public static readonly CurrentEnvironment: IRuntimeEnvironment = new RuntimeEnvironment(window);
|
||||
|
||||
public readonly isDesktop: boolean;
|
||||
|
||||
public readonly os: OperatingSystem | undefined;
|
||||
|
||||
public readonly isNonProduction: boolean;
|
||||
|
||||
protected constructor(
|
||||
window: Partial<Window>,
|
||||
environmentVariables: IEnvironmentVariables = EnvironmentVariablesFactory.Current.instance,
|
||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||
) {
|
||||
if (!window) {
|
||||
throw new Error('missing window');
|
||||
}
|
||||
this.isNonProduction = environmentVariables.isNonProduction;
|
||||
this.isDesktop = isDesktop(window);
|
||||
if (this.isDesktop) {
|
||||
this.os = window?.os;
|
||||
} else {
|
||||
this.os = undefined;
|
||||
const userAgent = getUserAgent(window);
|
||||
if (userAgent) {
|
||||
this.os = browserOsDetector.detect(userAgent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getUserAgent(window: Partial<Window>): string {
|
||||
return window?.navigator?.userAgent;
|
||||
}
|
||||
|
||||
function isDesktop(window: Partial<WindowVariables>): boolean {
|
||||
return window?.isDesktop === true;
|
||||
}
|
||||
31
src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts
Normal file
31
src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ISanityCheckOptions } from './ISanityCheckOptions';
|
||||
import { ISanityValidator } from './ISanityValidator';
|
||||
|
||||
export type FactoryFunction<T> = () => T;
|
||||
|
||||
export abstract class FactoryValidator<T> implements ISanityValidator {
|
||||
private readonly factory: FactoryFunction<T>;
|
||||
|
||||
protected constructor(factory: FactoryFunction<T>) {
|
||||
if (!factory) {
|
||||
throw new Error('missing factory');
|
||||
}
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
public abstract shouldValidate(options: ISanityCheckOptions): boolean;
|
||||
|
||||
public abstract name: string;
|
||||
|
||||
public* collectErrors(): Iterable<string> {
|
||||
try {
|
||||
const value = this.factory();
|
||||
if (!value) {
|
||||
// Do not remove this check, it ensures that the factory call is not optimized away.
|
||||
yield 'Factory resulted in a falsy value';
|
||||
}
|
||||
} catch (error) {
|
||||
yield `Error in factory creation: ${error.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ISanityCheckOptions {
|
||||
readonly validateEnvironmentVariables: boolean;
|
||||
readonly validateWindowVariables: boolean;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ISanityCheckOptions } from './ISanityCheckOptions';
|
||||
|
||||
export interface ISanityValidator {
|
||||
readonly name: string;
|
||||
shouldValidate(options: ISanityCheckOptions): boolean;
|
||||
collectErrors(): Iterable<string>;
|
||||
}
|
||||
50
src/infrastructure/RuntimeSanity/SanityChecks.ts
Normal file
50
src/infrastructure/RuntimeSanity/SanityChecks.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { ISanityCheckOptions } from './Common/ISanityCheckOptions';
|
||||
import { ISanityValidator } from './Common/ISanityValidator';
|
||||
import { EnvironmentVariablesValidator } from './Validators/EnvironmentVariablesValidator';
|
||||
|
||||
const DefaultSanityValidators: ISanityValidator[] = [
|
||||
new EnvironmentVariablesValidator(),
|
||||
];
|
||||
|
||||
/* Helps to fail-fast on errors */
|
||||
export function validateRuntimeSanity(
|
||||
options: ISanityCheckOptions,
|
||||
validators: readonly ISanityValidator[] = DefaultSanityValidators,
|
||||
): void {
|
||||
validateContext(options, validators);
|
||||
const errorMessages = validators.reduce((errors, validator) => {
|
||||
if (validator.shouldValidate(options)) {
|
||||
const errorMessage = getErrorMessage(validator);
|
||||
if (errorMessage) {
|
||||
errors.push(errorMessage);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}, new Array<string>());
|
||||
if (errorMessages.length > 0) {
|
||||
throw new Error(`Sanity check failed.\n${errorMessages.join('\n---\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateContext(
|
||||
options: ISanityCheckOptions,
|
||||
validators: readonly ISanityValidator[],
|
||||
) {
|
||||
if (!options) {
|
||||
throw new Error('missing options');
|
||||
}
|
||||
if (!validators?.length) {
|
||||
throw new Error('missing validators');
|
||||
}
|
||||
if (validators.some((validator) => !validator)) {
|
||||
throw new Error('missing validator in validators');
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorMessage(validator: ISanityValidator): string | undefined {
|
||||
const errorMessages = [...validator.collectErrors()];
|
||||
if (!errorMessages.length) {
|
||||
return undefined;
|
||||
}
|
||||
return `${validator.name}:\n${errorMessages.join('\n')}`;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
|
||||
import { FactoryValidator, FactoryFunction } from '../Common/FactoryValidator';
|
||||
|
||||
export class EnvironmentVariablesValidator extends FactoryValidator<IEnvironmentVariables> {
|
||||
constructor(
|
||||
factory: FactoryFunction<IEnvironmentVariables> = () => {
|
||||
return EnvironmentVariablesFactory.Current.instance;
|
||||
},
|
||||
) {
|
||||
super(factory);
|
||||
}
|
||||
|
||||
public override name = 'environment variables';
|
||||
|
||||
public override shouldValidate(options: ISanityCheckOptions): boolean {
|
||||
return options.validateEnvironmentVariables;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
import { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
|
||||
import { FactoryValidator, FactoryFunction } from '../Common/FactoryValidator';
|
||||
|
||||
export class WindowVariablesValidator extends FactoryValidator<WindowVariables> {
|
||||
constructor(factory: FactoryFunction<WindowVariables> = () => window) {
|
||||
super(factory);
|
||||
}
|
||||
|
||||
public override name = 'window variables';
|
||||
|
||||
public override shouldValidate(options: ISanityCheckOptions): boolean {
|
||||
return options.validateWindowVariables;
|
||||
}
|
||||
}
|
||||
24
src/infrastructure/SystemOperations/ISystemOperations.ts
Normal file
24
src/infrastructure/SystemOperations/ISystemOperations.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface ISystemOperations {
|
||||
readonly operatingSystem: IOperatingSystemOps;
|
||||
readonly location: ILocationOps;
|
||||
readonly fileSystem: IFileSystemOps;
|
||||
readonly command: ICommandOps;
|
||||
}
|
||||
|
||||
export interface IOperatingSystemOps {
|
||||
getTempDirectory(): string;
|
||||
}
|
||||
|
||||
export interface ILocationOps {
|
||||
combinePaths(...pathSegments: string[]): string;
|
||||
}
|
||||
|
||||
export interface ICommandOps {
|
||||
execute(command: string): void;
|
||||
}
|
||||
|
||||
export interface IFileSystemOps {
|
||||
setFilePermissions(filePath: string, mode: string | number): Promise<void>;
|
||||
createDirectory(directoryPath: string, isRecursive?: boolean): Promise<string>;
|
||||
writeToFile(filePath: string, data: string): Promise<void>;
|
||||
}
|
||||
33
src/infrastructure/SystemOperations/NodeSystemOperations.ts
Normal file
33
src/infrastructure/SystemOperations/NodeSystemOperations.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { chmod, mkdir, writeFile } from 'fs/promises';
|
||||
import { exec } from 'child_process';
|
||||
import { ISystemOperations } from './ISystemOperations';
|
||||
|
||||
export function createNodeSystemOperations(): ISystemOperations {
|
||||
return {
|
||||
operatingSystem: {
|
||||
getTempDirectory: () => tmpdir(),
|
||||
},
|
||||
location: {
|
||||
combinePaths: (...pathSegments) => join(...pathSegments),
|
||||
},
|
||||
fileSystem: {
|
||||
setFilePermissions: (
|
||||
filePath: string,
|
||||
mode: string | number,
|
||||
) => chmod(filePath, mode),
|
||||
createDirectory: (
|
||||
directoryPath: string,
|
||||
isRecursive?: boolean,
|
||||
) => mkdir(directoryPath, { recursive: isRecursive }),
|
||||
writeToFile: (
|
||||
filePath: string,
|
||||
data: string,
|
||||
) => writeFile(filePath, data),
|
||||
},
|
||||
command: {
|
||||
execute: (command) => exec(command),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { WindowVariables } from '../WindowVariables/WindowVariables';
|
||||
import { ISystemOperations } from './ISystemOperations';
|
||||
|
||||
export function getWindowInjectedSystemOperations(
|
||||
windowVariables: Partial<WindowVariables> = window,
|
||||
): ISystemOperations {
|
||||
if (!windowVariables) {
|
||||
throw new Error('missing window');
|
||||
}
|
||||
if (!windowVariables.system) {
|
||||
throw new Error('missing system');
|
||||
}
|
||||
return windowVariables.system;
|
||||
}
|
||||
11
src/infrastructure/WindowVariables/WindowVariables.ts
Normal file
11
src/infrastructure/WindowVariables/WindowVariables.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
|
||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
||||
|
||||
/* Primary entry point for platform-specific injections */
|
||||
export interface WindowVariables {
|
||||
readonly system: ISystemOperations;
|
||||
readonly isDesktop: boolean;
|
||||
readonly os: OperatingSystem;
|
||||
readonly log: ILogger;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user