Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b042b36aea | ||
|
|
be7a886225 |
57
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: Bug report (script bug or unexpected script behavior)
|
||||
about: Create a bug report for generated scripts to help privacy.sexy improve
|
||||
labels: bug
|
||||
title: '[BUG]: '
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for reporting an issue with generated script(s).
|
||||
Please fill in as much of the template below as you're able.
|
||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
||||
-->
|
||||
|
||||
### Description
|
||||
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
### OS
|
||||
|
||||
<!--
|
||||
Which OS are you using? What version of OS you were using?
|
||||
On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||
On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
||||
On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
||||
-->
|
||||
|
||||
### Reproduction steps
|
||||
|
||||
<!--
|
||||
How can the bug be recreated?
|
||||
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
||||
E.g.
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
### Scripts
|
||||
|
||||
<!--
|
||||
If applicable, please attach the generated privacy.sexy file instead of copy pasting which becomes too long.
|
||||
-->
|
||||
|
||||
### Screenshots
|
||||
|
||||
<!--
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
-->
|
||||
|
||||
### Additional information
|
||||
|
||||
<!--
|
||||
If applicable, add any other context about the problem here.
|
||||
-->
|
||||
114
.github/ISSUE_TEMPLATE/1-bug-report-scripts.yaml
vendored
@@ -1,114 +0,0 @@
|
||||
name: "Bug Report: Script Issues"
|
||||
description: 🐛 Report issues with generated scripts to enhance privacy.sexy
|
||||
labels: [ 'bug' ]
|
||||
title: '[Bug]: '
|
||||
body:
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||
Please complete as much of the form below as possible.
|
||||
Your feedback is valuable, even if you can't provide all details.
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: >-
|
||||
For example: "After running the cleanup script, music playback stopped functioning."
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: How can the bug be recreated?
|
||||
description: |-
|
||||
This is the most important information in the bug report.
|
||||
Bugs that cannot be reproduced cannot be fixed or verified.
|
||||
placeholder: |-
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Operating system
|
||||
description: |-
|
||||
Please specify your operating system and its version.
|
||||
|
||||
- On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||
- On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
||||
- On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
||||
placeholder: >-
|
||||
For example: "Windows 11 Pro 22H3"
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Script file
|
||||
description: |-
|
||||
If applicable, share the generated privacy.sexy file.
|
||||
|
||||
GitHub may restrict script file attachments.
|
||||
Upload your script file to a service like [GitHub Gist](https://gist.github.com/) and share the link here.
|
||||
|
||||
If you used the desktop version to run the script, it is already stored on your system.
|
||||
See the [documentation to locate it](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#secure-script-executionstorage).
|
||||
|
||||
> **💡 Tip:** You can attach script files by dragging them into this area.
|
||||
placeholder: |-
|
||||
Attach the script, or post GitHub Gist link.
|
||||
For example: https://gist.github.com/privacysexy-forks/6d85ad8ca27acc8c6a5417d4af28c9b6.
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: |-
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
> **💡 Tip:** You can attach screenshots by clicking this area to highlight it and then pasting them or dragging files in.
|
||||
placeholder: Attach screenshots here or link to image hosting.
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: |-
|
||||
If applicable, add any other context about the problem here.
|
||||
|
||||
Helpful information includes:
|
||||
|
||||
- Application logs (desktop version only), see: [how to find application logs](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#logging).
|
||||
- Terminal output
|
||||
- Proposed solutions
|
||||
- Other related context such as related issues, software behavior, etc.
|
||||
|
||||
> **💡 Tip:** You can attach log files by dragging them into this area.
|
||||
placeholder: >-
|
||||
For example: "Here are the logs I get from the privacy.sexy 0.13.2 desktop application: ..."
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
---
|
||||
|
||||
**✉️ A friendly note from the maintainer:**
|
||||
|
||||
> [!NOTE]
|
||||
> We are a small open-source project with a small community.
|
||||
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||
> But your issue will eventually get attention regardless.
|
||||
> <p align="right">@undergroundwires</p>
|
||||
|
||||
---
|
||||
104
.github/ISSUE_TEMPLATE/2-bug-report-general.yaml
vendored
@@ -1,104 +0,0 @@
|
||||
name: "Bug Report: General"
|
||||
description: 🐛 Report general issues to enhance privacy.sexy
|
||||
labels: [ 'bug' ]
|
||||
title: '[Bug]: '
|
||||
body:
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||
Please complete as much of the form below as possible.
|
||||
Your feedback is valuable, even if you can't provide all details.
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Provide a clear and concise description of the issue.
|
||||
placeholder: >-
|
||||
For example: "I cannot select any scripts."
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: |-
|
||||
This is the most important information in the bug report.
|
||||
Bugs that cannot be reproduced cannot be fixed or verified.
|
||||
placeholder: |-
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Describe what you expected to happen when the error occurred.
|
||||
placeholder: >-
|
||||
For example: "I expected the settings menu to open smoothly without crashing.".
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: |-
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
> **💡 Tip:** You can attach screenshots by clicking this area to highlight it and then pasting them or dragging files in.
|
||||
placeholder: >-
|
||||
Attach screenshots here or link to image hosting.
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: privacy.sexy environment details
|
||||
description: |-
|
||||
If applicable, mention how you were using privacy.sexy when the bug occurred:
|
||||
|
||||
- Web (on which operating system and browser?)
|
||||
- Or desktop (Windows, macOS, or Linux?)
|
||||
placeholder: >-
|
||||
For example: "The web version on Edge browser on Windows 11 23H2."
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |-
|
||||
If applicable, add any other context about the problem here.
|
||||
|
||||
Helpful information includes:
|
||||
|
||||
- Application logs (desktop version only), see: [how to find application logs](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#logging).
|
||||
- Terminal output
|
||||
- Proposed solutions
|
||||
- Other related context such as related issues, software behavior, etc.
|
||||
|
||||
> **💡 Tip:** You can attach log files by dragging them into this area.
|
||||
placeholder: >-
|
||||
For example: "Here are the logs I get from the privacy.sexy 0.13.2 desktop application: ..."
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
---
|
||||
|
||||
**✉️ A friendly note from the maintainer:**
|
||||
|
||||
> [!NOTE]
|
||||
> We are a small open-source project with a small community.
|
||||
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||
> But your issue will eventually get attention regardless.
|
||||
> <p align="right">@undergroundwires</p>
|
||||
|
||||
---
|
||||
55
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: Bug report (unrelated to generated scripts)
|
||||
about: Create a bug report that's not related to generated scripts to help privacy.sexy improve
|
||||
labels: bug
|
||||
title: '[BUG]: '
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for reporting an issue.
|
||||
Please fill in as much of the template below as you're able.
|
||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
||||
-->
|
||||
|
||||
### Description
|
||||
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
### Reproduction steps
|
||||
|
||||
<!--
|
||||
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
### Screenshots
|
||||
|
||||
<!--
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
-->
|
||||
|
||||
### Distribution
|
||||
|
||||
<!--
|
||||
If applicable, mention how you were using privacy.sexy when the bug was encountered:
|
||||
- Web (on Desktop or mobile?)
|
||||
- Or desktop (Windows, macOS or Linux?)
|
||||
-->
|
||||
|
||||
### Additional context
|
||||
|
||||
<!--
|
||||
If applicable, add any other context about the problem here.
|
||||
-->
|
||||
36
.github/ISSUE_TEMPLATE/3-feature-request.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for privacy.sexy
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for suggesting an idea to improve privacy better 🤗.
|
||||
Please fill in as much of the template below as you're able.
|
||||
-->
|
||||
|
||||
### Problem description
|
||||
|
||||
<!--
|
||||
What are we trying to solve?
|
||||
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||
E.g. I'm always frustrated when [...]
|
||||
-->
|
||||
|
||||
### Proposed solution
|
||||
|
||||
<!--
|
||||
Describe the solution you'd like in a clear and concise manner.
|
||||
-->
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
<!--
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
### Additional information
|
||||
|
||||
<!--
|
||||
If applicable, add any other context or screenshots about the feature request here.
|
||||
-->
|
||||
73
.github/ISSUE_TEMPLATE/3-suggestion-feature.yaml
vendored
@@ -1,73 +0,0 @@
|
||||
name: "Suggestion: Feature"
|
||||
description: 💡 Suggest new ideas to enhance privacy.sexy
|
||||
labels: [ 'enhancement' ]
|
||||
title: '[Feature]: '
|
||||
body:
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||
Please complete as much of the form below as possible.
|
||||
Your feedback is valuable, even if you can't provide all details.
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Problem statement
|
||||
description: |-
|
||||
What are we trying to solve?
|
||||
|
||||
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||
placeholder: >-
|
||||
For example: "Every time I use the app, I struggle with..."
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: |-
|
||||
Describe the solution you'd like in a clear and concise manner.
|
||||
placeholder: >-
|
||||
For example: "It would be great if the app could..."
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: |-
|
||||
Have you considered any alternative solutions or features?
|
||||
Different perspectives can inspire new ideas.
|
||||
placeholder: >-
|
||||
For example: "We could also solve it by...".
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: |-
|
||||
If applicable, add any other context or screenshots about the feature request here.
|
||||
|
||||
> **💡 Tip:** You can attach files or screenshots by dragging them into this area.
|
||||
placeholder: >-
|
||||
For example: "Challenges can be ..., but I'm unsure about ..., here is some documentation about it: ..."
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
---
|
||||
|
||||
**✉️ A friendly note from the maintainer:**
|
||||
|
||||
> [!NOTE]
|
||||
> We are a small open-source project with a small community.
|
||||
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||
> But your issue will eventually get attention regardless.
|
||||
> <p align="right">@undergroundwires</p>
|
||||
|
||||
---
|
||||
60
.github/ISSUE_TEMPLATE/4-new-script-suggestion.md
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: New script suggestion
|
||||
about: Suggest a new script for privacy.sexy
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for contributing to privacy.sexy! 🌟
|
||||
For guidance, see our script guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md.
|
||||
Consider submitting a PR for faster implementation: https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md#extend-scripts.
|
||||
-->
|
||||
|
||||
### Operating system
|
||||
|
||||
<!--
|
||||
Specify the OS: Windows, macOS, or Linux.
|
||||
-->
|
||||
|
||||
### Name
|
||||
|
||||
<!--
|
||||
Suggest a name for the script.
|
||||
Naming conventions: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#name.
|
||||
-->
|
||||
|
||||
### Code
|
||||
|
||||
<!--
|
||||
Provide or explain the code to execute when the script runs.
|
||||
Code guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#code.
|
||||
-->
|
||||
|
||||
### Revert code
|
||||
|
||||
<!--
|
||||
Include code to revert changes to the default state.
|
||||
Leave blank for non-reversible scripts.
|
||||
-->
|
||||
|
||||
### Category
|
||||
|
||||
<!--
|
||||
Suggest a category for the script.
|
||||
If unsure, leave blank for maintainers to decide.
|
||||
-->
|
||||
|
||||
### Recommendation level
|
||||
|
||||
<!--
|
||||
Suggest a recommendation level: STANDARD (non-breaking), STRICT (limits functionality), or NONE (for advanced users).
|
||||
If unsure, leave blank for maintainers to decide.
|
||||
-->
|
||||
|
||||
### Documentation/References
|
||||
|
||||
<!--
|
||||
Provide any relevant documentation or references.
|
||||
Prefer high-quality sources such as vendor documentation.
|
||||
Documentation guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#documentation.
|
||||
-->
|
||||
133
.github/ISSUE_TEMPLATE/4-suggestion-new-script.yaml
vendored
@@ -1,133 +0,0 @@
|
||||
name: "Suggestion: New Script"
|
||||
description: 💡 Suggest new scripts to enhance privacy.sexy
|
||||
labels: [ 'enhancement' ]
|
||||
title: '[New script]: '
|
||||
body:
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||
Please complete as much of the form below as possible.
|
||||
Your feedback is valuable, even if you can't provide all details.
|
||||
|
||||
For guidance, see our [script guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md).
|
||||
Consider submitting a PR to get your script added more quickly: (see [CONTRIBUTING.md](https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md#extend-scripts))
|
||||
-
|
||||
type: dropdown
|
||||
attributes:
|
||||
label: Operating system
|
||||
description: Which operating system will the new script configure?
|
||||
options:
|
||||
- macOS
|
||||
- Windows
|
||||
- Linux
|
||||
- All of them
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Name of the script
|
||||
description: |-
|
||||
Suggest a name for the script that clearly describes its function.
|
||||
|
||||
See [script naming conventions](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#name) for best practices.
|
||||
placeholder: E.g, "Disable error data submission"
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Documentation/References
|
||||
description: |-
|
||||
Provide any relevant documentation or references.
|
||||
Prefer high-quality sources such as vendor documentation.
|
||||
|
||||
See [documentation guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#documentation) for best practices.
|
||||
placeholder: >-
|
||||
For example: "This script will disable the error data submission, see https://microsoft.com/...".
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Code
|
||||
description: |-
|
||||
If possible, provide or explain the code that the script should execute.
|
||||
|
||||
See [script code guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#code).
|
||||
placeholder: |-
|
||||
For example: "Set registry key like this `reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "AllowTelemetry" /t "REG_DWORD" /d "1"`".
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Revert code
|
||||
description: |-
|
||||
If applicable, provide revert code to restore the changes made by the script.
|
||||
|
||||
The revert code restores changes to their default state before script execution.
|
||||
|
||||
Leave blank for non-reversible scripts.
|
||||
placeholder: |-
|
||||
For example: "Revert to operating system default like this `reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "AllowTelemetry" /t "REG_DWORD" /d "0"`".
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Suggested category
|
||||
description: |-
|
||||
Suggest a category for the script.
|
||||
|
||||
If unsure, leave blank for maintainers to decide.
|
||||
placeholder: >-
|
||||
For example: "Privacy Cleanup > Clear system logs"
|
||||
-
|
||||
type: dropdown
|
||||
attributes:
|
||||
label: Recommendation level
|
||||
description: |-
|
||||
Suggest a recommendation level for the script:
|
||||
|
||||
- **Standard**: Recommended for most users without side-effects.
|
||||
- **Strict**: Provides improved privacy at the cost of some functionality.
|
||||
- **None**: For advanced users or specific needs.
|
||||
|
||||
If unsure, leave blank for maintainers to decide.
|
||||
options:
|
||||
- Standard
|
||||
- Strict
|
||||
- None (do not recommend)
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: |-
|
||||
If applicable, add any other context or screenshots about the script request here.
|
||||
|
||||
> **💡 Tip:** You can attach additional documents or screenshots by dragging them into this area or pasting directly.
|
||||
placeholder: >-
|
||||
For example: "Challenges can be ..., but I am unsure about ..."
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
---
|
||||
|
||||
**✉️ A friendly note from the maintainer:**
|
||||
|
||||
> [!NOTE]
|
||||
> We are a small open-source project with a small community.
|
||||
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||
> But your issue will eventually get attention regardless.
|
||||
> <p align="right">@undergroundwires</p>
|
||||
|
||||
---
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,7 +1 @@
|
||||
# This file must be named `config.yml`. GitHub does not recognize the file if it is named `config.yaml`.
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Donate
|
||||
url: https://undergroundwires.dev/donate/
|
||||
about: ❤️ Donate to support the free software you love to keep it alive.
|
||||
# A separate link for reporting vulnerabilities is not included here because GitHub generates it automatically.
|
||||
blank_issues_enabled: true
|
||||
32
.github/actions/force-ipv4/README.md
vendored
@@ -1,32 +0,0 @@
|
||||
# force-ipv4
|
||||
|
||||
## Overview
|
||||
|
||||
This GitHub action enforces IPv4 for all outgoing network requests. It addresses connectivity issues encountered in GitHub runners, where IPv6 requests may lead to timeouts due to the lack of IPv6 support [1] [2].
|
||||
|
||||
## Background
|
||||
|
||||
Some applications attempt network connections over IPv6.
|
||||
Such as requests made by Node's `fetch` API causes `UND_ERR_CONNECT_TIMEOUT` [3] [4] and similar issues [5].
|
||||
This happens when the software cannot handle this such as by using Happy Eyeballs [6] [7].
|
||||
|
||||
## Usage
|
||||
|
||||
To use this action in your GitHub workflow, add the following step before any job that requires network access:
|
||||
|
||||
```yaml
|
||||
- name: Enforce IPv4 Connectivity
|
||||
uses: ./.github/actions/force-ipv4
|
||||
```
|
||||
|
||||
## Note
|
||||
|
||||
This action is a workaround addressing specific IPv6-related connectivity issues on GitHub runners and may not be necessary if GitHub's infrastructure evolves to fully support IPv6 in the future.
|
||||
|
||||
[1]: https://archive.ph/2024.03.28-185829/https://github.com/actions/runner/issues/3138 "Actions Runner fails on IPv6 only host · Issue #3138 · actions/runner · GitHub | github.com"
|
||||
[2]: https://archive.ph/2024.03.28-185838/https://github.com/actions/runner-images/issues/668 "IPv6 on GitHub-hosted runners · Issue #668 · actions/runner-images · GitHub | github.com"
|
||||
[3]: https://archive.ph/2024.03.28-185847/https://github.com/actions/runner/issues/3213 "GitHub runner cannot send `fetch` with `node`, failing with IPv6 DNS error `UND_ERR_CONNECT_TIMEOUT` · Issue #3213 · actions/runner · GitHub | github.com"
|
||||
[4]: https://archive.ph/2024.03.28-185853/https://github.com/actions/runner-images/issues/9540 "Cannot send outbound requests using node fetch, failing with IPv6 DNS error UND_ERR_CONNECT_TIMEOUT · Issue #9540 · actions/runner-images · GitHub | github.com"
|
||||
[5]: https://archive.today/2024.03.30-113315/https://github.com/nodejs/node/issues/40537 "\"localhost\" favours IPv6 in node v17, used to favour IPv4 · Issue #40537 · nodejs/node · GitHub"
|
||||
[6]: https://archive.ph/2024.03.28-185900/https://github.com/nodejs/node/issues/41625 "Happy Eyeballs support (address IPv6 issues in Node 17) · Issue #41625 · nodejs/node · GitHub | github.com"
|
||||
[7]: https://archive.ph/2024.03.28-185910/https://github.com/nodejs/undici/issues/1531 "fetch times out in under 5 seconds · Issue #1531 · nodejs/undici · GitHub | github.com"
|
||||
12
.github/actions/force-ipv4/action.yml
vendored
@@ -1,12 +0,0 @@
|
||||
inputs:
|
||||
project-root:
|
||||
required: false
|
||||
default: '.'
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
-
|
||||
name: Run prefer IPv4 script
|
||||
shell: bash
|
||||
run: ./.github/actions/force-ipv4/force-ipv4.sh
|
||||
working-directory: ${{ inputs.project-root }}
|
||||
80
.github/actions/force-ipv4/force-ipv4.sh
vendored
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
main() {
|
||||
if is_linux; then
|
||||
echo 'Configuring Linux...'
|
||||
|
||||
configure_warp_with_doh_and_ipv6_exclusion_on_linux # [WORKS] Resolves the issue when run independently on GitHub runners lacking IPv6 support.
|
||||
prefer_ipv4_on_linux # [DOES NOT WORK] It does not resolve the issue when run independently on GitHub runners without IPv6 support.
|
||||
|
||||
# Considered alternatives:
|
||||
# - `sysctl` commands, and direct changes to `/proc/sys/net/` and `/etc/sysctl.conf` led to silent
|
||||
# Node 18 exits (code: 13) when using `fetch`.
|
||||
elif is_macos; then
|
||||
echo 'Configuring macOS...'
|
||||
|
||||
configure_warp_with_doh_and_ipv6_exclusion_on_macos # [WORKS] Resolves the issue when run independently on GitHub runners lacking IPv6 support.
|
||||
disable_ipv6_on_macos # [WORKS INCONSISTENTLY] Resolves the issue inconsistently when run independently on GitHub runners without IPv6 support.
|
||||
fi
|
||||
echo "IPv4: $(curl --ipv4 --silent --max-time 15 --retry 3 --user-agent Mozilla https://api.ip.sb/geoip)"
|
||||
echo "IPv6: $(curl --ipv6 --silent --max-time 15 --retry 3 --user-agent Mozilla https://api.ip.sb/geoip)"
|
||||
}
|
||||
|
||||
is_linux() {
|
||||
[[ "$(uname -s)" == "Linux" ]]
|
||||
}
|
||||
|
||||
is_macos() {
|
||||
[[ "$(uname -s)" == "Darwin" ]]
|
||||
}
|
||||
|
||||
configure_warp_with_doh_and_ipv6_exclusion_on_linux() {
|
||||
install_warp_on_debian
|
||||
configure_warp_doh_and_exclude_ipv6
|
||||
}
|
||||
|
||||
configure_warp_with_doh_and_ipv6_exclusion_on_macos() {
|
||||
brew install cloudflare-warp
|
||||
configure_warp_doh_and_exclude_ipv6
|
||||
}
|
||||
|
||||
configure_warp_doh_and_exclude_ipv6() {
|
||||
echo 'Beginning configuration of the Cloudflare WARP client with DNS-over-HTTPS and IPv6 exclusion...'
|
||||
echo 'Initiating client registration with Cloudflare...'
|
||||
warp-cli --accept-tos registration new
|
||||
echo 'Configuring WARP to operate in DNS-over-HTTPS mode (warp+doh)...'
|
||||
warp-cli --accept-tos mode warp+doh
|
||||
echo 'Excluding IPv6 traffic from WARP by configuring it as a split tunnel...'
|
||||
warp-cli --accept-tos add-excluded-route '::/0' # Exclude IPv6, forcing IPv4 resolution
|
||||
# `tunnel ip add` does not work with IP ranges, see https://community.cloudflare.com/t/cant-cidr-for-split-tunnling/630834
|
||||
echo 'Establishing WARP connection...'
|
||||
warp-cli --accept-tos connect
|
||||
}
|
||||
|
||||
install_warp_on_debian() {
|
||||
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | sudo gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflare-client.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y cloudflare-warp
|
||||
}
|
||||
|
||||
disable_ipv6_on_macos() {
|
||||
networksetup -listallnetworkservices \
|
||||
| tail -n +2 \
|
||||
| while IFS= read -r interface; do
|
||||
echo "Disabling IPv6 on: $interface..."
|
||||
networksetup -setv6off "$interface"
|
||||
done
|
||||
}
|
||||
|
||||
prefer_ipv4_on_linux() {
|
||||
local -r gai_config_file_path='/etc/gai.conf'
|
||||
if [ ! -f "$gai_config_file_path" ]; then
|
||||
echo "Creating $gai_config_file_path since it doesn't exist..."
|
||||
touch "$gai_config_file_path"
|
||||
fi
|
||||
echo "precedence ::ffff:0:0/96 100" | sudo tee -a "$gai_config_file_path" > /dev/null
|
||||
echo "Configuration complete."
|
||||
}
|
||||
|
||||
main
|
||||
3
.github/actions/setup-node/action.yml
vendored
@@ -5,5 +5,4 @@ runs:
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
# check-latest: true # Newest versions can potentially have undiscovered bugs or regressions
|
||||
node-version: 18.x
|
||||
|
||||
15
.github/actions/upload-artifact/action.yaml
vendored
@@ -1,15 +0,0 @@
|
||||
inputs:
|
||||
name:
|
||||
required: true
|
||||
path:
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
-
|
||||
name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.name }}
|
||||
path: ${{ inputs.path }}
|
||||
17
.github/workflows/checks.build.yaml
vendored
@@ -72,19 +72,16 @@ jobs:
|
||||
build-docker:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- macos-13 # Downgraded due to lack of nested virtualization support in ARM-based runners (See: actions/runner-images#9460, actions/runner-images#9741, abiosoft/colima#1023)
|
||||
- ubuntu-latest
|
||||
# - windows-latest # Windows runners do not support Linux containers
|
||||
os: [ macos, ubuntu ] # Windows runners do not support Linux containers
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Install Docker on macOS
|
||||
if: contains(matrix.os, 'macos') # macOS runner is missing Docker
|
||||
if: matrix.os == 'macos' # macOS runner is missing Docker
|
||||
run: |-
|
||||
# Install Docker
|
||||
brew install docker
|
||||
@@ -98,12 +95,6 @@ jobs:
|
||||
-
|
||||
name: Run Docker image on port 8080
|
||||
run: docker run -d -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest
|
||||
-
|
||||
name: Enforce IPv4 Connectivity # Used due to GitHub runners' lack of IPv6 support, preventing request timeouts.
|
||||
uses: ./.github/actions/force-ipv4
|
||||
-
|
||||
name: Check server is up and returns HTTP 200
|
||||
run: >-
|
||||
node ./scripts/verify-web-server-status.js \
|
||||
--url http://localhost:8080 \
|
||||
--max-retries ${{ matrix.os == 'macos' && '90' || '30' }}
|
||||
run: node ./scripts/verify-web-server-status.js --url http://localhost:8080
|
||||
|
||||
@@ -9,13 +9,9 @@ jobs:
|
||||
run-check:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- macos-latest # Apple silicon (ARM64)
|
||||
- macos-13 # Intel-based (x86-64)
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@@ -28,7 +24,7 @@ jobs:
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Configure Ubuntu
|
||||
if: contains(matrix.os, 'ubuntu') # macOS runner is missing Docker
|
||||
if: matrix.os == 'ubuntu'
|
||||
shell: bash
|
||||
run: |-
|
||||
sudo apt update
|
||||
@@ -70,7 +66,7 @@ jobs:
|
||||
-
|
||||
name: Upload screenshot
|
||||
if: always() # Run even if previous step fails
|
||||
uses: ./.github/actions/upload-artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: screenshot-${{ matrix.os }}
|
||||
path: screenshot.png
|
||||
|
||||
11
.github/workflows/checks.external-urls.yaml
vendored
@@ -1,9 +1,11 @@
|
||||
name: checks.external-urls
|
||||
|
||||
on:
|
||||
push:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||
push:
|
||||
paths:
|
||||
- tests/checks/external-urls/**
|
||||
|
||||
jobs:
|
||||
run-check:
|
||||
@@ -18,13 +20,6 @@ jobs:
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Enforce IPv4 Connectivity # Used due to GitHub runners' lack of IPv6 support, preventing request timeouts.
|
||||
uses: ./.github/actions/force-ipv4
|
||||
-
|
||||
name: Test
|
||||
run: npm run check:external-urls
|
||||
env:
|
||||
RANDOMIZED_URL_CHECK_LIMIT: "${{ github.event_name == 'push' && '100' || '3000' }}"
|
||||
# - Scheduled checks has high limit for thorough testing.
|
||||
# - For push events, triggered by code changes, the amount of URLs are limited to provide quick feedback.
|
||||
|
||||
75
.github/workflows/checks.quality.yaml
vendored
@@ -1,10 +1,10 @@
|
||||
name: checks.quality
|
||||
name: quality-checks
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
lint-command:
|
||||
@@ -28,74 +28,3 @@ jobs:
|
||||
-
|
||||
name: Lint
|
||||
run: ${{ matrix.lint-command }}
|
||||
|
||||
todo-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Scan latest commit for TODO comments
|
||||
shell: bash
|
||||
run: |-
|
||||
readonly todo_comment_search_pattern='TODO'':' # Define search pattern in parts to prevent IDE from flagging this script line as a TODO item
|
||||
if git grep "$todo_comment_search_pattern" HEAD; then
|
||||
echo 'TODO comments found in the latest commit.'
|
||||
exit 1
|
||||
else
|
||||
echo 'No TODO comments found in the latest commit.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pylint:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Still interested to see results from other combinations
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
-
|
||||
name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pylint
|
||||
-
|
||||
name: Analyzing the code with pylint
|
||||
run: npm run lint:pylint
|
||||
|
||||
validate-collection-files:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Still interested to see results from other combinations
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
-
|
||||
name: Install dependencies
|
||||
run: python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
|
||||
-
|
||||
name: Validate
|
||||
run: python3 ./scripts/validate-collections-yaml
|
||||
|
||||
32
.github/workflows/checks.scripts.yaml
vendored
@@ -15,10 +15,6 @@ jobs:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Install ImageMagick on macOS
|
||||
if: matrix.os == 'macos'
|
||||
run: brew install imagemagick
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
@@ -57,31 +53,3 @@ jobs:
|
||||
-
|
||||
name: Run install-deps
|
||||
run: ${{ matrix.install-command }}
|
||||
|
||||
configure-vscode:
|
||||
runs-on: ${{ matrix.os.name }}-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- name: macos
|
||||
install-vscode-command: brew install --cask visual-studio-code
|
||||
- name: ubuntu
|
||||
install-vscode-command: sudo snap install code --classic
|
||||
- name: windows
|
||||
install-vscode-command: choco install vscode
|
||||
fail-fast: false # Still interested to see results from other combinations
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
-
|
||||
name: Install VSCode
|
||||
run: ${{ matrix.os.install-vscode-command }}
|
||||
-
|
||||
name: Configure VSCode
|
||||
run: python3 ./scripts/configure_vscode.py
|
||||
|
||||
4
.github/workflows/tests.e2e.yaml
vendored
@@ -51,14 +51,14 @@ jobs:
|
||||
-
|
||||
name: Upload screenshots
|
||||
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
|
||||
uses: ./.github/actions/upload-artifact
|
||||
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: ./.github/actions/upload-artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: e2e-videos-${{ matrix.os }}
|
||||
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}
|
||||
|
||||
4
.gitignore
vendored
@@ -14,7 +14,3 @@ node_modules
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
.venv
|
||||
4
.vscode/extensions.json
vendored
@@ -5,10 +5,8 @@
|
||||
"wengerk.highlight-bad-chars", // Highlights bad chars.
|
||||
"wayou.vscode-todo-highlight", // Highlights TODO.
|
||||
"wix.vscode-import-cost", // Shows in KB how much a require include in code.
|
||||
// Markdown
|
||||
// Documentation
|
||||
"davidanson.vscode-markdownlint", // Lints markdown.
|
||||
// YAML
|
||||
"redhat.vscode-yaml", // Lints YAML files, validates against schema.
|
||||
// TypeScript / JavaScript
|
||||
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
|
||||
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.
|
||||
|
||||
120
CHANGELOG.md
@@ -1,125 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.13.5 (2024-06-26)
|
||||
|
||||
* ci/cd: centralize and bump artifact uploads | [22d6c79](https://github.com/undergroundwires/privacy.sexy/commit/22d6c7991eb2c138578a7d41950f301906dbf703)
|
||||
* win: document and improve Firefox telemetry #259 | [8341411](https://github.com/undergroundwires/privacy.sexy/commit/8341411be434c6d145e942b1792020ccf02f58c8)
|
||||
* Add image to `README.md` to thank supporters | [fa2a92b](https://github.com/undergroundwires/privacy.sexy/commit/fa2a92bf893933bf5cd04512a712b7aa1b921277)
|
||||
* win: improve executable blocking, Chrome reporting | [f21ef92](https://github.com/undergroundwires/privacy.sexy/commit/f21ef9250a2f459dbd4f789d857c78298fc202e6)
|
||||
* mac: discourage and document captive portal script | [b29cd7b](https://github.com/undergroundwires/privacy.sexy/commit/b29cd7b5f74accf92c9700c3171670f82c8cb3b3)
|
||||
* win: fix revert scripts for removing shortcuts | [8becc7d](https://github.com/undergroundwires/privacy.sexy/commit/8becc7dbc46af4441900e9841a716a53735bc82e)
|
||||
* Refactor to unify scripts/categories as Executable | [c138f74](https://github.com/undergroundwires/privacy.sexy/commit/c138f74460bafaba3da55a65f3942bb6f95b1d99)
|
||||
* Add object property validation in parser #369 | [6ecfa9b](https://github.com/undergroundwires/privacy.sexy/commit/6ecfa9b954edc10401acaf5c735eec0fc9f991cd)
|
||||
* win: fix missing app access recommendations #369 | [1c2d82d](https://github.com/undergroundwires/privacy.sexy/commit/1c2d82dc9bd412ea601ab2550ba0b4f7d144f8e8)
|
||||
* win: fix text and handwriting script omission #369 | [1a10cf2](https://github.com/undergroundwires/privacy.sexy/commit/1a10cf2e5f87cd8eb421ef77f6ce764b5482515e)
|
||||
* mac: document, improve, encourage clearing logs | [e9a5285](https://github.com/undergroundwires/privacy.sexy/commit/e9a52859f63609c3f56def0b3e4d1ac6e5661536)
|
||||
* Add schema validation for collection files #369 | [dc03bff](https://github.com/undergroundwires/privacy.sexy/commit/dc03bff324d673101002bb16f14e0429e8170fbb)
|
||||
* win: fix incomplete VSCEIP, location scripts | [48761f6](https://github.com/undergroundwires/privacy.sexy/commit/48761f62a242f0910307994271cbe6730fb30f7e)
|
||||
* Add type validation for parameters and fix types | [fac26a6](https://github.com/undergroundwires/privacy.sexy/commit/fac26a6ca07479c84fe62c5ea2a572dad1898ef8)
|
||||
* Bump Electron to latest | [ed93614](https://github.com/undergroundwires/privacy.sexy/commit/ed93614ca34b1ab166e645cc5bedd497b0caeaac)
|
||||
* Trim compiler error output for better readability | [78c62cf](https://github.com/undergroundwires/privacy.sexy/commit/78c62cfc953dbba543d8bdc42828a4ef4b13a7c7)
|
||||
* win: fix errors due to missing Edge uninstaller | [2f82873](https://github.com/undergroundwires/privacy.sexy/commit/2f828735a87f98ba87b4fc826823d1482d4f2db2)
|
||||
* win: fix latest Edge removal on Windows 10 #309 | [e7031a3](https://github.com/undergroundwires/privacy.sexy/commit/e7031a3ae4e57b6522c6ca67fc30e8a8718506b2)
|
||||
* win: categorize, rename, doc Chrome & Edge scripts | [f286f92](https://github.com/undergroundwires/privacy.sexy/commit/f286f92b1fec49e89eea8982dffbc3d6ef1defde)
|
||||
* win: add disabling Edge/WebView2 auto-updates #309 | [ed7e69c](https://github.com/undergroundwires/privacy.sexy/commit/ed7e69c07efe83fdb7f4af13aa220ff991fbbe59)
|
||||
* win, linux, mac: fix typos #373 | [c09c5ff](https://github.com/undergroundwires/privacy.sexy/commit/c09c5ffa47865f7c76910644558b6783ed44f1e4)
|
||||
* win: add more Edge scripts including AI & ads | [1430d52](https://github.com/undergroundwires/privacy.sexy/commit/1430d5215ab094d8201710761d631dc2bd740918)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.4...0.13.5)
|
||||
|
||||
## 0.13.4 (2024-05-27)
|
||||
|
||||
* Add specific empty function name compiler error | [870120b](https://github.com/undergroundwires/privacy.sexy/commit/870120bc13909a3681e0f0a2351806849476342f)
|
||||
* ci/cd: fix recent Docker build failures on macOS | [a1922c5](https://github.com/undergroundwires/privacy.sexy/commit/a1922c50c12b3b7806e9e681ace842194a178bda)
|
||||
* win: standardize registry edit + delete on revert | [cec0b4b](https://github.com/undergroundwires/privacy.sexy/commit/cec0b4b4f63c3563a0e7923ce6324a38d71a3955)
|
||||
* Fix e2e test failing on Windows | [4a7efa2](https://github.com/undergroundwires/privacy.sexy/commit/4a7efa27c8df73ef9b7960afed29f216b066cba2)
|
||||
* Add support for macOS universal binary #348, #362 | [d25c4e8](https://github.com/undergroundwires/privacy.sexy/commit/d25c4e8c812b8d012010ba38070a2931dcd28908)
|
||||
* Migrate to GitHub issue forms | [9ab3ff7](https://github.com/undergroundwires/privacy.sexy/commit/9ab3ff75b0a69ac2ba27dd02e82db9b5bd76ea0f)
|
||||
* ci/cd: fix quality checks not running on all OSes | [2390530](https://github.com/undergroundwires/privacy.sexy/commit/2390530d929fb92c266558c52376569a0ecb90c1)
|
||||
* Bump Vue to latest and fix universal selector CSS | [aae5434](https://github.com/undergroundwires/privacy.sexy/commit/aae54344511ec51d17ad0420a92cb5a064e0e7bb)
|
||||
* Centralize and optimize `ResizeObserver` usage | [2923621](https://github.com/undergroundwires/privacy.sexy/commit/292362135db0519ec1050bab80ed373aad115731)
|
||||
* win: improve app access disabling and docs #138 | [ff3d5c4](https://github.com/undergroundwires/privacy.sexy/commit/ff3d5c48419f663379f5aba8936636c22f2c5de8)
|
||||
* win: document and discourage RSA key script #363 | [f347fde](https://github.com/undergroundwires/privacy.sexy/commit/f347fde0c85f8b51b0060fdea0a2724b042aaeed)
|
||||
* win: improve printing removal /w Print Queue #279 | [150e067](https://github.com/undergroundwires/privacy.sexy/commit/150e0670392bb62348c20ec644a4ed8a6bbffe74)
|
||||
* win: discourage blocking app access #121 #339 #350 | [7794846](https://github.com/undergroundwires/privacy.sexy/commit/77948461856e6837ddfbcbbef72a1bf9fc706b4e)
|
||||
* Improve context for errors thrown by compiler | [4212c7b](https://github.com/undergroundwires/privacy.sexy/commit/4212c7b9e0b1500378a1e4e88efc2d59f39f3d29)
|
||||
* win: document disabling firewall #115 #152 #364 | [12b1f18](https://github.com/undergroundwires/privacy.sexy/commit/12b1f183f7ce966d6ce090d98aeea7ec491f8c7c)
|
||||
* win: add script to disable Recall feature | [ce4cfdd](https://github.com/undergroundwires/privacy.sexy/commit/ce4cfdd169b7da0edc3da61143c988ed5f3c976e)
|
||||
* win, mac, linux: fix typos and dead URLs #367 | [9e34e64](https://github.com/undergroundwires/privacy.sexy/commit/9e34e644493674ca709b64a47206763d5d4bd60c)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.3...0.13.4)
|
||||
|
||||
## 0.13.3 (2024-05-11)
|
||||
|
||||
* win: organize and document network disablement | [2eed6f4](https://github.com/undergroundwires/privacy.sexy/commit/2eed6f4afb6cf85fdc1d6acb808f82405a35cafd)
|
||||
* win: improve disabling SMBv1 protocol | [f584fab](https://github.com/undergroundwires/privacy.sexy/commit/f584fabb50c7de70ba43751d721af94d8fa2fa8a)
|
||||
* win: improve disabling insecure renegotiations | [f261ab4](https://github.com/undergroundwires/privacy.sexy/commit/f261ab4cd9a53e31325e5c6da9129542971fe84b)
|
||||
* win: doc, improve, encourage cipher disabling | [8b224ee](https://github.com/undergroundwires/privacy.sexy/commit/8b224eefe71be6a556a1085d8fe20dbd4b889430)
|
||||
* ci/cd: add check for TODO comments | [4e21f05](https://github.com/undergroundwires/privacy.sexy/commit/4e21f05031d6cc90cda684bd598bec4735f8103b)
|
||||
* win: improve 'Snipping Tool' removal #343 | [e18907c](https://github.com/undergroundwires/privacy.sexy/commit/e18907ca91e483255b44d14d7d923d7eef92afbd)
|
||||
* ci/cd: lint Python scripts using `pylint` | [23bac0f](https://github.com/undergroundwires/privacy.sexy/commit/23bac0fc76ad697abb34f3fb327df5cdeb40286a)
|
||||
* win: improve disabling insecure hashes #131 | [d19dde6](https://github.com/undergroundwires/privacy.sexy/commit/d19dde603ddac47022ee2e0ea865d53857560c26)
|
||||
* Add system requirements documentation #134 | [0fc2ffc](https://github.com/undergroundwires/privacy.sexy/commit/0fc2ffc1ea36a9248c6a92da85a29f7b04b33796)
|
||||
* win, linux, mac: fix various typos #349 | [694bf1a](https://github.com/undergroundwires/privacy.sexy/commit/694bf1a74d935531d7cd46891823af1fa58c3c8c)
|
||||
* Fix script cancellation with new dialog on Linux | [8c17396](https://github.com/undergroundwires/privacy.sexy/commit/8c173962857a39dc0c9e5886cb2af4937e6618e7)
|
||||
* win: improve disabling protocols | [4ef16ce](https://github.com/undergroundwires/privacy.sexy/commit/4ef16cea56789120cd041412d86b5577cccf0725)
|
||||
* win: fix Copilot by excluding `r.bing.com` #329 | [66a5688](https://github.com/undergroundwires/privacy.sexy/commit/66a56888a4b3ead1a6bfef0feffa0218535701fe)
|
||||
* Fix blank window on load on desktop version #348 | [813d820](https://github.com/undergroundwires/privacy.sexy/commit/813d820b85e1b623c50f8e0325ad372bf2f344f9)
|
||||
* Improve desktop icon quality and generation | [ab25e0a](https://github.com/undergroundwires/privacy.sexy/commit/ab25e0a066be14ea979dafd0f80e1091bd5d33f8)
|
||||
* win: improve enabling secure connections #175 | [c75df1c](https://github.com/undergroundwires/privacy.sexy/commit/c75df1c8c1151b64cbf014383dea0b748a8c78b3)
|
||||
* Fix VSCode script issues with added CI/CD tests | [1d7cafc](https://github.com/undergroundwires/privacy.sexy/commit/1d7cafc831dcc339a10646794410dad7096bfe60)
|
||||
* Fix win execution with whitespace in username #351 | [a334320](https://github.com/undergroundwires/privacy.sexy/commit/a3343205b1196d5a81fd3cee2ae661ce871a7bef)
|
||||
* Fix misaligned tooltip positions in modal dialogs | [dd71536](https://github.com/undergroundwires/privacy.sexy/commit/dd71536316ec819caeb418b8635d544ac80e58ad)
|
||||
* Fix Chromium scrollbar-induced layout shifts | [bc4879c](https://github.com/undergroundwires/privacy.sexy/commit/bc4879cfe97becac3c54f6b40780a89464d3b772)
|
||||
* ci/cd: remove `check-latest` from `setup-node` | [52a4730](https://github.com/undergroundwires/privacy.sexy/commit/52a4730073b8ebfb2ce9d530b44e4a179f5849fe)
|
||||
* win: categorize and rename network security #131 | [9fd193e](https://github.com/undergroundwires/privacy.sexy/commit/9fd193e676f1f0646898f5130fbfaaf25050b2e3)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.2...0.13.3)
|
||||
|
||||
## 0.13.2 (2024-04-15)
|
||||
|
||||
* Update documentation for `logo-update.js` script | [4a9b430](https://github.com/undergroundwires/privacy.sexy/commit/4a9b430702bc6082426b50ecc3a06362b5720796)
|
||||
* win: improve and document removing Phone apps #279 | [8924337](https://github.com/undergroundwires/privacy.sexy/commit/89243371faa5d6aef5fce52b0d54a442143cdd39)
|
||||
* Fix bottom gap in card expansion panel | [79183d6](https://github.com/undergroundwires/privacy.sexy/commit/79183d64173e588d88bf074d5b50a52a71c2d885)
|
||||
* ci/cd: Fix macOS Docker build reliability issues | [8a5592f](https://github.com/undergroundwires/privacy.sexy/commit/8a5592f92be4366a806afc9eee9135696a1dd993)
|
||||
* ci/cd: fix IPv6 timeouts with `force-ipv4` action | [52fadcd](https://github.com/undergroundwires/privacy.sexy/commit/52fadcd6177ed06216be9c67dad57192ae02a4f9)
|
||||
* ci/cd: bump Node.js environment to 20.x | [59decd1](https://github.com/undergroundwires/privacy.sexy/commit/59decd17e273bada1493eaa855c43cbabf90308f)
|
||||
* ci/cd: trigger URL checks more, and limit amount | [4fb6302](https://github.com/undergroundwires/privacy.sexy/commit/4fb6302c67f2a3fedff419e8c22872593cf800ef)
|
||||
* Fix overflow in tree node content on small screens | [557cea3](https://github.com/undergroundwires/privacy.sexy/commit/557cea3f4866dc33236874f5fe4d2d69ee963dae)
|
||||
* Fix horizontal layout shift after script selection | [bc7e1fa](https://github.com/undergroundwires/privacy.sexy/commit/bc7e1faa1c3f2b61bf2046fdd6d6a4141b484662)
|
||||
* Fix card header expansion glitch on card collapse | [5d940b5](https://github.com/undergroundwires/privacy.sexy/commit/5d940b57ef2a4c219932cd15201401f8550cfb41)
|
||||
* Ignore `ResizeObserver` errors in Cypress tests | [4472c28](https://github.com/undergroundwires/privacy.sexy/commit/4472c2852e4b87083bda7979471ab9f377d17a01)
|
||||
* win: improve and document secret key scripts | [49f22f0](https://github.com/undergroundwires/privacy.sexy/commit/49f22f048f39e7388633c488b5fe59101b831984)
|
||||
* Fix card arrow not being animated in sync | [7b546c5](https://github.com/undergroundwires/privacy.sexy/commit/7b546c567c4683a37fe94595362f4c2bf92ffd59)
|
||||
* win: improve Windows feature disablement scripts | [b68711e](https://github.com/undergroundwires/privacy.sexy/commit/b68711ef88982c0ee2b1d41b4452e899821adc64)
|
||||
* Fix top script menu overflow on small screens | [b7a20d9](https://github.com/undergroundwires/privacy.sexy/commit/b7a20d9d41ea8bcefdd553b87641f3c22b4cde97)
|
||||
* win: fix Visual Studio remote analysis script #327 | [4142d08](https://github.com/undergroundwires/privacy.sexy/commit/4142d084f64a3b540487ff68b28032977d12006d)
|
||||
* win: improve firewall docs /w `winget` impact #142 | [ffd647d](https://github.com/undergroundwires/privacy.sexy/commit/ffd647d1529375474b81900cc7bee4c32fbf861f)
|
||||
* Centralize and use global spacing variables | [ae17200](https://github.com/undergroundwires/privacy.sexy/commit/ae172000a64416e5a3e2b2e32b7846f039f445f0)
|
||||
* win: improve service revert and docs | [b87b7aa](https://github.com/undergroundwires/privacy.sexy/commit/b87b7aac7d118a23a0d1bfb881e385347de4adb7)
|
||||
* Bump dependencies to latest, hold ESLint | [f3571ab](https://github.com/undergroundwires/privacy.sexy/commit/f3571abeafdbe1e6d152958fab26de91a9c08bc3)
|
||||
* Fix inability to tap outside modal on mobile | [cb144ae](https://github.com/undergroundwires/privacy.sexy/commit/cb144ae47273deeb7058d4b1380e480ebccdaf81)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.1...0.13.2)
|
||||
|
||||
## 0.13.1 (2024-03-22)
|
||||
|
||||
* ci/cd: Fix cross-platform git command compability | [255c51c](https://github.com/undergroundwires/privacy.sexy/commit/255c51c8a0524d3ea8a3b16ffc1b178650525010)
|
||||
* Fix tooltip falling behind elements on fade out | [1964524](https://github.com/undergroundwires/privacy.sexy/commit/19645248ab7bc78dc872fa176c1a3650d7d6d644)
|
||||
* Improve VSCode detection in `configure_vscode.py` | [98845e6](https://github.com/undergroundwires/privacy.sexy/commit/98845e6caee168db131aaf0736533e450827a52c)
|
||||
* Bump TypeScript to 5.3 with `verbatimModuleSyntax` | [a721e82](https://github.com/undergroundwires/privacy.sexy/commit/a721e82a4fb603c0732ccfdffc87396c2a01363e)
|
||||
* Migrate to Vite 5 and adjust configurations | [4ac1425](https://github.com/undergroundwires/privacy.sexy/commit/4ac1425f76079352268c488f3ff607d1fdc1beb2)
|
||||
* win: improve and unify service start/stop logic | [adc2089](https://github.com/undergroundwires/privacy.sexy/commit/adc20898873d50a8873ffc74c48257e69a45d367)
|
||||
* Upgrade vitest to v1 and fix test definitions | [e721885](https://github.com/undergroundwires/privacy.sexy/commit/e7218850ba62a7bebaf4768b13e46cba0dedd906)
|
||||
* Improve URL checks to reduce false-negatives | [5abf8ff](https://github.com/undergroundwires/privacy.sexy/commit/5abf8ff216a1da737fd489864eeee880f78d6601)
|
||||
* win: improve OneDrive data deletion safety | [5eff3a0](https://github.com/undergroundwires/privacy.sexy/commit/5eff3a04886d0d23a6e4c13a0178bb247105c5cb)
|
||||
* Bump Electron to latest and use native ESM | [840adf9](https://github.com/undergroundwires/privacy.sexy/commit/840adf9429ed47f9e88c05e90f1d3ab930c2dfc4)
|
||||
* Fix tooltip styling inconsistency | [ec34ac1](https://github.com/undergroundwires/privacy.sexy/commit/ec34ac1124e8b8ae53bf31a4dbdc88bb078b3d4e)
|
||||
* win: fix VSCode manual update switch script #312 | [b71ad79](https://github.com/undergroundwires/privacy.sexy/commit/b71ad797a3af0db45143249903cb5e178692de7c)
|
||||
* mac, linux, win: fix dead URLs and improve docs | [abec9de](https://github.com/undergroundwires/privacy.sexy/commit/abec9def075d82fdaee9663ef8fe1a488911f45b)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.0...0.13.1)
|
||||
|
||||
## 0.13.0 (2024-02-11)
|
||||
|
||||
* win: add disabling clipboard features #251, #247 | [c6ebba8](https://github.com/undergroundwires/privacy.sexy/commit/c6ebba85fb1b362be0d81d3078f19db71e0528b2)
|
||||
|
||||
15
README.md
@@ -60,8 +60,8 @@
|
||||
<br />
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Status of quality checks"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.quality/badge.svg"
|
||||
alt="Quality checks status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
|
||||
@@ -122,12 +122,9 @@
|
||||
## Get started
|
||||
|
||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-Setup-0.13.5.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-0.13.5.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-0.13.5.AppImage). For more options, see [here](#additional-install-options).
|
||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.0/privacy.sexy-Setup-0.13.0.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.0/privacy.sexy-0.13.0.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.0/privacy.sexy-0.13.0.AppImage). For more options, see [here](#additional-install-options).
|
||||
|
||||
See also:
|
||||
|
||||
- [Desktop vs. Web Features](./docs/desktop/desktop-vs-web-features.md): Differences and unique aspects of desktop and web versions.
|
||||
- [System Requirements](./docs/desktop/system-requirements.md): Hardware and software requirements for the desktop version.
|
||||
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).
|
||||
|
||||
💡 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.
|
||||
|
||||
@@ -186,7 +183,3 @@ Check [architecture.md](./docs/architecture.md) for an overview of design and ho
|
||||
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).
|
||||
|
||||
## Supporters
|
||||
|
||||
[](https://undergroundwires.dev/supporters)
|
||||
|
||||
15
SECURITY.md
@@ -43,17 +43,10 @@ privacy.sexy adopts a defense in depth strategy to protect users on multiple lay
|
||||
elevation of privileges for system modifications with explicit user consent and logs every action taken with high privileges. This
|
||||
approach actively minimizes potential security risks by limiting privileged operations and aligning with the principle of least privilege.
|
||||
- **Secure Script Execution/Storage:**
|
||||
- **Antivirus scans:**
|
||||
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans.
|
||||
This step allows confirming that the scripts are secure and safe to use.
|
||||
- **Tamper protection:**
|
||||
The application incorporates integrity checks for tamper protection.
|
||||
If the script file differs from the user's selected script, the application will not execute or save the script, ensuring the processing
|
||||
of authentic scripts.
|
||||
This safeguards against any unwanted modifications.
|
||||
- **Clean-up:**
|
||||
Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these scripts.
|
||||
This allows users to maintain their privacy by removing traces of their usage patterns or script preferences.
|
||||
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans. This safeguards against
|
||||
any unwanted modifications. Furthermore, the application incorporates integrity checks for tamper protection. If the script file differs from
|
||||
the user's selected script, the application will not execute or save the script, ensuring the processing of authentic scripts.
|
||||
Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these scripts.
|
||||
|
||||
### Update Security and Integrity
|
||||
|
||||
|
||||
5
build/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# build
|
||||
|
||||
This folder contains files that are used by Electron to serve the desktop version.
|
||||
|
||||
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md).
|
||||
BIN
build/icons/1024x1024.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
build/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
build/icons/16x16.png
Normal file
|
After Width: | Height: | Size: 553 B |
BIN
build/icons/24x24.png
Normal file
|
After Width: | Height: | Size: 963 B |
BIN
build/icons/256x256.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
build/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
build/icons/48x48.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
build/icons/512x512.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
build/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
build/icons/icon.icns
Normal file
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 353 KiB |
@@ -41,5 +41,5 @@ Application layer compiles templating syntax during parsing to create the end sc
|
||||
|
||||
The steps to extend the templating syntax:
|
||||
|
||||
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
|
||||
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).
|
||||
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
|
||||
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Collection files
|
||||
|
||||
privacy.sexy is a data-driven application that reads YAML files.
|
||||
This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model. The YAML schema [`.schema.yaml`](./../src/application/collections/.schema.yaml) is provided to provide better IDE support and be used in automated validations.
|
||||
This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model.
|
||||
|
||||
Related documentation:
|
||||
|
||||
- 📖 [`Collections README`](./../src/application/collections/README.md) includes references to code as documentation.
|
||||
- 📖 [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts) outlines code types.
|
||||
- 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices.
|
||||
|
||||
## Objects
|
||||
@@ -28,22 +28,11 @@ Related documentation:
|
||||
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
|
||||
- Sets the scripting language for all inline code used within the collection.
|
||||
|
||||
### Executables
|
||||
|
||||
They represent independently executable tweaks with documentation and reversibility.
|
||||
|
||||
An Executable is a logical entity that can
|
||||
|
||||
- execute once compiled,
|
||||
- include a `docs` property for documentation.
|
||||
|
||||
It's either [Category](#category) or a [Script](#script).
|
||||
|
||||
#### `Category`
|
||||
### `Category`
|
||||
|
||||
Represents a logical group of scripts and subcategories.
|
||||
|
||||
##### `Category` syntax
|
||||
#### `Category` syntax
|
||||
|
||||
- `category:` *`string`* **(required)**
|
||||
- Name of the category.
|
||||
@@ -54,7 +43,7 @@ Represents a logical group of scripts and subcategories.
|
||||
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||
- Markdown-formatted documentation related to the category.
|
||||
|
||||
#### `Script`
|
||||
### `Script`
|
||||
|
||||
Represents an individual tweak.
|
||||
|
||||
@@ -69,7 +58,7 @@ Types (like [functions](#function)):
|
||||
|
||||
📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md).
|
||||
|
||||
##### `Script` syntax
|
||||
#### `Script` syntax
|
||||
|
||||
- `name`: *`string`* **(required)**
|
||||
- Script name.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Desktop vs. Web Features
|
||||
|
||||
This table outlines the differences between the desktop and web versions of `privacy.sexy`.
|
||||
This table highlights differences between the desktop and web versions of `privacy.sexy`.
|
||||
|
||||
| Feature | Desktop | Web |
|
||||
| ------- | ------- | --- |
|
||||
@@ -8,8 +8,10 @@ This table outlines the differences between the desktop and web versions of `pri
|
||||
| [Offline usage](#offline-usage) | 🟢 Available | 🟡 Partially available |
|
||||
| [Auto-updates](#auto-updates) | 🟢 Available | 🟢 Available |
|
||||
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
|
||||
| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available |
|
||||
| [Script execution](#script-execution) | 🟢 Available | 🔴 Not available |
|
||||
| [Error handling](#error-handling) | 🟢 Advanced | 🟡 Limited |
|
||||
| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available |
|
||||
| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available |
|
||||
|
||||
## Feature descriptions
|
||||
|
||||
@@ -28,11 +30,11 @@ 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).
|
||||
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.
|
||||
[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.
|
||||
@@ -51,7 +53,7 @@ Log file locations vary by operating system:
|
||||
|
||||
> 💡 privacy.sexy provides scripts to securely erase these logs.
|
||||
|
||||
### Secure script execution/storage
|
||||
### Script execution
|
||||
|
||||
The desktop version of privacy.sexy enables direct script execution, providing a seamless and integrated experience.
|
||||
This direct execution capability isn't available in the web version due to inherent browser restrictions.
|
||||
@@ -67,27 +69,31 @@ These locations vary based on the operating system:
|
||||
|
||||
> 💡 privacy.sexy provides scripts to securely erase your script execution history.
|
||||
|
||||
**Script antivirus scans:**
|
||||
|
||||
To enhance system protection, the desktop version of privacy.sexy automatically verifies the security of script
|
||||
execution files by reading them back.
|
||||
This process triggers antivirus scans to verify that scripts are safe before the execution.
|
||||
|
||||
**Script integrity checks:**
|
||||
|
||||
The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage.
|
||||
Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them.
|
||||
If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script.
|
||||
This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability.
|
||||
|
||||
**Error handling:**
|
||||
### Error handling
|
||||
|
||||
The desktop version of privacy.sexy features advanced error handling capabilities.
|
||||
In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes.
|
||||
It employs robust and reliable execution strategies, including self-healing mechanisms, and provides guidance and troubleshooting information to resolve issues effectively.
|
||||
This proactive error handling and user guidance enhances the application's security and reliability.
|
||||
In contrast, the web version has more basic error handling due to browser limitations and the nature of web applications.
|
||||
|
||||
### Native dialogs
|
||||
|
||||
The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs.
|
||||
These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities.
|
||||
|
||||
### Secure script execution/storage
|
||||
|
||||
**Integrity checks:**
|
||||
|
||||
The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage.
|
||||
Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them.
|
||||
If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script.
|
||||
This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability.
|
||||
Due to browser constraints, this feature is absent in the web version.
|
||||
|
||||
**Error handling:**
|
||||
|
||||
In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes.
|
||||
It also guides users through potential issues with filesystem or third-party software, such as antivirus interventions.
|
||||
Specifically, the application is capable of identifying when antivirus software blocks or removes a script, providing users with tailored error messages
|
||||
and detailed resolution steps. This level of proactive error handling and user guidance enhances the application's security and reliability,
|
||||
offering a feature not achievable in the web version due to browser limitations.
|
||||
@@ -1,36 +0,0 @@
|
||||
# System Requirements for the Desktop Version
|
||||
|
||||
The following system requirements are the official ones for the desktop version.
|
||||
While we have tested and confirmed these requirements, the application might also work on other
|
||||
systems or configurations that haven't undergone official testing.
|
||||
|
||||
## Windows
|
||||
|
||||
- **Version:** Windows 10 and later.
|
||||
- **Processor:** Intel Pentium 4 or later.
|
||||
- **Architecture:** 64-bit (x86-64), ARM (ARM64).
|
||||
|
||||
> **⚠️ Compatibility Note:**
|
||||
> ARM version is only compatible with Windows 11 and later.
|
||||
> It runs non-natively, leading to slower performance due to emulation [1].
|
||||
|
||||
## macOS
|
||||
|
||||
- **Version:** macOS Catalina (10.15) and later.
|
||||
- **Architecture:** Intel-based (x86-64), Apple silicon (ARM64).
|
||||
|
||||
## Linux
|
||||
|
||||
- **Version:** Ubuntu 18.04 and later, Fedora 32 and later, and Debian 10 and later.
|
||||
- **Processor:** Intel Pentium 4 or later.
|
||||
- **Architecture:** 64-bit (x86-64).
|
||||
|
||||
## References
|
||||
|
||||
System requirements reflect Electron's platform capabilities [2] and Chromium's recommended configurations [3].
|
||||
|
||||
For details on the build process, see [electron-builder configuration file](./../../electron-builder.cjs).
|
||||
|
||||
[1]: https://web.archive.org/web/20240428082726/https://learn.microsoft.com/en-us/windows/arm/add-arm-support#emulation-on-arm-based-devices-for-x86-or-x64-windows-apps "Add support Arm devices to your Windows app | Microsoft Learn | learn.microsoft.com"
|
||||
[2]: https://archive.ph/2024.04.28-082958/https://github.com/electron/electron/blob/main/README.md#platform-support "Platform Support | electron/README.md at main · electron/electron · GitHub | github.com"
|
||||
[3]: https://web.archive.org/web/20240428082945/https://support.google.com/chrome/a/answer/7100626?hl=en "Chrome browser system requirements - Chrome Enterprise and Education Help | support.google.com"
|
||||
@@ -80,10 +80,8 @@ See [ci-cd.md](./ci-cd.md) for more information.
|
||||
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
|
||||
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
|
||||
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
|
||||
- [**`python3 ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py):
|
||||
- [**`python ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py):
|
||||
- Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment.
|
||||
- [**`python3 ./scripts/validate-collections-yaml`**](../scripts/validate-collections-yaml/README.md):
|
||||
- Validates the syntax and structure of collection YAML files.
|
||||
|
||||
#### Automation scripts
|
||||
|
||||
|
||||
@@ -14,19 +14,18 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
||||
- [**`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.
|
||||
- [**`components/`**](./../src/presentation/components/): Contains Vue components, helpers and styles coupled to Vue components.
|
||||
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
||||
- [**`Hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections).
|
||||
- [**`/public/`**](../src/presentation/public/): Contains static assets.
|
||||
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
|
||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
|
||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
||||
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||
- [`/main/` **`index.ts`**](./../src/presentation/electron/main/index.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||
- [`/preload/` **`index.ts`**](./../src/presentation/electron/preload/index.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||
- [**`/shared/`**](./../src/presentation/electron/shared/): Shared logic between different Electron processes.
|
||||
- [**`/build/`**](./../src/presentation/electron/build/): `electron-builder` build resources directory, [README.md](./../src/presentation/electron/build/README.md).
|
||||
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
||||
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
||||
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
||||
@@ -39,13 +38,6 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
||||
They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility.
|
||||
- **Borders**:
|
||||
privacy.sexy prefers sharper edges in its design language.
|
||||
- **Fonts**:
|
||||
- Use the primary font for regular text and monospace font for code or specific data.
|
||||
- Use cursive and logo fonts solely for branding.
|
||||
- Refer to [standardized font size variables](../src/presentation/assets/styles/_typography.scss) for font sizing, avoiding arbitrary `px`, `em`, `rem`, or percentage values.
|
||||
- **Spacing**:
|
||||
Use [global spacing variables](../src/presentation/assets/styles/_spacing.scss) for consistent margin, padding, and gap definitions.
|
||||
This provides uniform spatial distribution and alignment of elements, enhancing visual harmony and making the UI more scalable and maintainable.
|
||||
|
||||
## Application data
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ Key attributes of a good script:
|
||||
- `Minimize` over `Limit`, `Reduce`
|
||||
- `Maximize` over `Extend`, `Delay`, `Postpone`, `Prolong`
|
||||
- `Remove` over `Uninstall`
|
||||
- `Improve` over `Increase`
|
||||
- Structure your phrases for clarity, examples:
|
||||
- Prefer `Disable XX telemetry` over `Disable telemetry in XX`
|
||||
- Prefer `Clear XX data` over `Clear data from XX`, or `Clear data of XX`.
|
||||
@@ -36,8 +35,8 @@ Key attributes of a good script:
|
||||
## Documentation
|
||||
|
||||
- Use credible and reputable sources for references.
|
||||
- Use archived links by using [archive.org](https://archive.org) or [archive.ph](https://archive.ph).
|
||||
- Format archive.today links fully, for example: `https://archive.ph/YYYYMMDDhhmmss/https://privacy.sexy`.
|
||||
- Use archived links by using [archive.org](https://archive.org) or [archive.today](https://archive.today).
|
||||
- Format archive.today links fully, for example: `https://archive.today/YYYYMMDDhhmmss/https://privacy.sexy`.
|
||||
- Explain the default behavior if the script is not executed.
|
||||
|
||||
## Shared functions
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
|
||||
const { join, resolve } = require('node:path');
|
||||
const { readdirSync, existsSync } = require('node:fs');
|
||||
const { join } = require('node:path');
|
||||
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
||||
|
||||
/**
|
||||
* @type {import('electron-builder').Configuration}
|
||||
* @see https://www.electron.build/configuration/configuration
|
||||
*/
|
||||
module.exports = {
|
||||
// Common options
|
||||
publish: {
|
||||
@@ -17,12 +12,9 @@ module.exports = {
|
||||
},
|
||||
directories: {
|
||||
output: electronBundled,
|
||||
buildResources: resolvePathFromProjectRoot('src/presentation/electron/build'),
|
||||
},
|
||||
extraMetadata: {
|
||||
main: findMainEntryFile(
|
||||
join(electronUnbundled, 'main'), // do not `path.resolve`, it expects a relative path
|
||||
),
|
||||
main: join(electronUnbundled, 'main/index.cjs'), // do not `path.resolve`, it expects a relative path
|
||||
},
|
||||
|
||||
// Windows
|
||||
@@ -43,32 +35,9 @@ module.exports = {
|
||||
|
||||
// macOS
|
||||
mac: {
|
||||
target: {
|
||||
target: 'dmg',
|
||||
arch: 'universal',
|
||||
},
|
||||
target: 'dmg',
|
||||
},
|
||||
dmg: {
|
||||
artifactName: '${name}-${version}.${ext}',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds by accommodating different JS file extensions and module formats.
|
||||
*/
|
||||
function findMainEntryFile(parentDirectory) {
|
||||
const absoluteParentDirectory = resolvePathFromProjectRoot(parentDirectory);
|
||||
if (!existsSync(absoluteParentDirectory)) {
|
||||
return null; // Avoid disrupting other processes such `npm install`.
|
||||
}
|
||||
const files = readdirSync(absoluteParentDirectory);
|
||||
const entryFile = files.find((file) => /^index\.(cjs|mjs|js)$/.test(file));
|
||||
if (!entryFile) {
|
||||
throw new Error(`Main entry file not found in ${absoluteParentDirectory}.`);
|
||||
}
|
||||
return join(parentDirectory, entryFile);
|
||||
}
|
||||
|
||||
function resolvePathFromProjectRoot(pathSegment) {
|
||||
return resolve(__dirname, pathSegment);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const ELECTRON_DIST_SUBDIRECTORIES = {
|
||||
renderer: resolveElectronDistSubdirectory('renderer'),
|
||||
};
|
||||
|
||||
process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.mjs');
|
||||
process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.cjs');
|
||||
|
||||
export default defineConfig({
|
||||
main: getSharedElectronConfig({
|
||||
@@ -54,23 +54,13 @@ function getSharedElectronConfig(options: {
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
format: 'es',
|
||||
|
||||
// Ensure all generated files use '.mjs' for module consistency.
|
||||
// Otherwise, preloader process get `.mjs` extension but main process get `.js` extension, see https://github.com/alex8088/electron-vite/issues/397.
|
||||
entryFileNames: '[name].mjs',
|
||||
// Mark: electron-esm-support
|
||||
// This is needed so `type="module"` works
|
||||
entryFileNames: '[name].cjs',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [externalizeDepsPlugin({
|
||||
exclude: [
|
||||
// Keep 'electron-log' in bundling process.
|
||||
// This is a workaround for inability of Electron's ESM loader to resolve subpath imports.
|
||||
// Do not externalize `electron-log` so subpath imports such as `electron-log/main` works.
|
||||
// See https://github.com/electron/electron/issues/41241, https://github.com/alex8088/electron-vite/issues/401
|
||||
'electron-log',
|
||||
],
|
||||
})],
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
define: {
|
||||
...getClientEnvironmentVariables(),
|
||||
},
|
||||
|
||||
12435
package-lock.json
generated
82
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.13.5",
|
||||
"version": "0.13.0",
|
||||
"private": true,
|
||||
"slogan": "Privacy is sexy",
|
||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy.",
|
||||
@@ -14,7 +14,7 @@
|
||||
"test:integration": "vitest run --dir tests/integration",
|
||||
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
|
||||
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
||||
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml && npm run lint:pylint",
|
||||
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
||||
"install-deps": "node scripts/npm-install.js",
|
||||
"icons:build": "node scripts/logo-update.js",
|
||||
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
|
||||
@@ -29,68 +29,66 @@
|
||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||
"lint:pylint": "pylint **/*.py",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/vue": "^1.0.6",
|
||||
"@floating-ui/vue": "^1.0.2",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"ace-builds": "^1.33.0",
|
||||
"electron-log": "^5.1.2",
|
||||
"electron-progressbar": "^2.2.1",
|
||||
"electron-updater": "^6.1.9",
|
||||
"@types/markdown-it": "^13.0.7",
|
||||
"ace-builds": "^1.30.0",
|
||||
"electron-log": "^5.0.1",
|
||||
"electron-progressbar": "^2.1.0",
|
||||
"electron-updater": "^6.1.4",
|
||||
"file-saver": "^2.0.5",
|
||||
"markdown-it": "^14.1.0",
|
||||
"vue": "^3.4.27"
|
||||
"markdown-it": "^13.0.2",
|
||||
"vue": "^3.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||
"@rushstack/eslint-patch": "^1.10.2",
|
||||
"@types/ace": "^0.0.52",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/markdown-it": "^14.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"@rushstack/eslint-patch": "^1.6.1",
|
||||
"@types/ace": "^0.0.49",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"@vitejs/plugin-legacy": "^5.3.2",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "12.0.0",
|
||||
"@vue/test-utils": "^2.4.5",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"cypress": "^13.7.3",
|
||||
"electron": "^31.0.2",
|
||||
"electron-builder": "^24.13.3",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/test-utils": "^2.4.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"cypress": "^13.3.1",
|
||||
"electron": "^27.0.0",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-icon-builder": "^2.0.1",
|
||||
"electron-vite": "^2.1.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"eslint-plugin-vue": "^9.25.0",
|
||||
"eslint-plugin-vuejs-accessibility": "^2.2.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"markdownlint-cli": "^0.39.0",
|
||||
"postcss": "^8.4.38",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"eslint-plugin-vuejs-accessibility": "^2.2.0",
|
||||
"icon-gen": "^4.0.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"markdownlint-cli": "^0.37.0",
|
||||
"postcss": "^8.4.31",
|
||||
"remark-cli": "^12.0.0",
|
||||
"remark-lint-no-dead-urls": "^1.1.0",
|
||||
"remark-preset-lint-consistent": "^6.0.0",
|
||||
"remark-validate-links": "^13.0.1",
|
||||
"sass": "^1.75.0",
|
||||
"start-server-and-test": "^2.0.3",
|
||||
"terser": "^5.30.3",
|
||||
"remark-preset-lint-consistent": "^5.1.2",
|
||||
"remark-validate-links": "^13.0.0",
|
||||
"sass": "^1.69.3",
|
||||
"start-server-and-test": "^2.0.1",
|
||||
"svgexport": "^0.4.2",
|
||||
"terser": "^5.21.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.8",
|
||||
"vitest": "^1.5.0",
|
||||
"vue-tsc": "^2.0.13",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.6",
|
||||
"vitest": "^0.34.6",
|
||||
"vue-tsc": "^1.8.19",
|
||||
"yaml-lint": "^1.7.0"
|
||||
},
|
||||
"//devDependencies": {
|
||||
"terser": "Used by `@vitejs/plugin-legacy` for minification",
|
||||
"@rushstack/eslint-patch": "Needed by `@vue/eslint-config-typescript` and `@vue/eslint-config-airbnb-with-typescript`",
|
||||
"@typescript-eslint/eslint-plugin": "Cannot migrate to v7 because of `@vue/eslint-config-airbnb-with-typescript`, see https://github.com/vuejs/eslint-config-airbnb/issues/63",
|
||||
"@typescript-eslint/parser": "Cannot migrate to v7 because of `@vue/eslint-config-airbnb-with-typescript`, see https://github.com/vuejs/eslint-config-airbnb/issues/63",
|
||||
"@vue/eslint-config-typescript": "Cannot migrate to v13 because of `@vue/eslint-config-airbnb-with-typescript`, see https://github.com/vuejs/eslint-config-airbnb/issues/63",
|
||||
"eslint": "Cannot migrate to v9 `@typescript-eslint/eslint-plugin` (≤ v7), `@typescript-eslint/parser` (≤ v7), `@vue/eslint-config-airbnb-with-typescript@` (≤ v8) requires `eslint` ≤ v8, see https://github.com/vuejs/eslint-config-airbnb/issues/65, https://github.com/typescript-eslint/typescript-eslint/issues/8211"
|
||||
"@rushstack/eslint-patch": "Needed by `@vue/eslint-config-typescript` and `@vue/eslint-config-airbnb-with-typescript`"
|
||||
},
|
||||
"homepage": "https://privacy.sexy",
|
||||
"repository": {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
"""
|
||||
Description:
|
||||
This script configures project-level VSCode settings in '.vscode/settings.json' for
|
||||
development and installs recommended extensions from '.vscode/extensions.json'.
|
||||
|
||||
Usage:
|
||||
python3 ./scripts/configure_vscode.py
|
||||
This script configures project-level VSCode settings in '.vscode/settings.json' for
|
||||
development and installs recommended extensions from '.vscode/extensions.json'.
|
||||
"""
|
||||
# pylint: disable=missing-function-docstring
|
||||
|
||||
@@ -44,7 +40,7 @@ def ensure_setting_file_exists() -> None:
|
||||
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||
except IOError as error:
|
||||
print_error(f"Error creating file {VSCODE_SETTINGS_JSON_FILE}: {error}")
|
||||
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||
print(f"📄 Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||
|
||||
def add_or_update_settings() -> None:
|
||||
configure_setting_key('eslint.validate', ['vue', 'javascript', 'typescript'])
|
||||
@@ -58,10 +54,6 @@ def add_or_update_settings() -> None:
|
||||
# Details: # pylint: disable-next=line-too-long
|
||||
# - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274
|
||||
|
||||
# Disable telemetry
|
||||
configure_setting_key('redhat.telemetry.enabled', False)
|
||||
configure_setting_key('gitlens.telemetry.enabled', False)
|
||||
|
||||
def configure_setting_key(configuration_key: str, desired_value: Any) -> None:
|
||||
try:
|
||||
with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file:
|
||||
@@ -106,8 +98,7 @@ def locate_vscode_cli() -> Optional[str]:
|
||||
if vscode_alias:
|
||||
return vscode_alias
|
||||
potential_vscode_cli_paths = [
|
||||
# VS Code on macOS may not register 'code' command in PATH
|
||||
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code'
|
||||
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code' # macOS VS Code may not register 'code' command in PATH
|
||||
]
|
||||
for vscode_cli_candidate_path in potential_vscode_cli_paths:
|
||||
if Path(vscode_cli_candidate_path).is_file():
|
||||
@@ -118,7 +109,7 @@ def remove_json_comments(json_like: str) -> str:
|
||||
pattern: str = r'(?:"(?:\\.|[^"\\])*"|/\*[\s\S]*?\*/|//.*)|([^:]//.*$)'
|
||||
return re.sub(
|
||||
pattern,
|
||||
lambda m: '' if m.group(1) else m.group(0), json_like, flags=re.MULTILINE,
|
||||
lambda m: '' if m.group(1) else m.agroup(0), json_like, flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
def install_vscode_extensions(vscode_cli_path: str, extensions: list[str]) -> None:
|
||||
@@ -175,16 +166,16 @@ def print_installation_results(successful_installations: int, total_extensions:
|
||||
print_error("Failed to install any of the recommended extensions.")
|
||||
|
||||
def print_error(message: str) -> None:
|
||||
print(f"[ERROR] {message}", file=sys.stderr)
|
||||
print(f"💀 Error: {message}", file=sys.stderr)
|
||||
|
||||
def print_success(message: str) -> None:
|
||||
print(f"[SUCCESS] {message}")
|
||||
print(f"✅ Success: {message}")
|
||||
|
||||
def print_skip(message: str) -> None:
|
||||
print(f"[SKIPPED] {message}")
|
||||
print(f"⏩ Skipped: {message}")
|
||||
|
||||
def print_warning(message: str) -> None:
|
||||
print(f"[WARNING] {message}", file=sys.stderr)
|
||||
print(f"⚠️ Warning: {message}", file=sys.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,120 +1,84 @@
|
||||
/**
|
||||
* Description:
|
||||
* This script updates the logo images across the project based on the primary
|
||||
* logo file ('img/logo.svg' file).
|
||||
*
|
||||
* It handles the creation and update of various icon sizes for different purposes,
|
||||
* including desktop launcher icons, tray icons, and web favicons from a single source
|
||||
* SVG logo file.
|
||||
*
|
||||
* Usage:
|
||||
* node ./scripts/logo-update.js
|
||||
*
|
||||
* Notes:
|
||||
* ImageMagick must be installed and accessible in the system's PATH
|
||||
*/
|
||||
|
||||
import { resolve, join, dirname } from 'node:path';
|
||||
import { stat } from 'node:fs/promises';
|
||||
#!/usr/bin/env bash
|
||||
import { resolve, join } from 'node:path';
|
||||
import { rm, mkdtemp, stat } from 'node:fs/promises';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { URL, fileURLToPath } from 'node:url';
|
||||
import electronBuilderConfig from '../electron-builder.cjs';
|
||||
|
||||
class ImageAssetPaths {
|
||||
constructor(currentScriptDirectory) {
|
||||
const projectRoot = resolve(currentScriptDirectory, '../');
|
||||
class Paths {
|
||||
constructor(selfDirectory) {
|
||||
const projectRoot = resolve(selfDirectory, '../');
|
||||
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
||||
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
||||
this.electronBuildResourcesDirectory = electronBuilderConfig.directories.buildResources;
|
||||
}
|
||||
|
||||
get electronTrayIconFile() {
|
||||
return join(this.publicDirectory, 'icon.png');
|
||||
}
|
||||
|
||||
get webFaviconFile() {
|
||||
return join(this.publicDirectory, 'favicon.ico');
|
||||
this.electronBuildDirectory = join(projectRoot, 'build');
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Source image: ${this.sourceImage}`
|
||||
+ `\nPublic directory: ${this.publicDirectory}`
|
||||
+ `\n\t Electron tray icon file: ${this.electronTrayIconFile}`
|
||||
+ `\n\t Web favicon file: ${this.webFaviconFile}`
|
||||
+ `\nElectron build directory: ${this.electronBuildResourcesDirectory}`;
|
||||
return `Source image: ${this.sourceImage}\n`
|
||||
+ `Public directory: ${this.publicDirectory}\n`
|
||||
+ `Electron build directory: ${this.electronBuildDirectory}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const paths = new ImageAssetPaths(getCurrentScriptDirectory());
|
||||
const paths = new Paths(getCurrentScriptDirectory());
|
||||
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
||||
const convertCommand = await findAvailableImageMagickCommand();
|
||||
await generateDesktopAndTrayIcons(
|
||||
paths.sourceImage,
|
||||
paths.electronTrayIconFile,
|
||||
convertCommand,
|
||||
);
|
||||
await generateWebFavicon(
|
||||
paths.sourceImage,
|
||||
paths.webFaviconFile,
|
||||
convertCommand,
|
||||
);
|
||||
await generateDesktopIcons(
|
||||
paths.sourceImage,
|
||||
paths.electronBuildResourcesDirectory,
|
||||
convertCommand,
|
||||
);
|
||||
await updateDesktopLauncherAndTrayIcon(paths.sourceImage, paths.publicDirectory);
|
||||
await updateWebFavicon(paths.sourceImage, paths.publicDirectory);
|
||||
await updateDesktopIcons(paths.sourceImage, paths.electronBuildDirectory);
|
||||
console.log('🎉 (Re)created icons successfully.');
|
||||
}
|
||||
|
||||
async function generateDesktopAndTrayIcons(sourceImage, targetFile, convertCommand) {
|
||||
// Reference: https://web.archive.org/web/20240502124306/https://www.electronjs.org/docs/latest/api/tray
|
||||
console.log(`Updating desktop launcher and tray icon at ${targetFile}.`);
|
||||
async function updateDesktopLauncherAndTrayIcon(sourceImage, publicFolder) {
|
||||
await ensureFileExists(sourceImage);
|
||||
await ensureParentFolderExists(targetFile);
|
||||
await convertFromSvgToPng(
|
||||
convertCommand,
|
||||
await ensureFolderExists(publicFolder);
|
||||
const electronTrayIconFile = join(publicFolder, 'icon.png');
|
||||
console.log(`Updating desktop launcher and tray icon at ${electronTrayIconFile}.`);
|
||||
await runCommand(
|
||||
'npx',
|
||||
'svgexport',
|
||||
sourceImage,
|
||||
targetFile,
|
||||
'512x512',
|
||||
electronTrayIconFile,
|
||||
);
|
||||
}
|
||||
|
||||
async function generateWebFavicon(sourceImage, faviconFilePath, convertCommand) {
|
||||
console.log(`Updating favicon at ${faviconFilePath}.`);
|
||||
async function updateWebFavicon(sourceImage, faviconFolder) {
|
||||
console.log('Updating favicon');
|
||||
await ensureFileExists(sourceImage);
|
||||
await ensureParentFolderExists(faviconFilePath);
|
||||
await convertFromSvgToIco(
|
||||
convertCommand,
|
||||
sourceImage,
|
||||
faviconFilePath,
|
||||
[16, 24, 32, 48, 64, 128, 256],
|
||||
await ensureFolderExists(faviconFolder);
|
||||
await runCommand(
|
||||
'npx',
|
||||
'icon-gen',
|
||||
`--input ${sourceImage}`,
|
||||
`--output ${faviconFolder}`,
|
||||
'--ico',
|
||||
'--ico-name \'favicon\'',
|
||||
'--report',
|
||||
);
|
||||
}
|
||||
|
||||
async function generateDesktopIcons(sourceImage, electronBuildResourcesDirectory, convertCommand) {
|
||||
console.log(`Creating Electron icon files to ${electronBuildResourcesDirectory}.`);
|
||||
// Reference: https://web.archive.org/web/20240501103645/https://www.electron.build/icons.html
|
||||
await ensureFolderExists(electronBuildResourcesDirectory);
|
||||
async function updateDesktopIcons(sourceImage, electronIconsDir) {
|
||||
await ensureFileExists(sourceImage);
|
||||
const electronMainIconFile = join(electronBuildResourcesDirectory, 'icon.png');
|
||||
await convertFromSvgToPng(
|
||||
convertCommand,
|
||||
await ensureFolderExists(electronIconsDir);
|
||||
const temporaryDir = await mkdtemp('icon-');
|
||||
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
||||
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by `icon-builder`
|
||||
await runCommand(
|
||||
'npx',
|
||||
'svgexport',
|
||||
sourceImage,
|
||||
electronMainIconFile,
|
||||
'1024x1024', // Should be at least 512x512
|
||||
temporaryPngFile,
|
||||
'1024:1024',
|
||||
);
|
||||
// Relying on `electron-builder`s conversion from png to ico results in pixelated look on Windows
|
||||
// 10 and 11 according to tests, see:
|
||||
// - https://web.archive.org/web/20240502114650/https://github.com/electron-userland/electron-builder/issues/7328
|
||||
// - https://web.archive.org/web/20240502115448/https://github.com/electron-userland/electron-builder/issues/3867
|
||||
const electronWindowsIconFile = join(electronBuildResourcesDirectory, 'icon.ico');
|
||||
await convertFromSvgToIco(
|
||||
convertCommand,
|
||||
sourceImage,
|
||||
electronWindowsIconFile,
|
||||
[16, 24, 32, 48, 64, 128, 256],
|
||||
console.log(`Creating electron icons to ${electronIconsDir}.`);
|
||||
await runCommand(
|
||||
'npx',
|
||||
'electron-icon-builder',
|
||||
`--input="${temporaryPngFile}"`,
|
||||
`--output="${electronIconsDir}"`,
|
||||
'--flatten',
|
||||
);
|
||||
console.log('Cleaning up temporary directory.');
|
||||
await rm(temporaryDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function ensureFileExists(filePath) {
|
||||
@@ -125,60 +89,12 @@ async function ensureFileExists(filePath) {
|
||||
}
|
||||
|
||||
async function ensureFolderExists(folderPath) {
|
||||
if (!folderPath) {
|
||||
throw new Error('Path is missing');
|
||||
}
|
||||
const path = await stat(folderPath);
|
||||
if (!path.isDirectory()) {
|
||||
throw new Error(`Not a directory: ${folderPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureParentFolderExists(filePath) {
|
||||
return ensureFolderExists(dirname(filePath));
|
||||
}
|
||||
|
||||
const BaseImageMagickConvertArguments = Object.freeze([
|
||||
'-background none', // Transparent, so they do not get filled with white.
|
||||
'-strip', // Strip metadata.
|
||||
'-gravity Center', // Center the image when there's empty space
|
||||
]);
|
||||
|
||||
async function convertFromSvgToIco(
|
||||
convertCommand,
|
||||
inputFile,
|
||||
outputFile,
|
||||
sizes,
|
||||
) {
|
||||
await runCommand(
|
||||
convertCommand,
|
||||
...BaseImageMagickConvertArguments,
|
||||
`-density ${Math.max(...sizes).toString()}`, // High enough for sharpness
|
||||
`-define icon:auto-resize=${sizes.map((s) => s.toString()).join(',')}`, // Automatically store multiple sizes in an ico image
|
||||
'-compress None',
|
||||
inputFile,
|
||||
outputFile,
|
||||
);
|
||||
}
|
||||
|
||||
async function convertFromSvgToPng(
|
||||
convertCommand,
|
||||
inputFile,
|
||||
outputFile,
|
||||
size = undefined,
|
||||
) {
|
||||
await runCommand(
|
||||
convertCommand,
|
||||
...BaseImageMagickConvertArguments,
|
||||
...(size === undefined ? [] : [
|
||||
`-resize ${size}`,
|
||||
`-density ${size}`, // High enough for sharpness
|
||||
]),
|
||||
inputFile,
|
||||
outputFile,
|
||||
);
|
||||
}
|
||||
|
||||
async function runCommand(...args) {
|
||||
const command = args.join(' ');
|
||||
console.log(`Running command: ${command}`);
|
||||
@@ -208,27 +124,4 @@ function getCurrentScriptDirectory() {
|
||||
return fileURLToPath(new URL('.', import.meta.url));
|
||||
}
|
||||
|
||||
async function findAvailableImageMagickCommand() {
|
||||
// Reference: https://web.archive.org/web/20240502120041/https://imagemagick.org/script/convert.php
|
||||
const potentialBaseCommands = [
|
||||
'convert', // Legacy command, usually available on Linux/macOS installations
|
||||
'magick convert', // Newer command, available on Windows installations
|
||||
];
|
||||
for (const baseCommand of potentialBaseCommands) {
|
||||
const testCommand = `${baseCommand} -version`;
|
||||
try {
|
||||
await runCommand(testCommand); // eslint-disable-line no-await-in-loop
|
||||
console.log(`Confirmed: ImageMagick command '${baseCommand}' is available and operational.`);
|
||||
return baseCommand;
|
||||
} catch (err) {
|
||||
console.log(`Error: The command '${baseCommand}' is not found or failed to execute. Detailed error: ${err.message}"`);
|
||||
}
|
||||
}
|
||||
throw new Error([
|
||||
'Unable to locate any operational ImageMagick command.',
|
||||
`Attempted commands were: ${potentialBaseCommands.join(', ')}.`,
|
||||
'Please ensure ImageMagick is correctly installed and accessible.',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
await main();
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# validate-collections-yaml
|
||||
|
||||
This script validates YAML collection files against a predefined schema to ensure their integrity.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.x installed on your system.
|
||||
|
||||
## Running in a Virtual Environment (Recommended)
|
||||
|
||||
Using a virtual environment isolates dependencies and prevents conflicts.
|
||||
|
||||
1. **Create a virtual environment:**
|
||||
|
||||
```bash
|
||||
python3 -m venv ./scripts/validate-collections-yaml/.venv
|
||||
```
|
||||
|
||||
2. **Activate the virtual environment:**
|
||||
|
||||
```bash
|
||||
source ./scripts/validate-collections-yaml/.venv/bin/activate
|
||||
```
|
||||
|
||||
3. **Install dependencies:**
|
||||
|
||||
```bash
|
||||
python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
|
||||
```
|
||||
|
||||
4. **Run the script:**
|
||||
|
||||
```bash
|
||||
python3 ./scripts/validate-collections-yaml
|
||||
```
|
||||
|
||||
## Running Globally
|
||||
|
||||
Running the script globally is less recommended due to potential dependency conflicts.
|
||||
|
||||
1. **Install dependencies:**
|
||||
|
||||
```bash
|
||||
python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
|
||||
```
|
||||
|
||||
2. **Run the script:**
|
||||
|
||||
```bash
|
||||
python3 ./scripts/validate-collections-yaml
|
||||
```
|
||||
@@ -1,62 +0,0 @@
|
||||
"""
|
||||
Description:
|
||||
This script validates collection YAML files against the expected schema.
|
||||
|
||||
Usage:
|
||||
python3 ./scripts/validate-collections-yaml
|
||||
|
||||
Notes:
|
||||
This script requires the `jsonschema` and `pyyaml` packages (see requirements.txt).
|
||||
"""
|
||||
# pylint: disable=missing-function-docstring
|
||||
from os import path
|
||||
import sys
|
||||
from glob import glob
|
||||
from typing import List
|
||||
from jsonschema import exceptions, validate # pylint: disable=import-error
|
||||
import yaml # pylint: disable=import-error
|
||||
|
||||
SCHEMA_FILE_PATH = './src/application/collections/.schema.yaml'
|
||||
COLLECTIONS_GLOB_PATTERN = './src/application/collections/*.yaml'
|
||||
|
||||
def main() -> None:
|
||||
schema_yaml = read_file(SCHEMA_FILE_PATH)
|
||||
schema_json = convert_yaml_to_json(schema_yaml)
|
||||
collection_file_paths = find_collection_files(COLLECTIONS_GLOB_PATTERN)
|
||||
print(f'Found {len(collection_file_paths)} YAML files to validate.')
|
||||
|
||||
total_invalid_files = 0
|
||||
for collection_file_path in collection_file_paths:
|
||||
file_name = path.basename(collection_file_path)
|
||||
print(f'Validating {file_name}...')
|
||||
collection_yaml = read_file(collection_file_path)
|
||||
collection_json = convert_yaml_to_json(collection_yaml)
|
||||
try:
|
||||
validate(instance=collection_json, schema=schema_json)
|
||||
print(f'Success: {file_name} is valid.')
|
||||
except exceptions.ValidationError as err:
|
||||
print(f'Error: Validation failed for {file_name}.', file=sys.stderr)
|
||||
print(str(err), file=sys.stderr)
|
||||
total_invalid_files += 1
|
||||
|
||||
if total_invalid_files > 0:
|
||||
print(f'Validation complete with {total_invalid_files} invalid files.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print('Validation complete. All files are valid.')
|
||||
sys.exit(0)
|
||||
|
||||
def read_file(file_path: str) -> str:
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
return file.read()
|
||||
|
||||
def find_collection_files(glob_pattern: str) -> List[str]:
|
||||
files = glob(glob_pattern)
|
||||
filtered_files = [f for f in files if not path.basename(f).startswith('.')]
|
||||
return filtered_files
|
||||
|
||||
def convert_yaml_to_json(yaml_content: str) -> dict:
|
||||
return yaml.safe_load(yaml_content)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,6 +0,0 @@
|
||||
attrs==23.2.0
|
||||
jsonschema==4.22.0
|
||||
jsonschema-specifications==2023.12.1
|
||||
PyYAML==6.0.1
|
||||
referencing==0.35.1
|
||||
rpds-py==0.18.1
|
||||
@@ -44,8 +44,8 @@ function getBuildVerificationConfigs() {
|
||||
'--electron-unbundled': {
|
||||
printDistDirScriptArgument: '--electron-unbundled',
|
||||
filePatterns: [
|
||||
/main[/\\]index\.(cjs|mjs|js)/,
|
||||
/preload[/\\]index\.(cjs|mjs|js)/,
|
||||
/main[/\\]index\.cjs/,
|
||||
/preload[/\\]index\.cjs/,
|
||||
/renderer[/\\]index\.htm(l)?/,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,87 +1,62 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Description:
|
||||
* This script checks if a server, provided as a CLI argument, is up
|
||||
* and returns an HTTP 200 status code.
|
||||
* It is designed to provide easy verification of server availability
|
||||
* and will retry a specified number of times.
|
||||
* This script checks if a server, provided as a CLI argument, is up
|
||||
* and returns an HTTP 200 status code.
|
||||
* It is designed to provide easy verification of server availability
|
||||
* and will retry a specified number of times.
|
||||
*
|
||||
* Usage:
|
||||
* node ./scripts/verify-web-server-status.js --url [URL] [--max-retries NUMBER]
|
||||
* node ./scripts/verify-web-server-status.js --url [URL]
|
||||
*
|
||||
* Options:
|
||||
* --url URL of the server to check
|
||||
* --max-retries Maximum number of retry attempts (default: 30)
|
||||
* --url URL of the server to check
|
||||
*/
|
||||
|
||||
const DEFAULT_MAX_RETRIES = 30;
|
||||
const RETRY_DELAY_IN_SECONDS = 3;
|
||||
const PARAMETER_NAME_URL = '--url';
|
||||
const PARAMETER_NAME_MAX_RETRIES = '--max-retries';
|
||||
import { get } from 'http';
|
||||
|
||||
async function checkServer(currentRetryCount = 1) {
|
||||
const serverUrl = readRequiredParameterValue(PARAMETER_NAME_URL);
|
||||
const maxRetries = parseNumber(
|
||||
readOptionalParameterValue(PARAMETER_NAME_MAX_RETRIES, DEFAULT_MAX_RETRIES),
|
||||
);
|
||||
console.log(`🌐 Requesting ${serverUrl}...`);
|
||||
try {
|
||||
const response = await fetch(serverUrl);
|
||||
if (response.status === 200) {
|
||||
const MAX_RETRIES = 30;
|
||||
const RETRY_DELAY_IN_SECONDS = 3;
|
||||
const URL_PARAMETER_NAME = '--url';
|
||||
|
||||
function checkServer(currentRetryCount = 1) {
|
||||
const serverUrl = getServerUrl();
|
||||
console.log(`Requesting ${serverUrl}...`);
|
||||
get(serverUrl, (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
console.log('🎊 Success: The server is up and returned HTTP 200.');
|
||||
process.exit(0);
|
||||
} else {
|
||||
exitWithError(`Server returned unexpected HTTP status code ${response.statusCode}.`);
|
||||
console.log(`Server returned HTTP status code ${res.statusCode}.`);
|
||||
retry(currentRetryCount);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error making the request:', error);
|
||||
scheduleNextRetry(maxRetries, currentRetryCount);
|
||||
}
|
||||
}).on('error', (err) => {
|
||||
console.error('Error making the request:', err);
|
||||
retry(currentRetryCount);
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleNextRetry(maxRetries, currentRetryCount) {
|
||||
console.log(`Attempt ${currentRetryCount}/${maxRetries}:`);
|
||||
function retry(currentRetryCount) {
|
||||
console.log(`Attempt ${currentRetryCount}/${MAX_RETRIES}:`);
|
||||
console.log(`Retrying in ${RETRY_DELAY_IN_SECONDS} seconds.`);
|
||||
|
||||
const remainingTime = (maxRetries - currentRetryCount) * RETRY_DELAY_IN_SECONDS;
|
||||
const remainingTime = (MAX_RETRIES - currentRetryCount) * RETRY_DELAY_IN_SECONDS;
|
||||
console.log(`Time remaining before timeout: ${remainingTime}s`);
|
||||
|
||||
if (currentRetryCount < maxRetries) {
|
||||
if (currentRetryCount < MAX_RETRIES) {
|
||||
setTimeout(() => checkServer(currentRetryCount + 1), RETRY_DELAY_IN_SECONDS * 1000);
|
||||
} else {
|
||||
exitWithError('The server at did not return HTTP 200 within the allocated time.');
|
||||
console.log('Failure: The server at did not return HTTP 200 within the allocated time. Exiting.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function readRequiredParameterValue(parameterName) {
|
||||
const parameterValue = readOptionalParameterValue(parameterName);
|
||||
if (parameterValue === undefined) {
|
||||
exitWithError(`Parameter "${parameterName}" is required but not provided.`);
|
||||
function getServerUrl() {
|
||||
const urlIndex = process.argv.indexOf(URL_PARAMETER_NAME);
|
||||
if (urlIndex === -1 || urlIndex === process.argv.length - 1) {
|
||||
console.error(`Parameter "${URL_PARAMETER_NAME}" is not provided.`);
|
||||
process.exit(1);
|
||||
}
|
||||
return parameterValue;
|
||||
return process.argv[urlIndex + 1];
|
||||
}
|
||||
|
||||
function readOptionalParameterValue(parameterName, defaultValue) {
|
||||
const index = process.argv.indexOf(parameterName);
|
||||
if (index === -1 || index === process.argv.length - 1) {
|
||||
return defaultValue;
|
||||
}
|
||||
return process.argv[index + 1];
|
||||
}
|
||||
|
||||
function parseNumber(numberLike) {
|
||||
const number = parseInt(numberLike, 10);
|
||||
if (Number.isNaN(number)) {
|
||||
exitWithError(`Invalid number: ${numberLike}`);
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
function exitWithError(message) {
|
||||
console.error(`Failure: ${message}`);
|
||||
console.log('Exiting');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await checkServer();
|
||||
checkServer();
|
||||
|
||||
@@ -11,11 +11,10 @@ export type CodeRunErrorType =
|
||||
| 'FileWriteError'
|
||||
| 'FileReadbackVerificationError'
|
||||
| 'FilePathGenerationError'
|
||||
| 'UnsupportedPlatform'
|
||||
| 'DirectoryCreationError'
|
||||
| 'FilePermissionChangeError'
|
||||
| 'UnsupportedOperatingSystem'
|
||||
| 'FileExecutionError'
|
||||
| 'ExternalProcessTermination';
|
||||
| 'DirectoryCreationError'
|
||||
| 'UnexpectedError';
|
||||
|
||||
interface CodeRunStatus {
|
||||
readonly success: boolean;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isFunction, type ConstructorArguments } from '@/TypeHelpers';
|
||||
import { isFunction } from '@/TypeHelpers';
|
||||
|
||||
/*
|
||||
Provides a unified and resilient way to extend errors across platforms.
|
||||
@@ -12,8 +12,8 @@ import { isFunction, type ConstructorArguments } from '@/TypeHelpers';
|
||||
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||
*/
|
||||
export abstract class CustomError extends Error {
|
||||
constructor(...args: ConstructorArguments<typeof Error>) {
|
||||
super(...args);
|
||||
constructor(message?: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
|
||||
fixPrototype(this, new.target.prototype);
|
||||
ensureStackTrace(this);
|
||||
|
||||
@@ -5,13 +5,13 @@ export type EnumType = number | string;
|
||||
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
||||
= { [key in T]: TEnumValue };
|
||||
|
||||
export interface EnumParser<TEnum> {
|
||||
export interface IEnumParser<TEnum> {
|
||||
parseEnum(value: string, propertyName: string): TEnum;
|
||||
}
|
||||
|
||||
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>,
|
||||
): EnumParser<TEnumValue> {
|
||||
): IEnumParser<TEnumValue> {
|
||||
return {
|
||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||
};
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
Shuffle an array of strings, returning a new array with elements in random order.
|
||||
Uses the Fisher-Yates (or Durstenfeld) algorithm.
|
||||
*/
|
||||
export function shuffle<T>(array: readonly T[]): T[] {
|
||||
const shuffledArray = [...array];
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
|
||||
}
|
||||
return shuffledArray;
|
||||
}
|
||||
@@ -1,164 +1,44 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { PlatformTimer } from './PlatformTimer';
|
||||
import type { Timer, TimeoutType } from './Timer';
|
||||
|
||||
export type CallbackType = (..._: readonly unknown[]) => void;
|
||||
|
||||
export interface ThrottleOptions {
|
||||
/** Skip the immediate execution of the callback on the first invoke */
|
||||
readonly excludeLeadingCall: boolean;
|
||||
readonly timer: Timer;
|
||||
}
|
||||
|
||||
const DefaultOptions: ThrottleOptions = {
|
||||
excludeLeadingCall: false,
|
||||
timer: PlatformTimer,
|
||||
};
|
||||
|
||||
export interface ThrottleFunction {
|
||||
(
|
||||
callback: CallbackType,
|
||||
waitInMs: number,
|
||||
options?: Partial<ThrottleOptions>,
|
||||
): CallbackType;
|
||||
}
|
||||
|
||||
export const throttle: ThrottleFunction = (
|
||||
export function throttle(
|
||||
callback: CallbackType,
|
||||
waitInMs: number,
|
||||
options: Partial<ThrottleOptions> = DefaultOptions,
|
||||
): CallbackType => {
|
||||
const defaultedOptions: ThrottleOptions = {
|
||||
...DefaultOptions,
|
||||
...options,
|
||||
};
|
||||
const throttler = new Throttler(waitInMs, callback, defaultedOptions);
|
||||
timer: Timer = PlatformTimer,
|
||||
): CallbackType {
|
||||
const throttler = new Throttler(timer, waitInMs, callback);
|
||||
return (...args: unknown[]) => throttler.invoke(...args);
|
||||
};
|
||||
}
|
||||
|
||||
class Throttler {
|
||||
private lastExecutionTime: number | null = null;
|
||||
private queuedExecutionId: TimeoutType | undefined;
|
||||
|
||||
private executionScheduler: DelayedCallbackScheduler;
|
||||
|
||||
constructor(
|
||||
private readonly waitInMs: number,
|
||||
private readonly callback: CallbackType,
|
||||
private readonly options: ThrottleOptions,
|
||||
) {
|
||||
if (!waitInMs) { throw new Error('missing delay'); }
|
||||
if (waitInMs < 0) { throw new Error('negative delay'); }
|
||||
this.executionScheduler = new DelayedCallbackScheduler(options.timer);
|
||||
}
|
||||
|
||||
public invoke(...args: unknown[]): void {
|
||||
switch (true) {
|
||||
case this.isLeadingCallWithinThrottlePeriod(): {
|
||||
if (this.options.excludeLeadingCall) {
|
||||
this.scheduleNext(args);
|
||||
return;
|
||||
}
|
||||
this.executeNow(args);
|
||||
return;
|
||||
}
|
||||
case this.isAlreadyScheduled(): {
|
||||
this.updateNextScheduled(args);
|
||||
return;
|
||||
}
|
||||
case !this.isThrottlePeriodPassed(): {
|
||||
this.scheduleNext(args);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new Error('Throttle logical error: no conditions for execution or scheduling were met.');
|
||||
}
|
||||
}
|
||||
|
||||
private isLeadingCallWithinThrottlePeriod(): boolean {
|
||||
return this.isThrottlePeriodPassed()
|
||||
&& !this.isAlreadyScheduled();
|
||||
}
|
||||
|
||||
private isThrottlePeriodPassed(): boolean {
|
||||
if (this.lastExecutionTime === null) {
|
||||
return true;
|
||||
}
|
||||
const timeSinceLastExecution = this.options.timer.dateNow() - this.lastExecutionTime;
|
||||
const isThrottleTimePassed = timeSinceLastExecution >= this.waitInMs;
|
||||
return isThrottleTimePassed;
|
||||
}
|
||||
|
||||
private isAlreadyScheduled(): boolean {
|
||||
return this.executionScheduler.getNext() !== null;
|
||||
}
|
||||
|
||||
private scheduleNext(args: unknown[]): void {
|
||||
if (this.executionScheduler.getNext()) {
|
||||
throw new Error('An execution is already scheduled.');
|
||||
}
|
||||
this.executionScheduler.resetNext(
|
||||
() => this.executeNow(args),
|
||||
this.waitInMs,
|
||||
);
|
||||
}
|
||||
|
||||
private updateNextScheduled(args: unknown[]): void {
|
||||
const nextScheduled = this.executionScheduler.getNext();
|
||||
if (!nextScheduled) {
|
||||
throw new Error('A non-existent scheduled execution cannot be updated.');
|
||||
}
|
||||
const nextDelay = nextScheduled.scheduledTime - this.dateNow();
|
||||
this.executionScheduler.resetNext(
|
||||
() => this.executeNow(args),
|
||||
nextDelay,
|
||||
);
|
||||
}
|
||||
|
||||
private executeNow(args: unknown[]): void {
|
||||
this.callback(...args);
|
||||
this.lastExecutionTime = this.dateNow();
|
||||
}
|
||||
|
||||
private dateNow(): number {
|
||||
return this.options.timer.dateNow();
|
||||
}
|
||||
}
|
||||
|
||||
interface ScheduledCallback {
|
||||
readonly scheduleTimeoutId: TimeoutType;
|
||||
readonly scheduledTime: number;
|
||||
}
|
||||
|
||||
class DelayedCallbackScheduler {
|
||||
private scheduledCallback: ScheduledCallback | null = null;
|
||||
private previouslyRun: number;
|
||||
|
||||
constructor(
|
||||
private readonly timer: Timer,
|
||||
) { }
|
||||
|
||||
public getNext(): ScheduledCallback | null {
|
||||
return this.scheduledCallback;
|
||||
}
|
||||
|
||||
public resetNext(
|
||||
callback: () => void,
|
||||
delayInMs: number,
|
||||
private readonly waitInMs: number,
|
||||
private readonly callback: CallbackType,
|
||||
) {
|
||||
this.clear();
|
||||
this.scheduledCallback = {
|
||||
scheduledTime: this.timer.dateNow() + delayInMs,
|
||||
scheduleTimeoutId: this.timer.setTimeout(() => {
|
||||
this.clear();
|
||||
callback();
|
||||
}, delayInMs),
|
||||
};
|
||||
if (!waitInMs) { throw new Error('missing delay'); }
|
||||
if (waitInMs < 0) { throw new Error('negative delay'); }
|
||||
}
|
||||
|
||||
private clear() {
|
||||
if (this.scheduledCallback === null) {
|
||||
return;
|
||||
public invoke(...args: unknown[]): void {
|
||||
const now = this.timer.dateNow();
|
||||
if (this.queuedExecutionId !== undefined) {
|
||||
this.timer.clearTimeout(this.queuedExecutionId);
|
||||
this.queuedExecutionId = undefined;
|
||||
}
|
||||
if (!this.previouslyRun || (now - this.previouslyRun >= this.waitInMs)) {
|
||||
this.callback(...args);
|
||||
this.previouslyRun = now;
|
||||
} else {
|
||||
const nextCall = () => this.invoke(...args);
|
||||
const nextCallDelayInMs = this.waitInMs - (now - this.previouslyRun);
|
||||
this.queuedExecutionId = this.timer.setTimeout(nextCall, nextCallDelayInMs);
|
||||
}
|
||||
this.timer.clearTimeout(this.scheduledCallback.scheduleTimeoutId);
|
||||
this.scheduledCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IApplication } from '@/domain/IApplication';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { assertInRange } from '@/application/Common/Enum';
|
||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
|
||||
import { ApplicationCode } from './Code/ApplicationCode';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { IScript } from '@/domain/IScript';
|
||||
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import type { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||
@@ -6,13 +6,13 @@ import type { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
public readonly code: string;
|
||||
|
||||
public readonly addedScripts: ReadonlyArray<Script>;
|
||||
public readonly addedScripts: ReadonlyArray<IScript>;
|
||||
|
||||
public readonly removedScripts: ReadonlyArray<Script>;
|
||||
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||
|
||||
public readonly changedScripts: ReadonlyArray<Script>;
|
||||
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||
|
||||
private readonly scripts: Map<Script, ICodePosition>;
|
||||
private readonly scripts: Map<IScript, ICodePosition>;
|
||||
|
||||
constructor(
|
||||
code: string,
|
||||
@@ -25,7 +25,7 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
||||
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
||||
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
||||
this.scripts = new Map<Script, ICodePosition>();
|
||||
this.scripts = new Map<IScript, ICodePosition>();
|
||||
scripts.forEach((position, selection) => {
|
||||
this.scripts.set(selection.script, position);
|
||||
});
|
||||
@@ -35,13 +35,13 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
return this.scripts.size === 0;
|
||||
}
|
||||
|
||||
public getScriptPositionInCode(script: Script): ICodePosition {
|
||||
return this.getPositionById(script.executableId);
|
||||
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||
return this.getPositionById(script.id);
|
||||
}
|
||||
|
||||
private getPositionById(scriptId: string): ICodePosition {
|
||||
const position = [...this.scripts.entries()]
|
||||
.filter(([s]) => s.executableId === scriptId)
|
||||
.filter(([s]) => s.id === scriptId)
|
||||
.map(([, pos]) => pos)
|
||||
.at(0);
|
||||
if (!position) {
|
||||
@@ -65,7 +65,7 @@ function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodeP
|
||||
function getChangedScripts(
|
||||
oldScripts: ReadonlyArray<SelectedScript>,
|
||||
newScripts: ReadonlyArray<SelectedScript>,
|
||||
): ReadonlyArray<Script> {
|
||||
): ReadonlyArray<IScript> {
|
||||
return newScripts
|
||||
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||
&& oldScript.revert !== newScript.revert))
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { IScript } from '@/domain/IScript';
|
||||
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
|
||||
export interface ICodeChangedEvent {
|
||||
readonly code: string;
|
||||
readonly addedScripts: ReadonlyArray<Script>;
|
||||
readonly removedScripts: ReadonlyArray<Script>;
|
||||
readonly changedScripts: ReadonlyArray<Script>;
|
||||
readonly addedScripts: ReadonlyArray<IScript>;
|
||||
readonly removedScripts: ReadonlyArray<IScript>;
|
||||
readonly changedScripts: ReadonlyArray<IScript>;
|
||||
isEmpty(): boolean;
|
||||
getScriptPositionInCode(script: Script): ICodePosition;
|
||||
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { FilterChange } from './Event/FilterChange';
|
||||
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
|
||||
import type { FilterResult } from './Result/FilterResult';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { IScript } from '@/domain/IScript';
|
||||
import type { ICategory } from '@/domain/ICategory';
|
||||
import type { FilterResult } from './FilterResult';
|
||||
|
||||
export class AppliedFilterResult implements FilterResult {
|
||||
constructor(
|
||||
public readonly scriptMatches: ReadonlyArray<Script>,
|
||||
public readonly categoryMatches: ReadonlyArray<Category>,
|
||||
public readonly scriptMatches: ReadonlyArray<IScript>,
|
||||
public readonly categoryMatches: ReadonlyArray<ICategory>,
|
||||
public readonly query: string,
|
||||
) {
|
||||
if (!query) { throw new Error('Query is empty or undefined'); }
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { IScript, ICategory } from '@/domain/ICategory';
|
||||
|
||||
export interface FilterResult {
|
||||
readonly categoryMatches: ReadonlyArray<Category>;
|
||||
readonly scriptMatches: ReadonlyArray<Script>;
|
||||
readonly categoryMatches: ReadonlyArray<ICategory>;
|
||||
readonly scriptMatches: ReadonlyArray<IScript>;
|
||||
readonly query: string;
|
||||
hasAnyMatches(): boolean;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { FilterResult } from '../Result/FilterResult';
|
||||
|
||||
export interface FilterStrategy {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||
import type { Documentable } from '@/domain/Executables/Documentable';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { ICategory, IScript } from '@/domain/ICategory';
|
||||
import type { IScriptCode } from '@/domain/IScriptCode';
|
||||
import type { IDocumentable } from '@/domain/IDocumentable';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { AppliedFilterResult } from '../Result/AppliedFilterResult';
|
||||
import type { FilterStrategy } from './FilterStrategy';
|
||||
import type { FilterResult } from '../Result/FilterResult';
|
||||
@@ -25,7 +24,7 @@ export class LinearFilterStrategy implements FilterStrategy {
|
||||
}
|
||||
|
||||
function matchesCategory(
|
||||
category: Category,
|
||||
category: ICategory,
|
||||
filterLowercase: string,
|
||||
): boolean {
|
||||
return matchesAny(
|
||||
@@ -35,7 +34,7 @@ function matchesCategory(
|
||||
}
|
||||
|
||||
function matchesScript(
|
||||
script: Script,
|
||||
script: IScript,
|
||||
filterLowercase: string,
|
||||
): boolean {
|
||||
return matchesAny(
|
||||
@@ -59,7 +58,7 @@ function matchName(
|
||||
}
|
||||
|
||||
function matchCode(
|
||||
code: ScriptCode,
|
||||
code: IScriptCode,
|
||||
filterLowercase: string,
|
||||
): boolean {
|
||||
if (code.execute.toLowerCase().includes(filterLowercase)) {
|
||||
@@ -72,7 +71,7 @@ function matchCode(
|
||||
}
|
||||
|
||||
function matchDocumentation(
|
||||
documentable: Documentable,
|
||||
documentable: IDocumentable,
|
||||
filterLowercase: string,
|
||||
): boolean {
|
||||
return documentable.docs.some(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { IApplicationCode } from './Code/IApplicationCode';
|
||||
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { ICategory } from '@/domain/ICategory';
|
||||
import type { CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||
|
||||
export interface ReadonlyCategorySelection {
|
||||
areAllScriptsSelected(category: Category): boolean;
|
||||
isAnyScriptSelected(category: Category): boolean;
|
||||
areAllScriptsSelected(category: ICategory): boolean;
|
||||
isAnyScriptSelected(category: ICategory): boolean;
|
||||
}
|
||||
|
||||
export interface CategorySelection extends ReadonlyCategorySelection {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
|
||||
type CategorySelectionStatus = {
|
||||
readonly isSelected: true;
|
||||
readonly isReverted: boolean;
|
||||
@@ -8,7 +6,7 @@ type CategorySelectionStatus = {
|
||||
};
|
||||
|
||||
export interface CategorySelectionChange {
|
||||
readonly categoryId: ExecutableId;
|
||||
readonly categoryId: number;
|
||||
readonly newStatus: CategorySelectionStatus;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { ICategory } from '@/domain/ICategory';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||
import type { CategorySelection } from './CategorySelection';
|
||||
import type { ScriptSelection } from '../Script/ScriptSelection';
|
||||
@@ -13,7 +13,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
||||
|
||||
}
|
||||
|
||||
public areAllScriptsSelected(category: Category): boolean {
|
||||
public areAllScriptsSelected(category: ICategory): boolean {
|
||||
const { selectedScripts } = this.scriptSelection;
|
||||
if (selectedScripts.length === 0) {
|
||||
return false;
|
||||
@@ -23,11 +23,11 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
||||
return false;
|
||||
}
|
||||
return scripts.every(
|
||||
(script) => selectedScripts.some((selected) => selected.id === script.executableId),
|
||||
(script) => selectedScripts.some((selected) => selected.id === script.id),
|
||||
);
|
||||
}
|
||||
|
||||
public isAnyScriptSelected(category: Category): boolean {
|
||||
public isAnyScriptSelected(category: ICategory): boolean {
|
||||
const { selectedScripts } = this.scriptSelection;
|
||||
if (selectedScripts.length === 0) {
|
||||
return false;
|
||||
@@ -50,7 +50,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
||||
const scripts = category.getAllScriptsRecursively();
|
||||
const scriptsChangesInCategory = scripts
|
||||
.map((script): ScriptSelectionChange => ({
|
||||
scriptId: script.executableId,
|
||||
scriptId: script.id,
|
||||
newStatus: {
|
||||
...change.newStatus,
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { IScript } from '@/domain/IScript';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
||||
import { UserSelectedScript } from './UserSelectedScript';
|
||||
import type { ScriptSelection } from './ScriptSelection';
|
||||
@@ -16,7 +16,7 @@ export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeComma
|
||||
export class DebouncedScriptSelection implements ScriptSelection {
|
||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||
|
||||
private readonly scripts: Repository<SelectedScript>;
|
||||
private readonly scripts: Repository<string, SelectedScript>;
|
||||
|
||||
public readonly processChanges: ScriptSelection['processChanges'];
|
||||
|
||||
@@ -25,7 +25,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
debounce: DebounceFunction = batchedDebounce,
|
||||
) {
|
||||
this.scripts = new InMemoryRepository<SelectedScript>();
|
||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||
for (const script of selectedScripts) {
|
||||
this.scripts.addItem(script);
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
public selectAll(): void {
|
||||
const scriptsToSelect = this.collection
|
||||
.getAllScripts()
|
||||
.filter((script) => !this.scripts.exists(script.executableId))
|
||||
.filter((script) => !this.scripts.exists(script.id))
|
||||
.map((script) => new UserSelectedScript(script, false));
|
||||
if (scriptsToSelect.length === 0) {
|
||||
return;
|
||||
@@ -80,7 +80,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
});
|
||||
}
|
||||
|
||||
public selectOnly(scripts: readonly Script[]): void {
|
||||
public selectOnly(scripts: readonly IScript[]): void {
|
||||
assertNonEmptyScriptSelection(scripts);
|
||||
this.processChanges({
|
||||
changes: [
|
||||
@@ -116,9 +116,9 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
private applyChange(change: ScriptSelectionChange): number {
|
||||
const script = this.collection.getScript(change.scriptId);
|
||||
if (change.newStatus.isSelected) {
|
||||
return this.addOrUpdateScript(script.executableId, change.newStatus.isReverted);
|
||||
return this.addOrUpdateScript(script.id, change.newStatus.isReverted);
|
||||
}
|
||||
return this.removeScript(script.executableId);
|
||||
return this.removeScript(script.id);
|
||||
}
|
||||
|
||||
private addOrUpdateScript(scriptId: string, revert: boolean): number {
|
||||
@@ -145,31 +145,31 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
}
|
||||
}
|
||||
|
||||
function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) {
|
||||
function assertNonEmptyScriptSelection(selectedItems: readonly IScript[]) {
|
||||
if (selectedItems.length === 0) {
|
||||
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
|
||||
}
|
||||
}
|
||||
|
||||
function getScriptIdsToBeSelected(
|
||||
existingItems: ReadonlyRepository<SelectedScript>,
|
||||
desiredScripts: readonly Script[],
|
||||
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||
desiredScripts: readonly IScript[],
|
||||
): string[] {
|
||||
return desiredScripts
|
||||
.filter((script) => !existingItems.exists(script.executableId))
|
||||
.map((script) => script.executableId);
|
||||
.filter((script) => !existingItems.exists(script.id))
|
||||
.map((script) => script.id);
|
||||
}
|
||||
|
||||
function getScriptIdsToBeDeselected(
|
||||
existingItems: ReadonlyRepository<SelectedScript>,
|
||||
desiredScripts: readonly Script[],
|
||||
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||
desiredScripts: readonly IScript[],
|
||||
): string[] {
|
||||
return existingItems
|
||||
.getItems()
|
||||
.filter((existing) => !desiredScripts.some((script) => existing.id === script.executableId))
|
||||
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
|
||||
.map((script) => script.id);
|
||||
}
|
||||
|
||||
function equals(a: SelectedScript, b: SelectedScript): boolean {
|
||||
return a.script.executableId === b.script.executableId && a.revert === b.revert;
|
||||
return a.script.equals(b.script.id) && a.revert === b.revert;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { IScript } from '@/domain/IScript';
|
||||
import type { SelectedScript } from './SelectedScript';
|
||||
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface ReadonlyScriptSelection {
|
||||
}
|
||||
|
||||
export interface ScriptSelection extends ReadonlyScriptSelection {
|
||||
selectOnly(scripts: readonly Script[]): void;
|
||||
selectOnly(scripts: readonly IScript[]): void;
|
||||
selectAll(): void;
|
||||
deselectAll(): void;
|
||||
processChanges(action: ScriptSelectionChangeCommand): void;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
|
||||
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||
import type { IScript } from '@/domain/IScript';
|
||||
|
||||
export interface SelectedScript extends RepositoryEntity {
|
||||
readonly script: Script;
|
||||
type ScriptId = IScript['id'];
|
||||
|
||||
export interface SelectedScript extends IEntity<ScriptId> {
|
||||
readonly script: IScript;
|
||||
readonly revert: boolean;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import type { IScript } from '@/domain/IScript';
|
||||
import type { SelectedScript } from './SelectedScript';
|
||||
|
||||
export class UserSelectedScript implements RepositoryEntity {
|
||||
public readonly id: string;
|
||||
type SelectedScriptId = SelectedScript['id'];
|
||||
|
||||
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
|
||||
constructor(
|
||||
public readonly script: Script,
|
||||
public readonly script: IScript,
|
||||
public readonly revert: boolean,
|
||||
) {
|
||||
this.id = script.executableId;
|
||||
super(script.id);
|
||||
if (revert && !script.canRevert()) {
|
||||
throw new Error(`The script with ID '${script.executableId}' is not reversible and cannot be reverted.`);
|
||||
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
||||
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
||||
import type { CategorySelection } from './Category/CategorySelection';
|
||||
|
||||
@@ -1,48 +1,40 @@
|
||||
import type { CollectionData } from '@/application/collections/';
|
||||
import type { IApplication } from '@/domain/IApplication';
|
||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import WindowsData from '@/application/collections/windows.yaml';
|
||||
import MacOsData from '@/application/collections/macos.yaml';
|
||||
import LinuxData from '@/application/collections/linux.yaml';
|
||||
import { parseProjectDetails, type ProjectDetailsParser } from '@/application/Parser/ProjectDetailsParser';
|
||||
import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser';
|
||||
import { Application } from '@/domain/Application';
|
||||
import { parseCategoryCollection, type CategoryCollectionParser } from './CategoryCollectionParser';
|
||||
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
|
||||
import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
|
||||
export function parseApplication(
|
||||
collectionsData: readonly CollectionData[] = PreParsedCollections,
|
||||
utilities: ApplicationParserUtilities = DefaultUtilities,
|
||||
categoryParser = parseCategoryCollection,
|
||||
projectDetailsParser = parseProjectDetails,
|
||||
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
||||
collectionsData = PreParsedCollections,
|
||||
): IApplication {
|
||||
validateCollectionsData(collectionsData, utilities.validator);
|
||||
const projectDetails = utilities.parseProjectDetails();
|
||||
validateCollectionsData(collectionsData);
|
||||
const projectDetails = projectDetailsParser(metadata);
|
||||
const collections = collectionsData.map(
|
||||
(collection) => utilities.parseCategoryCollection(collection, projectDetails),
|
||||
(collection) => categoryParser(collection, projectDetails),
|
||||
);
|
||||
const app = new Application(projectDetails, collections);
|
||||
return app;
|
||||
}
|
||||
|
||||
const PreParsedCollections: readonly CollectionData[] = [
|
||||
export type CategoryCollectionParserType
|
||||
= (file: CollectionData, projectDetails: ProjectDetails) => ICategoryCollection;
|
||||
|
||||
const PreParsedCollections: readonly CollectionData [] = [
|
||||
WindowsData, MacOsData, LinuxData,
|
||||
];
|
||||
|
||||
function validateCollectionsData(
|
||||
collections: readonly CollectionData[],
|
||||
validator: TypeValidator,
|
||||
) {
|
||||
validator.assertNonEmptyCollection({
|
||||
value: collections,
|
||||
valueName: 'collections',
|
||||
});
|
||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||
if (!collections.length) {
|
||||
throw new Error('missing collections');
|
||||
}
|
||||
}
|
||||
|
||||
interface ApplicationParserUtilities {
|
||||
readonly parseCategoryCollection: CategoryCollectionParser;
|
||||
readonly validator: TypeValidator;
|
||||
readonly parseProjectDetails: ProjectDetailsParser;
|
||||
}
|
||||
|
||||
const DefaultUtilities: ApplicationParserUtilities = {
|
||||
parseCategoryCollection,
|
||||
parseProjectDetails,
|
||||
validator: createTypeValidator(),
|
||||
};
|
||||
|
||||
@@ -1,75 +1,34 @@
|
||||
import type { CollectionData } from '@/application/collections/';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { CategoryCollection } from '@/domain/Collection/CategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||
import { createEnumParser, type EnumParser } from '../Common/Enum';
|
||||
import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
|
||||
import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
|
||||
import { createCollectionUtilities, type CategoryCollectionSpecificUtilitiesFactory } from './Executable/CategoryCollectionSpecificUtilities';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
import { parseCategory } from './CategoryParser';
|
||||
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||
|
||||
export const parseCategoryCollection: CategoryCollectionParser = (
|
||||
content,
|
||||
projectDetails,
|
||||
utilities: CategoryCollectionParserUtilities = DefaultUtilities,
|
||||
) => {
|
||||
validateCollection(content, utilities.validator);
|
||||
const scripting = utilities.parseScriptingDefinition(content.scripting, projectDetails);
|
||||
const collectionUtilities = utilities.createUtilities(content.functions, scripting);
|
||||
const categories = content.actions.map(
|
||||
(action) => utilities.parseCategory(action, collectionUtilities),
|
||||
);
|
||||
const os = utilities.osParser.parseEnum(content.os, 'os');
|
||||
const collection = utilities.createCategoryCollection({
|
||||
os, actions: categories, scripting,
|
||||
});
|
||||
return collection;
|
||||
};
|
||||
|
||||
export type CategoryCollectionFactory = (
|
||||
...parameters: ConstructorParameters<typeof CategoryCollection>
|
||||
) => ICategoryCollection;
|
||||
|
||||
export interface CategoryCollectionParser {
|
||||
(
|
||||
content: CollectionData,
|
||||
projectDetails: ProjectDetails,
|
||||
utilities?: CategoryCollectionParserUtilities,
|
||||
): ICategoryCollection;
|
||||
}
|
||||
|
||||
function validateCollection(
|
||||
export function parseCategoryCollection(
|
||||
content: CollectionData,
|
||||
validator: TypeValidator,
|
||||
): void {
|
||||
validator.assertObject({
|
||||
value: content,
|
||||
valueName: 'collection',
|
||||
allowedProperties: [
|
||||
'os', 'scripting', 'actions', 'functions',
|
||||
],
|
||||
});
|
||||
validator.assertNonEmptyCollection({
|
||||
value: content.actions,
|
||||
valueName: '"actions" in collection',
|
||||
});
|
||||
projectDetails: ProjectDetails,
|
||||
osParser = createEnumParser(OperatingSystem),
|
||||
): ICategoryCollection {
|
||||
validate(content);
|
||||
const scripting = new ScriptingDefinitionParser()
|
||||
.parse(content.scripting, projectDetails);
|
||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||
const categories = content.actions.map((action) => parseCategory(action, context));
|
||||
const os = osParser.parseEnum(content.os, 'os');
|
||||
const collection = new CategoryCollection(
|
||||
os,
|
||||
categories,
|
||||
scripting,
|
||||
);
|
||||
return collection;
|
||||
}
|
||||
|
||||
interface CategoryCollectionParserUtilities {
|
||||
readonly osParser: EnumParser<OperatingSystem>;
|
||||
readonly validator: TypeValidator;
|
||||
readonly parseScriptingDefinition: ScriptingDefinitionParser;
|
||||
readonly createUtilities: CategoryCollectionSpecificUtilitiesFactory;
|
||||
readonly parseCategory: CategoryParser;
|
||||
readonly createCategoryCollection: CategoryCollectionFactory;
|
||||
function validate(content: CollectionData): void {
|
||||
if (!content.actions.length) {
|
||||
throw new Error('content does not define any action');
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultUtilities: CategoryCollectionParserUtilities = {
|
||||
osParser: createEnumParser(OperatingSystem),
|
||||
validator: createTypeValidator(),
|
||||
parseScriptingDefinition,
|
||||
createUtilities: createCollectionUtilities,
|
||||
parseCategory,
|
||||
createCategoryCollection: (...args) => new CategoryCollection(...args),
|
||||
};
|
||||
|
||||
133
src/application/Parser/CategoryParser.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type {
|
||||
CategoryData, ScriptData, CategoryOrScriptData,
|
||||
} from '@/application/collections/';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||
import { parseDocs } from './DocumentationParser';
|
||||
import { parseScript } from './Script/ScriptParser';
|
||||
import type { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||
|
||||
let categoryIdCounter = 0;
|
||||
|
||||
export function parseCategory(
|
||||
category: CategoryData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
factory: CategoryFactoryType = CategoryFactory,
|
||||
): Category {
|
||||
return parseCategoryRecursively({
|
||||
categoryData: category,
|
||||
context,
|
||||
factory,
|
||||
});
|
||||
}
|
||||
|
||||
interface ICategoryParseContext {
|
||||
readonly categoryData: CategoryData,
|
||||
readonly context: ICategoryCollectionParseContext,
|
||||
readonly factory: CategoryFactoryType,
|
||||
readonly parentCategory?: CategoryData,
|
||||
}
|
||||
|
||||
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
|
||||
ensureValidCategory(context.categoryData, context.parentCategory);
|
||||
const children: ICategoryChildren = {
|
||||
subCategories: new Array<Category>(),
|
||||
subScripts: new Array<Script>(),
|
||||
};
|
||||
for (const data of context.categoryData.children) {
|
||||
parseNode({
|
||||
nodeData: data,
|
||||
children,
|
||||
parent: context.categoryData,
|
||||
factory: context.factory,
|
||||
context: context.context,
|
||||
});
|
||||
}
|
||||
try {
|
||||
return context.factory(
|
||||
/* id: */ categoryIdCounter++,
|
||||
/* name: */ context.categoryData.category,
|
||||
/* docs: */ parseDocs(context.categoryData),
|
||||
/* categories: */ children.subCategories,
|
||||
/* scripts: */ children.subScripts,
|
||||
);
|
||||
} catch (err) {
|
||||
return new NodeValidator({
|
||||
type: NodeType.Category,
|
||||
selfNode: context.categoryData,
|
||||
parentNode: context.parentCategory,
|
||||
}).throw(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
|
||||
new NodeValidator({
|
||||
type: NodeType.Category,
|
||||
selfNode: category,
|
||||
parentNode: parentCategory,
|
||||
})
|
||||
.assertDefined(category)
|
||||
.assertValidName(category.category)
|
||||
.assert(
|
||||
() => category.children.length > 0,
|
||||
`"${category.category}" has no children.`,
|
||||
);
|
||||
}
|
||||
|
||||
interface ICategoryChildren {
|
||||
subCategories: Category[];
|
||||
subScripts: Script[];
|
||||
}
|
||||
|
||||
interface INodeParseContext {
|
||||
readonly nodeData: CategoryOrScriptData;
|
||||
readonly children: ICategoryChildren;
|
||||
readonly parent: CategoryData;
|
||||
readonly factory: CategoryFactoryType;
|
||||
readonly context: ICategoryCollectionParseContext;
|
||||
}
|
||||
function parseNode(context: INodeParseContext) {
|
||||
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
|
||||
validator.assertDefined(context.nodeData);
|
||||
if (isCategory(context.nodeData)) {
|
||||
const subCategory = parseCategoryRecursively({
|
||||
categoryData: context.nodeData,
|
||||
context: context.context,
|
||||
factory: context.factory,
|
||||
parentCategory: context.parent,
|
||||
});
|
||||
context.children.subCategories.push(subCategory);
|
||||
} else if (isScript(context.nodeData)) {
|
||||
const script = parseScript(context.nodeData, context.context);
|
||||
context.children.subScripts.push(script);
|
||||
} else {
|
||||
validator.throw('Node is neither a category or a script.');
|
||||
}
|
||||
}
|
||||
|
||||
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||
return hasCode(data) || hasCall(data);
|
||||
}
|
||||
|
||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||
return hasProperty(data, 'category');
|
||||
}
|
||||
|
||||
function hasCode(data: unknown): boolean {
|
||||
return hasProperty(data, 'code');
|
||||
}
|
||||
|
||||
function hasCall(data: unknown) {
|
||||
return hasProperty(data, 'call');
|
||||
}
|
||||
|
||||
function hasProperty(object: unknown, propertyName: string) {
|
||||
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||
}
|
||||
|
||||
export type CategoryFactoryType = (
|
||||
...parameters: ConstructorParameters<typeof Category>) => Category;
|
||||
|
||||
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);
|
||||
@@ -1,42 +0,0 @@
|
||||
import { CustomError } from '@/application/Common/CustomError';
|
||||
|
||||
export interface ErrorWithContextWrapper {
|
||||
(
|
||||
error: Error,
|
||||
additionalContext: string,
|
||||
): Error;
|
||||
}
|
||||
|
||||
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
||||
error: Error,
|
||||
additionalContext: string,
|
||||
) => {
|
||||
return (error instanceof ContextualError ? error : new ContextualError(error))
|
||||
.withAdditionalContext(additionalContext);
|
||||
};
|
||||
|
||||
/* AggregateError is similar but isn't well-serialized or displayed by browsers */
|
||||
class ContextualError extends CustomError {
|
||||
private readonly additionalContext = new Array<string>();
|
||||
|
||||
constructor(
|
||||
public readonly innerError: Error,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public withAdditionalContext(additionalContext: string): this {
|
||||
this.additionalContext.push(additionalContext);
|
||||
return this;
|
||||
}
|
||||
|
||||
public get message(): string { // toString() is not used when Chromium logs it on console
|
||||
return [
|
||||
'\n',
|
||||
this.innerError.message,
|
||||
'\n',
|
||||
'Additional context:',
|
||||
...this.additionalContext.map((context, index) => `${index + 1}: ${context}`),
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import type { PropertyKeys } from '@/TypeHelpers';
|
||||
import {
|
||||
isNullOrUndefined, isArray, isPlainObject, isString,
|
||||
} from '@/TypeHelpers';
|
||||
|
||||
export interface TypeValidator {
|
||||
assertObject<T>(assertion: ObjectAssertion<T>): void;
|
||||
assertNonEmptyCollection(assertion: NonEmptyCollectionAssertion): void;
|
||||
assertNonEmptyString(assertion: NonEmptyStringAssertion): void;
|
||||
}
|
||||
|
||||
export interface NonEmptyCollectionAssertion {
|
||||
readonly value: unknown;
|
||||
readonly valueName: string;
|
||||
}
|
||||
|
||||
export interface RegexValidationRule {
|
||||
readonly expectedMatch: RegExp;
|
||||
readonly errorMessage: string;
|
||||
}
|
||||
|
||||
export interface NonEmptyStringAssertion {
|
||||
readonly value: unknown;
|
||||
readonly valueName: string;
|
||||
readonly rule?: RegexValidationRule;
|
||||
}
|
||||
|
||||
export interface ObjectAssertion<T> {
|
||||
readonly value: T | unknown;
|
||||
readonly valueName: string;
|
||||
readonly allowedProperties?: readonly PropertyKeys<T>[];
|
||||
}
|
||||
|
||||
export function createTypeValidator(): TypeValidator {
|
||||
return {
|
||||
assertObject: (assertion) => {
|
||||
assertDefined(assertion.value, assertion.valueName);
|
||||
assertPlainObject(assertion.value, assertion.valueName);
|
||||
assertNoEmptyProperties(assertion.value, assertion.valueName);
|
||||
if (assertion.allowedProperties !== undefined) {
|
||||
const allowedProperties = assertion.allowedProperties.map((p) => p as string);
|
||||
assertAllowedProperties(assertion.value, assertion.valueName, allowedProperties);
|
||||
}
|
||||
},
|
||||
assertNonEmptyCollection: (assertion) => {
|
||||
assertDefined(assertion.value, assertion.valueName);
|
||||
assertArray(assertion.value, assertion.valueName);
|
||||
assertNonEmpty(assertion.value, assertion.valueName);
|
||||
},
|
||||
assertNonEmptyString: (assertion) => {
|
||||
assertDefined(assertion.value, assertion.valueName);
|
||||
assertString(assertion.value, assertion.valueName);
|
||||
if (assertion.value.length === 0) {
|
||||
throw new Error(`'${assertion.valueName}' is missing.`);
|
||||
}
|
||||
if (assertion.rule) {
|
||||
if (!assertion.value.match(assertion.rule.expectedMatch)) {
|
||||
throw new Error(assertion.rule.errorMessage);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function assertDefined<T>(
|
||||
value: T,
|
||||
valueName: string,
|
||||
): asserts value is NonNullable<T> {
|
||||
if (isNullOrUndefined(value)) {
|
||||
throw new Error(`'${valueName}' is missing.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertPlainObject(
|
||||
value: unknown,
|
||||
valueName: string,
|
||||
): asserts value is object {
|
||||
if (!isPlainObject(value)) {
|
||||
throw new Error(`'${valueName}' is not an object.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNoEmptyProperties(
|
||||
value: object,
|
||||
valueName: string,
|
||||
): void {
|
||||
if (Object.keys(value).length === 0) {
|
||||
throw new Error(`'${valueName}' is an empty object without properties.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertAllowedProperties(
|
||||
value: object,
|
||||
valueName: string,
|
||||
allowedProperties: readonly string[],
|
||||
): void {
|
||||
const properties = Object.keys(value).map((p) => p as string);
|
||||
const disallowedProperties = properties.filter(
|
||||
(prop) => !allowedProperties.map((p) => p as string).includes(prop),
|
||||
);
|
||||
if (disallowedProperties.length > 0) {
|
||||
throw new Error(`'${valueName}' has disallowed properties: ${disallowedProperties.join(', ')}.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertArray(
|
||||
value: unknown,
|
||||
valueName: string,
|
||||
): asserts value is Array<unknown> {
|
||||
if (!isArray(value)) {
|
||||
throw new Error(`'${valueName}' should be of type 'array', but is of type '${typeof value}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertString(
|
||||
value: unknown,
|
||||
valueName: string,
|
||||
): asserts value is string {
|
||||
if (!isString(value)) {
|
||||
throw new Error(`'${valueName}' should be of type 'string', but is of type '${typeof value}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNonEmpty(
|
||||
value: Array<unknown>,
|
||||
valueName: string,
|
||||
): void {
|
||||
if (value.length === 0) {
|
||||
throw new Error(`'${valueName}' cannot be an empty array.`);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||
import { isString, isArray } from '@/TypeHelpers';
|
||||
|
||||
export const parseDocs: DocsParser = (documentable) => {
|
||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||
const { docs } = documentable;
|
||||
if (!docs) {
|
||||
return [];
|
||||
@@ -9,12 +9,6 @@ export const parseDocs: DocsParser = (documentable) => {
|
||||
let result = new DocumentationContainer();
|
||||
result = addDocs(docs, result);
|
||||
return result.getAll();
|
||||
};
|
||||
|
||||
export interface DocsParser {
|
||||
(
|
||||
documentable: DocumentableData,
|
||||
): readonly string[];
|
||||
}
|
||||
|
||||
function addDocs(
|
||||
@@ -50,5 +44,5 @@ class DocumentationContainer {
|
||||
}
|
||||
|
||||
function throwInvalidType(): never {
|
||||
throw new Error('docs field (documentation) must be a single string or an array of strings.');
|
||||
throw new Error('docs field (documentation) must be an array of strings');
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import type { FunctionData } from '@/application/collections/';
|
||||
import { ScriptCompiler } from './Script/Compiler/ScriptCompiler';
|
||||
import { SyntaxFactory } from './Script/Validation/Syntax/SyntaxFactory';
|
||||
import type { IScriptCompiler } from './Script/Compiler/IScriptCompiler';
|
||||
import type { ILanguageSyntax } from './Script/Validation/Syntax/ILanguageSyntax';
|
||||
import type { ISyntaxFactory } from './Script/Validation/Syntax/ISyntaxFactory';
|
||||
|
||||
export interface CategoryCollectionSpecificUtilities {
|
||||
readonly compiler: IScriptCompiler;
|
||||
readonly syntax: ILanguageSyntax;
|
||||
}
|
||||
|
||||
export const createCollectionUtilities: CategoryCollectionSpecificUtilitiesFactory = (
|
||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||
scripting: IScriptingDefinition,
|
||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||
) => {
|
||||
const syntax = syntaxFactory.create(scripting.language);
|
||||
return {
|
||||
compiler: new ScriptCompiler({
|
||||
functions: functionsData ?? [],
|
||||
syntax,
|
||||
}),
|
||||
syntax,
|
||||
};
|
||||
};
|
||||
|
||||
export interface CategoryCollectionSpecificUtilitiesFactory {
|
||||
(
|
||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||
scripting: IScriptingDefinition,
|
||||
syntaxFactory?: ISyntaxFactory,
|
||||
): CategoryCollectionSpecificUtilities;
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
import type {
|
||||
CategoryData, ScriptData, ExecutableData,
|
||||
} from '@/application/collections/';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { createCategory, type CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
|
||||
import { parseDocs, type DocsParser } from './DocumentationParser';
|
||||
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
|
||||
import { ExecutableType } from './Validation/ExecutableType';
|
||||
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
|
||||
|
||||
export const parseCategory: CategoryParser = (
|
||||
category: CategoryData,
|
||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||
categoryUtilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
|
||||
) => {
|
||||
return parseCategoryRecursively({
|
||||
categoryData: category,
|
||||
collectionUtilities,
|
||||
categoryUtilities,
|
||||
});
|
||||
};
|
||||
|
||||
export interface CategoryParser {
|
||||
(
|
||||
category: CategoryData,
|
||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||
categoryUtilities?: CategoryParserUtilities,
|
||||
): Category;
|
||||
}
|
||||
|
||||
interface CategoryParseContext {
|
||||
readonly categoryData: CategoryData;
|
||||
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
|
||||
readonly parentCategory?: CategoryData;
|
||||
readonly categoryUtilities: CategoryParserUtilities;
|
||||
}
|
||||
|
||||
function parseCategoryRecursively(
|
||||
context: CategoryParseContext,
|
||||
): Category | never {
|
||||
const validator = ensureValidCategory(context);
|
||||
const children: CategoryChildren = {
|
||||
subcategories: new Array<Category>(),
|
||||
subscripts: new Array<Script>(),
|
||||
};
|
||||
for (const data of context.categoryData.children) {
|
||||
parseUnknownExecutable({
|
||||
data,
|
||||
children,
|
||||
parent: context.categoryData,
|
||||
categoryUtilities: context.categoryUtilities,
|
||||
collectionUtilities: context.collectionUtilities,
|
||||
});
|
||||
}
|
||||
try {
|
||||
return context.categoryUtilities.createCategory({
|
||||
executableId: context.categoryData.category, // arbitrary ID
|
||||
name: context.categoryData.category,
|
||||
docs: context.categoryUtilities.parseDocs(context.categoryData),
|
||||
subcategories: children.subcategories,
|
||||
scripts: children.subscripts,
|
||||
});
|
||||
} catch (error) {
|
||||
throw context.categoryUtilities.wrapError(
|
||||
error,
|
||||
validator.createContextualErrorMessage('Failed to parse category.'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidCategory(
|
||||
context: CategoryParseContext,
|
||||
): ExecutableValidator {
|
||||
const category = context.categoryData;
|
||||
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
|
||||
type: ExecutableType.Category,
|
||||
self: context.categoryData,
|
||||
parentCategory: context.parentCategory,
|
||||
});
|
||||
validator.assertType((v) => v.assertObject({
|
||||
value: category,
|
||||
valueName: category.category ?? 'category',
|
||||
allowedProperties: [
|
||||
'docs', 'children', 'category',
|
||||
],
|
||||
}));
|
||||
validator.assertValidName(category.category);
|
||||
validator.assertType((v) => v.assertNonEmptyCollection({
|
||||
value: category.children,
|
||||
valueName: category.category,
|
||||
}));
|
||||
return validator;
|
||||
}
|
||||
|
||||
interface CategoryChildren {
|
||||
readonly subcategories: Category[];
|
||||
readonly subscripts: Script[];
|
||||
}
|
||||
|
||||
interface ExecutableParseContext {
|
||||
readonly data: ExecutableData;
|
||||
readonly children: CategoryChildren;
|
||||
readonly parent: CategoryData;
|
||||
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
|
||||
readonly categoryUtilities: CategoryParserUtilities;
|
||||
}
|
||||
|
||||
function parseUnknownExecutable(context: ExecutableParseContext) {
|
||||
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
|
||||
self: context.data,
|
||||
parentCategory: context.parent,
|
||||
});
|
||||
validator.assertType((v) => v.assertObject({
|
||||
value: context.data,
|
||||
valueName: 'Executable',
|
||||
}));
|
||||
validator.assert(
|
||||
() => isCategory(context.data) || isScript(context.data),
|
||||
'Executable is neither a category or a script.',
|
||||
);
|
||||
if (isCategory(context.data)) {
|
||||
const subCategory = parseCategoryRecursively({
|
||||
categoryData: context.data,
|
||||
collectionUtilities: context.collectionUtilities,
|
||||
parentCategory: context.parent,
|
||||
categoryUtilities: context.categoryUtilities,
|
||||
});
|
||||
context.children.subcategories.push(subCategory);
|
||||
} else { // A script
|
||||
const script = context.categoryUtilities.parseScript(context.data, context.collectionUtilities);
|
||||
context.children.subscripts.push(script);
|
||||
}
|
||||
}
|
||||
|
||||
function isScript(data: ExecutableData): data is ScriptData {
|
||||
return hasCode(data) || hasCall(data);
|
||||
}
|
||||
|
||||
function isCategory(data: ExecutableData): data is CategoryData {
|
||||
return hasProperty(data, 'category');
|
||||
}
|
||||
|
||||
function hasCode(data: unknown): boolean {
|
||||
return hasProperty(data, 'code');
|
||||
}
|
||||
|
||||
function hasCall(data: unknown) {
|
||||
return hasProperty(data, 'call');
|
||||
}
|
||||
|
||||
function hasProperty(
|
||||
object: unknown,
|
||||
propertyName: string,
|
||||
): object is NonNullable<object> {
|
||||
if (typeof object !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (object === null) { // `typeof object` is `null`
|
||||
return false;
|
||||
}
|
||||
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||
}
|
||||
|
||||
interface CategoryParserUtilities {
|
||||
readonly createCategory: CategoryFactory;
|
||||
readonly wrapError: ErrorWithContextWrapper;
|
||||
readonly createValidator: ExecutableValidatorFactory;
|
||||
readonly parseScript: ScriptParser;
|
||||
readonly parseDocs: DocsParser;
|
||||
}
|
||||
|
||||
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
|
||||
createCategory,
|
||||
wrapError: wrapErrorWithAdditionalContext,
|
||||
createValidator: createExecutableDataValidator,
|
||||
parseScript,
|
||||
parseDocs,
|
||||
};
|
||||
@@ -1,127 +0,0 @@
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
|
||||
import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
|
||||
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory';
|
||||
import type { IExpressionParser } from '../IExpressionParser';
|
||||
import type { IExpression } from '../../Expression/IExpression';
|
||||
import type { FunctionParameter } from '../../../Function/Parameter/FunctionParameter';
|
||||
import type { IFunctionParameterCollection, IReadOnlyFunctionParameterCollection } from '../../../Function/Parameter/IFunctionParameterCollection';
|
||||
|
||||
export interface RegexParserUtilities {
|
||||
readonly wrapError: ErrorWithContextWrapper;
|
||||
readonly createPosition: ExpressionPositionFactory;
|
||||
readonly createExpression: ExpressionFactory;
|
||||
readonly createParameterCollection: FunctionParameterCollectionFactory;
|
||||
}
|
||||
|
||||
export abstract class RegexParser implements IExpressionParser {
|
||||
protected abstract readonly regex: RegExp;
|
||||
|
||||
public constructor(
|
||||
private readonly utilities: RegexParserUtilities = DefaultRegexParserUtilities,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
return Array.from(this.findRegexExpressions(code));
|
||||
}
|
||||
|
||||
protected abstract buildExpression(match: RegExpMatchArray): PrimitiveExpression;
|
||||
|
||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||
if (!code) {
|
||||
throw new Error(
|
||||
this.buildErrorMessageWithContext({ errorMessage: 'missing code', code: 'EMPTY' }),
|
||||
);
|
||||
}
|
||||
const createErrorContext = (message: string): ErrorContext => ({ code, errorMessage: message });
|
||||
const matches = this.doOrRethrow(
|
||||
() => code.matchAll(this.regex),
|
||||
createErrorContext('Failed to match regex.'),
|
||||
);
|
||||
for (const match of matches) {
|
||||
const primitiveExpression = this.doOrRethrow(
|
||||
() => this.buildExpression(match),
|
||||
createErrorContext('Failed to build expression.'),
|
||||
);
|
||||
const position = this.doOrRethrow(
|
||||
() => this.utilities.createPosition(match),
|
||||
createErrorContext('Failed to create position.'),
|
||||
);
|
||||
const parameters = this.doOrRethrow(
|
||||
() => createParameters(
|
||||
primitiveExpression,
|
||||
this.utilities.createParameterCollection(),
|
||||
),
|
||||
createErrorContext('Failed to create parameters.'),
|
||||
);
|
||||
const expression = this.doOrRethrow(
|
||||
() => this.utilities.createExpression({
|
||||
position,
|
||||
evaluator: primitiveExpression.evaluator,
|
||||
parameters,
|
||||
}),
|
||||
createErrorContext('Failed to create expression.'),
|
||||
);
|
||||
yield expression;
|
||||
}
|
||||
}
|
||||
|
||||
private doOrRethrow<T>(
|
||||
action: () => T,
|
||||
context: ErrorContext,
|
||||
): T {
|
||||
try {
|
||||
return action();
|
||||
} catch (error) {
|
||||
throw this.utilities.wrapError(
|
||||
error,
|
||||
this.buildErrorMessageWithContext(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private buildErrorMessageWithContext(context: ErrorContext): string {
|
||||
return [
|
||||
context.errorMessage,
|
||||
`Class name: ${this.constructor.name}`,
|
||||
`Regex pattern used: ${this.regex}`,
|
||||
`Code: ${context.code}`,
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
interface ErrorContext {
|
||||
readonly errorMessage: string,
|
||||
readonly code: string,
|
||||
}
|
||||
|
||||
function createParameters(
|
||||
expression: PrimitiveExpression,
|
||||
parameterCollection: IFunctionParameterCollection,
|
||||
): IReadOnlyFunctionParameterCollection {
|
||||
return (expression.parameters || [])
|
||||
.reduce((parameters, parameter) => {
|
||||
parameters.addParameter(parameter);
|
||||
return parameters;
|
||||
}, parameterCollection);
|
||||
}
|
||||
|
||||
export interface PrimitiveExpression {
|
||||
readonly evaluator: ExpressionEvaluator;
|
||||
readonly parameters?: readonly FunctionParameter[];
|
||||
}
|
||||
|
||||
export interface ExpressionFactory {
|
||||
(
|
||||
...args: ConstructorParameters<typeof Expression>
|
||||
): IExpression;
|
||||
}
|
||||
|
||||
const DefaultRegexParserUtilities: RegexParserUtilities = {
|
||||
wrapError: wrapErrorWithAdditionalContext,
|
||||
createPosition: createPositionFromRegexFullMatch,
|
||||
createExpression: (...args) => new Expression(...args),
|
||||
createParameterCollection: createFunctionParameterCollection,
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||
import { validateParameterName, type ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator';
|
||||
|
||||
export interface FunctionCallArgument {
|
||||
readonly parameterName: string;
|
||||
readonly argumentValue: string;
|
||||
}
|
||||
|
||||
export interface FunctionCallArgumentFactory {
|
||||
(
|
||||
parameterName: string,
|
||||
argumentValue: string,
|
||||
utilities?: FunctionCallArgumentFactoryUtilities,
|
||||
): FunctionCallArgument;
|
||||
}
|
||||
|
||||
export const createFunctionCallArgument: FunctionCallArgumentFactory = (
|
||||
parameterName: string,
|
||||
argumentValue: string,
|
||||
utilities: FunctionCallArgumentFactoryUtilities = DefaultUtilities,
|
||||
): FunctionCallArgument => {
|
||||
utilities.validateParameterName(parameterName);
|
||||
utilities.typeValidator.assertNonEmptyString({
|
||||
value: argumentValue,
|
||||
valueName: `Missing argument value for the parameter "${parameterName}".`,
|
||||
});
|
||||
return {
|
||||
parameterName,
|
||||
argumentValue,
|
||||
};
|
||||
};
|
||||
|
||||
interface FunctionCallArgumentFactoryUtilities {
|
||||
readonly typeValidator: TypeValidator;
|
||||
readonly validateParameterName: ParameterNameValidator;
|
||||
}
|
||||
|
||||
const DefaultUtilities: FunctionCallArgumentFactoryUtilities = {
|
||||
typeValidator: createTypeValidator(),
|
||||
validateParameterName,
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
import type {
|
||||
FunctionCallData,
|
||||
FunctionCallsData,
|
||||
FunctionCallParametersData,
|
||||
} from '@/application/collections/';
|
||||
import { isArray, isPlainObject } from '@/TypeHelpers';
|
||||
import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
||||
import { ParsedFunctionCall } from './ParsedFunctionCall';
|
||||
import { createFunctionCallArgument, type FunctionCallArgumentFactory } from './Argument/FunctionCallArgument';
|
||||
import type { FunctionCall } from './FunctionCall';
|
||||
|
||||
export interface FunctionCallsParser {
|
||||
(
|
||||
calls: FunctionCallsData,
|
||||
utilities?: FunctionCallParsingUtilities,
|
||||
): FunctionCall[];
|
||||
}
|
||||
|
||||
interface FunctionCallParsingUtilities {
|
||||
readonly typeValidator: TypeValidator;
|
||||
readonly createCallArgument: FunctionCallArgumentFactory;
|
||||
}
|
||||
|
||||
const DefaultUtilities: FunctionCallParsingUtilities = {
|
||||
typeValidator: createTypeValidator(),
|
||||
createCallArgument: createFunctionCallArgument,
|
||||
};
|
||||
|
||||
export const parseFunctionCalls: FunctionCallsParser = (
|
||||
calls,
|
||||
utilities = DefaultUtilities,
|
||||
) => {
|
||||
const sequence = getCallSequence(calls, utilities.typeValidator);
|
||||
return sequence.map((call) => parseFunctionCall(call, utilities));
|
||||
};
|
||||
|
||||
function getCallSequence(calls: FunctionCallsData, validator: TypeValidator): FunctionCallData[] {
|
||||
if (!isPlainObject(calls) && !isArray(calls)) {
|
||||
throw new Error('called function(s) must be an object or array');
|
||||
}
|
||||
if (isArray(calls)) {
|
||||
validator.assertNonEmptyCollection({
|
||||
value: calls,
|
||||
valueName: 'function call sequence',
|
||||
});
|
||||
return calls as FunctionCallData[];
|
||||
}
|
||||
const singleCall = calls as FunctionCallData;
|
||||
return [singleCall];
|
||||
}
|
||||
|
||||
function parseFunctionCall(
|
||||
call: FunctionCallData,
|
||||
utilities: FunctionCallParsingUtilities,
|
||||
): FunctionCall {
|
||||
utilities.typeValidator.assertObject({
|
||||
value: call,
|
||||
valueName: 'function call',
|
||||
allowedProperties: ['function', 'parameters'],
|
||||
});
|
||||
const callArgs = parseArgs(call.parameters, utilities.createCallArgument);
|
||||
return new ParsedFunctionCall(call.function, callArgs);
|
||||
}
|
||||
|
||||
function parseArgs(
|
||||
parameters: FunctionCallParametersData | undefined,
|
||||
createArgument: FunctionCallArgumentFactory,
|
||||
): FunctionCallArgumentCollection {
|
||||
const parametersMap = parameters ?? {};
|
||||
return Object.keys(parametersMap)
|
||||
.map((parameterName) => {
|
||||
const argumentValue = parametersMap[parameterName];
|
||||
return createArgument(parameterName, argumentValue);
|
||||
})
|
||||
.reduce((args, arg) => {
|
||||
args.addArgument(arg);
|
||||
return args;
|
||||
}, new FunctionCallArgumentCollection());
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { FunctionParameterCollection } from './FunctionParameterCollection';
|
||||
import type { IFunctionParameterCollection } from './IFunctionParameterCollection';
|
||||
|
||||
export interface FunctionParameterCollectionFactory {
|
||||
(
|
||||
...args: ConstructorParameters<typeof FunctionParameterCollection>
|
||||
): IFunctionParameterCollection;
|
||||
}
|
||||
|
||||
export const createFunctionParameterCollection: FunctionParameterCollectionFactory = (...args) => {
|
||||
return new FunctionParameterCollection(...args);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { ParameterDefinitionData } from '@/application/collections/';
|
||||
import { validateParameterName, type ParameterNameValidator } from '../Shared/ParameterNameValidator';
|
||||
import type { FunctionParameter } from './FunctionParameter';
|
||||
|
||||
export interface FunctionParameterParser {
|
||||
(
|
||||
data: ParameterDefinitionData,
|
||||
validator?: ParameterNameValidator,
|
||||
): FunctionParameter;
|
||||
}
|
||||
|
||||
export const parseFunctionParameter: FunctionParameterParser = (
|
||||
data,
|
||||
validator = validateParameterName,
|
||||
) => {
|
||||
validator(data.name);
|
||||
return {
|
||||
name: data.name,
|
||||
isOptional: data.optional || false,
|
||||
};
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { FunctionParameter } from './FunctionParameter';
|
||||
|
||||
export interface IReadOnlyFunctionParameterCollection {
|
||||
readonly all: readonly FunctionParameter[];
|
||||
}
|
||||
|
||||
export interface IFunctionParameterCollection extends IReadOnlyFunctionParameterCollection {
|
||||
addParameter(parameter: FunctionParameter): void;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||
|
||||
export interface ParameterNameValidator {
|
||||
(
|
||||
parameterName: string,
|
||||
typeValidator?: TypeValidator,
|
||||
): void;
|
||||
}
|
||||
|
||||
export const validateParameterName = (
|
||||
parameterName: string,
|
||||
typeValidator = createTypeValidator(),
|
||||
) => {
|
||||
typeValidator.assertNonEmptyString({
|
||||
value: parameterName,
|
||||
valueName: 'parameter name',
|
||||
rule: {
|
||||
expectedMatch: /^[0-9a-zA-Z]+$/,
|
||||
errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
||||
import { parseFunctionCalls } from './Function/Call/FunctionCallsParser';
|
||||
import { parseSharedFunctions, type SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||
import type { CompiledCode } from './Function/Call/Compiler/CompiledCode';
|
||||
import type { IScriptCompiler } from './IScriptCompiler';
|
||||
import type { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||
import type { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';
|
||||
|
||||
interface ScriptCompilerUtilities {
|
||||
readonly sharedFunctionsParser: SharedFunctionsParser;
|
||||
readonly callCompiler: FunctionCallCompiler;
|
||||
readonly codeValidator: ICodeValidator;
|
||||
readonly wrapError: ErrorWithContextWrapper;
|
||||
readonly scriptCodeFactory: ScriptCodeFactory;
|
||||
}
|
||||
|
||||
const DefaultUtilities: ScriptCompilerUtilities = {
|
||||
sharedFunctionsParser: parseSharedFunctions,
|
||||
callCompiler: FunctionCallSequenceCompiler.instance,
|
||||
codeValidator: CodeValidator.instance,
|
||||
wrapError: wrapErrorWithAdditionalContext,
|
||||
scriptCodeFactory: createScriptCode,
|
||||
};
|
||||
|
||||
interface CategoryCollectionDataContext {
|
||||
readonly functions: readonly FunctionData[];
|
||||
readonly syntax: ILanguageSyntax;
|
||||
}
|
||||
|
||||
export class ScriptCompiler implements IScriptCompiler {
|
||||
private readonly functions: ISharedFunctionCollection;
|
||||
|
||||
constructor(
|
||||
categoryContext: CategoryCollectionDataContext,
|
||||
private readonly utilities: ScriptCompilerUtilities = DefaultUtilities,
|
||||
) {
|
||||
this.functions = this.utilities.sharedFunctionsParser(
|
||||
categoryContext.functions,
|
||||
categoryContext.syntax,
|
||||
);
|
||||
}
|
||||
|
||||
public canCompile(script: ScriptData): boolean {
|
||||
return hasCall(script);
|
||||
}
|
||||
|
||||
public compile(script: ScriptData): ScriptCode {
|
||||
try {
|
||||
if (!hasCall(script)) {
|
||||
throw new Error('Script does include any calls.');
|
||||
}
|
||||
const calls = parseFunctionCalls(script.call);
|
||||
const compiledCode = this.utilities.callCompiler.compileFunctionCalls(calls, this.functions);
|
||||
validateCompiledCode(compiledCode, this.utilities.codeValidator);
|
||||
return this.utilities.scriptCodeFactory(
|
||||
compiledCode.code,
|
||||
compiledCode.revertCode,
|
||||
);
|
||||
} catch (error) {
|
||||
throw this.utilities.wrapError(error, `Failed to compile script: ${script.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
||||
[compiledCode.code, compiledCode.revertCode]
|
||||
.filter((code): code is string => Boolean(code))
|
||||
.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;
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
|
||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||
import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
|
||||
import { createScript, type ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
|
||||
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
||||
import { ExecutableType } from '../Validation/ExecutableType';
|
||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
|
||||
import { CodeValidator } from './Validation/CodeValidator';
|
||||
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||
import type { CategoryCollectionSpecificUtilities } from '../CategoryCollectionSpecificUtilities';
|
||||
|
||||
export interface ScriptParser {
|
||||
(
|
||||
data: ScriptData,
|
||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||
scriptUtilities?: ScriptParserUtilities,
|
||||
): Script;
|
||||
}
|
||||
|
||||
export const parseScript: ScriptParser = (
|
||||
data,
|
||||
collectionUtilities,
|
||||
scriptUtilities = DefaultUtilities,
|
||||
) => {
|
||||
const validator = scriptUtilities.createValidator({
|
||||
type: ExecutableType.Script,
|
||||
self: data,
|
||||
});
|
||||
validateScript(data, validator);
|
||||
try {
|
||||
const script = scriptUtilities.createScript({
|
||||
executableId: data.name, // arbitrary ID
|
||||
name: data.name,
|
||||
code: parseCode(
|
||||
data,
|
||||
collectionUtilities,
|
||||
scriptUtilities.codeValidator,
|
||||
scriptUtilities.createCode,
|
||||
),
|
||||
docs: scriptUtilities.parseDocs(data),
|
||||
level: parseLevel(data.recommend, scriptUtilities.levelParser),
|
||||
});
|
||||
return script;
|
||||
} catch (error) {
|
||||
throw scriptUtilities.wrapError(
|
||||
error,
|
||||
validator.createContextualErrorMessage('Failed to parse script.'),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function parseLevel(
|
||||
level: string | undefined,
|
||||
parser: EnumParser<RecommendationLevel>,
|
||||
): RecommendationLevel | undefined {
|
||||
if (!level) {
|
||||
return undefined;
|
||||
}
|
||||
return parser.parseEnum(level, 'level');
|
||||
}
|
||||
|
||||
function parseCode(
|
||||
script: ScriptData,
|
||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||
codeValidator: ICodeValidator,
|
||||
createCode: ScriptCodeFactory,
|
||||
): ScriptCode {
|
||||
if (collectionUtilities.compiler.canCompile(script)) {
|
||||
return collectionUtilities.compiler.compile(script);
|
||||
}
|
||||
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
||||
const code = createCode(codeScript.code, codeScript.revertCode);
|
||||
validateHardcodedCodeWithoutCalls(code, codeValidator, collectionUtilities.syntax);
|
||||
return code;
|
||||
}
|
||||
|
||||
function validateHardcodedCodeWithoutCalls(
|
||||
scriptCode: ScriptCode,
|
||||
validator: ICodeValidator,
|
||||
syntax: ILanguageSyntax,
|
||||
) {
|
||||
[scriptCode.execute, scriptCode.revert]
|
||||
.filter((code): code is string => Boolean(code))
|
||||
.forEach(
|
||||
(code) => validator.throwIfInvalid(
|
||||
code,
|
||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function validateScript(
|
||||
script: ScriptData,
|
||||
validator: ExecutableValidator,
|
||||
): asserts script is NonNullable<ScriptData> {
|
||||
validator.assertType((v) => v.assertObject<CallScriptData & CodeScriptData>({
|
||||
value: script,
|
||||
valueName: script.name ?? 'script',
|
||||
allowedProperties: [
|
||||
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
||||
],
|
||||
}));
|
||||
validator.assertValidName(script.name);
|
||||
validator.assert(
|
||||
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
||||
'Neither "call" or "code" is defined.',
|
||||
);
|
||||
validator.assert(
|
||||
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
|
||||
'Both "call" and "code" are defined.',
|
||||
);
|
||||
validator.assert(
|
||||
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
|
||||
'Both "call" and "revertCode" are defined.',
|
||||
);
|
||||
}
|
||||
|
||||
interface ScriptParserUtilities {
|
||||
readonly levelParser: EnumParser<RecommendationLevel>;
|
||||
readonly createScript: ScriptFactory;
|
||||
readonly codeValidator: ICodeValidator;
|
||||
readonly wrapError: ErrorWithContextWrapper;
|
||||
readonly createValidator: ExecutableValidatorFactory;
|
||||
readonly createCode: ScriptCodeFactory;
|
||||
readonly parseDocs: DocsParser;
|
||||
}
|
||||
|
||||
const DefaultUtilities: ScriptParserUtilities = {
|
||||
levelParser: createEnumParser(RecommendationLevel),
|
||||
createScript,
|
||||
codeValidator: CodeValidator.instance,
|
||||
wrapError: wrapErrorWithAdditionalContext,
|
||||
createValidator: createExecutableDataValidator,
|
||||
createCode: createScriptCode,
|
||||
parseDocs,
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { CategoryData, ScriptData, ExecutableData } from '@/application/collections/';
|
||||
import { ExecutableType } from './ExecutableType';
|
||||
|
||||
export type ExecutableErrorContext = {
|
||||
readonly parentCategory?: CategoryData;
|
||||
} & (CategoryErrorContext | ScriptErrorContext | UnknownExecutableErrorContext);
|
||||
|
||||
export type CategoryErrorContext = {
|
||||
readonly type: ExecutableType.Category;
|
||||
readonly self: CategoryData;
|
||||
readonly parentCategory?: CategoryData;
|
||||
};
|
||||
|
||||
export type ScriptErrorContext = {
|
||||
readonly type: ExecutableType.Script;
|
||||
readonly self: ScriptData;
|
||||
readonly parentCategory?: CategoryData;
|
||||
};
|
||||
|
||||
export type UnknownExecutableErrorContext = {
|
||||
readonly type?: undefined;
|
||||
readonly self: ExecutableData;
|
||||
readonly parentCategory?: CategoryData;
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { ExecutableData } from '@/application/collections/';
|
||||
import { ExecutableType } from './ExecutableType';
|
||||
import type { ExecutableErrorContext } from './ExecutableErrorContext';
|
||||
|
||||
export interface ExecutableContextErrorMessageCreator {
|
||||
(
|
||||
errorMessage: string,
|
||||
context: ExecutableErrorContext,
|
||||
): string;
|
||||
}
|
||||
|
||||
export const createExecutableContextErrorMessage: ExecutableContextErrorMessageCreator = (
|
||||
errorMessage,
|
||||
context,
|
||||
) => {
|
||||
let message = '';
|
||||
if (context.type !== undefined) {
|
||||
message += `${ExecutableType[context.type]}: `;
|
||||
}
|
||||
message += errorMessage;
|
||||
message += `\n\n${getErrorContextDetails(context)}`;
|
||||
return message;
|
||||
};
|
||||
|
||||
function getErrorContextDetails(context: ExecutableErrorContext): string {
|
||||
let output = `Executable: ${formatExecutable(context.self)}`;
|
||||
if (context.parentCategory) {
|
||||
output += `\n\nParent category: ${formatExecutable(context.parentCategory)}`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function formatExecutable(executable: ExecutableData): string {
|
||||
if (!executable) {
|
||||
return 'Executable data is missing.';
|
||||
}
|
||||
const maxLength = 1000;
|
||||
let output = JSON.stringify(executable, undefined, 2);
|
||||
if (output.length > maxLength) {
|
||||
output = `${output.substring(0, maxLength)}\n... [Rest of the executable trimmed]`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum ExecutableType {
|
||||
Script,
|
||||
Category,
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { isString } from '@/TypeHelpers';
|
||||
import { createTypeValidator, type TypeValidator } from '../../Common/TypeValidator';
|
||||
import { type ExecutableErrorContext } from './ExecutableErrorContext';
|
||||
import { createExecutableContextErrorMessage, type ExecutableContextErrorMessageCreator } from './ExecutableErrorContextMessage';
|
||||
|
||||
export interface ExecutableValidatorFactory {
|
||||
(context: ExecutableErrorContext): ExecutableValidator;
|
||||
}
|
||||
|
||||
type AssertTypeFunction = (validator: TypeValidator) => void;
|
||||
|
||||
export interface ExecutableValidator {
|
||||
assertValidName(nameValue: string): void;
|
||||
assertType(assert: AssertTypeFunction): void;
|
||||
assert(
|
||||
validationPredicate: () => boolean,
|
||||
errorMessage: string,
|
||||
): asserts validationPredicate is (() => true);
|
||||
createContextualErrorMessage(errorMessage: string): string;
|
||||
}
|
||||
|
||||
export const createExecutableDataValidator
|
||||
: ExecutableValidatorFactory = (context) => new ContextualExecutableValidator(context);
|
||||
|
||||
export class ContextualExecutableValidator implements ExecutableValidator {
|
||||
constructor(
|
||||
private readonly context: ExecutableErrorContext,
|
||||
private readonly createErrorMessage
|
||||
: ExecutableContextErrorMessageCreator = createExecutableContextErrorMessage,
|
||||
private readonly validator: TypeValidator = createTypeValidator(),
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public assertValidName(nameValue: string): void {
|
||||
this.assert(() => Boolean(nameValue), 'missing name');
|
||||
this.assert(
|
||||
() => isString(nameValue),
|
||||
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
||||
);
|
||||
}
|
||||
|
||||
public assertType(assert: AssertTypeFunction): void {
|
||||
try {
|
||||
assert(this.validator);
|
||||
} catch (error) {
|
||||
this.throw(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
public assert(
|
||||
validationPredicate: () => boolean,
|
||||
errorMessage: string,
|
||||
): asserts validationPredicate is (() => true) {
|
||||
if (!validationPredicate()) {
|
||||
this.throw(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public createContextualErrorMessage(errorMessage: string): string {
|
||||
return this.createErrorMessage(errorMessage, this.context);
|
||||
}
|
||||
|
||||
private throw(errorMessage: string): never {
|
||||
throw new Error(
|
||||
this.createContextualErrorMessage(errorMessage),
|
||||
);
|
||||
}
|
||||
}
|
||||
3
src/application/Parser/NodeValidation/NodeData.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { ScriptData, CategoryData } from '@/application/collections/';
|
||||
|
||||
export type NodeData = CategoryData | ScriptData;
|
||||