Compare commits

..

2 Commits

Author SHA1 Message Date
undergroundwires
493fb1ec16 mac: add new scripts and category for services 2023-10-19 01:21:03 +02:00
undergroundwires
b167a69976 Fix YAML error for site release in CI/CD
Fix the syntax error in the GitHub action script that was caused by
improper multi-line YAML notation. This correction ensures the action
can successfully parse and execute.
2023-10-19 00:45:23 +02:00
534 changed files with 11605 additions and 22009 deletions

View File

@@ -10,7 +10,7 @@ module.exports = {
}, },
extends: [ extends: [
// Vue specific rules, eslint-plugin-vue // Vue specific rules, eslint-plugin-vue
'plugin:vue/vue3-recommended', 'plugin:vue/essential',
// Extends eslint-config-airbnb // Extends eslint-config-airbnb
'@vue/eslint-config-airbnb-with-typescript', '@vue/eslint-config-airbnb-with-typescript',

View File

@@ -24,41 +24,3 @@ jobs:
- -
name: Run e2e tests name: Run e2e tests
run: npm run test:cy:run run: npm run test:cy:run
-
name: Output artifact directories
id: artifacts
if: always() # Run even if previous steps fail because test run video is always captured
shell: bash
run: |-
declare -r dirs_json_file='cypress-dirs.json'
if [ ! -f "${dirs_json_file}" ]; then
echo "${dirs_json_file} does not exist"
exit 1
fi
SCREENSHOTS_DIR=$(jq -r '.screenshots' "${dirs_json_file}")
VIDEOS_DIR=$(jq -r '.videos' "${dirs_json_file}")
for dir in "${SCREENSHOTS_DIR}" "${VIDEOS_DIR}"; do
if [ "${dir}" = 'null' ] || [ -z "${dir}" ]; then
echo "One or more directories are null or not specified in cypress-dirs.json"
exit 1
fi
done
echo "SCREENSHOTS_DIR=${SCREENSHOTS_DIR}" >> "${GITHUB_OUTPUT}"
echo "VIDEOS_DIR=${VIDEOS_DIR}" >> "${GITHUB_OUTPUT}"
-
name: Upload screenshots
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
uses: actions/upload-artifact@v3
with:
name: e2e-screenshots-${{ matrix.os }}
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
-
name: Upload videos
if: always() # Run even if previous steps fail because test run video is always captured
uses: actions/upload-artifact@v3
with:
name: e2e-videos-${{ matrix.os }}
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}

15
.gitignore vendored
View File

@@ -1,16 +1,5 @@
# Application build artifacts
/dist-*/
# npm
node_modules node_modules
/dist-*/
# Visual Studio Code .vs
.vscode/**/* .vscode/**/*
!.vscode/extensions.json !.vscode/extensions.json
# draw.io
*.bkp
*.dtmp
# macOS
.DS_Store

View File

@@ -1,63 +1,5 @@
# Changelog # Changelog
## 0.12.8 (2023-11-27)
* Remove duplicated `index.html` file | [aab0f7e](https://github.com/undergroundwires/privacy.sexy/commit/aab0f7ea4680f377c610066bd0e99011eed8b506)
* Refactor DI for simplicity and type safety | [7770a9b](https://github.com/undergroundwires/privacy.sexy/commit/7770a9b5211d7208cfb2bfa5f737d46dc90b7946)
* Refactor user selection state handling using hook | [58cd551](https://github.com/undergroundwires/privacy.sexy/commit/58cd551a304a03e42637e6858982f8c5dfd9f598)
* Refactor watch sources for reliability | [7ab16ec](https://github.com/undergroundwires/privacy.sexy/commit/7ab16ecccb31b2d54e5b634520a8246fbbc248c1)
* Refactor to enforce strictNullChecks | [949fac1](https://github.com/undergroundwires/privacy.sexy/commit/949fac1a7cbc962ed63058e6a896695cfb4d35c8)
* Fix icon tooltip alignment on instructions modal | [bd383ed](https://github.com/undergroundwires/privacy.sexy/commit/bd383ed273ca95c10ea1cce765c0aa6836ec508c)
* Fix mobile layout overflow caused by tooltips | [e541a35](https://github.com/undergroundwires/privacy.sexy/commit/e541a35e86c0eff83f84dd002b46de7c55ebbcac)
* win: improve disabling of scheduled tasks | [3864f04](https://github.com/undergroundwires/privacy.sexy/commit/3864f042180f62afe469fdfe36010b018f84f4b3)
* Fix card list UI layout shifts (jumps) on load | [bf3426f](https://github.com/undergroundwires/privacy.sexy/commit/bf3426f91b6b7dbcad58d58507222559a8d14242)
* Refactor to Vue 3 recommended ESLint rules | [4531645](https://github.com/undergroundwires/privacy.sexy/commit/4531645b4c0c5143f15240652368bb9b9ddb48a4)
* Fix code highlighting and optimize category select | [cb42f11](https://github.com/undergroundwires/privacy.sexy/commit/cb42f11b9785e74719338a0a80a50d81dfccb4b6)
* Fix layout jumps/shifts and overflow on modals | [e299d40](https://github.com/undergroundwires/privacy.sexy/commit/e299d40fa1d71d921d4dac37e469fe299c9da3af)
* win: fix and improve Store app categorization #190 | [094dbb0](https://github.com/undergroundwires/privacy.sexy/commit/094dbb01b83bce9925fafab778b922f64390c2be)
* win: fix persistent update disabling /w tasks #272 | [dee3279](https://github.com/undergroundwires/privacy.sexy/commit/dee3279f85c99a9c62201a093b1afa41ec2412ec)
* win: discourage IntelliCode disabling #267, #286 | [7f7a84e](https://github.com/undergroundwires/privacy.sexy/commit/7f7a84e3ba259fade22d4838563d16129a1585e6)
* Fix spacing in documentation for readability | [1442f62](https://github.com/undergroundwires/privacy.sexy/commit/1442f626335e30e3a8d74e4e13e561c41f073ef8)
* win: fix system app removal affecting updates #287 | [7c632f7](https://github.com/undergroundwires/privacy.sexy/commit/7c632f738853b32fd90952bb4ca1ac924f962eb0)
* Fix rendering of inline code blocks for docs | [9845a7c](https://github.com/undergroundwires/privacy.sexy/commit/9845a7cd68a9920c96da739b58238bb1fdb1251d)
* linux: fix Firefox settings not reverting #282 | [bcad357](https://github.com/undergroundwires/privacy.sexy/commit/bcad357017d9f29ce77e706ca943107dd9caefb6)
* Fix incorrect URL rendering in documentation texts | [d328f08](https://github.com/undergroundwires/privacy.sexy/commit/d328f0895244d998e885ad8df335b6444b9ac66b)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.7...0.12.8)
## 0.12.7 (2023-11-07)
* Add winget download instructions | [b2ffc90](https://github.com/undergroundwires/privacy.sexy/commit/b2ffc90da70367b9e65c82556e8f440f865ceb98)
* Fix unresponsive copy button on instructions modal | [8ccaec7](https://github.com/undergroundwires/privacy.sexy/commit/8ccaec7af6ea3ecfd46bab5c13b90f71d55e32c1)
* Fix tree node check states not being updated | [af7219f](https://github.com/undergroundwires/privacy.sexy/commit/af7219f6e12ab4a65ce07190f691cf3234e87e35)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.6...0.12.7)
## 0.12.6 (2023-11-03)
* Bump dependencies to latest | [25d7f7b](https://github.com/undergroundwires/privacy.sexy/commit/25d7f7b2a479e51e092881cc2751e67a7d3f179f)
* win: improve system app uninstall cleanup #73 | [dbe3c5c](https://github.com/undergroundwires/privacy.sexy/commit/dbe3c5cfb91ba8a1657838b69117858843c8fbc8)
* win: improve system app uninstall /w fallback #260 | [98a26f9](https://github.com/undergroundwires/privacy.sexy/commit/98a26f9ae47af2668aa53f39d1768983036048ce)
* Improve performance of rendering during search | [79b46bf](https://github.com/undergroundwires/privacy.sexy/commit/79b46bf21004d96d31551439e5db5d698a3f71f3)
* Fix YAML error for site release in CI/CD | [237d994](https://github.com/undergroundwires/privacy.sexy/commit/237d9944f900f5172366868d75219224ff0542b0)
* win: fix Microsoft Advertising app removal #200 | [e40b9a3](https://github.com/undergroundwires/privacy.sexy/commit/e40b9a3cf53c341f2e84023a9f0e9680ac08f3fa)
* win: improve directory cleanup security | [060e789](https://github.com/undergroundwires/privacy.sexy/commit/060e7896624309aebd25e8b190c127282de177e8)
* Centralize Electron entry file path configuration | [d6da406](https://github.com/undergroundwires/privacy.sexy/commit/d6da406c61e5b9f5408851d1302d6d7398157a2e)
* win: prevent updates from reinstalling apps #260 | [8570b02](https://github.com/undergroundwires/privacy.sexy/commit/8570b02dde14ffad64863f614682c3fc1f87b6c2)
* win: improve script environment robustness #221 | [dfd4451](https://github.com/undergroundwires/privacy.sexy/commit/dfd44515613f38abe5a806bda36f44e7b715b50b)
* Fix compiler failing with nested `with` expression | [80821fc](https://github.com/undergroundwires/privacy.sexy/commit/80821fca0769e5fd2c6338918fbdcea12fbe83d2)
* win: improve soft file/app delete security #260 | [f4a74f0](https://github.com/undergroundwires/privacy.sexy/commit/f4a74f058db9b5bcbcbe438785db5ec88ecc1657)
* Fix incorrect tooltip position after window resize | [f8e5f1a](https://github.com/undergroundwires/privacy.sexy/commit/f8e5f1a5a2afa1f18567e6d965359b6a1f082367)
* linux: fix string formatting of Firefox configs | [e775d68](https://github.com/undergroundwires/privacy.sexy/commit/e775d68a9b4a5f9e893ff0e3500dade036185193)
* win: improve file delete | [e72c1c1](https://github.com/undergroundwires/privacy.sexy/commit/e72c1c13ea2d73ebfc7a8da5a21254fdfc0e5b59)
* win: change system app removal to hard delete #260 | [77123d8](https://github.com/undergroundwires/privacy.sexy/commit/77123d8c929d23676a9cb21d7b697703fd1b6e82)
* Improve UI performance by optimizing reactivity | [4995e49](https://github.com/undergroundwires/privacy.sexy/commit/4995e49c469211404dac9fcb79b75eb121f80bce)
* Migrate to Vue 3.0 #230 | [ca81f68](https://github.com/undergroundwires/privacy.sexy/commit/ca81f68ff1c3bbe5b22981096ae9220b0b5851c7)
* win, linux: unify & improve Firefox clean-up #273 | [0466b86](https://github.com/undergroundwires/privacy.sexy/commit/0466b86f1013341c966a9bbf6513990337b16598)
* win: fix store revert for multiple installs #260 | [5bb13e3](https://github.com/undergroundwires/privacy.sexy/commit/5bb13e34f8de2e2a7ba943ff72b12c0569435e62)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.5...0.12.6)
## 0.12.5 (2023-10-13) ## 0.12.5 (2023-10-13)
* Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16) * Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16)

View File

@@ -43,7 +43,6 @@ 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. 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). 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).
- 💡 You should use existing shared functions for most of the operations, like `DisableService` for disabling services, to maintain code consistency and efficiency.
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md). - 📖 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). - 📖 If you wish to use templates, use [templating.md](./docs/templating.md).

View File

@@ -122,11 +122,11 @@
## Get started ## Get started
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy). - 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.8/privacy.sexy-Setup-0.12.8.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.8/privacy.sexy-0.12.8.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.8/privacy.sexy-0.12.8.AppImage). For more options, see [here](#additional-install-options). - 🖥️ **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).
For a detailed comparison of features between the desktop and web versions of privacy.sexy, see [Desktop vs. Web Features](./docs/desktop-vs-web-features.md). Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
💡 Regularly applying your configuration with privacy.sexy is recommended, especially after each new release and major operating system updates. Each version updates scripts to enhance stability, privacy, and security. 💡 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.
[![privacy.sexy application](img/screenshot.png?raw=true )](https://privacy.sexy) [![privacy.sexy application](img/screenshot.png?raw=true )](https://privacy.sexy)
@@ -153,22 +153,13 @@ For a detailed comparison of features between the desktop and web versions of pr
## Additional Install Options ## Additional Install Options
- Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions. - Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions.
- Other unofficial channels (not maintained by privacy.sexy) for Windows include: - Using [Scoop](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) package manager on Windows:
- [Scoop 🥄](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) (latest version):
```powershell ```powershell
scoop bucket add extras scoop bucket add extras
scoop install privacy.sexy scoop install privacy.sexy
``` ```
- [winget 🪟](https://winget.run/pkg/undergroundwires/privacy.sexy) (may be outdated):
```powershell
winget install -e --id undergroundwires.privacy.sexy
```
With winget, updates require manual submission; the auto-update feature within privacy.sexy will notify you of new releases post-installation.
## Development ## Development
Refer to [development.md](./docs/development.md) for Docker usage and reading more about setting up your development environment. Refer to [development.md](./docs/development.md) for Docker usage and reading more about setting up your development environment.
@@ -179,6 +170,4 @@ Check [architecture.md](./docs/architecture.md) for an overview of design and ho
## Security ## Security
Security is a top priority at privacy.sexy. 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).
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,7 +1,6 @@
# Security Policy # Security Policy
Security is a top priority at privacy.sexy. 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.
Please report any discovered vulnerabilities responsibly.
## Reporting a Vulnerability ## Reporting a Vulnerability
@@ -12,45 +11,20 @@ Efforts to responsibly disclose findings are greatly appreciated. To report a se
## Security Report Handling ## Security Report Handling
Upon receiving a security report, the process involves: Upon receipt of a security report, the following actions will be taken:
- Confirming the report and identifying affected components. - The report will be confirmed, identifying the affected components.
- Assessing the impact and severity of the issue. - The impact and severity of the issue will be assessed.
- Fixing the vulnerability and planning a release to address it. - Work on a fix and plan a release to address the vulnerability will be initiated.
- Keeping the reporter informed about progress. - The reporter will be kept updated about the progress.
## Security Practices ## Testing
### Application Security 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).
privacy.sexy adopts a defense in depth strategy to protect users on multiple layers:
- **Link Protection:**
privacy.sexy ensures each external link has special attributes for your privacy and security.
These attributes block the new site from accessing the privacy.sexy page, increasing your online safety and privacy.
- **Content Security Policies (CSP):**
privacy.sexy actively follows security guidelines from the Open Web Application Security Project (OWASP) at strictest level.
This approach protects against attacks like Cross Site Scripting (XSS) and data injection.
- **Context Isolation:**
The desktop application isolates different code sections based on their access level.
This separation prevents attackers from introducing harmful code into the app, known as injection attacks.
### Update Security and Integrity
privacy.sexy benefits from automated update processes including security tests. Automated deployments from source code ensure immediate and secure updates, mirroring the latest source code. This aligns the deployed application with the expected source code, enhancing transparency and trust. For more details, see [CI/CD Documentation](./docs/ci-cd.md).
Every desktop update undergoes a thorough verification process. Updates are cryptographically signed to ensure authenticity and integrity, preventing tampered versions from reaching your device. Version checks are conducted to prevent downgrade attacks.
### Testing
privacy.sexy's testing approach includes a mix of automated and community-driven tests.
Details on testing practices are available in the [Testing Documentation](./docs/tests.md).
## Support ## Support
For help or any questions, [submit a GitHub issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose). Addressing security concerns is a priority, and we ensure the necessary 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.
Support privacy.sexy's commitment to security by [making a donation ❤️](https://github.com/sponsors/undergroundwires). Your contributions aid in maintaining and enhancing the project's security features.
--- ---

View File

@@ -1,5 +0,0 @@
{
"base": "tests/e2e",
"videos": "tests/e2e/videos",
"screenshots": "tests/e2e/videos"
}

View File

@@ -1,31 +1,18 @@
import { defineConfig } from 'cypress'; import { defineConfig } from 'cypress';
import ViteConfig from './vite.config'; import ViteConfig from './vite.config';
import cypressDirs from './cypress-dirs.json' assert { type: 'json' };
const CYPRESS_BASE_DIR = 'tests/e2e/';
export default defineConfig({ export default defineConfig({
fixturesFolder: `${cypressDirs.base}/fixtures`, fixturesFolder: `${CYPRESS_BASE_DIR}/fixtures`,
screenshotsFolder: cypressDirs.screenshots, screenshotsFolder: `${CYPRESS_BASE_DIR}/screenshots`,
video: true, video: true,
videosFolder: cypressDirs.videos, videosFolder: `${CYPRESS_BASE_DIR}/videos`,
e2e: { e2e: {
baseUrl: `http://localhost:${getApplicationPort()}/`, baseUrl: `http://localhost:${ViteConfig.server.port}/`,
specPattern: `${cypressDirs.base}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx} specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
supportFile: `${cypressDirs.base}/support/e2e.ts`, supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
}, },
/*
Disabling Chrome's web security to allow for faster DOM queries to access DOM earlier than
`cy.get()`. It bypasses the usual same-origin policy constraints
*/
chromeWebSecurity: false,
}); });
function getApplicationPort(): number {
const port = ViteConfig.server?.port;
if (port === undefined) {
throw new Error('Unknown application port');
}
return port;
}

View File

@@ -1,54 +0,0 @@
# Desktop vs. Web Features
This table highlights differences between the desktop and web versions of `privacy.sexy`.
| Feature | Desktop | Web |
| ------- | ------- | --- |
| [Usage without installation](#usage-without-installation) | 🔴 Not available | 🟢 Available |
| [Offline usage](#offline-usage) | 🟢 Available | 🟡 Partially available |
| [Auto-updates](#auto-updates) | 🟢 Available | 🟢 Available |
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
| [Script execution](#script-execution) | 🟢 Available | 🔴 Not available |
## Feature descriptions
### Usage without installation
You can use the web version directly in a browser without installation.
The desktop version requires download and installation.
> **Note for Linux users:** On Linux, privacy.sexy is available as an `AppImage`, a portable format that doesn't need traditional installation.
> This allows Linux users to use the desktop version without full installation, akin to the web version.
### Offline usage
The web version, once loaded, supports offline use.
Desktop version inherently allows offline usage.
### Auto-updates
Both the desktop and web versions of privacy.sexy provide timely access to the latest features and security improvements. The updates are automatically deployed from source code, reflecting the latest changes for enhanced security and reliability. For more details, see [CI/CD documentation](./ci-cd.md).
The desktop version ensures secure delivery through cryptographic signatures and version checks.
[Security is a top priority](./../SECURITY.md#update-security-and-integrity) at privacy.sexy.
> **Note for macOS users:** On macOS, the desktop version's auto-update process involves manual steps due to Apple's code signing costs.
> Users get notified about updates but might need to complete the installation manually.
> Consider [donating](https://github.com/sponsors/undergroundwires) to help improve this process ❤️.
### Logging
The desktop version supports logging of activities to aid in troubleshooting.
This feature is not available in the web version.
Log file locations vary by operating system:
- macOS: `$HOME/Library/Logs/privacy.sexy`
- Linux: `$HOME/.config/privacy.sexy/logs`
- Windows: `%APPDATA%\privacy.sexy\logs`
### Script execution
Direct execution of scripts is possible in the desktop version, offering a more integrated experience.
This functionality is not present in the web version due to browser limitations.

View File

@@ -11,8 +11,6 @@ The presentation layer uses an event-driven architecture for bidirectional react
## Structure ## Structure
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code. - [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code.
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
- [**`index.html`**](./../src/presentation/index.html): The `index.html` entry file, located at the root of the project as required by Vite
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins. - [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers. - [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers. - [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
@@ -22,7 +20,8 @@ The presentation layer uses an event-driven architecture for bidirectional react
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts. - [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles. - [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components. - [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.. - [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code. - [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events. - [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use. - [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
@@ -71,11 +70,10 @@ 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: 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. - **Singletons**: Shared across components, instantiated once.
- **Transients**: Factories yielding a new instance on every access. - **Transients**: Factories yielding a new instance on every access.
2. **Provide the dependency**: 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.
Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency. 3. **Inject the dependency**: Use Vue's `inject` method alongside the defined symbol to incorporate the dependency into components.
[`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies. - For singletons, invoke the factory method: `inject(symbolKey)()`.
3. **Inject the dependency**: Use `injectKey` to inject a dependency. Pass a selector function to `injectKey` that retrieves the appropriate symbol from the provided dependencies. - For transients, directly inject: `inject(symbolKey)`.
- Example usage: `injectKey((keys) => keys.useCollectionState)`;
## Shared UI components ## Shared UI components

View File

@@ -2,142 +2,79 @@
## Benefits of templating ## Benefits of templating
- **Code sharing:** Share code across scripts for consistent practices and easier maintenance. - Generating scripts by sharing code to increase best-practice usage and maintainability.
- **Script independence:** Generate self-contained scripts, eliminating the need for external code. - Creating self-contained scripts without cross-dependencies.
- **Cleaner code:** Use pipes for complex operations, resulting in more readable and streamlined code. - Use of pipes for writing cleaner code and letting pipes do dirty work.
## Expressions ## Expressions
**Syntax:** - Expressions start and end with mustaches (double brackets, `{{` and `}}`).
- E.g. `Hello {{ $name }} !`
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
- Functions enables usage of expressions.
- 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).
- 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.
Expressions are enclosed within `{{` and `}}`. ```go
Example: `Hello {{ $name }}!`.
They are a core component of templating, enhancing scripts with dynamic capabilities and functionality.
**Syntax similarity:**
The syntax shares similarities with [Go Templates ❤️](https://pkg.go.dev/text/template), but with some differences:
**Function definitions:**
You can use expressions in function definition.
Refer to [Function](./collection-files.md#function) for more details.
Example usage:
```yaml
name: GreetFunction
parameters:
- name: name
code: Hello {{ $name }}!
```
If you assign `name` the value `world`, invoking `GreetFunction` would result in `Hello world!`.
**Function arguments:**
You can also use expressions in arguments in nested function calls.
Refer to [`Function | collection-files.md`](./collection-files.md#functioncall) for more details.
Example with nested function calls:
```yaml
-
name: PrintMessageFunction
parameters:
- name: message
code: echo "{{ $message }}"
-
name: GreetUserFunction
parameters:
- name: userName
call:
name: PrintMessageFunction
parameters:
argument: 'Hello, {{ $userName }}!'
```
Here, if `userName` is `Alice`, invoking `GreetUserFunction` would execute `echo "Hello, Alice!"`.
**Nested templates:**
You can nest expressions inside expressions (also called "nested templates").
This means that an expression can output another expression where compiler will compile both.
For example, following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output:
```go
{{ with $condition }} {{ with $condition }}
echo {{ $text }} echo {{ $text }}
{{ end }} {{ end }}
``` ```
### Parameter substitution ### Parameter substitution
Parameter substitution dynamically replaces variable references with their corresponding values in the script. A simple function example:
**Example function:**
```yaml ```yaml
name: DisplayTextFunction function: EchoArgument
parameters: parameters:
- name: 'text' - name: 'argument'
code: echo {{ $text }} code: Hello {{ $argument }} !
``` ```
Invoking `DisplayTextFunction` with `text` set to `"Hello, world!"` would result in `echo "Hello, World!"`. It would print "Hello world" if it's called in a [script](./collection-files.md#script) as following:
```yaml
script: Echo script
call:
function: EchoArgument
parameters:
argument: World
```
A function can call other functions such as:
```yaml
-
function: CallerFunction
parameters:
- name: 'value'
call:
function: EchoArgument
parameters:
argument: {{ $value }}
-
function: EchoArgument
parameters:
- name: 'argument'
code: Hello {{ $argument }} !
```
### with ### with
The `with` expression enables conditional rendering and provides a context variable for simpler code. Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
**Optional block rendering:** 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:
If the provided variable is falsy (`false`, `null`, or empty), the compiler skips the enclosed block of code.
A "block" lies between the with start (`{{ with .. }}`) and end (`{{ end }}`) expressions, defining its boundaries.
Example:
```go
{{ with $optionalVariable }}
Hello
{{ end }}
```
This would display `Hello` if `$optionalVariable` is truthy.
**Parameter declaration:**
You should set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
Declare parameters used for `with` condition as optional such as:
```yaml
name: ConditionalOutputFunction
parameters:
- name: 'data'
optional: true
code: |-
{{ with $data }}
Data is: {{ . }}
{{ end }}
```
**Context variable:**
`with` statement binds its context (value of the provided parameter value) as arbitrary `.` value.
`{{ . }}` syntax gives you access to the context variable.
This is optional to use, and not required to use `with` expressions.
For example:
```go ```go
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }} {{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
``` ```
**Multiline text:** It supports multiline text inside the block. You can have something like:
It supports multiline text inside the block. You can write something like:
```go ```go
{{ with $argument }} {{ with $argument }}
@@ -146,9 +83,7 @@ It supports multiline text inside the block. You can write something like:
{{ end }} {{ end }}
``` ```
**Inner expressions:** You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
You can also embed other expressions inside its block, such as [parameter substitution](#parameter-substitution):
```go ```go
{{ with $condition }} {{ with $condition }}
@@ -156,44 +91,32 @@ You can also embed other expressions inside its block, such as [parameter substi
{{ end }} {{ end }}
``` ```
This also includes nesting `with` statements: 💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
```go Example:
{{ with $condition1 }}
Value of $condition1: {{ . }} ```yaml
{{ with $condition2 }} function: FunctionThatOutputsConditionally
Value of $condition2: {{ . }} parameters:
{{ end }} - name: 'argument'
optional: true
code: |-
{{ with $argument }}
Value is: {{ . }}
{{ end }} {{ end }}
``` ```
### Pipes ### Pipes
Pipes are functions designed for text manipulation. - Pipes are functions available for handling text.
They allow for a sequential application of operations resembling [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), also known as "chaining". - Allows stacking actions one after another also known as "chaining".
Each pipeline's output becomes the input of the following pipe. - 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.
**Pre-defined**: - You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
- ❗ Pipe names must be camelCase without any space or special characters.
Pipes are pre-defined by the system. - **Existing pipes**
You cannot create pipes in [collection files](./collection-files.md). - `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
[A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files. - `escapeDoubleQuotes`: Escapes `"` characters, allows you to use them inside double quotes (`"`).
- **Example usages**
**Compatibility:** - `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
For example:
```go
{{ with $script }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}
```
**Naming:**
❗ Pipe names must be camelCase without any space or special characters.
**Available pipes:**
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
- `escapeDoubleQuotes`: Escapes `"` characters for batch command execution, allows you to use them inside double quotes (`"`).

View File

@@ -68,23 +68,21 @@ These checks validate various qualities like runtime execution, building process
- [`./src/`](./../src/): Contains the code subject to testing. - [`./src/`](./../src/): Contains the code subject to testing.
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories. - [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests. - [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
- [`Assertions/`](./../tests/shared/Assertions/): Contains common assertion functions, prefixed with `expect`.
- [`./tests/unit/`](./../tests/unit/) - [`./tests/unit/`](./../tests/unit/)
- Stores unit test code. - Stores unit test code.
- The directory structure mirrors [`./src/`](./../src). - 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). - 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/) - [`shared/`](./../tests/unit/shared/)
- Contains shared unit test functionalities. - Contains shared unit test functionalities.
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
- [`TestCases/`](./../tests/unit/shared/TestCases/) - [`TestCases/`](./../tests/unit/shared/TestCases/)
- Shared test cases. - Shared test cases.
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix. - 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. - [`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. - [`./tests/integration/`](./../tests/integration/): Contains integration test files.
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file. - [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
- [`cypress-dirs.json`](./../cypress-dirs.json): A central definition of directories used by Cypress, designed for reuse across different configurations.
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension. - [`./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. - [`/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)* `/videos`: Asset folder for videos taken during tests.
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests. - *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single test file.
- [`/support/interactions/`](./../tests/e2e/support/interactions/): Contains reusable functions for simulating user interactions, enhancing test readability and maintainability.

View File

@@ -8,21 +8,15 @@ import distDirs from './dist-dirs.json' assert { type: 'json' };
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts'); const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts'); const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html'); const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
const ELECTRON_DIST_SUBDIRECTORIES = { const DIST_DIR = resolvePathFromProjectRoot(distDirs.electronUnbundled);
main: resolveElectronDistSubdirectory('main'),
preload: resolveElectronDistSubdirectory('preload'),
renderer: resolveElectronDistSubdirectory('renderer'),
};
process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.cjs');
export default defineConfig({ export default defineConfig({
main: getSharedElectronConfig({ main: getSharedElectronConfig({
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.main, distDirSubfolder: 'main',
entryFilePath: MAIN_ENTRY_FILE, entryFilePath: MAIN_ENTRY_FILE,
}), }),
preload: getSharedElectronConfig({ preload: getSharedElectronConfig({
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.preload, distDirSubfolder: 'preload',
entryFilePath: PRELOAD_ENTRY_FILE, entryFilePath: PRELOAD_ENTRY_FILE,
}), }),
renderer: mergeConfig( renderer: mergeConfig(
@@ -31,7 +25,7 @@ export default defineConfig({
}), }),
{ {
build: { build: {
outDir: ELECTRON_DIST_SUBDIRECTORIES.renderer, outDir: resolve(DIST_DIR, 'renderer'),
rollupOptions: { rollupOptions: {
input: { input: {
index: WEB_INDEX_HTML_PATH, index: WEB_INDEX_HTML_PATH,
@@ -48,7 +42,7 @@ function getSharedElectronConfig(options: {
}): UserConfig { }): UserConfig {
return { return {
build: { build: {
outDir: options.distDirSubfolder, outDir: resolve(DIST_DIR, options.distDirSubfolder),
lib: { lib: {
entry: options.entryFilePath, entry: options.entryFilePath,
}, },
@@ -70,11 +64,6 @@ function getSharedElectronConfig(options: {
}; };
} }
function resolvePathFromProjectRoot(pathSegment: string): string { function resolvePathFromProjectRoot(pathSegment: string) {
return resolve(__dirname, pathSegment); return resolve(__dirname, pathSegment);
} }
function resolveElectronDistSubdirectory(subDirectory: string): string {
const electronDistDir = resolvePathFromProjectRoot(distDirs.electronUnbundled);
return resolve(electronDistDir, subDirectory);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

1202
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.12.8", "version": "0.12.5",
"private": true, "private": true,
"slogan": "Now you have the choice", "slogan": "Now you have the choice",
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆", "description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
"author": "undergroundwires", "author": "undergroundwires",
"type": "module", "type": "module",
"main": "./dist-electron-unbundled/main/index.cjs",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
@@ -24,7 +25,7 @@
"electron:preview": "electron-vite preview", "electron:preview": "electron-vite preview",
"electron:prebuild": "electron-vite build", "electron:prebuild": "electron-vite build",
"electron:build": "electron-builder", "electron:build": "electron-builder",
"lint:eslint": "eslint . --max-warnings=0 --ignore-path .gitignore", "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",
@@ -37,12 +38,12 @@
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"ace-builds": "^1.30.0", "ace-builds": "^1.30.0",
"cross-fetch": "^4.0.0", "cross-fetch": "^4.0.0",
"electron-log": "^5.0.1", "electron-log": "^4.4.8",
"electron-progressbar": "^2.1.0", "electron-progressbar": "^2.1.0",
"electron-updater": "^6.1.4", "electron-updater": "^6.1.4",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"markdown-it": "^13.0.2", "markdown-it": "^13.0.2",
"vue": "^3.3.7" "vue": "^2.7.14"
}, },
"devDependencies": { "devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.0.4", "@modyfi/vite-plugin-yaml": "^1.0.4",
@@ -52,17 +53,17 @@
"@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0", "@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-legacy": "^4.1.1", "@vitejs/plugin-legacy": "^4.1.1",
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue2": "^2.2.0",
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0", "@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.3", "@vue/eslint-config-typescript": "^11.0.3",
"@vue/test-utils": "^2.4.1", "@vue/test-utils": "^1.3.6",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"cypress": "^13.3.1", "cypress": "^13.3.1",
"electron": "^27.0.0", "electron": "^27.0.0",
"electron-builder": "^24.6.4", "electron-builder": "^24.6.4",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1", "electron-icon-builder": "^2.0.1",
"electron-vite": "^1.0.28", "electron-vite": "^1.0.27",
"eslint": "^8.51.0", "eslint": "^8.51.0",
"eslint-plugin-cypress": "^2.15.1", "eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-vue": "^9.17.0",
@@ -97,11 +98,5 @@
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/undergroundwires/privacy.sexy.git" "url": "https://github.com/undergroundwires/privacy.sexy.git"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
},
"//optionalDependencies": {
"dmg-license": "Required by `electron-builder` for DMG builds on macOS, https://github.com/electron-userland/electron-builder/issues/6489, https://github.com/electron-userland/electron-builder/issues/6520"
} }
} }

View File

@@ -12,6 +12,9 @@ export class ApplicationFactory implements IApplicationFactory {
private readonly getter: AsyncLazy<IApplication>; private readonly getter: AsyncLazy<IApplication>;
protected constructor(costlyGetter: ApplicationGetterType) { protected constructor(costlyGetter: ApplicationGetterType) {
if (!costlyGetter) {
throw new Error('missing getter');
}
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter())); this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
} }

View File

@@ -1,5 +1,7 @@
// 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 (!array2) { throw new Error('missing 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);
@@ -10,6 +12,8 @@ export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
// 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 (!array2) { throw new Error('missing second array'); }
if (array1.length !== array2.length) { if (array1.length !== array2.length) {
return false; return false;
} }

View File

@@ -20,30 +20,23 @@ export abstract class CustomError extends Error {
} }
} }
interface ErrorPrototypeManipulation { export const Environment = {
getSetPrototypeOf: () => (typeof Object.setPrototypeOf | undefined);
getCaptureStackTrace: () => (typeof Error.captureStackTrace | undefined);
}
export const PlatformErrorPrototypeManipulation: ErrorPrototypeManipulation = {
getSetPrototypeOf: () => Object.setPrototypeOf, getSetPrototypeOf: () => Object.setPrototypeOf,
getCaptureStackTrace: () => Error.captureStackTrace, getCaptureStackTrace: () => Error.captureStackTrace,
}; };
function fixPrototype(target: Error, prototype: CustomError) { function fixPrototype(target: Error, prototype: CustomError) {
// This is recommended by TypeScript guidelines. // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
// Source: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget const setPrototypeOf = Environment.getSetPrototypeOf();
// Snapshots: https://web.archive.org/web/20231111234849/https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget, https://archive.ph/tr7cX#support-for-newtarget if (!functionExists(setPrototypeOf)) {
const setPrototypeOf = PlatformErrorPrototypeManipulation.getSetPrototypeOf();
if (!isFunction(setPrototypeOf)) {
return; return;
} }
setPrototypeOf(target, prototype); setPrototypeOf(target, prototype);
} }
function ensureStackTrace(target: Error) { function ensureStackTrace(target: Error) {
const captureStackTrace = PlatformErrorPrototypeManipulation.getCaptureStackTrace(); const captureStackTrace = Environment.getCaptureStackTrace();
if (!isFunction(captureStackTrace)) { if (!functionExists(captureStackTrace)) {
// captureStackTrace is only available on V8, if it's not available // 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. // modern JS engines will usually generate a stack trace on error objects when they're thrown.
return; return;
@@ -51,7 +44,7 @@ function ensureStackTrace(target: Error) {
captureStackTrace(target, target.constructor); captureStackTrace(target, target.constructor);
} }
// eslint-disable-next-line @typescript-eslint/ban-types function functionExists(func: unknown): boolean {
function isFunction(func: unknown): func is Function { // Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness
return typeof func === 'function'; return typeof func === 'function';
} }

View File

@@ -54,6 +54,9 @@ export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
value: TEnumValue, value: TEnumValue,
enumVariable: EnumVariable<T, TEnumValue>, enumVariable: EnumVariable<T, TEnumValue>,
) { ) {
if (value === undefined || value === null) {
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,6 +0,0 @@
export interface Logger {
info(...params: unknown[]): void;
warn(...params: unknown[]): void;
error(...params: unknown[]): void;
debug(...params: unknown[]): void;
}

View File

@@ -1,5 +0,0 @@
import { Logger } from '@/application/Common/Log/Logger';
export interface LoggerFactory {
readonly logger: Logger;
}

View File

@@ -9,16 +9,19 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
public create(language: ScriptingLanguage): T { public create(language: ScriptingLanguage): T {
assertInRange(language, ScriptingLanguage); assertInRange(language, ScriptingLanguage);
const getter = this.getters.get(language); if (!this.getters.has(language)) {
if (!getter) {
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`); throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
} }
const getter = this.getters.get(language);
const instance = getter(); const instance = getter();
return instance; return instance;
} }
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) { protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
assertInRange(language, ScriptingLanguage); assertInRange(language, ScriptingLanguage);
if (!getter) {
throw new Error('missing getter');
}
if (this.getters.has(language)) { if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`); throw new Error(`${ScriptingLanguage[language]} is already registered`);
} }

View File

@@ -1,27 +0,0 @@
import { PlatformTimer } from './PlatformTimer';
import { TimeoutType, Timer } from './Timer';
export function batchedDebounce<T>(
callback: (batches: readonly T[]) => void,
waitInMs: number,
timer: Timer = PlatformTimer,
): (arg: T) => void {
let lastTimeoutId: TimeoutType | undefined;
let batches: Array<T> = [];
return (arg: T) => {
batches.push(arg);
const later = () => {
callback(batches);
batches = [];
lastTimeoutId = undefined;
};
if (lastTimeoutId !== undefined) {
timer.clearTimeout(lastTimeoutId);
}
lastTimeoutId = timer.setTimeout(later, waitInMs);
};
}

View File

@@ -1,7 +0,0 @@
import { Timer } from './Timer';
export const PlatformTimer: Timer = {
setTimeout: (callback, ms) => setTimeout(callback, ms),
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
dateNow: () => Date.now(),
};

View File

@@ -1,8 +0,0 @@
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
export type TimeoutType = ReturnType<typeof setTimeout>;
export interface Timer {
setTimeout: (callback: () => void, ms: number) => TimeoutType;
clearTimeout: (timeoutId: TimeoutType) => void;
dateNow(): number;
}

View File

@@ -26,6 +26,7 @@ export class ApplicationContext implements IApplicationContext {
public readonly app: IApplication, public readonly app: IApplication,
initialContext: OperatingSystem, initialContext: OperatingSystem,
) { ) {
validateApp(app);
this.states = initializeStates(app); this.states = initializeStates(app);
this.changeContext(initialContext); this.changeContext(initialContext);
} }
@@ -35,8 +36,10 @@ export class ApplicationContext implements IApplicationContext {
if (this.currentOs === os) { if (this.currentOs === os) {
return; return;
} }
const collection = this.app.getCollection(os); this.collection = this.app.getCollection(os);
this.collection = collection; if (!this.collection) {
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
}
const event: IApplicationContextChangedEvent = { const event: IApplicationContextChangedEvent = {
newState: this.states[os], newState: this.states[os],
oldState: this.states[this.currentOs], oldState: this.states[this.currentOs],
@@ -46,6 +49,12 @@ export class ApplicationContext implements IApplicationContext {
} }
} }
function validateApp(app: IApplication) {
if (!app) {
throw new Error('missing 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) {

View File

@@ -10,23 +10,18 @@ export async function buildContext(
factory: IApplicationFactory = ApplicationFactory.Current, factory: IApplicationFactory = ApplicationFactory.Current,
environment = RuntimeEnvironment.CurrentEnvironment, environment = RuntimeEnvironment.CurrentEnvironment,
): Promise<IApplicationContext> { ): Promise<IApplicationContext> {
if (!factory) { throw new Error('missing factory'); }
if (!environment) { throw new Error('missing environment'); }
const app = await factory.getApp(); const app = await factory.getApp();
const os = getInitialOs(app, environment.os); const os = getInitialOs(app, environment.os);
return new ApplicationContext(app, os); return new ApplicationContext(app, os);
} }
function getInitialOs( function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
app: IApplication,
currentOs: OperatingSystem | undefined,
): OperatingSystem {
const supportedOsList = app.getSupportedOsList(); const supportedOsList = app.getSupportedOsList();
if (currentOs !== undefined && supportedOsList.includes(currentOs)) { if (supportedOsList.includes(currentOs)) {
return currentOs; return currentOs;
} }
return getMostSupportedOs(supportedOsList, app);
}
function getMostSupportedOs(supportedOsList: OperatingSystem[], app: IApplication) {
supportedOsList.sort((os1, os2) => { supportedOsList.sort((os1, os2) => {
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts; const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
return getPriority(os2) - getPriority(os1); return getPriority(os2) - getPriority(os1);

View File

@@ -4,48 +4,23 @@ 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';
import { UserSelection } from './Selection/UserSelection'; import { UserSelection } from './Selection/UserSelection';
import { IUserSelection } from './Selection/IUserSelection';
import { ICategoryCollectionState } from './ICategoryCollectionState'; import { ICategoryCollectionState } from './ICategoryCollectionState';
import { IApplicationCode } from './Code/IApplicationCode'; import { IApplicationCode } from './Code/IApplicationCode';
import { UserSelectionFacade } from './Selection/UserSelectionFacade';
export class CategoryCollectionState implements ICategoryCollectionState { export class CategoryCollectionState implements ICategoryCollectionState {
public readonly os: OperatingSystem; public readonly os: OperatingSystem;
public readonly code: IApplicationCode; public readonly code: IApplicationCode;
public readonly selection: UserSelection; public readonly selection: IUserSelection;
public readonly filter: IUserFilter; public readonly filter: IUserFilter;
public constructor( public constructor(readonly collection: ICategoryCollection) {
public readonly collection: ICategoryCollection, this.selection = new UserSelection(collection, []);
selectionFactory = DefaultSelectionFactory, this.code = new ApplicationCode(this.selection, collection.scripting);
codeFactory = DefaultCodeFactory, this.filter = new UserFilter(collection);
filterFactory = DefaultFilterFactory,
) {
this.selection = selectionFactory(collection, []);
this.code = codeFactory(this.selection.scripts, collection.scripting);
this.filter = filterFactory(collection);
this.os = collection.os; this.os = collection.os;
} }
} }
export type CodeFactory = (
...params: ConstructorParameters<typeof ApplicationCode>
) => IApplicationCode;
const DefaultCodeFactory: CodeFactory = (...params) => new ApplicationCode(...params);
export type SelectionFactory = (
...params: ConstructorParameters<typeof UserSelectionFacade>
) => UserSelection;
const DefaultSelectionFactory: SelectionFactory = (
...params
) => new UserSelectionFacade(...params);
export type FilterFactory = (
...params: ConstructorParameters<typeof UserFilter>
) => IUserFilter;
const DefaultFilterFactory: FilterFactory = (...params) => new UserFilter(...params);

View File

@@ -1,7 +1,7 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { ReadonlyScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
import { CodeChangedEvent } from './Event/CodeChangedEvent'; import { CodeChangedEvent } from './Event/CodeChangedEvent';
import { CodePosition } from './Position/CodePosition'; import { CodePosition } from './Position/CodePosition';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
@@ -17,12 +17,15 @@ export class ApplicationCode implements IApplicationCode {
private scriptPositions = new Map<SelectedScript, CodePosition>(); private scriptPositions = new Map<SelectedScript, CodePosition>();
constructor( constructor(
selection: ReadonlyScriptSelection, userSelection: IReadOnlyUserSelection,
private readonly scriptingDefinition: IScriptingDefinition, private readonly scriptingDefinition: IScriptingDefinition,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(), private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
) { ) {
this.setCode(selection.selectedScripts); if (!userSelection) { throw new Error('missing userSelection'); }
selection.changed.on((scripts) => { if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); }
if (!generator) { throw new Error('missing generator'); }
this.setCode(userSelection.selectedScripts);
userSelection.changed.on((scripts) => {
this.setCode(scripts); this.setCode(scripts);
}); });
} }

View File

@@ -1,6 +1,6 @@
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 '@/application/Context/State/Selection/Script/SelectedScript'; import { SelectedScript } from '../../Selection/SelectedScript';
import { ICodeChangedEvent } from './ICodeChangedEvent'; import { ICodeChangedEvent } from './ICodeChangedEvent';
export class CodeChangedEvent implements ICodeChangedEvent { export class CodeChangedEvent implements ICodeChangedEvent {
@@ -36,18 +36,7 @@ export class CodeChangedEvent implements ICodeChangedEvent {
} }
public getScriptPositionInCode(script: IScript): ICodePosition { public getScriptPositionInCode(script: IScript): ICodePosition {
return this.getPositionById(script.id); return this.scripts.get(script);
}
private getPositionById(scriptId: string): ICodePosition {
const position = [...this.scripts.entries()]
.filter(([s]) => s.id === scriptId)
.map(([, pos]) => pos)
.at(0);
if (!position) {
throw new Error('Unknown script: Position could not be found for the script');
}
return position;
} }
} }

View File

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

View File

@@ -16,9 +16,7 @@ export abstract class CodeBuilder implements ICodeBuilder {
return this; return this;
} }
const lines = code.match(/[^\r\n]+/g); const lines = code.match(/[^\r\n]+/g);
if (lines) {
this.lines.push(...lines); this.lines.push(...lines);
}
return this; return this;
} }

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { CodePosition } from '../Position/CodePosition'; import { CodePosition } from '../Position/CodePosition';
import { IUserScriptGenerator } from './IUserScriptGenerator'; import { IUserScriptGenerator } from './IUserScriptGenerator';
import { IUserScript } from './IUserScript'; import { IUserScript } from './IUserScript';
@@ -17,6 +17,8 @@ export class UserScriptGenerator implements IUserScriptGenerator {
selectedScripts: ReadonlyArray<SelectedScript>, selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition, scriptingDefinition: IScriptingDefinition,
): IUserScript { ): IUserScript {
if (!selectedScripts) { throw new Error('missing scripts'); }
if (!scriptingDefinition) { throw new Error('missing definition'); }
if (!selectedScripts.length) { if (!selectedScripts.length) {
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() }; return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
} }
@@ -66,19 +68,8 @@ function appendSelection(
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder { function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
const { script } = selection; const { script } = selection;
const name = selection.revert ? `${script.name} (revert)` : script.name; const name = selection.revert ? `${script.name} (revert)` : script.name;
const scriptCode = getSelectedCode(selection); const scriptCode = selection.revert ? script.code.revert : script.code.execute;
return builder return builder
.appendLine() .appendLine()
.appendFunction(name, scriptCode); .appendFunction(name, scriptCode);
} }
function getSelectedCode(selection: SelectedScript): string {
const { code } = selection.script;
if (!selection.revert) {
return code.execute;
}
if (!code.revert) {
throw new Error('Reverted script lacks revert code.');
}
return code.revert;
}

View File

@@ -1,37 +1,37 @@
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { FilterActionType } from './FilterActionType'; import { FilterActionType } from './FilterActionType';
import { import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from './IFilterChangeDetails';
IFilterChangeDetails, IFilterChangeDetailsVisitor,
ApplyFilterAction, ClearFilterAction,
} from './IFilterChangeDetails';
export class FilterChange implements IFilterChangeDetails { export class FilterChange implements IFilterChangeDetails {
public static forApply( public static forApply(filter: IFilterResult) {
filter: IFilterResult, if (!filter) {
): IFilterChangeDetails { throw new Error('missing filter');
return new FilterChange({ type: FilterActionType.Apply, filter }); }
return new FilterChange(FilterActionType.Apply, filter);
} }
public static forClear(): IFilterChangeDetails { public static forClear() {
return new FilterChange({ type: FilterActionType.Clear }); return new FilterChange(FilterActionType.Clear);
} }
private constructor(public readonly action: ApplyFilterAction | ClearFilterAction) { } private constructor(
public readonly actionType: FilterActionType,
public readonly filter?: IFilterResult,
) { }
public visit(visitor: IFilterChangeDetailsVisitor): void { public visit(visitor: IFilterChangeDetailsVisitor): void {
switch (this.action.type) { if (!visitor) {
case FilterActionType.Apply: throw new Error('missing visitor');
if (visitor.onApply) {
visitor.onApply(this.action.filter);
} }
switch (this.actionType) {
case FilterActionType.Apply:
visitor.onApply(this.filter);
break; break;
case FilterActionType.Clear: case FilterActionType.Clear:
if (visitor.onClear) {
visitor.onClear(); visitor.onClear();
}
break; break;
default: default:
throw new Error(`Unknown action: ${this.action}`); throw new Error(`Unknown action type: ${this.actionType}`);
} }
} }
} }

View File

@@ -2,22 +2,13 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
import { FilterActionType } from './FilterActionType'; import { FilterActionType } from './FilterActionType';
export interface IFilterChangeDetails { export interface IFilterChangeDetails {
readonly action: FilterAction; readonly actionType: FilterActionType;
readonly filter?: IFilterResult;
visit(visitor: IFilterChangeDetailsVisitor): void; visit(visitor: IFilterChangeDetailsVisitor): void;
} }
export interface IFilterChangeDetailsVisitor { export interface IFilterChangeDetailsVisitor {
readonly onClear?: () => void; onClear(): void;
readonly onApply?: (filter: IFilterResult) => void; onApply(filter: IFilterResult): void;
} }
export type ApplyFilterAction = {
readonly type: FilterActionType.Apply,
readonly filter: IFilterResult;
};
export type ClearFilterAction = {
readonly type: FilterActionType.Clear,
};
export type FilterAction = ApplyFilterAction | ClearFilterAction;

View File

@@ -9,6 +9,8 @@ export class FilterResult implements IFilterResult {
public readonly query: string, public readonly query: string,
) { ) {
if (!query) { throw new Error('Query is empty or undefined'); } if (!query) { throw new Error('Query is empty or undefined'); }
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
} }
public hasAnyMatches(): boolean { public hasAnyMatches(): boolean {

View File

@@ -1,18 +1,18 @@
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter'; import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
import { ReadonlyUserSelection, UserSelection } from './Selection/UserSelection'; import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
import { IApplicationCode } from './Code/IApplicationCode'; import { IApplicationCode } from './Code/IApplicationCode';
export interface IReadOnlyCategoryCollectionState { export interface IReadOnlyCategoryCollectionState {
readonly code: IApplicationCode; readonly code: IApplicationCode;
readonly os: OperatingSystem; readonly os: OperatingSystem;
readonly filter: IReadOnlyUserFilter; readonly filter: IReadOnlyUserFilter;
readonly selection: ReadonlyUserSelection; readonly selection: IReadOnlyUserSelection;
readonly collection: ICategoryCollection; readonly collection: ICategoryCollection;
} }
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState { export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
readonly filter: IUserFilter; readonly filter: IUserFilter;
readonly selection: UserSelection; readonly selection: IUserSelection;
} }

View File

@@ -1,11 +0,0 @@
import { ICategory } from '@/domain/ICategory';
import { CategorySelectionChangeCommand } from './CategorySelectionChange';
export interface ReadonlyCategorySelection {
areAllScriptsSelected(category: ICategory): boolean;
isAnyScriptSelected(category: ICategory): boolean;
}
export interface CategorySelection extends ReadonlyCategorySelection {
processChanges(action: CategorySelectionChangeCommand): void;
}

View File

@@ -1,15 +0,0 @@
type CategorySelectionStatus = {
readonly isSelected: true;
readonly isReverted: boolean;
} | {
readonly isSelected: false;
};
export interface CategorySelectionChange {
readonly categoryId: number;
readonly newStatus: CategorySelectionStatus;
}
export interface CategorySelectionChangeCommand {
readonly changes: readonly CategorySelectionChange[];
}

View File

@@ -1,60 +0,0 @@
import { ICategory } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { ScriptSelection } from '../Script/ScriptSelection';
import { ScriptSelectionChange } from '../Script/ScriptSelectionChange';
import { CategorySelection } from './CategorySelection';
import { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
export class ScriptToCategorySelectionMapper implements CategorySelection {
constructor(
private readonly scriptSelection: ScriptSelection,
private readonly collection: ICategoryCollection,
) {
}
public areAllScriptsSelected(category: ICategory): boolean {
const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) {
return false;
}
const scripts = category.getAllScriptsRecursively();
if (selectedScripts.length < scripts.length) {
return false;
}
return scripts.every(
(script) => selectedScripts.some((selected) => selected.id === script.id),
);
}
public isAnyScriptSelected(category: ICategory): boolean {
const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) {
return false;
}
return selectedScripts.some((s) => category.includes(s.script));
}
public processChanges(action: CategorySelectionChangeCommand): void {
const scriptChanges = action.changes.reduce((changes, change) => {
changes.push(...this.collectScriptChanges(change));
return changes;
}, new Array<ScriptSelectionChange>());
this.scriptSelection.processChanges({
changes: scriptChanges,
});
}
private collectScriptChanges(change: CategorySelectionChange): ScriptSelectionChange[] {
const category = this.collection.getCategory(change.categoryId);
const scripts = category.getAllScriptsRecursively();
const scriptsChangesInCategory = scripts
.map((script): ScriptSelectionChange => ({
scriptId: script.id,
newStatus: {
...change.newStatus,
},
}));
return scriptsChangesInCategory;
}
}

View File

@@ -0,0 +1,23 @@
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { SelectedScript } from './SelectedScript';
export interface IReadOnlyUserSelection {
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>;
isSelected(scriptId: string): boolean;
areAllSelected(category: ICategory): boolean;
isAnySelected(category: ICategory): boolean;
}
export interface IUserSelection extends IReadOnlyUserSelection {
removeAllInCategory(categoryId: number): void;
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
addSelectedScript(scriptId: string, revert: boolean): void;
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
removeSelectedScript(scriptId: string): void;
selectOnly(scripts: ReadonlyArray<IScript>): void;
selectAll(): void;
deselectAll(): void;
}

View File

@@ -1,171 +0,0 @@
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
import { ScriptSelection } from './ScriptSelection';
import { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
import { SelectedScript } from './SelectedScript';
import { UserSelectedScript } from './UserSelectedScript';
const DEBOUNCE_DELAY_IN_MS = 100;
export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeCommand>;
export class DebouncedScriptSelection implements ScriptSelection {
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: Repository<string, SelectedScript>;
public readonly processChanges: ScriptSelection['processChanges'];
constructor(
private readonly collection: ICategoryCollection,
selectedScripts: ReadonlyArray<SelectedScript>,
debounce: DebounceFunction = batchedDebounce,
) {
this.scripts = new InMemoryRepository<string, SelectedScript>();
for (const script of selectedScripts) {
this.scripts.addItem(script);
}
this.processChanges = debounce(
(batchedRequests: readonly ScriptSelectionChangeCommand[]) => {
const consolidatedChanges = batchedRequests.flatMap((request) => request.changes);
this.processScriptChanges(consolidatedChanges);
},
DEBOUNCE_DELAY_IN_MS,
);
}
public isSelected(scriptId: string): boolean {
return this.scripts.exists(scriptId);
}
public get selectedScripts(): readonly SelectedScript[] {
return this.scripts.getItems();
}
public selectAll(): void {
const scriptsToSelect = this.collection
.getAllScripts()
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new UserSelectedScript(script, false));
if (scriptsToSelect.length === 0) {
return;
}
this.processChanges({
changes: scriptsToSelect.map((script): ScriptSelectionChange => ({
scriptId: script.id,
newStatus: {
isSelected: true,
isReverted: false,
},
})),
});
}
public deselectAll(): void {
if (this.scripts.length === 0) {
return;
}
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
this.processChanges({
changes: selectedScriptIds.map((scriptId): ScriptSelectionChange => ({
scriptId,
newStatus: {
isSelected: false,
},
})),
});
}
public selectOnly(scripts: readonly IScript[]): void {
if (scripts.length === 0) {
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
}
this.processChanges({
changes: [
...getScriptIdsToBeDeselected(this.scripts, scripts)
.map((scriptId): ScriptSelectionChange => ({
scriptId,
newStatus: {
isSelected: false,
},
})),
...getScriptIdsToBeSelected(this.scripts, scripts)
.map((scriptId): ScriptSelectionChange => ({
scriptId,
newStatus: {
isSelected: true,
isReverted: false,
},
})),
],
});
}
private processScriptChanges(changes: readonly ScriptSelectionChange[]): void {
let totalChanged = 0;
for (const change of changes) {
totalChanged += this.applyChange(change);
}
if (totalChanged > 0) {
this.changed.notify(this.scripts.getItems());
}
}
private applyChange(change: ScriptSelectionChange): number {
const script = this.collection.getScript(change.scriptId);
if (change.newStatus.isSelected) {
return this.addOrUpdateScript(script.id, change.newStatus.isReverted);
}
return this.removeScript(script.id);
}
private addOrUpdateScript(scriptId: string, revert: boolean): number {
const script = this.collection.getScript(scriptId);
const selectedScript = new UserSelectedScript(script, revert);
if (!this.scripts.exists(selectedScript.id)) {
this.scripts.addItem(selectedScript);
return 1;
}
const existingSelectedScript = this.scripts.getById(selectedScript.id);
if (equals(selectedScript, existingSelectedScript)) {
return 0;
}
this.scripts.addOrUpdateItem(selectedScript);
return 1;
}
private removeScript(scriptId: string): number {
if (!this.scripts.exists(scriptId)) {
return 0;
}
this.scripts.removeItem(scriptId);
return 1;
}
}
function getScriptIdsToBeSelected(
existingItems: ReadonlyRepository<string, SelectedScript>,
desiredScripts: readonly IScript[],
): string[] {
return desiredScripts
.filter((script) => !existingItems.exists(script.id))
.map((script) => script.id);
}
function getScriptIdsToBeDeselected(
existingItems: ReadonlyRepository<string, SelectedScript>,
desiredScripts: readonly IScript[],
): string[] {
return existingItems
.getItems()
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
.map((script) => script.id);
}
function equals(a: SelectedScript, b: SelectedScript): boolean {
return a.script.equals(b.script.id) && a.revert === b.revert;
}

View File

@@ -1,17 +0,0 @@
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IScript } from '@/domain/IScript';
import { SelectedScript } from './SelectedScript';
import { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
export interface ReadonlyScriptSelection {
readonly changed: IEventSource<readonly SelectedScript[]>;
readonly selectedScripts: readonly SelectedScript[];
isSelected(scriptId: string): boolean;
}
export interface ScriptSelection extends ReadonlyScriptSelection {
selectOnly(scripts: readonly IScript[]): void;
selectAll(): void;
deselectAll(): void;
processChanges(action: ScriptSelectionChangeCommand): void;
}

View File

@@ -1,15 +0,0 @@
export type ScriptSelectionStatus = {
readonly isSelected: true;
readonly isReverted: boolean;
} | {
readonly isSelected: false;
};
export interface ScriptSelectionChange {
readonly scriptId: string;
readonly newStatus: ScriptSelectionStatus;
}
export interface ScriptSelectionChangeCommand {
readonly changes: ReadonlyArray<ScriptSelectionChange>;
}

View File

@@ -1,9 +0,0 @@
import { IEntity } from '@/infrastructure/Entity/IEntity';
import { IScript } from '@/domain/IScript';
type ScriptId = IScript['id'];
export interface SelectedScript extends IEntity<ScriptId> {
readonly script: IScript;
readonly revert: boolean;
}

View File

@@ -1,17 +1,14 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity'; import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { SelectedScript } from './SelectedScript';
type SelectedScriptId = SelectedScript['id']; export class SelectedScript extends BaseEntity<string> {
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
constructor( constructor(
public readonly script: IScript, public readonly script: IScript,
public readonly revert: boolean, public readonly revert: boolean,
) { ) {
super(script.id); super(script.id);
if (revert && !script.canRevert()) { if (revert && !script.canRevert()) {
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`); throw new Error('cannot revert an irreversible script');
} }
} }
} }

View File

@@ -1,12 +1,167 @@
import { CategorySelection, ReadonlyCategorySelection } from './Category/CategorySelection'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { ReadonlyScriptSelection, ScriptSelection } from './Script/ScriptSelection'; import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IRepository } from '@/infrastructure/Repository/IRepository';
import { ICategory } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { IUserSelection } from './IUserSelection';
import { SelectedScript } from './SelectedScript';
export interface ReadonlyUserSelection { export class UserSelection implements IUserSelection {
readonly categories: ReadonlyCategorySelection; public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
readonly scripts: ReadonlyScriptSelection;
}
export interface UserSelection extends ReadonlyUserSelection { private readonly scripts: IRepository<string, SelectedScript>;
readonly categories: CategorySelection;
readonly scripts: ScriptSelection; constructor(
private readonly collection: ICategoryCollection,
selectedScripts: ReadonlyArray<SelectedScript>,
) {
this.scripts = new InMemoryRepository<string, SelectedScript>();
for (const script of selectedScripts) {
this.scripts.addItem(script);
}
}
public areAllSelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
}
const scripts = category.getAllScriptsRecursively();
if (this.selectedScripts.length < scripts.length) {
return false;
}
return scripts.every(
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
);
}
public isAnySelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
}
return this.selectedScripts.some((s) => category.includes(s.script));
}
public removeAllInCategory(categoryId: number): void {
const category = this.collection.findCategory(categoryId);
const scriptsToRemove = category.getAllScriptsRecursively()
.filter((script) => this.scripts.exists(script.id));
if (!scriptsToRemove.length) {
return;
}
for (const script of scriptsToRemove) {
this.scripts.removeItem(script.id);
}
this.changed.notify(this.scripts.getItems());
}
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
const scriptsToAddOrUpdate = this.collection
.findCategory(categoryId)
.getAllScriptsRecursively()
.filter(
(script) => !this.scripts.exists(script.id)
|| this.scripts.getById(script.id).revert !== revert,
)
.map((script) => new SelectedScript(script, revert));
if (!scriptsToAddOrUpdate.length) {
return;
}
for (const script of scriptsToAddOrUpdate) {
this.scripts.addOrUpdateItem(script);
}
this.changed.notify(this.scripts.getItems());
}
public addSelectedScript(scriptId: string, revert: boolean): void {
const script = this.collection.findScript(scriptId);
if (!script) {
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
}
const selectedScript = new SelectedScript(script, revert);
this.scripts.addItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
const script = this.collection.findScript(scriptId);
const selectedScript = new SelectedScript(script, revert);
this.scripts.addOrUpdateItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
public removeSelectedScript(scriptId: string): void {
this.scripts.removeItem(scriptId);
this.changed.notify(this.scripts.getItems());
}
public isSelected(scriptId: string): boolean {
return this.scripts.exists(scriptId);
}
/** Get users scripts based on his/her selections */
public get selectedScripts(): ReadonlyArray<SelectedScript> {
return this.scripts.getItems();
}
public selectAll(): void {
const scriptsToSelect = this.collection
.getAllScripts()
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new SelectedScript(script, false));
if (scriptsToSelect.length === 0) {
return;
}
for (const script of scriptsToSelect) {
this.scripts.addItem(script);
}
this.changed.notify(this.scripts.getItems());
}
public deselectAll(): void {
if (this.scripts.length === 0) {
return;
}
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
for (const scriptId of selectedScriptIds) {
this.scripts.removeItem(scriptId);
}
this.changed.notify([]);
}
public selectOnly(scripts: readonly IScript[]): void {
if (!scripts || scripts.length === 0) {
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
}
let totalChanged = 0;
totalChanged += this.unselectMissingWithoutNotifying(scripts);
totalChanged += this.selectNewWithoutNotifying(scripts);
if (totalChanged > 0) {
this.changed.notify(this.scripts.getItems());
}
}
private unselectMissingWithoutNotifying(scripts: readonly IScript[]): number {
if (this.scripts.length === 0 || scripts.length === 0) {
return 0;
}
const existingItems = this.scripts.getItems();
const missingIds = existingItems
.filter((existing) => !scripts.some((script) => existing.id === script.id))
.map((script) => script.id);
for (const id of missingIds) {
this.scripts.removeItem(id);
}
return missingIds.length;
}
private selectNewWithoutNotifying(scripts: readonly IScript[]): number {
const unselectedScripts = scripts
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new SelectedScript(script, false));
for (const newScript of unselectedScripts) {
this.scripts.addItem(newScript);
}
return unselectedScripts.length;
}
} }

View File

@@ -1,39 +0,0 @@
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategorySelection } from './Category/CategorySelection';
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
import { ScriptSelection } from './Script/ScriptSelection';
import { UserSelection } from './UserSelection';
import { SelectedScript } from './Script/SelectedScript';
export class UserSelectionFacade implements UserSelection {
public readonly categories: CategorySelection;
public readonly scripts: ScriptSelection;
constructor(
collection: ICategoryCollection,
selectedScripts: readonly SelectedScript[],
scriptsFactory = DefaultScriptsFactory,
categoriesFactory = DefaultCategoriesFactory,
) {
this.scripts = scriptsFactory(collection, selectedScripts);
this.categories = categoriesFactory(this.scripts, collection);
}
}
export type ScriptsFactory = (
...params: ConstructorParameters<typeof DebouncedScriptSelection>
) => ScriptSelection;
const DefaultScriptsFactory: ScriptsFactory = (
...params
) => new DebouncedScriptSelection(...params);
export type CategoriesFactory = (
...params: ConstructorParameters<typeof ScriptToCategorySelectionMapper>
) => CategorySelection;
const DefaultCategoriesFactory: CategoriesFactory = (
...params
) => new ScriptToCategorySelectionMapper(...params);

View File

@@ -32,7 +32,10 @@ const PreParsedCollections: readonly CollectionData [] = [
]; ];
function validateCollectionsData(collections: readonly CollectionData[]) { function validateCollectionsData(collections: readonly CollectionData[]) {
if (!collections.length) { if (!collections?.length) {
throw new Error('missing collections'); throw new Error('missing collections');
} }
if (collections.some((collection) => !collection)) {
throw new Error('missing collection provided');
}
} }

View File

@@ -28,7 +28,10 @@ export function parseCategoryCollection(
} }
function validate(content: CollectionData): void { function validate(content: CollectionData): void {
if (!content.actions.length) { if (!content) {
throw new Error('missing content');
}
if (!content.actions || content.actions.length <= 0) {
throw new Error('content does not define any action'); throw new Error('content does not define any action');
} }
} }

View File

@@ -1,5 +1,5 @@
import type { import type {
CategoryData, ScriptData, CategoryOrScriptData, CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
} from '@/application/collections/'; } from '@/application/collections/';
import { Script } from '@/domain/Script'; import { Script } from '@/domain/Script';
import { Category } from '@/domain/Category'; import { Category } from '@/domain/Category';
@@ -16,6 +16,7 @@ export function parseCategory(
context: ICategoryCollectionParseContext, context: ICategoryCollectionParseContext,
factory: CategoryFactoryType = CategoryFactory, factory: CategoryFactoryType = CategoryFactory,
): Category { ): Category {
if (!context) { throw new Error('missing context'); }
return parseCategoryRecursively({ return parseCategoryRecursively({
categoryData: category, categoryData: category,
context, context,
@@ -29,8 +30,8 @@ interface ICategoryParseContext {
readonly factory: CategoryFactoryType, readonly factory: CategoryFactoryType,
readonly parentCategory?: CategoryData, readonly parentCategory?: CategoryData,
} }
// eslint-disable-next-line consistent-return
function parseCategoryRecursively(context: ICategoryParseContext): Category | never { function parseCategoryRecursively(context: ICategoryParseContext): Category {
ensureValidCategory(context.categoryData, context.parentCategory); ensureValidCategory(context.categoryData, context.parentCategory);
const children: ICategoryChildren = { const children: ICategoryChildren = {
subCategories: new Array<Category>(), subCategories: new Array<Category>(),
@@ -54,7 +55,7 @@ function parseCategoryRecursively(context: ICategoryParseContext): Category | ne
/* scripts: */ children.subScripts, /* scripts: */ children.subScripts,
); );
} catch (err) { } catch (err) {
return new NodeValidator({ new NodeValidator({
type: NodeType.Category, type: NodeType.Category,
selfNode: context.categoryData, selfNode: context.categoryData,
parentNode: context.parentCategory, parentNode: context.parentCategory,
@@ -71,7 +72,7 @@ function ensureValidCategory(category: CategoryData, parentCategory?: CategoryDa
.assertDefined(category) .assertDefined(category)
.assertValidName(category.category) .assertValidName(category.category)
.assert( .assert(
() => category.children.length > 0, () => category.children && category.children.length > 0,
`"${category.category}" has no children.`, `"${category.category}" has no children.`,
); );
} }
@@ -93,14 +94,14 @@ function parseNode(context: INodeParseContext) {
validator.assertDefined(context.nodeData); validator.assertDefined(context.nodeData);
if (isCategory(context.nodeData)) { if (isCategory(context.nodeData)) {
const subCategory = parseCategoryRecursively({ const subCategory = parseCategoryRecursively({
categoryData: context.nodeData, categoryData: context.nodeData as CategoryData,
context: context.context, context: context.context,
factory: context.factory, factory: context.factory,
parentCategory: context.parent, parentCategory: context.parent,
}); });
context.children.subCategories.push(subCategory); context.children.subCategories.push(subCategory);
} else if (isScript(context.nodeData)) { } else if (isScript(context.nodeData)) {
const script = parseScript(context.nodeData, context.context); const script = parseScript(context.nodeData as ScriptData, context.context);
context.children.subScripts.push(script); context.children.subScripts.push(script);
} else { } else {
validator.throw('Node is neither a category or a script.'); validator.throw('Node is neither a category or a script.');
@@ -108,18 +109,19 @@ function parseNode(context: INodeParseContext) {
} }
function isScript(data: CategoryOrScriptData): data is ScriptData { function isScript(data: CategoryOrScriptData): data is ScriptData {
return hasCode(data) || hasCall(data); const holder = (data as InstructionHolder);
return hasCode(holder) || hasCall(holder);
} }
function isCategory(data: CategoryOrScriptData): data is CategoryData { function isCategory(data: CategoryOrScriptData): data is CategoryData {
return hasProperty(data, 'category'); return hasProperty(data, 'category');
} }
function hasCode(data: unknown): boolean { function hasCode(data: InstructionHolder): boolean {
return hasProperty(data, 'code'); return hasProperty(data, 'code');
} }
function hasCall(data: unknown) { function hasCall(data: InstructionHolder) {
return hasProperty(data, 'call'); return hasProperty(data, 'call');
} }

View File

@@ -1,6 +1,9 @@
import type { DocumentableData, DocumentationData } from '@/application/collections/'; import type { DocumentableData, DocumentationData } from '@/application/collections/';
export function parseDocs(documentable: DocumentableData): readonly string[] { export function parseDocs(documentable: DocumentableData): readonly string[] {
if (!documentable) {
throw new Error('missing documentable');
}
const { docs } = documentable; const { docs } = documentable;
if (!docs) { if (!docs) {
return []; return [];

View File

@@ -32,7 +32,7 @@ export class NodeValidator {
return this; return this;
} }
public throw(errorMessage: string): never { public throw(errorMessage: string) {
throw new NodeDataError(errorMessage, this.context); throw new NodeDataError(errorMessage, this.context);
} }
} }

View File

@@ -17,7 +17,8 @@ export class CategoryCollectionParseContext implements ICategoryCollectionParseC
scripting: IScriptingDefinition, scripting: IScriptingDefinition,
syntaxFactory: ISyntaxFactory = new SyntaxFactory(), syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
) { ) {
if (!scripting) { throw new Error('missing scripting'); }
this.syntax = syntaxFactory.create(scripting.language); this.syntax = syntaxFactory.create(scripting.language);
this.compiler = new ScriptCompiler(functionsData ?? [], this.syntax); this.compiler = new ScriptCompiler(functionsData, this.syntax);
} }
} }

View File

@@ -15,10 +15,19 @@ export class Expression implements IExpression {
public readonly evaluator: ExpressionEvaluator, public readonly evaluator: ExpressionEvaluator,
parameters?: IReadOnlyFunctionParameterCollection, parameters?: IReadOnlyFunctionParameterCollection,
) { ) {
if (!position) {
throw new Error('missing position');
}
if (!evaluator) {
throw new Error('missing evaluator');
}
this.parameters = parameters ?? new FunctionParameterCollection(); this.parameters = parameters ?? new FunctionParameterCollection();
} }
public evaluate(context: IExpressionEvaluationContext): string { public evaluate(context: IExpressionEvaluationContext): string {
if (!context) {
throw new Error('missing context');
}
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args); validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
const args = filterUnusedArguments(this.parameters, context.args); const args = filterUnusedArguments(this.parameters, context.args);
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler); const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);

View File

@@ -12,5 +12,8 @@ export class ExpressionEvaluationContext implements IExpressionEvaluationContext
public readonly args: IReadOnlyFunctionCallArgumentCollection, public readonly args: IReadOnlyFunctionCallArgumentCollection,
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(), public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
) { ) {
if (!args) {
throw new Error('missing args, send empty collection instead.');
}
} }
} }

View File

@@ -1,16 +0,0 @@
import { ExpressionPosition } from './ExpressionPosition';
export function createPositionFromRegexFullMatch(
match: RegExpMatchArray,
): ExpressionPosition {
const startPos = match.index;
if (startPos === undefined) {
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
}
const fullMatch = match[0];
if (!fullMatch.length) {
throw new Error(`Regex match is empty: ${JSON.stringify(match)}`);
}
const endPos = startPos + fullMatch.length;
return new ExpressionPosition(startPos, endPos);
}

View File

@@ -11,11 +11,14 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
) { } ) { }
public compileExpressions( public compileExpressions(
code: string, code: string | undefined,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,
): string { ): string {
if (!args) {
throw new Error('missing args, send empty collection instead.');
}
if (!code) { if (!code) {
return ''; return code;
} }
const context = new ExpressionEvaluationContext(args); const context = new ExpressionEvaluationContext(args);
const compiledCode = compileRecursively(code, context, this.extractor); const compiledCode = compileRecursively(code, context, this.extractor);
@@ -142,7 +145,7 @@ function ensureParamsUsedInCodeHasArgsProvided(
providedArgs: IReadOnlyFunctionCallArgumentCollection, providedArgs: IReadOnlyFunctionCallArgumentCollection,
): void { ): void {
const usedParameterNames = extractRequiredParameterNames(expressions); const usedParameterNames = extractRequiredParameterNames(expressions);
if (!usedParameterNames.length) { if (!usedParameterNames?.length) {
return; return;
} }
const notProvidedParameters = usedParameterNames const notProvidedParameters = usedParameterNames

View File

@@ -2,7 +2,6 @@ import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argume
export interface IExpressionsCompiler { export interface IExpressionsCompiler {
compileExpressions( compileExpressions(
code: string, code: string | undefined,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection): string;
): string;
} }

View File

@@ -10,9 +10,12 @@ const Parsers = [
export class CompositeExpressionParser implements IExpressionParser { export class CompositeExpressionParser implements IExpressionParser {
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) { public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
if (!leafs.length) { if (!leafs) {
throw new Error('missing leafs'); throw new Error('missing leafs');
} }
if (leafs.some((leaf) => !leaf)) {
throw new Error('missing leaf');
}
} }
public findExpressions(code: string): IExpression[] { public findExpressions(code: string): IExpression[] {

View File

@@ -14,44 +14,45 @@ export class ExpressionRegexBuilder {
.addRawRegex('\\s+'); .addRawRegex('\\s+');
} }
public captureOptionalPipeline() { public matchPipeline() {
return this return this
.addRawRegex('((?:\\|\\s*\\b[a-zA-Z]+\\b\\s*)*)'); .expectZeroOrMoreWhitespaces()
.addRawRegex('(\\|\\s*.+?)?');
} }
public captureUntilWhitespaceOrPipe() { public matchUntilFirstWhitespace() {
return this return this
.addRawRegex('([^|\\s]+)'); .addRawRegex('([^|\\s]+)');
} }
public captureMultilineAnythingExceptSurroundingWhitespaces() { public matchMultilineAnythingExceptSurroundingWhitespaces() {
return this return this
.expectOptionalWhitespaces() .expectZeroOrMoreWhitespaces()
.addRawRegex('([\\s\\S]*\\S)') .addRawRegex('([\\S\\s]+?)')
.expectOptionalWhitespaces(); .expectZeroOrMoreWhitespaces();
} }
public expectExpressionStart() { public expectExpressionStart() {
return this return this
.expectCharacters('{{') .expectCharacters('{{')
.expectOptionalWhitespaces(); .expectZeroOrMoreWhitespaces();
} }
public expectExpressionEnd() { public expectExpressionEnd() {
return this return this
.expectOptionalWhitespaces() .expectZeroOrMoreWhitespaces()
.expectCharacters('}}'); .expectCharacters('}}');
} }
public expectOptionalWhitespaces() {
return this
.addRawRegex('\\s*');
}
public buildRegExp(): RegExp { public buildRegExp(): RegExp {
return new RegExp(this.parts.join(''), 'g'); return new RegExp(this.parts.join(''), 'g');
} }
private expectZeroOrMoreWhitespaces() {
return this
.addRawRegex('\\s*');
}
private addRawRegex(regex: string) { private addRawRegex(regex: string) {
this.parts.push(regex); this.parts.push(regex);
return this; return this;

View File

@@ -1,9 +1,9 @@
import { IExpressionParser } from '../IExpressionParser'; import { IExpressionParser } from '../IExpressionParser';
import { ExpressionPosition } from '../../Expression/ExpressionPosition';
import { IExpression } from '../../Expression/IExpression'; import { IExpression } from '../../Expression/IExpression';
import { Expression, ExpressionEvaluator } from '../../Expression/Expression'; import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter'; import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection'; import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
export abstract class RegexParser implements IExpressionParser { export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp; protected abstract readonly regex: RegExp;
@@ -21,7 +21,7 @@ export abstract class RegexParser implements IExpressionParser {
const matches = code.matchAll(this.regex); const matches = code.matchAll(this.regex);
for (const match of matches) { for (const match of matches) {
const primitiveExpression = this.buildExpression(match); const primitiveExpression = this.buildExpression(match);
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code); const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
const parameters = createParameters(primitiveExpression); const parameters = createParameters(primitiveExpression);
const expression = new Expression(position, primitiveExpression.evaluator, parameters); const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression; yield expression;
@@ -37,6 +37,12 @@ export abstract class RegexParser implements IExpressionParser {
} }
} }
function createPosition(match: RegExpMatchArray): ExpressionPosition {
const startPos = match.index;
const endPos = startPos + match[0].length;
return new ExpressionPosition(startPos, endPos);
}
function createParameters( function createParameters(
expression: IPrimitiveExpression, expression: IPrimitiveExpression,
): FunctionParameterCollection { ): FunctionParameterCollection {

View File

@@ -28,7 +28,7 @@ function hasLines(text: string) {
*/ */
function inlineComments(code: string): string { function inlineComments(code: string): string {
const makeInlineComment = (comment: string) => { const makeInlineComment = (comment: string) => {
const value = comment.trim(); const value = comment?.trim();
if (!value) { if (!value) {
return '<##>'; return '<##>';
} }

View File

@@ -15,6 +15,12 @@ export class PipeFactory implements IPipeFactory {
private readonly pipes = new Map<string, IPipe>(); private readonly pipes = new Map<string, IPipe>();
constructor(pipes: readonly IPipe[] = RegisteredPipes) { constructor(pipes: readonly IPipe[] = RegisteredPipes) {
if (!pipes) {
throw new Error('missing pipes');
}
if (pipes.some((pipe) => !pipe)) {
throw new Error('missing pipe in list');
}
for (const pipe of pipes) { for (const pipe of pipes) {
this.registerPipe(pipe); this.registerPipe(pipe);
} }
@@ -22,11 +28,10 @@ export class PipeFactory implements IPipeFactory {
public get(pipeName: string): IPipe { public get(pipeName: string): IPipe {
validatePipeName(pipeName); validatePipeName(pipeName);
const pipe = this.pipes.get(pipeName); if (!this.pipes.has(pipeName)) {
if (!pipe) {
throw new Error(`Unknown pipe: "${pipeName}"`); throw new Error(`Unknown pipe: "${pipeName}"`);
} }
return pipe; return this.pipes.get(pipeName);
} }
private registerPipe(pipe: IPipe): void { private registerPipe(pipe: IPipe): void {

View File

@@ -6,9 +6,8 @@ export class ParameterSubstitutionParser extends RegexParser {
protected readonly regex = new ExpressionRegexBuilder() protected readonly regex = new ExpressionRegexBuilder()
.expectExpressionStart() .expectExpressionStart()
.expectCharacters('$') .expectCharacters('$')
.captureUntilWhitespaceOrPipe() // First capture: Parameter name .matchUntilFirstWhitespace() // First match: Parameter name
.expectOptionalWhitespaces() .matchPipeline() // Second match: Pipeline
.captureOptionalPipeline() // Second capture: Pipeline
.expectExpressionEnd() .expectExpressionEnd()
.buildRegExp(); .buildRegExp();

View File

@@ -1,220 +1,59 @@
// eslint-disable-next-line max-classes-per-file
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter'; import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { IExpression } from '../Expression/IExpression'; import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { ExpressionPosition } from '../Expression/ExpressionPosition';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
export class WithParser implements IExpressionParser { export class WithParser extends RegexParser {
public findExpressions(code: string): IExpression[] { protected readonly regex = new ExpressionRegexBuilder()
if (!code) {
throw new Error('missing code');
}
return parseWithExpressions(code);
}
}
enum WithStatementType {
Start,
End,
ContextVariable,
}
type WithStatement = {
readonly type: WithStatementType.Start;
readonly parameterName: string;
readonly position: ExpressionPosition;
} | {
readonly type: WithStatementType.End;
readonly position: ExpressionPosition;
} | {
readonly type: WithStatementType.ContextVariable;
readonly position: ExpressionPosition;
readonly pipeline: string | undefined;
};
function parseAllWithExpressions(
input: string,
): WithStatement[] {
const expressions = new Array<WithStatement>();
for (const match of input.matchAll(WithStatementStartRegEx)) {
expressions.push({
type: WithStatementType.Start,
parameterName: match[1],
position: createPositionFromRegexFullMatch(match),
});
}
for (const match of input.matchAll(WithStatementEndRegEx)) {
expressions.push({
type: WithStatementType.End,
position: createPositionFromRegexFullMatch(match),
});
}
for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) {
expressions.push({
type: WithStatementType.ContextVariable,
position: createPositionFromRegexFullMatch(match),
pipeline: match[1],
});
}
return expressions;
}
class WithStatementBuilder {
private readonly contextVariables = new Array<{
readonly positionInScope: ExpressionPosition;
readonly pipeline: string | undefined;
}>();
public addContextVariable(
absolutePosition: ExpressionPosition,
pipeline: string | undefined,
): void {
const positionInScope = new ExpressionPosition(
absolutePosition.start - this.startExpressionPosition.end,
absolutePosition.end - this.startExpressionPosition.end,
);
this.contextVariables.push({
positionInScope,
pipeline,
});
}
public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression {
const parameters = new FunctionParameterCollection();
parameters.addParameter(new FunctionParameter(this.parameterName, true));
const position = new ExpressionPosition(
this.startExpressionPosition.start,
endExpressionPosition.end,
);
const scope = input.substring(this.startExpressionPosition.end, endExpressionPosition.start);
return {
parameters,
position,
evaluate: (context) => {
const argumentValue = context.args.hasArgument(this.parameterName)
? context.args.getArgument(this.parameterName).argumentValue
: undefined;
if (!argumentValue) {
return '';
}
const substitutedScope = this.substituteContextVariables(scope, (pipeline) => {
if (!pipeline) {
return argumentValue;
}
return context.pipelineCompiler.compile(argumentValue, pipeline);
});
return substitutedScope;
},
};
}
constructor(
private readonly startExpressionPosition: ExpressionPosition,
private readonly parameterName: string,
) {
}
private substituteContextVariables(
scope: string,
substituter: (pipeline?: string) => string,
): string {
if (!this.contextVariables.length) {
return scope;
}
let substitutedScope = '';
let scopeSubstrIndex = 0;
for (const contextVariable of this.contextVariables) {
substitutedScope += scope.substring(scopeSubstrIndex, contextVariable.positionInScope.start);
substitutedScope += substituter(contextVariable.pipeline);
scopeSubstrIndex = contextVariable.positionInScope.end;
}
substitutedScope += scope.substring(scopeSubstrIndex, scope.length);
return substitutedScope;
}
}
function buildErrorContext(code: string, statements: readonly WithStatement[]): string {
const formattedStatements = statements.map((s) => `- [${s.position.start}, ${s.position.end}] ${WithStatementType[s.type]}`).join('\n');
return [
'Code:', '---', code, '---',
'nStatements:', '---', formattedStatements, '---',
].join('\n');
}
function parseWithExpressions(input: string): IExpression[] {
const allStatements = parseAllWithExpressions(input);
const sortedStatements = allStatements
.slice()
.sort((a, b) => b.position.start - a.position.start);
const expressions = new Array<IExpression>();
const builders = new Array<WithStatementBuilder>();
const throwWithContext = (message: string): never => {
throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`);
};
while (sortedStatements.length > 0) {
const statement = sortedStatements.pop();
if (!statement) {
break;
}
switch (statement.type) { // eslint-disable-line default-case
case WithStatementType.Start:
builders.push(new WithStatementBuilder(
statement.position,
statement.parameterName,
));
break;
case WithStatementType.ContextVariable:
if (builders.length === 0) {
throwWithContext('Context variable before `with` statement.');
}
builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline);
break;
case WithStatementType.End: {
const builder = builders.pop();
if (!builder) {
throwWithContext('Redundant `end` statement, missing `with`?');
break;
}
expressions.push(builder.buildExpression(statement.position, input));
break;
}
}
}
if (builders.length > 0) {
throwWithContext('Missing `end` statement, forgot `{{ end }}?');
}
return expressions;
}
const ContextVariableWithPipelineRegEx = new ExpressionRegexBuilder()
// {{ . | pipeName }}
.expectExpressionStart()
.expectCharacters('.')
.expectOptionalWhitespaces()
.captureOptionalPipeline() // First capture: pipeline
.expectExpressionEnd()
.buildRegExp();
const WithStatementStartRegEx = new ExpressionRegexBuilder()
// {{ with $parameterName }} // {{ with $parameterName }}
.expectExpressionStart() .expectExpressionStart()
.expectCharacters('with') .expectCharacters('with')
.expectOneOrMoreWhitespaces() .expectOneOrMoreWhitespaces()
.expectCharacters('$') .expectCharacters('$')
.captureUntilWhitespaceOrPipe() // First capture: parameter name .matchUntilFirstWhitespace() // First match: parameter name
.expectExpressionEnd() .expectExpressionEnd()
.expectOptionalWhitespaces() // ...
.buildRegExp(); .matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
const WithStatementEndRegEx = new ExpressionRegexBuilder()
// {{ end }} // {{ end }}
.expectOptionalWhitespaces()
.expectExpressionStart() .expectExpressionStart()
.expectCharacters('end') .expectCharacters('end')
.expectOptionalWhitespaces()
.expectExpressionEnd() .expectExpressionEnd()
.buildRegExp(); .buildRegExp();
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1];
const scopeText = match[2];
return {
parameters: [new FunctionParameter(parameterName, true)],
evaluator: (context) => {
const argumentValue = context.args.hasArgument(parameterName)
? context.args.getArgument(parameterName).argumentValue
: undefined;
if (!argumentValue) {
return '';
}
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
if (!pipeline) {
return argumentValue;
}
return context.pipelineCompiler.compile(argumentValue, pipeline);
});
},
};
}
}
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
// {{ . | pipeName }}
.expectExpressionStart()
.expectCharacters('.')
.matchPipeline() // First match: pipeline
.expectExpressionEnd()
.buildRegExp();
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
// but instead letting the pipeline compiler to fail on those.
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
return replacer(match1);
});
}

View File

@@ -5,6 +5,9 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
private readonly arguments = new Map<string, IFunctionCallArgument>(); private readonly arguments = new Map<string, IFunctionCallArgument>();
public addArgument(argument: IFunctionCallArgument): void { public addArgument(argument: IFunctionCallArgument): void {
if (!argument) {
throw new Error('missing argument');
}
if (this.hasArgument(argument.parameterName)) { if (this.hasArgument(argument.parameterName)) {
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`); throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
} }

View File

@@ -3,22 +3,18 @@ import { CodeSegmentMerger } from './CodeSegmentMerger';
export class NewlineCodeSegmentMerger implements CodeSegmentMerger { export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode { public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
if (!codeSegments.length) { if (!codeSegments?.length) {
throw new Error('missing segments'); throw new Error('missing segments');
} }
return { return {
code: joinCodeParts(codeSegments.map((f) => f.code)), code: joinCodeParts(codeSegments.map((f) => f.code)),
revertCode: joinCodeParts( revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)),
codeSegments
.map((f) => f.revertCode)
.filter((code): code is string => Boolean(code)),
),
}; };
} }
} }
function joinCodeParts(codeSegments: readonly string[]): string { function joinCodeParts(codeSegments: readonly string[]): string {
return codeSegments return codeSegments
.filter((segment) => segment.length > 0) .filter((segment) => segment?.length > 0)
.join('\n'); .join('\n');
} }

View File

@@ -21,7 +21,9 @@ export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
calls: readonly FunctionCall[], calls: readonly FunctionCall[],
functions: ISharedFunctionCollection, functions: ISharedFunctionCollection,
): CompiledCode { ): CompiledCode {
if (!calls.length) { throw new Error('missing calls'); } if (!functions) { throw new Error('missing functions'); }
if (!calls?.length) { throw new Error('missing calls'); }
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
const context: FunctionCallCompilationContext = { const context: FunctionCallCompilationContext = {
allFunctions: functions, allFunctions: functions,
rootCallSequence: calls, rootCallSequence: calls,

View File

@@ -1,6 +1,6 @@
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy'; import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
@@ -12,33 +12,19 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
} }
public canCompile(func: ISharedFunction): boolean { public canCompile(func: ISharedFunction): boolean {
return func.body.type === FunctionBodyType.Code; return func.body.code !== undefined;
} }
public compileFunction( public compileFunction(
calledFunction: ISharedFunction, calledFunction: ISharedFunction,
callToFunction: FunctionCall, callToFunction: FunctionCall,
): CompiledCode[] { ): CompiledCode[] {
if (calledFunction.body.type !== FunctionBodyType.Code) {
throw new Error([
'Unexpected function body type.',
`\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
`\tActual: "${FunctionBodyType[calledFunction.body.type]}"`,
'Function:',
`\t${JSON.stringify(callToFunction)}`,
].join('\n'));
}
const { code } = calledFunction.body; const { code } = calledFunction.body;
const { args } = callToFunction; const { args } = callToFunction;
return [ return [
{ {
code: this.expressionsCompiler.compileExpressions(code.execute, args), code: this.expressionsCompiler.compileExpressions(code.execute, args),
revertCode: (() => { revertCode: this.expressionsCompiler.compileExpressions(code.revert, args),
if (!code.revert) {
return undefined;
}
return this.expressionsCompiler.compileExpressions(code.revert, args);
})(),
}, },
]; ];
} }

View File

@@ -1,4 +1,4 @@
import { CallFunctionBody, FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
@@ -13,7 +13,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
} }
public canCompile(func: ISharedFunction): boolean { public canCompile(func: ISharedFunction): boolean {
return func.body.type === FunctionBodyType.Calls; return func.body.calls !== undefined;
} }
public compileFunction( public compileFunction(
@@ -21,7 +21,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
callToFunction: FunctionCall, callToFunction: FunctionCall,
context: FunctionCallCompilationContext, context: FunctionCallCompilationContext,
): CompiledCode[] { ): CompiledCode[] {
const nestedCalls = (calledFunction.body as CallFunctionBody).calls; const nestedCalls = calledFunction.body.calls;
return nestedCalls.map((nestedCall) => { return nestedCalls.map((nestedCall) => {
try { try {
const compiledParentCall = this.argumentCompiler const compiledParentCall = this.argumentCompiler

View File

@@ -5,6 +5,9 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument';
import { ParsedFunctionCall } from './ParsedFunctionCall'; import { ParsedFunctionCall } from './ParsedFunctionCall';
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] { export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
if (calls === undefined) {
throw new Error('missing call data');
}
const sequence = getCallSequence(calls); const sequence = getCallSequence(calls);
return sequence.map((call) => parseFunctionCall(call)); return sequence.map((call) => parseFunctionCall(call));
} }
@@ -16,21 +19,22 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
if (calls instanceof Array) { if (calls instanceof Array) {
return calls as FunctionCallData[]; return calls as FunctionCallData[];
} }
const singleCall = calls; return [calls as FunctionCallData];
return [singleCall];
} }
function parseFunctionCall(call: FunctionCallData): FunctionCall { function parseFunctionCall(call: FunctionCallData): FunctionCall {
if (!call) {
throw new Error('missing call data');
}
const callArgs = parseArgs(call.parameters); const callArgs = parseArgs(call.parameters);
return new ParsedFunctionCall(call.function, callArgs); return new ParsedFunctionCall(call.function, callArgs);
} }
function parseArgs( function parseArgs(
parameters: FunctionCallParametersData | undefined, parameters: FunctionCallParametersData,
): FunctionCallArgumentCollection { ): FunctionCallArgumentCollection {
const parametersMap = parameters ?? {}; return Object.keys(parameters || {})
return Object.keys(parametersMap) .map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
.map((parameterName) => new FunctionCallArgument(parameterName, parametersMap[parameterName]))
.reduce((args, arg) => { .reduce((args, arg) => {
args.addArgument(arg); args.addArgument(arg);
return args; return args;

View File

@@ -9,5 +9,8 @@ export class ParsedFunctionCall implements FunctionCall {
if (!functionName) { if (!functionName) {
throw new Error('missing function name in function call'); throw new Error('missing function name in function call');
} }
if (!args) {
throw new Error('missing args');
}
} }
} }

View File

@@ -4,21 +4,15 @@ import { FunctionCall } from './Call/FunctionCall';
export interface ISharedFunction { export interface ISharedFunction {
readonly name: string; readonly name: string;
readonly parameters: IReadOnlyFunctionParameterCollection; readonly parameters: IReadOnlyFunctionParameterCollection;
readonly body: SharedFunctionBody; readonly body: ISharedFunctionBody;
} }
export interface CallFunctionBody { export interface ISharedFunctionBody {
readonly type: FunctionBodyType.Calls, readonly type: FunctionBodyType;
readonly calls: readonly FunctionCall[], readonly code: IFunctionCode | undefined;
readonly calls: readonly FunctionCall[] | undefined;
} }
export interface CodeFunctionBody {
readonly type: FunctionBodyType.Code;
readonly code: IFunctionCode,
}
export type SharedFunctionBody = CallFunctionBody | CodeFunctionBody;
export enum FunctionBodyType { export enum FunctionBodyType {
Code, Code,
Calls, Calls,

View File

@@ -18,6 +18,9 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
} }
private ensureValidParameter(parameter: IFunctionParameter) { private ensureValidParameter(parameter: IFunctionParameter) {
if (!parameter) {
throw new Error('missing parameter');
}
if (this.includesName(parameter.name)) { if (this.includesName(parameter.name)) {
throw new Error(`duplicate parameter name: "${parameter.name}"`); throw new Error(`duplicate parameter name: "${parameter.name}"`);
} }

View File

@@ -1,7 +1,7 @@
import { FunctionCall } from './Call/FunctionCall'; import { FunctionCall } from './Call/FunctionCall';
import { import {
FunctionBodyType, IFunctionCode, ISharedFunction, SharedFunctionBody, FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
} from './ISharedFunction'; } from './ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
@@ -10,7 +10,7 @@ export function createCallerFunction(
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
callSequence: readonly FunctionCall[], callSequence: readonly FunctionCall[],
): ISharedFunction { ): ISharedFunction {
if (!callSequence.length) { if (!callSequence || !callSequence.length) {
throw new Error(`missing call sequence in function "${name}"`); throw new Error(`missing call sequence in function "${name}"`);
} }
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls); return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
@@ -33,7 +33,7 @@ export function createFunctionWithInlineCode(
} }
class SharedFunction implements ISharedFunction { class SharedFunction implements ISharedFunction {
public readonly body: SharedFunctionBody; public readonly body: ISharedFunctionBody;
constructor( constructor(
public readonly name: string, public readonly name: string,
@@ -42,22 +42,11 @@ class SharedFunction implements ISharedFunction {
bodyType: FunctionBodyType, bodyType: FunctionBodyType,
) { ) {
if (!name) { throw new Error('missing function name'); } if (!name) { throw new Error('missing function name'); }
if (!parameters) { throw new Error('missing parameters'); }
switch (bodyType) {
case FunctionBodyType.Code:
this.body = { this.body = {
type: FunctionBodyType.Code, type: bodyType,
code: content as IFunctionCode, code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined,
}; };
break;
case FunctionBodyType.Calls:
this.body = {
type: FunctionBodyType.Calls,
calls: content as readonly FunctionCall[],
};
break;
default:
throw new Error(`unknown body type: ${FunctionBodyType[bodyType]}`);
}
} }
} }

View File

@@ -5,6 +5,7 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
private readonly functionsByName = new Map<string, ISharedFunction>(); private readonly functionsByName = new Map<string, ISharedFunction>();
public addFunction(func: ISharedFunction): void { public addFunction(func: ISharedFunction): void {
if (!func) { throw new Error('missing function'); }
if (this.has(func.name)) { if (this.has(func.name)) {
throw new Error(`function with name ${func.name} already exists`); throw new Error(`function with name ${func.name} already exists`);
} }

View File

@@ -1,6 +1,4 @@
import type { import type { FunctionData, InstructionHolder } from '@/application/collections/';
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction,
} from '@/application/collections/';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator'; import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines'; import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
@@ -25,8 +23,9 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
functions: readonly FunctionData[], functions: readonly FunctionData[],
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
): ISharedFunctionCollection { ): ISharedFunctionCollection {
if (!syntax) { throw new Error('missing syntax'); }
const collection = new SharedFunctionCollection(); const collection = new SharedFunctionCollection();
if (!functions.length) { if (!functions || !functions.length) {
return collection; return collection;
} }
ensureValidFunctions(functions); ensureValidFunctions(functions);
@@ -56,13 +55,11 @@ function parseFunction(
} }
function validateCode( function validateCode(
data: CodeFunctionData, data: FunctionData,
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
validator: ICodeValidator, validator: ICodeValidator,
): void { ): void {
[data.code, data.revertCode] [data.code, data.revertCode].forEach(
.filter((code): code is string => Boolean(code))
.forEach(
(code) => validator.throwIfInvalid( (code) => validator.throwIfInvalid(
code, code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)], [new NoEmptyLines(), new NoDuplicatedLines(syntax)],
@@ -88,18 +85,19 @@ function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollecti
}, new FunctionParameterCollection()); }, new FunctionParameterCollection());
} }
function hasCode(data: FunctionData): data is CodeFunctionData { function hasCode(data: FunctionData): boolean {
return (data as CodeInstruction).code !== undefined; return Boolean(data.code);
} }
function hasCall(data: FunctionData): data is CallFunctionData { function hasCall(data: FunctionData): boolean {
return (data as CallInstruction).call !== undefined; return Boolean(data.call);
} }
function ensureValidFunctions(functions: readonly FunctionData[]) { function ensureValidFunctions(functions: readonly FunctionData[]) {
ensureNoUndefinedItem(functions);
ensureNoDuplicatesInFunctionNames(functions); ensureNoDuplicatesInFunctionNames(functions);
ensureEitherCallOrCodeIsDefined(functions);
ensureNoDuplicateCode(functions); ensureNoDuplicateCode(functions);
ensureEitherCallOrCodeIsDefined(functions);
ensureExpectedParametersType(functions); ensureExpectedParametersType(functions);
} }
@@ -107,7 +105,7 @@ function printList(list: readonly string[]): string {
return `"${list.join('","')}"`; return `"${list.join('","')}"`;
} }
function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) { function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
// Ensure functions do not define both call and code // Ensure functions do not define both call and code
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder)); const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
if (withBothCallAndCode.length) { if (withBothCallAndCode.length) {
@@ -134,7 +132,7 @@ function isArrayOfObjects(value: unknown): boolean {
&& value.every((item) => typeof item === 'object'); && value.every((item) => typeof item === 'object');
} }
function printNames(holders: readonly FunctionData[]) { function printNames(holders: readonly InstructionHolder[]) {
return printList(holders.map((holder) => holder.name)); return printList(holders.map((holder) => holder.name));
} }
@@ -146,19 +144,22 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
} }
} }
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
if (functions.some((func) => !func)) {
throw new Error('some functions are undefined');
}
}
function ensureNoDuplicateCode(functions: readonly FunctionData[]) { function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
const callFunctions = functions const duplicateCodes = getDuplicates(functions
.filter((func) => hasCode(func))
.map((func) => func as CodeFunctionData);
const duplicateCodes = getDuplicates(callFunctions
.map((func) => func.code) .map((func) => func.code)
.filter((code) => code)); .filter((code) => code));
if (duplicateCodes.length > 0) { if (duplicateCodes.length > 0) {
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`); throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
} }
const duplicateRevertCodes = getDuplicates(callFunctions const duplicateRevertCodes = getDuplicates(functions
.map((func) => func.revertCode) .filter((func) => func.revertCode)
.filter((code): code is string => Boolean(code))); .map((func) => func.revertCode));
if (duplicateRevertCodes.length > 0) { if (duplicateRevertCodes.length > 0) {
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`); throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
} }

View File

@@ -1,4 +1,4 @@
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/'; import type { FunctionData, ScriptData } from '@/application/collections/';
import { IScriptCode } from '@/domain/IScriptCode'; import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode'; import { ScriptCode } from '@/domain/ScriptCode';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
@@ -18,24 +18,27 @@ export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection; private readonly functions: ISharedFunctionCollection;
constructor( constructor(
functions: readonly FunctionData[], functions: readonly FunctionData[] | undefined,
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance, sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance, private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
private readonly codeValidator: ICodeValidator = CodeValidator.instance, private readonly codeValidator: ICodeValidator = CodeValidator.instance,
) { ) {
if (!syntax) { throw new Error('missing syntax'); }
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax); this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
} }
public canCompile(script: ScriptData): boolean { public canCompile(script: ScriptData): boolean {
return hasCall(script); if (!script) { throw new Error('missing script'); }
if (!script.call) {
return false;
}
return true;
} }
public compile(script: ScriptData): IScriptCode { public compile(script: ScriptData): IScriptCode {
if (!script) { throw new Error('missing script'); }
try { try {
if (!hasCall(script)) {
throw new Error('Script does include any calls.');
}
const calls = parseFunctionCalls(script.call); const calls = parseFunctionCalls(script.call);
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions); const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
validateCompiledCode(compiledCode, this.codeValidator); validateCompiledCode(compiledCode, this.codeValidator);
@@ -50,17 +53,7 @@ export class ScriptCompiler implements IScriptCompiler {
} }
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void { function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
[compiledCode.code, compiledCode.revertCode] [compiledCode.code, compiledCode.revertCode].forEach(
.filter((code): code is string => Boolean(code)) (code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
.map((code) => code as string)
.forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines()],
),
); );
} }
function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
return (data as CallInstruction).call !== undefined;
}

View File

@@ -1,4 +1,4 @@
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/'; import type { ScriptData } from '@/application/collections/';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines'; import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { Script } from '@/domain/Script'; import { Script } from '@/domain/Script';
@@ -14,6 +14,7 @@ import { ICategoryCollectionParseContext } from './ICategoryCollectionParseConte
import { CodeValidator } from './Validation/CodeValidator'; import { CodeValidator } from './Validation/CodeValidator';
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines'; import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
// eslint-disable-next-line consistent-return
export function parseScript( export function parseScript(
data: ScriptData, data: ScriptData,
context: ICategoryCollectionParseContext, context: ICategoryCollectionParseContext,
@@ -23,6 +24,7 @@ export function parseScript(
): Script { ): Script {
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data }); const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
validateScript(data, validator); validateScript(data, validator);
if (!context) { throw new Error('missing context'); }
try { try {
const script = scriptFactory( const script = scriptFactory(
/* name: */ data.name, /* name: */ data.name,
@@ -32,12 +34,12 @@ export function parseScript(
); );
return script; return script;
} catch (err) { } catch (err) {
return validator.throw(err.message); validator.throw(err.message);
} }
} }
function parseLevel( function parseLevel(
level: string | undefined, level: string,
parser: IEnumParser<RecommendationLevel>, parser: IEnumParser<RecommendationLevel>,
): RecommendationLevel | undefined { ): RecommendationLevel | undefined {
if (!level) { if (!level) {
@@ -54,45 +56,39 @@ function parseCode(
if (context.compiler.canCompile(script)) { if (context.compiler.canCompile(script)) {
return context.compiler.compile(script); return context.compiler.compile(script);
} }
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled const code = new ScriptCode(script.code, script.revertCode);
const code = new ScriptCode(codeScript.code, codeScript.revertCode);
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax); validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
return code; return code;
} }
function validateHardcodedCodeWithoutCalls( function validateHardcodedCodeWithoutCalls(
scriptCode: ScriptCode, scriptCode: ScriptCode,
validator: ICodeValidator, codeValidator: ICodeValidator,
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
) { ) {
[scriptCode.execute, scriptCode.revert] [scriptCode.execute, scriptCode.revert].forEach(
.filter((code): code is string => Boolean(code)) (code) => codeValidator.throwIfInvalid(
.forEach(
(code) => validator.throwIfInvalid(
code, code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)], [new NoEmptyLines(), new NoDuplicatedLines(syntax)],
), ),
); );
} }
function validateScript( function validateScript(script: ScriptData, validator: NodeValidator) {
script: ScriptData,
validator: NodeValidator,
): asserts script is NonNullable<ScriptData> {
validator validator
.assertDefined(script) .assertDefined(script)
.assertValidName(script.name) .assertValidName(script.name)
.assert( .assert(
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call), () => Boolean(script.code || script.call),
'Neither "call" or "code" is defined.', 'Must define either "call" or "code".',
) )
.assert( .assert(
() => !((script as CodeScriptData).code && (script as CallScriptData).call), () => !(script.code && script.call),
'Both "call" and "code" are defined.', 'Cannot define both "call" and "code".',
) )
.assert( .assert(
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call), () => !(script.revertCode && script.call),
'Both "call" and "revertCode" are defined.', 'Cannot define "revertCode" if "call" is defined.',
); );
} }

View File

@@ -9,7 +9,7 @@ export class CodeValidator implements ICodeValidator {
code: string, code: string,
rules: readonly ICodeValidationRule[], rules: readonly ICodeValidationRule[],
): void { ): void {
if (rules.length === 0) { throw new Error('missing rules'); } if (!rules || rules.length === 0) { throw new Error('missing rules'); }
if (!code) { if (!code) {
return; return;
} }

View File

@@ -3,7 +3,9 @@ import { ICodeLine } from '../ICodeLine';
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule'; import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
export class NoDuplicatedLines implements ICodeValidationRule { export class NoDuplicatedLines implements ICodeValidationRule {
constructor(private readonly syntax: ILanguageSyntax) { } constructor(private readonly syntax: ILanguageSyntax) {
if (!syntax) { throw new Error('missing syntax'); }
}
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] { public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
return lines return lines

View File

@@ -17,6 +17,7 @@ export class CodeSubstituter implements ICodeSubstituter {
public substitute(code: string, info: IProjectInformation): string { public substitute(code: string, info: IProjectInformation): string {
if (!code) { throw new Error('missing code'); } if (!code) { throw new Error('missing code'); }
if (!info) { throw new Error('missing info'); }
const args = new FunctionCallArgumentCollection(); const args = new FunctionCallArgumentCollection();
const substitute = (name: string, value: string) => args const substitute = (name: string, value: string) => args
.addArgument(new FunctionCallArgument(name, value)); .addArgument(new FunctionCallArgument(name, value));

View File

@@ -18,6 +18,8 @@ export class ScriptingDefinitionParser {
definition: ScriptingDefinitionData, definition: ScriptingDefinitionData,
info: IProjectInformation, info: IProjectInformation,
): IScriptingDefinition { ): IScriptingDefinition {
if (!info) { throw new Error('missing info'); }
if (!definition) { throw new Error('missing definition'); }
const language = this.languageParser.parseEnum(definition.language, 'language'); const language = this.languageParser.parseEnum(definition.language, 'language');
const startCode = this.codeSubstituter.substitute(definition.startCode, info); const startCode = this.codeSubstituter.substitute(definition.startCode, info);
const endCode = this.codeSubstituter.substitute(definition.endCode, info); const endCode = this.codeSubstituter.substitute(definition.endCode, info);

View File

@@ -1,17 +0,0 @@
import { IEntity } from '@/infrastructure/Entity/IEntity';
export interface ReadonlyRepository<TKey, TEntity extends IEntity<TKey>> {
readonly length: number;
getItems(predicate?: (entity: TEntity) => boolean): readonly TEntity[];
getById(id: TKey): TEntity;
exists(id: TKey): boolean;
}
export interface MutableRepository<TKey, TEntity extends IEntity<TKey>> {
addItem(item: TEntity): void;
addOrUpdateItem(item: TEntity): void;
removeItem(id: TKey): void;
}
export interface Repository<TKey, TEntity extends IEntity<TKey>>
extends ReadonlyRepository<TKey, TEntity>, MutableRepository<TKey, TEntity> { }

View File

@@ -12,38 +12,29 @@ declare module '@/application/collections/*' {
} }
export type CategoryOrScriptData = CategoryData | ScriptData; export type CategoryOrScriptData = CategoryData | ScriptData;
export type DocumentationData = ReadonlyArray<string> | string | undefined; export type DocumentationData = ReadonlyArray<string> | string;
export interface DocumentableData { export interface DocumentableData {
readonly docs?: DocumentationData; readonly docs?: DocumentationData;
} }
export interface CodeInstruction { export interface InstructionHolder {
readonly code: string; readonly name: string;
readonly code?: string;
readonly revertCode?: string; readonly revertCode?: string;
}
export interface CallInstruction { readonly call?: FunctionCallsData;
readonly call: FunctionCallsData;
} }
export type InstructionHolder = CodeInstruction | CallInstruction;
export interface ParameterDefinitionData { export interface ParameterDefinitionData {
readonly name: string; readonly name: string;
readonly optional?: boolean; readonly optional?: boolean;
} }
export type FunctionDefinition = { export interface FunctionData extends InstructionHolder {
readonly name: string;
readonly parameters?: readonly ParameterDefinitionData[]; readonly parameters?: readonly ParameterDefinitionData[];
}; }
export type CodeFunctionData = FunctionDefinition & CodeInstruction;
export type CallFunctionData = FunctionDefinition & CallInstruction;
export type FunctionData = CodeFunctionData | CallFunctionData;
export interface FunctionCallParametersData { export interface FunctionCallParametersData {
readonly [index: string]: string; readonly [index: string]: string;
@@ -56,16 +47,10 @@ declare module '@/application/collections/*' {
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined; export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
export type ScriptDefinition = DocumentableData & { export interface ScriptData extends InstructionHolder, DocumentableData {
readonly name: string; readonly name: string;
readonly recommend?: string; readonly recommend?: string;
}; }
export type CodeScriptData = ScriptDefinition & CodeInstruction;
export type CallScriptData = ScriptDefinition & CallInstruction;
export type ScriptData = CodeScriptData | CallScriptData;
export interface ScriptingDefinitionData { export interface ScriptingDefinitionData {
readonly language: string; readonly language: string;

View File

@@ -707,9 +707,15 @@ actions:
- -
category: Clear Firefox history category: Clear Firefox history
docs: |- docs: |-
This category encompasses a series of scripts aimed at helping users manage and delete their browsing history and related data in Mozilla Firefox. Mozilla Firefox, or simply Firefox, is a free and open-source web browser developed by the Mozilla Foundation and
its subsidiary the Mozilla Corporation [1].
The scripts are designed to target different aspects of user data stored by Firefox, providing users options for maintaining privacy and freeing up disk space. Firefox stores user-related data in user profiles [2].
See also [the Firefox homepage](https://web.archive.org/web/20221029214632/https://www.mozilla.org/en-US/firefox/).
[1]: https://web.archive.org/web/20221029145113/https://en.wikipedia.org/wiki/Firefox "Firefox | Wikipedia | en.wikipedia.org"
[2]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
children: children:
- -
name: Clear Firefox cache name: Clear Firefox cache
@@ -749,13 +755,9 @@ actions:
# Snap installation # Snap installation
rm -rfv ~/snap/firefox/common/.mozilla/firefox/Crash\ Reports/* rm -rfv ~/snap/firefox/common/.mozilla/firefox/Crash\ Reports/*
- -
function: DeleteFilesFromFirefoxProfiles function: DeleteFromFirefoxProfiles
parameters: parameters:
pathGlob: crashes/* path: crashes/
-
function: DeleteFilesFromFirefoxProfiles
parameters:
pathGlob: crashes/events/*
- -
name: Clear Firefox cookies name: Clear Firefox cookies
docs: |- docs: |-
@@ -763,37 +765,41 @@ actions:
[1]: https://web.archive.org/web/20221029140816/https://kb.mozillazine.org/Cookies.sqlite "Cookies.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org" [1]: https://web.archive.org/web/20221029140816/https://kb.mozillazine.org/Cookies.sqlite "Cookies.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org"
call: call:
function: DeleteFilesFromFirefoxProfiles function: DeleteFromFirefoxProfiles
parameters: parameters:
pathGlob: cookies.sqlite path: cookies.sqlite
- -
name: Clear Firefox browsing history (URLs, downloads, bookmarks, visits, etc.) name: Clear Firefox browsing history (URLs, downloads, bookmarks, visits, etc.)
# This script (name, documentation and code) is same in Linux and Windows collections.
# Changes should be done at both places.
# Marked: refactor-with-partials
docs: |- docs: |-
This script targets the Firefox browsing history, including URLs, downloads, bookmarks, and site visits, by deleting specific database entries. The file "places.sqlite" stores the annotations, bookmarks, favorite icons, input history, keywords, and browsing history (a record of visited pages) [1].
The tables include [1]:
- `moz_anno_attributes`: Annotation Attributes
- `moz_annos`: Annotations
- `moz_bookmarks`: Bookmarks
- `moz_bookmarks_roots`: Bookmark roots i.e. places, menu, toolbar, tags, unfiled
- `moz_favicons`: Favorite icons - including URL of icon
- `moz_historyvisits`: A history of the number of times a site has been visited
- `moz_inputhistory`: A history of URLs typed by the user
- `moz_items_annos`: Item annotations
- `moz_keywords`: Keywords
- `moz_places`: Places/Sites visited - referenced by `moz_historyvisits`
URL data is stored in the `moz_places` table. However, this table is connected to `moz_annos`, `moz_bookmarks`, and `moz_inputhistory` and `moz_historyvisits`.
As these entries are connected to each other, we'll delete all of them at the same time [2].
Firefox stores various user data in a file named `places.sqlite`. This file includes: **Bookmarks**:
Firefox bookmarks are stored in tables such as `moz_bookmarks`, `moz_bookmarks_folders`, `moz_bookmarks_roots` [3].
There are also not very well documented tables, such as `moz_bookmarks_deleted` [4].
- Annotations, bookmarks, and favorite icons (`moz_anno_attributes`, `moz_annos`, `moz_favicons`) [1] **Downloads:**
- Browsing history, a record of pages visited (`moz_places`, `moz_historyvisits`) [1] Firefox downloads are stored in the 'places.sqlite' database, within the 'moz_annos' table [5].
- Keywords and typed URLs (`moz_keywords`, `moz_inputhistory`) [1] The entries in `moz_annos` are linked to `moz_places` that store the actual history entry (`moz_places.id = moz_annos.place_id`) [6].
- Item annotations (`moz_items_annos`) [1] Associated URL information is stored within the 'moz_places' table [5].
- Bookmark roots such as places, menu, toolbar, tags, unfiled (`moz_bookmarks_roots`) [1] Downloads have been historically stored in `downloads.rdf` for Firefox 2.x and below [7].
Starting with Firefox 3.x they're stored in `downloads.sqlite` [7].
The `moz_places` table holds URL data, connecting to various other tables like `moz_annos`, `moz_bookmarks`, `moz_inputhistory`, and `moz_historyvisits` [2]. **Favicons:**
Due to these connections, the script removes entries from all relevant tables simultaneously to maintain database integrity. Firefox favicons are stored in the `favicons.sqlite` database, within the `moz_icons` table [5].
Older versions of Firefox stored Favicons in the 'places.sqlite' database, within the `moz_favicons` table [5].
**Bookmarks**: Stored across several tables (`moz_bookmarks`, `moz_bookmarks_folders`, `moz_bookmarks_roots`) [3], with additional undocumented tables like `moz_bookmarks_deleted` [4].
**Downloads**: Stored in the 'places.sqlite' database, within the 'moz_annos' table [5]. The entries in `moz_annos` are linked to `moz_places` that store the actual history entry
(`moz_places.id = moz_annos.place_id`) [6]. Associated URL information is stored within the 'moz_places' table [5]. Downloads have been historically stored in `downloads.rdf` for Firefox 2.x
and below [7], and `downloads.sqlite` later on [7].
**Favicons**: Older Firefox versions stored favicons in `places.sqlite` within the `moz_favicons` table [5], while newer versions use `favicons.sqlite` and the `moz_icons` table [5].
By executing this script, users can ensure their Firefox browsing history, bookmarks, and downloads are thoroughly removed, contributing to a cleaner and more private browsing experience.
[1]: https://web.archive.org/web/20221029141626/https://kb.mozillazine.org/Places.sqlite "Places.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org" [1]: https://web.archive.org/web/20221029141626/https://kb.mozillazine.org/Places.sqlite "Places.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org"
[2]: https://web.archive.org/web/20221030160803/https://wiki.mozilla.org/images/0/08/Places.sqlite.schema.pdf "Places.sqlite.schema.pdf | Mozilla Wiki" [2]: https://web.archive.org/web/20221030160803/https://wiki.mozilla.org/images/0/08/Places.sqlite.schema.pdf "Places.sqlite.schema.pdf | Mozilla Wiki"
@@ -804,21 +810,21 @@ actions:
[7]: https://web.archive.org/web/20221029145712/https://kb.mozillazine.org/Downloads.rdf "Downloads.rdf | MozillaZine Knowledge Base | kb.mozillazine.org" [7]: https://web.archive.org/web/20221029145712/https://kb.mozillazine.org/Downloads.rdf "Downloads.rdf | MozillaZine Knowledge Base | kb.mozillazine.org"
call: call:
- -
function: DeleteFilesFromFirefoxProfiles function: DeleteFromFirefoxProfiles
parameters: parameters:
pathGlob: downloads.rdf path: downloads.rdf
- -
function: DeleteFilesFromFirefoxProfiles function: DeleteFromFirefoxProfiles
parameters: parameters:
pathGlob: downloads.sqlite path: downloads.sqlite
- -
function: DeleteFilesFromFirefoxProfiles function: DeleteFromFirefoxProfiles
parameters: parameters:
pathGlob: places.sqlite path: places.sqlite
- -
function: DeleteFilesFromFirefoxProfiles function: DeleteFromFirefoxProfiles
parameters: parameters:
pathGlob: favicons.sqlite path: favicons.sqlite
- -
name: Clear Firefox logins name: Clear Firefox logins
docs: |- docs: |-
@@ -831,17 +837,17 @@ actions:
[2]: https://web.archive.org/web/20221029145757/https://bugzilla.mozilla.org/show_bug.cgi?id=1593467 "1593467 - Automatically restore from logins-backup.json when logins.json is missing or corrupt | Bugzilla | mozilla.org | bugzilla.mozilla.org" [2]: https://web.archive.org/web/20221029145757/https://bugzilla.mozilla.org/show_bug.cgi?id=1593467 "1593467 - Automatically restore from logins-backup.json when logins.json is missing or corrupt | Bugzilla | mozilla.org | bugzilla.mozilla.org"
call: call:
- -
function: DeleteFilesFromFirefoxProfiles function: DeleteFromFirefoxProfiles
parameters: parameters:
pathGlob: logins.json path: logins.json
- -
function: DeleteFilesFromFirefoxProfiles function: DeleteFromFirefoxProfiles
parameters: parameters:
pathGlob: logins-backup.json path: logins-backup.json
- -
function: DeleteFilesFromFirefoxProfiles function: DeleteFromFirefoxProfiles
parameters: parameters:
pathGlob: signons.sqlite path: signons.sqlite
- -
name: Clear Firefox autocomplete history name: Clear Firefox autocomplete history
docs: |- docs: |-
@@ -850,9 +856,9 @@ actions:
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org" [1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
call: call:
function: DeleteFilesFromFirefoxProfiles function: DeleteFromFirefoxProfiles
parameters: parameters:
pathGlob: formhistory.sqlite path: formhistory.sqlite
- -
name: Clear Firefox "Multi-Account Containers" data name: Clear Firefox "Multi-Account Containers" data
docs: |- docs: |-
@@ -860,9 +866,9 @@ actions:
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org" [1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
call: call:
function: DeleteFilesFromFirefoxProfiles function: DeleteFromFirefoxProfiles
parameters: parameters:
pathGlob: containers.json path: containers.json
- -
name: Clear Firefox open tabs and windows data name: Clear Firefox open tabs and windows data
docs: |- docs: |-
@@ -872,9 +878,9 @@ actions:
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org" [1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
call: call:
function: DeleteFilesFromFirefoxProfiles function: DeleteFromFirefoxProfiles
parameters: parameters:
pathGlob: sessionstore.jsonlz4 path: sessionstore.jsonlz4
- -
category: Clear system and kernel usage data category: Clear system and kernel usage data
docs: |- docs: |-
@@ -2905,8 +2911,7 @@ actions:
function: AddFirefoxPrefs function: AddFirefoxPrefs
parameters: parameters:
prefName: toolkit.telemetry.log.level prefName: toolkit.telemetry.log.level
jsonValue: >- jsonValue: 'Fatal'
"Fatal"
- -
name: Disable Firefox telemetry log output name: Disable Firefox telemetry log output
recommend: standard recommend: standard
@@ -2919,8 +2924,7 @@ actions:
function: AddFirefoxPrefs function: AddFirefoxPrefs
parameters: parameters:
prefName: toolkit.telemetry.log.dump prefName: toolkit.telemetry.log.dump
jsonValue: >- jsonValue: 'Fatal'
"Fatal"
- -
name: Clear Firefox telemetry user ID name: Clear Firefox telemetry user ID
recommend: standard recommend: standard
@@ -3241,8 +3245,6 @@ functions:
revertCode: '{{ with $revertCode }}{{ . }}{{ end }}' revertCode: '{{ with $revertCode }}{{ . }}{{ end }}'
- -
name: RunIfCommandExists # Skips if command does not exist name: RunIfCommandExists # Skips if command does not exist
# Marked: refactor-with-partials
# Same function as macOS
parameters: parameters:
- name: command - name: command
- name: code - name: code
@@ -3489,66 +3491,16 @@ functions:
>&2 echo "Failed, $service does not exist." >&2 echo "Failed, $service does not exist."
fi fi
- -
name: Comment name: DeleteFromFirefoxProfiles
# 💡 Purpose:
# Adds a comment in the executed code for better readability and debugging.
# This function does not affect the execution flow but helps in understanding the purpose of subsequent code.
parameters:
- name: codeComment
optional: true
- name: revertCodeComment
optional: true
call:
function: RunInlineCode
parameters:
code: '{{ with $codeComment }}# {{ . }}{{ end }}'
revertCode: '{{ with $revertCodeComment }}# {{ . }}{{ end }}'
-
name: DeleteFiles
parameters:
- name: fileGlob
call:
-
function: Comment
parameters:
codeComment: >-
Delete files matching pattern: "{{ $fileGlob }}"
-
function: RunPython3Code
parameters: parameters:
- name: path # file or folder in profile file
code: |- code: |-
import glob # {{ $path }}: Global installation
import os rm -rfv ~/.mozilla/firefox/*/{{ $path }}
path = '{{ $fileGlob }}' # {{ $path }}: Flatpak installation
expanded_path = os.path.expandvars(os.path.expanduser(path)) rm -rfv ~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/{{ $path }}
print(f'Deleting files matching pattern: {expanded_path}') # {{ $path }}: Snap installation
paths = glob.glob(expanded_path) rm -rfv ~/snap/firefox/common/.mozilla/firefox/*/{{ $path }}
if not paths:
print('Skipping, no paths found.')
for path in paths:
if not os.path.isfile(path):
print(f'Skipping folder: "{path}".')
continue
os.remove(path)
print(f'Successfully delete file: "{path}".')
print(f'Successfully deleted {len(paths)} file(s).')
-
name: DeleteFilesFromFirefoxProfiles
parameters:
- name: pathGlob # file or folder in profile file
call:
- # Global installation
function: DeleteFiles
parameters:
fileGlob: ~/.mozilla/firefox/*/{{ $pathGlob }}
- # Flatpak installation
function: DeleteFiles
parameters:
fileGlob: ~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/{{ $pathGlob }}
- # Snap installation
function: DeleteFiles
parameters:
fileGlob: ~/snap/firefox/common/.mozilla/firefox/*/{{ $pathGlob }}
- -
name: CleanTableFromFirefoxProfileDatabase name: CleanTableFromFirefoxProfileDatabase
parameters: parameters:
@@ -3741,7 +3693,7 @@ functions:
- name: prefName - name: prefName
- name: jsonValue - name: jsonValue
docs: |- docs: |-
This script modifies the `user.js` file in Firefox profiles to set specific preferences. This script either creates or updates the `user.js` file to set specific Mozilla Firefox preferences.
The `user.js` file can be found in a Firefox profile folder [1] and its location depends on the type of installation: The `user.js` file can be found in a Firefox profile folder [1] and its location depends on the type of installation:
@@ -3749,18 +3701,12 @@ functions:
- Flatpak: `~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/user.js` - Flatpak: `~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/user.js`
- Snap: `~/snap/firefox/common/.mozilla/firefox/<profile-name>/user.js` - Snap: `~/snap/firefox/common/.mozilla/firefox/<profile-name>/user.js`
While the `user.js` file is optional [2], if it's present, the Firefox will prioritize its settings over While the `user.js` file is optional [2], if it's present, the Firefox application will prioritize its settings over
those in `prefs.js` upon startup [1] [2]. It's recommended not to directly edit `prefs.js` to avoid profile corruption [2]. those in `prefs.js` upon startup [1][2]. To prevent potential profile corruption, Mozilla advises against editing
`prefs.js` directly [2].
When `user.js` is modified or deleted, corresponding changes in `prefs.js` are necessary for reversion, as Firefox
doesn't automatically revert these changes [3].
This script safely modifies `user.js` and ensures changes are reflected in `prefs.js` during reversion, addressing
issues with preference persistence [3].
[1]: https://web.archive.org/web/20230811005205/https://kb.mozillazine.org/User.js_file "User.js file - MozillaZine Knowledge Base" [1]: https://web.archive.org/web/20230811005205/https://kb.mozillazine.org/User.js_file "User.js file - MozillaZine Knowledge Base"
[2]: https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file "Prefs.js file - MozillaZine Knowledge Base" [2]: https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file "Prefs.js file - MozillaZine Knowledge Base"
[3]: https://github.com/undergroundwires/privacy.sexy/issues/282 "[BUG]: Reverting Firefox settings do not work on Linux · Issue #282 · undergroundwires/privacy.sexy | github.com"
code: |- code: |-
pref_name='{{ $prefName }}' pref_name='{{ $prefName }}'
pref_value='{{ $jsonValue }}' pref_value='{{ $jsonValue }}'
@@ -3800,16 +3746,12 @@ functions:
if [ "$total_profiles_found" -eq 0 ]; then if [ "$total_profiles_found" -eq 0 ]; then
echo 'No profile folders are found, no changes are made.' echo 'No profile folders are found, no changes are made.'
else else
echo "Successfully verified preferences in $total_profiles_found profiles." echo "Preferences verified in $total_profiles_found profiles."
fi fi
revertCode: |- revertCode: |-
pref_name='{{ $prefName }}' pref_name='{{ $prefName }}'
pref_value='{{ $jsonValue }}' pref_value='{{ $jsonValue }}'
echo "Reverting preference: \"$pref_name\" to its default." echo "Reverting preference: \"$pref_name\" to its default."
if command -v 'ps' &> /dev/null && ps aux | grep -i "[f]irefox" > /dev/null; then
>&2 echo -e "\e[33mWarning: Firefox is currently running. Please close Firefox before executing the revert script to ensure changes are applied effectively.\e[0m"
fi
declare -a files_to_modify=('prefs.js' 'user.js')
declare -a profile_paths=( declare -a profile_paths=(
~/.mozilla/firefox/*/ ~/.mozilla/firefox/*/
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/ ~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
@@ -3817,39 +3759,31 @@ functions:
) )
declare -i total_profiles_found=0 declare -i total_profiles_found=0
for profile_dir in "${profile_paths[@]}"; do for profile_dir in "${profile_paths[@]}"; do
if [ ! -d "$profile_dir" ]; then user_js_file="${profile_dir}user.js"
if [ ! -f "$user_js_file" ]; then
continue continue
fi fi
if [[ ! "$(basename "$profile_dir")" =~ ^[a-z0-9]{8}\..+ ]]; then
continue # Not a profile folder
fi
((total_profiles_found++)) ((total_profiles_found++))
for file_to_modify in "${files_to_modify[@]}"; do echo "$user_js_file:"
config_file_path="${profile_dir}${file_to_modify}"
if [ ! -f "$config_file_path" ]; then
continue
fi
echo "$config_file_path:"
pref_start="user_pref(\"$pref_name\"," pref_start="user_pref(\"$pref_name\","
pref_line="user_pref(\"$pref_name\", $pref_value);" pref_line="user_pref(\"$pref_name\", $pref_value);"
if ! grep --quiet "^$pref_start" "${config_file_path}"; then if ! grep --quiet "^$pref_start" "${user_js_file}"; then
echo $'\t''Skipping, preference was not configured before.' echo $'\t''Skipping, preference was not configured before.'
elif grep --quiet "^$pref_line$" "${config_file_path}"; then elif grep --quiet "^$pref_line$" "${user_js_file}"; then
sed --in-place "/^$pref_line/d" "$config_file_path" sed --in-place "/^$pref_line/d" "$user_js_file"
echo $'\t''Successfully reverted preference to default.' echo $'\t''Successfully reverted preference to default.'
if ! grep --quiet '[^[:space:]]' "$config_file_path"; then if ! grep --quiet '[^[:space:]]' "$user_js_file"; then
rm "$config_file_path" rm "$user_js_file"
echo $'\t'"Removed the file as it became empty." echo $'\t''Removed user.js file as it became empty.'
fi fi
else else
echo $'\t''Skipping, the preference has value that is not configured by privacy.sexy.' echo $'\t''Skipping, the preference has value that is not configured by privacy.sexy.'
fi fi
done done
done
if [ "$total_profiles_found" -eq 0 ]; then if [ "$total_profiles_found" -eq 0 ]; then
echo 'No reversion was necessary.' echo 'No reversion was necessary.'
else else
echo "Successfully verified preferences in $total_profiles_found profiles." echo "Preferences verified in $total_profiles_found profiles."
fi fi
- -
name: RenameFile name: RenameFile

View File

@@ -444,285 +444,47 @@ actions:
recommend: standard recommend: standard
code: sudo purge code: sudo purge
- -
category: Clear application privacy permissions category: Clear all privacy permissions for applications
docs: |-
This category provides scripts to reset privacy permissions for a variety of applications on your device,
helping you to re-establish control over your personal data. Each script targets a specific permission type such
as camera, microphone, contacts, or accessibility services enabling you to revoke permissions that have previously
been granted to applications.
By resetting these permissions, you not only enhance your privacy but also improve your device's security. After
running these scripts, applications will require your explicit permission again to access these services or
information. This means the next time an app attempts to use a service like your camera or access your contacts,
you'll be prompted to grant or deny permission. It's a proactive step to ensure that your sensitive information
or system services are accessed only with your current and informed consent.
children: children:
# Main documentation: https://archive.ph/26Hlq (https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services)
- -
name: Clear **"All"** permissions name: Clear "camera" permissions
docs: |- code: tccutil reset Camera
This script resets all permissions for applications.
It revokes all previously granted permissions, enhancing privacy and security by ensuring no application has unauthorized access to system services or user data.
call:
function: ResetServicePermissions
parameters:
serviceId: All
- -
name: Clear "Camera" permissions name: Clear "microphone" permissions
docs: |- code: tccutil reset Microphone
This script resets permissions for camera access [1].
It ensures no application can access the system camera without explicit user permission, protecting against unauthorized surveillance and data breaches.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: Camera
- -
name: Clear "Microphone" permissions name: Clear "accessibility" permissions
docs: |- code: tccutil reset Accessibility
This script resets permissions for microphone access [1].
It revokes all granted access to the microphone, protecting against eavesdropping and unauthorized audio recording by applications.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: Microphone
- -
name: Clear "Accessibility" permissions name: Clear "screen capture" permissions
docs: |- code: tccutil reset ScreenCapture
This script resets permissions for accessibility features [1].
It revokes application access to accessibility services, preventing misuse and ensuring these features are used only with user consent.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: Accessibility
- -
name: Clear "Screen Capture" permissions name: Clear "reminders" permissions
docs: |- code: tccutil reset Reminders
This script resets permissions for screen capture [1].
It ensures applications cannot capture screen content without user authorization, protecting sensitive information displayed on the screen.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: ScreenCapture
- -
name: Clear "Reminders" permissions name: Clear "photos" permissions
docs: |- code: tccutil reset Photos
This script resets permissions for accessing reminders information managed by the Reminders app [1].
It ensures applications cannot access or modify reminders data without explicit user permission, maintaining the privacy of personal reminders.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: Reminders
- -
name: Clear "Photos" permissions name: Clear "calendar" permissions
docs: |- code: tccutil reset Calendar
This script resets permissions for accessing the pictures managed by the Photos app [1].
It revokes all permissions granted to applications, safeguarding personal photos and media from unauthorized access.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: Photos
- -
name: Clear "Calendar" permissions name: Clear "full disk access" permissions
docs: |- code: tccutil reset SystemPolicyAllFiles
This script resets permissions for accessing the calendar information managed by the Calendar app [1].
It ensures that applications cannot access calendar data without user consent, protecting personal and sensitive calendar information.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: Calendar
- -
name: Clear "Full Disk Access" permissions name: Clear "contacts" permissions
docs: |- code: tccutil reset SystemPolicyAllFiles
This script resets permissions for full disk access.
Full disk access allows the application access to all protected files, including system administration files [1].
It revokes broad file access from applications, significantly reducing the risk of data exposure and enhancing overall system security.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: SystemPolicyAllFiles
- -
name: Clear "Contacts" permissions name: Clear "desktop folder" permissions
docs: |- code: tccutil reset SystemPolicyDesktopFolder
This script resets permissions for accessing contacts.
The contact information managed by the Contacts app [1].
It ensures that applications cannot access the user's contact list without explicit permission, maintaining the confidentiality of personal contacts.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: AddressBook
- -
name: Clear "Desktop Folder" permissions name: Clear "documents folder" permissions
docs: |- code: tccutil reset SystemPolicyDocumentsFolder
This script resets permissions for accessing the Desktop folder [1].
It revokes application access to files on the desktop, protecting personal and work-related documents from unauthorized access.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: SystemPolicyDesktopFolder
- -
name: Clear "Documents Folder" permissions name: Clear "downloads" permissions
docs: |- code: tccutil reset SystemPolicyDownloadsFolder
This script resets permissions for accessing the Documents folder [1].
It prevents applications from accessing files in this folder without user consent, safeguarding important and private documents.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: SystemPolicyDocumentsFolder
- -
name: Clear "Downloads Folder" permissions name: Clear all app permissions
docs: |- code: tccutil reset All
This script resets permissions for accessing the Downloads folder [1].
It ensures that applications cannot access downloaded files without user authorization, protecting downloaded content from misuse.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: SystemPolicyDownloadsFolder
-
name: Clear "Apple Events" permissions
docs: |-
This script resets permissions for Apple Events [1].
It revokes permissions for applications to send restricted Apple Events to other processes [1], enhancing privacy and security.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: AppleEvents
-
name: Clear "File Provider Presence" permissions
docs: |-
This script resets permissions for File Provider Presence [1].
It revokes the ability of File Provider applications to know when the user is accessing their managed files [1], enhancing user privacy.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: FileProviderPresence
-
name: Clear "Listen Events" permissions
docs: |-
This script resets "ListenEvent" permissions [1].
It revokes application access to listen to system events [1], preventing unauthorized monitoring of user interactions with the system.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: ListenEvent
-
name: Clear "Media Library" permissions
docs: |-
This script resets permissions for accessing the Media Library [1].
It ensures that applications cannot access Apple Music, music and video activity, and the media library [1] without user consent.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: MediaLibrary
-
name: Clear "Post Event" permissions
docs: |-
This script resets permissions for sending "PostEvent" [1].
It prevents applications from using CoreGraphics APIs to send system events [1], safeguarding against potential misuse.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: PostEvent
-
name: Clear "Speech Recognition" permissions
recommend: strict
docs: |-
This script resets permissions for using Speech Recognition [1].
It revokes application access to the speech recognition facility and sending speech data to Apple [1], protecting user privacy.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: SpeechRecognition
-
name: Clear "App Modification" permissions
docs: |-
This script resets permissions for modifying other apps [1].
It prevents applications from updating or deleting other apps [1], maintaining system integrity and user control.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: SystemPolicyAppBundles
-
name: Clear "Application Data" permissions
docs: |-
This script resets permissions for accessing application data [1].
It revokes application access to specific application data, enhancing privacy and data security.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: SystemPolicyAppData
-
name: Clear "Network Volumes" permissions
docs: |-
This script resets permissions for accessing files on network volumes [1].
It ensures applications cannot access network files without user authorization.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: SystemPolicyNetworkVolumes
-
name: Clear "Removable Volumes" permissions
docs: |-
This script resets permissions for accessing files on removable volumes [1].
It protects data on external drives from unauthorized application access.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: SystemPolicyRemovableVolumes
-
name: Clear "System Administration Files" permissions
docs: |-
This script resets permissions for accessing system administration files [1].
It enhances system security by restricting application access to critical system files.
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
call:
function: ResetServicePermissions
parameters:
serviceId: SystemPolicySysAdminFiles
- -
category: Configure programs category: Configure programs
children: children:
@@ -1476,6 +1238,376 @@ actions:
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool true sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool true
# Trigger background check with normal scan (critical updates only) # Trigger background check with normal scan (critical updates only)
sudo softwareupdate --background-critical sudo softwareupdate --background-critical
-
category: Disable OS services
children:
# Get active services : launchctl list | grep -v "\-\t0"
# Find a service : sudo grep -lR [service] /System/Library/Launch* /Library/Launch* ~/Library/LaunchAgents
# Locate a service : pgrep -fl [service]
# TODO: https://gist.github.com/ecompayment/b1054421eb90f296bbca226683c7ff7e
-
category: Disable continuously data-collecting services by default
children:
-
name: Disable diagnostics and usage data sender
recommend: standard
docs: https://apple.stackexchange.com/questions/66119/disable-submitdiaginfo
call:
function: DisableService
parameters:
name: com.apple.SubmitDiagInfo
type: LaunchDaemons
-
name: Disable diagnostics and usage data sender
recommend: standard
call:
-
function: DisableService
parameters:
name: com.apple.rtcreportingd.plist
type: LaunchDaemons
-
function: RenameSystemFile
parameters:
filePath: /usr/libexec/rtcreportingd
-
name: Disable Family Circle Daemon for Family Sharing
docs: https://support.apple.com/en-us/HT201060
recommend: standard
# Connects to setup.icloud.com HTTPS (TCP 443 )
call:
-
function: DisableService
parameters:
name: com.apple.familycircled
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: /System/Library/PrivateFrameworks/FamilyCircle.framework/Versions/A/Resources/familycircled
-
name: Disable home sharing
docs: https://discussions.apple.com/thread/7434075?answerId=29677460022#29677460022
# Connects to apps.mzstatic.com and init.itunes.apple.com HTTPS (TCP 443 )
recommend: strict
call:
-
function: DisableService
parameters:
name: com.apple.itunescloudd
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: /usr/libexec/rtcreportingd # TODO: SIP required?
-
name: Disable CommerceKit handling purchases for Apple products
# the Mac App Store, iTunes store, and Book Store
# Connects to init.itunes.apple.com and xp.apple.com HTTPS (TCP 443 )
recommend: strict
call:
-
function: DisableService
parameters:
name: com.apple.commerce.plist
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: /System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/commerce
-
category: Disable Siri services # TODO: merge with other assistantd script
children:
-
name: Disable Siri dictation service sending voice data
recommend: strict
docs: https://apple.stackexchange.com/questions/57514/what-is-assistantd
# Connects to guzzoni.apple.com HTTPS (TCP 443 )
call:
-
function: DisableService
parameters:
name: com.apple.assistantd
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: /System/Library/PrivateFrameworks/AssistantServices.framework/Versions/A/Support/assistantd
-
name: Disable Siri assistant service
recommend: strict
docs: https://www.howtogeek.com/354897/what-are-assistant_service-and-assistantd-and-why-are-they-running-on-my-mac/
# Connects to radio.itunes.apple.com HTTPS (TCP 443 )
call:
-
function: DisableService
parameters:
name: com.apple.assistant_service.plist
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: /System/Library/PrivateFrameworks/AssistantServices.framework/Versions/A/Support/assistant_service
-
category: Disable Messages services
docs: https://blog.quarkslab.com/imessage-privacy.html
children:
-
name: Disable Apple Push Service Daemon used for Notification Center and Messages
# Connects to *-courier.push.apple.com (where * is a number) using HTTPS (TCP 443) and apple-push (TCP 5223)
call:
-
function: DisableService
parameters:
name: com.apple.apsd
type: LaunchDaemons
-
function: RenameSystemFile
parameters:
filePath: /System/Library/PrivateFrameworks/ApplePushService.framework/apsd
-
name: Disable iMessage Agent in Messages app
# Used for e.g. FaceTime invitations
docs:
- https://apple.stackexchange.com/questions/86814/firewall-settings-with-imagent
- https://blog.quarkslab.com/imessage-privacy.html
# Connects to using HTTPS (TCP 443) and apple-push (TCP 5223)
call:
-
function: DisableService
parameters:
name: com.apple.imagent
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: /System/Library/PrivateFrameworks/IMCore.framework/imagent.app/Contents/MacOS/imagent
-
name: Disable Address Book Source Sync (breaks Contacts data sync)
# Synchronizes data data for the “Contacts” app with iCloud, CardDAV, and Exchange servers
docs: https://apple.stackexchange.com/questions/219774/how-to-disable-addressbooksourcesync-in-el-capitan
# Connects to p25-contacts.icloud.com using HTTPS (TCP 443) and apple-push (TCP 5223)
recommend: strict
call:
-
function: DisableService
parameters:
name: com.apple.AddressBook.SourceSync
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: /System/Library/Frameworks/AddressBook.framework/Versions/A/Helpers/AddressBookSourceSync.app/Contents/MacOS/AddressBookSourceSync
-
name: Disable usage tracking agent
recommend: strict
docs: https://www.unix.com/man-page/mojave/8/USAGETRACKINGAGENT/
# Connects to itunes.apple.com using HTTPS 443 (TCP)
call:
-
function: DisableService
parameters:
name: com.apple.UsageTrackingAgent
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: /System/Library/PrivateFrameworks/UsageTracking.framework/Versions/A/UsageTrackingAgent
-
name: Disable AMPLibraryAgent for Apple Music
# Connects to buy.itunes.apple.com, init.itunes.apple.com, play.itunes.apple.com, xp.apple.com using HTTPS 443 (TCP)
call:
-
function: DisableService
parameters:
name: com.apple.AMPLibraryAgent
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: System/Library/PrivateFrameworks/AMPLibrary.framework/Versions/A/Support/AMPLibraryAgent
-
category: Disable location services
children:
-
name: Disable Maps push daemon
docs:
- https://www.unix.com/man-page/mojave/8/MAPSPUSHD/
- https://discussions.apple.com/thread/7025815
call:
function: DisableService
parameters:
name: com.apple.Maps.pushdaemon
type: LaunchAgents
-
name: Disable Geo Daemon / geolocation daemon used to show maps by apps e.g. Maps
# Connects to Apple servers for loading map data on behalf of other apps and for resolving geographical coordinates to readable addresses.
# Connects to gspe*-ssl.ls.apple.com (where * is a number from 1 to 100 ), sp-ssl.ls.apple.com, configuration.ls.apple.com using HTTPS 443 (TCP)
call:
function: "RenameSystemFile (TODO: Just like Windows.yaml, requires SIP)"
parameters:
filePath: /System/Library/PrivateFrameworks/GeoServices.framework/Versions/A/XPCServices/com.apple.geod.xpc/Contents/MacOS/com.apple.geod
-
name: Disable Location-Based Suggestions for Siri, Spotlight and other places
# Used for suggestions in Spotlight, Messages, Lookup, Safari, Siri, and other place
# Connects to api-glb-euc1b.smoot.apple.com, api.smoot.apple.com using HTTPS 443 (TCP)
recommend: strict
call:
-
function: DisableService
parameters:
name: com.apple.parsecd
type: LaunchAgents
-
function: "RenameSystemFile (TODO: Just like Windows.yaml, requires SIP)"
parameters:
filePath: /System/Library/PrivateFrameworks/CoreParsec.framework/parsecd
-
category: Disable iCloud services
children:
-
name: Disable iCloud notification agent
recommend: strict
call:
function: DisableService
parameters:
name: com.apple.iCloudNotificationAgent
type: LaunchAgents
-
name: Disable Sync Defaults Daemon
# Syncs user preferences or other configuration related data via iCloud
docs: https://www.unix.com/man-page/mojave/8/syncdefaultsd
# Connects to keyvalueservice.icloud.com and p*-keyvalueservice.icloud.com (where * is a number) using HTTPS 443 (TCP)
recommend: strict
call:
-
function: DisableService
parameters:
name: com.apple.syncdefaultsd
type: LaunchAgents
-
function: "RenameSystemFile (TODO: Just like Windows.yaml, requires SIP)"
parameters:
filePath: /System/Library/PrivateFrameworks/SyncedDefaults.framework/Support/syncdefaultsd
-
name: Disable Reminder Daemon that synchronizes the reminder list in "Reminders" with iCloud
recommend: strict
call:
-
function: DisableService
parameters:
name: com.apple.remindd
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: /usr/libexec/remindd #TODO: Mb don't require SIP
-
name: Disable Cloud Daemon used for iCloud syncing
# Connects to gateway.icloud.com, metrics.icloud.com using HTTPS 443 (TCP)
recommend: strict
call:
-
function: DisableService
parameters:
name: com.apple.cloudd
type: LaunchAgents
-
function: DisableService
parameters:
name: com.apple.cloudd
type: LaunchDaemons
-
function: RenameSystemFile
parameters:
filePath: /System/Library/PrivateFrameworks/CloudKitDaemon.framework/Support/cloudd
-
name: Disable Help Daemon (breaks HelpViewer feature)
recommend: strict
docs: https://discussions.apple.com/thread/3930621
# Connects to cds.apple.com, help.apple.com using HTTPS (TCP 443)
call:
-
function: DisableService
parameters:
name: com.apple.helpd
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: /System/Library/PrivateFrameworks/HelpData.framework/Versions/A/Resources/helpd
-
name: Disable Rapport Daemon for communication between Apple devices
# Rapport Daemon is a macOS system process that enables Phone Call Handoff and other communication features between Apple devices.
# Connects to init.ess.apple.com using HTTPS (TCP 443)
docs: https://apple.stackexchange.com/questions/308294/what-is-rapportd-and-why-does-it-want-incoming-network-connections
call:
-
function: DisableService
parameters:
name: com.apple.rapportd-user
type: LaunchAgents
-
function: DisableService
parameters:
name: com.apple.rapportd
type: LaunchDaemons
-
function: RenameSystemFile
parameters:
filePath: /usr/libexec/rapportd #TODO: No SIP required?
-
name: Disable App Tracking Transparency framework
docs:
- https://apple.stackexchange.com/questions/409349/what-is-the-transparencyd-daemon-for
- https://developer.apple.com/documentation/apptrackingtransparency
# Connects to server kt-prod.apple.com using HTTPS (TCP 443 )
call:
-
function: DisableService
parameters:
name: com.apple.transparencyd
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: /usr/libexec/transparencyd #TODO: No need for SIP?
-
category: Disable Calendar Agent that sync Calender App to iCloud and other servers
call:
-
function: DisableService
parameters:
name: com.apple.CalendarAgent
type: LaunchAgents
-
function: RenameSystemFile
parameters:
filePath: /System/Library/PrivateFrameworks/CalendarAgent.framework/Executables/CalendarAgent
-
name: Disable advertising services daemon
recommend: strict
docs: https://www.unix.com/man-page/mojave/8/adservicesd
call:
function: DisableService
parameters:
name: com.apple.ap.adservicesd
type: LaunchAgents
-
name: Disable NetBIOS interactions (might break Microsoft services)
# Mostly used for mostly SMB network volumes
docs: https://www.manpagez.com/man/8/netbiosd/
call:
-
function: DisableService
parameters:
name: com.apple.netbiosd
type: LaunchDaemons
function: RenameSystemFile
parameters:
filePath: /usr/sbin/netbiosd
requireSip: false # TODO: Test
functions: functions:
- -
name: PersistUserEnvironmentConfiguration name: PersistUserEnvironmentConfiguration
@@ -1507,54 +1639,30 @@ functions:
fi fi
done done
- -
name: RunIfCommandExists # Skips if command does not exist name: DisableService
# Marked: refactor-with-partials
# Same function as Linux
parameters: parameters:
- name: command - name: name
- name: code - name: type
- name: revertCode
optional: true
code: |- code: |-
if ! command -v '{{ $command }}' &> /dev/null; then original_file='/System/Library/{{ $type }}/{{ $name }}.plist'
echo 'Skipping because "{{ $command }}" is not found.' backup_file="$original_file.disabled"
if [ -f "$original_file" ]; then
sudo launchctl unload -w "$original_file" 2> /dev/null
mv "$original_file" "$backup_file"
echo 'Disabled successfully'
else else
{{ $code }} echo 'Already disabled'
fi fi
revertCode: |- revertCode: |-
{{ with $revertCode }} original_file='/System/Library/{{ $type }}/{{ $name }}.plist'
if ! command -v '{{ $command }}' &> /dev/null; then backup_file="$original_file.disabled"
>&2 echo 'Cannot revert because "{{ $command }}" is not found.' if [ -f "$original_file" ]; then
sudo launchctl unload -w "$original_file" 2> /dev/null
if mv "$original_file" "$backup_file"; then
echo 'Disabled successfully'
else else
{{ . }} >&2 echo 'Failed to disable'
fi
{{ end }}
-
name: ResetServicePermissions
parameters:
- name: serviceId # Specifies the service ID for which to reset permissions
docs: |-
This function resets the specified service ID permissions.
The `serviceId` parameter allows you to define the specific service ID (e.g., Camera, Microphone,
Accessibility) for which you want to reset all user-granted permissions.
call:
function: RunIfCommandExists
parameters:
command: tccutil
code: |-
declare serviceId='{{ $serviceId }}'
declare reset_output reset_exit_code
{
reset_output=$(tccutil reset "$serviceId" 2>&1)
reset_exit_code=$?
}
if [ $reset_exit_code -eq 0 ]; then
echo "Successfully reset permissions for \"${serviceId}\"."
elif [ $reset_exit_code -eq 70 ]; then
echo "Skipping, service ID \"${serviceId}\" is not supported on your operating system version."
elif [ $reset_exit_code -ne 0 ]; then
>&2 echo "Failed to reset permissions for \"${serviceId}\". Exit code: $reset_exit_code."
if [ -n "$reset_output" ]; then
echo "Output from \`tccutil\`: $reset_output."
fi fi
else
echo 'Already disabled'
fi fi

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ export class Application implements IApplication {
public info: IProjectInformation, public info: IProjectInformation,
public collections: readonly ICategoryCollection[], public collections: readonly ICategoryCollection[],
) { ) {
validateInformation(info);
validateCollections(collections); validateCollections(collections);
} }
@@ -15,17 +16,19 @@ export class Application implements IApplication {
return this.collections.map((collection) => collection.os); return this.collections.map((collection) => collection.os);
} }
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection { public getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined {
const collection = this.collections.find((c) => c.os === operatingSystem); return this.collections.find((collection) => collection.os === operatingSystem);
if (!collection) {
throw new Error(`Operating system "${OperatingSystem[operatingSystem]}" is not defined in application`);
} }
return collection; }
function validateInformation(info: IProjectInformation) {
if (!info) {
throw new Error('missing project information');
} }
} }
function validateCollections(collections: readonly ICategoryCollection[]) { function validateCollections(collections: readonly ICategoryCollection[]) {
if (!collections.length) { if (!collections || !collections.length) {
throw new Error('missing collections'); throw new Error('missing collections');
} }
if (collections.filter((c) => !c).length > 0) { if (collections.filter((c) => !c).length > 0) {

View File

@@ -3,14 +3,14 @@ import { IScript } from './IScript';
import { ICategory } from './ICategory'; import { ICategory } from './ICategory';
export class Category extends BaseEntity<number> implements ICategory { export class Category extends BaseEntity<number> implements ICategory {
private allSubScripts?: ReadonlyArray<IScript> = undefined; private allSubScripts: ReadonlyArray<IScript> = undefined;
constructor( constructor(
id: number, id: number,
public readonly name: string, public readonly name: string,
public readonly docs: ReadonlyArray<string>, public readonly docs: ReadonlyArray<string>,
public readonly subCategories: ReadonlyArray<ICategory>, public readonly subCategories?: ReadonlyArray<ICategory>,
public readonly scripts: ReadonlyArray<IScript>, public readonly scripts?: ReadonlyArray<IScript>,
) { ) {
super(id); super(id);
validateCategory(this); validateCategory(this);
@@ -39,7 +39,10 @@ function validateCategory(category: ICategory) {
if (!category.name) { if (!category.name) {
throw new Error('missing name'); throw new Error('missing name');
} }
if (category.subCategories.length === 0 && category.scripts.length === 0) { if (
(!category.subCategories || category.subCategories.length === 0)
&& (!category.scripts || category.scripts.length === 0)
) {
throw new Error('A category must have at least one sub-category or script'); throw new Error('A category must have at least one sub-category or script');
} }
} }

View File

@@ -19,6 +19,9 @@ export class CategoryCollection implements ICategoryCollection {
public readonly actions: ReadonlyArray<ICategory>, public readonly actions: ReadonlyArray<ICategory>,
public readonly scripting: IScriptingDefinition, public readonly scripting: IScriptingDefinition,
) { ) {
if (!scripting) {
throw new Error('missing scripting definition');
}
this.queryable = makeQueryable(actions); this.queryable = makeQueryable(actions);
assertInRange(os, OperatingSystem); assertInRange(os, OperatingSystem);
ensureValid(this.queryable); ensureValid(this.queryable);
@@ -26,26 +29,17 @@ export class CategoryCollection implements ICategoryCollection {
ensureNoDuplicates(this.queryable.allScripts); ensureNoDuplicates(this.queryable.allScripts);
} }
public getCategory(categoryId: number): ICategory { public findCategory(categoryId: number): ICategory | undefined {
const category = this.queryable.allCategories.find((c) => c.id === categoryId); return this.queryable.allCategories.find((category) => category.id === categoryId);
if (!category) {
throw new Error(`Missing category with ID: "${categoryId}"`);
}
return category;
} }
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] { public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
assertInRange(level, RecommendationLevel); assertInRange(level, RecommendationLevel);
const scripts = this.queryable.scriptsByLevel.get(level); return this.queryable.scriptsByLevel.get(level);
return scripts ?? [];
} }
public getScript(scriptId: string): IScript { public findScript(scriptId: string): IScript | undefined {
const script = this.queryable.allScripts.find((s) => s.id === scriptId); return this.queryable.allScripts.find((script) => script.id === scriptId);
if (!script) {
throw new Error(`missing script: ${scriptId}`);
}
return script;
} }
public getAllScripts(): IScript[] { public getAllScripts(): IScript[] {
@@ -84,13 +78,13 @@ function ensureValid(application: IQueryableCollection) {
} }
function ensureValidCategories(allCategories: readonly ICategory[]) { function ensureValidCategories(allCategories: readonly ICategory[]) {
if (!allCategories.length) { if (!allCategories || allCategories.length === 0) {
throw new Error('must consist of at least one category'); throw new Error('must consist of at least one category');
} }
} }
function ensureValidScripts(allScripts: readonly IScript[]) { function ensureValidScripts(allScripts: readonly IScript[]) {
if (!allScripts.length) { if (!allScripts || allScripts.length === 0) {
throw new Error('must consist of at least one script'); throw new Error('must consist of at least one script');
} }
const missingRecommendationLevels = getEnumValues(RecommendationLevel) const missingRecommendationLevels = getEnumValues(RecommendationLevel)

View File

@@ -7,5 +7,5 @@ export interface IApplication {
readonly collections: readonly ICategoryCollection[]; readonly collections: readonly ICategoryCollection[];
getSupportedOsList(): OperatingSystem[]; getSupportedOsList(): OperatingSystem[];
getCollection(operatingSystem: OperatingSystem): ICategoryCollection; getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined;
} }

View File

@@ -5,8 +5,8 @@ import { IDocumentable } from './IDocumentable';
export interface ICategory extends IEntity<number>, IDocumentable { export interface ICategory extends IEntity<number>, IDocumentable {
readonly id: number; readonly id: number;
readonly name: string; readonly name: string;
readonly subCategories: ReadonlyArray<ICategory>; readonly subCategories?: ReadonlyArray<ICategory>;
readonly scripts: ReadonlyArray<IScript>; readonly scripts?: ReadonlyArray<IScript>;
includes(script: IScript): boolean; includes(script: IScript): boolean;
getAllScriptsRecursively(): ReadonlyArray<IScript>; getAllScriptsRecursively(): ReadonlyArray<IScript>;
} }

View File

@@ -12,8 +12,8 @@ export interface ICategoryCollection {
readonly actions: ReadonlyArray<ICategory>; readonly actions: ReadonlyArray<ICategory>;
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>; getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
getCategory(categoryId: number): ICategory; findCategory(categoryId: number): ICategory | undefined;
getScript(scriptId: string): IScript; findScript(scriptId: string): IScript | undefined;
getAllScripts(): ReadonlyArray<IScript>; getAllScripts(): ReadonlyArray<IScript>;
getAllCategories(): ReadonlyArray<ICategory>; getAllCategories(): ReadonlyArray<ICategory>;
} }

View File

@@ -1,4 +1,4 @@
export interface IScriptCode { export interface IScriptCode {
readonly execute: string; readonly execute: string;
readonly revert?: string; readonly revert: string;
} }

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