Compare commits
177 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9e0001ef8 | ||
|
|
62f8bfac2f | ||
|
|
75c9b51bf2 | ||
|
|
ec98d8417f | ||
|
|
736590558b | ||
|
|
6e40edd3f8 | ||
|
|
5f11c8d98f | ||
|
|
08737698c2 | ||
|
|
04b3133500 | ||
|
|
0d15992d56 | ||
|
|
a14929a13c | ||
|
|
6a20d804dc | ||
|
|
ae75059cc1 | ||
|
|
39e650cf11 | ||
|
|
bc91237d7c | ||
|
|
9e5491fdbf | ||
|
|
986ba078a6 | ||
|
|
061afad967 | ||
|
|
3bc8da4cbf | ||
|
|
1b9be8fe2d | ||
|
|
3a594ac7fd | ||
|
|
ff84f5676e | ||
|
|
4d0ce12c96 | ||
|
|
298b058e5c | ||
|
|
1e80ee1fb0 | ||
|
|
5901dc5f11 | ||
|
|
5721796378 | ||
|
|
8b374a37b4 | ||
|
|
c404dfebe2 | ||
|
|
e8199932b4 | ||
|
|
f4a7ca76b8 | ||
|
|
64cca1d9b8 | ||
|
|
68a5d698a2 | ||
|
|
bf0c55fa60 | ||
|
|
e7b816d156 | ||
|
|
a2e092190d | ||
|
|
c1c2f2925f | ||
|
|
e8d06e0f3e | ||
|
|
7d3670c26d | ||
|
|
430537f704 | ||
|
|
58ed7b456b | ||
|
|
6b3f4659df | ||
|
|
bbc6156281 | ||
|
|
df533ad3b1 | ||
|
|
6067bdb24e | ||
|
|
924b326244 | ||
|
|
8608072bfb | ||
|
|
3233d9b802 | ||
|
|
99e24b4134 | ||
|
|
b210aaddf2 | ||
|
|
65902e5b72 | ||
|
|
efd63ff85d | ||
|
|
242a497e7d | ||
|
|
05a6a84c37 | ||
|
|
112e79a64c | ||
|
|
eeb1d5b0c4 | ||
|
|
d6bc33ec86 | ||
|
|
956052c8ff | ||
|
|
3785e410db | ||
|
|
481a02afd5 | ||
|
|
5bbbb9cecc | ||
|
|
db47440d47 | ||
|
|
1bcc6c8b2b | ||
|
|
3c3ec80525 | ||
|
|
803ef2bb3e | ||
|
|
43ce834750 | ||
|
|
44d79e2c9a | ||
|
|
0e52a99efa | ||
|
|
834ce8cf9e | ||
|
|
2354f0ba9f | ||
|
|
8e96c19126 | ||
|
|
99fb4c73f5 | ||
|
|
d11a674a3c | ||
|
|
31f70913a2 | ||
|
|
bd23faa28f | ||
|
|
5b1fbe1e2f | ||
|
|
96265b75de | ||
|
|
17298f0b2c | ||
|
|
61b475fa8d | ||
|
|
455084c17b | ||
|
|
c3c5b897f3 | ||
|
|
a1871a2982 | ||
|
|
87de017afd | ||
|
|
5a2c263af3 | ||
|
|
ddd2e704db | ||
|
|
9b5e0b0591 | ||
|
|
9b6636e21a | ||
|
|
a8358b8e7a | ||
|
|
5f091bb6ab | ||
|
|
17b334aaad | ||
|
|
c65209e6a9 | ||
|
|
d2518b11a7 | ||
|
|
70cdf3865a | ||
|
|
7c02ffb6c9 | ||
|
|
f2d9881382 | ||
|
|
d7761ab30e | ||
|
|
bf83c58982 | ||
|
|
2e082932c9 | ||
|
|
2f90cac52a | ||
|
|
20a0071c0d | ||
|
|
a40f83d6b6 | ||
|
|
0db8cc4206 | ||
|
|
97ddc027cb | ||
|
|
82c43ba2e3 | ||
|
|
799fb091b8 | ||
|
|
5ead1a087d | ||
|
|
64631a4552 | ||
|
|
f47cb04860 | ||
|
|
504fa056d7 | ||
|
|
739287ac71 | ||
|
|
ab8bce7686 | ||
|
|
e6152fa76f | ||
|
|
a8031d18d5 | ||
|
|
9aa8166891 | ||
|
|
236a0f6c82 | ||
|
|
2492f2d814 | ||
|
|
410bcd8244 | ||
|
|
b08a6b5cec | ||
|
|
37ad26a082 | ||
|
|
0696ed8396 | ||
|
|
9942df16c8 | ||
|
|
20b7d283b0 | ||
|
|
f39ee76c0c | ||
|
|
4b2390736a | ||
|
|
c8cb7a5c28 | ||
|
|
5217b0b758 | ||
|
|
ddf417a16a | ||
|
|
2f0321f315 | ||
|
|
4d7ff7edc5 | ||
|
|
862914b06e | ||
|
|
6c3c2e6709 | ||
|
|
c92dc1e253 | ||
|
|
e73c0ad1bf | ||
|
|
6a89c6224b | ||
|
|
dcccb61781 | ||
|
|
c0c475ff56 | ||
|
|
6dc768817f | ||
|
|
439cd303ff | ||
|
|
ec0c972d34 | ||
|
|
2a08855e5d | ||
|
|
1c6b3057ea | ||
|
|
ea5f9ec27d | ||
|
|
f2935e4008 | ||
|
|
487001af48 | ||
|
|
71e70e50c5 | ||
|
|
0a857aa09e | ||
|
|
b976b92031 | ||
|
|
db62ed7f3a | ||
|
|
36f0805590 | ||
|
|
49600c5f37 | ||
|
|
eb9ac35a92 | ||
|
|
77148980e0 | ||
|
|
b3d2e82025 | ||
|
|
b25b8cc805 | ||
|
|
8141a01ef7 | ||
|
|
a2f10857e2 | ||
|
|
aea04e5f7c | ||
|
|
60c80611ea | ||
|
|
b1ed3ce55f | ||
|
|
040ed2701c | ||
|
|
00d8e551db | ||
|
|
3e9c99f5f8 | ||
|
|
02bdc4cf04 | ||
|
|
5c43965f0b | ||
|
|
b2376ecc30 | ||
|
|
aeaa6deeb4 | ||
|
|
448e378dc4 | ||
|
|
ac2249f256 | ||
|
|
05932c5a36 | ||
|
|
6f46cdb4ed | ||
|
|
5f527a00cf | ||
|
|
1935db1019 | ||
|
|
1f515e7be5 | ||
|
|
1a5f92021f | ||
|
|
f3c7413f52 | ||
|
|
646db90585 | ||
|
|
1f8a0cf9ab |
@@ -1,2 +1,3 @@
|
|||||||
> 1%
|
> 1%
|
||||||
last 2 versions
|
last 2 versions
|
||||||
|
not dead
|
||||||
|
|||||||
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[*.{js,jsx,ts,tsx,vue,sh}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 100
|
||||||
2
.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
dist/
|
||||||
|
dist_electron/
|
||||||
95
.eslintrc.cjs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
|
||||||
|
const tsconfigJson = require('./tsconfig.json');
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
es2022: true, // add globals and sets parserOptions.ecmaVersion to 2022
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
// Vue specific rules, eslint-plugin-vue
|
||||||
|
'plugin:vue/essential',
|
||||||
|
|
||||||
|
// Extends eslint-config-airbnb
|
||||||
|
'@vue/eslint-config-airbnb-with-typescript',
|
||||||
|
|
||||||
|
// Extends @typescript-eslint/recommended
|
||||||
|
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||||
|
'@vue/typescript/recommended',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
...getOwnRules(),
|
||||||
|
...getTurnedOffBrokenRules(),
|
||||||
|
...getOpinionatedRuleOverrides(),
|
||||||
|
...getTodoRules(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getOwnRules() {
|
||||||
|
return {
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'linebreak-style': ['error', 'unix'], // This is also enforced in .editorconfig and .gitattributes files
|
||||||
|
'import/order': [ // Enforce strict import order taking account into aliases
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
groups: [ // Enforce more strict order than AirBnb
|
||||||
|
'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||||
|
pathGroups: [ // Fix manually configured paths being incorrectly grouped as "external"
|
||||||
|
...getAliasesFromTsConfig(),
|
||||||
|
'js-yaml-loader!@/**',
|
||||||
|
].map((pattern) => ({ pattern, group: 'internal' })),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTodoRules() { // Should be worked on separate future commits
|
||||||
|
return {
|
||||||
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
// Accessibility improvements:
|
||||||
|
'vuejs-accessibility/form-control-has-label': 'off',
|
||||||
|
'vuejs-accessibility/click-events-have-key-events': 'off',
|
||||||
|
'vuejs-accessibility/anchor-has-content': 'off',
|
||||||
|
'vuejs-accessibility/accessible-emoji': 'off',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTurnedOffBrokenRules() {
|
||||||
|
return {
|
||||||
|
// Broken in TypeScript
|
||||||
|
'no-useless-constructor': 'off', // Cannot interpret TypeScript constructors
|
||||||
|
'no-shadow': 'off', // Fails with TypeScript enums
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpinionatedRuleOverrides() {
|
||||||
|
return {
|
||||||
|
// https://erkinekici.com/articles/linting-trap#no-use-before-define
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
'@typescript-eslint/no-use-before-define': 'off',
|
||||||
|
// https://erkinekici.com/articles/linting-trap#arrow-body-style
|
||||||
|
'arrow-body-style': 'off',
|
||||||
|
// https://erkinekici.com/articles/linting-trap#no-plusplus
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
// https://erkinekici.com/articles/linting-trap#no-param-reassign
|
||||||
|
'no-param-reassign': 'off',
|
||||||
|
// https://erkinekici.com/articles/linting-trap#class-methods-use-this
|
||||||
|
'class-methods-use-this': 'off',
|
||||||
|
// https://erkinekici.com/articles/linting-trap#importprefer-default-export
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
// https://erkinekici.com/articles/linting-trap#disallowing-for-of
|
||||||
|
// Original: https://github.com/airbnb/javascript/blob/d8cb404da74c302506f91e5928f30cc75109e74d/packages/eslint-config-airbnb-base/rules/style.js#L333-L351
|
||||||
|
'no-restricted-syntax': [
|
||||||
|
baseStyleRules['no-restricted-syntax'][0],
|
||||||
|
...baseStyleRules['no-restricted-syntax'].slice(1).filter((rule) => rule.selector !== 'ForOfStatement'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAliasesFromTsConfig() {
|
||||||
|
return Object.keys(tsconfigJson.compilerOptions.paths)
|
||||||
|
.map((path) => `${path}*`);
|
||||||
|
}
|
||||||
6
.gitattributes
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Prevent Git from auto-converting to CRLF on Windows, and convert to LF on checkin.
|
||||||
|
# * : All files
|
||||||
|
# text=auto : If Git decides content it text, it converts CRLF to LF on checkin.
|
||||||
|
# eol=lf : forces Git to normalize line endings to LF on checkin and prevents conversion
|
||||||
|
# to CRLF when the file is checked out.
|
||||||
|
* text=auto eol=lf
|
||||||
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: undergroundwires
|
||||||
41
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
@@ -11,26 +11,47 @@ 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.
|
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.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Describe the bug
|
### Description
|
||||||
|
|
||||||
<!-- A clear and concise description of what the bug is. -->
|
<!--
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
-->
|
||||||
|
|
||||||
### OS
|
### OS
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Which OS are you using? What version of OS you were using?
|
Which OS are you using? What version of OS you were using?
|
||||||
On Windows you can find it using "Start button" > "Settings" > "System" > "About".
|
On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||||
On macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
|
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
|
### Screenshots
|
||||||
|
|
||||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
<!--
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
### Scripts
|
-->
|
||||||
|
|
||||||
<!-- Which scripts did you execute? If applicable, please paste the executed scripts or attach the generated privacy.sexy file . -->
|
|
||||||
|
|
||||||
### Additional information
|
### Additional information
|
||||||
|
|
||||||
<!-- Add any other context about the problem here. -->
|
<!--
|
||||||
|
If applicable, add any other context about the problem here.
|
||||||
|
-->
|
||||||
|
|||||||
19
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
@@ -11,13 +11,16 @@ 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.
|
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.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Describe the bug
|
### Description
|
||||||
|
|
||||||
<!-- A clear and concise description of what the bug is. -->
|
|
||||||
|
|
||||||
### To Reproduce
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
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:
|
Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
@@ -41,12 +44,12 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
If applicable, mention how you were using privacy.sexy when the bug was encountered:
|
If applicable, mention how you were using privacy.sexy when the bug was encountered:
|
||||||
- Web (on Desktop or mobile?)
|
- Web (on Desktop or mobile?)
|
||||||
- Or desktop (Windows, macOS or Linux?)
|
- Or desktop (Windows, macOS or Linux?)
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Additional context
|
### Additional context
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Add any other context about the problem here.
|
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.
|
||||||
|
-->
|
||||||
27
.github/ISSUE_TEMPLATE/3-feature_request.md
vendored
@@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for privacy.sexy
|
|
||||||
labels: enhancement
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for suggesting an idea to make privacy better. 🤗
|
|
||||||
|
|
||||||
Please fill in as much of the template below as you're able.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Problem Description
|
|
||||||
|
|
||||||
<!-- Please add a clear and concise description of the problem you are seeking to solve with this feature request. Ex. 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
|
|
||||||
|
|
||||||
<!-- Add any other context or screenshots about the feature request here. -->
|
|
||||||
75
.github/ISSUE_TEMPLATE/4-new-script-suggestion.md
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
name: New script suggestion
|
||||||
|
about: Suggest a new script for privacy.sexy
|
||||||
|
labels: enhancement
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Thank you for suggesting an script to make privacy better. 🤗
|
||||||
|
Please fill in as much of the template below as you're able.
|
||||||
|
You could alternatively send a PR directly (see CONTRIBUTING.md).
|
||||||
|
-->
|
||||||
|
|
||||||
|
### OS
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Which OS will the new script configure?
|
||||||
|
One of the supported OSes: "Windows", "macOS" or "Linux".
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Name
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The name of the script.
|
||||||
|
It should start with an imperative noun such as "disable", "turn off" , "clear"...
|
||||||
|
E.g. "Disable webcam telemetry"
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Script code
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Code that will be executed when script is selected.
|
||||||
|
Try to keep it as simple and backwards-compatible as possible.
|
||||||
|
Allowed languages:
|
||||||
|
- Windows: PowerShell (ps1) or batchfile
|
||||||
|
- 💡 Prioritize the one that's simpler, batchfile if similar.
|
||||||
|
- macOS: bash (sh)
|
||||||
|
- Linux: bash (sh) or Python 3
|
||||||
|
- 💡 Prioritize the one that's simpler, bash if similar.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Revert code
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, add code that will revert the script code to its original (OS default) state.
|
||||||
|
It may require additional time, but it's much appreciated by the community.
|
||||||
|
Leave blank if the script is nonreversible (e.g. when clearing data without backup).
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Suggested category
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, suggest one more multiple suitable parent category of script.
|
||||||
|
A category is the item where the script will be presented under.
|
||||||
|
Most likely there already is a category for the script, so check the existing categories.
|
||||||
|
If you're unsure, leave blank and maintainer(s) will choose one.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Suggested recommendation level
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, suggest recommending the script or not recommending at all.
|
||||||
|
A script should be only recommended if it'll be safe for your grandmother to run.
|
||||||
|
So you have three options here:
|
||||||
|
STANDARD: Non-breaking scripts that does not limit any functionality.
|
||||||
|
STRICT: Scripts that can break certain functionality but not intrusive to common daily OS usage.
|
||||||
|
NONE: Script is not recommended for newbies at all, only those who knows what's going on should select it.
|
||||||
|
If you're unsure, leave blank and maintainer(s) will choose one.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Additional documentation/references
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, refer to documentation that should show up on the script description.
|
||||||
|
Sources (URLs) should be as high quality as possible e.g. vendor documentation is favored over user forums.
|
||||||
|
-->
|
||||||
8
.github/actions/setup-node/action.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 16.x
|
||||||
81
.github/workflows/checks.build.yaml
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
name: build-checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-web:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
mode: [
|
||||||
|
# Vite mode: https://vitejs.dev/guide/env-and-mode.html
|
||||||
|
development, # Used by `dev` command
|
||||||
|
production, # Used by `build` command
|
||||||
|
# Vitest mode: https://vitest.dev/guide/cli.html
|
||||||
|
test, # Used by Vitest
|
||||||
|
]
|
||||||
|
fail-fast: false # Allows to see results from other combinations
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Build
|
||||||
|
run: npm run build -- --mode ${{ matrix.mode }}
|
||||||
|
|
||||||
|
build-desktop:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
mode: [
|
||||||
|
# electron-vite modes: https://electron-vite.org/guide/env-and-mode.html#global-env-variables
|
||||||
|
development, # Used by `dev` command
|
||||||
|
production, # Used by `build` and `preview` commands
|
||||||
|
]
|
||||||
|
fail-fast: false # Allows to see results from other combinations
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Prebuild
|
||||||
|
run: npm run electron:prebuild -- --mode ${{ matrix.mode }}
|
||||||
|
-
|
||||||
|
name: Build
|
||||||
|
run: npm run electron:build -- --publish never
|
||||||
|
|
||||||
|
create-icons:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Allows to see results from other combinations
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Create icons
|
||||||
|
run: npm run icons:build
|
||||||
67
.github/workflows/checks.desktop-runtime-errors.yaml
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
name: checks.desktop-runtime-errors
|
||||||
|
# Verifies desktop builds for Electron applications across multiple OS platforms (macOS ,Ubuntu, and Windows).
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-desktop:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Allows to see results from other combinations
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Configure Ubuntu
|
||||||
|
if: matrix.os == 'ubuntu'
|
||||||
|
shell: bash
|
||||||
|
run: |-
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
|
# Configure AppImage dependencies
|
||||||
|
sudo apt install -y libfuse2
|
||||||
|
|
||||||
|
# Configure DBUS (fixes `Failed to connect to the bus: Could not parse server address: Unknown address type`)
|
||||||
|
if ! command -v 'dbus-launch' &> /dev/null; then
|
||||||
|
echo 'DBUS does not exist, installing...'
|
||||||
|
sudo apt install -y dbus-x11 # Gives both dbus and dbus-launch utility
|
||||||
|
fi
|
||||||
|
sudo systemctl start dbus
|
||||||
|
DBUS_LAUNCH_OUTPUT=$(dbus-launch)
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "${DBUS_LAUNCH_OUTPUT}" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo 'Error: dbus-launch command did not execute successfully. Exiting.' >&2
|
||||||
|
echo "${DBUS_LAUNCH_OUTPUT}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure fake (virtual) display
|
||||||
|
sudo apt install -y xvfb
|
||||||
|
sudo Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||||
|
echo "DISPLAY=:99" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Install ImageMagick for screenshots
|
||||||
|
sudo apt install -y imagemagick
|
||||||
|
|
||||||
|
# Install xdotool and xprop (from x11-utils) for window title capturing
|
||||||
|
sudo apt install -y xdotool x11-utils
|
||||||
|
-
|
||||||
|
name: Test
|
||||||
|
shell: bash
|
||||||
|
run: node ./scripts/check-desktop-runtime-errors --screenshot
|
||||||
|
-
|
||||||
|
name: Upload screenshot
|
||||||
|
if: always() # Run even if previous step fails
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: screenshot-${{ matrix.os }}
|
||||||
|
path: screenshot.png
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Quality checks
|
name: quality-checks
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
@@ -8,18 +8,18 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
lint-command:
|
lint-command:
|
||||||
- npm run lint:vue
|
- npm run lint:eslint
|
||||||
- npm run lint:yaml
|
- npm run lint:yaml
|
||||||
- npm run lint:md
|
- npm run lint:md
|
||||||
- npm run lint:md:relative-urls
|
- npm run lint:md:relative-urls
|
||||||
- npm run lint:md:consistency
|
- npm run lint:md:consistency
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: 14.x
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Lint
|
- name: Lint
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
name: Security checks
|
name: security-checks
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths: [ '/package.json', '/package-lock.json' ] # Allow PRs to be green if they do not introduce dependency change
|
paths: [ '/package.json', '/package-lock.json' ] # Allow PRs to be green if they do not introduce dependency change
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * 0'
|
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
npm-audit:
|
npm-audit:
|
||||||
@@ -16,9 +16,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: 14.x
|
|
||||||
-
|
-
|
||||||
name: NPM audit
|
name: NPM audit
|
||||||
run: npm audit
|
run: npm audit --omit=dev
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy desktop
|
name: release-desktop
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@@ -13,22 +13,29 @@ jobs:
|
|||||||
fail-fast: false # So publish runs for other OSes if one fails
|
fail-fast: false # So publish runs for other OSes if one fails
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
-
|
||||||
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: master # otherwise it defaults to the version tag missing bump commit
|
ref: master # otherwise it defaults to the version tag missing bump commit
|
||||||
fetch-depth: 0 # fetch all history
|
fetch-depth: 0 # fetch all history
|
||||||
- name: Checkout to bump commit
|
-
|
||||||
|
name: Checkout to bump commit
|
||||||
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
||||||
- name: Setup node
|
-
|
||||||
uses: actions/setup-node@v1
|
name: Setup node
|
||||||
with:
|
uses: ./.github/actions/setup-node
|
||||||
node-version: '14.x'
|
-
|
||||||
- name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Run tests
|
-
|
||||||
|
name: Run unit tests
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
- name: Publish desktop app
|
-
|
||||||
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
name: Prebuild
|
||||||
|
run: npm run electron:prebuild
|
||||||
|
-
|
||||||
|
name: Build and publish
|
||||||
|
run: npm run electron:build -- --publish always
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074
|
EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Bump & release
|
name: release-git
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push: # Ensure a new release is created for each new tag
|
push: # Ensure a new release is created for each new tag
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
name: Deploy site
|
name: release-site
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [created] # will be triggered when a NON-draft release is created and published.
|
types: [created] # will be triggered when a NON-draft release is created and published.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
|
aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
|
||||||
@@ -77,30 +77,28 @@ jobs:
|
|||||||
name: "App: Checkout"
|
name: "App: Checkout"
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
path: site
|
path: app
|
||||||
ref: master # otherwise we don't get version bump commit
|
ref: master # otherwise we don't get version bump commit
|
||||||
-
|
-
|
||||||
name: "App: Setup node"
|
name: "App: Setup node"
|
||||||
uses: actions/setup-node@v1
|
uses: ./app/.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: '14.x'
|
|
||||||
-
|
-
|
||||||
name: "App: Install dependencies"
|
name: "App: Install dependencies"
|
||||||
run: npm ci
|
run: npm ci
|
||||||
working-directory: site
|
working-directory: app
|
||||||
-
|
-
|
||||||
name: "App: Run tests"
|
name: "App: Run unit tests"
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
working-directory: site
|
working-directory: app
|
||||||
-
|
-
|
||||||
name: "App: Build"
|
name: "App: Build"
|
||||||
run: npm run build
|
run: npm run build
|
||||||
working-directory: site
|
working-directory: app
|
||||||
-
|
-
|
||||||
name: "App: Deploy to S3"
|
name: "App: Deploy to S3"
|
||||||
run: >-
|
run: >-
|
||||||
bash "aws/scripts/deploy/deploy-to-s3.sh" \
|
bash "aws/scripts/deploy/deploy-to-s3.sh" \
|
||||||
--folder site/dist \
|
--folder app/dist \
|
||||||
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
|
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
|
||||||
--storage-class ONEZONE_IA \
|
--storage-class ONEZONE_IA \
|
||||||
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \
|
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \
|
||||||
26
.github/workflows/tests.e2e.yaml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: e2e-tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos, ubuntu, windows]
|
||||||
|
fail-fast: false # So it still runs on other OSes if one of them fails
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Run e2e tests
|
||||||
|
run: npm run test:cy:run
|
||||||
28
.github/workflows/tests.integration.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: integration-tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule: # To get notified about problems from third party dependencies
|
||||||
|
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos, ubuntu, windows]
|
||||||
|
fail-fast: false # So it still runs on other OSes if one of them fails
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
-
|
||||||
|
name: Run integration tests
|
||||||
|
run: npm run test:integration
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
name: Test
|
name: unit-tests
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-tests:
|
run-tests:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos, ubuntu, windows]
|
os: [macos, ubuntu, windows]
|
||||||
|
fail-fast: false # So it still runs on other OSes if one of them fails
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Set-up node
|
||||||
uses: actions/setup-node@v1
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
|
||||||
node-version: '14.x'
|
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
-
|
-
|
||||||
name: Run tests
|
name: Run unit tests
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
3
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist/
|
dist/
|
||||||
.vs
|
.vs
|
||||||
.vscode
|
.vscode/**/*
|
||||||
|
!.vscode/extensions.json
|
||||||
#Electron-builder output
|
#Electron-builder output
|
||||||
/dist_electron
|
/dist_electron
|
||||||
23
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
// Common
|
||||||
|
"editorconfig.editorconfig", // Applies .editorconfig to follow project style.
|
||||||
|
"wengerk.highlight-bad-chars", // Highlights bad chars.
|
||||||
|
"wayou.vscode-todo-highlight", // Highlights TODO.
|
||||||
|
"wix.vscode-import-cost", // Shows in KB how much a require include in code.
|
||||||
|
// Documentation
|
||||||
|
"davidanson.vscode-markdownlint", // Lints markdown.
|
||||||
|
// TypeScript / JavaScript
|
||||||
|
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
|
||||||
|
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.
|
||||||
|
// Vue
|
||||||
|
"Vue.volar", // Official Vue extensions
|
||||||
|
"Vue.vscode-typescript-vue-plugin", // Official TypeScript Vue Plugin
|
||||||
|
// Scripting
|
||||||
|
"timonwong.shellcheck", // Lints bash files.
|
||||||
|
"ms-vscode.powershell", // Lints PowerShell files.
|
||||||
|
"ms-python.python", // Lints Python files.
|
||||||
|
// Distribution
|
||||||
|
"ms-azuretools.vscode-docker" // Adds Docker support.
|
||||||
|
]
|
||||||
|
}
|
||||||
222
CHANGELOG.md
@@ -1,5 +1,227 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.12.1 (2023-08-17)
|
||||||
|
|
||||||
|
* Transition to eslint-config-airbnb-with-typescript | [ff84f56](https://github.com/undergroundwires/privacy.sexy/commit/ff84f5676e496dd7ec5b3599e34ec9627d181ea2)
|
||||||
|
* Improve user privacy with secure outbound links | [3a594ac](https://github.com/undergroundwires/privacy.sexy/commit/3a594ac7fd708dc1e98155ffb9b21acd4e1fcf2d)
|
||||||
|
* Refactor Vue components using Composition API #230 | [1b9be8f](https://github.com/undergroundwires/privacy.sexy/commit/1b9be8fe2d72d8fb5cf1fed6dcc0b9777171aa98)
|
||||||
|
* Fix failing security tests | [3bc8da4](https://github.com/undergroundwires/privacy.sexy/commit/3bc8da4cbf1e2bd758dc3fffe4b1e62dc3beb7b3)
|
||||||
|
* Improve Defender scripts #201 | [061afad](https://github.com/undergroundwires/privacy.sexy/commit/061afad9673a41454c2421c318898f2b4f4cf504)
|
||||||
|
* Fix failing tests due to failed error logging | [986ba07](https://github.com/undergroundwires/privacy.sexy/commit/986ba078a643de6acbee50fff9cf77494ca7ea7f)
|
||||||
|
* Implement custom lightweight modal #230 | [9e5491f](https://github.com/undergroundwires/privacy.sexy/commit/9e5491fdbf2d9d40d974f5ad0e879a6d5c6d1e55)
|
||||||
|
* Refactor usage of tooltips for flexibility | [bc91237](https://github.com/undergroundwires/privacy.sexy/commit/bc91237d7c54bdcd15c5c39a55def50d172bb659)
|
||||||
|
* Fix revert toggle partial rendering | [39e650c](https://github.com/undergroundwires/privacy.sexy/commit/39e650cf110bee6b1b21d9b2902b36b0e2568d54)
|
||||||
|
* Increase testability through dependency injection | [ae75059](https://github.com/undergroundwires/privacy.sexy/commit/ae75059cc14db41f55dd2056f528442c7d319dd2)
|
||||||
|
* Refactor filter (search query) event handling | [6a20d80](https://github.com/undergroundwires/privacy.sexy/commit/6a20d804dc365d22c1248d787f9912271f508eeb)
|
||||||
|
* Migrate to ES6 modules | [a14929a](https://github.com/undergroundwires/privacy.sexy/commit/a14929a13cc6260b514692d9b4f1cdf5fb85d8b2)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.0...0.12.1)
|
||||||
|
|
||||||
|
## 0.12.0 (2023-08-03)
|
||||||
|
|
||||||
|
* Improve script/category name validation | [b210aad](https://github.com/undergroundwires/privacy.sexy/commit/b210aaddf26629179f77fe19f62f65d8a0ca2b87)
|
||||||
|
* Improve touch like hover on devices without mouse | [99e24b4](https://github.com/undergroundwires/privacy.sexy/commit/99e24b4134c461c336f6d08f49d193d853325d31)
|
||||||
|
* Improve click/touch without unintended interaction | [3233d9b](https://github.com/undergroundwires/privacy.sexy/commit/3233d9b8024dd59600edddef6d017e0089f59a9d)
|
||||||
|
* Align card icons vertically in cards view | [8608072](https://github.com/undergroundwires/privacy.sexy/commit/8608072bfb52d10a843a86d3d89b14e8b9776779)
|
||||||
|
* Fix broken npm installation and builds | [924b326](https://github.com/undergroundwires/privacy.sexy/commit/924b326244a175428175e0df3a50685ee5ac2ec6)
|
||||||
|
* Improve documentation support with markdown | [6067bdb](https://github.com/undergroundwires/privacy.sexy/commit/6067bdb24e6729d2249c9685f4f1c514c3167d91)
|
||||||
|
* win: add more Visual Studio scripts, support 2022 | [df533ad](https://github.com/undergroundwires/privacy.sexy/commit/df533ad3b19cebdf3454895aa2182bd4184e0360)
|
||||||
|
* win: add script to remove Widgets | [bbc6156](https://github.com/undergroundwires/privacy.sexy/commit/bbc6156281fb3fd4b66c63dec3f765780fafa855)
|
||||||
|
* Use line endings based on script language #88 | [6b3f465](https://github.com/undergroundwires/privacy.sexy/commit/6b3f4659df0afe1c99a8af6598df44a33c1f863a)
|
||||||
|
* win: improve OneDrive removal | [58ed7b4](https://github.com/undergroundwires/privacy.sexy/commit/58ed7b456b3cf11774c83c8c1c04db37ef3058c2)
|
||||||
|
* Use lowercase in script names and search text | [430537f](https://github.com/undergroundwires/privacy.sexy/commit/430537f70411756bbcaae837964c0223f78581e8)
|
||||||
|
* Improve manual execution instructions | [7d3670c](https://github.com/undergroundwires/privacy.sexy/commit/7d3670c26d0151ddc43303e8ed5e47715f0e0f00)
|
||||||
|
* Add multiline support for with expression | [e8d06e0](https://github.com/undergroundwires/privacy.sexy/commit/e8d06e0f3e178a69861e0197f9d1cce9af3958f1)
|
||||||
|
* Break line in inline codes in documentation | [c1c2f29](https://github.com/undergroundwires/privacy.sexy/commit/c1c2f2925fe88ec1f56bf7655b6b9a10aa3ea024)
|
||||||
|
* win: add script to increase RSA key exchange #165 | [a2e0921](https://github.com/undergroundwires/privacy.sexy/commit/a2e092190d8eb0fc9ceb8533572f04fff52f097b)
|
||||||
|
* win: add scripts to downloaded file handling #153 | [e7b816d](https://github.com/undergroundwires/privacy.sexy/commit/e7b816d1564afa98c63291f9d7fd6f3fee92f4ec)
|
||||||
|
* Drop support for dead browsers | [bf0c55f](https://github.com/undergroundwires/privacy.sexy/commit/bf0c55fa60bf2be070678ba27db14baf13fec511)
|
||||||
|
* Add support for nested templates | [68a5d69](https://github.com/undergroundwires/privacy.sexy/commit/68a5d698a2ce644ce25754016fb9e9bb642e41a7)
|
||||||
|
* mac: add scripts to configure Parallels Desktop | [64cca1d](https://github.com/undergroundwires/privacy.sexy/commit/64cca1d9b8946b92e21e86deb6db5612570befb1)
|
||||||
|
* Rework icon with higher quality and new color | [f4a7ca7](https://github.com/undergroundwires/privacy.sexy/commit/f4a7ca76b885b8346d8a9c32e6269eabc2d8139f)
|
||||||
|
* Relax and improve code validation | [e819993](https://github.com/undergroundwires/privacy.sexy/commit/e8199932b462380741d9f2d8b6b55485ab16af02)
|
||||||
|
* Add initial Linux support #150 | [c404dfe](https://github.com/undergroundwires/privacy.sexy/commit/c404dfebe2908bb165279f8279f3f5e805b647d7)
|
||||||
|
* mac: add script to disable personalized ads | [8b374a3](https://github.com/undergroundwires/privacy.sexy/commit/8b374a37b401699d5056bfd6b735b6a26c395ae0)
|
||||||
|
* Update dependencies and add npm setup script | [5721796](https://github.com/undergroundwires/privacy.sexy/commit/57217963787a8ab0c71d681c6b1673c484c88226)
|
||||||
|
* Fix macOS desktop build failure in CI | [5901dc5](https://github.com/undergroundwires/privacy.sexy/commit/5901dc5f11dd29be14c2616fc0ceb45196a43224)
|
||||||
|
* Change subtitle heading to new slogan | [1e80ee1](https://github.com/undergroundwires/privacy.sexy/commit/1e80ee1fb0208d92943619468dc427853cbe8de7)
|
||||||
|
* win: add new scripts to disable more telemetry | [298b058](https://github.com/undergroundwires/privacy.sexy/commit/298b058e5c89397db6f759b275442ba05499ac8c)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.4...0.12.0)
|
||||||
|
|
||||||
|
## 0.11.4 (2022-03-08)
|
||||||
|
|
||||||
|
* Improve performance of selecting scripts | [8e96c19](https://github.com/undergroundwires/privacy.sexy/commit/8e96c19126aa4cba6418de5ccaa9e2dcf8faab78)
|
||||||
|
* Fix reverting of Windows NVIDIA telemetry service | [2354f0b](https://github.com/undergroundwires/privacy.sexy/commit/2354f0ba9fed3aa23569b5ea6391a7119fe1ab53)
|
||||||
|
* Add AirBnb TypeScript overrides for linting | [834ce8c](https://github.com/undergroundwires/privacy.sexy/commit/834ce8cf9e8e46934dfa604526360870d109765b)
|
||||||
|
* Transpile dependencies for wider browser support | [0e52a99](https://github.com/undergroundwires/privacy.sexy/commit/0e52a99efa2b02d1aba10885a76e03aa6f9be7f8)
|
||||||
|
* Add more and unify tests for absent object cases | [44d79e2](https://github.com/undergroundwires/privacy.sexy/commit/44d79e2c9a97639bbd188a8fdfd740f1a5a1d6ee)
|
||||||
|
* Fix Windows DoSvc not being disabled #115 | [43ce834](https://github.com/undergroundwires/privacy.sexy/commit/43ce834750ddf471636d1ece4324d02357947f9f)
|
||||||
|
* Move stubs from `./stubs` to `./shared/Stubs` | [803ef2b](https://github.com/undergroundwires/privacy.sexy/commit/803ef2bb3eea68306377e40e326c791402998650)
|
||||||
|
* Improve documentation for developing | [3c3ec80](https://github.com/undergroundwires/privacy.sexy/commit/3c3ec80525b97e8a24db4c44bbf42a7b4e089056)
|
||||||
|
* Improve documentation for architecture | [1bcc6c8](https://github.com/undergroundwires/privacy.sexy/commit/1bcc6c8b2b923b4d4b1662f990d86b190ce73342)
|
||||||
|
* Improve existing documentation | [db47440](https://github.com/undergroundwires/privacy.sexy/commit/db47440d470ea6a6e100b620b10d078c01314992)
|
||||||
|
* Refactor to remove code coupling with Webpack | [5bbbb9c](https://github.com/undergroundwires/privacy.sexy/commit/5bbbb9cecca0a3828036e7fc34dcd66970ce334a)
|
||||||
|
* Refactor to remove hardcoding of aliases | [481a02a](https://github.com/undergroundwires/privacy.sexy/commit/481a02afd5190eb77a37fa450e50816b2268e99c)
|
||||||
|
* Document WpnService breaking on Windows 10 #110 | [3785e41](https://github.com/undergroundwires/privacy.sexy/commit/3785e410db461f667a834e0b388d81e4baa028e4)
|
||||||
|
* Fix error when reverting Windows Defender setting | [956052c](https://github.com/undergroundwires/privacy.sexy/commit/956052c8fff042812fe84fe4d7fa5c579365ff9b)
|
||||||
|
* Fix Windows 11 being detected as Windows 10 | [d6bc33e](https://github.com/undergroundwires/privacy.sexy/commit/d6bc33ec865d50efc6b8d4ccc2f789edd874fcee)
|
||||||
|
* Refactor to use version object #59 | [eeb1d5b](https://github.com/undergroundwires/privacy.sexy/commit/eeb1d5b0c40a55675921af3f67f366b2ff658acf)
|
||||||
|
* Fix Microsoft Defender alert for uninstaller #114 | [112e79a](https://github.com/undergroundwires/privacy.sexy/commit/112e79a64c6153f4ce3b48c27a09639e7647aebc)
|
||||||
|
* Add donation information | [05a6a84](https://github.com/undergroundwires/privacy.sexy/commit/05a6a84c3739ec900343591ac1f7a9f310cd73f2)
|
||||||
|
* Bump node environment to 16.x | [242a497](https://github.com/undergroundwires/privacy.sexy/commit/242a497e7debb351da19b20b63a3554f0cca4b5c)
|
||||||
|
* Bump dependencies to latest | [efd63ff](https://github.com/undergroundwires/privacy.sexy/commit/efd63ff85dea4c9a9c033c54bc1be378742de351)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.3...0.11.4)
|
||||||
|
|
||||||
|
## 0.11.3 (2022-01-05)
|
||||||
|
|
||||||
|
* Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd)
|
||||||
|
* Fix OS desktop detection tests and edge cases | [a8358b8](https://github.com/undergroundwires/privacy.sexy/commit/a8358b8e7a93214f3d22a4488007ded5f623d845)
|
||||||
|
* Fix clearing Windows product key showing dialog | [9b6636e](https://github.com/undergroundwires/privacy.sexy/commit/9b6636e21a922a4750dc19f4854f8ae679187926)
|
||||||
|
* Document and unrecommend Cloud Experience Host | [9b5e0b0](https://github.com/undergroundwires/privacy.sexy/commit/9b5e0b0591fee56af52d83334a1f19180a49516f)
|
||||||
|
* Add initial e2e testing with cypress | [ddd2e70](https://github.com/undergroundwires/privacy.sexy/commit/ddd2e704dbd361cbd219f3dfe644b983ad254095)
|
||||||
|
* Restructure pipelines and badges | [5a2c263](https://github.com/undergroundwires/privacy.sexy/commit/5a2c263af35b8785e75ead6c43c3f17186dc15c8)
|
||||||
|
* Fix failing of functions without revert code | [87de017](https://github.com/undergroundwires/privacy.sexy/commit/87de017afd6e08acbd2deea150c6af9c7ee778fc)
|
||||||
|
* Fix typos in privacy modal #109 | [a1871a2](https://github.com/undergroundwires/privacy.sexy/commit/a1871a2982c9e3192193f836b97b1a6ccda5a2ab)
|
||||||
|
* Refactor to add readonly interfaces | [c3c5b89](https://github.com/undergroundwires/privacy.sexy/commit/c3c5b897f308f613c252182a02cdd4cfa7150fa3)
|
||||||
|
* Document and unrecommend AAD app removal #24, #54 | [455084c](https://github.com/undergroundwires/privacy.sexy/commit/455084c17b32d11d046515e8dc1447adf4bea4c3)
|
||||||
|
* Migrate from TSLint to ESLint | [61b475f](https://github.com/undergroundwires/privacy.sexy/commit/61b475fa8de433cdada2efa7eac197683aacd956)
|
||||||
|
* Add build checks and improve existing CI/CD checks | [17298f0](https://github.com/undergroundwires/privacy.sexy/commit/17298f0b2c51cb9becc0eb2ffe0d93d6a4c503a6)
|
||||||
|
* Upgrade to Vue CLI 5 (and webpack 5) | [96265b7](https://github.com/undergroundwires/privacy.sexy/commit/96265b75deafb85978b16460138fb4a814c07cfe)
|
||||||
|
* Refactor code to comply with ESLint rules | [5b1fbe1](https://github.com/undergroundwires/privacy.sexy/commit/5b1fbe1e2fb1354a5f060f8c8e3794ce756e16a7)
|
||||||
|
* Fix mutated line endings on Windows | [bd23faa](https://github.com/undergroundwires/privacy.sexy/commit/bd23faa28f6d781581a33d5b780f4b33f7e2cd8b)
|
||||||
|
* Refactor to improve iterations | [31f7091](https://github.com/undergroundwires/privacy.sexy/commit/31f70913a2f30baf5a9d6690f192e6a63da50114)
|
||||||
|
* win: unrecommend and document Live ID service #100 | [d11a674](https://github.com/undergroundwires/privacy.sexy/commit/d11a674a3c4ad8f4972a870c2f0977ac53297273)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.2...0.11.3)
|
||||||
|
|
||||||
|
## 0.11.2 (2021-12-03)
|
||||||
|
|
||||||
|
* Fix Windows TrustedInstaller session errors | [20a0071](https://github.com/undergroundwires/privacy.sexy/commit/20a0071c0d3d769a8f31218abdbfc4cafa25c6ff)
|
||||||
|
* Improve tests for `UserSelection` | [2f90cac](https://github.com/undergroundwires/privacy.sexy/commit/2f90cac52ab9e57615aeec41f9daa842bce770a5)
|
||||||
|
* Fix disabling/enabling Defender on Windows #104 | [2e08293](https://github.com/undergroundwires/privacy.sexy/commit/2e082932c952b0849ab2b8709ff0c75293b88e95)
|
||||||
|
* Refactor Saas naming, structure and modules | [bf83c58](https://github.com/undergroundwires/privacy.sexy/commit/bf83c58982ffa178facc6d35e50c7f1eac7ff236)
|
||||||
|
* Fix Defender features errors in Windows #104 | [d7761ab](https://github.com/undergroundwires/privacy.sexy/commit/d7761ab30e7f1e10a2919c196804d67511d6163a)
|
||||||
|
* Fix unintendedly inlined Windows scripts | [f2d9881](https://github.com/undergroundwires/privacy.sexy/commit/f2d988138257ff184884e4adc83c39e3bc247e9b)
|
||||||
|
* Fix Defender error due to non-english Windows #104 | [7c02ffb](https://github.com/undergroundwires/privacy.sexy/commit/7c02ffb6c95382b94f0b05e6f259cc418ec91c93)
|
||||||
|
* Improve and unify disabling of Windows services | [70cdf38](https://github.com/undergroundwires/privacy.sexy/commit/70cdf3865a0de3214fc9e26fbdada4b0cb413c46)
|
||||||
|
* Improve Windows defender docs and errors #104 | [d2518b1](https://github.com/undergroundwires/privacy.sexy/commit/d2518b11a7774ec58b9b46a691e2f013855bf0f9)
|
||||||
|
* Unrecommend and complete Windows Push Notif. #101 | [c65209e](https://github.com/undergroundwires/privacy.sexy/commit/c65209e6a99230f15ace8955e8d5a6f3333d146b)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.1...0.11.2)
|
||||||
|
|
||||||
|
## 0.11.1 (2021-11-04)
|
||||||
|
|
||||||
|
* Update dependencies | [64631a4](https://github.com/undergroundwires/privacy.sexy/commit/64631a4552fad7f7b06286aba8d3ca2d731f9342)
|
||||||
|
* Fix, document, unrecommend Windows browser cleanup | [5ead1a0](https://github.com/undergroundwires/privacy.sexy/commit/5ead1a087d91948890bc4ae6fea176123f18c285)
|
||||||
|
* Fix failing URL status checking integration tests | [799fb09](https://github.com/undergroundwires/privacy.sexy/commit/799fb091b8eb06c70ac0c67f2ef5385dce73501f)
|
||||||
|
* Refactor to remove "Async" function name suffix | [82c43ba](https://github.com/undergroundwires/privacy.sexy/commit/82c43ba2e37fb6e7f62ccd9bec8c5f48575f0613)
|
||||||
|
* Fix dead URLs and use forks as GitHub references | [97ddc02](https://github.com/undergroundwires/privacy.sexy/commit/97ddc027cb5395a74991cabc1d8c875ee945636d)
|
||||||
|
* Fix website not loading on Safari | [0db8cc4](https://github.com/undergroundwires/privacy.sexy/commit/0db8cc420655e01cbbed57c4658489b761a15899)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.0...0.11.1)
|
||||||
|
|
||||||
|
## 0.11.0 (2021-10-21)
|
||||||
|
|
||||||
|
* Change "grouping" to "view" | [c0c475f](https://github.com/undergroundwires/privacy.sexy/commit/c0c475ff564b23a4dabcc03ac2909207a8eb61ce)
|
||||||
|
* Tighten parameter substitution tolerance | [dcccb61](https://github.com/undergroundwires/privacy.sexy/commit/dcccb617813625c224a28242c5b965bb4cd6f189)
|
||||||
|
* Add optionality for parameters | [6a89c62](https://github.com/undergroundwires/privacy.sexy/commit/6a89c6224bdef5eb96980471f3b3935b9351b197)
|
||||||
|
* Do not collapse cards on links and code area #88 | [e73c0ad](https://github.com/undergroundwires/privacy.sexy/commit/e73c0ad1bf922b1dd3360fc5aafc3434951fa63c)
|
||||||
|
* Add scripts to disable, hide and opt-out from Siri | [c92dc1e](https://github.com/undergroundwires/privacy.sexy/commit/c92dc1e25387c65a3a41ca64d2a23cf8131b4c86)
|
||||||
|
* Improve macOS scripts for cleaning OS logs | [6c3c2e6](https://github.com/undergroundwires/privacy.sexy/commit/6c3c2e6709ec84f8e0411f19c024bab2c7e5753b)
|
||||||
|
* Add "with" expression for templating #53 | [862914b](https://github.com/undergroundwires/privacy.sexy/commit/862914b06ea9ef74c4b58a9a4164a10a38273638)
|
||||||
|
* Add support for pipes in templates #53 | [4d7ff7e](https://github.com/undergroundwires/privacy.sexy/commit/4d7ff7edc5a96cc0d99d3c1ca4fdf9bbdace3fd2)
|
||||||
|
* Bump node environment to 15.x | [2f0321f](https://github.com/undergroundwires/privacy.sexy/commit/2f0321f315ac0da8c713dd50e37032f1de194942)
|
||||||
|
* Add new UX for optionally downloading updates | [ddf417a](https://github.com/undergroundwires/privacy.sexy/commit/ddf417a16a79551b43576befab0541ea08487969)
|
||||||
|
* Add pipes to write pretty PowerShell #53 | [5217b0b](https://github.com/undergroundwires/privacy.sexy/commit/5217b0b7587ccfe509ba8adc3a7748b9bae14d7a)
|
||||||
|
* Improve alignment, padding/margin issues on UI | [c8cb7a5](https://github.com/undergroundwires/privacy.sexy/commit/c8cb7a5c28420557319606da82f56b011e88f470)
|
||||||
|
* Support disabling per-user services in Windows #16 | [4b23907](https://github.com/undergroundwires/privacy.sexy/commit/4b2390736ac1f9de2d5176b7b07da0e827112f9a)
|
||||||
|
* Add script to remove Meet Now icon in Windows | [f39ee76](https://github.com/undergroundwires/privacy.sexy/commit/f39ee76c0cda95f54502b19d5c49390fd0f12b5e)
|
||||||
|
* Add support for more depth in function calls | [20b7d28](https://github.com/undergroundwires/privacy.sexy/commit/20b7d283b02dd751dfbde18ef1fe334c6bf76e2b)
|
||||||
|
* Increase default screen width on desktop app | [9942df1](https://github.com/undergroundwires/privacy.sexy/commit/9942df16c8334ff041fb92f432a3a29e351c88df)
|
||||||
|
* Improve disabling of SmartScreen #74 | [0696ed8](https://github.com/undergroundwires/privacy.sexy/commit/0696ed8396e298a358bec17adb91c9145dd90418)
|
||||||
|
* Remove integration tests from deployments #90 | [37ad26a](https://github.com/undergroundwires/privacy.sexy/commit/37ad26a082851c02497c36e7fce40555b9480e11)
|
||||||
|
* Use a consistent color system | [b08a6b5](https://github.com/undergroundwires/privacy.sexy/commit/b08a6b5cecf4a53023053695292146edbd24b960)
|
||||||
|
* Add semi-automatic update support for macOS | [410bcd8](https://github.com/undergroundwires/privacy.sexy/commit/410bcd82445097c29c9fcf0eabf7af9ebcb93c1e)
|
||||||
|
* Add more ways to disable and clean Defender #74 | [2492f2d](https://github.com/undergroundwires/privacy.sexy/commit/2492f2d8141b3abdf590ccad59680b1f50ecb59e)
|
||||||
|
* Add privacy over security scripts for macOS #83 | [236a0f6](https://github.com/undergroundwires/privacy.sexy/commit/236a0f6c8241294fc397194cd1b20bdeccbbb50b)
|
||||||
|
* Change PowerShell double quotes escape | [9aa8166](https://github.com/undergroundwires/privacy.sexy/commit/9aa816689146ee6cd86d8262112677c38651c6bd)
|
||||||
|
* Change theme colors | [a8031d1](https://github.com/undergroundwires/privacy.sexy/commit/a8031d18d520dd3b0567f7b8cfe2dcd694b65073)
|
||||||
|
* Improve security hardening for macOS | [e6152fa](https://github.com/undergroundwires/privacy.sexy/commit/e6152fa76f5e7d23b0f79d5dd98713daaecbff90)
|
||||||
|
* Support disabling of protected services #74 | [ab8bce7](https://github.com/undergroundwires/privacy.sexy/commit/ab8bce768650a10677f0a13b3a9fae93c83802ff)
|
||||||
|
* Fix minor issues with Defender scripts | [739287a](https://github.com/undergroundwires/privacy.sexy/commit/739287ac71b3f8b04348fc101f1fa06f2d7d86a2)
|
||||||
|
* Update screenshot | [504fa05](https://github.com/undergroundwires/privacy.sexy/commit/504fa056d7d8b17fc20afd398f9a557495fca7e8)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.3...0.11.0)
|
||||||
|
|
||||||
|
## 0.10.3 (2021-08-27)
|
||||||
|
|
||||||
|
* unrecommend VSS and document its breaking behavior | [7714898](https://github.com/undergroundwires/privacy.sexy/commit/77148980e08859f89c15c6604e55b56ce4f74358)
|
||||||
|
* fix incorrect modification of Desktop folder on ThisPC (#71) | [eb9ac35](https://github.com/undergroundwires/privacy.sexy/commit/eb9ac35a923325cc2c9983ef71c0d904337a58f5)
|
||||||
|
* add initial integration tests | [49600c5](https://github.com/undergroundwires/privacy.sexy/commit/49600c5f37ca33c1687885fdf02a71ef7d3e6e8c)
|
||||||
|
* unify usage of sleepAsync and add tests | [36f0805](https://github.com/undergroundwires/privacy.sexy/commit/36f08055909f371fd9cbe3480ea813b963aea22b)
|
||||||
|
* fix broken URLs and automate broken URL checks #70 | [db62ed7](https://github.com/undergroundwires/privacy.sexy/commit/db62ed7f3ac63e9f2d762eb946060595eb9f5626)
|
||||||
|
* fix hiding recent files in quick access | [b976b92](https://github.com/undergroundwires/privacy.sexy/commit/b976b920318dba55b32d39f148fdca4f6be3cce3)
|
||||||
|
* bump dependencies to latest #75, #69 | [0a857aa](https://github.com/undergroundwires/privacy.sexy/commit/0a857aa09ee703d34ad0422bd1731158017a9a58)
|
||||||
|
* Fix NTP configuration before running the service (#72) | [71e70e5](https://github.com/undergroundwires/privacy.sexy/commit/71e70e50c51249bb10f6203414948b325acc2b2a)
|
||||||
|
* Fix typo on main page (#82) | [487001a](https://github.com/undergroundwires/privacy.sexy/commit/487001af485fdbb958615d7b52c09c2e386ddaf2)
|
||||||
|
* Improve issue templates | [f2935e4](https://github.com/undergroundwires/privacy.sexy/commit/f2935e4008f1231ef174f8932290e11715564d20)
|
||||||
|
* Fix infinitely subscribing to state changes | [ea5f9ec](https://github.com/undergroundwires/privacy.sexy/commit/ea5f9ec27df7cec6ac575e23fef18948d2b8e68a)
|
||||||
|
* Fix select options being clickable when disabled | [1c6b305](https://github.com/undergroundwires/privacy.sexy/commit/1c6b3057ea6e45125cadf374f20a905712ccdf3c)
|
||||||
|
* Fix tests for `ParameterSubstitutionParser` | [2a08855](https://github.com/undergroundwires/privacy.sexy/commit/2a08855e5d1bdf74354fd692cbfebd1a48e495ac)
|
||||||
|
* Fix excessive highlighting on hover | [ec0c972](https://github.com/undergroundwires/privacy.sexy/commit/ec0c972d348ffd5897f115d201031b704875b56a)
|
||||||
|
* Fix dead URLs | [439cd30](https://github.com/undergroundwires/privacy.sexy/commit/439cd303ff3db96a53664e5f44fefe12b95c5e6c)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.2...0.10.3)
|
||||||
|
|
||||||
|
## 0.10.2 (2021-04-19)
|
||||||
|
|
||||||
|
* in CI/CD, run other tests/check even if one of them fails | [5c43965](https://github.com/undergroundwires/privacy.sexy/commit/5c43965f0bc44f991ada7d3bad68937a80665dc3)
|
||||||
|
* fix desktop initial window size being bigger than current display size on smaller Linux/Windows screens | [02bdc4c](https://github.com/undergroundwires/privacy.sexy/commit/02bdc4cf0426c452f3fc9af52b819ca9b0757290)
|
||||||
|
* refactor extra code, duplicates, complexity | [00d8e55](https://github.com/undergroundwires/privacy.sexy/commit/00d8e551db001247fadfb6f6af7a4c5ce19a9e64)
|
||||||
|
* improve disabling ads and marketing #65 | [040ed27](https://github.com/undergroundwires/privacy.sexy/commit/040ed2701c4a468749901f4c5369b221bc0973c4)
|
||||||
|
* document breaking behavior in script name #64 | [b1ed3ce](https://github.com/undergroundwires/privacy.sexy/commit/b1ed3ce55f2d003cad1ead23e674aa66d4eb5802)
|
||||||
|
* add module alias '@tests/' | [60c8061](https://github.com/undergroundwires/privacy.sexy/commit/60c80611eab227791fabb883caf93418cef5fd00)
|
||||||
|
* document chromium warning for policy changes | [aea04e5](https://github.com/undergroundwires/privacy.sexy/commit/aea04e5f7cd48fbb9b407b68ade75575a6064c82)
|
||||||
|
* fix script revert activating recommendation level | [a2f1085](https://github.com/undergroundwires/privacy.sexy/commit/a2f10857e2a8debb3ce01f79b0dfbe8649ea9a17)
|
||||||
|
* fix typo and dead URL in Windows scripts (#70) | [8141a01](https://github.com/undergroundwires/privacy.sexy/commit/8141a01ef798331b4d82f5ca95f7b18df4f6f912)
|
||||||
|
* fix vue warning for undefined property during render | [b25b8cc](https://github.com/undergroundwires/privacy.sexy/commit/b25b8cc8052655af70b0695c6c3085974d783bb6)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.1...0.10.2)
|
||||||
|
|
||||||
|
## 0.10.1 (2021-03-25)
|
||||||
|
|
||||||
|
* refactor script compilation to make it easy to add new expressions #41 #53 | [646db90](https://github.com/undergroundwires/privacy.sexy/commit/646db9058541cebd0af437554de04fdc6bb63a6e)
|
||||||
|
* restructure presentation layer | [f3c7413](https://github.com/undergroundwires/privacy.sexy/commit/f3c7413f529be4a00dba7b0ab23904b48ea13a35)
|
||||||
|
* fix a test where "it" is not used inside "describe" | [1a5f920](https://github.com/undergroundwires/privacy.sexy/commit/1a5f92021f7423cd039f8f5326cd6f99b355c962)
|
||||||
|
* bump dependencies to latest | [1f515e7](https://github.com/undergroundwires/privacy.sexy/commit/1f515e7be525291c960ccb71db05312db6da53f5)
|
||||||
|
* fix throttle function not being able to run with argument(s) | [1935db1](https://github.com/undergroundwires/privacy.sexy/commit/1935db10192051401ab00ca2cd767955d0d3b866)
|
||||||
|
* fix fs module hanging not allowing code to run | [5f527a0](https://github.com/undergroundwires/privacy.sexy/commit/5f527a00cf225d3e74b3f6577d6e2456e919de24)
|
||||||
|
* refactor all modals to use same dialog component | [6f46cdb](https://github.com/undergroundwires/privacy.sexy/commit/6f46cdb4ed49a8941c6c0dde5c5e2a816c06daef)
|
||||||
|
* fix safari cleanup scripts that are not working on modern versions | [05932c5](https://github.com/undergroundwires/privacy.sexy/commit/05932c5a36446d551c5bc811165e3295fbe15e3f)
|
||||||
|
* refactor features to use shared functions #41 | [ac2249f](https://github.com/undergroundwires/privacy.sexy/commit/ac2249f25664827d8a6d2c7ebd659ccf126b0cde)
|
||||||
|
* increase performance by polyfilling ResizeObserver only if required | [448e378](https://github.com/undergroundwires/privacy.sexy/commit/448e378dc4501f9de69af63634c87d0e5060bf52)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.0...0.10.1)
|
||||||
|
|
||||||
|
## 0.10.0 (2021-03-02)
|
||||||
|
|
||||||
|
* allow functions to call other functions #53 | [7661575](https://github.com/undergroundwires/privacy.sexy/commit/7661575573c6d3e8f4bc28bfa7a124a764c72ef9)
|
||||||
|
* add option to run script directly in desktop app | [9a6b903](https://github.com/undergroundwires/privacy.sexy/commit/9a6b903b9297802845043fd41115756acd4a145c)
|
||||||
|
* add script to automatically kill devicecensus process | [c9b91f6](https://github.com/undergroundwires/privacy.sexy/commit/c9b91f6d8f9bd16308b6beda119e7154a985b6cf)
|
||||||
|
* refactor disabling application experience and document better | [45a3669](https://github.com/undergroundwires/privacy.sexy/commit/45a3669443d82855a52f60524d341c15f380f9e7)
|
||||||
|
* escape printed characters to prevent command injection #45 | [1260eea](https://github.com/undergroundwires/privacy.sexy/commit/1260eea690e4fa5420e58c9de9f88cc29cb242db)
|
||||||
|
* move code area to right on bigger screens | [cf39e6d](https://github.com/undergroundwires/privacy.sexy/commit/cf39e6d2541ea547f41d9553c380c54c24c58038)
|
||||||
|
* more scripts to disable speech recognition and Cortana | [ee43fd9](https://github.com/undergroundwires/privacy.sexy/commit/ee43fd92a019ebd26c13890f9146c5b5bb56afaf)
|
||||||
|
* add more macos scripts for privacy cleanup | [b0a7d0b](https://github.com/undergroundwires/privacy.sexy/commit/b0a7d0b53b3d8ac144a0241d70c037f460b0c0cc)
|
||||||
|
* add better error messages to setting vscode settings | [65226f3](https://github.com/undergroundwires/privacy.sexy/commit/65226f3984480d0bc7932fd8d76a328f08308850)
|
||||||
|
* remove windows scripts for removing non-bloating system apps #55 | [15004ff](https://github.com/undergroundwires/privacy.sexy/commit/15004ff1f1fb85a1d92e11ef695bcb2f37110610)
|
||||||
|
* remove "preview" disclaimer from macOS | [970221b](https://github.com/undergroundwires/privacy.sexy/commit/970221b996e25fe5b029cbaa78607c9bbc8c3c0e)
|
||||||
|
* update screenshot | [bd41af4](https://github.com/undergroundwires/privacy.sexy/commit/bd41af466fd135f7dc2f171633e4f60d8547c373)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.9.2...0.10.0)
|
||||||
|
|
||||||
## 0.9.2 (2021-02-13)
|
## 0.9.2 (2021-02-13)
|
||||||
|
|
||||||
* do not compile with unused locals vuejs/vetur#1063 | [73e0520](https://github.com/undergroundwires/privacy.sexy/commit/73e0520de70cdbaf0ecdc6e9be5e85f003fcfb79)
|
* do not compile with unused locals vuejs/vetur#1063 | [73e0520](https://github.com/undergroundwires/privacy.sexy/commit/73e0520de70cdbaf0ecdc6e9be5e85f003fcfb79)
|
||||||
|
|||||||
@@ -1,28 +1,51 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
- Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
|
Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
|
||||||
- Reporting a bug
|
|
||||||
- Discussing the current state of the code
|
|
||||||
- Submitting a fix
|
|
||||||
- Proposing new features
|
|
||||||
- Becoming a maintainer
|
|
||||||
|
|
||||||
## Pull Request Process
|
- reporting a bug,
|
||||||
|
- discussing the current state of the code,
|
||||||
|
- submitting a fix,
|
||||||
|
- proposing new features,
|
||||||
|
- or becoming a maintainer.
|
||||||
|
|
||||||
- [GitHub flow](https://guides.github.com/introduction/flow/index.html) is used
|
As a small open source project with small community, it can sometimes take a long time to address the issues so please be patient.
|
||||||
- Your pull requests are actively welcomed.
|
|
||||||
- The steps:
|
## Pull request process
|
||||||
1. Fork the repo and create your branch from master.
|
|
||||||
2. If you've added code that should be tested, add tests.
|
Your pull requests are actively welcomed. We collaborate using [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow).
|
||||||
3. If you've changed APIs, update the documentation.
|
|
||||||
4. Ensure the test suite passes.
|
The steps:
|
||||||
5. Make sure your code lints.
|
|
||||||
6. Issue that pull request!
|
1. Fork the repo and create your branch from master.
|
||||||
- 🙏 DO
|
2. If you've added code that requires testing, add tests. See [tests.md](./docs/tests.md).
|
||||||
- Document your changes in the pull request
|
3. If you've done a major change, update the documentation. See [docs/](./docs/).
|
||||||
- ❗ DON'T
|
4. Ensure the test suite passes. See [development.md | Testing](./docs/development.md#testing) for commands.
|
||||||
- Do not update the versions, current version is only [set by the maintainer](./img/architecture/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
|
5. Make sure your code lints.See [development.md | Linting](./docs/development.md#linting) for commands.
|
||||||
|
6. Issue that pull request!
|
||||||
|
|
||||||
|
**🙏 DO:**
|
||||||
|
|
||||||
|
- Document why (what you're trying to solve) rather than what in the pull request.
|
||||||
|
|
||||||
|
**❗ DON'T:**
|
||||||
|
|
||||||
|
- Do not update the versions, current version is [set by the maintainer](./docs/ci-cd.md#gitops) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
||||||
|
|
||||||
|
Automated pipelines will run to control your PR and they will publish your code once the maintainer merges your PR.
|
||||||
|
|
||||||
|
📖 You can read more in [ci-cd.md](./docs/ci-cd.md).
|
||||||
|
|
||||||
|
## Extend scripts
|
||||||
|
|
||||||
|
Here's quick information for you who want to add more scripts.
|
||||||
|
|
||||||
|
You have two alternatives:
|
||||||
|
|
||||||
|
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) and ask for someone else to add the script for you.
|
||||||
|
2. Or send a PR yourself. This would make it faster to get your code into the project. You need to add scripts to related OS in [collections](src/application/collections/) folder. Then you'd sent a pull request, see [pull request process](#pull-request-process).
|
||||||
|
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md).
|
||||||
|
- 📖 If you wish to use templates, use [templating.md](./docs/templating.md).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
By contributing, you agree that your contributions will be licensed under its GNU General Public License v3.0.
|
By contributing, you agree that your [GNU General Public License v3.0](./LICENSE) will be the license for your contributions.
|
||||||
|
|||||||
195
README.md
@@ -1,84 +1,147 @@
|
|||||||
# privacy.sexy
|
# privacy.sexy — Now you have the choice
|
||||||
|
|
||||||
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
> Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆
|
||||||
|
|
||||||
[](./CONTRIBUTING.md)
|
<!-- markdownlint-disable MD033 -->
|
||||||
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
<p align="center">
|
||||||
[](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
|
<a href="https://undergroundwires.dev/donate?project=privacy.sexy" target="_blank" rel="noopener noreferrer">
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
<img
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
alt="donation badge"
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
src="https://undergroundwires.dev/img/badges/donate/flat.svg"
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
/>
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
</a>
|
||||||
[](https://github.com/undergroundwires/bump-everywhere)
|
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="contributions are welcome"
|
||||||
|
src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Code quality -->
|
||||||
|
<br />
|
||||||
|
<a href="https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Language grade: JavaScript/TypeScript"
|
||||||
|
src="https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Maintainability"
|
||||||
|
src="https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Tests -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.unit.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Unit tests status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/unit-tests/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.integration.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Integration tests status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/integration-tests/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.e2e.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="E2E tests status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/e2e-tests/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Checks -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Quality checks status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Security checks status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/security-checks/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Build checks status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.desktop-runtime-errors.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Status of runtime error checks for the desktop application"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.desktop-runtime-errors/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Release -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Git release status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-git/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.site.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Site release status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-site/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.desktop.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Desktop application release status"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-desktop/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Others -->
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/undergroundwires/bump-everywhere" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Auto-versioned by bump-everywhere"
|
||||||
|
src="https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 💡 No need to run any compiled software on your computer.
|
- 🖥️ **Offline**: Check [releases page](https://github.com/undergroundwires/privacy.sexy/releases), or download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-Setup-0.11.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.dmg), [Linux](https://github.com/undergroundwires/pr.vacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.AppImage).
|
||||||
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-Setup-0.9.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-0.9.2.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-0.9.2.AppImage).
|
|
||||||
- 💡 Single click to execute your script.
|
|
||||||
- ❗ Come back regularly to apply latest version for stronger privacy and security.
|
|
||||||
|
|
||||||
[](https://privacy.sexy)
|
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
||||||
|
|
||||||
## Why
|
💡 You should apply your configuration from time to time (more than once). It would strengthen your privacy and security control because privacy.sexy and its scripts get better and stronger in every new version.
|
||||||
|
|
||||||
- Rich tweak pool to harden security & privacy of the OS and other software on it
|
[](https://privacy.sexy)
|
||||||
- Free (both free as in beer and free as in speech)
|
|
||||||
- No need to run any compiled software that has access to your system, just run the generated scripts
|
|
||||||
- Have full visibility into what the tweaks do as you enable them
|
|
||||||
- Ability to revert (undo) applied scripts
|
|
||||||
- Everything is transparent: both application and its infrastructure are open-source and automated
|
|
||||||
- Easily extendable
|
|
||||||
|
|
||||||
## Extend scripts
|
## Features
|
||||||
|
|
||||||
1. Fork the repository
|
- **Rich**: Hundreds of scripts that aims to give you control of your data.
|
||||||
2. Add more scripts in respective script collection in [collections](src/application/collections/) folder.
|
- **Free**: Both free as in "beer" and free as in "speech".
|
||||||
- 📖 If you're unsure about the syntax you can refer to the [collection files | documentation](docs/collection-files.md).
|
- **Transparent**. Have full visibility into what the tweaks do as you enable them.
|
||||||
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
- **Reversible**. Revert if something feels wrong.
|
||||||
3. Send a pull request 👌
|
- **Accessible**. No need to run any compiled software on your computer with web version.
|
||||||
|
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
||||||
|
- **Tested**. A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
||||||
|
- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md).
|
||||||
|
- **Portable and simple**. Every script is independently executable without cross-dependencies.
|
||||||
|
|
||||||
## Commands
|
## Support
|
||||||
|
|
||||||
- Project setup: `npm install`
|
**Sponsor 💕**. Consider sponsoring on [GitHub Sponsors](https://github.com/sponsors/undergroundwires), or you can donate using [other ways such as crypto or a coffee](https://undergroundwires.dev/donate).
|
||||||
- Testing
|
|
||||||
- Run unit tests: `npm run test:unit`
|
|
||||||
- Lint: `npm run lint`
|
|
||||||
- **Desktop app**
|
|
||||||
- Development: `npm run electron:serve`
|
|
||||||
- Production: `npm run electron:build` to build an executable
|
|
||||||
- **Webpage**
|
|
||||||
- Development: `npm run serve` to compile & hot-reload for development.
|
|
||||||
- Production: `npm run build` to prepare files for distribution.
|
|
||||||
- Or run using Docker:
|
|
||||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.9.2 .`
|
|
||||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.9.2 undergroundwires/privacy.sexy:0.9.2`
|
|
||||||
|
|
||||||
## Architecture overview
|
**Star 🤩**. Feel free to give it a star ⭐ .
|
||||||
|
|
||||||
### Application
|
**Contribute 👷**. Contributions of any type are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) as the starting point. It includes useful information like [how to add new scripts](./CONTRIBUTING.md#extend-scripts).
|
||||||
|
|
||||||
- Powered by **TypeScript**, **Vue.js** and **Electron** 💪
|
## Development
|
||||||
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
|
|
||||||
- Application uses highly decoupled models & services in different DDD layers.
|
|
||||||
- 📖 Read more on • [Presentation](./docs/presentation.md) • [Application](./docs/application.md)
|
|
||||||
|
|
||||||

|
Refer to [development.md](./docs/development.md) for Docker usage and reading more about setting up your development environment.
|
||||||
|
|
||||||
### AWS Infrastructure
|
Check [architecture.md](./docs/architecture.md) for an overview of design and how different parts and layers work together. You can refer to [application.md](./docs/application.md) for a closer look at application layer codebase and [presentation.md](./docs/presentation.md) for code related to GUI layer. [collection-files.md](./docs/collection-files.md) explains the YAML files that are the core of the application and [templating.md](./docs/templating.md) documents how to use templating language in those files. In [ci-cd.md](./docs/ci-cd.md), you can read more about the pipelines that automates maintenance tasks and ensures you get what see.
|
||||||
|
|
||||||
[](https://github.com/undergroundwires/aws-static-site-with-cd)
|
[docs/](./docs/) folder includes all other documentation.
|
||||||
|
|
||||||
- It uses infrastructure from the following repository: [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd)
|
|
||||||
- Runs on AWS 100% serverless and automatically provisioned using [GitHub Actions](.github/workflows/).
|
|
||||||
- Maximum security & automation and minimum AWS costs are the highest priorities of the design.
|
|
||||||
|
|
||||||
#### GitOps: CI/CD to AWS
|
|
||||||
|
|
||||||
- CI/CD is fully automated for this repo using different GIT events & GitHub actions.
|
|
||||||
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
|
|
||||||
- Everything that's merged in the master goes directly to production.
|
|
||||||
|
|
||||||
[](.github/workflows/)
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# build
|
# build
|
||||||
|
|
||||||
- These are the file that are used by electron.
|
This folder contains files that are used by Electron to serve the desktop version.
|
||||||
- Logos are created by from the [PNG icon](./../public/icon.png)
|
|
||||||
- by running `npx electron-icon-builder --input=./public/icon.png --output=build --flatten`
|
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md).
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 740 B After Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |
15
cypress.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'cypress';
|
||||||
|
import ViteConfig from './vite.config';
|
||||||
|
|
||||||
|
const CYPRESS_BASE_DIR = 'tests/e2e/';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
fixturesFolder: `${CYPRESS_BASE_DIR}/fixtures`,
|
||||||
|
screenshotsFolder: `${CYPRESS_BASE_DIR}/screenshots`,
|
||||||
|
videosFolder: `${CYPRESS_BASE_DIR}/videos`,
|
||||||
|
e2e: {
|
||||||
|
baseUrl: `http://localhost:${ViteConfig.server.port}/`,
|
||||||
|
specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
|
||||||
|
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,22 +1,45 @@
|
|||||||
# Application
|
# Application
|
||||||
|
|
||||||
- It's mainly responsible for
|
Application layer is mainly responsible for:
|
||||||
- creating and event based [application state](#application-state)
|
|
||||||
- parsing and compiling [application data](#application-data)
|
- creating an event-based and mutable [application state](#application-state),
|
||||||
|
- [parsing and compiling](#parsing-and-compiling) the [application data](#application-data).
|
||||||
|
|
||||||
|
📖 Refer to [architecture.md | Layered Application](./architecture.md#layered-application) to read more about the layered architecture.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
Application layer code exists in [`/src/application`](./../src/application/) and includes following structure:
|
||||||
|
|
||||||
|
- [**`collections/`**](./../src/application/collections/): Holds [collection files](./collection-files.md).
|
||||||
|
- [**`Common/`**](./../src/application/Common/): Contains common functionality in application layer.
|
||||||
|
- `...`: rest of the application layer source code organized using folders-by-feature structure.
|
||||||
|
|
||||||
## Application state
|
## Application state
|
||||||
|
|
||||||
- [ApplicationContext.ts](./../src/application/Context/ApplicationContext.ts) holds the [CategoryCollectionState](./../src/application/Context/State/CategoryCollectionState.ts) for each OS
|
It uses [state pattern](https://en.wikipedia.org/wiki/State_pattern) with context and state objects. [`ApplicationContext.ts`](./../src/application/Context/ApplicationContext.ts) the "Context" of state pattern provides an instance of [`CategoryCollectionState.ts`](./../src/application/Context/State/CategoryCollectionState.ts) (the "State" of the state pattern) for every supported collection.
|
||||||
- Uses [state pattern](https://en.wikipedia.org/wiki/State_pattern)
|
|
||||||
- Same instance is shared throughout the application to ensure consistent state
|
Presentation layer uses a singleton (same instance of) [`ApplicationContext.ts`](./../src/application/Context/ApplicationContext.ts) throughout the application to ensure consistent state.
|
||||||
- 📖 See [Application State | Presentation layer](./presentation.md#application-state) to read more about how the state should be managed by the presentation layer.
|
|
||||||
- 📖 See [ApplicationContext.ts](./../src/application/Context/ApplicationContext.ts) to start diving into the state code.
|
📖 Refer to [architecture.md | Application State](./architecture.md#application-state) to get an overview of event handling and [presentation.md | Application State](./presentation.md#application-state) for deeper look into how the presentation layer manages state.
|
||||||
|
|
||||||
## Application data
|
## Application data
|
||||||
|
|
||||||
- Compiled to `Application` domain object.
|
Application data is collection files using YAML. You can refer to [collection-files.md](./collection-files.md) to read more about the scheme and structure of application data files. You can also check the source code [collection yaml files](./../src/application/collections/) to directly see the application data using that scheme.
|
||||||
- The scripts are defined and controlled in different data files per OS
|
|
||||||
- Enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) and easier contributions
|
Application layer [parses and compiles](#parsing-and-compiling) application data into [`Application`](./../src/domain/Application.ts)). Once parsed, application layer provides the necessary functionality to presentation layer based on the application data. You can read more about how presentation layer consumes the application data in [presentation.md | Application Data](./presentation.md#application-data).
|
||||||
- Application data is defined in collection files and
|
|
||||||
- 📖 See [Application data | Presentation layer](./presentation.md#application-data) to read how the application data is read by the presentation layer.
|
Application layer enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) by leveraging the data to the rest of the source code. It makes it easy for community to contribute on the project by using a declarative language used in collection files.
|
||||||
- 📖 See [collection files documentation](./collection-files.md) to read more about how the data files are structured/defined and see [collection yaml files](./../src/application/collections/) to directly check the code.
|
|
||||||
|
### Parsing and compiling
|
||||||
|
|
||||||
|
Application layer parses the application data to compile the domain object [`Application.ts`](./../src/domain/Application.ts).
|
||||||
|
|
||||||
|
The build tool loads (or injects) application data ([collection yaml files](./../src/application/collections/)) into the application layer in compile time. Application layer ([`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)) parses and compiles this data in runtime.
|
||||||
|
|
||||||
|
Application layer compiles templating syntax during parsing to create the end scripts. You can read more about templating syntax in [templating.md](./templating.md) and how application data uses them through functions in [collection-files.md | Function](./collection-files.md#function).
|
||||||
|
|
||||||
|
The steps to extend the templating syntax:
|
||||||
|
|
||||||
|
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
|
||||||
|
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).
|
||||||
|
|||||||
80
docs/architecture.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Architecture overview
|
||||||
|
|
||||||
|
This repository consists of:
|
||||||
|
|
||||||
|
- A [layered application](#layered-application).
|
||||||
|
- [AWS infrastructure](#aws-infrastructure) as code and instructions to host the website.
|
||||||
|
- [GitOps](#gitops) practices for development, maintenance and deployment.
|
||||||
|
|
||||||
|
## Layered application
|
||||||
|
|
||||||
|
Application is
|
||||||
|
|
||||||
|
- powered by **TypeScript**, **Vue.js** and **Electron** 💪,
|
||||||
|
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
|
||||||
|
|
||||||
|
Application uses highly decoupled models & services in different DDD layers:
|
||||||
|
|
||||||
|
**Application layer** (see [application.md](./application.md)):
|
||||||
|
|
||||||
|
- Coordinates application activities and consumes the domain layer.
|
||||||
|
|
||||||
|
**Presentation layer** (see [presentation.md](./presentation.md)):
|
||||||
|
|
||||||
|
- Handles UI/UX, consumes both the application and domain layers.
|
||||||
|
- May communicate directly with the infrastructure layer for technical needs, but avoids domain logic.
|
||||||
|
|
||||||
|
**Domain layer**:
|
||||||
|
|
||||||
|
- Serves as the system's core and central truth.
|
||||||
|
- Facilitates communication between the application and presentation layers through the domain model.
|
||||||
|
|
||||||
|
**Infrastructure layer**:
|
||||||
|
|
||||||
|
- Manages technical implementations without dependencies on other layers or domain knowledge.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Application state
|
||||||
|
|
||||||
|
State handling uses an event-driven subscription model to signal state changes and special functions to register changes. It does not depend on third party packages.
|
||||||
|
|
||||||
|
The presentation layer can read and modify state through the context. State changes trigger events that components can subscribe to for reactivity.
|
||||||
|
|
||||||
|
Each layer treat application layer differently.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*[Presentation layer](./presentation.md)*:
|
||||||
|
|
||||||
|
- Each component holds their own state about presentation-related data.
|
||||||
|
- Components register shared state changes into application state using functions.
|
||||||
|
- Components listen to shared state changes using event subscriptions.
|
||||||
|
- 📖 Read more: [presentation.md | Application state](./presentation.md#application-state).
|
||||||
|
|
||||||
|
*[Application layer](./application.md)*:
|
||||||
|
|
||||||
|
- Stores the application-specific state.
|
||||||
|
- The state it exposed for read with getter functions and set using setter functions, setter functions also fire application events that allows other parts of application and the view in presentation layer to react.
|
||||||
|
- So state is mutable, and fires related events when mutated.
|
||||||
|
- 📖 Read more: [application.md | Application state](./application.md#application-state).
|
||||||
|
|
||||||
|
It's comparable with `flux`, `vuex`, and `pinia`. A difference is that mutable application layer state in privacy.sexy is mutable and lies in single "store" that holds app state and logic. The "actions" mutate the state directly which in turns act as dispatcher to notify its own event subscriptions (callbacks).
|
||||||
|
|
||||||
|
## AWS infrastructure
|
||||||
|
|
||||||
|
The web-site runs on serverless AWS infrastructure. Infrastructure is open-source and deployed as code. [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd) project includes the source code.
|
||||||
|
|
||||||
|
[](https://github.com/undergroundwires/aws-static-site-with-cd)
|
||||||
|
|
||||||
|
The design priorities highest security then minimizing cloud infrastructure costs.
|
||||||
|
|
||||||
|
This project includes [GitHub Actions](../.github/workflows/) to automatically provision the infrastructure with zero-touch and without any "hidden" steps, ensuring everything is open-source and transparent. Git repositories includes all necessary instructions and automation with [GitOps](#gitops) practices.
|
||||||
|
|
||||||
|
## GitOps
|
||||||
|
|
||||||
|
CI/CD pipelines automate operational tasks based on different Git events. [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) enables this automation.
|
||||||
|
|
||||||
|
📖 Read more in [`ci-cd.md`](./ci-cd.md#gitops).
|
||||||
|
|
||||||
|
[](../.github/workflows/)
|
||||||
45
docs/ci-cd.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# CI/CD overview
|
||||||
|
|
||||||
|
## GitOps
|
||||||
|
|
||||||
|
CI/CD is fully automated using different Git events and GitHub actions. This repository uses [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) to automate versioning, tagging, creation of `CHANGELOG.md` and GitHub releases. A dedicated workflow [release.desktop.yaml](./../.github/workflows/release.desktop.yaml) creates desktop installers and executables and attaches them into GitHub releases.
|
||||||
|
|
||||||
|
Everything that's merged in the master goes directly to production.
|
||||||
|
|
||||||
|
[](../.github/workflows/)
|
||||||
|
|
||||||
|
## Pipeline files
|
||||||
|
|
||||||
|
privacy.sexy uses [GitHub actions](https://github.com/features/actions) to define and run pipelines as code.
|
||||||
|
|
||||||
|
GitHub workflows i.e. pipelines exist in [`/.github/workflows/`](./../.github/workflows/) folder without any subfolders due to GitHub actions requirements [1] .
|
||||||
|
|
||||||
|
Local GitHub actions are defined in [`/.github/actions/`](./../.github/actions/) and used to reuse same workflow steps.
|
||||||
|
|
||||||
|
## Pipeline types
|
||||||
|
|
||||||
|
We categorize pipelines into different categories. We use these names in convention when naming files and actions, see [naming conventions](#naming-conventions).
|
||||||
|
|
||||||
|
The categories consist of:
|
||||||
|
|
||||||
|
- `tests`: Different types of tests to verify functionality.
|
||||||
|
- `checks`: Other controls such as vulnerability scans or styling checks.
|
||||||
|
- `release`: Pipelines used for release of deployment such as building and testing.
|
||||||
|
|
||||||
|
## Naming conventions
|
||||||
|
|
||||||
|
Convention for naming pipeline files: **`<type>.<name>.yaml`**.
|
||||||
|
|
||||||
|
**`type`**:
|
||||||
|
|
||||||
|
- Sub-folders do not work for GitHub workflows [1] so we use `<type>.` prefix to organize them.
|
||||||
|
- See also [pipeline types](#pipeline-types) for list of all usable types.
|
||||||
|
|
||||||
|
**`name`**:
|
||||||
|
|
||||||
|
- We name workflows using kebab-case.
|
||||||
|
- E.g. file name `tests.unit.yaml`, pipeline file should set the naem as: `name: unit-tests`.
|
||||||
|
- Kebab-case allows to have better URL references to them.
|
||||||
|
- [README.md](./../README.md) uses URL references to show status badges for actions.
|
||||||
|
|
||||||
|
[1]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
|
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
|
||||||
- 💡 Best practices
|
- 💡 Best practices
|
||||||
- If you repeat yourself, try to utilize [YAML-defined functions](#Function)
|
- If you repeat yourself, try to utilize [YAML-defined functions](#function)
|
||||||
- Always try to add documentation and a way to revert a tweak in [scripts](#Script)
|
- Always try to add documentation and a way to revert a tweak in [scripts](#script)
|
||||||
- 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
|
- 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
|
||||||
|
|
||||||
## Objects
|
## Objects
|
||||||
@@ -13,19 +13,19 @@
|
|||||||
- A collection simply defines:
|
- A collection simply defines:
|
||||||
- different categories and their scripts in a tree structure
|
- different categories and their scripts in a tree structure
|
||||||
- OS specific details
|
- OS specific details
|
||||||
- Also allows defining common [function](#Function)s to be used throughout the collection if you'd like different scripts to share same code.
|
- Also allows defining common [function](#function)s to be used throughout the collection if you'd like different scripts to share same code.
|
||||||
|
|
||||||
#### `Collection` syntax
|
#### `Collection` syntax
|
||||||
|
|
||||||
- `os:` *`string`* (**required**)
|
- `os:` *`string`* (**required**)
|
||||||
- Operating system that the [Collection](#collection) is written for.
|
- Operating system that the [Collection](#collection) is written for.
|
||||||
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
|
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
|
||||||
- `actions: [` ***[`Category`](#Category)*** `, ... ]` **(required)**
|
- `actions: [` ***[`Category`](#category)*** `, ... ]` **(required)**
|
||||||
- Each [category](#category) is rendered as different cards in card presentation.
|
- Each [category](#category) is rendered as different cards in card presentation.
|
||||||
- ❗ A [Collection](#collection) must consist of at least one category.
|
- ❗ A [Collection](#collection) must consist of at least one category.
|
||||||
- `functions: [` ***[`Function`](#Function)*** `, ... ]`
|
- `functions: [` ***[`Function`](#function)*** `, ... ]`
|
||||||
- Functions are optionally defined to re-use the same code throughout different scripts.
|
- Functions are optionally defined to re-use the same code throughout different scripts.
|
||||||
- `scripting:` ***[`ScriptingDefinition`](#ScriptingDefinition)*** **(required)**
|
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
|
||||||
- Defines the scripting language that the code of other action uses.
|
- Defines the scripting language that the code of other action uses.
|
||||||
|
|
||||||
### `Category`
|
### `Category`
|
||||||
@@ -38,16 +38,21 @@
|
|||||||
- `category:` *`string`* (**required**)
|
- `category:` *`string`* (**required**)
|
||||||
- Name of the category
|
- Name of the category
|
||||||
- ❗ Must be unique throughout the [Collection](#collection)
|
- ❗ Must be unique throughout the [Collection](#collection)
|
||||||
- `children: [` ***[`Category`](#Category)*** `|` [***`Script`***](#Script) `, ... ]` (**required**)
|
- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#script) `, ... ]` (**required**)
|
||||||
- ❗ Category must consist of at least one subcategory or script.
|
- ❗ Category must consist of at least one subcategory or script.
|
||||||
- Children can be combination of scripts and subcategories.
|
- Children can be combination of scripts and subcategories.
|
||||||
|
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||||
|
- Documentation pieces related to the category.
|
||||||
|
- Rendered as markdown.
|
||||||
|
|
||||||
### `Script`
|
### `Script`
|
||||||
|
|
||||||
- Script represents a single tweak.
|
- Script represents a single tweak.
|
||||||
- A script must include either:
|
- A script can be of two different types (just like [functions](#function)):
|
||||||
- A `code` and `revertCode`
|
1. Inline script; a script with an inline code
|
||||||
- Or `call` to call YAML-defined functions
|
- Must define `code` property and optionally `revertCode` but not `call`
|
||||||
|
2. Caller script; a script that calls other functions
|
||||||
|
- Must define `call` property but not `code` or `revertCode`
|
||||||
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||||
|
|
||||||
#### `Script` syntax
|
#### `Script` syntax
|
||||||
@@ -65,12 +70,12 @@
|
|||||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
- ❗ Do not define if `call` is defined.
|
- ❗ Do not define if `call` is defined.
|
||||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**)
|
||||||
- A shared function or sequence of functions to call (called in order)
|
- A shared function or sequence of functions to call (called in order)
|
||||||
- ❗ If not defined `code` must be defined
|
- ❗ If not defined `code` must be defined
|
||||||
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||||
- Single documentation URL or list of URLs for those who wants to learn more about the script
|
- Documentation pieces related to the script.
|
||||||
- E.g. `https://docs.microsoft.com/en-us/windows-server/`
|
- Rendered as markdown.
|
||||||
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
|
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
|
||||||
- If not defined then the script will not be recommended
|
- If not defined then the script will not be recommended
|
||||||
- If defined it can be either
|
- If defined it can be either
|
||||||
@@ -80,7 +85,7 @@
|
|||||||
### `FunctionCall`
|
### `FunctionCall`
|
||||||
|
|
||||||
- Describes a single call to a function by optionally providing values to its parameters.
|
- Describes a single call to a function by optionally providing values to its parameters.
|
||||||
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
- 👀 See [parameter substitution](./templating.md#parameter-substitution) for an example usage
|
||||||
|
|
||||||
#### `FunctionCall` syntax
|
#### `FunctionCall` syntax
|
||||||
|
|
||||||
@@ -98,52 +103,18 @@
|
|||||||
appName: Microsoft.WindowsFeedbackHub
|
appName: Microsoft.WindowsFeedbackHub
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- 💡 [Expressions (templating)](./templating.md#expressions) can be used as parameter value
|
||||||
|
|
||||||
### `Function`
|
### `Function`
|
||||||
|
|
||||||
- Functions allow re-usable code throughout the defined scripts.
|
- Functions allow re-usable code throughout the defined scripts.
|
||||||
- Functions are templates compiled by privacy.sexy and uses special [expressions](#expressions).
|
- Functions are templates compiled by privacy.sexy and uses special expression expressions.
|
||||||
- Functions can call other functions by defining `call` property instead of `code`
|
- A function can be of two different types (just like [scripts](#script)):
|
||||||
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
1. Inline function: a function with an inline code.
|
||||||
|
- Must define `code` property and optionally `revertCode` but not `call`.
|
||||||
#### Expressions
|
2. Caller function: a function that calls other functions.
|
||||||
|
- Must define `call` property but not `code` or `revertCode`.
|
||||||
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
- 👀 Read more on [Templating](./templating.md) for function expressions and [example usages](./templating.md#parameter-substitution).
|
||||||
|
|
||||||
##### Parameter substitution
|
|
||||||
|
|
||||||
A simple function example
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
function: EchoArgument
|
|
||||||
parameters: [ 'argument' ]
|
|
||||||
code: Hello {{ $argument }} !
|
|
||||||
```
|
|
||||||
|
|
||||||
It would print "Hello world" if it's called in a [script](#script) as following:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
script: Echo script
|
|
||||||
call:
|
|
||||||
function: EchoArgument
|
|
||||||
parameters:
|
|
||||||
argument: World
|
|
||||||
```
|
|
||||||
|
|
||||||
A function can call other functions such as:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
-
|
|
||||||
function: CallerFunction
|
|
||||||
parameters: [ 'value' ]
|
|
||||||
call:
|
|
||||||
function: EchoArgument
|
|
||||||
parameters:
|
|
||||||
argument: {{ $value }}
|
|
||||||
-
|
|
||||||
function: EchoArgument
|
|
||||||
parameters: [ 'argument' ]
|
|
||||||
code: Hello {{ $argument }} !
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `Function` syntax
|
#### `Function` syntax
|
||||||
|
|
||||||
@@ -152,24 +123,43 @@ A function can call other functions such as:
|
|||||||
- Convention is to use camelCase, and be verbs.
|
- Convention is to use camelCase, and be verbs.
|
||||||
- E.g. `uninstallStoreApp`
|
- E.g. `uninstallStoreApp`
|
||||||
- ❗ Function names must be unique
|
- ❗ Function names must be unique
|
||||||
- `parameters`: `[` *`string`* `, ... ]`
|
- `parameters`: `[` ***[`FunctionParameter`](#functionparameter)*** `, ... ]`
|
||||||
- Name of the parameters that the function has.
|
- List of parameters that function code refers to.
|
||||||
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#FunctionCall)
|
- ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
|
||||||
- Parameter names must be defined to be used in [expressions](#expressions)
|
|
||||||
- ❗ Parameter names must be unique
|
|
||||||
`code`: *`string`* (**required** if `call` is undefined)
|
`code`: *`string`* (**required** if `call` is undefined)
|
||||||
- Batch file commands that will be executed
|
- Batch file commands that will be executed
|
||||||
|
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in its value
|
||||||
- 💡 If defined, best practice to also define `revertCode`
|
- 💡 If defined, best practice to also define `revertCode`
|
||||||
- ❗ If not defined `call` must be defined
|
- ❗ If not defined `call` must be defined
|
||||||
- `revertCode`: *`string`*
|
- `revertCode`: *`string`*
|
||||||
- Code that'll undo the change done by `code` property.
|
- Code that'll undo the change done by `code` property.
|
||||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in code
|
||||||
|
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**)
|
||||||
- A shared function or sequence of functions to call (called in order)
|
- A shared function or sequence of functions to call (called in order)
|
||||||
- The parameter values that are sent can use [expressions](#expressions)
|
- The parameter values that are sent can use [expressions (templating)](./templating.md#expressions)
|
||||||
- ❗ If not defined `code` must be defined
|
- ❗ If not defined `code` must be defined
|
||||||
|
|
||||||
|
### `FunctionParameter`
|
||||||
|
|
||||||
|
- Defines a parameter that function requires optionally or mandatory.
|
||||||
|
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#functioncall).
|
||||||
|
|
||||||
|
#### `FunctionParameter` syntax
|
||||||
|
|
||||||
|
- `name`: *`string`* (**required**)
|
||||||
|
- Name of the parameters that the function has.
|
||||||
|
- Parameter names must be defined to be used in [expressions (templating)](./templating.md#expressions).
|
||||||
|
- ❗ Parameter names must be unique and include alphanumeric characters only.
|
||||||
|
- `optional`: *`boolean`* (default: `false`)
|
||||||
|
- Specifies whether the caller [Script](#script) must provide any value for the parameter.
|
||||||
|
- If set to `false` i.e. an argument value is not optional then it expects a non-empty value for the variable;
|
||||||
|
- Otherwise it throws.
|
||||||
|
- 💡 Set it to `true` if a parameter is used conditionally;
|
||||||
|
- Or else set it to `false` for verbosity or do not define it as default value is `false` anyway.
|
||||||
|
- 💡 Can be used in conjunction with [`with` expression](./templating.md#with).
|
||||||
|
|
||||||
### `ScriptingDefinition`
|
### `ScriptingDefinition`
|
||||||
|
|
||||||
- Defines global properties for scripting that's used throughout its parent [Collection](#collection).
|
- Defines global properties for scripting that's used throughout its parent [Collection](#collection).
|
||||||
@@ -180,7 +170,7 @@ A function can call other functions such as:
|
|||||||
- 📖 See [ScriptingLanguage.ts](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values.
|
- 📖 See [ScriptingLanguage.ts](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values.
|
||||||
- `startCode:` *`string`* (**required**)
|
- `startCode:` *`string`* (**required**)
|
||||||
- Code that'll be inserted on top of user created script.
|
- Code that'll be inserted on top of user created script.
|
||||||
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||||
- `endCode:` *`string`* (**required**)
|
- `endCode:` *`string`* (**required**)
|
||||||
- Code that'll be inserted at the end of user created script.
|
- Code that'll be inserted at the end of user created script.
|
||||||
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||||
|
|||||||
75
docs/development.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Development
|
||||||
|
|
||||||
|
Before your commit, a good practice is to:
|
||||||
|
|
||||||
|
1. [Run unit tests](#testing)
|
||||||
|
2. [Lint your code](#linting)
|
||||||
|
|
||||||
|
You could run other types of tests as well, but they may take longer time and overkill for your changes. Automated actions executes the tests for a pull request or change in the main branch. See [ci-cd.md](./ci-cd.md) for more information.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Install node >15.x.
|
||||||
|
- Install dependencies using `npm install`.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Run unit tests: `npm run test:unit`
|
||||||
|
- Run integration tests: `npm run test:integration`
|
||||||
|
- Run end-to-end (e2e) tests:
|
||||||
|
- `npm run test:cy:open`: Run tests interactively using the development server with hot-reloading.
|
||||||
|
- `npm run test:cy:run`: Run tests on the production build in a headless mode.
|
||||||
|
|
||||||
|
📖 Read more about testing in [tests](./tests.md).
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
- Lint all (recommended 💡): `npm run lint`
|
||||||
|
- Markdown: `npm run lint:md`
|
||||||
|
- Markdown consistency `npm run lint:md:consistency`
|
||||||
|
- Markdown relative URLs: `npm run lint:md:relative-urls`
|
||||||
|
- JavaScript/TypeScript: `npm run lint:eslint`
|
||||||
|
- Yaml: `npm run lint:yaml`
|
||||||
|
|
||||||
|
### Running
|
||||||
|
|
||||||
|
**Web:**
|
||||||
|
|
||||||
|
- Run in local server: `npm run dev`
|
||||||
|
- 💡 Meant for local development with features such as hot-reloading.
|
||||||
|
- Preview production build: `npm run preview`
|
||||||
|
- Start a local web server that serves the built solution from `./dist`.
|
||||||
|
- 💡 Run `npm run build` before `npm run preview`.
|
||||||
|
|
||||||
|
**Desktop apps:**
|
||||||
|
|
||||||
|
- `npm run electron:dev`: The command will build the main process and preload scripts source code, and start a dev server for the renderer, and start the Electron app.
|
||||||
|
- `npm run electron:preview`: The command will build the main process, preload scripts and renderer source code, and start the Electron app to preview.
|
||||||
|
- `npm run electron:prebuild`: The command will build the main process, preload scripts and renderer source code. Usually before packaging the Electron application, you need to execute this command.
|
||||||
|
- `npm run electron:build`: Prebuilds the Electron application, packages and publishes it through `electron-builder`.
|
||||||
|
|
||||||
|
**Docker:**
|
||||||
|
|
||||||
|
1. Build: `docker build -t undergroundwires/privacy.sexy:latest .`
|
||||||
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest`
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
- Build web application: `npm run build`
|
||||||
|
- Build desktop application: `npm run electron:build`
|
||||||
|
- (Re)create icons (see [documentation](../img/README.md)): `npm run create-icons`
|
||||||
|
|
||||||
|
### Utility Scripts
|
||||||
|
|
||||||
|
- Run fresh NPM install: [`./scripts/fresh-npm-install.sh`](../scripts/fresh-npm-install.sh)
|
||||||
|
- This script provides a clean NPM install, removing existing node modules and optionally the package-lock.json (when run with -n), then installs dependencies and runs unit tests.
|
||||||
|
- Configure VSCode: [`./scripts/configure-vscode.sh`](../scripts/configure-vscode.sh)
|
||||||
|
- This script checks and sets the necessary configurations for VSCode in `settings.json` file.
|
||||||
|
|
||||||
|
## Recommended extensions
|
||||||
|
|
||||||
|
You should use EditorConfig to follow project style.
|
||||||
|
|
||||||
|
For Visual Studio Code, [`.vscode/extensions.json`](./../.vscode/extensions.json) includes list of recommended extensions.
|
||||||
@@ -1,24 +1,107 @@
|
|||||||
# Presentation layer
|
# Presentation layer
|
||||||
|
|
||||||
- Consists of Vue.js components and other UI-related code.
|
The presentation layer handles UI concerns using Vue as JavaScript framework and Electron to provide desktop functionality.
|
||||||
- Desktop application is created using [Electron](https://www.electronjs.org/).
|
|
||||||
- Event driven as in components simply listens to events from the state and act accordingly.
|
It reflects the [application state](./application.md#application-state) and allows user interactions to modify it. Components manage their own local UI state.
|
||||||
|
|
||||||
|
The presentation layer uses an event-driven architecture for bidirectional reactivity between the application state and UI. State change events flow bottom-up to trigger UI updates, while user events flow top-down through components, some ultimately modifying the application state.
|
||||||
|
|
||||||
|
📖 Refer to [architecture.md (Layered Application)](./architecture.md#layered-application) to read more about the layered architecture.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code.
|
||||||
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
||||||
|
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
||||||
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
||||||
|
- [**`hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections).
|
||||||
|
- [**`/public/`**](../src/presentation/public/): Contains static assets.
|
||||||
|
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
|
||||||
|
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
||||||
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||||
|
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles for Vue components.
|
||||||
|
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles for third-party components.
|
||||||
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
|
||||||
|
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
||||||
|
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||||
|
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||||
|
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||||
|
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
||||||
|
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
||||||
|
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
||||||
|
|
||||||
|
## Visual design best-practices
|
||||||
|
|
||||||
|
Add visual clues for clickable items. It should be as clear as possible that they're interactable at first look without hovering. They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility.
|
||||||
|
|
||||||
## Application data
|
## Application data
|
||||||
|
|
||||||
- Components and should use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain.
|
Components (should) use [`UseApplication`](./../src/presentation/components/Shared/Hooks/UseApplication.ts) to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again.
|
||||||
- [Application.ts](../src/domain/Application.ts) domain model is the stateless application representation including
|
|
||||||
- available scripts, collections as defined in [collection files](./collection-files.md)
|
[Application.ts](../src/domain/Application.ts) is an immutable domain model that represents application state. It includes:
|
||||||
- package information as defined in [`package.json`](./../package.json)
|
|
||||||
- 📖 See [Application data | Application layer](./presentation.md#application-data) where application data is parsed and compiled.
|
- available scripts, collections as defined in [collection files](./collection-files.md),
|
||||||
|
- package information as defined in [`package.json`](./../package.json).
|
||||||
|
|
||||||
|
You can read more about how application layer provides application data to he presentation in [application.md | Application data](./application.md#application-data).
|
||||||
|
|
||||||
## Application state
|
## Application state
|
||||||
|
|
||||||
- Stateful components mutate or/and react to state changes in [ApplicationContext](./../src/application/Context/ApplicationContext.ts).
|
This project uses a singleton instance of the application state, making it available to all Vue components.
|
||||||
- Stateless components that does not handle state extends `Vue`
|
|
||||||
- Stateful components that depends on the collection state such as user selection, search queries and more extends [`StatefulVue`](./../src/presentation/StatefulVue.ts)
|
The decision to not use third-party state management libraries like [`vuex`](https://web.archive.org/web/20230801191617/https://vuex.vuejs.org/) or [`pinia`](https://web.archive.org/web/20230801191743/https://pinia.vuejs.org/) was made to promote code independence and enhance portability.
|
||||||
- The single source of truth is a singleton of the state created and made available to presentation layer by [`StatefulVue`](./../src/presentation/StatefulVue.ts)
|
|
||||||
- `StatefulVue` includes abstract `handleCollectionState` that is fired once the component is loaded and also each time [collection](./collection-files.md) is changed.
|
Stateful components can mutate and/or react to state changes (e.g., user selection, search queries) in the [ApplicationContext](./../src/application/Context/ApplicationContext.ts). Vue components import [`CollectionState.ts`](./../src/presentation/components/Shared/Hooks/UseCollectionState.ts) to access both the application context and the state.
|
||||||
- Do not forget to subscribe from events when component is destroyed or if needed [collection](./collection-files.md) is changed.
|
|
||||||
- 💡 `events` in base class [`StatefulVue`](./../src/presentation/StatefulVue.ts) makes lifecycling easier
|
[`UseCollectionState.ts`](./../src/presentation/components/Shared/Hooks/UseCollectionState.ts) provides several functionalities including:
|
||||||
- 📖 See [Application state | Application layer](./presentation.md#application-state) where the state is implemented using using state pattern.
|
|
||||||
|
- **Singleton State Instance**: It creates a singleton instance of the state, which is shared across the presentation layer. The singleton instance ensures that there's a single source of truth for the application's state.
|
||||||
|
- **State Change Callback and Lifecycle Management**: It offers a mechanism to register callbacks, which will be invoked when the state initializes or mutates. It ensures that components unsubscribe from state events when they are no longer in use or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md).
|
||||||
|
- **State Access and Modification**: It provides functions to read and mutate for accessing and modifying the state, encapsulating the details of these operations.
|
||||||
|
- **Event Subscription Lifecycle Management**: Includes an `events` member that simplifies state subscription lifecycle events. This ensures that components unsubscribe from state events when they are no longer in use, or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md).
|
||||||
|
|
||||||
|
📖 Refer to [architecture.md | Application State](./architecture.md#application-state) for an overview of event handling and [application.md | Application State](./presentation.md#application-state) for an in-depth understanding of state management in the application layer.
|
||||||
|
|
||||||
|
## Dependency injections
|
||||||
|
|
||||||
|
The presentation layer uses Vue's native dependency injection system to increase testability and decouple components.
|
||||||
|
|
||||||
|
To add a new dependency:
|
||||||
|
|
||||||
|
1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into:
|
||||||
|
- **Singletons**: Shared across components, instantiated once.
|
||||||
|
- **Transients**: Factories yielding a new instance on every access.
|
||||||
|
2. **Provide the dependency**: Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency. [`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
|
||||||
|
3. **Inject the dependency**: Use Vue's `inject` method alongside the defined symbol to incorporate the dependency into components.
|
||||||
|
- For singletons, invoke the factory method: `inject(symbolKey)()`.
|
||||||
|
- For transients, directly inject: `inject(symbolKey)`.
|
||||||
|
|
||||||
|
## Shared UI components
|
||||||
|
|
||||||
|
Shared UI components promote consistency and simplifies the creation of the front-end.
|
||||||
|
|
||||||
|
In order to maintain portability and easy maintainability, the preference is towards using homegrown components over third-party ones or comprehensive UI frameworks like Quasar.
|
||||||
|
|
||||||
|
Shared components include:
|
||||||
|
|
||||||
|
- [ModalDialog.vue](./../src/presentation/components/Shared/Modal/ModalDialog.vue) is utilized for rendering modal windows.
|
||||||
|
- [TooltipWrapper.vue](./../src/presentation/components/Shared/TooltipWrapper.vue) acts as a wrapper for rendering tooltips.
|
||||||
|
|
||||||
|
## Desktop builds
|
||||||
|
|
||||||
|
Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages.
|
||||||
|
|
||||||
|
## Sass naming convention
|
||||||
|
|
||||||
|
- Use lowercase for variables/functions/mixins, e.g.:
|
||||||
|
- Variable: `$variable: value;`
|
||||||
|
- Function: `@function function() {}`
|
||||||
|
- Mixin: `@mixin mixin() {}`
|
||||||
|
- Use - for a phrase/compound word, e.g.:
|
||||||
|
- Variable: `$some-variable: value;`
|
||||||
|
- Function: `@function some-function() {}`
|
||||||
|
- Mixin: `@mixin some-mixin() {}`
|
||||||
|
- Grouping and name variables from generic to specific, e.g.:
|
||||||
|
- ✅ `$border-blue`, `$border-blue-light`, `$border-blue-lightest`, `$border-red`
|
||||||
|
- ❌ `$blue-border`, `$light-blue-border`, `$lightest-blue-border`, `$red-border`
|
||||||
|
|
||||||
122
docs/templating.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# Templating
|
||||||
|
|
||||||
|
## Benefits of templating
|
||||||
|
|
||||||
|
- Generating scripts by sharing code to increase best-practice usage and maintainability.
|
||||||
|
- Creating self-contained scripts without cross-dependencies.
|
||||||
|
- Use of pipes for writing cleaner code and letting pipes do dirty work.
|
||||||
|
|
||||||
|
## Expressions
|
||||||
|
|
||||||
|
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
|
||||||
|
- E.g. `Hello {{ $name }} !`
|
||||||
|
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
|
||||||
|
- Functions enables usage of expressions.
|
||||||
|
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
||||||
|
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
||||||
|
- Expressions inside expressions (nested templates) are supported.
|
||||||
|
- An expression can output another expression that will also be compiled.
|
||||||
|
- E.g. following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output.
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $condition }}
|
||||||
|
echo {{ $text }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameter substitution
|
||||||
|
|
||||||
|
A simple function example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
- name: 'argument'
|
||||||
|
code: Hello {{ $argument }} !
|
||||||
|
```
|
||||||
|
|
||||||
|
It would print "Hello world" if it's called in a [script](./collection-files.md#script) as following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
script: Echo script
|
||||||
|
call:
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
argument: World
|
||||||
|
```
|
||||||
|
|
||||||
|
A function can call other functions such as:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
-
|
||||||
|
function: CallerFunction
|
||||||
|
parameters:
|
||||||
|
- name: 'value'
|
||||||
|
call:
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
argument: {{ $value }}
|
||||||
|
-
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
- name: 'argument'
|
||||||
|
code: Hello {{ $argument }} !
|
||||||
|
```
|
||||||
|
|
||||||
|
### with
|
||||||
|
|
||||||
|
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
|
||||||
|
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
|
||||||
|
|
||||||
|
It binds its context (value of the provided parameter value) as arbitrary `.` value. It allows you to use the argument value of the given parameter when it is provided and not empty such as:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
It supports multiline text inside the block. You can have something like:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $argument }}
|
||||||
|
First line
|
||||||
|
Second line
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $condition }}
|
||||||
|
This is a different parameter: {{ $text }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
function: FunctionThatOutputsConditionally
|
||||||
|
parameters:
|
||||||
|
- name: 'argument'
|
||||||
|
optional: true
|
||||||
|
code: |-
|
||||||
|
{{ with $argument }}
|
||||||
|
Value is: {{ . }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pipes
|
||||||
|
|
||||||
|
- Pipes are functions available for handling text.
|
||||||
|
- Allows stacking actions one after another also known as "chaining".
|
||||||
|
- Like [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), the concept is simple: each pipeline's output becomes the input of the following pipe.
|
||||||
|
- You cannot create pipes. [A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
|
||||||
|
- You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
|
||||||
|
- ❗ Pipe names must be camelCase without any space or special characters.
|
||||||
|
- **Existing pipes**
|
||||||
|
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
|
||||||
|
- `escapeDoubleQuotes`: Escapes `"` characters, allows you to use them inside double quotes (`"`).
|
||||||
|
- **Example usages**
|
||||||
|
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
|
||||||
|
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
|
||||||
@@ -1,29 +1,83 @@
|
|||||||
# Unit tests
|
# Tests
|
||||||
|
|
||||||
- Unit tests are defined in [`./tests`](./../tests)
|
There are different types of tests executed:
|
||||||
- They follow same folder structure as [`./src`](./../src)
|
|
||||||
|
|
||||||
## Naming
|
1. [Unit tests](#unit-tests)
|
||||||
|
2. [Integration tests](#integration-tests)
|
||||||
|
3. [End-to-end (E2E) tests](#e2e-tests)
|
||||||
|
4. [Automated checks](#automated-checks)
|
||||||
|
|
||||||
- Each test suite first describe the system under test
|
## Unit and integration tests
|
||||||
- E.g. tests for class `Application` is categorized under `Application`
|
|
||||||
- Tests for specific methods are categorized under method name (if applicable)
|
|
||||||
- E.g. test for `run()` is categorized under `run`
|
|
||||||
|
|
||||||
## Act, arrange, assert
|
- They utilize [Vitest](https://vitest.dev/).
|
||||||
|
- Test files are suffixed with `.spec.ts`.
|
||||||
|
|
||||||
- Tests use act, arrange and assert (AAA) pattern when applicable
|
### Act, arrange, assert
|
||||||
|
|
||||||
|
- Tests implement the act, arrange, and assert (AAA) pattern.
|
||||||
- **Arrange**
|
- **Arrange**
|
||||||
- Should set up the test case
|
- Sets up the test scenario and environment.
|
||||||
- Starts with comment line `// arrange`
|
- Begins with comment line `// arrange`.
|
||||||
- **Act**
|
- **Act**
|
||||||
- Should cover the main thing to be tested
|
- Executes the actual test.
|
||||||
- Starts with comment line `// act`
|
- Begins with comment line `// act`.
|
||||||
- **Assert**
|
- **Assert**
|
||||||
- Should elicit some sort of response
|
- Sets an expectation for the test's outcome.
|
||||||
- Starts with comment line `// assert`
|
- Begins with comment line `// assert`.
|
||||||
|
|
||||||
## Stubs
|
### Unit tests
|
||||||
|
|
||||||
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs)
|
- Evaluate individual components in isolation.
|
||||||
- They implement dummy behavior to be functional
|
- Located in [`./tests/unit`](./../tests/unit).
|
||||||
|
- Achieve isolation using [stubs](./../tests/unit/shared/Stubs).
|
||||||
|
- Include Vue component tests, enabled by `@vue/test-utils`.
|
||||||
|
|
||||||
|
#### Unit tests naming
|
||||||
|
|
||||||
|
- Test suites start with a description of the component or system under test.
|
||||||
|
- E.g., tests for `Application.ts` are contained in `Application.spec.ts`.
|
||||||
|
- Whenever possible, `describe` blocks group tests of the same function.
|
||||||
|
- E.g., tests for `run()` are inside `describe('run', () => ...)`.
|
||||||
|
|
||||||
|
### Integration tests
|
||||||
|
|
||||||
|
- Assess the combined functionality of components.
|
||||||
|
- They verify that third-party dependencies function as anticipated.
|
||||||
|
|
||||||
|
## E2E tests
|
||||||
|
|
||||||
|
- Examine the live web application's functionality and performance.
|
||||||
|
- Uses Cypress to run the tests.
|
||||||
|
|
||||||
|
## Automated checks
|
||||||
|
|
||||||
|
These checks validate various qualities like runtime execution, building process, security testing, etc.
|
||||||
|
|
||||||
|
- Use [various tools](./../package.json) and [scripts](./../scripts).
|
||||||
|
- Are automatically executed as [GitHub workflows](./../.github/workflows).
|
||||||
|
|
||||||
|
## Tests structure
|
||||||
|
|
||||||
|
- [`package.json`](./../package.json): Defines test commands and includes tools used in tests.
|
||||||
|
- [`vite.config.ts`](./../vite.config.ts): Configures `vitest` for unit and integration tests.
|
||||||
|
- [`./src/`](./../src/): Contains the code subject to testing.
|
||||||
|
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
|
||||||
|
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
|
||||||
|
- [`./tests/unit/`](./../tests/unit/)
|
||||||
|
- Stores unit test code.
|
||||||
|
- The directory structure mirrors [`./src/`](./../src).
|
||||||
|
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
||||||
|
- [`shared/`](./../tests/unit/shared/)
|
||||||
|
- Contains shared unit test functionalities.
|
||||||
|
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
|
||||||
|
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
||||||
|
- Shared test cases.
|
||||||
|
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
|
||||||
|
- [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities.
|
||||||
|
- [`./tests/integration/`](./../tests/integration/): Contains integration test files.
|
||||||
|
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
|
||||||
|
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension.
|
||||||
|
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single spec file.
|
||||||
|
- [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation.
|
||||||
|
- *(git ignored)* `/videos`: Asset folder for videos taken during tests.
|
||||||
|
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
||||||
|
|||||||
31
electron-builder.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# -------
|
||||||
|
# Windows
|
||||||
|
# -------
|
||||||
|
win:
|
||||||
|
target: nsis
|
||||||
|
nsis:
|
||||||
|
artifactName: ${name}-${version}-Setup.${ext}
|
||||||
|
|
||||||
|
# -----
|
||||||
|
# Linux
|
||||||
|
# -----
|
||||||
|
linux:
|
||||||
|
target: AppImage
|
||||||
|
appImage:
|
||||||
|
artifactName: ${name}-${version}.${ext}
|
||||||
|
|
||||||
|
# -----
|
||||||
|
# macOS
|
||||||
|
# -----
|
||||||
|
mac:
|
||||||
|
target: dmg
|
||||||
|
dmg:
|
||||||
|
artifactName: ${name}-${version}.${ext}
|
||||||
|
|
||||||
|
# ----------------
|
||||||
|
# Publish options
|
||||||
|
# ----------------
|
||||||
|
publish:
|
||||||
|
provider: 'github'
|
||||||
|
vPrefixedTagName: false # default: true
|
||||||
|
releaseType: release # default: draft
|
||||||
68
electron.vite.config.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { resolve } from 'path';
|
||||||
|
import { mergeConfig, UserConfig } from 'vite';
|
||||||
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||||
|
import { getAliasesFromTsConfig, getClientEnvironmentVariables } from './vite-config-helper';
|
||||||
|
import { createVueConfig } from './vite.config';
|
||||||
|
|
||||||
|
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
|
||||||
|
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
|
||||||
|
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
|
||||||
|
const DIST_DIR = resolvePathFromProjectRoot('dist_electron/');
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
main: getSharedElectronConfig({
|
||||||
|
distDirSubfolder: 'main',
|
||||||
|
entryFilePath: MAIN_ENTRY_FILE,
|
||||||
|
}),
|
||||||
|
preload: getSharedElectronConfig({
|
||||||
|
distDirSubfolder: 'preload',
|
||||||
|
entryFilePath: PRELOAD_ENTRY_FILE,
|
||||||
|
}),
|
||||||
|
renderer: mergeConfig(
|
||||||
|
createVueConfig({
|
||||||
|
supportLegacyBrowsers: false,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
build: {
|
||||||
|
outDir: resolve(DIST_DIR, 'renderer'),
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: WEB_INDEX_HTML_PATH,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
function getSharedElectronConfig(options: {
|
||||||
|
readonly distDirSubfolder: string;
|
||||||
|
readonly entryFilePath: string;
|
||||||
|
}): UserConfig {
|
||||||
|
return {
|
||||||
|
build: {
|
||||||
|
outDir: resolve(DIST_DIR, options.distDirSubfolder),
|
||||||
|
lib: {
|
||||||
|
entry: options.entryFilePath,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: '[name].cjs', // This is needed so `type="module"` works
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
define: {
|
||||||
|
...getClientEnvironmentVariables(),
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
...getAliasesFromTsConfig(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePathFromProjectRoot(pathSegment: string) {
|
||||||
|
return resolve(__dirname, pathSegment);
|
||||||
|
}
|
||||||
9
img/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# img
|
||||||
|
|
||||||
|
This folder contains image files and other resources related to images.
|
||||||
|
|
||||||
|
## logo.svg
|
||||||
|
|
||||||
|
[`logo.svg`](./logo.svg) serves as the primary logo from which all other icons and images are derived.
|
||||||
|
Only modify this file manually.
|
||||||
|
After making changes, execute `npm run build:icons` to regenerate logo files in various formats.
|
||||||
1
img/architecture/app-state.drawio
Normal file
BIN
img/architecture/app-state.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 483 KiB After Width: | Height: | Size: 255 KiB |
56
img/logo.svg
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="90.2998mm" height="90.2998mm"
|
||||||
|
viewBox="0 0 256 256">
|
||||||
|
<path id="logo"
|
||||||
|
fill="#3a65ab" stroke="#3a65ab" stroke-width="1"
|
||||||
|
d="M 128.00,173.00
|
||||||
|
C 128.00,173.00 102.00,175.00 102.00,175.00
|
||||||
|
85.39,174.97 64.02,170.31 49.00,163.22
|
||||||
|
38.46,158.24 28.39,152.17 20.01,143.96
|
||||||
|
14.88,138.93 10.72,133.32 7.31,127.00
|
||||||
|
3.36,119.66 1.10,112.37 1.00,104.00
|
||||||
|
1.00,104.00 1.00,98.00 1.00,98.00
|
||||||
|
1.29,73.92 24.76,53.44 44.00,42.43
|
||||||
|
84.66,19.15 129.75,23.12 169.00,47.00
|
||||||
|
188.74,59.02 207.93,76.23 208.00,101.00
|
||||||
|
208.00,101.00 188.00,101.00 188.00,101.00
|
||||||
|
186.46,86.48 168.72,71.84 157.00,64.61
|
||||||
|
140.76,54.59 121.33,46.78 102.00,47.00
|
||||||
|
88.42,47.16 72.20,52.52 60.00,58.32
|
||||||
|
45.30,65.30 19.83,84.81 21.10,103.00
|
||||||
|
21.39,107.16 22.92,110.33 24.76,114.00
|
||||||
|
32.70,129.78 48.16,140.02 64.00,146.72
|
||||||
|
75.16,151.44 92.90,155.26 105.00,154.99
|
||||||
|
113.45,154.79 121.81,152.84 130.00,151.00
|
||||||
|
130.00,151.00 128.00,173.00 128.00,173.00 Z
|
||||||
|
M 136.00,79.00
|
||||||
|
C 142.71,81.35 144.84,93.60 144.99,100.00
|
||||||
|
145.51,122.74 130.31,140.73 107.00,141.00
|
||||||
|
83.63,141.26 67.43,126.52 66.09,103.00
|
||||||
|
64.82,80.73 85.85,58.90 104.00,64.00
|
||||||
|
100.18,69.73 95.45,74.53 96.20,82.00
|
||||||
|
97.29,92.87 110.06,102.98 121.00,99.03
|
||||||
|
129.92,95.81 134.61,87.96 136.00,79.00 Z
|
||||||
|
M 186.00,113.46
|
||||||
|
C 206.11,110.69 225.57,114.92 239.91,130.01
|
||||||
|
252.85,143.63 255.21,157.09 255.00,175.00
|
||||||
|
254.76,195.49 241.26,214.25 223.00,222.88
|
||||||
|
213.06,227.58 204.72,228.12 194.00,228.00
|
||||||
|
150.34,227.49 126.71,178.85 146.32,142.00
|
||||||
|
154.93,125.82 168.55,117.23 186.00,113.46 Z
|
||||||
|
M 233.00,181.00
|
||||||
|
C 242.24,158.78 221.84,133.54 199.00,133.01
|
||||||
|
188.40,132.77 182.75,135.31 174.00,141.00
|
||||||
|
178.60,146.85 195.92,157.24 203.00,161.86
|
||||||
|
209.82,166.32 226.61,178.55 233.00,181.00 Z
|
||||||
|
M 221.00,200.00
|
||||||
|
C 216.39,194.15 206.42,188.61 200.00,184.33
|
||||||
|
192.31,179.21 168.77,162.59 162.00,160.00
|
||||||
|
159.67,165.03 159.94,166.57 160.00,172.00
|
||||||
|
160.23,190.99 177.11,207.55 196.00,207.99
|
||||||
|
206.60,208.23 212.25,205.69 221.00,200.00 Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 98 KiB |
46116
package-lock.json
generated
134
package.json
@@ -1,70 +1,106 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.9.2",
|
"version": "0.12.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
"slogan": "Now you have the choice",
|
||||||
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"dev": "vite",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"preview": "vite preview",
|
||||||
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
|
"test:unit": "vitest run --dir tests/unit",
|
||||||
"electron:build": "vue-cli-service electron:build",
|
"test:integration": "vitest run --dir tests/integration",
|
||||||
"electron:serve": "vue-cli-service electron:serve",
|
"test:e2e": "vue-cli-service test:e2e",
|
||||||
|
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
|
||||||
|
"test:cy: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",
|
||||||
|
"icons:build": "node scripts/logo-update.js",
|
||||||
|
"electron:dev": "electron-vite dev",
|
||||||
|
"electron:preview": "electron-vite preview",
|
||||||
|
"electron:prebuild": "electron-vite build",
|
||||||
|
"electron:build": "electron-builder",
|
||||||
|
"lint:eslint": "eslint .",
|
||||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||||
"lint:vue": "vue-cli-service lint --no-fix",
|
|
||||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "./dist_electron/main/index.cjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.15.1",
|
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.15.1",
|
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/vue-fontawesome": "^2.0.2",
|
"@fortawesome/vue-fontawesome": "^2.0.9",
|
||||||
"ace-builds": "^1.4.12",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"core-js": "^3.6.5",
|
"ace-builds": "^1.23.4",
|
||||||
|
"cross-fetch": "^4.0.0",
|
||||||
|
"electron-progressbar": "^2.1.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"inversify": "^5.0.5",
|
"install": "^0.13.0",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"markdown-it": "^13.0.1",
|
||||||
"v-tooltip": "2.0.2",
|
"npm": "^9.8.1",
|
||||||
"vue": "^2.6.12",
|
"v-tooltip": "2.1.3",
|
||||||
"vue-class-component": "^7.2.6",
|
"vue": "^2.7.14"
|
||||||
"vue-js-modal": "^2.0.0-rc.6",
|
|
||||||
"vue-property-decorator": "^9.1.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ace": "0.0.44",
|
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||||
"@types/chai": "^4.2.14",
|
"@rushstack/eslint-patch": "^1.3.2",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/ace": "^0.0.48",
|
||||||
"@types/mocha": "^8.2.0",
|
"@types/file-saver": "^2.0.5",
|
||||||
"@vue/cli-plugin-babel": "^4.5.10",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@vue/cli-plugin-typescript": "^4.5.9",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vue/cli-plugin-unit-mocha": "^4.5.9",
|
"@vitejs/plugin-legacy": "^4.1.1",
|
||||||
"@vue/cli-service": "^4.5.9",
|
"@vitejs/plugin-vue2": "^2.2.0",
|
||||||
"@vue/test-utils": "1.1.2",
|
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
||||||
"chai": "^4.2.0",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"electron": "^11.1.0",
|
"@vue/test-utils": "^1.3.6",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"autoprefixer": "^10.4.15",
|
||||||
"electron-log": "^4.3.1",
|
"cypress": "^12.17.2",
|
||||||
"electron-updater": "^4.3.5",
|
"electron": "^25.3.2",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"electron-builder": "^24.6.3",
|
||||||
"markdownlint-cli": "^0.26.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"remark-cli": "^9.0.0",
|
"electron-icon-builder": "^2.0.1",
|
||||||
|
"electron-log": "^4.4.8",
|
||||||
|
"electron-updater": "^6.1.4",
|
||||||
|
"electron-vite": "^1.0.27",
|
||||||
|
"eslint": "^8.46.0",
|
||||||
|
"eslint-plugin-cypress": "^2.14.0",
|
||||||
|
"eslint-plugin-vue": "^9.6.0",
|
||||||
|
"eslint-plugin-vuejs-accessibility": "^1.2.0",
|
||||||
|
"icon-gen": "^3.0.1",
|
||||||
|
"jsdom": "^22.1.0",
|
||||||
|
"markdownlint-cli": "^0.35.0",
|
||||||
|
"postcss": "^8.4.28",
|
||||||
|
"remark-cli": "^11.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^4.0.0",
|
"remark-preset-lint-consistent": "^5.1.2",
|
||||||
"remark-validate-links": "^10.0.2",
|
"remark-validate-links": "^12.1.1",
|
||||||
"sass": "^1.30.0",
|
"sass": "^1.64.1",
|
||||||
"sass-loader": "^10.1.0",
|
"start-server-and-test": "^2.0.0",
|
||||||
"typescript": "^4.1.3",
|
"svgexport": "^0.4.2",
|
||||||
"vue-cli-plugin-electron-builder": "^2.0.0-rc.5",
|
"terser": "^5.19.2",
|
||||||
"vue-template-compiler": "^2.6.12",
|
"tslib": "~2.4.0",
|
||||||
"yaml-lint": "^1.2.4"
|
"typescript": "~4.6.2",
|
||||||
|
"vite": "^4.4.9",
|
||||||
|
"vitest": "^0.34.2",
|
||||||
|
"vue-tsc": "^1.8.8",
|
||||||
|
"yaml-lint": "^1.7.0"
|
||||||
|
},
|
||||||
|
"//devDependencies": {
|
||||||
|
"terser": "Used by @vitejs/plugin-legacy for minification",
|
||||||
|
"typescript": [
|
||||||
|
"Cannot upgrade to 5.X.X due to unmaintained @vue/cli-plugin-typescript, https://github.com/vuejs/vue-cli/issues/7401",
|
||||||
|
"Cannot upgrade to > 4.6.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252"
|
||||||
|
],
|
||||||
|
"tslib": "Cannot upgrade to > 2.4.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252",
|
||||||
|
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60",
|
||||||
|
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60"
|
||||||
},
|
},
|
||||||
"homepage": "https://privacy.sexy",
|
"homepage": "https://privacy.sexy",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
9
postcss.config.cjs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const autoprefixer = require('autoprefixer');
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
return {
|
||||||
|
plugins: [
|
||||||
|
autoprefixer(),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
autoprefixer: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 106 KiB |
BIN
public/icon.png
|
Before Width: | Height: | Size: 14 KiB |
10
scripts/check-desktop-runtime-errors/.eslintrc.cjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
require('@rushstack/eslint-patch/modern-module-resolution.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'import/extensions': ['error', 'always'],
|
||||||
|
},
|
||||||
|
};
|
||||||
35
scripts/check-desktop-runtime-errors/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# check-desktop-runtime-errors
|
||||||
|
|
||||||
|
This script automates the processes of:
|
||||||
|
|
||||||
|
1) Building
|
||||||
|
2) Packaging
|
||||||
|
3) Installing
|
||||||
|
4) Executing
|
||||||
|
5) Verifying Electron distributions
|
||||||
|
|
||||||
|
It runs the application for a duration and detects runtime errors in the packaged application via:
|
||||||
|
|
||||||
|
- **Log verification**: Checking application logs for errors and validating successful application initialization.
|
||||||
|
- **`stderr` monitoring**: Continuous listening to the `stderr` stream for unexpected errors.
|
||||||
|
- **Window title inspection**: Checking for window titles that indicate crashes before logging becomes possible.
|
||||||
|
|
||||||
|
Upon error, the script captures a screenshot (if `--screenshot` is provided) and terminates.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node ./scripts/check-desktop-runtime-errors
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
- `--build`: Clears the electron distribution directory and forces a rebuild of the Electron app.
|
||||||
|
- `--screenshot`: Takes a screenshot of the desktop environment after running the application.
|
||||||
|
|
||||||
|
This module provides utilities for building, executing, and validating Electron desktop apps.
|
||||||
|
It can be used to automate checking for runtime errors during development.
|
||||||
|
|
||||||
|
## Configs
|
||||||
|
|
||||||
|
Configurations are defined in [`config.js`](./config.js).
|
||||||
55
scripts/check-desktop-runtime-errors/app/app-logs.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { unlink, readFile } from 'fs/promises';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { log, die, LOG_LEVELS } from '../utils/log.js';
|
||||||
|
import { exists } from '../utils/io.js';
|
||||||
|
import { SUPPORTED_PLATFORMS, CURRENT_PLATFORM } from '../utils/platform.js';
|
||||||
|
import { getAppName } from '../utils/npm.js';
|
||||||
|
|
||||||
|
export async function clearAppLogFile(projectDir) {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
const logPath = await determineLogPath(projectDir);
|
||||||
|
if (!logPath || !await exists(logPath)) {
|
||||||
|
log(`Skipping clearing logs, log file does not exist: ${logPath}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await unlink(logPath);
|
||||||
|
log(`Successfully cleared the log file at: ${logPath}.`);
|
||||||
|
} catch (error) {
|
||||||
|
die(`Failed to clear the log file at: ${logPath}. Reason: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readAppLogFile(projectDir) {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
const logPath = await determineLogPath(projectDir);
|
||||||
|
if (!logPath || !await exists(logPath)) {
|
||||||
|
log(`No log file at: ${logPath}`, LOG_LEVELS.WARN);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const logContent = await readLogFile(logPath);
|
||||||
|
return logContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function determineLogPath(projectDir) {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
const appName = await getAppName(projectDir);
|
||||||
|
if (!appName) {
|
||||||
|
die('App name not found.');
|
||||||
|
}
|
||||||
|
const logFilePaths = {
|
||||||
|
[SUPPORTED_PLATFORMS.MAC]: () => join(process.env.HOME, 'Library', 'Logs', appName, 'main.log'),
|
||||||
|
[SUPPORTED_PLATFORMS.LINUX]: () => join(process.env.HOME, '.config', appName, 'logs', 'main.log'),
|
||||||
|
[SUPPORTED_PLATFORMS.WINDOWS]: () => join(process.env.USERPROFILE, 'AppData', 'Roaming', appName, 'logs', 'main.log'),
|
||||||
|
};
|
||||||
|
const logFilePath = logFilePaths[CURRENT_PLATFORM]?.();
|
||||||
|
if (!logFilePath) {
|
||||||
|
log(`Cannot determine log path, unsupported OS: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
|
||||||
|
}
|
||||||
|
return logFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readLogFile(logFilePath) {
|
||||||
|
const content = await readFile(logFilePath, 'utf-8');
|
||||||
|
return content?.trim().length > 0 ? content : undefined;
|
||||||
|
}
|
||||||
126
scripts/check-desktop-runtime-errors/app/check-for-errors.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { splitTextIntoLines, indentText } from '../utils/text.js';
|
||||||
|
import { die } from '../utils/log.js';
|
||||||
|
import { readAppLogFile } from './app-logs.js';
|
||||||
|
|
||||||
|
const ELECTRON_CRASH_TITLE = 'Error'; // Used by electron for early crashes
|
||||||
|
const LOG_ERROR_MARKER = '[error]'; // from electron-log
|
||||||
|
const EXPECTED_LOG_MARKERS = [
|
||||||
|
'[WINDOW_INIT]',
|
||||||
|
'[PRELOAD_INIT]',
|
||||||
|
'[APP_MOUNT_INIT]',
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function checkForErrors(stderr, windowTitles, projectDir) {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
const errors = await gatherErrors(stderr, windowTitles, projectDir);
|
||||||
|
if (errors.length) {
|
||||||
|
die(formatErrors(errors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gatherErrors(stderr, windowTitles, projectDir) {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
const logContent = await readAppLogFile(projectDir);
|
||||||
|
return [
|
||||||
|
verifyStdErr(stderr),
|
||||||
|
verifyApplicationLogsExist(logContent),
|
||||||
|
...EXPECTED_LOG_MARKERS.map((marker) => verifyLogMarkerExistsInLogs(logContent, marker)),
|
||||||
|
verifyWindowTitle(windowTitles),
|
||||||
|
verifyErrorsInLogs(logContent),
|
||||||
|
].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatErrors(errors) {
|
||||||
|
if (!errors || !errors.length) { throw new Error('missing errors'); }
|
||||||
|
return [
|
||||||
|
'Errors detected during execution:',
|
||||||
|
...errors.map(
|
||||||
|
(error) => formatError(error),
|
||||||
|
),
|
||||||
|
].join('\n---\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatError(error) {
|
||||||
|
if (!error) { throw new Error('missing error'); }
|
||||||
|
if (!error.reason) { throw new Error(`missing reason, error (${typeof error}): ${JSON.stringify(error)}`); }
|
||||||
|
let message = `Reason: ${indentText(error.reason, 1)}`;
|
||||||
|
if (error.description) {
|
||||||
|
message += `\nDescription:\n${indentText(error.description, 2)}`;
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyApplicationLogsExist(logContent) {
|
||||||
|
if (!logContent || !logContent.length) {
|
||||||
|
return describeError(
|
||||||
|
'Missing application logs',
|
||||||
|
'Application logs are empty not were not found.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyLogMarkerExistsInLogs(logContent, marker) {
|
||||||
|
if (!marker) {
|
||||||
|
throw new Error('missing marker');
|
||||||
|
}
|
||||||
|
if (!logContent?.includes(marker)) {
|
||||||
|
return describeError(
|
||||||
|
'Incomplete application logs',
|
||||||
|
`Missing identifier "${marker}" in application logs.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyWindowTitle(windowTitles) {
|
||||||
|
const errorTitles = windowTitles.filter(
|
||||||
|
(title) => title.toLowerCase().includes(ELECTRON_CRASH_TITLE),
|
||||||
|
);
|
||||||
|
if (errorTitles.length) {
|
||||||
|
return describeError(
|
||||||
|
'Unexpected window title',
|
||||||
|
'One or more window titles suggest an error occurred in the application:'
|
||||||
|
+ `\nError Titles: ${errorTitles.join(', ')}`
|
||||||
|
+ `\nAll Titles: ${windowTitles.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyStdErr(stderrOutput) {
|
||||||
|
if (stderrOutput && stderrOutput.length > 0) {
|
||||||
|
return describeError(
|
||||||
|
'Standard error stream (`stderr`) is not empty.',
|
||||||
|
stderrOutput,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyErrorsInLogs(logContent) {
|
||||||
|
if (!logContent || !logContent.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const logLines = getNonEmptyLines(logContent)
|
||||||
|
.filter((line) => line.includes(LOG_ERROR_MARKER));
|
||||||
|
if (!logLines.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return describeError(
|
||||||
|
'Application log file',
|
||||||
|
logLines.join('\n'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeError(reason, description) {
|
||||||
|
return {
|
||||||
|
reason,
|
||||||
|
description: `${description}\n\nThis might indicate an early crash or significant runtime issue.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNonEmptyLines(text) {
|
||||||
|
return splitTextIntoLines(text)
|
||||||
|
.filter((line) => line?.trim().length > 0);
|
||||||
|
}
|
||||||
34
scripts/check-desktop-runtime-errors/app/extractors/linux.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { access, chmod } from 'fs/promises';
|
||||||
|
import { constants } from 'fs';
|
||||||
|
import { findSingleFileByExtension } from '../../utils/io.js';
|
||||||
|
import { log } from '../../utils/log.js';
|
||||||
|
|
||||||
|
export async function prepareLinuxApp(desktopDistPath) {
|
||||||
|
const { absolutePath: appFile } = await findSingleFileByExtension(
|
||||||
|
'AppImage',
|
||||||
|
desktopDistPath,
|
||||||
|
);
|
||||||
|
await makeExecutable(appFile);
|
||||||
|
return {
|
||||||
|
appExecutablePath: appFile,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeExecutable(appFile) {
|
||||||
|
if (!appFile) { throw new Error('missing file'); }
|
||||||
|
if (await isExecutable(appFile)) {
|
||||||
|
log('AppImage is already executable.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log('Making it executable...');
|
||||||
|
await chmod(appFile, 0o755);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isExecutable(file) {
|
||||||
|
try {
|
||||||
|
await access(file, constants.X_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
66
scripts/check-desktop-runtime-errors/app/extractors/macos.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { runCommand } from '../../utils/run-command.js';
|
||||||
|
import { findSingleFileByExtension, exists } from '../../utils/io.js';
|
||||||
|
import { log, die, LOG_LEVELS } from '../../utils/log.js';
|
||||||
|
|
||||||
|
export async function prepareMacOsApp(desktopDistPath) {
|
||||||
|
const { absolutePath: dmgPath } = await findSingleFileByExtension('dmg', desktopDistPath);
|
||||||
|
const { mountPath } = await mountDmg(dmgPath);
|
||||||
|
const appPath = await findMacAppExecutablePath(mountPath);
|
||||||
|
return {
|
||||||
|
appExecutablePath: appPath,
|
||||||
|
cleanup: async () => {
|
||||||
|
log('Cleaning up resources...');
|
||||||
|
await detachMount(mountPath);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mountDmg(dmgFile) {
|
||||||
|
const { stdout: hdiutilOutput, error } = await runCommand(`hdiutil attach '${dmgFile}'`);
|
||||||
|
if (error) {
|
||||||
|
die(`Failed to mount DMG file at ${dmgFile}.\n${error}`);
|
||||||
|
}
|
||||||
|
const mountPathMatch = hdiutilOutput.match(/\/Volumes\/[^\n]+/);
|
||||||
|
const mountPath = mountPathMatch ? mountPathMatch[0] : null;
|
||||||
|
return {
|
||||||
|
mountPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findMacAppExecutablePath(mountPath) {
|
||||||
|
const { stdout: findOutput, error } = await runCommand(
|
||||||
|
`find '${mountPath}' -maxdepth 1 -type d -name "*.app"`,
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
die(`Failed to find executable path at mount path ${mountPath}\n${error}`);
|
||||||
|
}
|
||||||
|
const appFolder = findOutput.trim();
|
||||||
|
const appName = appFolder.split('/').pop().replace('.app', '');
|
||||||
|
const appPath = `${appFolder}/Contents/MacOS/${appName}`;
|
||||||
|
if (await exists(appPath)) {
|
||||||
|
log(`Application is located at ${appPath}`);
|
||||||
|
} else {
|
||||||
|
die(`Application does not exist at ${appPath}`);
|
||||||
|
}
|
||||||
|
return appPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detachMount(mountPath, retries = 5) {
|
||||||
|
const { error } = await runCommand(`hdiutil detach '${mountPath}'`);
|
||||||
|
if (error) {
|
||||||
|
if (retries <= 0) {
|
||||||
|
log(`Failed to detach mount after multiple attempts: ${mountPath}\n${error}`, LOG_LEVELS.WARN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sleep(500);
|
||||||
|
await detachMount(mountPath, retries - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log(`Successfully detached from ${mountPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(milliseconds) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, milliseconds);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { mkdtemp, rmdir } from 'fs/promises';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { findSingleFileByExtension, exists } from '../../utils/io.js';
|
||||||
|
import { log, die } from '../../utils/log.js';
|
||||||
|
import { runCommand } from '../../utils/run-command.js';
|
||||||
|
|
||||||
|
export async function prepareWindowsApp(desktopDistPath) {
|
||||||
|
const workdir = await mkdtemp(join(tmpdir(), 'win-nsis-installation-'));
|
||||||
|
if (await exists(workdir)) {
|
||||||
|
log(`Temporary directory ${workdir} already exists, cleaning up...`);
|
||||||
|
await rmdir(workdir, { recursive: true });
|
||||||
|
}
|
||||||
|
const { appExecutablePath } = await installNsis(workdir, desktopDistPath);
|
||||||
|
return {
|
||||||
|
appExecutablePath,
|
||||||
|
cleanup: async () => {
|
||||||
|
log(`Cleaning up working directory ${workdir}...`);
|
||||||
|
await rmdir(workdir, { recursive: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installNsis(installationPath, desktopDistPath) {
|
||||||
|
const { absolutePath: installerPath } = await findSingleFileByExtension('exe', desktopDistPath);
|
||||||
|
|
||||||
|
log(`Silently installing contents of ${installerPath} to ${installationPath}...`);
|
||||||
|
const { error } = await runCommand(`"${installerPath}" /S /D=${installationPath}`);
|
||||||
|
if (error) {
|
||||||
|
die(`Failed to install.\n${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { absolutePath: appExecutablePath } = await findSingleFileByExtension('exe', installationPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
appExecutablePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
164
scripts/check-desktop-runtime-errors/app/runner.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { log, LOG_LEVELS, die } from '../utils/log.js';
|
||||||
|
import { captureScreen } from './system-capture/screen-capture.js';
|
||||||
|
import { captureWindowTitles } from './system-capture/window-title-capture.js';
|
||||||
|
|
||||||
|
const TERMINATION_GRACE_PERIOD_IN_SECONDS = 60;
|
||||||
|
const TERMINATION_CHECK_INTERVAL_IN_MS = 1000;
|
||||||
|
const WINDOW_TITLE_CAPTURE_INTERVAL_IN_MS = 100;
|
||||||
|
|
||||||
|
export function runApplication(
|
||||||
|
appFile,
|
||||||
|
executionDurationInSeconds,
|
||||||
|
enableScreenshot,
|
||||||
|
screenshotPath,
|
||||||
|
) {
|
||||||
|
if (!appFile) {
|
||||||
|
throw new Error('Missing app file');
|
||||||
|
}
|
||||||
|
|
||||||
|
logDetails(appFile, executionDurationInSeconds);
|
||||||
|
|
||||||
|
const processDetails = {
|
||||||
|
stderrData: '',
|
||||||
|
stdoutData: '',
|
||||||
|
explicitlyKilled: false,
|
||||||
|
windowTitles: [],
|
||||||
|
isCrashed: false,
|
||||||
|
isDone: false,
|
||||||
|
process: undefined,
|
||||||
|
resolve: () => { /* NOOP */ },
|
||||||
|
};
|
||||||
|
|
||||||
|
const process = spawn(appFile);
|
||||||
|
processDetails.process = process;
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
processDetails.resolve = resolve;
|
||||||
|
handleTitleCapture(process.pid, processDetails);
|
||||||
|
handleProcessEvents(
|
||||||
|
processDetails,
|
||||||
|
enableScreenshot,
|
||||||
|
screenshotPath,
|
||||||
|
executionDurationInSeconds,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function logDetails(appFile, executionDurationInSeconds) {
|
||||||
|
log(
|
||||||
|
[
|
||||||
|
'Executing the app to check for errors...',
|
||||||
|
`Maximum execution time: ${executionDurationInSeconds}`,
|
||||||
|
`Application path: ${appFile}`,
|
||||||
|
].join('\n\t'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTitleCapture(processId, processDetails) {
|
||||||
|
const capture = async () => {
|
||||||
|
const titles = await captureWindowTitles(processId);
|
||||||
|
|
||||||
|
(titles || []).forEach((title) => {
|
||||||
|
if (!title || !title.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!processDetails.windowTitles.includes(title)) {
|
||||||
|
log(`New window title captured: ${title}`);
|
||||||
|
processDetails.windowTitles.push(title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!processDetails.isDone) {
|
||||||
|
setTimeout(capture, WINDOW_TITLE_CAPTURE_INTERVAL_IN_MS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
capture();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProcessEvents(
|
||||||
|
processDetails,
|
||||||
|
enableScreenshot,
|
||||||
|
screenshotPath,
|
||||||
|
executionDurationInSeconds,
|
||||||
|
) {
|
||||||
|
const { process } = processDetails;
|
||||||
|
process.stderr.on('data', (data) => {
|
||||||
|
processDetails.stderrData += data.toString();
|
||||||
|
});
|
||||||
|
process.stdout.on('data', (data) => {
|
||||||
|
processDetails.stdoutData += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('error', (error) => {
|
||||||
|
die(`An issue spawning the child process: ${error}`, LOG_LEVELS.ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('exit', async (code) => {
|
||||||
|
await onProcessExit(code, processDetails, enableScreenshot, screenshotPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
await onExecutionLimitReached(process, processDetails, enableScreenshot, screenshotPath);
|
||||||
|
}, executionDurationInSeconds * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onProcessExit(code, processDetails, enableScreenshot, screenshotPath) {
|
||||||
|
log(`Application exited ${code === null || Number.isNaN(code) ? '.' : `with code ${code}`}`);
|
||||||
|
|
||||||
|
if (processDetails.explicitlyKilled) return;
|
||||||
|
|
||||||
|
processDetails.isCrashed = true;
|
||||||
|
|
||||||
|
if (enableScreenshot) {
|
||||||
|
await captureScreen(screenshotPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
finishProcess(processDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onExecutionLimitReached(process, processDetails, enableScreenshot, screenshotPath) {
|
||||||
|
if (enableScreenshot) {
|
||||||
|
await captureScreen(screenshotPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
processDetails.explicitlyKilled = true;
|
||||||
|
await terminateGracefully(process);
|
||||||
|
finishProcess(processDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishProcess(processDetails) {
|
||||||
|
processDetails.isDone = true;
|
||||||
|
processDetails.resolve({
|
||||||
|
stderr: processDetails.stderrData,
|
||||||
|
stdout: processDetails.stdoutData,
|
||||||
|
windowTitles: [...processDetails.windowTitles],
|
||||||
|
isCrashed: processDetails.isCrashed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function terminateGracefully(process) {
|
||||||
|
let elapsedSeconds = 0;
|
||||||
|
log('Attempting to terminate the process gracefully...');
|
||||||
|
process.kill('SIGTERM');
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
elapsedSeconds += TERMINATION_CHECK_INTERVAL_IN_MS / 1000;
|
||||||
|
|
||||||
|
if (!process.killed) {
|
||||||
|
if (elapsedSeconds >= TERMINATION_GRACE_PERIOD_IN_SECONDS) {
|
||||||
|
process.kill('SIGKILL');
|
||||||
|
log('Process did not terminate gracefully within the grace period. Forcing termination.', LOG_LEVELS.WARN);
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('Process terminated gracefully.');
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, TERMINATION_CHECK_INTERVAL_IN_MS);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { unlink } from 'fs/promises';
|
||||||
|
import { runCommand } from '../../utils/run-command.js';
|
||||||
|
import { log, LOG_LEVELS } from '../../utils/log.js';
|
||||||
|
import { CURRENT_PLATFORM, SUPPORTED_PLATFORMS } from '../../utils/platform.js';
|
||||||
|
import { exists } from '../../utils/io.js';
|
||||||
|
|
||||||
|
export async function captureScreen(imagePath) {
|
||||||
|
if (!imagePath) {
|
||||||
|
throw new Error('Path for screenshot not provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await exists(imagePath)) {
|
||||||
|
log(`Screenshot file already exists at ${imagePath}. It will be overwritten.`, LOG_LEVELS.WARN);
|
||||||
|
unlink(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformCommands = {
|
||||||
|
[SUPPORTED_PLATFORMS.MAC]: `screencapture -x ${imagePath}`,
|
||||||
|
[SUPPORTED_PLATFORMS.LINUX]: `import -window root ${imagePath}`,
|
||||||
|
[SUPPORTED_PLATFORMS.WINDOWS]: `powershell -NoProfile -EncodedCommand ${encodeForPowershell(getScreenshotPowershellScript(imagePath))}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandForPlatform = platformCommands[CURRENT_PLATFORM];
|
||||||
|
|
||||||
|
if (!commandForPlatform) {
|
||||||
|
log(`Screenshot capture not supported on: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Capturing screenshot to ${imagePath} using command:\n\t> ${commandForPlatform}`);
|
||||||
|
|
||||||
|
const { error } = await runCommand(commandForPlatform);
|
||||||
|
if (error) {
|
||||||
|
log(`Failed to capture screenshot.\n${error}`, LOG_LEVELS.WARN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log(`Captured screenshot to ${imagePath}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScreenshotPowershellScript(imagePath) {
|
||||||
|
return `
|
||||||
|
$ProgressPreference = 'SilentlyContinue' # Do not pollute stderr
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
$screenBounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
||||||
|
|
||||||
|
$bmp = New-Object System.Drawing.Bitmap $screenBounds.Width, $screenBounds.Height
|
||||||
|
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
||||||
|
$graphics.CopyFromScreen([System.Drawing.Point]::Empty, [System.Drawing.Point]::Empty, $screenBounds.Size)
|
||||||
|
|
||||||
|
$bmp.Save('${imagePath}')
|
||||||
|
$graphics.Dispose()
|
||||||
|
$bmp.Dispose()
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeForPowershell(script) {
|
||||||
|
const buffer = Buffer.from(script, 'utf-16le');
|
||||||
|
return buffer.toString('base64');
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { runCommand } from '../../utils/run-command.js';
|
||||||
|
import { log, LOG_LEVELS } from '../../utils/log.js';
|
||||||
|
import { SUPPORTED_PLATFORMS, CURRENT_PLATFORM } from '../../utils/platform.js';
|
||||||
|
|
||||||
|
export async function captureWindowTitles(processId) {
|
||||||
|
if (!processId) { throw new Error('Missing process ID.'); }
|
||||||
|
|
||||||
|
const captureFunction = windowTitleCaptureFunctions[CURRENT_PLATFORM];
|
||||||
|
if (!captureFunction) {
|
||||||
|
log(`Cannot capture window title, unsupported OS: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return captureFunction(processId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowTitleCaptureFunctions = {
|
||||||
|
[SUPPORTED_PLATFORMS.MAC]: captureTitlesOnMac,
|
||||||
|
[SUPPORTED_PLATFORMS.LINUX]: captureTitlesOnLinux,
|
||||||
|
[SUPPORTED_PLATFORMS.WINDOWS]: captureTitlesOnWindows,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function captureTitlesOnWindows(processId) {
|
||||||
|
if (!processId) { throw new Error('Missing process ID.'); }
|
||||||
|
|
||||||
|
const { stdout: tasklistOutput, error } = await runCommand(
|
||||||
|
`tasklist /FI "PID eq ${processId}" /fo list /v`,
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
log(`Failed to retrieve window title.\n${error}`, LOG_LEVELS.WARN);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const match = tasklistOutput.match(/Window Title:\s*(.*)/);
|
||||||
|
if (match && match[1]) {
|
||||||
|
const title = match[1].trim();
|
||||||
|
if (title === 'N/A') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [title];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureTitlesOnLinux(processId) {
|
||||||
|
if (!processId) { throw new Error('Missing process ID.'); }
|
||||||
|
|
||||||
|
const { stdout: windowIdsOutput, error: windowIdError } = await runCommand(
|
||||||
|
`xdotool search --pid '${processId}'`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (windowIdError || !windowIdsOutput) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowIds = windowIdsOutput.trim().split('\n');
|
||||||
|
|
||||||
|
const titles = await Promise.all(windowIds.map(async (windowId) => {
|
||||||
|
const { stdout: titleOutput, error: titleError } = await runCommand(
|
||||||
|
`xprop -id ${windowId} | grep "WM_NAME(STRING)" | cut -d '=' -f 2 | sed 's/^[[:space:]]*"\\(.*\\)"[[:space:]]*$/\\1/'`,
|
||||||
|
);
|
||||||
|
if (titleError || !titleOutput) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return titleOutput.trim();
|
||||||
|
}));
|
||||||
|
|
||||||
|
return titles.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasAssistiveAccessOnMac = true;
|
||||||
|
|
||||||
|
async function captureTitlesOnMac(processId) {
|
||||||
|
if (!processId) { throw new Error('Missing process ID.'); }
|
||||||
|
if (!hasAssistiveAccessOnMac) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const script = `
|
||||||
|
tell application "System Events"
|
||||||
|
try
|
||||||
|
set targetProcess to first process whose unix id is ${processId}
|
||||||
|
on error
|
||||||
|
return
|
||||||
|
end try
|
||||||
|
tell targetProcess
|
||||||
|
if (count of windows) > 0 then
|
||||||
|
set window_name to name of front window
|
||||||
|
return window_name
|
||||||
|
end if
|
||||||
|
end tell
|
||||||
|
end tell
|
||||||
|
`;
|
||||||
|
const argument = script.trim()
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((line) => `-e '${line.trim()}'`)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
const { stdout: titleOutput, error } = await runCommand(`osascript ${argument}`);
|
||||||
|
if (error) {
|
||||||
|
let errorMessage = '';
|
||||||
|
if (error.includes('-25211')) {
|
||||||
|
errorMessage += 'Capturing window title requires assistive access. You do not have it.\n';
|
||||||
|
hasAssistiveAccessOnMac = false;
|
||||||
|
}
|
||||||
|
errorMessage += error;
|
||||||
|
log(errorMessage, LOG_LEVELS.WARN);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const title = titleOutput?.trim();
|
||||||
|
if (!title) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [title];
|
||||||
|
}
|
||||||
20
scripts/check-desktop-runtime-errors/cli-args.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { log } from './utils/log.js';
|
||||||
|
|
||||||
|
const PROCESS_ARGUMENTS = process.argv.slice(2);
|
||||||
|
|
||||||
|
export const COMMAND_LINE_FLAGS = Object.freeze({
|
||||||
|
FORCE_REBUILD: '--build',
|
||||||
|
TAKE_SCREENSHOT: '--screenshot',
|
||||||
|
});
|
||||||
|
|
||||||
|
export function logCurrentArgs() {
|
||||||
|
if (!PROCESS_ARGUMENTS.length) {
|
||||||
|
log('No additional arguments provided.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log(`Arguments: ${PROCESS_ARGUMENTS.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasCommandLineFlag(flag) {
|
||||||
|
return PROCESS_ARGUMENTS.includes(flag);
|
||||||
|
}
|
||||||
7
scripts/check-desktop-runtime-errors/config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export const DESKTOP_BUILD_COMMAND = 'npm run electron:prebuild && npm run electron:build -- --publish never';
|
||||||
|
export const PROJECT_DIR = process.cwd();
|
||||||
|
export const DESKTOP_DIST_PATH = join(PROJECT_DIR, 'dist');
|
||||||
|
export const APP_EXECUTION_DURATION_IN_SECONDS = 60; // Long enough for CI runners
|
||||||
|
export const SCREENSHOT_PATH = join(PROJECT_DIR, 'screenshot.png');
|
||||||
3
scripts/check-desktop-runtime-errors/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { main } from './main.js';
|
||||||
|
|
||||||
|
await main();
|
||||||
68
scripts/check-desktop-runtime-errors/main.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { logCurrentArgs, COMMAND_LINE_FLAGS, hasCommandLineFlag } from './cli-args.js';
|
||||||
|
import { log, die } from './utils/log.js';
|
||||||
|
import { ensureNpmProjectDir, npmInstall, npmBuild } from './utils/npm.js';
|
||||||
|
import { clearAppLogFile } from './app/app-logs.js';
|
||||||
|
import { checkForErrors } from './app/check-for-errors.js';
|
||||||
|
import { runApplication } from './app/runner.js';
|
||||||
|
import { CURRENT_PLATFORM, SUPPORTED_PLATFORMS } from './utils/platform.js';
|
||||||
|
import { prepareLinuxApp } from './app/extractors/linux.js';
|
||||||
|
import { prepareWindowsApp } from './app/extractors/windows.js';
|
||||||
|
import { prepareMacOsApp } from './app/extractors/macos.js';
|
||||||
|
import {
|
||||||
|
DESKTOP_BUILD_COMMAND,
|
||||||
|
PROJECT_DIR,
|
||||||
|
DESKTOP_DIST_PATH,
|
||||||
|
APP_EXECUTION_DURATION_IN_SECONDS,
|
||||||
|
SCREENSHOT_PATH,
|
||||||
|
} from './config.js';
|
||||||
|
|
||||||
|
export async function main() {
|
||||||
|
logCurrentArgs();
|
||||||
|
await ensureNpmProjectDir(PROJECT_DIR);
|
||||||
|
await npmInstall(PROJECT_DIR);
|
||||||
|
await npmBuild(
|
||||||
|
PROJECT_DIR,
|
||||||
|
DESKTOP_BUILD_COMMAND,
|
||||||
|
DESKTOP_DIST_PATH,
|
||||||
|
hasCommandLineFlag(COMMAND_LINE_FLAGS.FORCE_REBUILD),
|
||||||
|
);
|
||||||
|
await clearAppLogFile(PROJECT_DIR);
|
||||||
|
const {
|
||||||
|
stderr, stdout, isCrashed, windowTitles,
|
||||||
|
} = await extractAndRun();
|
||||||
|
if (stdout) {
|
||||||
|
log(`Output (stdout) from application execution:\n${stdout}`);
|
||||||
|
}
|
||||||
|
if (isCrashed) {
|
||||||
|
die('The application encountered an error during its execution.');
|
||||||
|
}
|
||||||
|
await checkForErrors(stderr, windowTitles, PROJECT_DIR);
|
||||||
|
log('🥳🎈 Success! Application completed without any runtime errors.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractAndRun() {
|
||||||
|
const extractors = {
|
||||||
|
[SUPPORTED_PLATFORMS.MAC]: () => prepareMacOsApp(DESKTOP_DIST_PATH),
|
||||||
|
[SUPPORTED_PLATFORMS.LINUX]: () => prepareLinuxApp(DESKTOP_DIST_PATH),
|
||||||
|
[SUPPORTED_PLATFORMS.WINDOWS]: () => prepareWindowsApp(DESKTOP_DIST_PATH),
|
||||||
|
};
|
||||||
|
const extractor = extractors[CURRENT_PLATFORM];
|
||||||
|
if (!extractor) {
|
||||||
|
throw new Error(`Platform not supported: ${CURRENT_PLATFORM}`);
|
||||||
|
}
|
||||||
|
const { appExecutablePath, cleanup } = await extractor();
|
||||||
|
try {
|
||||||
|
return await runApplication(
|
||||||
|
appExecutablePath,
|
||||||
|
APP_EXECUTION_DURATION_IN_SECONDS,
|
||||||
|
hasCommandLineFlag(COMMAND_LINE_FLAGS.TAKE_SCREENSHOT),
|
||||||
|
SCREENSHOT_PATH,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (cleanup) {
|
||||||
|
log('Cleaning up post-execution resources...');
|
||||||
|
await cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
scripts/check-desktop-runtime-errors/utils/io.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { extname, join } from 'path';
|
||||||
|
import { readdir, access } from 'fs/promises';
|
||||||
|
import { constants } from 'fs';
|
||||||
|
import { log, die, LOG_LEVELS } from './log.js';
|
||||||
|
|
||||||
|
export async function findSingleFileByExtension(extension, directory) {
|
||||||
|
if (!directory) { throw new Error('Missing directory'); }
|
||||||
|
if (!extension) { throw new Error('Missing file extension'); }
|
||||||
|
|
||||||
|
if (!await exists(directory)) {
|
||||||
|
die(`Directory does not exist: ${directory}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const directoryContents = await readdir(directory);
|
||||||
|
const foundFileNames = directoryContents.filter((file) => extname(file) === `.${extension}`);
|
||||||
|
const withoutUninstaller = foundFileNames.filter(
|
||||||
|
(fileName) => !fileName.toLowerCase().includes('uninstall'), // NSIS build has `Uninstall {app-name}.exe`
|
||||||
|
);
|
||||||
|
if (!withoutUninstaller.length) {
|
||||||
|
die(`No ${extension} found in ${directory} directory.`);
|
||||||
|
}
|
||||||
|
if (withoutUninstaller.length > 1) {
|
||||||
|
log(`Found multiple ${extension} files: ${withoutUninstaller.join(', ')}. Using first occurrence`, LOG_LEVELS.WARN);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
absolutePath: join(directory, withoutUninstaller[0]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exists(path) {
|
||||||
|
if (!path) { throw new Error('Missing path'); }
|
||||||
|
try {
|
||||||
|
await access(path, constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isDirMissingOrEmpty(dir) {
|
||||||
|
if (!dir) { throw new Error('Missing directory'); }
|
||||||
|
if (!await exists(dir)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const contents = await readdir(dir);
|
||||||
|
return contents.length === 0;
|
||||||
|
}
|
||||||
39
scripts/check-desktop-runtime-errors/utils/log.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export const LOG_LEVELS = Object.freeze({
|
||||||
|
INFO: 'INFO',
|
||||||
|
WARN: 'WARN',
|
||||||
|
ERROR: 'ERROR',
|
||||||
|
});
|
||||||
|
|
||||||
|
export function log(message, level = LOG_LEVELS.INFO) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const config = LOG_LEVEL_CONFIG[level] || LOG_LEVEL_CONFIG[LOG_LEVELS.INFO];
|
||||||
|
const formattedMessage = `[${timestamp}][${config.color}${level}${COLOR_CODES.RESET}] ${message}`;
|
||||||
|
config.method(formattedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function die(message) {
|
||||||
|
log(message, LOG_LEVELS.ERROR);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLOR_CODES = {
|
||||||
|
RESET: '\x1b[0m',
|
||||||
|
LIGHT_RED: '\x1b[91m',
|
||||||
|
YELLOW: '\x1b[33m',
|
||||||
|
LIGHT_BLUE: '\x1b[94m',
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOG_LEVEL_CONFIG = {
|
||||||
|
[LOG_LEVELS.INFO]: {
|
||||||
|
color: COLOR_CODES.LIGHT_BLUE,
|
||||||
|
method: console.log,
|
||||||
|
},
|
||||||
|
[LOG_LEVELS.WARN]: {
|
||||||
|
color: COLOR_CODES.YELLOW,
|
||||||
|
method: console.warn,
|
||||||
|
},
|
||||||
|
[LOG_LEVELS.ERROR]: {
|
||||||
|
color: COLOR_CODES.LIGHT_RED,
|
||||||
|
method: console.error,
|
||||||
|
},
|
||||||
|
};
|
||||||
87
scripts/check-desktop-runtime-errors/utils/npm.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
import { rmdir, readFile } from 'fs/promises';
|
||||||
|
import { exists, isDirMissingOrEmpty } from './io.js';
|
||||||
|
import { runCommand } from './run-command.js';
|
||||||
|
import { LOG_LEVELS, die, log } from './log.js';
|
||||||
|
|
||||||
|
export async function ensureNpmProjectDir(projectDir) {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
if (!await exists(join(projectDir, 'package.json'))) {
|
||||||
|
die(`'package.json' not found in project directory: ${projectDir}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function npmInstall(projectDir) {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
const npmModulesPath = join(projectDir, 'node_modules');
|
||||||
|
if (!await isDirMissingOrEmpty(npmModulesPath)) {
|
||||||
|
log(`Directory "${npmModulesPath}" exists and has content. Skipping \`npm install\`.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log('Starting dependency installation...');
|
||||||
|
const { error } = await runCommand('npm install --loglevel=error', {
|
||||||
|
stdio: 'inherit',
|
||||||
|
cwd: projectDir,
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
die(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function npmBuild(projectDir, buildCommand, distDir, forceRebuild) {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
if (!buildCommand) { throw new Error('missing build command'); }
|
||||||
|
if (!distDir) { throw new Error('missing distribution directory'); }
|
||||||
|
|
||||||
|
const isMissingBuild = await isDirMissingOrEmpty(distDir);
|
||||||
|
|
||||||
|
if (!isMissingBuild && !forceRebuild) {
|
||||||
|
log(`Directory "${distDir}" exists and has content. Skipping build: '${buildCommand}'.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceRebuild) {
|
||||||
|
log(`Removing directory "${distDir}" for a clean build (triggered by --build flag).`);
|
||||||
|
await rmdir(distDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Starting project build...');
|
||||||
|
const { error } = await runCommand(buildCommand, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
cwd: projectDir,
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
log(error, LOG_LEVELS.WARN); // Cannot disable Vue CLI errors, stderr contains false-positives.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAppName(projectDir) {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
const packageData = await readPackageJsonContents(projectDir);
|
||||||
|
try {
|
||||||
|
const packageJson = JSON.parse(packageData);
|
||||||
|
if (!packageJson.name) {
|
||||||
|
die(`The 'package.json' file doesn't specify a name: ${packageData}`);
|
||||||
|
}
|
||||||
|
return packageJson.name;
|
||||||
|
} catch (error) {
|
||||||
|
die(`Unable to parse 'package.json'. Error: ${error}\nContent: ${packageData}`, LOG_LEVELS.ERROR);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readPackageJsonContents(projectDir) {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
const packagePath = join(projectDir, 'package.json');
|
||||||
|
if (!await exists(packagePath)) {
|
||||||
|
die(`'package.json' file not found at ${packagePath}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const packageData = await readFile(packagePath, 'utf8');
|
||||||
|
return packageData;
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error reading 'package.json' from ${packagePath}.`, LOG_LEVELS.ERROR);
|
||||||
|
die(`Error detail: ${error}`, LOG_LEVELS.ERROR);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
scripts/check-desktop-runtime-errors/utils/platform.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { platform } from 'os';
|
||||||
|
|
||||||
|
export const SUPPORTED_PLATFORMS = {
|
||||||
|
MAC: 'darwin',
|
||||||
|
LINUX: 'linux',
|
||||||
|
WINDOWS: 'win32',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CURRENT_PLATFORM = platform();
|
||||||
44
scripts/check-desktop-runtime-errors/utils/run-command.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { exec } from 'child_process';
|
||||||
|
import { indentText } from './text.js';
|
||||||
|
|
||||||
|
const TIMEOUT_IN_SECONDS = 180;
|
||||||
|
const MAX_OUTPUT_BUFFER_SIZE = 1024 * 1024; // 1 MB
|
||||||
|
|
||||||
|
export function runCommand(commandString, options) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
options = {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
timeout: TIMEOUT_IN_SECONDS * 1000,
|
||||||
|
maxBuffer: MAX_OUTPUT_BUFFER_SIZE * 2,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
exec(commandString, options, (error, stdout, stderr) => {
|
||||||
|
let errorText;
|
||||||
|
if (error || stderr?.length > 0) {
|
||||||
|
errorText = formatError(commandString, error, stdout, stderr);
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
stdout,
|
||||||
|
error: errorText,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatError(commandString, error, stdout, stderr) {
|
||||||
|
const errorParts = [
|
||||||
|
'Error while running command.',
|
||||||
|
`Command:\n${indentText(commandString, 1)}`,
|
||||||
|
];
|
||||||
|
if (error?.toString().trim()) {
|
||||||
|
errorParts.push(`Error:\n${indentText(error.toString(), 1)}`);
|
||||||
|
}
|
||||||
|
if (stderr?.toString().trim()) {
|
||||||
|
errorParts.push(`stderr:\n${indentText(stderr, 1)}`);
|
||||||
|
}
|
||||||
|
if (stdout?.toString().trim()) {
|
||||||
|
errorParts.push(`stdout:\n${indentText(stdout, 1)}`);
|
||||||
|
}
|
||||||
|
return errorParts.join('\n---\n');
|
||||||
|
}
|
||||||
19
scripts/check-desktop-runtime-errors/utils/text.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export function indentText(text, indentLevel = 1) {
|
||||||
|
validateText(text);
|
||||||
|
const indentation = '\t'.repeat(indentLevel);
|
||||||
|
return splitTextIntoLines(text)
|
||||||
|
.map((line) => (line ? `${indentation}${line}` : line))
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitTextIntoLines(text) {
|
||||||
|
validateText(text);
|
||||||
|
return text
|
||||||
|
.split(/[\r\n]+/);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateText(text) {
|
||||||
|
if (typeof text !== 'string') {
|
||||||
|
throw new Error(`text is not a string. It is: ${typeof text}\n${text}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
scripts/configure-vscode.sh
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# This script ensures that the '.vscode/settings.json' file exists and is configured correctly for ESLint validation on Vue and JavaScript files.
|
||||||
|
# See https://web.archive.org/web/20230801024405/https://eslint.vuejs.org/user-guide/#visual-studio-code
|
||||||
|
|
||||||
|
declare -r SETTINGS_FILE='.vscode/settings.json'
|
||||||
|
declare -ra CONFIG_KEYS=('vue' 'javascript' 'typescript')
|
||||||
|
declare -r TEMP_FILE="tmp.$$.json"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
ensure_vscode_directory_exists
|
||||||
|
create_or_update_settings
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_vscode_directory_exists() {
|
||||||
|
local dir_name
|
||||||
|
dir_name=$(dirname "${SETTINGS_FILE}")
|
||||||
|
if [[ ! -d ${dir_name} ]]; then
|
||||||
|
mkdir -p "${dir_name}"
|
||||||
|
echo "🎉 Created directory: ${dir_name}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_or_update_settings() {
|
||||||
|
if [[ ! -f ${SETTINGS_FILE} ]]; then
|
||||||
|
create_default_settings
|
||||||
|
else
|
||||||
|
add_or_update_eslint_validate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_default_settings() {
|
||||||
|
local default_validate
|
||||||
|
default_validate=$(printf '%s' "${CONFIG_KEYS[*]}" | jq -R -s -c -M 'split(" ")')
|
||||||
|
echo "{ \"eslint.validate\": ${default_validate} }" | jq '.' > "${SETTINGS_FILE}"
|
||||||
|
echo "🎉 Created default ${SETTINGS_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
add_or_update_eslint_validate() {
|
||||||
|
if ! jq -e '.["eslint.validate"]' "${SETTINGS_FILE}" >/dev/null; then
|
||||||
|
add_default_eslint_validate
|
||||||
|
else
|
||||||
|
update_eslint_validate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
add_default_eslint_validate() {
|
||||||
|
jq --argjson keys "$(printf '%s' "${CONFIG_KEYS[*]}" \
|
||||||
|
| jq -R -s -c 'split(" ")')" '. += {"eslint.validate": $keys}' "${SETTINGS_FILE}" > "${TEMP_FILE}"
|
||||||
|
replace_and_confirm
|
||||||
|
echo "🎉 Added default 'eslint.validate' to ${SETTINGS_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_eslint_validate() {
|
||||||
|
local existing_keys
|
||||||
|
existing_keys=$(jq '.["eslint.validate"]' "${SETTINGS_FILE}")
|
||||||
|
for key in "${CONFIG_KEYS[@]}"; do
|
||||||
|
if ! echo "${existing_keys}" | jq 'index("'"${key}"'")' >/dev/null; then
|
||||||
|
jq '.["eslint.validate"] += ["'"${key}"'"]' "${SETTINGS_FILE}" > "${TEMP_FILE}"
|
||||||
|
mv "${TEMP_FILE}" "${SETTINGS_FILE}"
|
||||||
|
echo "🎉 Updated 'eslint.validate' in ${SETTINGS_FILE} for ${key}"
|
||||||
|
else
|
||||||
|
echo "⏩️ No updated needed for ${key} ${SETTINGS_FILE}."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
replace_and_confirm() {
|
||||||
|
if mv "${TEMP_FILE}" "${SETTINGS_FILE}"; then
|
||||||
|
echo "🎉 Updated ${SETTINGS_FILE}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
95
scripts/fresh-npm-install.sh
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Description:
|
||||||
|
# This script ensures npm is available, removes existing node modules, optionally
|
||||||
|
# removes package-lock.json (when -n flag is used), installs dependencies and runs unit tests.
|
||||||
|
# Usage:
|
||||||
|
# ./fresh-npm-install.sh # Regular execution
|
||||||
|
# ./fresh-npm-install.sh -n # Non-deterministic mode (removes package-lock.json)
|
||||||
|
|
||||||
|
declare NON_DETERMINISTIC_FLAG=0
|
||||||
|
|
||||||
|
|
||||||
|
main() {
|
||||||
|
parse_args "$@"
|
||||||
|
ensure_npm_is_available
|
||||||
|
ensure_npm_root
|
||||||
|
remove_existing_modules
|
||||||
|
if [[ $NON_DETERMINISTIC_FLAG -eq 1 ]]; then
|
||||||
|
remove_package_lock_json
|
||||||
|
fi
|
||||||
|
install_dependencies
|
||||||
|
run_unit_tests
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_npm_is_available() {
|
||||||
|
if ! command -v npm &> /dev/null; then
|
||||||
|
log::fatal 'npm could not be found, please install it first.'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_npm_root() {
|
||||||
|
if [ ! -f package.json ]; then
|
||||||
|
log::fatal 'Current directory is not a npm root. Please run the script in a npm root directory.'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_existing_modules() {
|
||||||
|
if [ -d ./node_modules ]; then
|
||||||
|
log::info 'Removing existing node modules...'
|
||||||
|
if ! rm -rf ./node_modules; then
|
||||||
|
log::fatal 'Could not remove existing node modules.'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_dependencies() {
|
||||||
|
log::info 'Installing dependencies...'
|
||||||
|
if ! npm install; then
|
||||||
|
log::fatal 'Failed to install dependencies.'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_package_lock_json() {
|
||||||
|
if [ -f ./package-lock.json ]; then
|
||||||
|
log::info 'Removing package-lock.json...'
|
||||||
|
if ! rm -rf ./package-lock.json; then
|
||||||
|
log::fatal 'Could not remove package-lock.json.'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_unit_tests() {
|
||||||
|
log::info 'Running unit tests...'
|
||||||
|
if ! npm run test:unit; then
|
||||||
|
pwd
|
||||||
|
log::fatal 'Failed to run unit tests.'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info() {
|
||||||
|
local -r message="$1"
|
||||||
|
echo "📣 ${message}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log::fatal() {
|
||||||
|
local -r message="$1"
|
||||||
|
echo "❌ ${message}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while getopts "n" opt; do
|
||||||
|
case ${opt} in
|
||||||
|
n)
|
||||||
|
NON_DETERMINISTIC_FLAG=1
|
||||||
|
;;
|
||||||
|
\?)
|
||||||
|
echo "Invalid option: $OPTARG" 1>&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$1"
|
||||||
127
scripts/logo-update.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
import { resolve, join } from 'path';
|
||||||
|
import { rm, mkdtemp, stat } from 'fs/promises';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { URL, fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
class Paths {
|
||||||
|
constructor(selfDirectory) {
|
||||||
|
const projectRoot = resolve(selfDirectory, '../');
|
||||||
|
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
||||||
|
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
||||||
|
this.electronBuildDirectory = join(projectRoot, 'build');
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `Source image: ${this.sourceImage}\n`
|
||||||
|
+ `Public directory: ${this.publicDirectory}\n`
|
||||||
|
+ `Electron build directory: ${this.electronBuildDirectory}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const paths = new Paths(getCurrentScriptDirectory());
|
||||||
|
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
||||||
|
await updateDesktopLauncherAndTrayIcon(paths.sourceImage, paths.publicDirectory);
|
||||||
|
await updateWebFavicon(paths.sourceImage, paths.publicDirectory);
|
||||||
|
await updateDesktopIcons(paths.sourceImage, paths.electronBuildDirectory);
|
||||||
|
console.log('🎉 (Re)created icons successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDesktopLauncherAndTrayIcon(sourceImage, publicFolder) {
|
||||||
|
await ensureFileExists(sourceImage);
|
||||||
|
await ensureFolderExists(publicFolder);
|
||||||
|
const electronTrayIconFile = join(publicFolder, 'icon.png');
|
||||||
|
console.log(`Updating desktop launcher and tray icon at ${electronTrayIconFile}.`);
|
||||||
|
await runCommand(
|
||||||
|
'npx',
|
||||||
|
'svgexport',
|
||||||
|
sourceImage,
|
||||||
|
electronTrayIconFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateWebFavicon(sourceImage, faviconFolder) {
|
||||||
|
console.log('Updating favicon');
|
||||||
|
await ensureFileExists(sourceImage);
|
||||||
|
await ensureFolderExists(faviconFolder);
|
||||||
|
await runCommand(
|
||||||
|
'npx',
|
||||||
|
'icon-gen',
|
||||||
|
`--input ${sourceImage}`,
|
||||||
|
`--output ${faviconFolder}`,
|
||||||
|
'--ico',
|
||||||
|
'--ico-name \'favicon\'',
|
||||||
|
'--report',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDesktopIcons(sourceImage, electronIconsDir) {
|
||||||
|
await ensureFileExists(sourceImage);
|
||||||
|
await ensureFolderExists(electronIconsDir);
|
||||||
|
const temporaryDir = await mkdtemp('icon-');
|
||||||
|
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
||||||
|
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by `icon-builder`
|
||||||
|
await runCommand(
|
||||||
|
'npx',
|
||||||
|
'svgexport',
|
||||||
|
sourceImage,
|
||||||
|
temporaryPngFile,
|
||||||
|
'1024:1024',
|
||||||
|
);
|
||||||
|
console.log(`Creating electron icons to ${electronIconsDir}.`);
|
||||||
|
await runCommand(
|
||||||
|
'npx',
|
||||||
|
'electron-icon-builder',
|
||||||
|
`--input="${temporaryPngFile}"`,
|
||||||
|
`--output="${electronIconsDir}"`,
|
||||||
|
'--flatten',
|
||||||
|
);
|
||||||
|
console.log('Cleaning up temporary directory.');
|
||||||
|
await rm(temporaryDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureFileExists(filePath) {
|
||||||
|
const path = await stat(filePath);
|
||||||
|
if (!path.isFile()) {
|
||||||
|
throw new Error(`Not a file: ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureFolderExists(folderPath) {
|
||||||
|
const path = await stat(folderPath);
|
||||||
|
if (!path.isDirectory()) {
|
||||||
|
throw new Error(`Not a directory: ${folderPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommand(...args) {
|
||||||
|
const command = args.join(' ');
|
||||||
|
console.log(`Running command: ${command}`);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const process = spawn(command, { shell: true });
|
||||||
|
process.stdout.on('data', (stdout) => {
|
||||||
|
console.log(stdout.toString());
|
||||||
|
});
|
||||||
|
process.stderr.on('data', (stderr) => {
|
||||||
|
console.error(stderr.toString());
|
||||||
|
});
|
||||||
|
process.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
process.on('close', (exitCode) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
reject(new Error(`Process exited with non-zero exit code: ${exitCode}`));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
process.stdin.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentScriptDirectory() {
|
||||||
|
return fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
72
src/App.vue
@@ -1,72 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="app">
|
|
||||||
<div class="wrapper">
|
|
||||||
<TheHeader class="row" />
|
|
||||||
<TheSearchBar class="row" />
|
|
||||||
<TheScriptArea class="row" />
|
|
||||||
<TheCodeButtons class="row code-buttons" />
|
|
||||||
<TheFooter />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
|
||||||
import TheHeader from '@/presentation/TheHeader.vue';
|
|
||||||
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
|
||||||
import TheCodeButtons from '@/presentation/Code/CodeButtons/TheCodeButtons.vue';
|
|
||||||
import TheScriptArea from '@/presentation/Scripts/TheScriptArea.vue';
|
|
||||||
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
TheHeader,
|
|
||||||
TheCodeButtons,
|
|
||||||
TheScriptArea,
|
|
||||||
TheSearchBar,
|
|
||||||
TheFooter,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class App extends Vue {
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import "@/presentation/styles/colors.scss";
|
|
||||||
@import "@/presentation/styles/fonts.scss";
|
|
||||||
@import "@/presentation/styles/media.scss";
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: $light-gray;
|
|
||||||
font-family: $main-font;
|
|
||||||
color: $slate;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
margin-right: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
max-width: 1600px;
|
|
||||||
.wrapper {
|
|
||||||
margin: 0% 2% 0% 2%;
|
|
||||||
background-color: white;
|
|
||||||
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.06);
|
|
||||||
padding: 2%;
|
|
||||||
display:flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.row {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.code-buttons {
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@import "@/presentation/styles/tooltip.scss";
|
|
||||||
@import "@/presentation/styles/tree.scss";
|
|
||||||
</style>
|
|
||||||
15
src/TypeHelpers.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export type Constructible<T, TArgs extends unknown[] = never> = {
|
||||||
|
prototype: T;
|
||||||
|
apply: (this: unknown, args: TArgs) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropertyKeys<T> = {
|
||||||
|
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? never : K;
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
export type ConstructorArguments<T> =
|
||||||
|
T extends new (...args: infer U) => unknown ? U : never;
|
||||||
|
|
||||||
|
export type FunctionKeys<T> = {
|
||||||
|
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never;
|
||||||
|
}[keyof T];
|
||||||
@@ -3,19 +3,22 @@ import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
|||||||
import { IApplicationFactory } from './IApplicationFactory';
|
import { IApplicationFactory } from './IApplicationFactory';
|
||||||
import { parseApplication } from './Parser/ApplicationParser';
|
import { parseApplication } from './Parser/ApplicationParser';
|
||||||
|
|
||||||
export type ApplicationGetter = () => IApplication;
|
export type ApplicationGetterType = () => IApplication;
|
||||||
const ApplicationGetter: ApplicationGetter = parseApplication;
|
const ApplicationGetter: ApplicationGetterType = parseApplication;
|
||||||
|
|
||||||
export class ApplicationFactory implements IApplicationFactory {
|
export class ApplicationFactory implements IApplicationFactory {
|
||||||
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
||||||
private readonly getter: AsyncLazy<IApplication>;
|
|
||||||
protected constructor(costlyGetter: ApplicationGetter) {
|
private readonly getter: AsyncLazy<IApplication>;
|
||||||
if (!costlyGetter) {
|
|
||||||
throw new Error('undefined getter');
|
protected constructor(costlyGetter: ApplicationGetterType) {
|
||||||
}
|
if (!costlyGetter) {
|
||||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
throw new Error('missing getter');
|
||||||
}
|
|
||||||
public getAppAsync(): Promise<IApplication> {
|
|
||||||
return this.getter.getValueAsync();
|
|
||||||
}
|
}
|
||||||
|
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getApp(): Promise<IApplication> {
|
||||||
|
return this.getter.getValue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/application/Common/Array.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Compares to Array<T> objects for equality, ignoring order
|
||||||
|
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
|
if (!array1) { throw new Error('missing first array'); }
|
||||||
|
if (!array2) { throw new Error('missing second array'); }
|
||||||
|
const sortedArray1 = sort(array1);
|
||||||
|
const sortedArray2 = sort(array2);
|
||||||
|
return sequenceEqual(sortedArray1, sortedArray2);
|
||||||
|
function sort(array: readonly T[]) {
|
||||||
|
return array.slice().sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares to Array<T> objects for equality in same order
|
||||||
|
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
|
if (!array1) { throw new Error('missing first array'); }
|
||||||
|
if (!array2) { throw new Error('missing second array'); }
|
||||||
|
if (array1.length !== array2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return array1.every((val, index) => val === array2[index]);
|
||||||
|
}
|
||||||
50
src/application/Common/CustomError.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
Provides a unified and resilient way to extend errors across platforms.
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
- Babel:
|
||||||
|
> "Built-in classes cannot be properly subclassed due to limitations in ES5"
|
||||||
|
> https://web.archive.org/web/20230810014108/https://babeljs.io/docs/caveats#classes
|
||||||
|
- TypeScript:
|
||||||
|
> "Extending built-ins like Error, Array, and Map may no longer work"
|
||||||
|
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||||
|
*/
|
||||||
|
export abstract class CustomError extends Error {
|
||||||
|
constructor(message?: string, options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
|
||||||
|
fixPrototype(this, new.target.prototype);
|
||||||
|
ensureStackTrace(this);
|
||||||
|
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Environment = {
|
||||||
|
getSetPrototypeOf: () => Object.setPrototypeOf,
|
||||||
|
getCaptureStackTrace: () => Error.captureStackTrace,
|
||||||
|
};
|
||||||
|
|
||||||
|
function fixPrototype(target: Error, prototype: CustomError) {
|
||||||
|
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
||||||
|
const setPrototypeOf = Environment.getSetPrototypeOf();
|
||||||
|
if (!functionExists(setPrototypeOf)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPrototypeOf(target, prototype);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureStackTrace(target: Error) {
|
||||||
|
const captureStackTrace = Environment.getCaptureStackTrace();
|
||||||
|
if (!functionExists(captureStackTrace)) {
|
||||||
|
// captureStackTrace is only available on V8, if it's not available
|
||||||
|
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
captureStackTrace(target, target.constructor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function functionExists(func: unknown): boolean {
|
||||||
|
// Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness
|
||||||
|
return typeof func === 'function';
|
||||||
|
}
|
||||||
@@ -1,43 +1,63 @@
|
|||||||
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
||||||
type EnumType = number | string;
|
export type EnumType = number | string;
|
||||||
type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
|
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
||||||
|
= { [key in T]: TEnumValue };
|
||||||
|
|
||||||
export interface IEnumParser<TEnum> {
|
export interface IEnumParser<TEnum> {
|
||||||
parseEnum(value: string, propertyName: string): TEnum;
|
parseEnum(value: string, propertyName: string): TEnum;
|
||||||
}
|
|
||||||
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> {
|
|
||||||
return {
|
|
||||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
|
||||||
value: string,
|
|
||||||
enumName: string,
|
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue {
|
|
||||||
if (!value) {
|
|
||||||
throw new Error(`undefined ${enumName}`);
|
|
||||||
}
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
|
|
||||||
}
|
|
||||||
const casedValue = getEnumNames(enumVariable)
|
|
||||||
.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
|
|
||||||
if (!casedValue) {
|
|
||||||
throw new Error(`unknown ${enumName}: "${value}"`);
|
|
||||||
}
|
|
||||||
return enumVariable[casedValue as keyof typeof enumVariable];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
|
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): string[] {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
return Object
|
): IEnumParser<TEnumValue> {
|
||||||
.values(enumVariable)
|
return {
|
||||||
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
value: string,
|
||||||
|
enumName: string,
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
): TEnumValue {
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`missing ${enumName}`);
|
||||||
|
}
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
|
||||||
|
}
|
||||||
|
const casedValue = getEnumNames(enumVariable)
|
||||||
|
.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
|
||||||
|
if (!casedValue) {
|
||||||
|
throw new Error(`unknown ${enumName}: "${value}"`);
|
||||||
|
}
|
||||||
|
return enumVariable[casedValue as keyof typeof enumVariable];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEnumNames
|
||||||
|
<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
): string[] {
|
||||||
|
return Object
|
||||||
|
.values(enumVariable)
|
||||||
|
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] {
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
return getEnumNames(enumVariable)
|
): TEnumValue[] {
|
||||||
.map((level) => enumVariable[level]) as TEnumValue[];
|
return getEnumNames(enumVariable)
|
||||||
|
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
value: TEnumValue,
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
|
) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
throw new Error('absent enum value');
|
||||||
|
}
|
||||||
|
if (!(value in enumVariable)) {
|
||||||
|
throw new RangeError(`enum value "${value}" is out of range`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
|
||||||
|
export interface IScriptingLanguageFactory<T> {
|
||||||
|
create(language: ScriptingLanguage): T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
|
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
||||||
|
|
||||||
|
type Getter<T> = () => T;
|
||||||
|
|
||||||
|
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<T> {
|
||||||
|
private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
|
||||||
|
|
||||||
|
public create(language: ScriptingLanguage): T {
|
||||||
|
assertInRange(language, ScriptingLanguage);
|
||||||
|
if (!this.getters.has(language)) {
|
||||||
|
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||||
|
}
|
||||||
|
const getter = this.getters.get(language);
|
||||||
|
const instance = getter();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||||
|
assertInRange(language, ScriptingLanguage);
|
||||||
|
if (!getter) {
|
||||||
|
throw new Error('missing getter');
|
||||||
|
}
|
||||||
|
if (this.getters.has(language)) {
|
||||||
|
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
||||||
|
}
|
||||||
|
this.getters.set(language, getter);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +1,64 @@
|
|||||||
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
|
||||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
|
||||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
|
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||||
|
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||||
|
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
||||||
|
|
||||||
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
||||||
|
|
||||||
export class ApplicationContext implements IApplicationContext {
|
export class ApplicationContext implements IApplicationContext {
|
||||||
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
|
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
|
||||||
public collection: ICategoryCollection;
|
|
||||||
public currentOs: OperatingSystem;
|
|
||||||
|
|
||||||
public get state(): ICategoryCollectionState {
|
public collection: ICategoryCollection;
|
||||||
return this.states[this.collection.os];
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly states: StateMachine;
|
public currentOs: OperatingSystem;
|
||||||
public constructor(
|
|
||||||
public readonly app: IApplication,
|
|
||||||
initialContext: OperatingSystem) {
|
|
||||||
validateApp(app);
|
|
||||||
validateOs(initialContext);
|
|
||||||
this.states = initializeStates(app);
|
|
||||||
this.changeContext(initialContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
public changeContext(os: OperatingSystem): void {
|
public get state(): ICategoryCollectionState {
|
||||||
if (this.currentOs === os) {
|
return this.states[this.collection.os];
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
this.collection = this.app.getCollection(os);
|
private readonly states: StateMachine;
|
||||||
if (!this.collection) {
|
|
||||||
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
public constructor(
|
||||||
}
|
public readonly app: IApplication,
|
||||||
const event: IApplicationContextChangedEvent = {
|
initialContext: OperatingSystem,
|
||||||
newState: this.states[os],
|
) {
|
||||||
oldState: this.states[this.currentOs],
|
validateApp(app);
|
||||||
};
|
this.states = initializeStates(app);
|
||||||
this.contextChanged.notify(event);
|
this.changeContext(initialContext);
|
||||||
this.currentOs = os;
|
}
|
||||||
|
|
||||||
|
public changeContext(os: OperatingSystem): void {
|
||||||
|
assertInRange(os, OperatingSystem);
|
||||||
|
if (this.currentOs === os) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this.collection = this.app.getCollection(os);
|
||||||
|
if (!this.collection) {
|
||||||
|
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
||||||
|
}
|
||||||
|
const event: IApplicationContextChangedEvent = {
|
||||||
|
newState: this.states[os],
|
||||||
|
oldState: this.states[this.currentOs],
|
||||||
|
};
|
||||||
|
this.contextChanged.notify(event);
|
||||||
|
this.currentOs = os;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateApp(app: IApplication) {
|
function validateApp(app: IApplication) {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
throw new Error('undefined app');
|
throw new Error('missing app');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function validateOs(os: OperatingSystem) {
|
|
||||||
if (os === undefined) {
|
|
||||||
throw new Error('undefined os');
|
|
||||||
}
|
|
||||||
if (os === OperatingSystem.Unknown) {
|
|
||||||
throw new Error('unknown os');
|
|
||||||
}
|
|
||||||
if (!(os in OperatingSystem)) {
|
|
||||||
throw new Error(`os "${os}" is out of range`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeStates(app: IApplication): StateMachine {
|
function initializeStates(app: IApplication): StateMachine {
|
||||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||||
for (const collection of app.collections) {
|
for (const collection of app.collections) {
|
||||||
machine[collection.os] = new CategoryCollectionState(collection);
|
machine[collection.os] = new CategoryCollectionState(collection);
|
||||||
}
|
}
|
||||||
return machine;
|
return machine;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
import { ApplicationContext } from './ApplicationContext';
|
|
||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { Environment } from '../Environment/Environment';
|
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { IEnvironment } from '../Environment/IEnvironment';
|
import { Environment } from '@/infrastructure/Environment/Environment';
|
||||||
|
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
|
||||||
import { IApplicationFactory } from '../IApplicationFactory';
|
import { IApplicationFactory } from '../IApplicationFactory';
|
||||||
import { ApplicationFactory } from '../ApplicationFactory';
|
import { ApplicationFactory } from '../ApplicationFactory';
|
||||||
|
import { ApplicationContext } from './ApplicationContext';
|
||||||
|
|
||||||
export async function buildContextAsync(
|
export async function buildContext(
|
||||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||||
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
|
environment = Environment.CurrentEnvironment,
|
||||||
if (!factory) { throw new Error('undefined factory'); }
|
): Promise<IApplicationContext> {
|
||||||
if (!environment) { throw new Error('undefined environment'); }
|
if (!factory) { throw new Error('missing factory'); }
|
||||||
const app = await factory.getAppAsync();
|
if (!environment) { throw new Error('missing environment'); }
|
||||||
const os = getInitialOs(app, environment);
|
const app = await factory.getApp();
|
||||||
return new ApplicationContext(app, os);
|
const os = getInitialOs(app, environment);
|
||||||
|
return new ApplicationContext(app, os);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
||||||
const currentOs = environment.os;
|
const currentOs = environment.os;
|
||||||
const supportedOsList = app.getSupportedOsList();
|
const supportedOsList = app.getSupportedOsList();
|
||||||
if (supportedOsList.includes(currentOs)) {
|
if (supportedOsList.includes(currentOs)) {
|
||||||
return currentOs;
|
return currentOs;
|
||||||
}
|
}
|
||||||
supportedOsList.sort((os1, os2) => {
|
supportedOsList.sort((os1, os2) => {
|
||||||
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||||
return getPriority(os2) - getPriority(os1);
|
return getPriority(os2) - getPriority(os1);
|
||||||
});
|
});
|
||||||
return supportedOsList[0];
|
return supportedOsList[0];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
|
||||||
|
|
||||||
export interface IApplicationContext {
|
export interface IReadOnlyApplicationContext {
|
||||||
readonly app: IApplication;
|
readonly app: IApplication;
|
||||||
readonly state: ICategoryCollectionState;
|
readonly state: IReadOnlyCategoryCollectionState;
|
||||||
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
||||||
changeContext(os: OperatingSystem): void;
|
}
|
||||||
|
|
||||||
|
export interface IApplicationContext extends IReadOnlyApplicationContext {
|
||||||
|
readonly state: ICategoryCollectionState;
|
||||||
|
changeContext(os: OperatingSystem): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IApplicationContextChangedEvent {
|
export interface IApplicationContextChangedEvent {
|
||||||
readonly newState: ICategoryCollectionState;
|
readonly newState: ICategoryCollectionState;
|
||||||
readonly oldState: ICategoryCollectionState;
|
readonly oldState: ICategoryCollectionState;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { UserFilter } from './Filter/UserFilter';
|
import { UserFilter } from './Filter/UserFilter';
|
||||||
import { IUserFilter } from './Filter/IUserFilter';
|
import { IUserFilter } from './Filter/IUserFilter';
|
||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
@@ -5,19 +7,20 @@ import { UserSelection } from './Selection/UserSelection';
|
|||||||
import { IUserSelection } from './Selection/IUserSelection';
|
import { IUserSelection } from './Selection/IUserSelection';
|
||||||
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
import { ICategoryCollection } from '../../../domain/ICategoryCollection';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
|
|
||||||
export class CategoryCollectionState implements ICategoryCollectionState {
|
export class CategoryCollectionState implements ICategoryCollectionState {
|
||||||
public readonly os: OperatingSystem;
|
public readonly os: OperatingSystem;
|
||||||
public readonly code: IApplicationCode;
|
|
||||||
public readonly selection: IUserSelection;
|
|
||||||
public readonly filter: IUserFilter;
|
|
||||||
|
|
||||||
public constructor(readonly collection: ICategoryCollection) {
|
public readonly code: IApplicationCode;
|
||||||
this.selection = new UserSelection(collection, []);
|
|
||||||
this.code = new ApplicationCode(this.selection, collection.scripting);
|
public readonly selection: IUserSelection;
|
||||||
this.filter = new UserFilter(collection);
|
|
||||||
this.os = collection.os;
|
public readonly filter: IUserFilter;
|
||||||
}
|
|
||||||
|
public constructor(readonly collection: ICategoryCollection) {
|
||||||
|
this.selection = new UserSelection(collection, []);
|
||||||
|
this.code = new ApplicationCode(this.selection, collection.scripting);
|
||||||
|
this.filter = new UserFilter(collection);
|
||||||
|
this.os = collection.os;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,41 @@
|
|||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
|
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||||
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||||
import { CodePosition } from './Position/CodePosition';
|
import { CodePosition } from './Position/CodePosition';
|
||||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
|
||||||
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
|
||||||
import { IApplicationCode } from './IApplicationCode';
|
import { IApplicationCode } from './IApplicationCode';
|
||||||
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
|
||||||
|
|
||||||
export class ApplicationCode implements IApplicationCode {
|
export class ApplicationCode implements IApplicationCode {
|
||||||
public readonly changed = new EventSource<ICodeChangedEvent>();
|
public readonly changed = new EventSource<ICodeChangedEvent>();
|
||||||
public current: string;
|
|
||||||
|
|
||||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
public current: string;
|
||||||
|
|
||||||
constructor(
|
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||||
userSelection: IUserSelection,
|
|
||||||
private readonly scriptingDefinition: IScriptingDefinition,
|
|
||||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
|
||||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
|
||||||
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
|
||||||
if (!generator) { throw new Error('generator is null or undefined'); }
|
|
||||||
this.setCode(userSelection.selectedScripts);
|
|
||||||
userSelection.changed.on((scripts) => {
|
|
||||||
this.setCode(scripts);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
constructor(
|
||||||
const oldScripts = Array.from(this.scriptPositions.keys());
|
userSelection: IReadOnlyUserSelection,
|
||||||
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
|
private readonly scriptingDefinition: IScriptingDefinition,
|
||||||
this.current = code.code;
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
||||||
this.scriptPositions = code.scriptPositions;
|
) {
|
||||||
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
if (!userSelection) { throw new Error('missing userSelection'); }
|
||||||
this.changed.notify(event);
|
if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); }
|
||||||
}
|
if (!generator) { throw new Error('missing generator'); }
|
||||||
|
this.setCode(userSelection.selectedScripts);
|
||||||
|
userSelection.changed.on((scripts) => {
|
||||||
|
this.setCode(scripts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
||||||
|
const oldScripts = Array.from(this.scriptPositions.keys());
|
||||||
|
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
|
||||||
|
this.current = code.code;
|
||||||
|
this.scriptPositions = code.scriptPositions;
|
||||||
|
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
||||||
|
this.changed.notify(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||