Compare commits

..

27 Commits

Author SHA1 Message Date
undergroundwires
bd2082e8c5 Fix slow appearance of nodes on tree view
The tree view rendering performance is optimized by improving the node
render queue ordering. The node rendering order is modified based on the
expansion state and the depth in the hierarchy, leading to faster
rendering of visible nodes. This optimization is applied when the tree
nodes are not expanded to improve the rendering speed.

This new ordering ensures that nodes are rendered more efficiently,
prioritizing nodes that are collapsed and are at a higher level in the
hierarchy.
2023-09-25 14:21:29 +02:00
undergroundwires
8f188acd3c Fix loss of tree node state when switching views
This commit fixes an issue where the check state of categories was lost
when toggling between card and tree views. This is solved by immediately
emitting node state changes for all nodes. This ensures consistent view
transitions without any loss of node state information.

Furthermore, this commit includes added unit tests for the modified code
sections.
2023-09-24 20:34:47 +02:00
undergroundwires
0303ef2fd9 Fix outdated and broken links in README #161
This commit fixes issues with download URLs of desktop application
artifacts on README.md

- Corrected typo in Linux AppImage link
- Updated older version links to the newest release

Co-authored-by: MrEddX <66828538+MrEddX@users.noreply.github.com>
2023-09-23 10:33:46 +02:00
undergroundwires
cb21a970b6 win: fix Defender scan artifacts removal #246
- Modify script to run as `TrustedInstaller`, resolving access right
  problems discussed in #246.
- Change script name for better alignment with its functionality.
- Improve script description for clarity and detailed documentation.
2023-09-22 14:11:52 +02:00
undergroundwires
203daeb4a2 win: fix delivery optimization side-effects #173
- Add non-intrusive way to disable delivery optimization. This new
  script do not introduce side-effects caused by disabling Delivery
  Optimization service.
- Recomend delivery optimization service (`DoSvc`) only on Strict
  mode, removing it from Standard recommendation.
- Categorize delivery optimization disabling under one category.
- Move disabling delivery optimization to "Disable OS collection" >
  "Disable Windows Update data collection".
- Add more documentation.
2023-09-21 11:40:15 +02:00
undergroundwires
60dde11311 win: fix uninstallation of newer Edge #236
- Fix script failing when multiple installations of Edge is found.
- Fix Edge not being able to be uninstalled due in newer Edge versions.
- Add documentation
- Add missing revert script
2023-09-20 07:48:50 +02:00
undergroundwires
8b930fc57c Rewrite tooltip UI for efficiency and Vue 3.0 #230
- Introduce a new UI component for tooltips.
- Fix tooltip arrow misalignment issues in code download/execution
  instructions dialogs.

Reasons for dropping `v-tooltip` dependency:

- Lack of support for Vue 3.0, which blocks migration to Vue 3.0 (see
  #230).
- Inability to render HTML content that's required for privacy.sexy.
- Inefficient, adding an extra 162.48 KB to the production bundle for
  web distribution (tested using `npm run build -- --mode production`).

Advantages of adopting `floating-ui` (Floating UI):

- Compatibility across multiple Vue versions including 2.0, 2.7, and 3.0.
- Reduced boilerplate resulting in cleaner, more maintainable code.
- Efficient position recalculations without reinventing the wheel.
2023-09-18 17:57:50 +02:00
undergroundwires
f810ed0c14 Fix no spacing after lists in documentation text
This commit adds missing vertical margin paragraphs that appear after
lists. It also changes vertical margin gap to match the font size along
with refactoring that makes paragraph gap modification easier to
understand.
2023-09-17 13:38:40 +02:00
undergroundwires
53222fd83c Fix compiler bug with nested optional arguments
This commit fixes compiler bug where it fails when optional values are
compiled into absent values in nested calls.

- Throw exception with more context for easier future debugging.
- Add better validation of argument values for nested calls.
- Refactor `FunctionCallCompiler` for better clarity and modularize it
  to make it more maintainable and testable.
- Refactor related interface to not have `I` prefix, and
  function/variable names for better clarity.

Context:

Discovered this issue while attempting to call
`RunInlineCodeAsTrustedInstaller` which in turn invokes `RunPowerShell`
for issue #246. This led to the realization that despite parameters
flagged as optional, the nested argument compilation didn't support
them.
2023-09-16 16:11:41 +02:00
undergroundwires
a1f2497381 Fix wrong action path in website CI deployment 2023-09-15 13:36:05 +02:00
Couleur
c27172c32e win: refactor update.mode key for VSCode #215
Removed unnecessary single quotes wrapping the value `manual` in yaml.
2023-09-14 12:47:33 +02:00
undergroundwires
6e9b65d8b1 win: fix, improve disabling automatic updates #252
- Add script to disable `WaaSMedicSvc` service (#252)
- Refine script granularity for more precise control.
- Introduce detailed documentation for the category and associated
  scripts.
- Fix `ScheduledInstallTime` being set to `3` which schedules updates to
  install at 3 AM.
- Fix `ScheduledInstallDay` is being set to `0` which schedules daily
  update installation.
- Fix `NoAutoUpdate` being set to `0` (enable) instead of `1` (disable).
- Add disabling of missing `wuauserv` service.
- Add parent category for disabling Windows update services for better
  organization.
2023-09-13 13:18:14 +02:00
billy
6d301f9961 win: fix Edge telemetry disabling for v116+ #242 2023-09-12 13:28:22 +02:00
undergroundwires
659fea7afc win: fix Windows spotlight revert, docs, recommend
- Move disabling Windows Spotlight from Standard to Strict
  recommendation due to unexpected behavior for some users (#65).
- Enhance documentation.
- Correct revert code to ensure return to the default OS state.
2023-09-11 14:08:33 +02:00
undergroundwires-bot
e0303058a3 ⬆️ bump everywhere to 0.12.3 2023-09-10 11:21:25 +00:00
undergroundwires
65f121c451 Introduce new TreeView UI component
Key highlights:

- Written from scratch to cater specifically to privacy.sexy's
  needs and requirements.
- The visual look mimics the previous component with minimal changes,
  but its internal code is completely rewritten.
- Lays groundwork for future functionalities like the "expand all"
  button a flat view mode as discussed in #158.
- Facilitates the transition to Vue 3 by omitting the Vue 2.0 dependent
  `liquour-tree` as part of #230.

Improvements and features:

- Caching for quicker node queries.
- Gradual rendering of nodes that introduces a noticable boost in
  performance, particularly during search/filtering.
  - `TreeView` solely governs the check states of branch nodes.

Changes:

- Keyboard interactions now alter the background color to highlight the
  focused item. Previously, it was changing the color of the text.
- Better state management with clear separation of concerns:
  - `TreeView` exclusively manages indeterminate states.
  - `TreeView` solely governs the check states of branch nodes.
  - Introduce transaction pattern to update state in batches to minimize
    amount of events handled.
- Improve keyboard focus, style background instead of foreground. Use
  hover/touch color on keyboard focus.
- `SelectableTree` has been removed. Instead, `TreeView` is now directly
  integrated with `ScriptsTree`.
- `ScriptsTree` has been refactored to incorporate hooks for clearer
  code and separation of duties.
- Adopt Vue-idiomatic bindings instead of keeping a reference of the
  tree component.
- Simplify and change filter event management.
- Abandon global styles in favor of class-scoped styles.
- Use global mixins with descriptive names to clarify indended
  functionality.
2023-09-09 22:26:21 +02:00
undergroundwires
821cc62c4c Change license to AGPLv3 2023-09-08 16:52:41 +02:00
Snowz
4ce327eb6a win: fix disable recent apps revert #211, #248
Add missing space
2023-09-07 00:02:02 +02:00
undergroundwires
4beb1bb574 Introduce retry mechanism for npm install in CI/CD
This commit addresses occasional pipeline failures caused by transient
network errors during dependency installation with `npm ci`. It
centralizes the logic for installing npm dependencies and introduces a
retry mechanism.

The new approach will attempt `npm ci` up to 5 times with a 5-second
interval between each attempt, thereby increasing the resilience of
CI/CD pipelines.

This commit adds a new script `npm-install.js` with `npm run
install-deps` command to centralize npm dependency installation process
throughout the project. Separate testing of scripts to a separate
workflow.

It removes unused `install` dependency from `package.json`.
2023-09-05 13:39:15 +02:00
undergroundwires
0a2a1a026b Refactor build configs and improve CI/CD checks
This commit makes the build process more robust, simplifies
configurations and reduce the risk of incomplete or erroneous
deployments.

- Centralize output directory definitions by introducing
  `dist-dirs.json`.
- Add `verify-build-artifacts` utility to ensure correct build outputs
  and `print-dist-dir` to determine distribution directory.
- Add steps in CI/CD pipeline to verify build artifacts.
- Migrate Electron Builder config from YAML to CJS for capability to
  read JSON.
- Fix `release-site.yaml` failing due to pointing to wrong distribution
  directory, change it to use `print-dist-dir`.
- Improve `check-desktop-runtime-errors` to verify build artifacts for
  more reliable builds. Ensure tests fail and succeed reliably.
- Update `.gitignore` and configure ESLint to use it to define and
  ignore build artifact directories from one place, remove
  `.eslintignore` that does not add anything after this change.
- Keep `"main"` field in `package.json` as `electron-vite` depends on it
  (alex8088/electron-vite#270).
- Improve documentation
2023-09-03 14:50:31 +02:00
undergroundwires
eb096d07e2 Fix memory leaks via auto-unsubscribing and DI
This commit simplifies event handling, providing a unified and robust
way to handle event lifecycling. This way, it fixes events not being
unsubscribed when state is changed.

Introduce a new function in `EventSubscriptionCollection` to remove
existing events and adding new events. This provides an easier to use
API, which leads to code that's easier to understand. It also prevents
potential bugs that may occur due to forgetting to call both functions.
It fixes `TheScriptsMenu` not unregistering events on state change.
Other improvements include:
  - Include a getter to get total amount of registered subcriptions.
    This helps in unit testing.
  - Have nullish checks to prevent potential errors further down the
    execution.
  - Use array instead of rest parameters to increase readability and
    simplify tests.

Ensure `SliderHandler` stops resizes on unmount, unsubscribing from all
events and resetting state to default.

Update `injectionKeys` to do imports as types to avoid circular
dependencies. Simplify importing `injectionKeys` to enable and strict
typings for iterating injection keys.

Add tests covering new behavior.
2023-09-01 18:14:25 +02:00
undergroundwires
19e42c9c52 Refactor and improve external URL checks
- Move external URL checks to its own module under `tests/`. This
  separates them from integration test, addressing long runs and
  frequent failures that led to ignoring test results.
- Move `check-desktop-runtime-errors` to `tests/checks` to keep all
  test-related checks into one directory.
- Replace `ts-node` with `vite` for running
  `check-desktop-runtime-errors` to maintain a consistent execution
  environment across checks.
- Implement a timeout for each fetch call.
- Be nice to external sources, wait 5 seconds before sending another
  request to an URL under same domain. This solves rate-limiting issues.
- Instead of running test on every push/pull request, run them only
  weekly.
- Do not run tests on each commit/PR but only scheduled (weekly) to
  minimize noise.
- Fix URLs are not captured correctly inside backticks or parenthesis.
2023-09-01 00:18:47 +02:00
undergroundwires
f4d86fccfd Fix Windows artifact naming in desktop packaging
- Fix the naming convention in Electron output to align with previous
  artifact naming to not break external/internal URLs.
- In desktop execution tests, make artifact locator logic stricter to
  test regression.
2023-08-30 13:34:30 +02:00
undergroundwires
ad0576a752 Improve desktop runtime execution tests
Test improvements:

- Capture titles for all macOS windows, not just the frontmost.
- Incorporate missing application log files.
- Improve log clarity with enriched context.
- Improve application termination on macOS by reducing grace period.
- Ensure complete application termination on macOS.
- Validate Vue application loading through an initial log.
- Support ignoring environment-specific `stderr` errors.
- Do not fail the test if working directory cannot be deleted.
- Use retry pattern when installing dependencies due to network errors.

Refactorings:

- Migrate the test code to TypeScript.
- Replace deprecated `rmdir` with `rm` for error-resistant directory
  removal.
- Improve sanity checking by shifting from App.vue to Vue bootstrapper.
- Centralize environment variable management with `EnvironmentVariables`
  construct.
- Rename infrastructure/Environment to RuntimeEnvironment for clarity.
- Isolate WindowVariables and SystemOperations from RuntimeEnvironment.
- Inject logging via preloader.
- Correct mislabeled RuntimeSanity tests.

Configuration:

- Introduce `npm run check:desktop` for simplified execution.
- Omit `console.log` override due to `nodeIntegration` restrictions and
  reveal logging functionality using context-bridging.
2023-08-29 16:30:00 +02:00
tromcho
35be05df20 win: fix typo in Defender retention script #213 2023-08-28 13:15:41 +02:00
undergroundwires
dae6d114da linux: use user.js over prefs.js for Firefox #232
Manage Firefox preferences through `user.js` instead of `prefs.js`.
Because of Mozilla's recommendation against direct `prefs.js` edits to
avoid potential profile corruption. Instead, the `user.js` file, if
present, overrides the settings in `prefs.js` at application startup.

Change AddFirefoxPrefs function to update `user.js` and manage
creation/deletion of this file:

1. Handle file creation if `user.js` does not exist.
2. Deletes file if `user.js` becomes empty after reverting settings.

Other changes:

- Improve log messages
- Minimal refactorings
2023-08-27 20:14:10 +02:00
undergroundwires-bot
ecce47fdcd ⬆️ bump everywhere to 0.12.2 2023-08-25 12:46:25 +00:00
399 changed files with 13214 additions and 4311 deletions

View File

@@ -1,2 +0,0 @@
dist/
dist_electron/

View 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 }}"

View File

@@ -27,10 +27,13 @@ jobs:
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
- -
name: Install dependencies name: Install dependencies
run: npm ci uses: ./.github/actions/npm-install-dependencies
- -
name: Build name: Build web
run: npm run build -- --mode ${{ matrix.mode }} run: npm run build -- --mode ${{ matrix.mode }}
-
name: Verify web build artifacts
run: npm run check:verify-build-artifacts -- --web
build-desktop: build-desktop:
strategy: strategy:
@@ -52,30 +55,16 @@ jobs:
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
- -
name: Install dependencies name: Install dependencies
run: npm ci uses: ./.github/actions/npm-install-dependencies
- -
name: Prebuild name: Prebuild desktop
run: npm run electron:prebuild -- --mode ${{ matrix.mode }} run: npm run electron:prebuild -- --mode ${{ matrix.mode }}
- -
name: Build name: Verify unbundled desktop build artifacts
run: npm run check:verify-build-artifacts -- --electron-unbundled
-
name: Build (bundle and package) desktop application
run: npm run electron:build -- --publish never run: npm run electron:build -- --publish never
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: Checkout name: Verify bundled desktop build artifacts
uses: actions/checkout@v2 run: npm run check:verify-build-artifacts -- --electron-bundled
-
name: Setup node
uses: ./.github/actions/setup-node
-
name: Install dependencies
run: npm ci
-
name: Create icons
run: npm run icons:build

View File

@@ -6,7 +6,7 @@ on:
pull_request: pull_request:
jobs: jobs:
build-desktop: run-check:
strategy: strategy:
matrix: matrix:
os: [ macos, ubuntu, windows ] os: [ macos, ubuntu, windows ]
@@ -19,6 +19,9 @@ jobs:
- -
name: Setup node name: Setup node
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
-
name: Install dependencies
uses: ./.github/actions/npm-install-dependencies
- -
name: Configure Ubuntu name: Configure Ubuntu
if: matrix.os == 'ubuntu' if: matrix.os == 'ubuntu'
@@ -57,7 +60,9 @@ jobs:
- -
name: Test name: Test
shell: bash shell: bash
run: node ./scripts/check-desktop-runtime-errors --screenshot run: |-
export SCREENSHOT=true
npm run check:desktop
- -
name: Upload screenshot name: Upload screenshot
if: always() # Run even if previous step fails if: always() # Run even if previous step fails

View 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

View File

@@ -16,11 +16,15 @@ jobs:
os: [ macos, ubuntu, windows ] os: [ macos, ubuntu, windows ]
fail-fast: false # Still interested to see results from other combinations fail-fast: false # Still interested to see results from other combinations
steps: steps:
- name: Checkout -
name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup node -
name: Setup node
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
- name: Install dependencies -
run: npm ci name: Install dependencies
- name: Lint uses: ./.github/actions/npm-install-dependencies
-
name: Lint
run: ${{ matrix.lint-command }} run: ${{ matrix.lint-command }}

55
.github/workflows/checks.scripts.yaml vendored Normal file
View 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 }}

View File

@@ -26,7 +26,7 @@ jobs:
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
- -
name: Install dependencies name: Install dependencies
run: npm ci uses: ./.github/actions/npm-install-dependencies
- -
name: Run unit tests name: Run unit tests
run: npm run test:unit run: npm run test:unit

View File

@@ -84,8 +84,9 @@ jobs:
uses: ./app/.github/actions/setup-node uses: ./app/.github/actions/setup-node
- -
name: "App: Install dependencies" name: "App: Install dependencies"
run: npm ci uses: ./app/.github/actions/npm-install-dependencies
working-directory: app with:
working-directory: app
- -
name: "App: Run unit tests" name: "App: Run unit tests"
run: npm run test:unit run: npm run test:unit
@@ -94,11 +95,21 @@ jobs:
name: "App: Build" name: "App: Build"
run: npm run build run: npm run build
working-directory: app 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" name: "App: Deploy to S3"
shell: bash
run: >- 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" \ 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 \ --web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
--storage-class ONEZONE_IA \ --storage-class ONEZONE_IA \
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \ --role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \

View File

@@ -20,7 +20,7 @@ jobs:
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
- -
name: Install dependencies name: Install dependencies
run: npm ci uses: ./.github/actions/npm-install-dependencies
- -
name: Run e2e tests name: Run e2e tests
run: npm run test:cy:run run: npm run test:cy:run

View File

@@ -22,7 +22,7 @@ jobs:
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
- -
name: Install dependencies name: Install dependencies
run: npm ci uses: ./.github/actions/npm-install-dependencies
- -
name: Run integration tests name: Run integration tests
run: npm run test:integration run: npm run test:integration

View File

@@ -20,7 +20,7 @@ jobs:
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
- -
name: Install dependencies name: Install dependencies
run: npm ci uses: ./.github/actions/npm-install-dependencies
- -
name: Run unit tests name: Run unit tests
run: npm run test:unit run: npm run test:unit

4
.gitignore vendored
View File

@@ -1,7 +1,5 @@
node_modules node_modules
dist/ /dist-*/
.vs .vs
.vscode/**/* .vscode/**/*
!.vscode/extensions.json !.vscode/extensions.json
#Electron-builder output
/dist_electron

View File

@@ -1,5 +1,35 @@
# Changelog # 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) ## 0.12.1 (2023-08-17)
* Transition to eslint-config-airbnb-with-typescript | [ff84f56](https://github.com/undergroundwires/privacy.sexy/commit/ff84f5676e496dd7ec5b3599e34ec9627d181ea2) * Transition to eslint-config-airbnb-with-typescript | [ff84f56](https://github.com/undergroundwires/privacy.sexy/commit/ff84f5676e496dd7ec5b3599e34ec9627d181ea2)

141
LICENSE
View File

@@ -1,5 +1,5 @@
GNU GENERAL PUBLIC LICENSE GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies Everyone is permitted to copy and distribute verbatim copies
@@ -7,17 +7,15 @@
Preamble Preamble
The GNU General Public License is a free, copyleft license for The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works. 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 The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast, 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 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 software for all its users.
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.
When we speak of free software, we are referring to freedom, not When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you 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 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. free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you Developers that use our General Public Licenses protect your rights
these rights or asking you to surrender the rights. Therefore, you have with two steps: (1) assert copyright on the software, and (2) offer
certain responsibilities if you distribute copies of the software, or if you this License which gives you legal permission to copy, distribute
you modify it: responsibilities to respect the freedom of others. and/or modify the software.
For example, if you distribute copies of such a program, whether A secondary benefit of defending all users' freedom is that
gratis or for a fee, you must pass on to the recipients the same improvements made in alternate versions of the program, if they
freedoms that you received. You must make sure that they, too, receive receive widespread use, become available for other developers to
or can get the source code. And you must show them these terms so they incorporate. Many developers of free software are heartened and
know their rights. 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: The GNU Affero General Public License is designed specifically to
(1) assert copyright on the software, and (2) offer you this License ensure that, in such cases, the modified source code becomes available
giving you legal permission to copy, distribute and/or modify it. 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 An older license, called the Affero General Public License and
that there is no warranty for this free software. For both users' and published by Affero, was designed to accomplish similar goals. This is
authors' sake, the GPL requires that modified versions be marked as a different license, not a version of the Affero GPL, but Affero has
changed, so that their problems will not be attributed erroneously to released a new version of the Affero GPL which permits relicensing under
authors of previous versions. this license.
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.
The precise terms and conditions for copying, distribution and The precise terms and conditions for copying, distribution and
modification follow. modification follow.
@@ -72,7 +60,7 @@ modification follow.
0. Definitions. 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 "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks. 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 the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program. 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 Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed 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 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, License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License, but the work with which it is combined will remain governed by version
section 13, concerning interaction through a network will apply to the 3 of the GNU General Public License.
combination as such.
14. Revised Versions of this License. 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of 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 the GNU Affero General Public License from time to time. Such new versions
be similar in spirit to the present version, but may differ in detail to will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns. address new problems or concerns.
Each version is given a distinguishing version number. If the 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 Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the 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. by the Free Software Foundation.
If the Program specifies that a proxy can decide which future 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 public statement of acceptance of a version permanently authorizes you
to choose that version for the Program. 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> Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify 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 the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 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/>. 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. Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short If your software can interact with users remotely through a computer
notice like this when it starts in an interactive mode: 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
<program> Copyright (C) <year> <name of author> interface could display a "Source" link that leads users to an archive
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. of the code. There are many ways you could offer source, and different
This is free software, and you are welcome to redistribute it solutions will be better for different programs; see section 13 for the
under certain conditions; type `show c' for details. specific requirements.
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".
You should also get your employer (if you work as a programmer) or school, 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. 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/>. <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>.

View File

@@ -76,6 +76,18 @@
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.desktop-runtime-errors/badge.svg" src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.desktop-runtime-errors/badge.svg"
/> />
</a> </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 --> <!-- Release -->
<br /> <br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml" target="_blank" rel="noopener noreferrer"> <a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml" target="_blank" rel="noopener noreferrer">
@@ -110,7 +122,7 @@
## Get started ## Get started
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy). - 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
- 🖥️ **Offline**: Check [releases page](https://github.com/undergroundwires/privacy.sexy/releases), or download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-Setup-0.11.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.dmg), [Linux](https://github.com/undergroundwires/pr.vacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.AppImage). - 🖥️ **Offline**: 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. Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.

5
dist-dirs.json Normal file
View File

@@ -0,0 +1,5 @@
{
"electronUnbundled": "dist-electron-unbundled",
"electronBundled": "dist-electron-bundled",
"web": "dist-web"
}

View File

@@ -5,14 +5,16 @@ Before your commit, a good practice is to:
1. [Run unit tests](#testing) 1. [Run unit tests](#testing)
2. [Lint your code](#linting) 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 ## Commands
### Prerequisites ### Prerequisites
- Install node >15.x. - Install Node >16.x.
- Install dependencies using `npm install`. - Install dependencies using `npm install` (or [`npm run install-deps`](#utility-scripts) for more options).
### Testing ### Testing
@@ -21,6 +23,10 @@ You could run other types of tests as well, but they may take longer time and ov
- Run end-to-end (e2e) tests: - 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: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. - `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). 📖 Read more about testing in [tests](./tests.md).
@@ -61,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` - Build desktop application: `npm run electron:build`
- (Re)create icons (see [documentation](../img/README.md)): `npm run create-icons` - (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) 📖 For detailed options and behavior for any of the following scripts, please refer to the script file itself.
- 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) #### 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. - 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 ## Recommended extensions
You should use EditorConfig to follow project style. You should use EditorConfig to follow project style.

View File

@@ -14,13 +14,12 @@ The presentation layer uses an event-driven architecture for bidirectional react
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins. - [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers. - [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers. - [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
- [**`hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections). - [**`Hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections).
- [**`/public/`**](../src/presentation/public/): Contains static assets. - [**`/public/`**](../src/presentation/public/): Contains static assets.
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite. - [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts. - [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles. - [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles for Vue components. - [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles for third-party components.
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint. - [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app. - [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code. - [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
@@ -91,7 +90,14 @@ Shared components include:
Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages. Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages.
## Sass naming convention ## 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.: - Use lowercase for variables/functions/mixins, e.g.:
- Variable: `$variable: value;` - Variable: `$variable: value;`

43
electron-builder.cjs Normal file
View 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}',
},
};

View File

@@ -1,31 +0,0 @@
# -------
# Windows
# -------
win:
target: nsis
nsis:
artifactName: ${name}-${version}-Setup.${ext}
# -----
# Linux
# -----
linux:
target: AppImage
appImage:
artifactName: ${name}-${version}.${ext}
# -----
# macOS
# -----
mac:
target: dmg
dmg:
artifactName: ${name}-${version}.${ext}
# ----------------
# Publish options
# ----------------
publish:
provider: 'github'
vPrefixedTagName: false # default: true
releaseType: release # default: draft

View File

@@ -3,11 +3,12 @@ import { mergeConfig, UserConfig } from 'vite';
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'; import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import { getAliasesFromTsConfig, getClientEnvironmentVariables } from './vite-config-helper'; import { getAliasesFromTsConfig, getClientEnvironmentVariables } from './vite-config-helper';
import { createVueConfig } from './vite.config'; 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 MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts'); const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html'); const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
const DIST_DIR = resolvePathFromProjectRoot('dist_electron/'); const DIST_DIR = resolvePathFromProjectRoot(distDirs.electronUnbundled);
export default defineConfig({ export default defineConfig({
main: getSharedElectronConfig({ main: getSharedElectronConfig({

196
package-lock.json generated
View File

@@ -1,14 +1,15 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.12.1", "version": "0.12.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.12.1", "version": "0.12.3",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@floating-ui/vue": "^1.0.2",
"@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0", "@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0", "@fortawesome/free-regular-svg-icons": "^6.4.0",
@@ -19,11 +20,8 @@
"cross-fetch": "^4.0.0", "cross-fetch": "^4.0.0",
"electron-progressbar": "^2.1.0", "electron-progressbar": "^2.1.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"install": "^0.13.0",
"liquor-tree": "^0.2.70",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"npm": "^9.8.1", "npm": "^9.8.1",
"v-tooltip": "2.1.3",
"vue": "^2.7.14" "vue": "^2.7.14"
}, },
"devDependencies": { "devDependencies": {
@@ -1718,6 +1716,7 @@
"version": "7.22.10", "version": "7.22.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz",
"integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==",
"dev": true,
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@@ -1728,7 +1727,8 @@
"node_modules/@babel/runtime/node_modules/regenerator-runtime": { "node_modules/@babel/runtime/node_modules/regenerator-runtime": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
"dev": true
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.22.5", "version": "7.22.5",
@@ -2522,6 +2522,62 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
"integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==",
"dependencies": {
"@floating-ui/utils": "^0.1.3"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz",
"integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==",
"dependencies": {
"@floating-ui/core": "^1.4.2",
"@floating-ui/utils": "^0.1.3"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.3.tgz",
"integrity": "sha512-uvnFKtPgzLnpzzTRfhDlvXX0kLYi9lDRQbcDmT8iXl71Rx+uwSuaUIQl3DNC7w5OweAQ7XQMDObML+KaYDQfng=="
},
"node_modules/@floating-ui/vue": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.0.2.tgz",
"integrity": "sha512-sImlAl9mAoCKZLNlwWz2P2ZMJIDlOEDXrRD6aD2sIHAka1LPC+nWtB+D3lPe7IE7FGWSbwBPTnlSdlABa3Fr0A==",
"dependencies": {
"@floating-ui/dom": "^1.4.5",
"vue-demi": ">=0.13.0"
}
},
"node_modules/@floating-ui/vue/node_modules/vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@fortawesome/fontawesome-common-types": { "node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.4.2", "version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
@@ -9425,14 +9481,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/install": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz",
"integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
@@ -10771,11 +10819,6 @@
"uc.micro": "^1.0.1" "uc.micro": "^1.0.1"
} }
}, },
"node_modules/liquor-tree": {
"version": "0.2.70",
"resolved": "https://registry.npmjs.org/liquor-tree/-/liquor-tree-0.2.70.tgz",
"integrity": "sha512-5CiMlDVmuveYwwc27mYe1xZ3J4aHhZBErUhIp9ov4v4wIBso+s5JAByOOit4iOCMCQ5ODd8VggbKymzZREYbBQ=="
},
"node_modules/listr2": { "node_modules/listr2": {
"version": "3.14.0", "version": "3.14.0",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
@@ -10924,7 +10967,8 @@
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
}, },
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
@@ -16113,16 +16157,6 @@
"node": ">=12.13.0" "node": ">=12.13.0"
} }
}, },
"node_modules/popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.28", "version": "8.4.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz",
@@ -20354,17 +20388,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/v-tooltip": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/v-tooltip/-/v-tooltip-2.1.3.tgz",
"integrity": "sha512-xXngyxLQTOx/yUEy50thb8te7Qo4XU6h4LZB6cvEfVd9mnysUxLEoYwGWDdqR+l69liKsy3IPkdYff3J1gAJ5w==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"lodash": "^4.17.21",
"popper.js": "^1.16.1",
"vue-resize": "^1.0.1"
}
},
"node_modules/validate-npm-package-license": { "node_modules/validate-npm-package-license": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -20765,17 +20788,6 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/vue-resize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-1.0.1.tgz",
"integrity": "sha512-z5M7lJs0QluJnaoMFTIeGx6dIkYxOwHThlZDeQnWZBizKblb99GSejPnK37ZbNE/rVwDcYcHY+Io+AxdpY952w==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"vue": "^2.6.0"
}
},
"node_modules/vue-template-compiler": { "node_modules/vue-template-compiler": {
"version": "2.7.14", "version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",
@@ -22451,6 +22463,7 @@
"version": "7.22.10", "version": "7.22.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz",
"integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==",
"dev": true,
"requires": { "requires": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@@ -22458,7 +22471,8 @@
"regenerator-runtime": { "regenerator-runtime": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
"dev": true
} }
} }
}, },
@@ -22962,6 +22976,45 @@
"integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==", "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==",
"dev": true "dev": true
}, },
"@floating-ui/core": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
"integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==",
"requires": {
"@floating-ui/utils": "^0.1.3"
}
},
"@floating-ui/dom": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz",
"integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==",
"requires": {
"@floating-ui/core": "^1.4.2",
"@floating-ui/utils": "^0.1.3"
}
},
"@floating-ui/utils": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.3.tgz",
"integrity": "sha512-uvnFKtPgzLnpzzTRfhDlvXX0kLYi9lDRQbcDmT8iXl71Rx+uwSuaUIQl3DNC7w5OweAQ7XQMDObML+KaYDQfng=="
},
"@floating-ui/vue": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.0.2.tgz",
"integrity": "sha512-sImlAl9mAoCKZLNlwWz2P2ZMJIDlOEDXrRD6aD2sIHAka1LPC+nWtB+D3lPe7IE7FGWSbwBPTnlSdlABa3Fr0A==",
"requires": {
"@floating-ui/dom": "^1.4.5",
"vue-demi": ">=0.13.0"
},
"dependencies": {
"vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"requires": {}
}
}
},
"@fortawesome/fontawesome-common-types": { "@fortawesome/fontawesome-common-types": {
"version": "6.4.2", "version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
@@ -28223,11 +28276,6 @@
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
"dev": true "dev": true
}, },
"install": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz",
"integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA=="
},
"internal-slot": { "internal-slot": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
@@ -29256,11 +29304,6 @@
"uc.micro": "^1.0.1" "uc.micro": "^1.0.1"
} }
}, },
"liquor-tree": {
"version": "0.2.70",
"resolved": "https://registry.npmjs.org/liquor-tree/-/liquor-tree-0.2.70.tgz",
"integrity": "sha512-5CiMlDVmuveYwwc27mYe1xZ3J4aHhZBErUhIp9ov4v4wIBso+s5JAByOOit4iOCMCQ5ODd8VggbKymzZREYbBQ=="
},
"listr2": { "listr2": {
"version": "3.14.0", "version": "3.14.0",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
@@ -29368,7 +29411,8 @@
"lodash": { "lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
}, },
"lodash.debounce": { "lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
@@ -32878,11 +32922,6 @@
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
"dev": true "dev": true
}, },
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
},
"postcss": { "postcss": {
"version": "8.4.28", "version": "8.4.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz",
@@ -36104,17 +36143,6 @@
"sade": "^1.7.3" "sade": "^1.7.3"
} }
}, },
"v-tooltip": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/v-tooltip/-/v-tooltip-2.1.3.tgz",
"integrity": "sha512-xXngyxLQTOx/yUEy50thb8te7Qo4XU6h4LZB6cvEfVd9mnysUxLEoYwGWDdqR+l69liKsy3IPkdYff3J1gAJ5w==",
"requires": {
"@babel/runtime": "^7.13.10",
"lodash": "^4.17.21",
"popper.js": "^1.16.1",
"vue-resize": "^1.0.1"
}
},
"validate-npm-package-license": { "validate-npm-package-license": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -36341,14 +36369,6 @@
} }
} }
}, },
"vue-resize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-1.0.1.tgz",
"integrity": "sha512-z5M7lJs0QluJnaoMFTIeGx6dIkYxOwHThlZDeQnWZBizKblb99GSejPnK37ZbNE/rVwDcYcHY+Io+AxdpY952w==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"vue-template-compiler": { "vue-template-compiler": {
"version": "2.7.14", "version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",

View File

@@ -1,27 +1,31 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.12.1", "version": "0.12.3",
"private": true, "private": true,
"slogan": "Now you have the choice", "slogan": "Now you have the choice",
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆", "description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
"author": "undergroundwires", "author": "undergroundwires",
"type": "module", "type": "module",
"main": "./dist-electron-unbundled/main/index.cjs",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"preview": "vite preview", "preview": "vite preview",
"test:unit": "vitest run --dir tests/unit", "test:unit": "vitest run --dir tests/unit",
"test:integration": "vitest run --dir tests/integration", "test:integration": "vitest run --dir tests/integration",
"test:e2e": "vue-cli-service test:e2e",
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"", "test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"", "test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml", "lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
"install-deps": "node scripts/npm-install.js",
"icons:build": "node scripts/logo-update.js", "icons:build": "node scripts/logo-update.js",
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
"check: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:dev": "electron-vite dev",
"electron:preview": "electron-vite preview", "electron:preview": "electron-vite preview",
"electron:prebuild": "electron-vite build", "electron:prebuild": "electron-vite build",
"electron:build": "electron-builder", "electron:build": "electron-builder",
"lint:eslint": "eslint .", "lint:eslint": "eslint . --ignore-path .gitignore",
"lint:md": "markdownlint **/*.md --ignore node_modules", "lint:md": "markdownlint **/*.md --ignore node_modules",
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent", "lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
"lint:md:relative-urls": "remark . --frail --use remark-validate-links", "lint:md:relative-urls": "remark . --frail --use remark-validate-links",
@@ -29,8 +33,8 @@
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps" "postuninstall": "electron-builder install-app-deps"
}, },
"main": "./dist_electron/main/index.cjs",
"dependencies": { "dependencies": {
"@floating-ui/vue": "^1.0.2",
"@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0", "@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0", "@fortawesome/free-regular-svg-icons": "^6.4.0",
@@ -41,11 +45,8 @@
"cross-fetch": "^4.0.0", "cross-fetch": "^4.0.0",
"electron-progressbar": "^2.1.0", "electron-progressbar": "^2.1.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"install": "^0.13.0",
"liquor-tree": "^0.2.70",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"npm": "^9.8.1", "npm": "^9.8.1",
"v-tooltip": "2.1.3",
"vue": "^2.7.14" "vue": "^2.7.14"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,10 +0,0 @@
require('@rushstack/eslint-patch/modern-module-resolution.js');
module.exports = {
env: {
node: true,
},
rules: {
'import/extensions': ['error', 'always'],
},
};

View File

@@ -1,55 +0,0 @@
import { unlink, readFile } from 'fs/promises';
import { join } from 'path';
import { log, die, LOG_LEVELS } from '../utils/log.js';
import { exists } from '../utils/io.js';
import { SUPPORTED_PLATFORMS, CURRENT_PLATFORM } from '../utils/platform.js';
import { getAppName } from '../utils/npm.js';
export async function clearAppLogFile(projectDir) {
if (!projectDir) { throw new Error('missing project directory'); }
const logPath = await determineLogPath(projectDir);
if (!logPath || !await exists(logPath)) {
log(`Skipping clearing logs, log file does not exist: ${logPath}.`);
return;
}
try {
await unlink(logPath);
log(`Successfully cleared the log file at: ${logPath}.`);
} catch (error) {
die(`Failed to clear the log file at: ${logPath}. Reason: ${error}`);
}
}
export async function readAppLogFile(projectDir) {
if (!projectDir) { throw new Error('missing project directory'); }
const logPath = await determineLogPath(projectDir);
if (!logPath || !await exists(logPath)) {
log(`No log file at: ${logPath}`, LOG_LEVELS.WARN);
return undefined;
}
const logContent = await readLogFile(logPath);
return logContent;
}
async function determineLogPath(projectDir) {
if (!projectDir) { throw new Error('missing project directory'); }
const appName = await getAppName(projectDir);
if (!appName) {
die('App name not found.');
}
const logFilePaths = {
[SUPPORTED_PLATFORMS.MAC]: () => join(process.env.HOME, 'Library', 'Logs', appName, 'main.log'),
[SUPPORTED_PLATFORMS.LINUX]: () => join(process.env.HOME, '.config', appName, 'logs', 'main.log'),
[SUPPORTED_PLATFORMS.WINDOWS]: () => join(process.env.USERPROFILE, 'AppData', 'Roaming', appName, 'logs', 'main.log'),
};
const logFilePath = logFilePaths[CURRENT_PLATFORM]?.();
if (!logFilePath) {
log(`Cannot determine log path, unsupported OS: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
}
return logFilePath;
}
async function readLogFile(logFilePath) {
const content = await readFile(logFilePath, 'utf-8');
return content?.trim().length > 0 ? content : undefined;
}

View File

@@ -1,126 +0,0 @@
import { splitTextIntoLines, indentText } from '../utils/text.js';
import { die } from '../utils/log.js';
import { readAppLogFile } from './app-logs.js';
const ELECTRON_CRASH_TITLE = 'Error'; // Used by electron for early crashes
const LOG_ERROR_MARKER = '[error]'; // from electron-log
const EXPECTED_LOG_MARKERS = [
'[WINDOW_INIT]',
'[PRELOAD_INIT]',
'[APP_MOUNT_INIT]',
];
export async function checkForErrors(stderr, windowTitles, projectDir) {
if (!projectDir) { throw new Error('missing project directory'); }
const errors = await gatherErrors(stderr, windowTitles, projectDir);
if (errors.length) {
die(formatErrors(errors));
}
}
async function gatherErrors(stderr, windowTitles, projectDir) {
if (!projectDir) { throw new Error('missing project directory'); }
const logContent = await readAppLogFile(projectDir);
return [
verifyStdErr(stderr),
verifyApplicationLogsExist(logContent),
...EXPECTED_LOG_MARKERS.map((marker) => verifyLogMarkerExistsInLogs(logContent, marker)),
verifyWindowTitle(windowTitles),
verifyErrorsInLogs(logContent),
].filter(Boolean);
}
function formatErrors(errors) {
if (!errors || !errors.length) { throw new Error('missing errors'); }
return [
'Errors detected during execution:',
...errors.map(
(error) => formatError(error),
),
].join('\n---\n');
}
function formatError(error) {
if (!error) { throw new Error('missing error'); }
if (!error.reason) { throw new Error(`missing reason, error (${typeof error}): ${JSON.stringify(error)}`); }
let message = `Reason: ${indentText(error.reason, 1)}`;
if (error.description) {
message += `\nDescription:\n${indentText(error.description, 2)}`;
}
return message;
}
function verifyApplicationLogsExist(logContent) {
if (!logContent || !logContent.length) {
return describeError(
'Missing application logs',
'Application logs are empty not were not found.',
);
}
return undefined;
}
function verifyLogMarkerExistsInLogs(logContent, marker) {
if (!marker) {
throw new Error('missing marker');
}
if (!logContent?.includes(marker)) {
return describeError(
'Incomplete application logs',
`Missing identifier "${marker}" in application logs.`,
);
}
return undefined;
}
function verifyWindowTitle(windowTitles) {
const errorTitles = windowTitles.filter(
(title) => title.toLowerCase().includes(ELECTRON_CRASH_TITLE),
);
if (errorTitles.length) {
return describeError(
'Unexpected window title',
'One or more window titles suggest an error occurred in the application:'
+ `\nError Titles: ${errorTitles.join(', ')}`
+ `\nAll Titles: ${windowTitles.join(', ')}`,
);
}
return undefined;
}
function verifyStdErr(stderrOutput) {
if (stderrOutput && stderrOutput.length > 0) {
return describeError(
'Standard error stream (`stderr`) is not empty.',
stderrOutput,
);
}
return undefined;
}
function verifyErrorsInLogs(logContent) {
if (!logContent || !logContent.length) {
return undefined;
}
const logLines = getNonEmptyLines(logContent)
.filter((line) => line.includes(LOG_ERROR_MARKER));
if (!logLines.length) {
return undefined;
}
return describeError(
'Application log file',
logLines.join('\n'),
);
}
function describeError(reason, description) {
return {
reason,
description: `${description}\n\nThis might indicate an early crash or significant runtime issue.`,
};
}
function getNonEmptyLines(text) {
return splitTextIntoLines(text)
.filter((line) => line?.trim().length > 0);
}

View File

@@ -1,34 +0,0 @@
import { access, chmod } from 'fs/promises';
import { constants } from 'fs';
import { findSingleFileByExtension } from '../../utils/io.js';
import { log } from '../../utils/log.js';
export async function prepareLinuxApp(desktopDistPath) {
const { absolutePath: appFile } = await findSingleFileByExtension(
'AppImage',
desktopDistPath,
);
await makeExecutable(appFile);
return {
appExecutablePath: appFile,
};
}
async function makeExecutable(appFile) {
if (!appFile) { throw new Error('missing file'); }
if (await isExecutable(appFile)) {
log('AppImage is already executable.');
return;
}
log('Making it executable...');
await chmod(appFile, 0o755);
}
async function isExecutable(file) {
try {
await access(file, constants.X_OK);
return true;
} catch {
return false;
}
}

View File

@@ -1,38 +0,0 @@
import { mkdtemp, rmdir } from 'fs/promises';
import { join } from 'path';
import { tmpdir } from 'os';
import { findSingleFileByExtension, exists } from '../../utils/io.js';
import { log, die } from '../../utils/log.js';
import { runCommand } from '../../utils/run-command.js';
export async function prepareWindowsApp(desktopDistPath) {
const workdir = await mkdtemp(join(tmpdir(), 'win-nsis-installation-'));
if (await exists(workdir)) {
log(`Temporary directory ${workdir} already exists, cleaning up...`);
await rmdir(workdir, { recursive: true });
}
const { appExecutablePath } = await installNsis(workdir, desktopDistPath);
return {
appExecutablePath,
cleanup: async () => {
log(`Cleaning up working directory ${workdir}...`);
await rmdir(workdir, { recursive: true });
},
};
}
async function installNsis(installationPath, desktopDistPath) {
const { absolutePath: installerPath } = await findSingleFileByExtension('exe', desktopDistPath);
log(`Silently installing contents of ${installerPath} to ${installationPath}...`);
const { error } = await runCommand(`"${installerPath}" /S /D=${installationPath}`);
if (error) {
die(`Failed to install.\n${error}`);
}
const { absolutePath: appExecutablePath } = await findSingleFileByExtension('exe', installationPath);
return {
appExecutablePath,
};
}

View File

@@ -1,20 +0,0 @@
import { log } from './utils/log.js';
const PROCESS_ARGUMENTS = process.argv.slice(2);
export const COMMAND_LINE_FLAGS = Object.freeze({
FORCE_REBUILD: '--build',
TAKE_SCREENSHOT: '--screenshot',
});
export function logCurrentArgs() {
if (!PROCESS_ARGUMENTS.length) {
log('No additional arguments provided.');
return;
}
log(`Arguments: ${PROCESS_ARGUMENTS.join(', ')}`);
}
export function hasCommandLineFlag(flag) {
return PROCESS_ARGUMENTS.includes(flag);
}

View File

@@ -1,7 +0,0 @@
import { join } from 'path';
export const DESKTOP_BUILD_COMMAND = 'npm run electron:prebuild && npm run electron:build -- --publish never';
export const PROJECT_DIR = process.cwd();
export const DESKTOP_DIST_PATH = join(PROJECT_DIR, 'dist');
export const APP_EXECUTION_DURATION_IN_SECONDS = 60; // Long enough for CI runners
export const SCREENSHOT_PATH = join(PROJECT_DIR, 'screenshot.png');

View File

@@ -1,3 +0,0 @@
import { main } from './main.js';
await main();

View File

@@ -1,68 +0,0 @@
import { logCurrentArgs, COMMAND_LINE_FLAGS, hasCommandLineFlag } from './cli-args.js';
import { log, die } from './utils/log.js';
import { ensureNpmProjectDir, npmInstall, npmBuild } from './utils/npm.js';
import { clearAppLogFile } from './app/app-logs.js';
import { checkForErrors } from './app/check-for-errors.js';
import { runApplication } from './app/runner.js';
import { CURRENT_PLATFORM, SUPPORTED_PLATFORMS } from './utils/platform.js';
import { prepareLinuxApp } from './app/extractors/linux.js';
import { prepareWindowsApp } from './app/extractors/windows.js';
import { prepareMacOsApp } from './app/extractors/macos.js';
import {
DESKTOP_BUILD_COMMAND,
PROJECT_DIR,
DESKTOP_DIST_PATH,
APP_EXECUTION_DURATION_IN_SECONDS,
SCREENSHOT_PATH,
} from './config.js';
export async function main() {
logCurrentArgs();
await ensureNpmProjectDir(PROJECT_DIR);
await npmInstall(PROJECT_DIR);
await npmBuild(
PROJECT_DIR,
DESKTOP_BUILD_COMMAND,
DESKTOP_DIST_PATH,
hasCommandLineFlag(COMMAND_LINE_FLAGS.FORCE_REBUILD),
);
await clearAppLogFile(PROJECT_DIR);
const {
stderr, stdout, isCrashed, windowTitles,
} = await extractAndRun();
if (stdout) {
log(`Output (stdout) from application execution:\n${stdout}`);
}
if (isCrashed) {
die('The application encountered an error during its execution.');
}
await checkForErrors(stderr, windowTitles, PROJECT_DIR);
log('🥳🎈 Success! Application completed without any runtime errors.');
process.exit(0);
}
async function extractAndRun() {
const extractors = {
[SUPPORTED_PLATFORMS.MAC]: () => prepareMacOsApp(DESKTOP_DIST_PATH),
[SUPPORTED_PLATFORMS.LINUX]: () => prepareLinuxApp(DESKTOP_DIST_PATH),
[SUPPORTED_PLATFORMS.WINDOWS]: () => prepareWindowsApp(DESKTOP_DIST_PATH),
};
const extractor = extractors[CURRENT_PLATFORM];
if (!extractor) {
throw new Error(`Platform not supported: ${CURRENT_PLATFORM}`);
}
const { appExecutablePath, cleanup } = await extractor();
try {
return await runApplication(
appExecutablePath,
APP_EXECUTION_DURATION_IN_SECONDS,
hasCommandLineFlag(COMMAND_LINE_FLAGS.TAKE_SCREENSHOT),
SCREENSHOT_PATH,
);
} finally {
if (cleanup) {
log('Cleaning up post-execution resources...');
await cleanup();
}
}
}

View File

@@ -1,48 +0,0 @@
import { extname, join } from 'path';
import { readdir, access } from 'fs/promises';
import { constants } from 'fs';
import { log, die, LOG_LEVELS } from './log.js';
export async function findSingleFileByExtension(extension, directory) {
if (!directory) { throw new Error('Missing directory'); }
if (!extension) { throw new Error('Missing file extension'); }
if (!await exists(directory)) {
die(`Directory does not exist: ${directory}`);
return [];
}
const directoryContents = await readdir(directory);
const foundFileNames = directoryContents.filter((file) => extname(file) === `.${extension}`);
const withoutUninstaller = foundFileNames.filter(
(fileName) => !fileName.toLowerCase().includes('uninstall'), // NSIS build has `Uninstall {app-name}.exe`
);
if (!withoutUninstaller.length) {
die(`No ${extension} found in ${directory} directory.`);
}
if (withoutUninstaller.length > 1) {
log(`Found multiple ${extension} files: ${withoutUninstaller.join(', ')}. Using first occurrence`, LOG_LEVELS.WARN);
}
return {
absolutePath: join(directory, withoutUninstaller[0]),
};
}
export async function exists(path) {
if (!path) { throw new Error('Missing path'); }
try {
await access(path, constants.F_OK);
return true;
} catch {
return false;
}
}
export async function isDirMissingOrEmpty(dir) {
if (!dir) { throw new Error('Missing directory'); }
if (!await exists(dir)) {
return true;
}
const contents = await readdir(dir);
return contents.length === 0;
}

View File

@@ -1,39 +0,0 @@
export const LOG_LEVELS = Object.freeze({
INFO: 'INFO',
WARN: 'WARN',
ERROR: 'ERROR',
});
export function log(message, level = LOG_LEVELS.INFO) {
const timestamp = new Date().toISOString();
const config = LOG_LEVEL_CONFIG[level] || LOG_LEVEL_CONFIG[LOG_LEVELS.INFO];
const formattedMessage = `[${timestamp}][${config.color}${level}${COLOR_CODES.RESET}] ${message}`;
config.method(formattedMessage);
}
export function die(message) {
log(message, LOG_LEVELS.ERROR);
process.exit(1);
}
const COLOR_CODES = {
RESET: '\x1b[0m',
LIGHT_RED: '\x1b[91m',
YELLOW: '\x1b[33m',
LIGHT_BLUE: '\x1b[94m',
};
const LOG_LEVEL_CONFIG = {
[LOG_LEVELS.INFO]: {
color: COLOR_CODES.LIGHT_BLUE,
method: console.log,
},
[LOG_LEVELS.WARN]: {
color: COLOR_CODES.YELLOW,
method: console.warn,
},
[LOG_LEVELS.ERROR]: {
color: COLOR_CODES.LIGHT_RED,
method: console.error,
},
};

View File

@@ -1,87 +0,0 @@
import { join } from 'path';
import { rmdir, readFile } from 'fs/promises';
import { exists, isDirMissingOrEmpty } from './io.js';
import { runCommand } from './run-command.js';
import { LOG_LEVELS, die, log } from './log.js';
export async function ensureNpmProjectDir(projectDir) {
if (!projectDir) { throw new Error('missing project directory'); }
if (!await exists(join(projectDir, 'package.json'))) {
die(`'package.json' not found in project directory: ${projectDir}`);
}
}
export async function npmInstall(projectDir) {
if (!projectDir) { throw new Error('missing project directory'); }
const npmModulesPath = join(projectDir, 'node_modules');
if (!await isDirMissingOrEmpty(npmModulesPath)) {
log(`Directory "${npmModulesPath}" exists and has content. Skipping \`npm install\`.`);
return;
}
log('Starting dependency installation...');
const { error } = await runCommand('npm install --loglevel=error', {
stdio: 'inherit',
cwd: projectDir,
});
if (error) {
die(error);
}
}
export async function npmBuild(projectDir, buildCommand, distDir, forceRebuild) {
if (!projectDir) { throw new Error('missing project directory'); }
if (!buildCommand) { throw new Error('missing build command'); }
if (!distDir) { throw new Error('missing distribution directory'); }
const isMissingBuild = await isDirMissingOrEmpty(distDir);
if (!isMissingBuild && !forceRebuild) {
log(`Directory "${distDir}" exists and has content. Skipping build: '${buildCommand}'.`);
return;
}
if (forceRebuild) {
log(`Removing directory "${distDir}" for a clean build (triggered by --build flag).`);
await rmdir(distDir, { recursive: true });
}
log('Starting project build...');
const { error } = await runCommand(buildCommand, {
stdio: 'inherit',
cwd: projectDir,
});
if (error) {
log(error, LOG_LEVELS.WARN); // Cannot disable Vue CLI errors, stderr contains false-positives.
}
}
export async function getAppName(projectDir) {
if (!projectDir) { throw new Error('missing project directory'); }
const packageData = await readPackageJsonContents(projectDir);
try {
const packageJson = JSON.parse(packageData);
if (!packageJson.name) {
die(`The 'package.json' file doesn't specify a name: ${packageData}`);
}
return packageJson.name;
} catch (error) {
die(`Unable to parse 'package.json'. Error: ${error}\nContent: ${packageData}`, LOG_LEVELS.ERROR);
return undefined;
}
}
async function readPackageJsonContents(projectDir) {
if (!projectDir) { throw new Error('missing project directory'); }
const packagePath = join(projectDir, 'package.json');
if (!await exists(packagePath)) {
die(`'package.json' file not found at ${packagePath}`);
}
try {
const packageData = await readFile(packagePath, 'utf8');
return packageData;
} catch (error) {
log(`Error reading 'package.json' from ${packagePath}.`, LOG_LEVELS.ERROR);
die(`Error detail: ${error}`, LOG_LEVELS.ERROR);
throw error;
}
}

View File

@@ -1,9 +0,0 @@
import { platform } from 'os';
export const SUPPORTED_PLATFORMS = {
MAC: 'darwin',
LINUX: 'linux',
WINDOWS: 'win32',
};
export const CURRENT_PLATFORM = platform();

View File

@@ -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"

199
scripts/npm-install.js Normal file
View 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
View 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();

View 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();

View File

@@ -1,6 +1,7 @@
export type Constructible<T, TArgs extends unknown[] = never> = { export type Constructible<T, TArgs extends unknown[] = never> = {
prototype: T; prototype: T;
apply: (this: unknown, args: TArgs) => void; apply: (this: unknown, args: TArgs) => void;
readonly name: string;
}; };
export type PropertyKeys<T> = { export type PropertyKeys<T> = {

View File

@@ -1,25 +1,23 @@
import { IApplicationContext } from '@/application/Context/IApplicationContext'; import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { Environment } from '@/infrastructure/Environment/Environment'; import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
import { IApplicationFactory } from '../IApplicationFactory'; import { IApplicationFactory } from '../IApplicationFactory';
import { ApplicationFactory } from '../ApplicationFactory'; import { ApplicationFactory } from '../ApplicationFactory';
import { ApplicationContext } from './ApplicationContext'; import { ApplicationContext } from './ApplicationContext';
export async function buildContext( export async function buildContext(
factory: IApplicationFactory = ApplicationFactory.Current, factory: IApplicationFactory = ApplicationFactory.Current,
environment = Environment.CurrentEnvironment, environment = RuntimeEnvironment.CurrentEnvironment,
): Promise<IApplicationContext> { ): Promise<IApplicationContext> {
if (!factory) { throw new Error('missing factory'); } if (!factory) { throw new Error('missing factory'); }
if (!environment) { throw new Error('missing environment'); } if (!environment) { throw new Error('missing environment'); }
const app = await factory.getApp(); const app = await factory.getApp();
const os = getInitialOs(app, environment); const os = getInitialOs(app, environment.os);
return new ApplicationContext(app, os); return new ApplicationContext(app, os);
} }
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem { function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
const currentOs = environment.os;
const supportedOsList = app.getSupportedOsList(); const supportedOsList = app.getSupportedOsList();
if (supportedOsList.includes(currentOs)) { if (supportedOsList.includes(currentOs)) {
return currentOs; return currentOs;

View File

@@ -7,14 +7,14 @@ import MacOsData from '@/application/collections/macos.yaml';
import LinuxData from '@/application/collections/linux.yaml'; import LinuxData from '@/application/collections/linux.yaml';
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser'; import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
import { Application } from '@/domain/Application'; import { Application } from '@/domain/Application';
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata'; import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory'; import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
import { parseCategoryCollection } from './CategoryCollectionParser'; import { parseCategoryCollection } from './CategoryCollectionParser';
export function parseApplication( export function parseApplication(
categoryParser = parseCategoryCollection, categoryParser = parseCategoryCollection,
informationParser = parseProjectInformation, informationParser = parseProjectInformation,
metadata: IAppMetadata = AppMetadataFactory.Current.instance, metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
collectionsData = PreParsedCollections, collectionsData = PreParsedCollections,
): IApplication { ): IApplication {
validateCollectionsData(collectionsData); validateCollectionsData(collectionsData);

View File

@@ -1,13 +1,13 @@
import { IProjectInformation } from '@/domain/IProjectInformation'; import { IProjectInformation } from '@/domain/IProjectInformation';
import { ProjectInformation } from '@/domain/ProjectInformation'; import { ProjectInformation } from '@/domain/ProjectInformation';
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata'; import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
import { Version } from '@/domain/Version'; import { Version } from '@/domain/Version';
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory'; import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
import { ConstructorArguments } from '@/TypeHelpers'; import { ConstructorArguments } from '@/TypeHelpers';
export function export function
parseProjectInformation( parseProjectInformation(
metadata: IAppMetadata = AppMetadataFactory.Current.instance, metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
createProjectInformation: ProjectInformationFactory = ( createProjectInformation: ProjectInformationFactory = (
...args ...args
) => new ProjectInformation(...args), ) => new ProjectInformation(...args),

View File

@@ -0,0 +1,5 @@
import { CompiledCode } from '../CompiledCode';
export interface CodeSegmentMerger {
mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode;
}

View File

@@ -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');
}

View File

@@ -1,4 +1,4 @@
export interface ICompiledCode { export interface CompiledCode {
readonly code: string; readonly code: string;
readonly revertCode?: string; readonly revertCode?: string;
} }

View File

@@ -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;
}

View File

@@ -1,149 +1,10 @@
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
import { FunctionCall } from '../FunctionCall'; import { FunctionCall } from '../FunctionCall';
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection'; import { CompiledCode } from './CompiledCode';
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
import { ICompiledCode } from './ICompiledCode';
export class FunctionCallCompiler implements IFunctionCallCompiler { export interface FunctionCallCompiler {
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler(); compileFunctionCalls(
calls: readonly FunctionCall[],
protected constructor(
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
) {
}
public compileCall(
calls: IFunctionCall[],
functions: ISharedFunctionCollection, functions: ISharedFunctionCollection,
): ICompiledCode { ): CompiledCode;
if (!functions) { throw new Error('missing functions'); }
if (!calls) { throw new Error('missing calls'); }
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
const context: ICompilationContext = {
allFunctions: functions,
callSequence: calls,
expressionsCompiler: this.expressionsCompiler,
};
const code = compileCallSequence(context);
return code;
}
}
interface ICompilationContext {
allFunctions: ISharedFunctionCollection;
callSequence: readonly IFunctionCall[];
expressionsCompiler: IExpressionsCompiler;
}
interface ICompiledFunctionCall {
readonly code: string;
readonly revertCode: string;
}
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
const compiledFunctions = context.callSequence
.flatMap((call) => compileSingleCall(call, context));
return {
code: merge(compiledFunctions.map((f) => f.code)),
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
};
}
function compileSingleCall(
call: IFunctionCall,
context: ICompilationContext,
): ICompiledFunctionCall[] {
const func = context.allFunctions.getFunctionByName(call.functionName);
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
if (func.body.code) { // Function with inline code
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
return [compiledCode];
}
// Function with inner calls
return func.body.calls
.map((innerCall) => {
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
return compileSingleCall(compiledCall, context);
})
.flat();
}
function compileCode(
code: IFunctionCode,
args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler,
): ICompiledFunctionCall {
return {
code: compiler.compileExpressions(code.execute, args),
revertCode: compiler.compileExpressions(code.revert, args),
};
}
function compileArgs(
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler,
): IReadOnlyFunctionCallArgumentCollection {
return argsToCompile
.getAllParameterNames()
.map((parameterName) => {
const { argumentValue } = argsToCompile.getArgument(parameterName);
const compiledValue = compiler.compileExpressions(argumentValue, args);
return new FunctionCallArgument(parameterName, compiledValue);
})
.reduce((compiledArgs, arg) => {
compiledArgs.addArgument(arg);
return compiledArgs;
}, new FunctionCallArgumentCollection());
}
function merge(codeParts: readonly string[]): string {
return codeParts
.filter((part) => part?.length > 0)
.join('\n');
}
function ensureThatCallArgumentsExistInParameterDefinition(
func: ISharedFunction,
args: IReadOnlyFunctionCallArgumentCollection,
): void {
const callArgumentNames = args.getAllParameterNames();
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
}
function findUnexpectedParameters(
callArgumentNames: string[],
functionParameterNames: string[],
): string[] {
if (!callArgumentNames.length && !functionParameterNames.length) {
return [];
}
return callArgumentNames
.filter((callParam) => !functionParameterNames.includes(callParam));
}
function throwIfNotEmpty(
functionName: string,
unexpectedParameters: string[],
expectedParameters: string[],
) {
if (!unexpectedParameters.length) {
return;
}
throw new Error(
// eslint-disable-next-line prefer-template
`Function "${functionName}" has unexpected parameter(s) provided: `
+ `"${unexpectedParameters.join('", "')}"`
+ '. Expected parameter(s): '
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
);
} }

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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'),
);
}

View File

@@ -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[];
}

View File

@@ -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[],
}

View File

@@ -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;
}

View File

@@ -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());
}

View File

@@ -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),
},
];
}
}

View File

@@ -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();
}
}

View File

@@ -1,16 +1,6 @@
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection'; import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
import { IFunctionCall } from './IFunctionCall';
export class FunctionCall implements IFunctionCall { export interface FunctionCall {
constructor( readonly functionName: string;
public readonly functionName: string, readonly args: IReadOnlyFunctionCallArgumentCollection;
public readonly args: IReadOnlyFunctionCallArgumentCollection,
) {
if (!functionName) {
throw new Error('missing function name in function call');
}
if (!args) {
throw new Error('missing args');
}
}
} }

View File

@@ -1,10 +1,10 @@
import type { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from '@/application/collections/'; import type { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from '@/application/collections/';
import { IFunctionCall } from './IFunctionCall'; import { FunctionCall } from './FunctionCall';
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
import { FunctionCallArgument } from './Argument/FunctionCallArgument'; 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) { if (calls === undefined) {
throw new Error('missing call data'); throw new Error('missing call data');
} }
@@ -22,12 +22,12 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
return [calls as FunctionCallData]; return [calls as FunctionCallData];
} }
function parseFunctionCall(call: FunctionCallData): IFunctionCall { function parseFunctionCall(call: FunctionCallData): FunctionCall {
if (!call) { if (!call) {
throw new Error('missing call data'); throw new Error('missing call data');
} }
const callArgs = parseArgs(call.parameters); const callArgs = parseArgs(call.parameters);
return new FunctionCall(call.function, callArgs); return new ParsedFunctionCall(call.function, callArgs);
} }
function parseArgs( function parseArgs(

View File

@@ -1,6 +0,0 @@
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
export interface IFunctionCall {
readonly functionName: string;
readonly args: IReadOnlyFunctionCallArgumentCollection;
}

View File

@@ -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');
}
}
}

View File

@@ -1,5 +1,5 @@
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
import { IFunctionCall } from './Call/IFunctionCall'; import { FunctionCall } from './Call/FunctionCall';
export interface ISharedFunction { export interface ISharedFunction {
readonly name: string; readonly name: string;
@@ -9,8 +9,8 @@ export interface ISharedFunction {
export interface ISharedFunctionBody { export interface ISharedFunctionBody {
readonly type: FunctionBodyType; readonly type: FunctionBodyType;
readonly code: IFunctionCode; readonly code: IFunctionCode | undefined;
readonly calls: readonly IFunctionCall[]; readonly calls: readonly FunctionCall[] | undefined;
} }
export enum FunctionBodyType { export enum FunctionBodyType {

View File

@@ -2,4 +2,5 @@ import { ISharedFunction } from './ISharedFunction';
export interface ISharedFunctionCollection { export interface ISharedFunctionCollection {
getFunctionByName(name: string): ISharedFunction; getFunctionByName(name: string): ISharedFunction;
getRequiredParameterNames(functionName: string): string[];
} }

View File

@@ -1,4 +1,4 @@
import { IFunctionCall } from './Call/IFunctionCall'; import { FunctionCall } from './Call/FunctionCall';
import { import {
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody, FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
@@ -8,7 +8,7 @@ import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParam
export function createCallerFunction( export function createCallerFunction(
name: string, name: string,
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
callSequence: readonly IFunctionCall[], callSequence: readonly FunctionCall[],
): ISharedFunction { ): ISharedFunction {
if (!callSequence || !callSequence.length) { if (!callSequence || !callSequence.length) {
throw new Error(`missing call sequence in function "${name}"`); throw new Error(`missing call sequence in function "${name}"`);
@@ -38,7 +38,7 @@ class SharedFunction implements ISharedFunction {
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly parameters: IReadOnlyFunctionParameterCollection, public readonly parameters: IReadOnlyFunctionParameterCollection,
content: IFunctionCode | readonly IFunctionCall[], content: IFunctionCode | readonly FunctionCall[],
bodyType: FunctionBodyType, bodyType: FunctionBodyType,
) { ) {
if (!name) { throw new Error('missing function name'); } if (!name) { throw new Error('missing function name'); }
@@ -46,7 +46,7 @@ class SharedFunction implements ISharedFunction {
this.body = { this.body = {
type: bodyType, type: bodyType,
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined, 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,
}; };
} }
} }

View File

@@ -21,6 +21,15 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
return func; 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) { private has(functionName: string) {
return this.functionsByName.has(functionName); return this.functionsByName.has(functionName);
} }

View File

@@ -7,12 +7,12 @@ import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmp
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator'; import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
import { IScriptCompiler } from './IScriptCompiler'; import { IScriptCompiler } from './IScriptCompiler';
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection'; 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 { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser'; import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
import { SharedFunctionsParser } from './Function/SharedFunctionsParser'; import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
import { parseFunctionCalls } from './Function/Call/FunctionCallParser'; 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 { export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection; private readonly functions: ISharedFunctionCollection;
@@ -21,7 +21,7 @@ export class ScriptCompiler implements IScriptCompiler {
functions: readonly FunctionData[] | undefined, functions: readonly FunctionData[] | undefined,
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance, sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance, private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
private readonly codeValidator: ICodeValidator = CodeValidator.instance, private readonly codeValidator: ICodeValidator = CodeValidator.instance,
) { ) {
if (!syntax) { throw new Error('missing syntax'); } if (!syntax) { throw new Error('missing syntax'); }
@@ -40,7 +40,7 @@ export class ScriptCompiler implements IScriptCompiler {
if (!script) { throw new Error('missing script'); } if (!script) { throw new Error('missing script'); }
try { try {
const calls = parseFunctionCalls(script.call); 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); validateCompiledCode(compiledCode, this.codeValidator);
return new ScriptCode( return new ScriptCode(
compiledCode.code, 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( [compiledCode.code, compiledCode.revertCode].forEach(
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]), (code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
); );

View File

@@ -3558,82 +3558,98 @@ functions:
parameters: parameters:
- name: prefName - name: prefName
- name: jsonValue - name: jsonValue
# prefs.js file (https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file) exists at docs: |-
# - Default installation: This script either creates or updates the `user.js` file to set specific Mozilla Firefox preferences.
# ~/.mozilla/firefox/<profile-name>/prefs.js
# - Flatpak installation: The `user.js` file can be found in a Firefox profile folder [1] and its location depends on the type of installation:
# ~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/prefs.js
# - Snap installation: - Default: `~/.mozilla/firefox/<profile-name>/user.js`
# ~/snap/firefox/common/.mozilla/firefox/<profile-name>/prefs.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: |- code: |-
pref_name='{{ $prefName }}' pref_name='{{ $prefName }}'
pref_value='{{ $jsonValue }}' pref_value='{{ $jsonValue }}'
echo "Setting preference \"$pref_name\" to \"$pref_value\"." echo "Setting preference \"$pref_name\" to \"$pref_value\"."
pref_file_paths=( declare -a profile_paths=(
~/.mozilla/firefox/*/prefs.js ~/.mozilla/firefox/*/
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/prefs.js ~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
~/snap/firefox/common/.mozilla/firefox/*/prefs.js ~/snap/firefox/common/.mozilla/firefox/*/
) )
declare -i total_files_found=0 declare -i total_profiles_found=0
for pref_file in "${pref_file_paths[@]}"; do for profile_dir in "${profile_paths[@]}"; do
if [ -f "$pref_file" ]; then if [ ! -d "$profile_dir" ]; then
((total_files_found++)) continue
echo "$pref_file:" fi
pref_start="user_pref(\"$pref_name\"," if [[ ! "$(basename "$profile_dir")" =~ ^[a-z0-9]{8}\..+ ]]; then
pref_line="user_pref(\"$pref_name\", $pref_value);" continue # Not a profile folder
if ! grep --quiet "^$pref_start" "${pref_file}"; then fi
echo $'\t'"Preference is not configured before." ((total_profiles_found++))
echo -n $'\n'"$pref_line" >> "$pref_file" user_js_file="${profile_dir}user.js"
echo $'\t'"Successfully configured." echo "$user_js_file:"
else if [ ! -f "$user_js_file" ]; then
if grep --quiet "^$pref_line$" "${pref_file}"; then touch "$user_js_file"
echo $'\t'"Skipping. Preference is already configured as expected." echo $'\t''Created new user.js file'
else fi
sed --in-place "/^$pref_start/d" "$pref_file" pref_start="user_pref(\"$pref_name\","
echo $'\t'"Deleted assignment with unexpected value." pref_line="user_pref(\"$pref_name\", $pref_value);"
echo -n $'\n'"$pref_line" >> "$pref_file" if ! grep --quiet "^$pref_start" "${user_js_file}"; then
echo $'\t'"Successfully reconfigured with expected value." echo -n $'\n'"$pref_line" >> "$user_js_file"
fi echo $'\t'"Successfully added a new preference in $user_js_file."
fi 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 fi
done done
if [ "$total_files_found" -eq 0 ]; then if [ "$total_profiles_found" -eq 0 ]; then
echo "No changes, no preference file is found." echo 'No profile folders are found, no changes are made.'
else else
echo "Ensured that $total_files_found profiles are compilant." echo "Preferences verified in $total_profiles_found profiles."
fi fi
revertCode: |- revertCode: |-
pref_name='{{ $prefName }}' pref_name='{{ $prefName }}'
pref_value='{{ $jsonValue }}' pref_value='{{ $jsonValue }}'
echo "Restoring \"$pref_name\" to its default." echo "Reverting preference: \"$pref_name\" to its default."
pref_file_paths=( declare -a profile_paths=(
~/.mozilla/firefox/*/prefs.js ~/.mozilla/firefox/*/
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/prefs.js ~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
~/snap/firefox/common/.mozilla/firefox/*/prefs.js ~/snap/firefox/common/.mozilla/firefox/*/
) )
declare -i total_files_found=0 declare -i total_profiles_found=0
for pref_file in "${pref_file_paths[@]}"; do for profile_dir in "${profile_paths[@]}"; do
if [ -f "$pref_file" ]; then user_js_file="${profile_dir}user.js"
((total_files_found++)) if [ ! -f "$user_js_file" ]; then
echo "$pref_file:" continue
pref_start="user_pref(\"$pref_name\"," fi
pref_line="user_pref(\"$pref_name\", $pref_value);" ((total_profiles_found++))
if ! grep --quiet "^$pref_start" "${pref_file}"; then echo "$user_js_file:"
echo $'\t'"Skipping. Preference is not configured before." pref_start="user_pref(\"$pref_name\","
else pref_line="user_pref(\"$pref_name\", $pref_value);"
if grep --quiet "^$pref_line$" "${pref_file}"; then if ! grep --quiet "^$pref_start" "${user_js_file}"; then
sed --in-place "/^$pref_line/d" "$pref_file" echo $'\t''Skipping, preference was not configured before.'
echo $'\t'"Successfully restored preference value to its default." elif grep --quiet "^$pref_line$" "${user_js_file}"; then
else sed --in-place "/^$pref_line/d" "$user_js_file"
echo $'\t'"Skipping, the preference has value that is not configured by privacy.sexy." echo $'\t''Successfully reverted preference to default.'
fi if ! grep --quiet '[^[:space:]]' "$user_js_file"; then
rm "$user_js_file"
echo $'\t''Removed user.js file as it became empty.'
fi fi
else
echo $'\t''Skipping, the preference has value that is not configured by privacy.sexy.'
fi fi
done done
if [ "$total_files_found" -eq 0 ]; then if [ "$total_profiles_found" -eq 0 ]; then
echo "No changes, no preference file is found." echo 'No reversion was necessary.'
else else
echo "Ensured that $total_files_found profiles are compilant." echo "Preferences verified in $total_profiles_found profiles."
fi fi
- -
name: RenameFile name: RenameFile

View File

@@ -606,9 +606,46 @@ actions:
wevtutil.exe cl %1 "%%i" wevtutil.exe cl %1 "%%i"
) )
- -
name: Clean Windows Defender scan history name: Clear Defender scan (protection) history
docs: https://www.thewindowsclub.com/clear-windows-defender-protection-history docs: |-
code: del "%ProgramData%\Microsoft\Windows Defender\Scans\History\" /s /f /q 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 name: Clear credentials from Windows Credential Manager
code: |- code: |-
@@ -1094,32 +1131,130 @@ actions:
serviceName: wercplsupport # Check: (Get-Service -Name wercplsupport).StartType serviceName: wercplsupport # Check: (Get-Service -Name wercplsupport).StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual defaultStartupMode: Manual # Allowed values: Automatic | Manual
- -
category: Disable automatic driver updates by Windows Update category: Disable Windows Update data collection
children: children:
- -
name: Disable device metadata retrieval (breaks auto updates) category: Disable automatic driver updates by Windows Update
recommend: strict children:
docs: -
- https://www.stigviewer.com/stig/windows_server_2012_member_server/2014-01-07/finding/V-21964 name: Disable device metadata retrieval (breaks auto updates)
- https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-deviceinstallation#deviceinstallation-preventdevicemetadatafromnetwork recommend: strict
code: |- docs:
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f - https://www.stigviewer.com/stig/windows_server_2012_member_server/2014-01-07/finding/V-21964
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f - https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-deviceinstallation#deviceinstallation-preventdevicemetadatafromnetwork
revertCode: |- code: |-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 0 /f 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 0 /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: Do not include drivers with Windows Updates category: Disable obtaining updates from other PCs on the Internet (delivery optimization)
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsUpdate::ExcludeWUDriversInQualityUpdate docs: |-
recommend: strict Windows Delivery Optimization is a feature introduced by Microsoft to facilitate a more efficient downloading process for Windows
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "ExcludeWUDriversInQualityUpdate" /t REG_DWORD /d 1 /f updates, upgrades, and applications [1] [2]. Instead of exclusively relying on Microsoft's servers, this feature identifies other
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "ExcludeWUDriversInQualityUpdate" /t REG_DWORD /d 0 /f 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
name: Prevent Windows Update for device driver search system ensures more efficient downloads [2]. To support this process, Delivery Optimization uses a local cache to temporarily store
docs: https://www.stigviewer.com/stig/windows_7/2018-02-12/finding/V-21965 downloaded files [2].
recommend: strict
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DriverSearching" /v "SearchOrderConfig" /t REG_DWORD /d 0 /f While Delivery Optimization is designed for speed and reliability, its operation raises privacy concerns. Specifically, when enabled,
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DriverSearching" /v "SearchOrderConfig" /t REG_DWORD /d 1 /f 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 name: Disable cloud speech recognition
recommend: standard recommend: standard
@@ -1776,12 +1911,26 @@ actions:
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" /v "DisableSoftLanding" /t REG_DWORD /d "0" /f 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) name: Disable Windows Spotlight (random wallpaper on lock screen)
recommend: standard recommend: strict
docs: docs: |-
- https://docs.microsoft.com/en-us/windows/configuration/windows-spotlight The script disables the Windows Spotlight feature. Windows Spotlight is a feature in Windows 10 and Windows 11 [1] that automatically downloads
- https://docs.microsoft.com/en-us/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services#25-windows-spotlight 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 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 name: Disable Microsoft consumer experiences
recommend: standard recommend: standard
@@ -2272,8 +2421,7 @@ actions:
function: SetVsCodeSetting function: SetVsCodeSetting
parameters: parameters:
setting: update.mode setting: update.mode
powerShellValue: >- powerShellValue: manual
'manual'
- -
name: Show Release Notes from Microsoft online service after an update name: Show Release Notes from Microsoft online service after an update
call: call:
@@ -2415,21 +2563,25 @@ actions:
category: Chromium Edge settings category: Chromium Edge settings
children: children:
- -
name: Disable Edge usage and crash-related data reporting (shows "Your browser is managed") # Obselete since Microsoft Edge version 89 name: Disable Edge diagnostic data sending (shows "Your browser is managed")
recommend: standard recommend: standard
docs: docs:
- https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::MetricsReportingEnabled - http://archive.today/2023.08.26-152941/https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::DiagnosticData
- https://docs.microsoft.com/en-us/DeployEdge/microsoft-edge-policies#metricsreportingenabled - https://learn.microsoft.com/DeployEdge/microsoft-edge-policies#diagnosticdata
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /t REG_DWORD /d 0 /f - http://archive.today/2023.08.26-152952/https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::MetricsReportingEnabled
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /f - 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
name: Disable sending site information (shows "Your browser is managed") # Obselete since Microsoft Edge version 89 - https://learn.microsoft.com/DeployEdge/microsoft-edge-policies#sendsiteinfotoimproveservices
recommend: standard code: |-
docs: :: Disabling metrics and site info sending for Edge v88 ≥
- https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::SendSiteInfoToImproveServices reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /t REG_DWORD /d 0 /f
- https://docs.microsoft.com/en-us/DeployEdge/microsoft-edge-policies#sendsiteinfotoimproveservices reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "SendSiteInfoToImproveServices" /t REG_DWORD /d 0 /f
code: 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 ≤)
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "SendSiteInfoToImproveServices" /f 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 name: Disable Automatic Installation of Microsoft Edge Chromium
docs: docs:
@@ -2456,7 +2608,7 @@ actions:
recommend: standard recommend: standard
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.EdgeUI::DisableRecentApps 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 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 name: Turn off backtracking
recommend: standard 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 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 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: docs:
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::Scan_PurgeItemsAfterDelay - https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::Scan_PurgeItemsAfterDelay
# Managing with MpPreference module: # 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 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 reg delete "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost" /v "EnableWebContentEvaluation" /f 2>nul
- -
name: Disable automatic updates category: Disable automatic updates
docs: docs: |-
- https://docs.microsoft.com/fr-fr/security-updates/windowsupdateservices/18127152 Disabling automatic updates is often considered counterintuitive when it comes to securing your system. However, there are substantial arguments
- http://batcmd.com/windows/10/services/usosvc/ to consider this option if you're privacy-centric:
call:
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 name: Disable Automatic Updates (AU) feature
parameters: docs: |-
code: |- This script deactivates the Automatic Updates feature in Windows. By disabling Automatic Updates,
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "0" /f you gain control over when your system is updated, which may be preferable in specific
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "2" /f privacy-sensitive environments.
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 The script changes a specific setting in your computer's registry, with a key called `NoAutoUpdate`, which has
revertCode: |- two possible states [1] [2]:
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 - `0`: Automatic Updates are enabled.
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /f 2>nul - `1`: Automatic Updates are disabled.
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime" /f 2>nul
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 NonActive 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 name: Disable installing Windows updates without user approval
parameters: docs: |-
serviceName: UsoSvc # Check: (Get-Service -Name 'UsoSvc').StartType This script changes how your Windows computer handles automatic updates by modifying the `AUOptions` registry key.
defaultStartupMode: Automatic # Allowed values: Automatic | Manual 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 NonActive 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 NonActive 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 category: Configure handling of downloaded files
docs: |- docs: |-
@@ -5169,25 +5525,6 @@ actions:
- -
category: Disable OS services category: Disable OS services
children: 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) name: Microsoft Account Sign-in Assistant (breaks Microsoft Store and Microsoft Account sign-in)
recommend: strict recommend: strict
@@ -6613,16 +6950,75 @@ actions:
code: reg delete "HKCU\Environment" /v "OneDrive" /f 2>nul code: reg delete "HKCU\Environment" /v "OneDrive" /f 2>nul
- -
name: Uninstall Edge (chromium-based) 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: call:
function: RunPowerShell -
parameters: function: RunInlineCode
code: |- parameters:
$installer = (Get-ChildItem "$env:ProgramFiles*\Microsoft\Edge\Application\*\Installer\setup.exe") code: reg add "HKLM\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdateDev" /v "AllowUninstall" /t REG_DWORD /d "1" /f
if (!$installer) { 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
Write-Host 'Could not find the installer' -
} else { function: RunPowerShell
& $installer.FullName -Uninstall -System-Level -Verbose-Logging -Force-Uninstall 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 category: Disable built-in Windows features
children: children:
@@ -7473,6 +7869,7 @@ functions:
parameters: parameters:
- name: code - name: code
- name: revertCode - name: revertCode
optional: true
call: call:
function: RunPowerShell function: RunPowerShell
parameters: parameters:
@@ -7540,7 +7937,8 @@ functions:
Remove-Item $streamOutFile, $batchFile Remove-Item $streamOutFile, $batchFile
} }
revertCode: |- # Duplicated until custom pipes are implemented 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') $trustedInstallerSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464')
$trustedInstallerName = $trustedInstallerSid.Translate([System.Security.Principal.NTAccount]) $trustedInstallerName = $trustedInstallerSid.Translate([System.Security.Principal.NTAccount])
$streamOutFile = New-TemporaryFile $streamOutFile = New-TemporaryFile
@@ -7583,6 +7981,7 @@ functions:
} finally { } finally {
Remove-Item $streamOutFile, $batchFile Remove-Item $streamOutFile, $batchFile
} }
{{ end }}
- -
name: DisableServiceInRegistry name: DisableServiceInRegistry
parameters: parameters:

View File

@@ -1,32 +1,37 @@
import { Environment } from '@/infrastructure/Environment/Environment'; import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { getWindowInjectedSystemOperations } from './SystemOperations/WindowInjectedSystemOperations';
export class CodeRunner { export class CodeRunner {
constructor( constructor(
private readonly environment = Environment.CurrentEnvironment, private readonly system = getWindowInjectedSystemOperations(),
private readonly environment = RuntimeEnvironment.CurrentEnvironment,
) { ) {
if (!environment.system) { if (!system) {
throw new Error('missing system operations'); throw new Error('missing system operations');
} }
} }
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> { public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
const { system } = this.environment; const { os } = this.environment;
const dir = system.location.combinePaths( const dir = this.system.location.combinePaths(
system.operatingSystem.getTempDirectory(), this.system.operatingSystem.getTempDirectory(),
folderName, folderName,
); );
await system.fileSystem.createDirectory(dir, true); await this.system.fileSystem.createDirectory(dir, true);
const filePath = system.location.combinePaths(dir, `run.${fileExtension}`); const filePath = this.system.location.combinePaths(dir, `run.${fileExtension}`);
await system.fileSystem.writeToFile(filePath, code); await this.system.fileSystem.writeToFile(filePath, code);
await system.fileSystem.setFilePermissions(filePath, '755'); await this.system.fileSystem.setFilePermissions(filePath, '755');
const command = getExecuteCommand(filePath, this.environment); const command = getExecuteCommand(filePath, os);
system.command.execute(command); this.system.command.execute(command);
} }
} }
function getExecuteCommand(scriptPath: string, environment: Environment): string { function getExecuteCommand(
switch (environment.os) { scriptPath: string,
currentOperatingSystem: OperatingSystem,
): string {
switch (currentOperatingSystem) {
case OperatingSystem.Linux: case OperatingSystem.Linux:
return `x-terminal-emulator -e '${scriptPath}'`; return `x-terminal-emulator -e '${scriptPath}'`;
case OperatingSystem.macOS: case OperatingSystem.macOS:
@@ -37,6 +42,6 @@ function getExecuteCommand(scriptPath: string, environment: Environment): string
case OperatingSystem.Windows: case OperatingSystem.Windows:
return scriptPath; return scriptPath;
default: default:
throw Error(`unsupported os: ${OperatingSystem[environment.os]}`); throw Error(`unsupported os: ${OperatingSystem[currentOperatingSystem]}`);
} }
} }

View File

@@ -1,8 +0,0 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ISystemOperations } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
export interface IEnvironment {
readonly isDesktop: boolean;
readonly os: OperatingSystem | undefined;
readonly system: ISystemOperations | undefined;
}

View File

@@ -1,13 +0,0 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ISystemOperations } from './SystemOperations/ISystemOperations';
export type WindowVariables = {
system: ISystemOperations;
isDesktop: boolean;
os: OperatingSystem;
};
declare global {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Window extends WindowVariables { }
}

View File

@@ -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;

View File

@@ -1,24 +1,24 @@
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata'; import { IEnvironmentVariables } from './IEnvironmentVariables';
/* Validation is externalized to keep the environment objects simple */ /* Validation is externalized to keep the environment objects simple */
export function validateMetadata(metadata: IAppMetadata): void { export function validateEnvironmentVariables(environment: IEnvironmentVariables): void {
if (!metadata) { if (!environment) {
throw new Error('missing metadata'); throw new Error('missing environment');
} }
const keyValues = capturePropertyValues(metadata); const keyValues = capturePropertyValues(environment);
if (!Object.keys(keyValues).length) { if (!Object.keys(keyValues).length) {
throw new Error('Unable to capture metadata key/value pairs'); throw new Error('Unable to capture key/value pairs');
} }
const keysMissingValue = getMissingMetadataKeys(keyValues); const keysMissingValue = getKeysMissingValues(keyValues);
if (keysMissingValue.length > 0) { if (keysMissingValue.length > 0) {
throw new Error(`Metadata keys missing: ${keysMissingValue.join(', ')}`); throw new Error(`Environment keys missing: ${keysMissingValue.join(', ')}`);
} }
} }
function getMissingMetadataKeys(keyValuePairs: Record<string, unknown>): string[] { function getKeysMissingValues(keyValuePairs: Record<string, unknown>): string[] {
return Object.entries(keyValuePairs) return Object.entries(keyValuePairs)
.reduce((acc, [key, value]) => { .reduce((acc, [key, value]) => {
if (!value) { if (!value && typeof value !== 'boolean') {
acc.push(key); acc.push(key);
} }
return acc; return acc;

View File

@@ -1,8 +1,5 @@
/** /**
* Represents essential metadata about the application. * Represents essential metadata about the application.
*
* Designed to decouple the process of retrieving metadata
* (e.g., from the build environment) from the rest of the application.
*/ */
export interface IAppMetadata { export interface IAppMetadata {
readonly version: string; readonly version: string;

View File

@@ -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;
}

View File

@@ -0,0 +1,5 @@
import { IEnvironmentVariables } from './IEnvironmentVariables';
export interface IEnvironmentVariablesFactory {
readonly instance: IEnvironmentVariables;
}

View File

@@ -1,8 +1,13 @@
// Only variables prefixed with VITE_ are exposed to Vite-processed code // Only variables prefixed with VITE_ are exposed to Vite-processed code
export const VITE_ENVIRONMENT_KEYS = { export const VITE_USER_DEFINED_ENVIRONMENT_KEYS = {
VERSION: 'VITE_APP_VERSION', VERSION: 'VITE_APP_VERSION',
NAME: 'VITE_APP_NAME', NAME: 'VITE_APP_NAME',
SLOGAN: 'VITE_APP_SLOGAN', SLOGAN: 'VITE_APP_SLOGAN',
REPOSITORY_URL: 'VITE_APP_REPOSITORY_URL', REPOSITORY_URL: 'VITE_APP_REPOSITORY_URL',
HOMEPAGE_URL: 'VITE_APP_HOMEPAGE_URL', HOMEPAGE_URL: 'VITE_APP_HOMEPAGE_URL',
} as const; } as const;
export const VITE_ENVIRONMENT_KEYS = {
...VITE_USER_DEFINED_ENVIRONMENT_KEYS,
DEV: 'DEV',
} as const;

View File

@@ -1,9 +1,9 @@
import { IAppMetadata } from '../IAppMetadata'; import { IEnvironmentVariables } from '../IEnvironmentVariables';
/** /**
* Provides the application's metadata using Vite's environment variables. * Provides the application's environment variables.
*/ */
export class ViteAppMetadata implements IAppMetadata { export class ViteEnvironmentVariables implements IEnvironmentVariables {
// Ensure the use of import.meta.env prefix for the following properties. // Ensure the use of import.meta.env prefix for the following properties.
// Vue will replace these statically during production builds. // Vue will replace these statically during production builds.
@@ -26,4 +26,8 @@ export class ViteAppMetadata implements IAppMetadata {
public get homepageUrl(): string { public get homepageUrl(): string {
return import.meta.env.VITE_APP_HOMEPAGE_URL; return import.meta.env.VITE_APP_HOMEPAGE_URL;
} }
public get isNonProduction(): boolean {
return import.meta.env.DEV;
}
} }

View File

@@ -1,10 +1,20 @@
import { IEventSubscription } from './IEventSource';
import { IEventSubscriptionCollection } from './IEventSubscriptionCollection'; import { IEventSubscriptionCollection } from './IEventSubscriptionCollection';
import { IEventSubscription } from './IEventSource';
export class EventSubscriptionCollection implements IEventSubscriptionCollection { export class EventSubscriptionCollection implements IEventSubscriptionCollection {
private readonly subscriptions = new Array<IEventSubscription>(); 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); this.subscriptions.push(...subscriptions);
} }
@@ -12,4 +22,9 @@ export class EventSubscriptionCollection implements IEventSubscriptionCollection
this.subscriptions.forEach((listener) => listener.unsubscribe()); this.subscriptions.forEach((listener) => listener.unsubscribe());
this.subscriptions.splice(0, this.subscriptions.length); this.subscriptions.splice(0, this.subscriptions.length);
} }
public unsubscribeAllAndRegister(subscriptions: IEventSubscription[]) {
this.unsubscribeAll();
this.register(subscriptions);
}
} }

View File

@@ -1,7 +1,9 @@
import { IEventSubscription } from '@/infrastructure/Events/IEventSource'; import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
export interface IEventSubscriptionCollection { export interface IEventSubscriptionCollection {
register(...subscriptions: IEventSubscription[]); readonly subscriptionCount: number;
unsubscribeAll(); register(subscriptions: IEventSubscription[]): void;
unsubscribeAll(): void;
unsubscribeAllAndRegister(subscriptions: IEventSubscription[]);
} }

View 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);
}
}

View 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),
};
}

View File

@@ -0,0 +1,3 @@
export interface ILogger {
info (...params: unknown[]): void;
}

View File

@@ -0,0 +1,5 @@
import { ILogger } from './ILogger';
export interface ILoggerFactory {
readonly logger: ILogger;
}

View File

@@ -0,0 +1,5 @@
import { ILogger } from './ILogger';
export class NoopLogger implements ILogger {
public info(): void { /* NOOP */ }
}

View 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);
}
}

View File

@@ -1,18 +0,0 @@
import { IAppMetadata } from './IAppMetadata';
import { IAppMetadataFactory } from './IAppMetadataFactory';
import { validateMetadata } from './MetadataValidator';
import { ViteAppMetadata } from './Vite/ViteAppMetadata';
export class AppMetadataFactory implements IAppMetadataFactory {
public static readonly Current = new AppMetadataFactory();
public readonly instance: IAppMetadata;
protected constructor(validator: MetadataValidator = validateMetadata) {
const metadata = new ViteAppMetadata();
validator(metadata);
this.instance = metadata;
}
}
export type MetadataValidator = typeof validateMetadata;

View File

@@ -1,5 +0,0 @@
import { IAppMetadata } from './IAppMetadata';
export interface IAppMetadataFactory {
readonly instance: IAppMetadata;
}

View File

@@ -0,0 +1,7 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IRuntimeEnvironment {
readonly isDesktop: boolean;
readonly os: OperatingSystem | undefined;
readonly isNonProduction: boolean;
}

View File

@@ -1,29 +1,29 @@
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ISystemOperations } from '@/infrastructure/Environment/SystemOperations/ISystemOperations'; import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector'; import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector'; import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
import { IEnvironment } from './IEnvironment'; import { IRuntimeEnvironment } from './IRuntimeEnvironment';
import { WindowVariables } from './WindowVariables';
import { validateWindowVariables } from './WindowVariablesValidator';
export class Environment implements IEnvironment { export class RuntimeEnvironment implements IRuntimeEnvironment {
public static readonly CurrentEnvironment: IEnvironment = new Environment(window); public static readonly CurrentEnvironment: IRuntimeEnvironment = new RuntimeEnvironment(window);
public readonly isDesktop: boolean; public readonly isDesktop: boolean;
public readonly os: OperatingSystem | undefined; public readonly os: OperatingSystem | undefined;
public readonly system: ISystemOperations | undefined; public readonly isNonProduction: boolean;
protected constructor( protected constructor(
window: Partial<Window>, window: Partial<Window>,
environmentVariables: IEnvironmentVariables = EnvironmentVariablesFactory.Current.instance,
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(), browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
windowValidator: WindowValidator = validateWindowVariables,
) { ) {
if (!window) { if (!window) {
throw new Error('missing window'); throw new Error('missing window');
} }
windowValidator(window); this.isNonProduction = environmentVariables.isNonProduction;
this.isDesktop = isDesktop(window); this.isDesktop = isDesktop(window);
if (this.isDesktop) { if (this.isDesktop) {
this.os = window?.os; this.os = window?.os;
@@ -34,7 +34,6 @@ export class Environment implements IEnvironment {
this.os = browserOsDetector.detect(userAgent); this.os = browserOsDetector.detect(userAgent);
} }
} }
this.system = window?.system;
} }
} }
@@ -45,5 +44,3 @@ function getUserAgent(window: Partial<Window>): string {
function isDesktop(window: Partial<WindowVariables>): boolean { function isDesktop(window: Partial<WindowVariables>): boolean {
return window?.isDesktop === true; return window?.isDesktop === true;
} }
export type WindowValidator = typeof validateWindowVariables;

View File

@@ -1,4 +1,4 @@
export interface ISanityCheckOptions { export interface ISanityCheckOptions {
readonly validateMetadata: boolean; readonly validateEnvironmentVariables: boolean;
readonly validateEnvironment: boolean; readonly validateWindowVariables: boolean;
} }

View File

@@ -1,9 +1,9 @@
import { ISanityCheckOptions } from './Common/ISanityCheckOptions'; import { ISanityCheckOptions } from './Common/ISanityCheckOptions';
import { ISanityValidator } from './Common/ISanityValidator'; import { ISanityValidator } from './Common/ISanityValidator';
import { MetadataValidator } from './Validators/MetadataValidator'; import { EnvironmentVariablesValidator } from './Validators/EnvironmentVariablesValidator';
const DefaultSanityValidators: ISanityValidator[] = [ const DefaultSanityValidators: ISanityValidator[] = [
new MetadataValidator(), new EnvironmentVariablesValidator(),
]; ];
/* Helps to fail-fast on errors */ /* Helps to fail-fast on errors */

View File

@@ -1,16 +0,0 @@
import { Environment } from '@/infrastructure/Environment/Environment';
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
import { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
import { FactoryValidator, FactoryFunction } from '../Common/FactoryValidator';
export class EnvironmentValidator extends FactoryValidator<IEnvironment> {
constructor(factory: FactoryFunction<IEnvironment> = () => Environment.CurrentEnvironment) {
super(factory);
}
public override name = 'environment';
public override shouldValidate(options: ISanityCheckOptions): boolean {
return options.validateEnvironment;
}
}

View File

@@ -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;
}
}

Some files were not shown because too many files have changed in this diff Show More