Compare commits

..

1 Commits

Author SHA1 Message Date
undergroundwires
0c8412c467 Improve disabling of Windows services
- Unify way of disabling Windows services using templating.
- Remove false-positive error messages.
- Use smarter logic to start/stop service.

Logic improvements include:
  - Check if service is running before stopping/starting the service.
  - Do not start the service it's not an Automatic service.
  - Do not write stderr if service cannot be stopped/started as it's not
    not the main goal of the function.
  - Check whether service is already disabled.

Add more documentation about the disabled Windows service.

Script: Disable diagnostics telemetry
 - Add missing revert script
 - Add more granular control for each service.
2021-11-21 20:58:44 +01:00
865 changed files with 53540 additions and 72955 deletions

View File

@@ -1,3 +1,2 @@
> 1% > 1%
last 2 versions last 2 versions
not dead

View File

@@ -1,11 +0,0 @@
[*.{js,jsx,ts,tsx,vue,sh}]
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
[{Dockerfile}]
indent_style = space
indent_size = 4

View File

@@ -1,95 +0,0 @@
const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
const tsconfigJson = require('./tsconfig.json');
require('@rushstack/eslint-patch/modern-module-resolution');
module.exports = {
root: true,
env: {
node: true,
es2022: true, // add globals and sets parserOptions.ecmaVersion to 2022
},
extends: [
// Vue specific rules, eslint-plugin-vue
'plugin:vue/essential',
// Extends eslint-config-airbnb
'@vue/eslint-config-airbnb-with-typescript',
// Extends @typescript-eslint/recommended
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
'@vue/typescript/recommended',
],
rules: {
...getOwnRules(),
...getTurnedOffBrokenRules(),
...getOpinionatedRuleOverrides(),
...getTodoRules(),
},
};
function getOwnRules() {
return {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'linebreak-style': ['error', 'unix'], // This is also enforced in .editorconfig and .gitattributes files
'import/order': [ // Enforce strict import order taking account into aliases
'error',
{
groups: [ // Enforce more strict order than AirBnb
'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
pathGroups: [ // Fix manually configured paths being incorrectly grouped as "external"
...getAliasesFromTsConfig(),
'js-yaml-loader!@/**',
].map((pattern) => ({ pattern, group: 'internal' })),
},
],
};
}
function getTodoRules() { // Should be worked on separate future commits
return {
'import/no-extraneous-dependencies': 'off',
// Accessibility improvements:
'vuejs-accessibility/form-control-has-label': 'off',
'vuejs-accessibility/click-events-have-key-events': 'off',
'vuejs-accessibility/anchor-has-content': 'off',
'vuejs-accessibility/accessible-emoji': 'off',
};
}
function getTurnedOffBrokenRules() {
return {
// Broken in TypeScript
'no-useless-constructor': 'off', // Cannot interpret TypeScript constructors
'no-shadow': 'off', // Fails with TypeScript enums
};
}
function getOpinionatedRuleOverrides() {
return {
// https://erkinekici.com/articles/linting-trap#no-use-before-define
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
// https://erkinekici.com/articles/linting-trap#arrow-body-style
'arrow-body-style': 'off',
// https://erkinekici.com/articles/linting-trap#no-plusplus
'no-plusplus': 'off',
// https://erkinekici.com/articles/linting-trap#no-param-reassign
'no-param-reassign': 'off',
// https://erkinekici.com/articles/linting-trap#class-methods-use-this
'class-methods-use-this': 'off',
// https://erkinekici.com/articles/linting-trap#importprefer-default-export
'import/prefer-default-export': 'off',
// https://erkinekici.com/articles/linting-trap#disallowing-for-of
// Original: https://github.com/airbnb/javascript/blob/d8cb404da74c302506f91e5928f30cc75109e74d/packages/eslint-config-airbnb-base/rules/style.js#L333-L351
'no-restricted-syntax': [
baseStyleRules['no-restricted-syntax'][0],
...baseStyleRules['no-restricted-syntax'].slice(1).filter((rule) => rule.selector !== 'ForOfStatement'),
],
};
}
function getAliasesFromTsConfig() {
return Object.keys(tsconfigJson.compilerOptions.paths)
.map((path) => `${path}*`);
}

6
.gitattributes vendored
View File

@@ -1,6 +0,0 @@
# Prevent Git from auto-converting to CRLF on Windows, and convert to LF on checkin.
# * : All files
# text=auto : If Git decides content it text, it converts CRLF to LF on checkin.
# eol=lf : forces Git to normalize line endings to LF on checkin and prevents conversion
# to CRLF when the file is checked out.
* text=auto eol=lf

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
github: undergroundwires

View File

@@ -21,9 +21,8 @@ A clear and concise description of what the bug is.
<!-- <!--
Which OS are you using? What version of OS you were using? Which OS are you using? What version of OS you were using?
On Windows: Open "Start button" > "Settings" > "System" > "About". On Windows you can find it using "Start button" > "Settings" > "System" > "About".
On macOS: Open "Apple menu (top left corner)" > "About This Mac". On macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
On Linux: Open terminal > type: lsb_release -a > copy paste the result.
--> -->
### Reproduction steps ### Reproduction steps

View File

@@ -14,7 +14,7 @@ You could alternatively send a PR directly (see CONTRIBUTING.md).
<!-- <!--
Which OS will the new script configure? Which OS will the new script configure?
One of the supported OSes: "Windows", "macOS" or "Linux". Either "Windows" or "macOS".
--> -->
### Name ### Name
@@ -31,11 +31,9 @@ E.g. "Disable webcam telemetry"
Code that will be executed when script is selected. Code that will be executed when script is selected.
Try to keep it as simple and backwards-compatible as possible. Try to keep it as simple and backwards-compatible as possible.
Allowed languages: Allowed languages:
- macOS: bash (sh)
- Windows: PowerShell (ps1) or batchfile - Windows: PowerShell (ps1) or batchfile
- 💡 Prioritize the one that's simpler, batchfile if similar. - 💡 Prioritize the one that's simpler, batchfile if similar.
- macOS: bash (sh)
- Linux: bash (sh) or Python 3
- 💡 Prioritize the one that's simpler, bash if similar.
--> -->
### Revert code ### Revert code

View File

@@ -1,12 +0,0 @@
inputs:
working-directory:
required: false
default: '.'
runs:
using: composite
steps:
-
name: Run `npm ci` with retries
shell: bash
run: npm run install-deps -- --ci
working-directory: ${{ inputs.working-directory }}

View File

@@ -1,8 +0,0 @@
runs:
using: composite
steps:
-
name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16.x

View File

@@ -1,4 +1,4 @@
name: release-git name: Bump & release
on: on:
push: # Ensure a new release is created for each new tag push: # Ensure a new release is created for each new tag

View File

@@ -1,100 +0,0 @@
name: checks.build
on:
push:
pull_request:
jobs:
build-web:
strategy:
matrix:
os: [ macos, ubuntu, windows ]
mode: [
# Vite mode: https://vitejs.dev/guide/env-and-mode.html
development, # Used by `dev` command
production, # Used by `build` command
# Vitest mode: https://vitest.dev/guide/cli.html
test, # Used by Vitest
]
fail-fast: false # Allows to see results from other combinations
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
-
name: Install dependencies
uses: ./.github/actions/npm-install-dependencies
-
name: Build web
run: npm run build -- --mode ${{ matrix.mode }}
-
name: Verify web build artifacts
run: npm run check:verify-build-artifacts -- --web
build-desktop:
strategy:
matrix:
os: [ macos, ubuntu, windows ]
mode: [
# electron-vite modes: https://electron-vite.org/guide/env-and-mode.html#global-env-variables
development, # Used by `dev` command
production, # Used by `build` and `preview` commands
]
fail-fast: false # Allows to see results from other combinations
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
-
name: Install dependencies
uses: ./.github/actions/npm-install-dependencies
-
name: Prebuild desktop
run: npm run electron:prebuild -- --mode ${{ matrix.mode }}
-
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
-
name: Verify bundled desktop build artifacts
run: npm run check:verify-build-artifacts -- --electron-bundled
build-docker:
strategy:
matrix:
os: [ macos, ubuntu ] # Windows runners do not support Linux containers
fail-fast: false # Allows to see results from other combinations
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Install Docker on macOS
if: matrix.os == 'macos' # macOS runner is missing Docker
run: |-
# Install Docker
brew install docker
# Docker on macOS misses daemon due to licensing, so install colima as runtime
brew install colima
# Start the daemon
colima start
-
name: Build Docker image
run: docker build -t undergroundwires/privacy.sexy:latest .
-
name: Run Docker image on port 8080
run: docker run -d -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest
-
name: Check server is up and returns HTTP 200
run: node ./scripts/verify-web-server-status.js --url http://localhost:8080

View File

@@ -1,72 +0,0 @@
name: checks.desktop-runtime-errors
# Verifies desktop builds for Electron applications across multiple OS platforms (macOS ,Ubuntu, and Windows).
on:
push:
pull_request:
jobs:
run-check:
strategy:
matrix:
os: [ macos, ubuntu, windows ]
fail-fast: false # Allows to see results from other combinations
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
-
name: Install dependencies
uses: ./.github/actions/npm-install-dependencies
-
name: Configure Ubuntu
if: matrix.os == 'ubuntu'
shell: bash
run: |-
sudo apt update
# Configure AppImage dependencies
sudo apt install -y libfuse2
# Configure DBUS (fixes `Failed to connect to the bus: Could not parse server address: Unknown address type`)
if ! command -v 'dbus-launch' &> /dev/null; then
echo 'DBUS does not exist, installing...'
sudo apt install -y dbus-x11 # Gives both dbus and dbus-launch utility
fi
sudo systemctl start dbus
DBUS_LAUNCH_OUTPUT=$(dbus-launch)
if [ $? -eq 0 ]; then
echo "${DBUS_LAUNCH_OUTPUT}" >> $GITHUB_ENV
else
echo 'Error: dbus-launch command did not execute successfully. Exiting.' >&2
echo "${DBUS_LAUNCH_OUTPUT}" >&2
exit 1
fi
# Configure fake (virtual) display
sudo apt install -y xvfb
sudo Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
echo "DISPLAY=:99" >> $GITHUB_ENV
# Install ImageMagick for screenshots
sudo apt install -y imagemagick
# Install xdotool and xprop (from x11-utils) for window title capturing
sudo apt install -y xdotool x11-utils
-
name: Test
shell: bash
run: |-
export SCREENSHOT=true
npm run check:desktop
-
name: Upload screenshot
if: always() # Run even if previous step fails
uses: actions/upload-artifact@v3
with:
name: screenshot-${{ matrix.os }}
path: screenshot.png

View File

@@ -1,22 +0,0 @@
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@v4
-
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

@@ -1,30 +0,0 @@
name: quality-checks
on: [ push, pull_request ]
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
lint-command:
- npm run lint:eslint
- npm run lint:yaml
- npm run lint:md
- npm run lint:md:relative-urls
- npm run lint:md:consistency
os: [ macos, ubuntu, windows ]
fail-fast: false # Still interested to see results from other combinations
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
-
name: Install dependencies
uses: ./.github/actions/npm-install-dependencies
-
name: Lint
run: ${{ matrix.lint-command }}

View File

@@ -1,55 +0,0 @@
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@v4
-
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@v4
-
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

@@ -1,42 +0,0 @@
name: checks.security.sast
on:
push:
pull_request:
schedule:
- cron: '0 0 * * 0' # at 00:00 on every Sunday
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [
javascript # analyzes code written in JavaScript, TypeScript and both.
]
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
-
name: Autobuild
uses: github/codeql-action/autobuild@v2
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{ matrix.language }}"

View File

@@ -1,4 +1,4 @@
name: release-desktop name: Deploy desktop
on: on:
release: release:
@@ -13,29 +13,22 @@ jobs:
fail-fast: false # So publish runs for other OSes if one fails fail-fast: false # So publish runs for other OSes if one fails
runs-on: ${{ matrix.os }}-latest runs-on: ${{ matrix.os }}-latest
steps: steps:
- - uses: actions/checkout@v2
uses: actions/checkout@v4
with: with:
ref: master # otherwise it defaults to the version tag missing bump commit ref: master # otherwise it defaults to the version tag missing bump commit
fetch-depth: 0 # fetch all history fetch-depth: 0 # fetch all history
- - name: Checkout to bump commit
name: Checkout to bump commit
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)" run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
- - name: Setup node
name: Setup node uses: actions/setup-node@v1
uses: ./.github/actions/setup-node with:
- node-version: 15.x
name: Install dependencies - name: Install dependencies
uses: ./.github/actions/npm-install-dependencies run: npm ci
- - name: Run unit tests
name: Run unit tests
run: npm run test:unit run: npm run test:unit
- - name: Publish desktop app
name: Prebuild run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
run: npm run electron:prebuild
-
name: Build and publish
run: npm run electron:build -- --publish always
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074 EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074

View File

@@ -1,8 +1,8 @@
name: release-site name: Deploy site
on: on:
release: release:
types: [created] # will be triggered when a NON-draft release is created and published. types: [created] # will be triggered when a NON-draft release is created and published.
jobs: jobs:
aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
@@ -10,7 +10,7 @@ jobs:
steps: steps:
- -
name: "Infrastructure: Checkout" name: "Infrastructure: Checkout"
uses: actions/checkout@v4 uses: actions/checkout@v2
with: with:
path: aws path: aws
repository: undergroundwires/aws-static-site-with-cd repository: undergroundwires/aws-static-site-with-cd
@@ -75,41 +75,32 @@ jobs:
working-directory: aws working-directory: aws
- -
name: "App: Checkout" name: "App: Checkout"
uses: actions/checkout@v4 uses: actions/checkout@v2
with: with:
path: app path: site
ref: master # otherwise we don't get version bump commit ref: master # otherwise we don't get version bump commit
- -
name: "App: Setup node" name: "App: Setup node"
uses: ./app/.github/actions/setup-node uses: actions/setup-node@v1
with:
node-version: 15.x
- -
name: "App: Install dependencies" name: "App: Install dependencies"
uses: ./app/.github/actions/npm-install-dependencies run: npm ci
with: working-directory: site
working-directory: app
- -
name: "App: Run unit tests" name: "App: Run unit tests"
run: npm run test:unit run: npm run test:unit
working-directory: app working-directory: site
- -
name: "App: Build" name: "App: Build"
run: npm run build run: npm run build
working-directory: app working-directory: site
-
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 "${web_output_dir}" \ --folder site/dist \
--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}} \

27
.github/workflows/quality-checks.yaml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Quality checks
on: [ push, pull_request ]
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
lint-command:
- npm run lint:vue
- npm run lint:yaml
- npm run lint:md
- npm run lint:md:relative-urls
- npm run lint:md:consistency
fail-fast: false # So it continues with other commands if one fails
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v1
with:
node-version: 15.x
- name: Install dependencies
run: npm ci
- name: Lint
run: ${{ matrix.lint-command }}

View File

@@ -1,4 +1,4 @@
name: checks.security.dependencies name: Security checks
on: on:
push: push:
@@ -13,10 +13,12 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- -
name: Setup node name: Setup node
uses: ./.github/actions/setup-node uses: actions/setup-node@v1
with:
node-version: 15.x
- -
name: NPM audit name: NPM audit
run: npm audit --omit=dev run: exit "$(npm audit)" # Since node 15.x, it does not fail with error if we don't explicitly exit

View File

@@ -1,9 +1,9 @@
name: integration-tests name: Test
on: on:
push: push:
pull_request: pull_request:
schedule: # To get notified about problems from third party dependencies schedule: # for integration tests
- cron: '0 0 * * 0' # at 00:00 on every Sunday - cron: '0 0 * * 0' # at 00:00 on every Sunday
jobs: jobs:
@@ -16,13 +16,18 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- -
name: Setup node name: Setup node
uses: ./.github/actions/setup-node uses: actions/setup-node@v1
with:
node-version: 15.x
- -
name: Install dependencies name: Install dependencies
uses: ./.github/actions/npm-install-dependencies run: npm ci
-
name: Run unit tests
run: npm run test:unit
- -
name: Run integration tests name: Run integration tests
run: npm run test:integration run: npm run test:integration

View File

@@ -1,26 +0,0 @@
name: e2e-tests
on:
push:
pull_request:
jobs:
run-tests:
strategy:
matrix:
os: [macos, ubuntu, windows]
fail-fast: false # So it still runs on other OSes if one of them fails
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
-
name: Install dependencies
uses: ./.github/actions/npm-install-dependencies
-
name: Run e2e tests
run: npm run test:cy:run

View File

@@ -1,26 +0,0 @@
name: unit-tests
on:
push:
pull_request:
jobs:
run-tests:
strategy:
matrix:
os: [macos, ubuntu, windows]
fail-fast: false # So it still runs on other OSes if one of them fails
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set-up node
uses: ./.github/actions/setup-node
-
name: Install dependencies
uses: ./.github/actions/npm-install-dependencies
-
name: Run unit tests
run: npm run test:unit

7
.gitignore vendored
View File

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

View File

@@ -1,23 +0,0 @@
{
"recommendations": [
// Common
"editorconfig.editorconfig", // Applies .editorconfig to follow project style.
"wengerk.highlight-bad-chars", // Highlights bad chars.
"wayou.vscode-todo-highlight", // Highlights TODO.
"wix.vscode-import-cost", // Shows in KB how much a require include in code.
// Documentation
"davidanson.vscode-markdownlint", // Lints markdown.
// TypeScript / JavaScript
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.
// Vue
"Vue.volar", // Official Vue extensions
"Vue.vscode-typescript-vue-plugin", // Official TypeScript Vue Plugin
// Scripting
"timonwong.shellcheck", // Lints bash files.
"ms-vscode.powershell", // Lints PowerShell files.
"ms-python.python", // Lints Python files.
// Distribution
"ms-azuretools.vscode-docker" // Adds Docker support.
]
}

View File

@@ -1,186 +1,5 @@
# Changelog # Changelog
## 0.12.5 (2023-10-13)
* Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16)
* Add SAST security checks with SECURITY.md #178 | [3e5239f](https://github.com/undergroundwires/privacy.sexy/commit/3e5239f7d35e57749c01adf3dbbcd365aebb39c8)
* Add Scoop download instructions #174 | [cf55ca9](https://github.com/undergroundwires/privacy.sexy/commit/cf55ca9e28b064fa7a516077a9da23e3a8e3f534)
* win: fix and improve temp dir cleanup #176, #89 | [d457504](https://github.com/undergroundwires/privacy.sexy/commit/d45750428cca010daf2721b33a8ae3a01b28813b)
* win, linux: improve VSCode setting robustness #196 | [e8a52f7](https://github.com/undergroundwires/privacy.sexy/commit/e8a52f717dc799b34ceeb1c27c2b8219391dff6a)
* linux: fix obsolete Firefox DPI script #239 | [e5f6edf](https://github.com/undergroundwires/privacy.sexy/commit/e5f6edf405bcec7c29ea4d7932d1910620fa15f8)
* win: add removal of Edge assocations #64 | [888c916](https://github.com/undergroundwires/privacy.sexy/commit/888c9166fc66a2094137fa8be739cc21bafef5f6)
* win: improve Edge & OneDrive shortcut removal #73 | [8501495](https://github.com/undergroundwires/privacy.sexy/commit/8501495c170af61913288a63dbd369db5bbc5003)
* win: relocate and document SecHealthUI #190 | [2862951](https://github.com/undergroundwires/privacy.sexy/commit/286295128d0179358e0c6b7b6415d752175a1aed)
* Add developer toolkit UI component | [2147eae](https://github.com/undergroundwires/privacy.sexy/commit/2147eae687b82d05bc43bb4605d9068f148bb92a)
* win: fix and improve network data usage reset #265 | [5e359c2](https://github.com/undergroundwires/privacy.sexy/commit/5e359c2fb82a08e6acf7159b70ca86a8234b359b)
* win: improve app reversion and docs #260 | [a3f11df](https://github.com/undergroundwires/privacy.sexy/commit/a3f11dff187c821a00910c20dac05e285cda9073)
* Fix working directory in CI/CD web release | [698b570](https://github.com/undergroundwires/privacy.sexy/commit/698b570ee6e300d6703015464f4345b5e706f1cb)
* Implement new UI component for icons #230 | [48730bc](https://github.com/undergroundwires/privacy.sexy/commit/48730bca0506120bca4bf3a23545d59f2b1a9009)
* win: fix and improve AppCompat disabling #255 | [bab6316](https://github.com/undergroundwires/privacy.sexy/commit/bab6316e7625230cf4a4cf67c3aca417347db75c)
* win, linux, mac: fix typos and improve naming | [67c3677](https://github.com/undergroundwires/privacy.sexy/commit/67c3677621b201525a813e8a26f07d607176e89b)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.4...0.12.5)
## 0.12.4 (2023-09-25)
* win: fix Windows spotlight revert, docs, recommend | [659fea7](https://github.com/undergroundwires/privacy.sexy/commit/659fea7afcabcd0ea273cfdcc8c4bae190c126f3)
* win: fix Edge telemetry disabling for v116+ #242 | [6d301f9](https://github.com/undergroundwires/privacy.sexy/commit/6d301f99616ed49975876803d0098eafe4d3cb2e)
* win: fix, improve disabling automatic updates #252 | [6e9b65d](https://github.com/undergroundwires/privacy.sexy/commit/6e9b65d8b1b481c1471dde90876c37838b4ac4e5)
* win: refactor `update.mode` key for VSCode #215 | [c27172c](https://github.com/undergroundwires/privacy.sexy/commit/c27172c32e7c316b7cb0f44cab611eed89ca034e)
* Fix wrong action path in website CI deployment | [a1f2497](https://github.com/undergroundwires/privacy.sexy/commit/a1f24973813ccbdd7e1f06c64e1912a991a6bb64)
* Fix compiler bug with nested optional arguments | [53222fd](https://github.com/undergroundwires/privacy.sexy/commit/53222fd83c2846089746a217482195806f960d18)
* Fix no spacing after lists in documentation text | [f810ed0](https://github.com/undergroundwires/privacy.sexy/commit/f810ed0c147c2a46cae3b70b635ed81128646fff)
* Rewrite tooltip UI for efficiency and Vue 3.0 #230 | [8b930fc](https://github.com/undergroundwires/privacy.sexy/commit/8b930fc57c8ee6691ed6165bcb27d97e64a1a0c0)
* win: fix uninstallation of newer Edge #236 | [60dde11](https://github.com/undergroundwires/privacy.sexy/commit/60dde11311a2409537f5965f370b0daaaec53339)
* win: fix delivery optimization side-effects #173 | [203daeb](https://github.com/undergroundwires/privacy.sexy/commit/203daeb4a2fca0a0295cbc2a736394f9f87725e6)
* win: fix Defender scan artifacts removal #246 | [cb21a97](https://github.com/undergroundwires/privacy.sexy/commit/cb21a970b6b867e1476a5eb8a72b9a7fdd53a744)
* Fix outdated and broken links in README #161 | [0303ef2](https://github.com/undergroundwires/privacy.sexy/commit/0303ef2fd98b36306523e2a0c5f5ae812a4c6c99)
* Fix loss of tree node state when switching views | [8f188ac](https://github.com/undergroundwires/privacy.sexy/commit/8f188acd3c2d93e40c89569c74bc5cff992f0052)
* Fix slow appearance of nodes on tree view | [bd2082e](https://github.com/undergroundwires/privacy.sexy/commit/bd2082e8c574db065bb4462f30ea3ace2cb028cb)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.3...0.12.4)
## 0.12.3 (2023-09-09)
* linux: use user.js over prefs.js for Firefox #232 | [dae6d11](https://github.com/undergroundwires/privacy.sexy/commit/dae6d114daab6857d773071211eb57619b136281)
* win: fix typo in Defender retention script #213 | [35be05d](https://github.com/undergroundwires/privacy.sexy/commit/35be05df2094ea8bba4ee4725e6fa4956a79493d)
* Improve desktop runtime execution tests | [ad0576a](https://github.com/undergroundwires/privacy.sexy/commit/ad0576a752f8fd6ea2f917a59173fe61f9951246)
* Fix Windows artifact naming in desktop packaging | [f4d86fc](https://github.com/undergroundwires/privacy.sexy/commit/f4d86fccfd0e73e94c8c6e400a33514900bc5abe)
* Refactor and improve external URL checks | [19e42c9](https://github.com/undergroundwires/privacy.sexy/commit/19e42c9c52a18c813ded4265e687e01032cdd4c8)
* Fix memory leaks via auto-unsubscribing and DI | [eb096d0](https://github.com/undergroundwires/privacy.sexy/commit/eb096d07e276e1b4c8040220c47f186d02841e14)
* Refactor build configs and improve CI/CD checks | [0a2a1a0](https://github.com/undergroundwires/privacy.sexy/commit/0a2a1a026b0efb29624be82b06536c518c1ea439)
* Introduce retry mechanism for npm install in CI/CD | [4beb1bb](https://github.com/undergroundwires/privacy.sexy/commit/4beb1bb5748a60886210187ca3cdc7f4b41067c0)
* win: fix disable recent apps revert #211, #248 | [4ce327e](https://github.com/undergroundwires/privacy.sexy/commit/4ce327eb6af542ed2916d649553e5e1ba5833882)
* Change license to AGPLv3 | [821cc62](https://github.com/undergroundwires/privacy.sexy/commit/821cc62c4c8347cb76d041f82f574754e4d948c5)
* Introduce new TreeView UI component | [65f121c](https://github.com/undergroundwires/privacy.sexy/commit/65f121c451af87315e1c91df4198562e0445b2c2)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.2...0.12.3)
## 0.12.2 (2023-08-25)
* Add automated checks for desktop app runtime #233 | [04b3133](https://github.com/undergroundwires/privacy.sexy/commit/04b3133500485d0d278a81a177a1677134131405)
* win: fix automatic updates revert #234 | [0873769](https://github.com/undergroundwires/privacy.sexy/commit/08737698c2283bdf535d1611a730031ebfc7c0df)
* Migrate unit/integration tests to Vitest with Vite | [5f11c8d](https://github.com/undergroundwires/privacy.sexy/commit/5f11c8d98f782dd7c77f27649a1685fb7bd06e13)
* Remove Vue ESLint plugin for Vite compatibility | [6e40edd](https://github.com/undergroundwires/privacy.sexy/commit/6e40edd3f8a063c1b7482c27d8368e14c2fbcfbf)
* Migrate web builds from Vue CLI to Vite | [7365905](https://github.com/undergroundwires/privacy.sexy/commit/736590558be51a09435bb87e78b6655e8533bc2e)
* Migrate Cypress (E2E) tests to Vite and TypeScript | [ec98d84](https://github.com/undergroundwires/privacy.sexy/commit/ec98d8417f779fa818ccdda6bb90f521e1738002)
* Migrate to `electron-vite` and `electron-builder` | [75c9b51](https://github.com/undergroundwires/privacy.sexy/commit/75c9b51bf2d1dc7269adfd7b5ed71acfb5031299)
* Fix searching/filtering bugs #235 | [62f8bfa](https://github.com/undergroundwires/privacy.sexy/commit/62f8bfac2f481c93598fe19a51594769f522d684)
* Improve desktop security by isolating Electron | [e9e0001](https://github.com/undergroundwires/privacy.sexy/commit/e9e0001ef845fa6935c59a4e20a89aac9e71756a)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.1...0.12.2)
## 0.12.1 (2023-08-17)
* Transition to eslint-config-airbnb-with-typescript | [ff84f56](https://github.com/undergroundwires/privacy.sexy/commit/ff84f5676e496dd7ec5b3599e34ec9627d181ea2)
* Improve user privacy with secure outbound links | [3a594ac](https://github.com/undergroundwires/privacy.sexy/commit/3a594ac7fd708dc1e98155ffb9b21acd4e1fcf2d)
* Refactor Vue components using Composition API #230 | [1b9be8f](https://github.com/undergroundwires/privacy.sexy/commit/1b9be8fe2d72d8fb5cf1fed6dcc0b9777171aa98)
* Fix failing security tests | [3bc8da4](https://github.com/undergroundwires/privacy.sexy/commit/3bc8da4cbf1e2bd758dc3fffe4b1e62dc3beb7b3)
* Improve Defender scripts #201 | [061afad](https://github.com/undergroundwires/privacy.sexy/commit/061afad9673a41454c2421c318898f2b4f4cf504)
* Fix failing tests due to failed error logging | [986ba07](https://github.com/undergroundwires/privacy.sexy/commit/986ba078a643de6acbee50fff9cf77494ca7ea7f)
* Implement custom lightweight modal #230 | [9e5491f](https://github.com/undergroundwires/privacy.sexy/commit/9e5491fdbf2d9d40d974f5ad0e879a6d5c6d1e55)
* Refactor usage of tooltips for flexibility | [bc91237](https://github.com/undergroundwires/privacy.sexy/commit/bc91237d7c54bdcd15c5c39a55def50d172bb659)
* Fix revert toggle partial rendering | [39e650c](https://github.com/undergroundwires/privacy.sexy/commit/39e650cf110bee6b1b21d9b2902b36b0e2568d54)
* Increase testability through dependency injection | [ae75059](https://github.com/undergroundwires/privacy.sexy/commit/ae75059cc14db41f55dd2056f528442c7d319dd2)
* Refactor filter (search query) event handling | [6a20d80](https://github.com/undergroundwires/privacy.sexy/commit/6a20d804dc365d22c1248d787f9912271f508eeb)
* Migrate to ES6 modules | [a14929a](https://github.com/undergroundwires/privacy.sexy/commit/a14929a13cc6260b514692d9b4f1cdf5fb85d8b2)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.0...0.12.1)
## 0.12.0 (2023-08-03)
* Improve script/category name validation | [b210aad](https://github.com/undergroundwires/privacy.sexy/commit/b210aaddf26629179f77fe19f62f65d8a0ca2b87)
* Improve touch like hover on devices without mouse | [99e24b4](https://github.com/undergroundwires/privacy.sexy/commit/99e24b4134c461c336f6d08f49d193d853325d31)
* Improve click/touch without unintended interaction | [3233d9b](https://github.com/undergroundwires/privacy.sexy/commit/3233d9b8024dd59600edddef6d017e0089f59a9d)
* Align card icons vertically in cards view | [8608072](https://github.com/undergroundwires/privacy.sexy/commit/8608072bfb52d10a843a86d3d89b14e8b9776779)
* Fix broken npm installation and builds | [924b326](https://github.com/undergroundwires/privacy.sexy/commit/924b326244a175428175e0df3a50685ee5ac2ec6)
* Improve documentation support with markdown | [6067bdb](https://github.com/undergroundwires/privacy.sexy/commit/6067bdb24e6729d2249c9685f4f1c514c3167d91)
* win: add more Visual Studio scripts, support 2022 | [df533ad](https://github.com/undergroundwires/privacy.sexy/commit/df533ad3b19cebdf3454895aa2182bd4184e0360)
* win: add script to remove Widgets | [bbc6156](https://github.com/undergroundwires/privacy.sexy/commit/bbc6156281fb3fd4b66c63dec3f765780fafa855)
* Use line endings based on script language #88 | [6b3f465](https://github.com/undergroundwires/privacy.sexy/commit/6b3f4659df0afe1c99a8af6598df44a33c1f863a)
* win: improve OneDrive removal | [58ed7b4](https://github.com/undergroundwires/privacy.sexy/commit/58ed7b456b3cf11774c83c8c1c04db37ef3058c2)
* Use lowercase in script names and search text | [430537f](https://github.com/undergroundwires/privacy.sexy/commit/430537f70411756bbcaae837964c0223f78581e8)
* Improve manual execution instructions | [7d3670c](https://github.com/undergroundwires/privacy.sexy/commit/7d3670c26d0151ddc43303e8ed5e47715f0e0f00)
* Add multiline support for with expression | [e8d06e0](https://github.com/undergroundwires/privacy.sexy/commit/e8d06e0f3e178a69861e0197f9d1cce9af3958f1)
* Break line in inline codes in documentation | [c1c2f29](https://github.com/undergroundwires/privacy.sexy/commit/c1c2f2925fe88ec1f56bf7655b6b9a10aa3ea024)
* win: add script to increase RSA key exchange #165 | [a2e0921](https://github.com/undergroundwires/privacy.sexy/commit/a2e092190d8eb0fc9ceb8533572f04fff52f097b)
* win: add scripts to downloaded file handling #153 | [e7b816d](https://github.com/undergroundwires/privacy.sexy/commit/e7b816d1564afa98c63291f9d7fd6f3fee92f4ec)
* Drop support for dead browsers | [bf0c55f](https://github.com/undergroundwires/privacy.sexy/commit/bf0c55fa60bf2be070678ba27db14baf13fec511)
* Add support for nested templates | [68a5d69](https://github.com/undergroundwires/privacy.sexy/commit/68a5d698a2ce644ce25754016fb9e9bb642e41a7)
* mac: add scripts to configure Parallels Desktop | [64cca1d](https://github.com/undergroundwires/privacy.sexy/commit/64cca1d9b8946b92e21e86deb6db5612570befb1)
* Rework icon with higher quality and new color | [f4a7ca7](https://github.com/undergroundwires/privacy.sexy/commit/f4a7ca76b885b8346d8a9c32e6269eabc2d8139f)
* Relax and improve code validation | [e819993](https://github.com/undergroundwires/privacy.sexy/commit/e8199932b462380741d9f2d8b6b55485ab16af02)
* Add initial Linux support #150 | [c404dfe](https://github.com/undergroundwires/privacy.sexy/commit/c404dfebe2908bb165279f8279f3f5e805b647d7)
* mac: add script to disable personalized ads | [8b374a3](https://github.com/undergroundwires/privacy.sexy/commit/8b374a37b401699d5056bfd6b735b6a26c395ae0)
* Update dependencies and add npm setup script | [5721796](https://github.com/undergroundwires/privacy.sexy/commit/57217963787a8ab0c71d681c6b1673c484c88226)
* Fix macOS desktop build failure in CI | [5901dc5](https://github.com/undergroundwires/privacy.sexy/commit/5901dc5f11dd29be14c2616fc0ceb45196a43224)
* Change subtitle heading to new slogan | [1e80ee1](https://github.com/undergroundwires/privacy.sexy/commit/1e80ee1fb0208d92943619468dc427853cbe8de7)
* win: add new scripts to disable more telemetry | [298b058](https://github.com/undergroundwires/privacy.sexy/commit/298b058e5c89397db6f759b275442ba05499ac8c)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.4...0.12.0)
## 0.11.4 (2022-03-08)
* Improve performance of selecting scripts | [8e96c19](https://github.com/undergroundwires/privacy.sexy/commit/8e96c19126aa4cba6418de5ccaa9e2dcf8faab78)
* Fix reverting of Windows NVIDIA telemetry service | [2354f0b](https://github.com/undergroundwires/privacy.sexy/commit/2354f0ba9fed3aa23569b5ea6391a7119fe1ab53)
* Add AirBnb TypeScript overrides for linting | [834ce8c](https://github.com/undergroundwires/privacy.sexy/commit/834ce8cf9e8e46934dfa604526360870d109765b)
* Transpile dependencies for wider browser support | [0e52a99](https://github.com/undergroundwires/privacy.sexy/commit/0e52a99efa2b02d1aba10885a76e03aa6f9be7f8)
* Add more and unify tests for absent object cases | [44d79e2](https://github.com/undergroundwires/privacy.sexy/commit/44d79e2c9a97639bbd188a8fdfd740f1a5a1d6ee)
* Fix Windows DoSvc not being disabled #115 | [43ce834](https://github.com/undergroundwires/privacy.sexy/commit/43ce834750ddf471636d1ece4324d02357947f9f)
* Move stubs from `./stubs` to `./shared/Stubs` | [803ef2b](https://github.com/undergroundwires/privacy.sexy/commit/803ef2bb3eea68306377e40e326c791402998650)
* Improve documentation for developing | [3c3ec80](https://github.com/undergroundwires/privacy.sexy/commit/3c3ec80525b97e8a24db4c44bbf42a7b4e089056)
* Improve documentation for architecture | [1bcc6c8](https://github.com/undergroundwires/privacy.sexy/commit/1bcc6c8b2b923b4d4b1662f990d86b190ce73342)
* Improve existing documentation | [db47440](https://github.com/undergroundwires/privacy.sexy/commit/db47440d470ea6a6e100b620b10d078c01314992)
* Refactor to remove code coupling with Webpack | [5bbbb9c](https://github.com/undergroundwires/privacy.sexy/commit/5bbbb9cecca0a3828036e7fc34dcd66970ce334a)
* Refactor to remove hardcoding of aliases | [481a02a](https://github.com/undergroundwires/privacy.sexy/commit/481a02afd5190eb77a37fa450e50816b2268e99c)
* Document WpnService breaking on Windows 10 #110 | [3785e41](https://github.com/undergroundwires/privacy.sexy/commit/3785e410db461f667a834e0b388d81e4baa028e4)
* Fix error when reverting Windows Defender setting | [956052c](https://github.com/undergroundwires/privacy.sexy/commit/956052c8fff042812fe84fe4d7fa5c579365ff9b)
* Fix Windows 11 being detected as Windows 10 | [d6bc33e](https://github.com/undergroundwires/privacy.sexy/commit/d6bc33ec865d50efc6b8d4ccc2f789edd874fcee)
* Refactor to use version object #59 | [eeb1d5b](https://github.com/undergroundwires/privacy.sexy/commit/eeb1d5b0c40a55675921af3f67f366b2ff658acf)
* Fix Microsoft Defender alert for uninstaller #114 | [112e79a](https://github.com/undergroundwires/privacy.sexy/commit/112e79a64c6153f4ce3b48c27a09639e7647aebc)
* Add donation information | [05a6a84](https://github.com/undergroundwires/privacy.sexy/commit/05a6a84c3739ec900343591ac1f7a9f310cd73f2)
* Bump node environment to 16.x | [242a497](https://github.com/undergroundwires/privacy.sexy/commit/242a497e7debb351da19b20b63a3554f0cca4b5c)
* Bump dependencies to latest | [efd63ff](https://github.com/undergroundwires/privacy.sexy/commit/efd63ff85dea4c9a9c033c54bc1be378742de351)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.3...0.11.4)
## 0.11.3 (2022-01-05)
* Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd)
* Fix OS desktop detection tests and edge cases | [a8358b8](https://github.com/undergroundwires/privacy.sexy/commit/a8358b8e7a93214f3d22a4488007ded5f623d845)
* Fix clearing Windows product key showing dialog | [9b6636e](https://github.com/undergroundwires/privacy.sexy/commit/9b6636e21a922a4750dc19f4854f8ae679187926)
* Document and unrecommend Cloud Experience Host | [9b5e0b0](https://github.com/undergroundwires/privacy.sexy/commit/9b5e0b0591fee56af52d83334a1f19180a49516f)
* Add initial e2e testing with cypress | [ddd2e70](https://github.com/undergroundwires/privacy.sexy/commit/ddd2e704dbd361cbd219f3dfe644b983ad254095)
* Restructure pipelines and badges | [5a2c263](https://github.com/undergroundwires/privacy.sexy/commit/5a2c263af35b8785e75ead6c43c3f17186dc15c8)
* Fix failing of functions without revert code | [87de017](https://github.com/undergroundwires/privacy.sexy/commit/87de017afd6e08acbd2deea150c6af9c7ee778fc)
* Fix typos in privacy modal #109 | [a1871a2](https://github.com/undergroundwires/privacy.sexy/commit/a1871a2982c9e3192193f836b97b1a6ccda5a2ab)
* Refactor to add readonly interfaces | [c3c5b89](https://github.com/undergroundwires/privacy.sexy/commit/c3c5b897f308f613c252182a02cdd4cfa7150fa3)
* Document and unrecommend AAD app removal #24, #54 | [455084c](https://github.com/undergroundwires/privacy.sexy/commit/455084c17b32d11d046515e8dc1447adf4bea4c3)
* Migrate from TSLint to ESLint | [61b475f](https://github.com/undergroundwires/privacy.sexy/commit/61b475fa8de433cdada2efa7eac197683aacd956)
* Add build checks and improve existing CI/CD checks | [17298f0](https://github.com/undergroundwires/privacy.sexy/commit/17298f0b2c51cb9becc0eb2ffe0d93d6a4c503a6)
* Upgrade to Vue CLI 5 (and webpack 5) | [96265b7](https://github.com/undergroundwires/privacy.sexy/commit/96265b75deafb85978b16460138fb4a814c07cfe)
* Refactor code to comply with ESLint rules | [5b1fbe1](https://github.com/undergroundwires/privacy.sexy/commit/5b1fbe1e2fb1354a5f060f8c8e3794ce756e16a7)
* Fix mutated line endings on Windows | [bd23faa](https://github.com/undergroundwires/privacy.sexy/commit/bd23faa28f6d781581a33d5b780f4b33f7e2cd8b)
* Refactor to improve iterations | [31f7091](https://github.com/undergroundwires/privacy.sexy/commit/31f70913a2f30baf5a9d6690f192e6a63da50114)
* win: unrecommend and document Live ID service #100 | [d11a674](https://github.com/undergroundwires/privacy.sexy/commit/d11a674a3c4ad8f4972a870c2f0977ac53297273)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.2...0.11.3)
## 0.11.2 (2021-12-03)
* Fix Windows TrustedInstaller session errors | [20a0071](https://github.com/undergroundwires/privacy.sexy/commit/20a0071c0d3d769a8f31218abdbfc4cafa25c6ff)
* Improve tests for `UserSelection` | [2f90cac](https://github.com/undergroundwires/privacy.sexy/commit/2f90cac52ab9e57615aeec41f9daa842bce770a5)
* Fix disabling/enabling Defender on Windows #104 | [2e08293](https://github.com/undergroundwires/privacy.sexy/commit/2e082932c952b0849ab2b8709ff0c75293b88e95)
* Refactor Saas naming, structure and modules | [bf83c58](https://github.com/undergroundwires/privacy.sexy/commit/bf83c58982ffa178facc6d35e50c7f1eac7ff236)
* Fix Defender features errors in Windows #104 | [d7761ab](https://github.com/undergroundwires/privacy.sexy/commit/d7761ab30e7f1e10a2919c196804d67511d6163a)
* Fix unintendedly inlined Windows scripts | [f2d9881](https://github.com/undergroundwires/privacy.sexy/commit/f2d988138257ff184884e4adc83c39e3bc247e9b)
* Fix Defender error due to non-english Windows #104 | [7c02ffb](https://github.com/undergroundwires/privacy.sexy/commit/7c02ffb6c95382b94f0b05e6f259cc418ec91c93)
* Improve and unify disabling of Windows services | [70cdf38](https://github.com/undergroundwires/privacy.sexy/commit/70cdf3865a0de3214fc9e26fbdada4b0cb413c46)
* Improve Windows defender docs and errors #104 | [d2518b1](https://github.com/undergroundwires/privacy.sexy/commit/d2518b11a7774ec58b9b46a691e2f013855bf0f9)
* Unrecommend and complete Windows Push Notif. #101 | [c65209e](https://github.com/undergroundwires/privacy.sexy/commit/c65209e6a99230f15ace8955e8d5a6f3333d146b)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.1...0.11.2)
## 0.11.1 (2021-11-04) ## 0.11.1 (2021-11-04)
* Update dependencies | [64631a4](https://github.com/undergroundwires/privacy.sexy/commit/64631a4552fad7f7b06286aba8d3ca2d731f9342) * Update dependencies | [64631a4](https://github.com/undergroundwires/privacy.sexy/commit/64631a4552fad7f7b06286aba8d3ca2d731f9342)

View File

@@ -1,51 +1,34 @@
# Contributing # Contributing
Love your input! Contributing to this project should be as easy and transparent as possible, whether it's: - Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
- Reporting a bug
- reporting a bug, - Discussing the current state of the code
- discussing the current state of the code, - Submitting a fix
- submitting a fix, - Proposing new features
- proposing new features, - Becoming a maintainer
- or becoming a maintainer.
As a small open source project with small community, it can sometimes take a long time to address the issues so please be patient.
## Pull request process ## Pull request process
Your pull requests are actively welcomed. We collaborate using [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow). - [GitHub flow](https://guides.github.com/introduction/flow/index.html) with [GitOps](./img/architecture/gitops.png) is used
- Your pull requests are actively welcomed.
The steps: - The steps:
1. Fork the repo and create your branch from master.
1. Fork the repo and create your branch from master. 2. If you've added code that should be tested, add tests.
2. If you've added code that requires testing, add tests. See [tests.md](./docs/tests.md). 3. If you've changed APIs, update the documentation.
3. If you've done a major change, update the documentation. See [docs/](./docs/). 4. Ensure the test suite passes.
4. Ensure the test suite passes. See [development.md | Testing](./docs/development.md#testing) for commands. 5. Make sure your code lints.
5. Make sure your code lints.See [development.md | Linting](./docs/development.md#linting) for commands. 6. Issue that pull request!
6. Issue that pull request! - 🙏 DO
- Document your changes in the pull request
**🙏 DO:** - ❗ DON'T
- Do not update the versions, current version is only [set by the maintainer](./img/architecture/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
- Document why (what you're trying to solve) rather than what in the pull request.
**❗ DON'T:**
- Do not update the versions, current version is [set by the maintainer](./docs/ci-cd.md#gitops) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
Automated pipelines will run to control your PR and they will publish your code once the maintainer merges your PR.
📖 You can read more in [ci-cd.md](./docs/ci-cd.md).
## Extend scripts
Here's quick information for you who want to add more scripts.
You have two alternatives:
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) and ask for someone else to add the script for you.
2. Or send a PR yourself. This would make it faster to get your code into the project. You need to add scripts to related OS in [collections](src/application/collections/) folder. Then you'd sent a pull request, see [pull request process](#pull-request-process).
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md).
- 📖 If you wish to use templates, use [templating.md](./docs/templating.md).
## License ## License
By contributing, you agree that your [GNU General Public License v3.0](./LICENSE) will be the license for your contributions. By contributing, you agree that your contributions will be licensed under its [GNU General Public License v3.0](./LICENSE).
## Read more
- See [tests](./docs/tests.md) for testing
- See [extend script](./README.md#extend-scripts) for quick steps to extend scripts
- See [architecture overview](./README.md#architecture-overview) to deep dive into privacy.sexy codebase

View File

@@ -1,16 +1,13 @@
# Build # Build
FROM node:lts-alpine AS build-stage FROM node:lts-alpine as build-stage
WORKDIR /app WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . . COPY . .
RUN npm run install-deps RUN npm run build
RUN npm run build \
&& npm run check:verify-build-artifacts -- --web
RUN mkdir /dist \
&& dist_directory=$(node 'scripts/print-dist-dir.js' --web) \
&& cp -a "${dist_directory}/." '/dist'
# Production stage # Production stage
FROM nginx:stable-alpine AS production-stage FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /dist /usr/share/nginx/html COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

141
LICENSE
View File

@@ -1,5 +1,5 @@
GNU AFFERO GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 3, 19 November 2007 Version 3, 29 June 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,15 +7,17 @@
Preamble Preamble
The GNU Affero General Public License is a free, copyleft license for The GNU General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure software and other kinds of works.
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,
our General Public Licenses are intended to guarantee your freedom to the GNU General Public License is 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. software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
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
@@ -24,34 +26,44 @@ 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.
Developers that use our General Public Licenses protect your rights To protect your rights, we need to prevent others from denying you
with two steps: (1) assert copyright on the software, and (2) offer these rights or asking you to surrender the rights. Therefore, you have
you this License which gives you legal permission to copy, distribute certain responsibilities if you distribute copies of the software, or if
and/or modify the software. you modify it: responsibilities to respect the freedom of others.
A secondary benefit of defending all users' freedom is that For example, if you distribute copies of such a program, whether
improvements made in alternate versions of the program, if they gratis or for a fee, you must pass on to the recipients the same
receive widespread use, become available for other developers to freedoms that you received. You must make sure that they, too, receive
incorporate. Many developers of free software are heartened and or can get the source code. And you must show them these terms so they
encouraged by the resulting cooperation. However, in the case of know their rights.
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.
The GNU Affero General Public License is designed specifically to Developers that use the GNU GPL protect your rights with two steps:
ensure that, in such cases, the modified source code becomes available (1) assert copyright on the software, and (2) offer you this License
to the community. It requires the operator of a network server to giving you legal permission to copy, distribute and/or modify it.
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.
An older license, called the Affero General Public License and For the developers' and authors' protection, the GPL clearly explains
published by Affero, was designed to accomplish similar goals. This is that there is no warranty for this free software. For both users' and
a different license, not a version of the Affero GPL, but Affero has authors' sake, the GPL requires that modified versions be marked as
released a new version of the Affero GPL which permits relicensing under changed, so that their problems will not be attributed erroneously to
this license. authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and The precise terms and conditions for copying, distribution and
modification follow. modification follow.
@@ -60,7 +72,7 @@ modification follow.
0. Definitions. 0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License. "This License" refers to version 3 of the GNU 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.
@@ -537,45 +549,35 @@ 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. Remote Network Interaction; Use with the GNU General Public License. 13. Use with the GNU Affero 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 General Public License into a single under version 3 of the GNU Affero 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 work with which it is combined will remain governed by version but the special requirements of the GNU Affero General Public License,
3 of the GNU General Public License. section 13, concerning interaction through a network will apply to the
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 Affero General Public License from time to time. Such new versions the GNU General Public License from time to time. Such new versions will
will be similar in spirit to the present version, but may differ in detail to 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 Affero General Program specifies that a certain numbered version of the GNU 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 Affero General Public License, you may choose any version ever published GNU 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 Affero General Public License can be used, that proxy's versions of the GNU 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.
@@ -633,29 +635,40 @@ 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 Affero General Public License as published by it under the terms of the GNU 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 Affero General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU 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 your software can interact with users remotely through a computer If the program does terminal interaction, make it output a short
network, you should also make sure that it provides a way for users to notice like this when it starts in an interactive mode:
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive <program> Copyright (C) <year> <name of author>
of the code. There are many ways you could offer source, and different This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
solutions will be better for different programs; see section 13 for the This is free software, and you are welcome to redistribute it
specific requirements. under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
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 AGPL, see For more information on this, and how to apply and follow the GNU GPL, 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>.

217
README.md
View File

@@ -1,173 +1,88 @@
# privacy.sexy — Now you have the choice # privacy.sexy
> Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆 > Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
<!-- markdownlint-disable MD033 --> [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](./CONTRIBUTING.md)
<p align="center"> [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
<a href="https://undergroundwires.dev/donate?project=privacy.sexy" target="_blank" rel="noopener noreferrer"> [![Maintainability](https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability)](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
<img [![Tests status](https://github.com/undergroundwires/privacy.sexy/workflows/Test/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
alt="donation badge" [![Quality checks status](https://github.com/undergroundwires/privacy.sexy/workflows/Quality%20checks/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
src="https://undergroundwires.dev/img/badges/donate/flat.svg" [![Security checks status](https://github.com/undergroundwires/privacy.sexy/workflows/Security%20checks/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
/> [![Bump & release status](https://github.com/undergroundwires/privacy.sexy/workflows/Bump%20&%20release/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
</a> [![Deploy status](https://github.com/undergroundwires/privacy.sexy/workflows/Build%20&%20deploy/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md" target="_blank" rel="noopener noreferrer"> [![Auto-versioned by bump-everywhere](https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true)](https://github.com/undergroundwires/bump-everywhere)
<img
alt="contributions are welcome"
src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"
/>
</a>
<a href="https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability" target="_blank" rel="noopener noreferrer">
<img
alt="Maintainability"
src="https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability"
/>
</a>
<!-- Tests -->
<br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.unit.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Unit tests status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/unit-tests/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.integration.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Integration tests status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/integration-tests/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.e2e.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="E2E tests status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/e2e-tests/badge.svg"
/>
</a>
<!-- Security checks -->
<br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.sast.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Status of dependency security checks"
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.security.sast/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.dependencies.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Status of Static Analysis Security Testing (SAST)"
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.security.dependencies/badge.svg"
/>
</a>
<!-- Checks -->
<br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Quality checks status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Status of build checks"
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.build/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.desktop-runtime-errors.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Status of runtime error checks for the desktop application"
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.desktop-runtime-errors/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.scripts.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Status of script checks"
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.scripts/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.external-urls.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Status of external URL checks"
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.external-urls/badge.svg"
/>
</a>
<!-- Release -->
<br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Git release status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-git/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.site.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Site release status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-site/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.desktop.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Desktop application release status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-desktop/badge.svg"
/>
</a>
<!-- Others -->
<br />
<a href="https://github.com/undergroundwires/bump-everywhere" target="_blank" rel="noopener noreferrer">
<img
alt="Auto-versioned by bump-everywhere"
src="https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true"
/>
</a>
</p>
<!-- markdownlint-restore -->
## Get started ## Get started
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy). - Online version at [https://privacy.sexy](https://privacy.sexy)
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-Setup-0.12.5.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-0.12.5.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-0.12.5.AppImage). For more options, see [here](#additional-install-options). - 💡 No need to run any compiled software on your computer.
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-Setup-0.11.1.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-0.11.1.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-0.11.1.AppImage).
- 💡 Single click to execute your script.
- ❗ Come back regularly to apply latest version for stronger privacy and security.
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly. [![privacy.sexy application](img/screenshot.png?raw=true)](https://privacy.sexy)
💡 You should apply your configuration from time to time (more than once). It would strengthen your privacy and security control because privacy.sexy and its scripts get better and stronger in every new version. ## Why
[![privacy.sexy application](img/screenshot.png?raw=true )](https://privacy.sexy) - Rich tweak pool to harden security & privacy of the OS and other software on it
- Free (both free as in beer and free as in speech)
- No need to run any compiled software that has access to your system, just run the generated scripts
- Have full visibility into what the tweaks do as you enable them
- Ability to revert (undo) applied scripts
- Everything is transparent: both application and its infrastructure are open-source and automated
- Easily extendable with [own powerful templating language](./docs/templating.md)
- Each script is independently executable without cross-dependencies
## Features ## Extend scripts
- **Rich**: Hundreds of scripts that aims to give you control of your data. - You can either [create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose)
- **Free**: Both free as in "beer" and free as in "speech". - Or send a PR:
- **Transparent**. Have full visibility into what the tweaks do as you enable them. 1. Fork the repository
- **Reversible**. Revert if something feels wrong. 2. Add more scripts in respective script collection in [collections](src/application/collections/) folder.
- **Accessible**. No need to run any compiled software on your computer with web version. - 📖 If you're unsure about the syntax you can refer to the [collection files | documentation](docs/collection-files.md).
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere). - 🙏 For any new script, please add `revertCode` and `docs` values if possible.
- **Tested**. A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features. 3. Send a pull request 👌
- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md).
- **Portable and simple**. Every script is independently executable without cross-dependencies.
## Support ## Commands
**Sponsor 💕**. Consider sponsoring on [GitHub Sponsors](https://github.com/sponsors/undergroundwires), or you can donate using [other ways such as crypto or a coffee](https://undergroundwires.dev/donate). - Project setup: `npm install`
- Testing
- Run unit tests: `npm run test:unit`
- Run integration tests: `npm run test:integration`
- Lint: `npm run lint`
- **Desktop app**
- Development: `npm run electron:serve`
- Production: `npm run electron:build` to build an executable
- **Webpage**
- Development: `npm run serve` to compile & hot-reload for development.
- Production: `npm run build` to prepare files for distribution.
- Or run using Docker:
1. Build: `docker build -t undergroundwires/privacy.sexy:0.11.1 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.1 undergroundwires/privacy.sexy:0.11.1`
**Star 🤩**. Feel free to give it a star ⭐ . ## Architecture overview
**Contribute 👷**. Contributions of any type are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) as the starting point. It includes useful information like [how to add new scripts](./CONTRIBUTING.md#extend-scripts). ### Application
## Additional Install Options - Powered by **TypeScript**, **Vue.js** and **Electron** 💪
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
- Application uses highly decoupled models & services in different DDD layers.
- 📖 Read more on • [Presentation](./docs/presentation.md) • [Application](./docs/application.md)
- Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions. ![DDD + vue.js](img/architecture/app-ddd.png)
- Using [Scoop](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) package manager on Windows:
```powershell ### AWS Infrastructure
scoop bucket add extras
scoop install privacy.sexy
```
## Development [![AWS solution](img/architecture/aws-solution.png)](https://github.com/undergroundwires/aws-static-site-with-cd)
Refer to [development.md](./docs/development.md) for Docker usage and reading more about setting up your development environment. - It uses infrastructure from the following repository: [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd)
- Runs on AWS 100% serverless and automatically provisioned using [GitHub Actions](.github/workflows/).
- Maximum security & automation and minimum AWS costs are the highest priorities of the design.
Check [architecture.md](./docs/architecture.md) for an overview of design and how different parts and layers work together. You can refer to [application.md](./docs/application.md) for a closer look at application layer codebase and [presentation.md](./docs/presentation.md) for code related to GUI layer. [collection-files.md](./docs/collection-files.md) explains the YAML files that are the core of the application and [templating.md](./docs/templating.md) documents how to use templating language in those files. In [ci-cd.md](./docs/ci-cd.md), you can read more about the pipelines that automates maintenance tasks and ensures you get what see. #### GitOps: CI/CD to AWS
[docs/](./docs/) folder includes all other documentation. - CI/CD is fully automated for this repo using different GIT events & GitHub actions.
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
- Everything that's merged in the master goes directly to production.
## Security [![CI/CD to AWS with GitHub Actions](img/architecture/gitops.png)](.github/workflows/)
Security is a top priority at privacy.sexy. An extensive commitment to security verification ensures this priority. For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md).

View File

@@ -1,31 +0,0 @@
# Security Policy
privacy.sexy takes security seriously. Commitment is made to address all security issues with urgency. Responsible reporting of any discovered vulnerabilities in the project is highly encouraged.
## Reporting a Vulnerability
Efforts to responsibly disclose findings are greatly appreciated. To report a security vulnerability, follow these steps:
- For general vulnerabilities, [open an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) using the bug report template.
- For sensitive matters, [contact the developer directly](https://undergroundwires.dev).
## Security Report Handling
Upon receipt of a security report, the following actions will be taken:
- The report will be confirmed, identifying the affected components.
- The impact and severity of the issue will be assessed.
- Work on a fix and plan a release to address the vulnerability will be initiated.
- The reporter will be kept updated about the progress.
## Testing
Regular and extensive testing is conducted to ensure robust security in the project. Information about testing practices can be found in the [Testing Documentation](./docs/tests.md).
## Support
For additional assistance or any unanswered questions, [submit a GitHub issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose). Security concerns are a priority, and necessary support to address them is assured.
---
Active contribution to the safety and security of privacy.sexy is thanked. This collaborative effort keeps the project resilient and trustworthy for all.

5
babel.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@@ -1,5 +1,5 @@
# build # build
This folder contains files that are used by Electron to serve the desktop version. - These are the file that are used by electron.
- Logos are created by from the [PNG icon](./../public/icon.png)
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md). - by running `npx electron-icon-builder --input=./public/icon.png --output=build --flatten`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 963 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 353 KiB

View File

@@ -1,18 +0,0 @@
import { defineConfig } from 'cypress';
import ViteConfig from './vite.config';
const CYPRESS_BASE_DIR = 'tests/e2e/';
export default defineConfig({
fixturesFolder: `${CYPRESS_BASE_DIR}/fixtures`,
screenshotsFolder: `${CYPRESS_BASE_DIR}/screenshots`,
video: true,
videosFolder: `${CYPRESS_BASE_DIR}/videos`,
e2e: {
baseUrl: `http://localhost:${ViteConfig.server.port}/`,
specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
},
});

View File

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

View File

@@ -1,45 +1,44 @@
# Application # Application
Application layer is mainly responsible for: - It's mainly responsible for
- creating and event based [application state](#application-state)
- creating an event-based and mutable [application state](#application-state), - [parsing](#parsing) and [compiling](#compiling) [application data](#application-data)
- [parsing and compiling](#parsing-and-compiling) the [application data](#application-data). - Consumed by [presentation layer](./presentation.md)
📖 Refer to [architecture.md | Layered Application](./architecture.md#layered-application) to read more about the layered architecture.
## Structure ## Structure
Application layer code exists in [`/src/application`](./../src/application/) and includes following structure: - [`/src/` **`application/`**](./../src/application/): Contains all application related code.
- [**`collections/`**](./../src/application/collections/): Holds [collection files](./collection-files.md)
- [**`collections/`**](./../src/application/collections/): Holds [collection files](./collection-files.md). - [**`Common/`**](./../src/application/Common/): Contains common functionality that is shared in application layer.
- [**`Common/`**](./../src/application/Common/): Contains common functionality in application layer. - `..`: other classes are categorized using folders-by-feature structure
- `...`: rest of the application layer source code organized using folders-by-feature structure.
## Application state ## Application state
It uses [state pattern](https://en.wikipedia.org/wiki/State_pattern) with context and state objects. [`ApplicationContext.ts`](./../src/application/Context/ApplicationContext.ts) the "Context" of state pattern provides an instance of [`CategoryCollectionState.ts`](./../src/application/Context/State/CategoryCollectionState.ts) (the "State" of the state pattern) for every supported collection. - [ApplicationContext.ts](./../src/application/Context/ApplicationContext.ts) holds the [CategoryCollectionState](./../src/application/Context/State/CategoryCollectionState.ts) for each OS
- Uses [state pattern](https://en.wikipedia.org/wiki/State_pattern)
Presentation layer uses a singleton (same instance of) [`ApplicationContext.ts`](./../src/application/Context/ApplicationContext.ts) throughout the application to ensure consistent state. - Same instance is shared throughout the application to ensure consistent state
- 📖 See [Application State | Presentation layer](./presentation.md#application-state) to read more about how the state should be managed by the presentation layer.
📖 Refer to [architecture.md | Application State](./architecture.md#application-state) to get an overview of event handling and [presentation.md | Application State](./presentation.md#application-state) for deeper look into how the presentation layer manages state. - 📖 See [ApplicationContext.ts](./../src/application/Context/ApplicationContext.ts) to start diving into the state code.
## Application data ## Application data
Application data is collection files using YAML. You can refer to [collection-files.md](./collection-files.md) to read more about the scheme and structure of application data files. You can also check the source code [collection yaml files](./../src/application/collections/) to directly see the application data using that scheme. - Compiled to [`Application`](./../src/domain/Application.ts) domain object.
- The scripts are defined and controlled in different data files per OS
- Enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) and easier contributions
- Application data is defined in collection files and
- 📖 See [Application data | Presentation layer](./presentation.md#application-data) to read how the application data is read by the presentation layer.
- 📖 See [collection files documentation](./collection-files.md) to read more about how the data files are structured/defined and see [collection yaml files](./../src/application/collections/) to directly check the code.
Application layer [parses and compiles](#parsing-and-compiling) application data into [`Application`](./../src/domain/Application.ts)). Once parsed, application layer provides the necessary functionality to presentation layer based on the application data. You can read more about how presentation layer consumes the application data in [presentation.md | Application Data](./presentation.md#application-data). ## Parsing
Application layer enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) by leveraging the data to the rest of the source code. It makes it easy for community to contribute on the project by using a declarative language used in collection files. - Application data is parsed to domain object [`Application.ts`](./../src/domain/Application.ts)
- Steps
1. (Compile time) Load application data from [collection yaml files](./../src/application/collections/) using webpack loader
2. (Runtime) Parse and compile application and make it available to presentation layer by [`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)
### Parsing and compiling ### Compiling
Application layer parses the application data to compile the domain object [`Application.ts`](./../src/domain/Application.ts). - Parsing the application files includes compiling scripts using [collection file defined functions](./collection-files.md#function)
- To extend the syntax:
The build tool loads (or injects) application data ([collection yaml files](./../src/application/collections/)) into the application layer in compile time. Application layer ([`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)) parses and compiles this data in runtime. 1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts)
Application layer compiles templating syntax during parsing to create the end scripts. You can read more about templating syntax in [templating.md](./templating.md) and how application data uses them through functions in [collection-files.md | Function](./collection-files.md#function).
The steps to extend the templating syntax:
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).

View File

@@ -1,80 +0,0 @@
# Architecture overview
This repository consists of:
- A [layered application](#layered-application).
- [AWS infrastructure](#aws-infrastructure) as code and instructions to host the website.
- [GitOps](#gitops) practices for development, maintenance and deployment.
## Layered application
Application is
- powered by **TypeScript**, **Vue.js** and **Electron** 💪,
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
Application uses highly decoupled models & services in different DDD layers:
**Application layer** (see [application.md](./application.md)):
- Coordinates application activities and consumes the domain layer.
**Presentation layer** (see [presentation.md](./presentation.md)):
- Handles UI/UX, consumes both the application and domain layers.
- May communicate directly with the infrastructure layer for technical needs, but avoids domain logic.
**Domain layer**:
- Serves as the system's core and central truth.
- Facilitates communication between the application and presentation layers through the domain model.
**Infrastructure layer**:
- Manages technical implementations without dependencies on other layers or domain knowledge.
![DDD + vue.js](./../img/architecture/app-ddd.png)
### Application state
State handling uses an event-driven subscription model to signal state changes and special functions to register changes. It does not depend on third party packages.
The presentation layer can read and modify state through the context. State changes trigger events that components can subscribe to for reactivity.
Each layer treat application layer differently.
![State](./../img/architecture/app-state.png)
*[Presentation layer](./presentation.md)*:
- Each component holds their own state about presentation-related data.
- Components register shared state changes into application state using functions.
- Components listen to shared state changes using event subscriptions.
- 📖 Read more: [presentation.md | Application state](./presentation.md#application-state).
*[Application layer](./application.md)*:
- Stores the application-specific state.
- The state it exposed for read with getter functions and set using setter functions, setter functions also fire application events that allows other parts of application and the view in presentation layer to react.
- So state is mutable, and fires related events when mutated.
- 📖 Read more: [application.md | Application state](./application.md#application-state).
It's comparable with `flux`, `vuex`, and `pinia`. A difference is that mutable application layer state in privacy.sexy is mutable and lies in single "store" that holds app state and logic. The "actions" mutate the state directly which in turns act as dispatcher to notify its own event subscriptions (callbacks).
## AWS infrastructure
The web-site runs on serverless AWS infrastructure. Infrastructure is open-source and deployed as code. [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd) project includes the source code.
[![AWS solution](../img/architecture/aws-solution.png)](https://github.com/undergroundwires/aws-static-site-with-cd)
The design priorities highest security then minimizing cloud infrastructure costs.
This project includes [GitHub Actions](../.github/workflows/) to automatically provision the infrastructure with zero-touch and without any "hidden" steps, ensuring everything is open-source and transparent. Git repositories includes all necessary instructions and automation with [GitOps](#gitops) practices.
## GitOps
CI/CD pipelines automate operational tasks based on different Git events. [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) enables this automation.
📖 Read more in [`ci-cd.md`](./ci-cd.md#gitops).
[![CI/CD using GitHub Actions](../img/architecture/gitops.png)](../.github/workflows/)

View File

@@ -1,45 +0,0 @@
# CI/CD overview
## GitOps
CI/CD is fully automated using different Git events and GitHub actions. This repository uses [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) to automate versioning, tagging, creation of `CHANGELOG.md` and GitHub releases. A dedicated workflow [release.desktop.yaml](./../.github/workflows/release.desktop.yaml) creates desktop installers and executables and attaches them into GitHub releases.
Everything that's merged in the master goes directly to production.
[![CI/CD using GitHub Actions](./../img/architecture/gitops.png)](../.github/workflows/)
## Pipeline files
privacy.sexy uses [GitHub actions](https://github.com/features/actions) to define and run pipelines as code.
GitHub workflows i.e. pipelines exist in [`/.github/workflows/`](./../.github/workflows/) folder without any subfolders due to GitHub actions requirements [1] .
Local GitHub actions are defined in [`/.github/actions/`](./../.github/actions/) and used to reuse same workflow steps.
## Pipeline types
We categorize pipelines into different categories. We use these names in convention when naming files and actions, see [naming conventions](#naming-conventions).
The categories consist of:
- `tests`: Different types of tests to verify functionality.
- `checks`: Other controls such as vulnerability scans or styling checks.
- `release`: Pipelines used for release of deployment such as building and testing.
## Naming conventions
Convention for naming pipeline files: **`<type>.<name>.yaml`**.
**`type`**:
- Sub-folders do not work for GitHub workflows [1] so we use `<type>.` prefix to organize them.
- See also [pipeline types](#pipeline-types) for list of all usable types.
**`name`**:
- We name workflows using kebab-case.
- E.g. file name `tests.unit.yaml`, pipeline file should set the naem as: `name: unit-tests`.
- Kebab-case allows to have better URL references to them.
- [README.md](./../README.md) uses URL references to show status badges for actions.
[1]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows

View File

@@ -2,8 +2,8 @@
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/) - privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
- 💡 Best practices - 💡 Best practices
- If you repeat yourself, try to utilize [YAML-defined functions](#function) - If you repeat yourself, try to utilize [YAML-defined functions](#Function)
- Always try to add documentation and a way to revert a tweak in [scripts](#script) - Always try to add documentation and a way to revert a tweak in [scripts](#Script)
- 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts) - 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
## Objects ## Objects
@@ -13,19 +13,19 @@
- A collection simply defines: - A collection simply defines:
- different categories and their scripts in a tree structure - different categories and their scripts in a tree structure
- OS specific details - OS specific details
- Also allows defining common [function](#function)s to be used throughout the collection if you'd like different scripts to share same code. - Also allows defining common [function](#Function)s to be used throughout the collection if you'd like different scripts to share same code.
#### `Collection` syntax #### `Collection` syntax
- `os:` *`string`* (**required**) - `os:` *`string`* (**required**)
- Operating system that the [Collection](#collection) is written for. - Operating system that the [Collection](#collection) is written for.
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values. - 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
- `actions: [` ***[`Category`](#category)*** `, ... ]` **(required)** - `actions: [` ***[`Category`](#Category)*** `, ... ]` **(required)**
- Each [category](#category) is rendered as different cards in card presentation. - Each [category](#category) is rendered as different cards in card presentation.
- ❗ A [Collection](#collection) must consist of at least one category. - ❗ A [Collection](#collection) must consist of at least one category.
- `functions: [` ***[`Function`](#function)*** `, ... ]` - `functions: [` ***[`Function`](#Function)*** `, ... ]`
- Functions are optionally defined to re-use the same code throughout different scripts. - Functions are optionally defined to re-use the same code throughout different scripts.
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)** - `scripting:` ***[`ScriptingDefinition`](#ScriptingDefinition)*** **(required)**
- Defines the scripting language that the code of other action uses. - Defines the scripting language that the code of other action uses.
### `Category` ### `Category`
@@ -38,12 +38,9 @@
- `category:` *`string`* (**required**) - `category:` *`string`* (**required**)
- Name of the category - Name of the category
- ❗ Must be unique throughout the [Collection](#collection) - ❗ Must be unique throughout the [Collection](#collection)
- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#script) `, ... ]` (**required**) - `children: [` ***[`Category`](#Category)*** `|` [***`Script`***](#Script) `, ... ]` (**required**)
- ❗ Category must consist of at least one subcategory or script. - ❗ Category must consist of at least one subcategory or script.
- Children can be combination of scripts and subcategories. - Children can be combination of scripts and subcategories.
- `docs`: *`string`* | `[`*`string`*`, ... ]`
- Documentation pieces related to the category.
- Rendered as markdown.
### `Script` ### `Script`
@@ -70,12 +67,12 @@
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1` - E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0` - then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
- ❗ Do not define if `call` is defined. - ❗ Do not define if `call` is defined.
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**) - `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
- A shared function or sequence of functions to call (called in order) - A shared function or sequence of functions to call (called in order)
- ❗ If not defined `code` must be defined - ❗ If not defined `code` must be defined
- `docs`: *`string`* | `[`*`string`*`, ... ]` - `docs`: *`string`* | `[`*`string`*`, ... ]`
- Documentation pieces related to the script. - Single documentation URL or list of URLs for those who wants to learn more about the script
- Rendered as markdown. - E.g. `https://docs.microsoft.com/en-us/windows-server/`
- `recommend`: `"standard"` | `"strict"` | `undefined` (default) - `recommend`: `"standard"` | `"strict"` | `undefined` (default)
- If not defined then the script will not be recommended - If not defined then the script will not be recommended
- If defined it can be either - If defined it can be either
@@ -123,7 +120,7 @@
- Convention is to use camelCase, and be verbs. - Convention is to use camelCase, and be verbs.
- E.g. `uninstallStoreApp` - E.g. `uninstallStoreApp`
- ❗ Function names must be unique - ❗ Function names must be unique
- `parameters`: `[` ***[`FunctionParameter`](#functionparameter)*** `, ... ]` - `parameters`: `[` ***[`FunctionParameter`](#FunctionParameter)*** `, ... ]`
- List of parameters that function code refers to. - List of parameters that function code refers to.
- ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions) - ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
`code`: *`string`* (**required** if `call` is undefined) `code`: *`string`* (**required** if `call` is undefined)
@@ -136,7 +133,7 @@
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1` - E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0` - then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in code - 💡 [Expressions (templating)](./templating.md#expressions) can be used in code
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**) - `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
- A shared function or sequence of functions to call (called in order) - A shared function or sequence of functions to call (called in order)
- The parameter values that are sent can use [expressions (templating)](./templating.md#expressions) - The parameter values that are sent can use [expressions (templating)](./templating.md#expressions)
- ❗ If not defined `code` must be defined - ❗ If not defined `code` must be defined
@@ -144,7 +141,7 @@
### `FunctionParameter` ### `FunctionParameter`
- Defines a parameter that function requires optionally or mandatory. - Defines a parameter that function requires optionally or mandatory.
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#functioncall). - Its arguments are provided by a [Script](#script) through a [FunctionCall](#FunctionCall).
#### `FunctionParameter` syntax #### `FunctionParameter` syntax
@@ -174,19 +171,3 @@
- `endCode:` *`string`* (**required**) - `endCode:` *`string`* (**required**)
- Code that'll be inserted at the end of user created script. - Code that'll be inserted at the end of user created script.
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!` - Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
## Naming guidelines
- Prioritize consistency throughout all names.
- Use an instruction format like "do this, do that" for clear, direct guidance. This approach reduces potential confusion and offers easy-to-follow steps. It provides specific, unambiguous instructions.
- Ensure brand names adhere to their official casing.
- Choose clear and uncomplicated language.
- Favor the terms:
- "Disable" over "Turn off"
- "Configure" over "Set up"
- "Clear" over "Erase" or "Clean"
- "Minimize" over "Limit" or "Reduce" (when it enhances clarity)
- "Remove" over "Uninstall"
- Structure your phrases for clarity.
- For instance, "Disable XX telemetry" or "Clear XX data" are preferred over "Clear data from XX", "Disable telemetry in XX", or "Clear data of XX".
- Use sentence case rather than Title Case.

View File

@@ -1,96 +0,0 @@
# Development
Before your commit, a good practice is to:
1. [Run unit tests](#testing)
2. [Lint your code](#linting)
You could run other types of tests as well, but they may take longer time and overkill for your changes.
Automated actions are set up to execute these tests as necessary.
See [ci-cd.md](./ci-cd.md) for more information.
## Commands
### Prerequisites
- Install Node >16.x.
- Install dependencies using `npm install` (or [`npm run install-deps`](#utility-scripts) for more options).
### Testing
- Run unit tests: `npm run test:unit`
- Run integration tests: `npm run test:integration`
- Run end-to-end (e2e) tests:
- `npm run test:cy:open`: Run tests interactively using the development server with hot-reloading.
- `npm run test:cy:run`: Run tests on the production build in a headless mode.
- Run checks:
- `npm run check:desktop`: Run runtime checks for packaged desktop applications ([README.md](./../tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/README.md)).
- You can set environment variables active its flags such as `BUILD=true SCREENSHOT=true npm run check:desktop`
- `npm run check:external-urls`: Test whether external URLs used in applications are alive.
📖 Read more about testing in [tests](./tests.md).
### Linting
- Lint all (recommended 💡): `npm run lint`
- Markdown: `npm run lint:md`
- Markdown consistency `npm run lint:md:consistency`
- Markdown relative URLs: `npm run lint:md:relative-urls`
- JavaScript/TypeScript: `npm run lint:eslint`
- Yaml: `npm run lint:yaml`
### Running
**Web:**
- Run in local server: `npm run dev`
- 💡 Meant for local development with features such as hot-reloading.
- Preview production build: `npm run preview`
- Start a local web server that serves the built solution from `./dist`.
- 💡 Run `npm run build` before `npm run preview`.
**Desktop apps:**
- `npm run electron:dev`: The command will build the main process and preload scripts source code, and start a dev server for the renderer, and start the Electron app.
- `npm run electron:preview`: The command will build the main process, preload scripts and renderer source code, and start the Electron app to preview.
- `npm run electron:prebuild`: The command will build the main process, preload scripts and renderer source code. Usually before packaging the Electron application, you need to execute this command.
- `npm run electron:build`: Prebuilds the Electron application, packages and publishes it through `electron-builder`.
**Docker:**
1. Build: `docker build -t undergroundwires/privacy.sexy:latest .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest`
3. Application should be available at [`http://localhost:8080`](http://localhost:8080)
### Building
- Build web application: `npm run build`
- Build desktop application: `npm run electron:build`
- (Re)create icons (see [documentation](../img/README.md)): `npm run create-icons`
### Scripts
📖 For detailed options and behavior for any of the following scripts, please refer to the script file itself.
#### Utility scripts
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
- [**`./scripts/configure-vscode.sh`**](../scripts/configure-vscode.sh):
- This script checks and sets the necessary configurations for VSCode in `settings.json` file.
#### Automation scripts
- [**`node scripts/print-dist-dir.js [<options>]`**](../scripts/print-dist-dir.js):
- Determines the absolute path of a distribution directory based on CLI arguments and outputs its absolute path.
- [**`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.
- [**`node scripts/verify-web-server-status.js --url [URL]`**](../scripts/verify-web-server-status.js):
- Checks if a specified server is up with retries and returns an HTTP 200 status code.
## Recommended extensions
You should use EditorConfig to follow project style.
For Visual Studio Code, [`.vscode/extensions.json`](./../.vscode/extensions.json) includes list of recommended extensions.

View File

@@ -1,113 +1,72 @@
# Presentation layer # Presentation layer
The presentation layer handles UI concerns using Vue as JavaScript framework and Electron to provide desktop functionality. - Consists of Vue.js components and other UI-related code.
- Desktop application is created using [Electron](https://www.electronjs.org/).
It reflects the [application state](./application.md#application-state) and allows user interactions to modify it. Components manage their own local UI state. - Event driven as in components simply listens to events from the state and act accordingly.
The presentation layer uses an event-driven architecture for bidirectional reactivity between the application state and UI. State change events flow bottom-up to trigger UI updates, while user events flow top-down through components, some ultimately modifying the application state.
📖 Refer to [architecture.md (Layered Application)](./architecture.md#layered-application) to read more about the layered architecture.
## Structure ## Structure
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code. - [`/src/` **`presentation/`**](./../src/presentation/): Contains all presentation related code including Vue and Electron configurations
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins. - [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers. - [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers. - [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components.
- [**`Hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections). - [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that will be processed by webpack.
- [**`/public/`**](../src/presentation/public/): Contains static assets. - [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite. - [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts. - [**`components/`**](./../src/presentation/assets/styles/components): Contains styles that are reusable and tightly coupled a Vue/HTML component.
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles. - [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles that override third-party components used.
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components. - [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Primary Sass file, passes along all other styles, should be the only file used from other components.
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint. - [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app. - [**`electron/`**](./../src/presentation/electron/): Electron configuration for the desktop application.
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code. - [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events. - [**`/public/`**](./../public/): Contains static assets that will directly be copied and not go through webpack.
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use. - [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application. - [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications. - [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
## Visual design best-practices
Add visual clues for clickable items. It should be as clear as possible that they're interactable at first look without hovering. They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility.
## Application data ## Application data
Components (should) use [`UseApplication`](./../src/presentation/components/Shared/Hooks/UseApplication.ts) to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again. - Components and should use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain.
- [Application.ts](../src/domain/Application.ts) domain model is the stateless application representation including
[Application.ts](../src/domain/Application.ts) is an immutable domain model that represents application state. It includes: - available scripts, collections as defined in [collection files](./collection-files.md)
- package information as defined in [`package.json`](./../package.json)
- available scripts, collections as defined in [collection files](./collection-files.md), - 📖 See [Application data | Application layer](./presentation.md#application-data) where application data is parsed and compiled.
- package information as defined in [`package.json`](./../package.json).
You can read more about how application layer provides application data to he presentation in [application.md | Application data](./application.md#application-data).
## Application state ## Application state
This project uses a singleton instance of the application state, making it available to all Vue components. - Stateful components mutate or/and react to state changes in [ApplicationContext](./../src/application/Context/ApplicationContext.ts).
- Stateless components that does not handle state extends `Vue`
- Stateful components that depends on the collection state such as user selection, search queries and more extends [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts)
- The single source of truth is a singleton of the state created and made available to presentation layer by [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts)
- `StatefulVue` includes abstract `handleCollectionState` that is fired once the component is loaded and also each time [collection](./collection-files.md) is changed.
- Do not forget to subscribe from events when component is destroyed or if needed [collection](./collection-files.md) is changed.
- 💡 `events` in base class [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) makes lifecycling easier
- 📖 See [Application state | Application layer](./presentation.md#application-state) where the state is implemented using using state pattern.
The decision to not use third-party state management libraries like [`vuex`](https://web.archive.org/web/20230801191617/https://vuex.vuejs.org/) or [`pinia`](https://web.archive.org/web/20230801191743/https://pinia.vuejs.org/) was made to promote code independence and enhance portability. ## Modals
Stateful components can mutate and/or react to state changes (e.g., user selection, search queries) in the [ApplicationContext](./../src/application/Context/ApplicationContext.ts). Vue components import [`CollectionState.ts`](./../src/presentation/components/Shared/Hooks/UseCollectionState.ts) to access both the application context and the state. - [Dialog.vue](./../src/presentation/components/Shared/Dialog.vue) is a shared component that can be used to show modal windows
- Simply wrap the content inside of its slot and call `.show()` method on its reference.
- Example:
[`UseCollectionState.ts`](./../src/presentation/components/Shared/Hooks/UseCollectionState.ts) provides several functionalities including: ```html
<Dialog ref="testDialog">
<div>Hello world</div>
</Dialog>
<div @click="$refs.testDialog.show()">Show dialog</div>
```
- **Singleton State Instance**: It creates a singleton instance of the state, which is shared across the presentation layer. The singleton instance ensures that there's a single source of truth for the application's state. ## Sass naming convention
- **State Change Callback and Lifecycle Management**: It offers a mechanism to register callbacks, which will be invoked when the state initializes or mutates. It ensures that components unsubscribe from state events when they are no longer in use or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md).
- **State Access and Modification**: It provides functions to read and mutate for accessing and modifying the state, encapsulating the details of these operations.
- **Event Subscription Lifecycle Management**: Includes an `events` member that simplifies state subscription lifecycle events. This ensures that components unsubscribe from state events when they are no longer in use, or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md).
📖 Refer to [architecture.md | Application State](./architecture.md#application-state) for an overview of event handling and [application.md | Application State](./presentation.md#application-state) for an in-depth understanding of state management in the application layer. - Use lowercase for variables/functions/mixins e.g.
## Dependency injections
The presentation layer uses Vue's native dependency injection system to increase testability and decouple components.
To add a new dependency:
1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into:
- **Singletons**: Shared across components, instantiated once.
- **Transients**: Factories yielding a new instance on every access.
2. **Provide the dependency**: Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency. [`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
3. **Inject the dependency**: Use Vue's `inject` method alongside the defined symbol to incorporate the dependency into components.
- For singletons, invoke the factory method: `inject(symbolKey)()`.
- For transients, directly inject: `inject(symbolKey)`.
## Shared UI components
Shared UI components promote consistency and simplifies the creation of the front-end.
In order to maintain portability and easy maintainability, the preference is towards using homegrown components over third-party ones or comprehensive UI frameworks like Quasar.
Shared components include:
- [ModalDialog.vue](./../src/presentation/components/Shared/Modal/ModalDialog.vue) is utilized for rendering modal windows.
- [TooltipWrapper.vue](./../src/presentation/components/Shared/TooltipWrapper.vue) acts as a wrapper for rendering tooltips.
## Desktop builds
Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages.
## Styles
### Style location
- **Global styles**: The [`assets/styles/`](#structure) directory is reserved for styles that have a broader scope, affecting multiple components or entire layouts. They are generic and should not be tightly coupled to a specific component's functionality.
- **Component-specific styles**: Styles closely tied to a particular component's functionality or appearance should reside near the component they are used by. This makes it easier to locate and modify styles when working on a specific component.
### Sass naming convention
- Use lowercase for variables/functions/mixins, e.g.:
- Variable: `$variable: value;` - Variable: `$variable: value;`
- Function: `@function function() {}` - Function: `@function function() {}`
- Mixin: `@mixin mixin() {}` - Mixin: `@mixin mixin() {}`
- Use - for a phrase/compound word, e.g.: - Use - for a phrase/compound word e.g.
- Variable: `$some-variable: value;` - Variable: `$some-variable: value;`
- Function: `@function some-function() {}` - Function: `@function some-function() {}`
- Mixin: `@mixin some-mixin() {}` - Mixin: `@mixin some-mixin() {}`
- Grouping and name variables from generic to specific, e.g.: - Grouping and name variables from generic to specific e.g.
- ✅ `$border-blue`, `$border-blue-light`, `$border-blue-lightest`, `$border-red` - ✅ `$border-blue`, `$border-blue-light`, `$border-blue-lightest`, `$border-red`
- ❌ `$blue-border`, `$light-blue-border`, `$lightest-blue-border`, `$red-border` - ❌ `$blue-border`, `$light-blue-border`, `$lightest-blue-border`, `$red-border`

View File

@@ -3,26 +3,16 @@
## Benefits of templating ## Benefits of templating
- Generating scripts by sharing code to increase best-practice usage and maintainability. - Generating scripts by sharing code to increase best-practice usage and maintainability.
- Creating self-contained scripts without cross-dependencies. - Creating self-contained scripts without depending on each other that can be easily shared.
- Use of pipes for writing cleaner code and letting pipes do dirty work. - Use of pipes for writing cleaner code and letting pipes do dirty work.
## Expressions ## Expressions
- Expressions start and end with mustaches (double brackets, `{{` and `}}`). - Expressions in the language are defined inside mustaches (double brackets, `{{` and `}}`).
- E.g. `Hello {{ $name }} !` - Expression syntax is inspired mainly by [Go Templates](https://pkg.go.dev/text/template).
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same. - Expressions are used in and enabled by functions where they can be used.
- Functions enables usage of expressions.
- In script definition parts of a function, see [`Function`](./collection-files.md#Function). - In script definition parts of a function, see [`Function`](./collection-files.md#Function).
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function). - When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
- Expressions inside expressions (nested templates) are supported.
- An expression can output another expression that will also be compiled.
- E.g. following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output.
```go
{{ with $condition }}
echo {{ $text }}
{{ end }}
```
### Parameter substitution ### Parameter substitution
@@ -65,58 +55,34 @@ A function can call other functions such as:
### with ### with
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions. - Skips the block if the variable is absent or empty.
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value.. - Binds its context (`.`) value of provided argument for the parameter if provided one.
- A block is defined as `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
- The parameters used for `with` condition should be declared as optional, otherwise `with` block becomes redundant.
- Example:
It binds its context (value of the provided parameter value) as arbitrary `.` value. It allows you to use the argument value of the given parameter when it is provided and not empty such as: ```yaml
function: FunctionThatOutputsConditionally
```go parameters:
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }} - name: 'argument'
``` optional: true
code: |-
It supports multiline text inside the block. You can have something like: {{ with $argument }}
Value is: {{ . }}
```go {{ end }}
{{ with $argument }} ```
First line
Second line
{{ end }}
```
You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
```go
{{ with $condition }}
This is a different parameter: {{ $text }}
{{ end }}
```
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
Example:
```yaml
function: FunctionThatOutputsConditionally
parameters:
- name: 'argument'
optional: true
code: |-
{{ with $argument }}
Value is: {{ . }}
{{ end }}
```
### Pipes ### Pipes
- Pipes are functions available for handling text. - Pipes are set of functions available for handling text in privacy.sexy.
- Allows stacking actions one after another also known as "chaining". - Allows stacking actions one after another also known as "chaining".
- Like [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), the concept is simple: each pipeline's output becomes the input of the following pipe. - Just like [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), the concept is simple: each pipeline's output becomes the input of the following pipe.
- You cannot create pipes. [A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files. - Pipes are provided and defined by the compiler and consumed by collection files.
- You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax. - Pipes can be combined with [parameter substitution](#parameter-substitution) and [with](#with).
- ❗ Pipe names must be camelCase without any space or special characters. - ❗ Pipe names must be camelCase without any space or special characters.
- **Existing pipes** - **Existing pipes**
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line. - `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
- `escapeDoubleQuotes`: Escapes `"` characters, allows you to use them inside double quotes (`"`). - `escapeDoubleQuotes`: Escapes `"` characters to be used inside double quotes (`"`)
- **Example usages** - **Example usages**
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}` - `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}` - `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`

View File

@@ -1,88 +1,43 @@
# Tests # Tests
There are different types of tests executed: - There are two different types of tests executed:
1. [Unit tests](#unit-tests)
2. [Integration tests](#integration-tests)
- 💡 You can use path/module alias `@/tests` in import statements.
1. [Unit tests](#unit-tests) ## Unit tests
2. [Integration tests](#integration-tests)
3. [End-to-end (E2E) tests](#e2e-tests)
4. [Automated checks](#automated-checks)
## Unit and integration tests - Tests each component in isolation
- Defined in [`./tests/unit`](./../tests/unit)
- They follow same folder structure as [`./src`](./../src)
- They utilize [Vitest](https://vitest.dev/). ### Naming
- Test files are suffixed with `.spec.ts`.
- Each test suite first describe the system under test
- E.g. tests for class `Application` is categorized under `Application`
- Tests for specific methods are categorized under method name (if applicable)
- E.g. test for `run()` is categorized under `run`
### Act, arrange, assert ### Act, arrange, assert
- Tests implement the act, arrange, and assert (AAA) pattern. - Tests use act, arrange and assert (AAA) pattern when applicable
- **Arrange** - **Arrange**
- Sets up the test scenario and environment. - Should set up the test case
- Begins with comment line `// arrange`. - Starts with comment line `// arrange`
- **Act** - **Act**
- Executes the actual test. - Should cover the main thing to be tested
- Begins with comment line `// act`. - Starts with comment line `// act`
- **Assert** - **Assert**
- Sets an expectation for the test's outcome. - Should elicit some sort of response
- Begins with comment line `// assert`. - Starts with comment line `// assert`
### Unit tests ### Stubs
- Evaluate individual components in isolation. - Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs)
- Located in [`./tests/unit`](./../tests/unit). - They implement dummy behavior to be functional
- Achieve isolation using [stubs](./../tests/unit/shared/Stubs).
- Include Vue component tests, enabled by `@vue/test-utils`.
#### Unit tests naming ## Integration tests
- Test suites start with a description of the component or system under test. - Tests functionality of a component in combination with others (not isolated)
- E.g., tests for `Application.ts` are contained in `Application.spec.ts`. - Ensure dependencies to third parties work as expected
- Whenever possible, `describe` blocks group tests of the same function. - Defined in [`./tests/integration`](./../tests/integration)
- E.g., tests for `run()` are inside `describe('run', () => ...)`.
### Integration tests
- Assess the combined functionality of components.
- They verify that third-party dependencies function as anticipated.
## E2E tests
- Examine the live web application's functionality and performance.
- Uses Cypress to run the tests.
## Automated checks
These checks validate various qualities like runtime execution, building process, security testing, etc.
- Use [various tools](./../package.json) and [scripts](./../scripts).
- Are automatically executed as [GitHub workflows](./../.github/workflows).
### Security checks
- [`checks.security.sast`](./../.github/workflows/checks.security.sast.yaml): Utilizes CodeQL to conduct Static Analysis Security Testing (SAST) to ensure the secure integrity of the codebase.
- [`checks.security.dependencies`](./../.github/workflows/checks.security.dependencies.yaml): Performs audits on third-party dependencies to identify and mitigate potential vulnerabilities, safeguarding the project from exploitable weaknesses.
## Tests structure
- [`package.json`](./../package.json): Defines test commands and includes tools used in tests.
- [`vite.config.ts`](./../vite.config.ts): Configures `vitest` for unit and integration tests.
- [`./src/`](./../src/): Contains the code subject to testing.
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
- [`./tests/unit/`](./../tests/unit/)
- Stores unit test code.
- The directory structure mirrors [`./src/`](./../src).
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
- [`shared/`](./../tests/unit/shared/)
- Contains shared unit test functionalities.
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
- [`TestCases/`](./../tests/unit/shared/TestCases/)
- Shared test cases.
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
- [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities.
- [`./tests/integration/`](./../tests/integration/): Contains integration test files.
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension.
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single spec file.
- [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation.
- *(git ignored)* `/videos`: Asset folder for videos taken during tests.
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.

View File

@@ -1,43 +0,0 @@
/* 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,69 +0,0 @@
import { resolve } from 'path';
import { mergeConfig, UserConfig } from 'vite';
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import { getAliasesFromTsConfig, getClientEnvironmentVariables } from './vite-config-helper';
import { createVueConfig } from './vite.config';
import distDirs from './dist-dirs.json' assert { type: 'json' };
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
const DIST_DIR = resolvePathFromProjectRoot(distDirs.electronUnbundled);
export default defineConfig({
main: getSharedElectronConfig({
distDirSubfolder: 'main',
entryFilePath: MAIN_ENTRY_FILE,
}),
preload: getSharedElectronConfig({
distDirSubfolder: 'preload',
entryFilePath: PRELOAD_ENTRY_FILE,
}),
renderer: mergeConfig(
createVueConfig({
supportLegacyBrowsers: false,
}),
{
build: {
outDir: resolve(DIST_DIR, 'renderer'),
rollupOptions: {
input: {
index: WEB_INDEX_HTML_PATH,
},
},
},
},
),
});
function getSharedElectronConfig(options: {
readonly distDirSubfolder: string;
readonly entryFilePath: string;
}): UserConfig {
return {
build: {
outDir: resolve(DIST_DIR, options.distDirSubfolder),
lib: {
entry: options.entryFilePath,
},
rollupOptions: {
output: {
entryFileNames: '[name].cjs', // This is needed so `type="module"` works
},
},
},
plugins: [externalizeDepsPlugin()],
define: {
...getClientEnvironmentVariables(),
},
resolve: {
alias: {
...getAliasesFromTsConfig(),
},
},
};
}
function resolvePathFromProjectRoot(pathSegment: string) {
return resolve(__dirname, pathSegment);
}

View File

@@ -1,9 +0,0 @@
# img
This folder contains image files and other resources related to images.
## logo.svg
[`logo.svg`](./logo.svg) serves as the primary logo from which all other icons and images are derived.
Only modify this file manually.
After making changes, execute `npm run build:icons` to regenerate logo files in various formats.

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 579 KiB

View File

@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
width="90.2998mm" height="90.2998mm"
viewBox="0 0 256 256">
<path id="logo"
fill="#3a65ab" stroke="#3a65ab" stroke-width="1"
d="M 128.00,173.00
C 128.00,173.00 102.00,175.00 102.00,175.00
85.39,174.97 64.02,170.31 49.00,163.22
38.46,158.24 28.39,152.17 20.01,143.96
14.88,138.93 10.72,133.32 7.31,127.00
3.36,119.66 1.10,112.37 1.00,104.00
1.00,104.00 1.00,98.00 1.00,98.00
1.29,73.92 24.76,53.44 44.00,42.43
84.66,19.15 129.75,23.12 169.00,47.00
188.74,59.02 207.93,76.23 208.00,101.00
208.00,101.00 188.00,101.00 188.00,101.00
186.46,86.48 168.72,71.84 157.00,64.61
140.76,54.59 121.33,46.78 102.00,47.00
88.42,47.16 72.20,52.52 60.00,58.32
45.30,65.30 19.83,84.81 21.10,103.00
21.39,107.16 22.92,110.33 24.76,114.00
32.70,129.78 48.16,140.02 64.00,146.72
75.16,151.44 92.90,155.26 105.00,154.99
113.45,154.79 121.81,152.84 130.00,151.00
130.00,151.00 128.00,173.00 128.00,173.00 Z
M 136.00,79.00
C 142.71,81.35 144.84,93.60 144.99,100.00
145.51,122.74 130.31,140.73 107.00,141.00
83.63,141.26 67.43,126.52 66.09,103.00
64.82,80.73 85.85,58.90 104.00,64.00
100.18,69.73 95.45,74.53 96.20,82.00
97.29,92.87 110.06,102.98 121.00,99.03
129.92,95.81 134.61,87.96 136.00,79.00 Z
M 186.00,113.46
C 206.11,110.69 225.57,114.92 239.91,130.01
252.85,143.63 255.21,157.09 255.00,175.00
254.76,195.49 241.26,214.25 223.00,222.88
213.06,227.58 204.72,228.12 194.00,228.00
150.34,227.49 126.71,178.85 146.32,142.00
154.93,125.82 168.55,117.23 186.00,113.46 Z
M 233.00,181.00
C 242.24,158.78 221.84,133.54 199.00,133.01
188.40,132.77 182.75,135.31 174.00,141.00
178.60,146.85 195.92,157.24 203.00,161.86
209.82,166.32 226.61,178.55 233.00,181.00 Z
M 221.00,200.00
C 216.39,194.15 206.42,188.61 200.00,184.33
192.31,179.21 168.77,162.59 162.00,160.00
159.67,165.03 159.94,166.57 160.00,172.00
160.23,190.99 177.11,207.55 196.00,207.99
206.60,208.23 212.25,205.69 221.00,200.00 Z" />
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

56066
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,98 +1,76 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.12.5", "version": "0.11.1",
"private": true, "private": true,
"slogan": "Now you have the choice", "description": "Enforce privacy & security best-practices on Windows and macOS, 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",
"main": "./dist-electron-unbundled/main/index.cjs",
"scripts": { "scripts": {
"dev": "vite", "serve": "vue-cli-service serve",
"build": "vue-tsc --noEmit && vite build", "build": "vue-cli-service build",
"preview": "vite preview", "test:unit": "vue-cli-service test:unit",
"test:unit": "vitest run --dir tests/unit", "test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\"",
"test:integration": "vitest run --dir tests/integration", "lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"", "electron:build": "vue-cli-service electron:build",
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"", "electron:serve": "vue-cli-service electron:serve",
"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",
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
"check:external-urls": "vitest run --dir tests/checks/external-urls --environment node",
"check:verify-build-artifacts": "node scripts/verify-build-artifacts",
"electron:dev": "electron-vite dev",
"electron:preview": "electron-vite preview",
"electron:prebuild": "electron-vite build",
"electron:build": "electron-builder",
"lint:eslint": "eslint . --ignore-path .gitignore",
"lint:md": "markdownlint **/*.md --ignore node_modules", "lint:md": "markdownlint **/*.md --ignore node_modules",
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent", "lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
"lint:md:relative-urls": "remark . --frail --use remark-validate-links", "lint:md:relative-urls": "remark . --frail --use remark-validate-links",
"lint:vue": "vue-cli-service lint --no-fix",
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml", "lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
"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": "background.js",
"dependencies": { "dependencies": {
"@floating-ui/vue": "^1.0.2", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@juggle/resize-observer": "^3.4.0", "@fortawesome/free-brands-svg-icons": "^5.15.4",
"ace-builds": "^1.30.0", "@fortawesome/free-regular-svg-icons": "^5.15.4",
"cross-fetch": "^4.0.0", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"electron-log": "^4.4.8", "@fortawesome/vue-fontawesome": "^2.0.6",
"electron-progressbar": "^2.1.0", "@juggle/resize-observer": "^3.3.1",
"electron-updater": "^6.1.4", "ace-builds": "^1.4.13",
"core-js": "^3.18.3",
"cross-fetch": "^3.1.4",
"electron-progressbar": "^2.0.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"markdown-it": "^13.0.2", "install": "^0.13.0",
"vue": "^2.7.14" "liquor-tree": "^0.2.70",
"npm": "^8.1.1",
"v-tooltip": "2.1.3",
"vue": "^2.6.14",
"vue-class-component": "^7.2.6",
"vue-js-modal": "^2.0.1",
"vue-property-decorator": "^9.1.2"
}, },
"devDependencies": { "devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.0.4", "@types/ace": "0.0.47",
"@rushstack/eslint-patch": "^1.5.1", "@types/chai": "^4.2.22",
"@types/ace": "^0.0.49", "@types/file-saver": "^2.0.3",
"@types/file-saver": "^2.0.5", "@types/mocha": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@vue/cli-plugin-babel": "^4.5.14",
"@typescript-eslint/parser": "^5.62.0", "@vue/cli-plugin-typescript": "^4.5.14",
"@vitejs/plugin-legacy": "^4.1.1", "@vue/cli-plugin-unit-mocha": "^4.5.14",
"@vitejs/plugin-vue2": "^2.2.0", "@vue/cli-service": "^4.5.14",
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0", "@vue/test-utils": "1.2.2",
"@vue/eslint-config-typescript": "^11.0.3", "chai": "^4.3.4",
"@vue/test-utils": "^1.3.6", "electron": "^15.3.0",
"autoprefixer": "^10.4.16",
"cypress": "^13.3.1",
"electron": "^27.0.0",
"electron-builder": "^24.6.4",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1", "electron-log": "^4.4.1",
"electron-vite": "^1.0.27", "electron-updater": "^4.3.9",
"eslint": "^8.51.0", "js-yaml-loader": "^1.2.2",
"eslint-plugin-cypress": "^2.15.1", "markdownlint-cli": "^0.29.0",
"eslint-plugin-vue": "^9.17.0", "raw-loader": "^4.0.2",
"eslint-plugin-vuejs-accessibility": "^2.2.0", "remark-cli": "^10.0.0",
"icon-gen": "^4.0.0",
"jsdom": "^22.1.0",
"markdownlint-cli": "^0.37.0",
"postcss": "^8.4.31",
"remark-cli": "^12.0.0",
"remark-lint-no-dead-urls": "^1.1.0", "remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^5.1.2", "remark-preset-lint-consistent": "^5.1.0",
"remark-validate-links": "^13.0.0", "remark-validate-links": "^11.0.1",
"sass": "^1.69.3", "sass": "^1.43.3",
"start-server-and-test": "^2.0.1", "sass-loader": "10.2.0",
"svgexport": "^0.4.2", "tslib": "^2.3.1",
"terser": "^5.21.0", "typescript": "^4.4.4",
"tslib": "^2.6.2", "vue-cli-plugin-electron-builder": "^2.1.1",
"typescript": "^5.2.2", "vue-template-compiler": "^2.6.14",
"vite": "^4.4.11", "yaml-lint": "^1.2.4"
"vitest": "^0.34.6",
"vue-tsc": "^1.8.19",
"yaml-lint": "^1.7.0"
},
"//devDependencies": {
"terser": "Used by @vitejs/plugin-legacy for minification",
"@rushstack/eslint-patch": "Needed by @vue/eslint-config-typescript",
"@vue/eslint-config-typescript": "Cannot upgrade to 12.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58",
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58",
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58"
}, },
"homepage": "https://privacy.sexy", "homepage": "https://privacy.sexy",
"repository": { "repository": {

View File

@@ -1,9 +0,0 @@
const autoprefixer = require('autoprefixer');
module.exports = () => {
return {
plugins: [
autoprefixer(),
],
};
};

5
postcss.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,4 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows and macOS</title>
<meta name="robots" content="index,follow" />
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
</head>
<body> <body>
<noscript> <noscript>
<style> <style>
@@ -19,5 +29,7 @@
<p>The page does not work without JavaScript enabled. Please enable it to use privacy.sexy. There's no shady stuff as 100% of the website is open source.</p> <p>The page does not work without JavaScript enabled. Please enable it to use privacy.sexy. There's no shady stuff as 100% of the website is open source.</p>
</div> </div>
</noscript> </noscript>
<script type="module" src="/src/main.js"></script> <div id="app"></div>
<!-- built files will be auto injected -->
</body> </body>
</html>

View File

@@ -1,74 +0,0 @@
#!/usr/bin/env bash
# This script ensures that the '.vscode/settings.json' file exists and is configured correctly for ESLint validation on Vue and JavaScript files.
# See https://web.archive.org/web/20230801024405/https://eslint.vuejs.org/user-guide/#visual-studio-code
declare -r SETTINGS_FILE='.vscode/settings.json'
declare -ra CONFIG_KEYS=('vue' 'javascript' 'typescript')
declare -r TEMP_FILE="tmp.$$.json"
main() {
ensure_vscode_directory_exists
create_or_update_settings
}
ensure_vscode_directory_exists() {
local dir_name
dir_name=$(dirname "${SETTINGS_FILE}")
if [[ ! -d ${dir_name} ]]; then
mkdir -p "${dir_name}"
echo "🎉 Created directory: ${dir_name}"
fi
}
create_or_update_settings() {
if [[ ! -f ${SETTINGS_FILE} ]]; then
create_default_settings
else
add_or_update_eslint_validate
fi
}
create_default_settings() {
local default_validate
default_validate=$(printf '%s' "${CONFIG_KEYS[*]}" | jq -R -s -c -M 'split(" ")')
echo "{ \"eslint.validate\": ${default_validate} }" | jq '.' > "${SETTINGS_FILE}"
echo "🎉 Created default ${SETTINGS_FILE}"
}
add_or_update_eslint_validate() {
if ! jq -e '.["eslint.validate"]' "${SETTINGS_FILE}" >/dev/null; then
add_default_eslint_validate
else
update_eslint_validate
fi
}
add_default_eslint_validate() {
jq --argjson keys "$(printf '%s' "${CONFIG_KEYS[*]}" \
| jq -R -s -c 'split(" ")')" '. += {"eslint.validate": $keys}' "${SETTINGS_FILE}" > "${TEMP_FILE}"
replace_and_confirm
echo "🎉 Added default 'eslint.validate' to ${SETTINGS_FILE}"
}
update_eslint_validate() {
local existing_keys
existing_keys=$(jq '.["eslint.validate"]' "${SETTINGS_FILE}")
for key in "${CONFIG_KEYS[@]}"; do
if ! echo "${existing_keys}" | jq 'index("'"${key}"'")' >/dev/null; then
jq '.["eslint.validate"] += ["'"${key}"'"]' "${SETTINGS_FILE}" > "${TEMP_FILE}"
mv "${TEMP_FILE}" "${SETTINGS_FILE}"
echo "🎉 Updated 'eslint.validate' in ${SETTINGS_FILE} for ${key}"
else
echo "⏩️ No updated needed for ${key} ${SETTINGS_FILE}."
fi
done
}
replace_and_confirm() {
if mv "${TEMP_FILE}" "${SETTINGS_FILE}"; then
echo "🎉 Updated ${SETTINGS_FILE}"
fi
}
main

View File

@@ -1,127 +0,0 @@
#!/usr/bin/env bash
import { resolve, join } from 'path';
import { rm, mkdtemp, stat } from 'fs/promises';
import { spawn } from 'child_process';
import { URL, fileURLToPath } from 'url';
class Paths {
constructor(selfDirectory) {
const projectRoot = resolve(selfDirectory, '../');
this.sourceImage = join(projectRoot, 'img/logo.svg');
this.publicDirectory = join(projectRoot, 'src/presentation/public');
this.electronBuildDirectory = join(projectRoot, 'build');
}
toString() {
return `Source image: ${this.sourceImage}\n`
+ `Public directory: ${this.publicDirectory}\n`
+ `Electron build directory: ${this.electronBuildDirectory}`;
}
}
async function main() {
const paths = new Paths(getCurrentScriptDirectory());
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
await updateDesktopLauncherAndTrayIcon(paths.sourceImage, paths.publicDirectory);
await updateWebFavicon(paths.sourceImage, paths.publicDirectory);
await updateDesktopIcons(paths.sourceImage, paths.electronBuildDirectory);
console.log('🎉 (Re)created icons successfully.');
}
async function updateDesktopLauncherAndTrayIcon(sourceImage, publicFolder) {
await ensureFileExists(sourceImage);
await ensureFolderExists(publicFolder);
const electronTrayIconFile = join(publicFolder, 'icon.png');
console.log(`Updating desktop launcher and tray icon at ${electronTrayIconFile}.`);
await runCommand(
'npx',
'svgexport',
sourceImage,
electronTrayIconFile,
);
}
async function updateWebFavicon(sourceImage, faviconFolder) {
console.log('Updating favicon');
await ensureFileExists(sourceImage);
await ensureFolderExists(faviconFolder);
await runCommand(
'npx',
'icon-gen',
`--input ${sourceImage}`,
`--output ${faviconFolder}`,
'--ico',
'--ico-name \'favicon\'',
'--report',
);
}
async function updateDesktopIcons(sourceImage, electronIconsDir) {
await ensureFileExists(sourceImage);
await ensureFolderExists(electronIconsDir);
const temporaryDir = await mkdtemp('icon-');
const temporaryPngFile = join(temporaryDir, 'icon.png');
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by `icon-builder`
await runCommand(
'npx',
'svgexport',
sourceImage,
temporaryPngFile,
'1024:1024',
);
console.log(`Creating electron icons to ${electronIconsDir}.`);
await runCommand(
'npx',
'electron-icon-builder',
`--input="${temporaryPngFile}"`,
`--output="${electronIconsDir}"`,
'--flatten',
);
console.log('Cleaning up temporary directory.');
await rm(temporaryDir, { recursive: true, force: true });
}
async function ensureFileExists(filePath) {
const path = await stat(filePath);
if (!path.isFile()) {
throw new Error(`Not a file: ${filePath}`);
}
}
async function ensureFolderExists(folderPath) {
const path = await stat(folderPath);
if (!path.isDirectory()) {
throw new Error(`Not a directory: ${folderPath}`);
}
}
async function runCommand(...args) {
const command = args.join(' ');
console.log(`Running command: ${command}`);
await new Promise((resolve, reject) => {
const process = spawn(command, { shell: true });
process.stdout.on('data', (stdout) => {
console.log(stdout.toString());
});
process.stderr.on('data', (stderr) => {
console.error(stderr.toString());
});
process.on('error', (err) => {
reject(err);
});
process.on('close', (exitCode) => {
if (exitCode !== 0) {
reject(new Error(`Process exited with non-zero exit code: ${exitCode}`));
} else {
resolve();
}
process.stdin.end();
});
});
}
function getCurrentScriptDirectory() {
return fileURLToPath(new URL('.', import.meta.url));
}
await main();

View File

@@ -1,199 +0,0 @@
/*
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();

View File

@@ -1,58 +0,0 @@
/**
* 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

@@ -1,133 +0,0 @@
/**
* 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,62 +0,0 @@
/**
* Description:
* This script checks if a server, provided as a CLI argument, is up
* and returns an HTTP 200 status code.
* It is designed to provide easy verification of server availability
* and will retry a specified number of times.
*
* Usage:
* node ./scripts/verify-web-server-status.js --url [URL]
*
* Options:
* --url URL of the server to check
*/
import { get } from 'http';
const MAX_RETRIES = 30;
const RETRY_DELAY_IN_SECONDS = 3;
const URL_PARAMETER_NAME = '--url';
function checkServer(currentRetryCount = 1) {
const serverUrl = getServerUrl();
console.log(`Requesting ${serverUrl}...`);
get(serverUrl, (res) => {
if (res.statusCode === 200) {
console.log('🎊 Success: The server is up and returned HTTP 200.');
process.exit(0);
} else {
console.log(`Server returned HTTP status code ${res.statusCode}.`);
retry(currentRetryCount);
}
}).on('error', (err) => {
console.error('Error making the request:', err);
retry(currentRetryCount);
});
}
function retry(currentRetryCount) {
console.log(`Attempt ${currentRetryCount}/${MAX_RETRIES}:`);
console.log(`Retrying in ${RETRY_DELAY_IN_SECONDS} seconds.`);
const remainingTime = (MAX_RETRIES - currentRetryCount) * RETRY_DELAY_IN_SECONDS;
console.log(`Time remaining before timeout: ${remainingTime}s`);
if (currentRetryCount < MAX_RETRIES) {
setTimeout(() => checkServer(currentRetryCount + 1), RETRY_DELAY_IN_SECONDS * 1000);
} else {
console.log('Failure: The server at did not return HTTP 200 within the allocated time. Exiting.');
process.exit(1);
}
}
function getServerUrl() {
const urlIndex = process.argv.indexOf(URL_PARAMETER_NAME);
if (urlIndex === -1 || urlIndex === process.argv.length - 1) {
console.error(`Parameter "${URL_PARAMETER_NAME}" is not provided.`);
process.exit(1);
}
return process.argv[urlIndex + 1];
}
checkServer();

View File

@@ -1,16 +0,0 @@
export type Constructible<T, TArgs extends unknown[] = never> = {
prototype: T;
apply: (this: unknown, args: TArgs) => void;
readonly name: string;
};
export type PropertyKeys<T> = {
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? never : K;
}[keyof T];
export type ConstructorArguments<T> =
T extends new (...args: infer U) => unknown ? U : never;
export type FunctionKeys<T> = {
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never;
}[keyof T];

View File

@@ -3,22 +3,19 @@ import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
import { IApplicationFactory } from './IApplicationFactory'; import { IApplicationFactory } from './IApplicationFactory';
import { parseApplication } from './Parser/ApplicationParser'; import { parseApplication } from './Parser/ApplicationParser';
export type ApplicationGetterType = () => IApplication; export type ApplicationGetter = () => IApplication;
const ApplicationGetter: ApplicationGetterType = parseApplication; const ApplicationGetter: ApplicationGetter = parseApplication;
export class ApplicationFactory implements IApplicationFactory { export class ApplicationFactory implements IApplicationFactory {
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter); public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
private readonly getter: AsyncLazy<IApplication>;
private readonly getter: AsyncLazy<IApplication>; protected constructor(costlyGetter: ApplicationGetter) {
if (!costlyGetter) {
protected constructor(costlyGetter: ApplicationGetterType) { throw new Error('undefined getter');
if (!costlyGetter) { }
throw new Error('missing getter'); this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
}
public getApp(): Promise<IApplication> {
return this.getter.getValue();
} }
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
}
public getApp(): Promise<IApplication> {
return this.getter.getValue();
}
} }

View File

@@ -1,21 +1,21 @@
// Compares to Array<T> objects for equality, ignoring order // Compares to Array<T> objects for equality, ignoring order
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) { export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('missing first array'); } if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('missing second array'); } if (!array2) { throw new Error('undefined second array'); }
const sortedArray1 = sort(array1); const sortedArray1 = sort(array1);
const sortedArray2 = sort(array2); const sortedArray2 = sort(array2);
return sequenceEqual(sortedArray1, sortedArray2); return sequenceEqual(sortedArray1, sortedArray2);
function sort(array: readonly T[]) { function sort(array: readonly T[]) {
return array.slice().sort(); return array.slice().sort();
} }
} }
// Compares to Array<T> objects for equality in same order // Compares to Array<T> objects for equality in same order
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) { export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('missing first array'); } if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('missing second array'); } if (!array2) { throw new Error('undefined second array'); }
if (array1.length !== array2.length) { if (array1.length !== array2.length) {
return false; return false;
} }
return array1.every((val, index) => val === array2[index]); return array1.every((val, index) => val === array2[index]);
} }

View File

@@ -1,50 +0,0 @@
/*
Provides a unified and resilient way to extend errors across platforms.
Rationale:
- Babel:
> "Built-in classes cannot be properly subclassed due to limitations in ES5"
> https://web.archive.org/web/20230810014108/https://babeljs.io/docs/caveats#classes
- TypeScript:
> "Extending built-ins like Error, Array, and Map may no longer work"
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
*/
export abstract class CustomError extends Error {
constructor(message?: string, options?: ErrorOptions) {
super(message, options);
fixPrototype(this, new.target.prototype);
ensureStackTrace(this);
this.name = this.constructor.name;
}
}
export const Environment = {
getSetPrototypeOf: () => Object.setPrototypeOf,
getCaptureStackTrace: () => Error.captureStackTrace,
};
function fixPrototype(target: Error, prototype: CustomError) {
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
const setPrototypeOf = Environment.getSetPrototypeOf();
if (!functionExists(setPrototypeOf)) {
return;
}
setPrototypeOf(target, prototype);
}
function ensureStackTrace(target: Error) {
const captureStackTrace = Environment.getCaptureStackTrace();
if (!functionExists(captureStackTrace)) {
// captureStackTrace is only available on V8, if it's not available
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
return;
}
captureStackTrace(target, target.constructor);
}
function functionExists(func: unknown): boolean {
// Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness
return typeof func === 'function';
}

View File

@@ -1,63 +1,54 @@
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611 // Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
export type EnumType = number | string; export type EnumType = number | string;
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
= { [key in T]: TEnumValue };
export interface IEnumParser<TEnum> { export interface IEnumParser<TEnum> {
parseEnum(value: string, propertyName: string): TEnum; parseEnum(value: string, propertyName: string): TEnum;
} }
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>( export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>, enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> {
): IEnumParser<TEnumValue> { return {
return { parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable), };
};
} }
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>( function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
value: string, value: string,
enumName: string, enumName: string,
enumVariable: EnumVariable<T, TEnumValue>, enumVariable: EnumVariable<T, TEnumValue>): TEnumValue {
): TEnumValue { if (!value) {
if (!value) { throw new Error(`undefined ${enumName}`);
throw new Error(`missing ${enumName}`); }
} if (typeof value !== 'string') {
if (typeof value !== 'string') { throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`); }
} const casedValue = getEnumNames(enumVariable)
const casedValue = getEnumNames(enumVariable) .find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase()); if (!casedValue) {
if (!casedValue) { throw new Error(`unknown ${enumName}: "${value}"`);
throw new Error(`unknown ${enumName}: "${value}"`); }
} return enumVariable[casedValue as keyof typeof enumVariable];
return enumVariable[casedValue as keyof typeof enumVariable];
} }
export function getEnumNames export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
<T extends EnumType, TEnumValue extends EnumType>( enumVariable: EnumVariable<T, TEnumValue>): string[] {
enumVariable: EnumVariable<T, TEnumValue>, return Object
): string[] { .values(enumVariable)
return Object .filter((enumMember) => typeof enumMember === 'string') as string[];
.values(enumVariable)
.filter((enumMember) => typeof enumMember === 'string') as string[];
} }
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>( export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>, enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] {
): TEnumValue[] { return getEnumNames(enumVariable)
return getEnumNames(enumVariable) .map((level) => enumVariable[level]) as TEnumValue[];
.map((level) => enumVariable[level]) as TEnumValue[];
} }
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>( export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
value: TEnumValue, value: TEnumValue,
enumVariable: EnumVariable<T, TEnumValue>, enumVariable: EnumVariable<T, TEnumValue>) {
) { if (value === undefined) {
if (value === undefined || value === null) { throw new Error('undefined enum value');
throw new Error('absent enum value'); }
} if (!(value in enumVariable)) {
if (!(value in enumVariable)) { throw new RangeError(`enum value "${value}" is out of range`);
throw new RangeError(`enum value "${value}" is out of range`); }
}
} }

View File

@@ -1,5 +1,5 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
export interface IScriptingLanguageFactory<T> { export interface IScriptingLanguageFactory<T> {
create(language: ScriptingLanguage): T; create(language: ScriptingLanguage): T;
} }

View File

@@ -1,30 +1,31 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { assertInRange } from '@/application/Common/Enum';
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory'; import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
import { assertInRange } from '@/application/Common/Enum';
type Getter<T> = () => T; type Getter<T> = () => T;
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<T> { export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<T> {
private readonly getters = new Map<ScriptingLanguage, Getter<T>>(); private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
public create(language: ScriptingLanguage): T { public create(language: ScriptingLanguage): T {
assertInRange(language, ScriptingLanguage); assertInRange(language, ScriptingLanguage);
if (!this.getters.has(language)) { if (!this.getters.has(language)) {
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`); throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
const getter = this.getters.get(language);
const instance = getter();
return instance;
} }
const getter = this.getters.get(language);
const instance = getter();
return instance;
}
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) { protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
assertInRange(language, ScriptingLanguage); assertInRange(language, ScriptingLanguage);
if (!getter) { if (!getter) {
throw new Error('missing getter'); throw new Error('undefined getter');
}
if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`);
}
this.getters.set(language, getter);
} }
if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`);
}
this.getters.set(language, getter);
}
} }

View File

@@ -1,64 +1,60 @@
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
import { CategoryCollectionState } from './State/CategoryCollectionState';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { assertInRange } from '@/application/Common/Enum'; import { assertInRange } from '@/application/Common/Enum';
import { CategoryCollectionState } from './State/CategoryCollectionState';
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>; type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
export class ApplicationContext implements IApplicationContext { export class ApplicationContext implements IApplicationContext {
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>(); public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
public collection: ICategoryCollection;
public currentOs: OperatingSystem;
public collection: ICategoryCollection; public get state(): ICategoryCollectionState {
return this.states[this.collection.os];
public currentOs: OperatingSystem;
public get state(): ICategoryCollectionState {
return this.states[this.collection.os];
}
private readonly states: StateMachine;
public constructor(
public readonly app: IApplication,
initialContext: OperatingSystem,
) {
validateApp(app);
this.states = initializeStates(app);
this.changeContext(initialContext);
}
public changeContext(os: OperatingSystem): void {
assertInRange(os, OperatingSystem);
if (this.currentOs === os) {
return;
} }
this.collection = this.app.getCollection(os);
if (!this.collection) { private readonly states: StateMachine;
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`); public constructor(
public readonly app: IApplication,
initialContext: OperatingSystem) {
validateApp(app);
assertInRange(initialContext, OperatingSystem);
this.states = initializeStates(app);
this.changeContext(initialContext);
}
public changeContext(os: OperatingSystem): void {
if (this.currentOs === os) {
return;
}
this.collection = this.app.getCollection(os);
if (!this.collection) {
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
}
const event: IApplicationContextChangedEvent = {
newState: this.states[os],
oldState: this.states[this.currentOs],
};
this.contextChanged.notify(event);
this.currentOs = os;
} }
const event: IApplicationContextChangedEvent = {
newState: this.states[os],
oldState: this.states[this.currentOs],
};
this.contextChanged.notify(event);
this.currentOs = os;
}
} }
function validateApp(app: IApplication) { function validateApp(app: IApplication) {
if (!app) { if (!app) {
throw new Error('missing app'); throw new Error('undefined app');
} }
} }
function initializeStates(app: IApplication): StateMachine { function initializeStates(app: IApplication): StateMachine {
const machine = new Map<OperatingSystem, ICategoryCollectionState>(); const machine = new Map<OperatingSystem, ICategoryCollectionState>();
for (const collection of app.collections) { for (const collection of app.collections) {
machine[collection.os] = new CategoryCollectionState(collection); machine[collection.os] = new CategoryCollectionState(collection);
} }
return machine; return machine;
} }

View File

@@ -1,30 +1,31 @@
import { ApplicationContext } from './ApplicationContext';
import { IApplicationContext } from '@/application/Context/IApplicationContext'; import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { Environment } from '../Environment/Environment';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment'; import { IEnvironment } from '../Environment/IEnvironment';
import { IApplicationFactory } from '../IApplicationFactory'; import { IApplicationFactory } from '../IApplicationFactory';
import { ApplicationFactory } from '../ApplicationFactory'; import { ApplicationFactory } from '../ApplicationFactory';
import { ApplicationContext } from './ApplicationContext';
export async function buildContext( export async function buildContext(
factory: IApplicationFactory = ApplicationFactory.Current, factory: IApplicationFactory = ApplicationFactory.Current,
environment = RuntimeEnvironment.CurrentEnvironment, environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
): Promise<IApplicationContext> { if (!factory) { throw new Error('undefined factory'); }
if (!factory) { throw new Error('missing factory'); } if (!environment) { throw new Error('undefined 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, currentOs: OperatingSystem): OperatingSystem { function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
const supportedOsList = app.getSupportedOsList(); const currentOs = environment.os;
if (supportedOsList.includes(currentOs)) { const supportedOsList = app.getSupportedOsList();
return currentOs; if (supportedOsList.includes(currentOs)) {
} return currentOs;
supportedOsList.sort((os1, os2) => { }
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts; supportedOsList.sort((os1, os2) => {
return getPriority(os2) - getPriority(os1); const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
}); return getPriority(os2) - getPriority(os1);
return supportedOsList[0]; });
return supportedOsList[0];
} }

View File

@@ -1,20 +1,16 @@
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { IEventSource } from '@/infrastructure/Events/IEventSource'; import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
export interface IReadOnlyApplicationContext { export interface IApplicationContext {
readonly app: IApplication; readonly app: IApplication;
readonly state: IReadOnlyCategoryCollectionState; readonly state: ICategoryCollectionState;
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>; readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
} changeContext(os: OperatingSystem): void;
export interface IApplicationContext extends IReadOnlyApplicationContext {
readonly state: ICategoryCollectionState;
changeContext(os: OperatingSystem): void;
} }
export interface IApplicationContextChangedEvent { export interface IApplicationContextChangedEvent {
readonly newState: ICategoryCollectionState; readonly newState: ICategoryCollectionState;
readonly oldState: ICategoryCollectionState; readonly oldState: ICategoryCollectionState;
} }

View File

@@ -1,5 +1,3 @@
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { UserFilter } from './Filter/UserFilter'; import { UserFilter } from './Filter/UserFilter';
import { IUserFilter } from './Filter/IUserFilter'; import { IUserFilter } from './Filter/IUserFilter';
import { ApplicationCode } from './Code/ApplicationCode'; import { ApplicationCode } from './Code/ApplicationCode';
@@ -7,20 +5,19 @@ import { UserSelection } from './Selection/UserSelection';
import { IUserSelection } from './Selection/IUserSelection'; import { IUserSelection } from './Selection/IUserSelection';
import { ICategoryCollectionState } from './ICategoryCollectionState'; import { ICategoryCollectionState } from './ICategoryCollectionState';
import { IApplicationCode } from './Code/IApplicationCode'; import { IApplicationCode } from './Code/IApplicationCode';
import { ICategoryCollection } from '../../../domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
export class CategoryCollectionState implements ICategoryCollectionState { export class CategoryCollectionState implements ICategoryCollectionState {
public readonly os: OperatingSystem; public readonly os: OperatingSystem;
public readonly code: IApplicationCode;
public readonly selection: IUserSelection;
public readonly filter: IUserFilter;
public readonly code: IApplicationCode; public constructor(readonly collection: ICategoryCollection) {
this.selection = new UserSelection(collection, []);
public readonly selection: IUserSelection; this.code = new ApplicationCode(this.selection, collection.scripting);
this.filter = new UserFilter(collection);
public readonly filter: IUserFilter; this.os = collection.os;
}
public constructor(readonly collection: ICategoryCollection) {
this.selection = new UserSelection(collection, []);
this.code = new ApplicationCode(this.selection, collection.scripting);
this.filter = new UserFilter(collection);
this.os = collection.os;
}
} }

View File

@@ -1,41 +1,39 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { CodeChangedEvent } from './Event/CodeChangedEvent'; import { CodeChangedEvent } from './Event/CodeChangedEvent';
import { CodePosition } from './Position/CodePosition'; import { CodePosition } from './Position/CodePosition';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
import { UserScriptGenerator } from './Generation/UserScriptGenerator'; import { UserScriptGenerator } from './Generation/UserScriptGenerator';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IApplicationCode } from './IApplicationCode'; import { IApplicationCode } from './IApplicationCode';
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator'; import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
export class ApplicationCode implements IApplicationCode { export class ApplicationCode implements IApplicationCode {
public readonly changed = new EventSource<ICodeChangedEvent>(); public readonly changed = new EventSource<ICodeChangedEvent>();
public current: string;
public current: string; private scriptPositions = new Map<SelectedScript, CodePosition>();
private scriptPositions = new Map<SelectedScript, CodePosition>(); constructor(
userSelection: IUserSelection,
private readonly scriptingDefinition: IScriptingDefinition,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
if (!generator) { throw new Error('generator is null or undefined'); }
this.setCode(userSelection.selectedScripts);
userSelection.changed.on((scripts) => {
this.setCode(scripts);
});
}
constructor( private setCode(scripts: ReadonlyArray<SelectedScript>): void {
userSelection: IReadOnlyUserSelection, const oldScripts = Array.from(this.scriptPositions.keys());
private readonly scriptingDefinition: IScriptingDefinition, const code = this.generator.buildCode(scripts, this.scriptingDefinition);
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(), this.current = code.code;
) { this.scriptPositions = code.scriptPositions;
if (!userSelection) { throw new Error('missing userSelection'); } const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); } this.changed.notify(event);
if (!generator) { throw new Error('missing generator'); } }
this.setCode(userSelection.selectedScripts);
userSelection.changed.on((scripts) => {
this.setCode(scripts);
});
}
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
const oldScripts = Array.from(this.scriptPositions.keys());
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
this.current = code.code;
this.scriptPositions = code.scriptPositions;
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
this.changed.notify(event);
}
} }

View File

@@ -1,71 +1,64 @@
import { ICodeChangedEvent } from './ICodeChangedEvent';
import { SelectedScript } from '../../Selection/SelectedScript';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { SelectedScript } from '../../Selection/SelectedScript';
import { ICodeChangedEvent } from './ICodeChangedEvent';
export class CodeChangedEvent implements ICodeChangedEvent { export class CodeChangedEvent implements ICodeChangedEvent {
public readonly code: string; public readonly code: string;
public readonly addedScripts: ReadonlyArray<IScript>;
public readonly removedScripts: ReadonlyArray<IScript>;
public readonly changedScripts: ReadonlyArray<IScript>;
public readonly addedScripts: ReadonlyArray<IScript>; private readonly scripts: Map<IScript, ICodePosition>;
public readonly removedScripts: ReadonlyArray<IScript>; constructor(
code: string,
oldScripts: ReadonlyArray<SelectedScript>,
scripts: Map<SelectedScript, ICodePosition>) {
ensureAllPositionsExist(code, Array.from(scripts.values()));
this.code = code;
const newScripts = Array.from(scripts.keys());
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
this.changedScripts = getChangedScripts(oldScripts, newScripts);
this.scripts = new Map<IScript, ICodePosition>();
scripts.forEach((position, selection) => {
this.scripts.set(selection.script, position);
});
}
public readonly changedScripts: ReadonlyArray<IScript>; public isEmpty(): boolean {
return this.scripts.size === 0;
}
private readonly scripts: Map<IScript, ICodePosition>; public getScriptPositionInCode(script: IScript): ICodePosition {
return this.scripts.get(script);
constructor( }
code: string,
oldScripts: ReadonlyArray<SelectedScript>,
scripts: Map<SelectedScript, ICodePosition>,
) {
ensureAllPositionsExist(code, Array.from(scripts.values()));
this.code = code;
const newScripts = Array.from(scripts.keys());
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
this.changedScripts = getChangedScripts(oldScripts, newScripts);
this.scripts = new Map<IScript, ICodePosition>();
scripts.forEach((position, selection) => {
this.scripts.set(selection.script, position);
});
}
public isEmpty(): boolean {
return this.scripts.size === 0;
}
public getScriptPositionInCode(script: IScript): ICodePosition {
return this.scripts.get(script);
}
} }
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) { function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
const totalLines = script.split(/\r\n|\r|\n/).length; const totalLines = script.split(/\r\n|\r|\n/).length;
const missingPositions = positions.filter((position) => position.endLine > totalLines); for (const position of positions) {
if (missingPositions.length > 0) { if (position.endLine > totalLines) {
throw new Error( throw new Error(`script end line (${position.endLine}) is out of range.` +
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"` `(total code lines: ${totalLines}`);
+ `(total code lines: ${totalLines}).`, }
); }
}
} }
function getChangedScripts( function getChangedScripts(
oldScripts: ReadonlyArray<SelectedScript>, oldScripts: ReadonlyArray<SelectedScript>,
newScripts: ReadonlyArray<SelectedScript>, newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
): ReadonlyArray<IScript> { return newScripts
return newScripts .filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id && oldScript.revert !== newScript.revert ))
&& oldScript.revert !== newScript.revert)) .map((selection) => selection.script);
.map((selection) => selection.script);
} }
function selectIfNotExists( function selectIfNotExists(
selectableContainer: ReadonlyArray<SelectedScript>, selectableContainer: ReadonlyArray<SelectedScript>,
test: ReadonlyArray<SelectedScript>, test: ReadonlyArray<SelectedScript>) {
) { return selectableContainer
return selectableContainer .filter((script) => !test.find((oldScript) => oldScript.id === script.id))
.filter((script) => !test.find((oldScript) => oldScript.id === script.id)) .map((selection) => selection.script);
.map((selection) => selection.script);
} }

View File

@@ -2,10 +2,10 @@ import { IScript } from '@/domain/IScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
export interface ICodeChangedEvent { export interface ICodeChangedEvent {
readonly code: string; readonly code: string;
addedScripts: ReadonlyArray<IScript>; addedScripts: ReadonlyArray<IScript>;
removedScripts: ReadonlyArray<IScript>; removedScripts: ReadonlyArray<IScript>;
changedScripts: ReadonlyArray<IScript>; changedScripts: ReadonlyArray<IScript>;
isEmpty(): boolean; isEmpty(): boolean;
getScriptPositionInCode(script: IScript): ICodePosition; getScriptPositionInCode(script: IScript): ICodePosition;
} }

View File

@@ -1,69 +1,67 @@
import { ICodeBuilder } from './ICodeBuilder'; import { ICodeBuilder } from './ICodeBuilder';
const NewLine = '\n';
const TotalFunctionSeparatorChars = 58; const TotalFunctionSeparatorChars = 58;
export abstract class CodeBuilder implements ICodeBuilder { export abstract class CodeBuilder implements ICodeBuilder {
private readonly lines = new Array<string>(); private readonly lines = new Array<string>();
// Returns current line starting from 0 (no lines), or 1 (have single line) // Returns current line starting from 0 (no lines), or 1 (have single line)
public get currentLine(): number { public get currentLine(): number {
return this.lines.length; return this.lines.length;
}
public appendLine(code?: string): CodeBuilder {
if (!code) {
this.lines.push('');
return this;
} }
const lines = code.match(/[^\r\n]+/g);
this.lines.push(...lines);
return this;
}
public appendTrailingHyphensCommentLine( public appendLine(code?: string): CodeBuilder {
totalRepeatHyphens: number = TotalFunctionSeparatorChars, if (!code) {
): CodeBuilder { this.lines.push('');
return this.appendCommentLine('-'.repeat(totalRepeatHyphens)); return this;
} }
const lines = code.match(/[^\r\n]+/g);
public appendCommentLine(commentLine?: string): CodeBuilder { for (const line of lines) {
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`); this.lines.push(line);
return this; }
} return this;
public appendFunction(name: string, code: string): CodeBuilder {
if (!name) { throw new Error('name cannot be empty or null'); }
if (!code) { throw new Error('code cannot be empty or null'); }
return this
.appendCommentLineWithHyphensAround(name)
.appendLine(this.writeStandardOut(`--- ${name}`))
.appendLine(code)
.appendTrailingHyphensCommentLine();
}
public appendCommentLineWithHyphensAround(
sectionName: string,
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
): CodeBuilder {
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
if (sectionName.length >= totalRepeatHyphens) {
return this.appendCommentLine(sectionName);
} }
const firstHyphens = '-'.repeat(Math.floor((totalRepeatHyphens - sectionName.length) / 2));
const secondHyphens = '-'.repeat(Math.ceil((totalRepeatHyphens - sectionName.length) / 2));
return this
.appendTrailingHyphensCommentLine()
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
.appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars);
}
public toString(): string { public appendTrailingHyphensCommentLine(
return this.lines.join(this.getNewLineTerminator()); totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
} return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
}
protected abstract getCommentDelimiter(): string; public appendCommentLine(commentLine?: string): CodeBuilder {
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
return this;
}
protected abstract writeStandardOut(text: string): string; public appendFunction(name: string, code: string): CodeBuilder {
if (!name) { throw new Error('name cannot be empty or null'); }
if (!code) { throw new Error('code cannot be empty or null'); }
return this
.appendCommentLineWithHyphensAround(name)
.appendLine(this.writeStandardOut(`--- ${name}`))
.appendLine(code)
.appendTrailingHyphensCommentLine();
}
protected abstract getNewLineTerminator(): string; public appendCommentLineWithHyphensAround(
sectionName: string,
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
if (sectionName.length >= totalRepeatHyphens) {
return this.appendCommentLine(sectionName);
}
const firstHyphens = '-'.repeat(Math.floor((totalRepeatHyphens - sectionName.length) / 2));
const secondHyphens = '-'.repeat(Math.ceil((totalRepeatHyphens - sectionName.length) / 2));
return this
.appendTrailingHyphensCommentLine()
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
.appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars);
}
public toString(): string {
return this.lines.join(NewLine);
}
protected abstract getCommentDelimiter(): string;
protected abstract writeStandardOut(text: string): string;
} }

View File

@@ -5,12 +5,10 @@ import { BatchBuilder } from './Languages/BatchBuilder';
import { ShellBuilder } from './Languages/ShellBuilder'; import { ShellBuilder } from './Languages/ShellBuilder';
import { ICodeBuilderFactory } from './ICodeBuilderFactory'; import { ICodeBuilderFactory } from './ICodeBuilderFactory';
export class CodeBuilderFactory export class CodeBuilderFactory extends ScriptingLanguageFactory<ICodeBuilder> implements ICodeBuilderFactory {
extends ScriptingLanguageFactory<ICodeBuilder> constructor() {
implements ICodeBuilderFactory { super();
constructor() { this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
super(); this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder()); }
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
}
} }

View File

@@ -1,9 +1,9 @@
export interface ICodeBuilder { export interface ICodeBuilder {
currentLine: number; currentLine: number;
appendLine(code?: string): ICodeBuilder; appendLine(code?: string): ICodeBuilder;
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder; appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
appendCommentLine(commentLine?: string): ICodeBuilder; appendCommentLine(commentLine?: string): ICodeBuilder;
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder; appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
appendFunction(name: string, code: string): ICodeBuilder; appendFunction(name: string, code: string): ICodeBuilder;
toString(): string; toString(): string;
} }

View File

@@ -1,4 +1,5 @@
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
import { ICodeBuilder } from './ICodeBuilder'; import { ICodeBuilder } from './ICodeBuilder';
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
export type ICodeBuilderFactory = IScriptingLanguageFactory<ICodeBuilder>; export interface ICodeBuilderFactory extends IScriptingLanguageFactory<ICodeBuilder> {
}

View File

@@ -2,6 +2,6 @@ import { SelectedScript } from '@/application/Context/State/Selection/SelectedSc
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
export interface IUserScript { export interface IUserScript {
code: string; code: string;
scriptPositions: Map<SelectedScript, ICodePosition>; scriptPositions: Map<SelectedScript, ICodePosition>;
} }

View File

@@ -1,9 +1,9 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { IUserScript } from './IUserScript'; import { IUserScript } from './IUserScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
export interface IUserScriptGenerator { export interface IUserScriptGenerator {
buildCode( buildCode(
selectedScripts: ReadonlyArray<SelectedScript>, selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition): IUserScript; scriptingDefinition: IScriptingDefinition): IUserScript;
} }

View File

@@ -1,21 +1,16 @@
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder'; import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
export class BatchBuilder extends CodeBuilder { export class BatchBuilder extends CodeBuilder {
protected getCommentDelimiter(): string { protected getCommentDelimiter(): string {
return '::'; return '::';
} }
protected writeStandardOut(text: string): string {
protected writeStandardOut(text: string): string { return `echo ${escapeForEcho(text)}`;
return `echo ${escapeForEcho(text)}`; }
}
protected getNewLineTerminator(): string {
return '\r\n';
}
} }
function escapeForEcho(text: string) { function escapeForEcho(text: string) {
return text return text
.replace(/&/g, '^&') .replace(/&/g, '^&')
.replace(/%/g, '%%'); .replace(/%/g, '%%');
} }

View File

@@ -1,20 +1,15 @@
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder'; import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
export class ShellBuilder extends CodeBuilder { export class ShellBuilder extends CodeBuilder {
protected getCommentDelimiter(): string { protected getCommentDelimiter(): string {
return '#'; return '#';
} }
protected writeStandardOut(text: string): string {
protected writeStandardOut(text: string): string { return `echo '${escapeForEcho(text)}'`;
return `echo '${escapeForEcho(text)}'`; }
}
protected getNewLineTerminator(): string {
return '\n';
}
} }
function escapeForEcho(text: string) { function escapeForEcho(text: string) {
return text return text
.replace(/'/g, '\'\\\'\''); .replace(/'/g, '\'\\\'\'');
} }

View File

@@ -1,75 +1,71 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { CodePosition } from '../Position/CodePosition';
import { IUserScriptGenerator } from './IUserScriptGenerator'; import { IUserScriptGenerator } from './IUserScriptGenerator';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { CodePosition } from '../Position/CodePosition';
import { IUserScript } from './IUserScript'; import { IUserScript } from './IUserScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ICodeBuilder } from './ICodeBuilder'; import { ICodeBuilder } from './ICodeBuilder';
import { ICodeBuilderFactory } from './ICodeBuilderFactory'; import { ICodeBuilderFactory } from './ICodeBuilderFactory';
import { CodeBuilderFactory } from './CodeBuilderFactory'; import { CodeBuilderFactory } from './CodeBuilderFactory';
export class UserScriptGenerator implements IUserScriptGenerator { export class UserScriptGenerator implements IUserScriptGenerator {
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) { constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
}
public buildCode(
selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition,
): IUserScript {
if (!selectedScripts) { throw new Error('missing scripts'); }
if (!scriptingDefinition) { throw new Error('missing definition'); }
if (!selectedScripts.length) {
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
} }
let builder = this.codeBuilderFactory.create(scriptingDefinition.language); public buildCode(
builder = initializeCode(scriptingDefinition.startCode, builder); selectedScripts: ReadonlyArray<SelectedScript>,
const scriptPositions = selectedScripts.reduce((result, selection) => { scriptingDefinition: IScriptingDefinition): IUserScript {
return appendSelection(selection, result, builder); if (!selectedScripts) { throw new Error('undefined scripts'); }
}, new Map<SelectedScript, ICodePosition>()); if (!scriptingDefinition) { throw new Error('undefined definition'); }
const code = finalizeCode(builder, scriptingDefinition.endCode); let scriptPositions = new Map<SelectedScript, ICodePosition>();
return { code, scriptPositions }; if (!selectedScripts.length) {
} return { code: '', scriptPositions };
}
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
builder = initializeCode(scriptingDefinition.startCode, builder);
for (const selection of selectedScripts) {
scriptPositions = appendSelection(selection, scriptPositions, builder);
}
const code = finalizeCode(builder, scriptingDefinition.endCode);
return { code, scriptPositions };
}
} }
function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder { function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder {
if (!startCode) { if (!startCode) {
return builder; return builder;
} }
return builder return builder
.appendLine(startCode) .appendLine(startCode)
.appendLine(); .appendLine();
} }
function finalizeCode(builder: ICodeBuilder, endCode: string): string { function finalizeCode(builder: ICodeBuilder, endCode: string): string {
if (!endCode) { if (!endCode) {
return builder.toString(); return builder.toString();
} }
return builder.appendLine() return builder.appendLine()
.appendLine(endCode) .appendLine(endCode)
.toString(); .toString();
} }
function appendSelection( function appendSelection(
selection: SelectedScript, selection: SelectedScript,
scriptPositions: Map<SelectedScript, ICodePosition>, scriptPositions: Map<SelectedScript, ICodePosition>,
builder: ICodeBuilder, builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
): Map<SelectedScript, ICodePosition> { const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts
// Start from next line because first line will be empty to separate scripts builder = appendCode(selection, builder);
const startPosition = builder.currentLine + 1; const endPosition = builder.currentLine - 1;
appendCode(selection, builder); builder.appendLine();
const endPosition = builder.currentLine - 1; const position = new CodePosition(startPosition, endPosition);
builder.appendLine(); scriptPositions.set(selection, position);
const position = new CodePosition(startPosition, endPosition); return scriptPositions;
scriptPositions.set(selection, position);
return scriptPositions;
} }
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder { function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
const { script } = selection; const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
const name = selection.revert ? `${script.name} (revert)` : script.name; const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
const scriptCode = selection.revert ? script.code.revert : script.code.execute; return builder
return builder .appendLine()
.appendLine() .appendFunction(name, scriptCode);
.appendFunction(name, scriptCode);
} }

View File

@@ -1,7 +1,7 @@
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { IEventSource } from '@/infrastructure/Events/IEventSource';
export interface IApplicationCode { export interface IApplicationCode {
readonly changed: IEventSource<ICodeChangedEvent>; readonly changed: IEventSource<ICodeChangedEvent>;
readonly current: string; readonly current: string;
} }

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