Compare commits

..

44 Commits

Author SHA1 Message Date
undergroundwires
a14929a13c Migrate to ES6 modules
Configure project to use ES6 modules to enable top-level await
capabilities. This change helps project to align well with modern JS
standards.

- Set `type` to `module` in `package.json`.
- Use import/export syntax in Cypress configuration files.
- Rename configurations files that do not support modules to use
  the `.cjs` extension:
  - `vue.config.js` to `vue.config.cjs` (vuejs/vue-cli#4477).
  - `babel.config.js` to `babel.config.cjs (babel/babel-loader#894)
  - `.eslintrc.js` to `.eslintrc.cjs` (eslint/eslint#13440,
    eslint/eslint#14137)
  - `postcss.config.js` to `postcss.config.cjs` (postcss/postcss#1771)
- Provide a workaround for Vue CLI & Mocha ES6 modules conflict in
  Vue configuration file (vuejs/vue-cli#7417).
2023-08-17 18:50:14 +02:00
undergroundwires
6a20d804dc Refactor filter (search query) event handling
Refactor filter event handling to a unified event with visitor pattern
to simplify the code, avoid future bugs and provide better test
coverage.

This commit shifts from using separate `filtered` and `filterRemoved`
events to a singular, more expressive `filterChanged` event. The new
approach emits a detailed payload that explicitly indicates the filter
action and the associated filter data. The event object unifies the way
the presentation layer reacts to the events.

Benefits with this approach include:

- Simplifying event listeners by reducing the number of events to
  handle.
- Increasing code clarity and reduces potential for oversight by
  providing explicit action details in the event payload.
- Offering extensibility for future actions without introducing new
  events.
- Providing visitor pattern to handle different kind of events in easy
  and robust manner without code repetition.

Other changes:

- Refactor components handling of events to follow DRY and KISS
  principles better.
- Refactor `UserFilter.spec.ts` to:
  - Make it easier to add new tests.
  - Increase code coverage by running all event-based tests on the
    current property.
2023-08-16 15:09:26 +02:00
undergroundwires
ae75059cc1 Increase testability through dependency injection
- Remove existing integration tests for hooks as they're redundant after
  this change.
- Document the pattern in relevant documentation.
- Introduce `useEnvironment` to increase testability.
- Update components to inject dependencies rather than importing hooks
  directly.
2023-08-15 18:11:30 +02:00
undergroundwires
39e650cf11 Fix revert toggle partial rendering
This commits fixes an issue where the `REVERT` label on revert toggle
might render as `REVER` or in a similarly clipped manner due to its
fixed width. The problem is visible when certain fonts fail to load or
browser engines render content non-standardly.

Changes:
- Refactor UI component to have its own separate Vue component with unit
  tests.
- Rework component design to utilize flexbox, enhancing its adaptability
  and simplifying the structure.
- Remove obselete `webkit` directives.
- Refactor SCSS for clearer structure and better SCSS best-practices.
- Use `em` when possible instead of `px` for improved responsiveness.
2023-08-14 15:28:15 +02:00
undergroundwires
bc91237d7c Refactor usage of tooltips for flexibility
This commit introduces a new Vue component to handle tooltips. It acts
as a wrapper for the `v-tooltip`. It enhances the maintainability,
readability and portability of tooltips by enabling the direct inclusion
of inline HTML in the tooltip components. It solves issues such as
absence of linting or editor support and cumbersome string
concatenation.

It also provides an abstraction layer that simplifies the switching
between different tooltip implementations, which would allow a smooth
migration to Vue 3 (see #230).
2023-08-12 16:53:58 +02:00
undergroundwires
9e5491fdbf Implement custom lightweight modal #230
Introduce a brand new lightweight and efficient modal component. It is
designed to be visually similar to the previous one to not introduce a
change in feel of the application in a patch release, but behind the
scenes it features:

- Enhanced application speed and reduced bundle size.
- New flexbox-driven layout, eliminating JS calculations.
- Composition API ready for Vue 3.0 #230.

Other changes:

- Adopt idiomatic Vue via `v-modal` binding.
- Add unit tests for both the modal and dialog.
- Remove `vue-js-modal` dependency in favor of the new implementation.
- Adjust modal shadow color to better match theme.
- Add `@vue/test-utils` for unit testing.
2023-08-11 19:35:26 +02:00
undergroundwires
986ba078a6 Fix failing tests due to failed error logging
Unit and integration tests have been failing due to failed logging of
`Error` objects. These were creating an issue where `mocha` was not
properly returning right exit codes, leading to test pipelines
incorrectly passing despite test failures.

- Fix runtime behavior of failing to retrieve error stacks.
- Add tests for error handling.
- Add more robust custom error handling.

Related issues: babel/babel#14273, vuejs/vue-cli#6994.
2023-08-10 19:49:08 +02:00
he3als
061afad967 Improve Defender scripts #201
Reliably disable Defender services (by always using `reg` with
TrustedInstaller`), and put Firewall services in Windows Firewall
section, so that people do not accidently disable Windows Firewall
services and break Microsoft Store.

Co-authored-by: undergroundwires <git@undergroundwires.dev>
2023-08-09 15:16:31 +02:00
undergroundwires
3bc8da4cbf Fix failing security tests
This commit changes the behavior of auditing to audit only production
dependencies.

Security checks have been failing for months due to Vue CLI dependencies
and lack of resolution from the developers. This commit makes auditing
ignore development dependencies.

The reasons include:

- Vulnerabilities in developer dependencies cause pipelines to fail
  on every run.
- This is caused by dependencies such that lack resolution from the
  developers. Vue developers consider `npm audit` broken design and do
  not prioritize solutions. Discussions: vuejs/vue-cli#6637,
  vuejs/vue-cli#6621, vuejs/vue-cli#6555, vuejs/vue-cli#6553,
  vuejs/vue-cli#6523, vuejs/vue-cli#6486, vuejs/vue-cli#6632.
- Development packages are not relevant for the production payload.
- False positives create behavior of ignoring them completely instead of
  taking action, which creates a security vulnerability itself.
- Failed tests are shown in a badge on README file, giving wrong picture
  of security posture of users.

`npm audit --omit=dev` is used instead of `npm audit --production` which
is deprecated as of npm v8.7.0 npm/cli#4744.

This commit also removes exiting with output of `npm audit` command to
fix exiting with textual output, leading to failures.
2023-08-08 20:02:09 +02:00
undergroundwires
1b9be8fe2d Refactor Vue components using Composition API #230
- Migrate `StatefulVue`:
  - Introduce `UseCollectionState` that replaces its behavior and acts
    as a shared state store.
  - Add more encapsulated, granular functions based on read or write
    access to state in CollectionState.
- Some linting rules get activates due to new code-base compability to
  modern parses, fix linting errors.
  - Rename Dialog to ModalDialog as after refactoring,
    eslintvue/no-reserved-component-names does not allow name Dialog.
  - To comply with `vue/multi-word-component-names`, rename:
    - `Code`          -> `CodeInstruction`
    - `Handle`        -> `SliderHandle`
    - `Documentable`  -> `DocumentableNode`
    - `Node`          -> `NodeContent`
    - `INode`         -> `INodeContent`
    - `Responsive`    -> `SizeObserver`
- Remove `vue-property-decorator` and `vue-class-component`
  dependencies.
- Refactor `watch` with computed properties when possible for cleaner
  code.
  - Introduce `UseApplication` to reduce repeated code in new components
    that use `computed` more heavily than before.
- Change TypeScript target to `es2017` to allow top level async calls
  for getting application context/state/instance to simplify the code by
  removing async calls. However, mocha (unit and integration) tests do
  not run with top level awaits, so a workaround is used.
2023-08-07 13:16:39 +02:00
undergroundwires
3a594ac7fd Improve user privacy with secure outbound links
All outbound links now include `rel="noopener noreferrer"` attribute.
This security improvement prevents the new page from being able to
access the `window.opener` property and ensures it runs in a separate
process.

`rel="noopener"`:

   When a new page is opened using `target="_blank"`, the new page runs
   on the same process as the originating page, and has a reference to
   the originating page `window.opener`. By implementing
   `rel="noopener"`, the new page is prevented to use `window.opener`
   property.
   It's security issue because the newly opened website could
   potentially redirect the page to a malicious URL. Even though
   privacy.sexy doesn't have any sensitive information to protect, this
   can still be a vector for phishing attacks.

`rel="noreferrer"`:

  It implies features of `noopener`, and also prevents `Referer` header
  from being sent to the new page. Referer headers may include
  sensitive data, because they tell the new page the URL of the page
  the request is coming from.
2023-08-06 02:09:11 +02:00
undergroundwires
ff84f5676e Transition to eslint-config-airbnb-with-typescript
- Migrate to newer `eslint-config-airbnb-with-typescript` from
  `eslint-config-airbnb`.
- Add also `rushstack/eslint-patch` as per instructed by
  `eslint-config-airbnb-with-typescript` docs.
- Update codebase to align with new linting standards.
- Add script to configure VS Code for effective linting for project
  developers, move it to `scripts` directory along with clean npm
  install script for better organization.
2023-08-04 16:39:36 +02:00
undergroundwires-bot
4d0ce12c96 ⬆️ bump everywhere to 0.12.0 2023-08-03 18:22:36 +00:00
undergroundwires
298b058e5c win: add new scripts to disable more telemetry
- Add new scripts under "Disable Windows telemetry and data collection".
- Update script names and documentations to align with Microsoft's
  latest branding for telemetry.
- Introduce broader configurability to minimize data collection.
- Add missing revert code to allow the reversion of changes, increasing
  flexibility and safety.
- Include comprehensive documentation to provide more context and
  understanding for users.
2023-08-03 17:17:56 +02:00
undergroundwires
1e80ee1fb0 Change subtitle heading to new slogan
- Unify reading subtitle/slogan throughout the application.
- Refactor related unit tests for easier future changes.
- Add typed constants for Vue app environment variables.
2023-08-01 17:50:36 +02:00
undergroundwires
5901dc5f11 Fix macOS desktop build failure in CI
The GitHub-hosted runners began experiencing issues while building macOS
desktop distributions, exclusively affecting the macOS environment.
The Ubuntu and Windows environments remained unaffected.

The logs highlighted the absence of Python in the macOS environment, which
resulted in build failure:

```sh
  Error: Exit code: ENOENT. spawn /usr/bin/python ENOENT
```

Since the `electron-builder` package uses Python scripts to create DMG
disk images for macOS distributions, Python is needed for building the
application. However, electron-builder uses Python 2.X meanwhile modern
macOS versions have removed Python 2.X from the operating system on
default installation.

Although this issue was resolved in `electron-builder` version 23,,
`vue-cli-plugin-electron-builder` continues to use version 22. Due to a
lack of maintenance, the package is unlikely to receive updates.

This commit forces `vue-cli-plugin-electron-builder` to use the latest
`electron-builder` which resolves the macOS distribution build failure.

In CI process, GitHub-hosted runners start to fail when building macOS
desktop distributions. It is only observered in the macOS environment
while the application is built successfully in both the Ubuntu and
Windows environments.

The error message in the logs indicated that Python was not found in the
macOS environment:

```sh
Error: Exit code: ENOENT. spawn /usr/bin/python ENOENT
```

`electron-builder` package uses Python scripts for certain operations,
specifically for creating DMG disk images for macOS distributions. As a
result, Python is a necessary dependency when building the application
for macOS.

`electron-builder` has fixed this starting from version 23, but
vue-cli-plugin-electron-builder still refers to version 22 and it is
unmaintained and not likely to get updates.

The solution is to add a step in the GitHub Actions workflow to set up
Python in the macOS environment. `actions/setup-python` sets up the
Python environment in the runner if the OS is macOS.

This change does not impact the Ubuntu and Windows environments as the
setup-python step is conditionally executed only for macOS. The addition
of Python to the macOS environment in CI process has resolved the build
failure issue for the macOS distribution.

See also:

- electron-userland/electron-builder#6606
- electron-userland/electron-builder#6726
- electron-userland/electron-builder#6732
- nklayman/vue-cli-plugin-electron-builder#1691
- nklayman/vue-cli-plugin-electron-builder#1701
2023-08-01 01:42:53 +02:00
undergroundwires
5721796378 Update dependencies and add npm setup script
- Introduce `fresh-npm-install.sh` to automate clean npm environment
  setup.
- Revert workaround 924b326244, resolved
  by updating Font Awesome.
- Remove `vue-template-compiler` and `@vue/test-utils` from
  dependencies, they're obsolete in 2.7.
- Update anchor references to start with lower case in line with
  MD051/link-fragments, introduced by updated `markdownlint`.
- Upgrade cypress to > 10, which includes:
  - Change spec extensions from `*.spec.js` to `*.cy.js`.
  - Change configuration file from `cypress.json` to
    `cypress.config.ts`.
  - Remove most configurations from `cypress/plugins/index.js`. These
    configurations were initially generated by Vue CLI but obsoleted in
    newer cypress versions.
- Lock Typescript version to 4.6.x due to lack of support in
  unmaintained Vue CLI TypeScript plugin (see vuejs/vue-cli#7401).
- Use `setWindowOpenHandler` on Electron, replacing deprecated
  `new-event` event.
- Document inability to upgrade `typescript-eslint` dependencies because
  `@vue/eslint-config-typescript` does not support them. See
   vuejs/eslint-config-typescript#60, vuejs/eslint-config-typescript#59,
   vuejs/eslint-config-typescript#57.
- Fix `typescript` version to 4.6.X and `tslib` version to 2.4.x,
  unit tests exit with a maximum call stack size exceeded error:

  ```
    ...
    MOCHA  Testing...
    RUNTIME EXCEPTION  Exception occurred while loading your tests
    [=========================] 100% (completed)
    RangeError: Maximum call stack size exceeded
      at RegExp.exec (<anonymous>)
      at retrieveSourceMapURL (/project/node_modules/source-map-support/source-map-support.js:174:21)
      at Array.<anonymous> (/project/node_modules/source-map-support/source-map-support.js:186:26)
      at /project/node_modules/source-map-support/source-map-support.js:85:24
      at mapSourcePosition (/project/node_modules/source-map-support/source-map-support.js:216:21)
    ...
  ```

  Issue has been reported but not fixed, suggested solutions did not
  work, see evanw/node-source-map-support#252.
- Update `vue-cli-plugin-electron-builder` to latest alpha version. This
  allows upgrading `ts-loader` to latest and using latest
  `electron-builder`. Change `main` property value in `package.json` to
  `index.js` for successful electron builds (see
  nklayman/vue-cli-plugin-electron-builder#188).
2023-08-01 00:24:06 +02:00
undergroundwires
8b374a37b4 mac: add script to disable personalized ads 2023-07-31 17:59:23 +02:00
undergroundwires
c404dfebe2 Add initial Linux support #150
Key features of Linux support:
- It supports python 3 scripts execution.
- It supports Flatpak and Snap installation for software
  clean-up/configurations.
- Extensive documentation.
2023-07-30 22:54:24 +02:00
undergroundwires
e8199932b4 Relax and improve code validation
Rework code validation to be bound to a context and not
context-independent. It means that the generated code is validated based
on different phases during the compilation. This is done by moving
validation from `ScriptCode` constructor to a different callable
function.

It removes duplicate detection for function calls once a call is fully
compiled, but still checks for duplicates inside each function body that
has inline code. This allows for having duplicates in final scripts
(thus relaxing the duplicate detection), e.g., when multiple calls to
the same function is made.

It fixes non-duplicates (when using common syntax) being misrepresented
as duplicate lines.

It improves the output of errors, such as printing valid lines, to give
more context. This improvement also fixes empty line validation not
showing the right empty lines in the error output. Empty line validation
shows tabs and whitespaces more clearly.

Finally, it adds more tests including tests for existing logic, such as
singleton factories.
2022-10-29 20:03:06 +02:00
undergroundwires
f4a7ca76b8 Rework icon with higher quality and new color
Change icon color to match the primary color of the theme (i.e.,
`#3a65ab`). The new color looks good on both dark and light surfaces
which solves #155.

Introduce SVG logo instead of PNG for better quality and scalability.

Improve icon creation. Introduce an automated script to create different
logo formats in different sizes enabling easier update of logo from
single place.
2022-10-13 21:26:30 +02:00
undergroundwires
64cca1d9b8 mac: add scripts to configure Parallels Desktop 2022-10-12 21:43:17 +02:00
undergroundwires
68a5d698a2 Add support for nested templates
Add support for expressions inside expressions.

Add support for templating where the output of one expression results in
another template part with expressions.

E.g., this did not work before, but compilation will now evaluate both
with expression with `$condition` and parameter substitution with
`$text`:

```
{{ with $condition }}
  echo '{{ $text }}'
{{ end }}
```

Add also more sanity checks (validation logic) when compiling
expressions to reveal problems quickly.
2022-10-11 20:42:38 +02:00
undergroundwires
bf0c55fa60 Drop support for dead browsers
It reduces distribution size to half of the size (50%) from 17.4 MB to
8.75 MB.

It follows new Vue default configuration vuejs/vue-cli#5232.

It drops support for `baidu 13.18`, `ie 11`, `ie 10`,  `bb 10`, `bb 7`,
`ie_mob 11`, `ie_mob 10`, `op_mob 12.1` as reported by
`npx browserslist`.
2022-10-09 22:38:35 +02:00
undergroundwires
e7b816d156 win: add scripts to downloaded file handling #153 2022-10-05 22:11:18 +02:00
Brice Dobson
a2e092190d win: add script to increase RSA key exchange #165
Add script to increase RSA key exchange to 2048-bit for ISS

Co-authored-by: undergroundwires <git@undergroundwires.dev>
2022-10-04 20:27:02 +02:00
undergroundwires
c1c2f2925f Break line in inline codes in documentation
It fixes the behavior where the whole text goes outside of the screen
(overflows) due to inline code not breaking.
2022-10-03 21:25:57 +02:00
undergroundwires
e8d06e0f3e Add multiline support for with expression
Improve templating support for block rendering for `with` expression
that has multiline code. This improves templating support to render
multiline code conditionally.

This did not work before but works now:

```
{{ with $middleLine }}
  first line
  second line
{{ end }}
```
2022-10-02 20:12:49 +02:00
undergroundwires
7d3670c26d Improve manual execution instructions
- Simplify alternatives to run the script.
- Change style of code parts to be easier to the eye:
  - Use the same font size as other texts in body.
  - Add vertical padding.
  - Align the contents (the code, copy button and dollar sign) in the
    middle.
  - Align information icon to center of context next to it.
- Fix minor typos and punctations.
- Refactor instruction list to be more generic to able to be used by
  other operating systems.
- Make dialogs scrollable so instruction list on smaller screens can be
  read until the end.
2022-10-01 19:50:45 +02:00
undergroundwires
430537f704 Use lowercase in script names and search text
Remove uppercase text transformation from node titles (script and
categories) and "no results found" text when searching. It increases the
readability giving it a clean look.
2022-09-30 19:50:46 +02:00
undergroundwires
58ed7b456b win: improve OneDrive removal
- Improve documentation for OneDrive removal scripts.
- Add support for deleting OneDrive icon from the navigation pane.
- Do not revert OneDrive install code on Windows 11 as it does not exist
  by default.
- Remove "Prevent automatic OneDrive install for new users" script as
  HKU scripts are not really supported elsewhere and makes the code
  harder to maintain.
- Do not print errors when the behavior is as expected. Surpress errors
  on registry key deletion, ensure re-running script does not cause any
  errors with proper checks.
- Change revert logic to match default Windows state.
- Hardcode service names for OneDrive to avoid side-effects.
- Rerruning OneDrive now runs it in background.
- Add Windows 11 support for running the installer/uninstaller.
- Rename scripts to simpler and easier-to-understand names
2022-09-29 23:27:46 +02:00
undergroundwires
6b3f4659df Use line endings based on script language #88
Use CRLF in batchfile and LF in shellscript.
2022-09-28 23:09:23 +02:00
undergroundwires
bbc6156281 win: add script to remove Widgets 2022-09-27 17:36:14 +02:00
undergroundwires
df533ad3b1 win: add more Visual Studio scripts, support 2022
Improve documentation for Visual Studio scripts.

Add different keys reported by community for deleting Visual Studio 2022
licenses, see beatcracker/VSCELicense#14 for the key reports.

Add cleanup for SQM files that Visual Studio generates when it is unable
to connect to internet, to send the data when online. Improve cleanup
for Visual Studio logs.

Change revert behavior of the scripts to match default state of clean
Visual Studio installation.
2022-09-26 21:37:32 +02:00
undergroundwires
6067bdb24e Improve documentation support with markdown
Rework documentation URLs as inline markdown.

Redesign documentations with markdown text.

Redesign way to document scripts/categories and present the
documentation.

Documentation is showed in an expandable box instead of tooltip. This is
to allow writing longer documentation (tooltips are meant to be used for
short text) and have better experience on mobile.

If a node (script/category) has documentation it's now shown with single
information icon (ℹ) aligned to right.

Add support for rendering documentation as markdown. It automatically
converts plain URLs to URLs with display names (e.g.
https://docs.microsoft.com/..) will be rendered automatically like
"docs.microsoft.com - Windows 11 Privacy...".
2022-09-25 23:25:43 +02:00
undergroundwires
924b326244 Fix broken npm installation and builds
This commit fixes two issues:
a. Fix `npm install` not working
b. Fix building not working after npm install fix.

npm install fails with dependency resolution issue due to Vue CLI as
following:

```txt
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR!
npm ERR! While resolving: cache-loader@4.1.0
...
```txt

As suggested in Vue issues regenerating packages-lock.json solves the
issue, see: vuejs/vue-cli#6793, vuejs/vue-cli#7095.

However with the new package-lock.json a Font Awesome dependency issue
breaks the builds such as the following:

```txt
ERROR in src/presentation/bootstrapping/Modules/IconBootstrapper.ts:17:7
TS2345: Argument of type 'IconDefinition' is not assignable to parameter of type 'IconDefinitionOrPack'.
  Type 'IconDefinition' is not assignable to type 'IconPack'.
    Index signature for type 'string' is missing in type 'IconDefinition'.
    15 |   public bootstrap(vue: VueConstructor): void {
    16 |     library.add(
  > 17 |       faGithub,
```

This is solved by adding a patch in `tsconfig.json`. This issue was
discussed in FortAwesome/Font-Awesome#12575 where the workaround was
recommended.
2022-09-22 20:13:20 +02:00
undergroundwires
8608072bfb Align card icons vertically in cards view
Align folder and battery icons to be on same vertical line on every
card.
2022-03-14 18:30:05 +01:00
undergroundwires
3233d9b802 Improve click/touch without unintended interaction
Disable selecting clickables as text. Selecting buttons leads to
unintended selection. This is seen when touching on clickables using
mobile devices.

Prevent blue highlight when touching on clickables. This is seen on
mobile webkit browsers. It looks ugly and the visual clue provided is
not needed beacuse all clickables on mobile already  have visual clues.
2022-03-13 12:45:17 +01:00
undergroundwires
99e24b4134 Improve touch like hover on devices without mouse
This commit improves mobile support. `:hover` CSS selector is not mobile
friendly because there is typically no mouse support on mobile. This
commit make hover behavior to become active during touch on mobile.

`:hover` selector is emulated on mobile devices. But this emulated
behavior is not desired. When emulated, the CSS style gets attached when
starting touching but does not get removed after stopping touching. This
sticky behavior is undesired.

This commit solve this issue by using Saas mixing that uses `:active`
selector instead of `:hover` when `:hover` is not really supported but
emulated.
2022-03-12 10:59:28 +01:00
undergroundwires
b210aaddf2 Improve script/category name validation
- Use better error messages with more context.
- Unify their validation logic and share tests.
- Validate also type of the name.
- Refactor node (Script/Category) parser tests for easier future
  changes and cleaner test code (using `TestBuilder` to do dirty work in
  unified way).
- Add more tests. Custom `Error` properties are compared manually due to
  `chai` not supporting deep equality checks (chaijs/chai#1065,
  chaijs/chai#1405).
2022-03-11 09:56:50 +01:00
undergroundwires-bot
65902e5b72 ⬆️ bump everywhere to 0.11.4 2022-03-08 17:32:34 +00:00
undergroundwires
efd63ff85d Bump dependencies to latest
Purge unused dependencies.

Update dependencies to latest except:

- ts-lint. Keep locked to 9.0.1 because that's the latest version that
  works with Webpack 4 that's still used by
  vue-cli-plugin-electron-builder.
- Keep eslint at version 7 because tests cannot be run/compiled with
  version 7, see eslint/eslint#15678, vuejs/vue-cli#6759.

Newer versions of ESLint modules do not allow linebreak after or before
= operator (operator-linebreak). This commit also changes files to
comply with it.

Closes #116, #119, #122, #130.
2022-03-08 18:03:19 +01:00
undergroundwires
242a497e7d Bump node environment to 16.x
- Bump setup-node action to v2.
- Use composite actions to reuse same setting. This is preferred over
  reusable templates because reusable templates are on job-level but
  setting up node should be a step.
2022-03-07 21:38:30 +01:00
undergroundwires
05a6a84c37 Add donation information 2022-03-05 13:10:27 +01:00
289 changed files with 31439 additions and 23008 deletions

View File

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

View File

@@ -1,4 +1,4 @@
[*.{js,jsx,ts,tsx,vue}] [*.{js,jsx,ts,tsx,vue,sh}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
end_of_line = lf end_of_line = lf

1
.eslintignore Normal file
View File

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

123
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,123 @@
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,
},
extends: [
// Vue specific rules, eslint-plugin-vue
// Added by Vue CLI
'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
// Added by Vue CLI
'@vue/typescript/recommended',
],
parserOptions: {
ecmaVersion: 12, // ECMA 2021
/*
Having 'latest' leads to:
```
Parsing error: ecmaVersion must be a number. Received value of type string instead
```
For .js files in the project
*/
},
rules: {
...getOwnRules(),
...getTurnedOffBrokenRules(),
...getOpinionatedRuleOverrides(),
...getTodoRules(),
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)',
],
env: {
mocha: true,
},
},
{
files: ['**/tests/**/*.{j,t}s?(x)'],
rules: {
'no-console': 'off',
},
},
],
};
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}*`);
}

View File

@@ -1,291 +0,0 @@
const { rules: baseBestPracticesRules } = require('eslint-config-airbnb-base/rules/best-practices');
const { rules: baseErrorsRules } = require('eslint-config-airbnb-base/rules/errors');
const { rules: baseES6Rules } = require('eslint-config-airbnb-base/rules/es6');
const { rules: baseImportsRules } = require('eslint-config-airbnb-base/rules/imports');
const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
const { rules: baseVariablesRules } = require('eslint-config-airbnb-base/rules/variables');
const tsconfigJson = require('./tsconfig.json');
module.exports = {
root: true,
env: {
node: true,
},
extends: [
// Vue specific rules, eslint-plugin-vue
// Added by Vue CLI
'plugin:vue/essential',
// Extends eslint-config-airbnb
// Added by Vue CLI
// Here until https://github.com/vuejs/eslint-config-airbnb/issues/23 is done
'@vue/airbnb',
// Extends @typescript-eslint/recommended
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
// Added by Vue CLI
'@vue/typescript/recommended',
],
parserOptions: {
ecmaVersion: 'latest',
},
rules: {
...getOwnRules(),
...getTurnedOffBrokenRules(),
...getOpinionatedRuleOverrides(),
...getTodoRules(),
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)',
],
env: {
mocha: true,
},
},
{
files: ['**/*.ts?(x)', '**/*.d.ts'],
parserOptions: {
// Setting project is required for some rules such as @typescript-eslint/dot-notation,
// @typescript-eslint/return-await and @typescript-eslint/no-throw-literal.
// If this property is missing they fail due to missing parser.
project: ['./tsconfig.json'],
},
rules: {
...getTypeScriptOverrides(),
},
},
{
files: ['**/tests/**/*.{j,t}s?(x)'],
rules: {
'no-console': 'off',
},
},
],
};
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',
// 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 getTypeScriptOverrides() {
/*
Here until Vue supports AirBnb Typescript overrides (vuejs/eslint-config-airbnb#23).
Based on `eslint-config-airbnb-typescript`.
Source: https://github.com/iamturns/eslint-config-airbnb-typescript/blob/v16.1.0/lib/shared.js
It cannot be used directly due to compilation errors.
*/
return {
'brace-style': 'off',
'@typescript-eslint/brace-style': baseStyleRules['brace-style'],
camelcase: 'off',
'@typescript-eslint/naming-convention': [
'error',
{ selector: 'variable', format: ['camelCase', 'PascalCase', 'UPPER_CASE'] },
{ selector: 'function', format: ['camelCase', 'PascalCase'] },
{ selector: 'typeLike', format: ['PascalCase'] },
],
'comma-dangle': 'off',
'@typescript-eslint/comma-dangle': [
baseStyleRules['comma-dangle'][0],
{
...baseStyleRules['comma-dangle'][1],
enums: baseStyleRules['comma-dangle'][1].arrays,
generics: baseStyleRules['comma-dangle'][1].arrays,
tuples: baseStyleRules['comma-dangle'][1].arrays,
},
],
'comma-spacing': 'off',
'@typescript-eslint/comma-spacing': baseStyleRules['comma-spacing'],
'default-param-last': 'off',
'@typescript-eslint/default-param-last': baseBestPracticesRules['default-param-last'],
'dot-notation': 'off',
'@typescript-eslint/dot-notation': baseBestPracticesRules['dot-notation'],
'func-call-spacing': 'off',
'@typescript-eslint/func-call-spacing': baseStyleRules['func-call-spacing'],
// ❌ Broken for some cases, but still useful.
// Here until Prettifier is used.
indent: 'off',
'@typescript-eslint/indent': baseStyleRules.indent,
'keyword-spacing': 'off',
'@typescript-eslint/keyword-spacing': baseStyleRules['keyword-spacing'],
'lines-between-class-members': 'off',
'@typescript-eslint/lines-between-class-members': baseStyleRules['lines-between-class-members'],
'no-array-constructor': 'off',
'@typescript-eslint/no-array-constructor': baseStyleRules['no-array-constructor'],
'no-dupe-class-members': 'off',
'@typescript-eslint/no-dupe-class-members': baseES6Rules['no-dupe-class-members'],
'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': baseBestPracticesRules['no-empty-function'],
'no-extra-parens': 'off',
'@typescript-eslint/no-extra-parens': baseErrorsRules['no-extra-parens'],
'no-extra-semi': 'off',
'@typescript-eslint/no-extra-semi': baseErrorsRules['no-extra-semi'],
// ❌ Fails due to missing parser
// 'no-implied-eval': 'off',
// 'no-new-func': 'off',
// '@typescript-eslint/no-implied-eval': baseBestPracticesRules['no-implied-eval'],
'no-loss-of-precision': 'off',
'@typescript-eslint/no-loss-of-precision': baseErrorsRules['no-loss-of-precision'],
'no-loop-func': 'off',
'@typescript-eslint/no-loop-func': baseBestPracticesRules['no-loop-func'],
'no-magic-numbers': 'off',
'@typescript-eslint/no-magic-numbers': baseBestPracticesRules['no-magic-numbers'],
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': baseBestPracticesRules['no-redeclare'],
// ESLint variant does not work with TypeScript enums.
'no-shadow': 'off',
'@typescript-eslint/no-shadow': baseVariablesRules['no-shadow'],
'no-throw-literal': 'off',
'@typescript-eslint/no-throw-literal': baseBestPracticesRules['no-throw-literal'],
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': baseBestPracticesRules['no-unused-expressions'],
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': baseVariablesRules['no-unused-vars'],
// https://erkinekici.com/articles/linting-trap#no-use-before-define
// 'no-use-before-define': 'off',
// '@typescript-eslint/no-use-before-define': baseVariablesRules['no-use-before-define'],
// ESLint variant does not understand TypeScript constructors.
// eslint/eslint/#14118, typescript-eslint/typescript-eslint#873
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': baseES6Rules['no-useless-constructor'],
quotes: 'off',
'@typescript-eslint/quotes': baseStyleRules.quotes,
semi: 'off',
'@typescript-eslint/semi': baseStyleRules.semi,
'space-before-function-paren': 'off',
'@typescript-eslint/space-before-function-paren': baseStyleRules['space-before-function-paren'],
'require-await': 'off',
'@typescript-eslint/require-await': baseBestPracticesRules['require-await'],
'no-return-await': 'off',
'@typescript-eslint/return-await': baseBestPracticesRules['no-return-await'],
'space-infix-ops': 'off',
'@typescript-eslint/space-infix-ops': baseStyleRules['space-infix-ops'],
'object-curly-spacing': 'off',
'@typescript-eslint/object-curly-spacing': baseStyleRules['object-curly-spacing'],
'import/extensions': [
baseImportsRules['import/extensions'][0],
baseImportsRules['import/extensions'][1],
{
...baseImportsRules['import/extensions'][2],
ts: 'never',
tsx: 'never',
},
],
// Changes required is not yet implemented:
// 'import/no-extraneous-dependencies': [
// baseImportsRules['import/no-extraneous-dependencies'][0],
// {
// ...baseImportsRules['import/no-extraneous-dependencies'][1],
// devDependencies: baseImportsRules[
// 'import/no-extraneous-dependencies'
// ][1].devDependencies.reduce((result, devDep) => {
// const toAppend = [devDep];
// const devDepWithTs = devDep.replace(/\bjs(x?)\b/g, 'ts$1');
// if (devDepWithTs !== devDep) {
// toAppend.push(devDepWithTs);
// }
// return [...result, ...toAppend];
// }, []),
// },
// ],
};
}
function getAliasesFromTsConfig() {
return Object.keys(tsconfigJson.compilerOptions.paths)
.map((path) => `${path}*`);
}

View File

@@ -21,8 +21,9 @@ A clear and concise description of what the bug is.
<!-- <!--
Which OS are you using? What version of OS you were using? Which OS are you using? What version of OS you were using?
On Windows 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 ### Reproduction steps

View File

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

8
.github/actions/setup-node/action.yml vendored Normal file
View File

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

View File

@@ -18,9 +18,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: 15.x
- -
name: Install dependencies name: Install dependencies
run: npm ci run: npm ci
@@ -42,9 +40,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: 15.x
- -
name: Install dependencies name: Install dependencies
run: npm ci run: npm ci
@@ -57,3 +53,23 @@ jobs:
run: |- run: |-
cross-env-shell NODE_ENV=${{ matrix.mode }} cross-env-shell NODE_ENV=${{ matrix.mode }}
npm run electron:build -- --publish never --mode ${{ matrix.mode }} npm run electron:build -- --publish never --mode ${{ matrix.mode }}
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 create-icons

View File

@@ -19,9 +19,7 @@ jobs:
- 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: 15.x
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Lint - name: Lint

View File

@@ -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: 15.x
- -
name: NPM audit name: NPM audit
run: exit "$(npm audit)" # Since node 15.x, it does not fail with error if we don't explicitly exit run: npm audit --omit=dev

View File

@@ -20,9 +20,7 @@ jobs:
- name: Checkout to bump commit - name: Checkout to bump commit
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)" run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
- name: Setup node - name: Setup node
uses: actions/setup-node@v1 uses: ./.github/actions/setup-node
with:
node-version: 15.x
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Run unit tests - name: Run unit tests
@@ -31,4 +29,4 @@ jobs:
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074 EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074

View File

@@ -1,8 +1,8 @@
name: release-site name: 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: 15.x
- -
name: "App: Install dependencies" name: "App: Install dependencies"
run: npm ci run: npm ci
working-directory: site working-directory: app
- -
name: "App: Run unit tests" name: "App: Run unit tests"
run: npm run test:unit run: npm run test:unit
working-directory: 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}} \

View File

@@ -17,9 +17,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: 15.x
- -
name: Install dependencies name: Install dependencies
run: npm ci run: npm ci

View File

@@ -19,9 +19,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: 15.x
- -
name: Install dependencies name: Install dependencies
run: npm ci run: npm ci

View File

@@ -16,10 +16,8 @@ jobs:
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: 15.x
- -
name: Install dependencies name: Install dependencies
run: npm ci run: npm ci

View File

@@ -1,5 +1,62 @@
# Changelog # Changelog
## 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) ## 0.11.3 (2022-01-05)
* Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd) * Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd)

View File

@@ -1,16 +1,16 @@
# 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 🍑🍆
<!-- markdownlint-disable MD033 --> <!-- markdownlint-disable MD033 -->
<p align="center"> <p align="center">
<a href="https://undergroundwires.dev/donate?project=privacy.sexy"> <a href="https://undergroundwires.dev/donate?project=privacy.sexy" target="_blank" rel="noopener noreferrer">
<img <img
alt="donation badge" alt="donation badge"
src="https://undergroundwires.dev/img/badges/donate/flat.svg" src="https://undergroundwires.dev/img/badges/donate/flat.svg"
/> />
</a> </a>
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md"> <a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md" target="_blank" rel="noopener noreferrer">
<img <img
alt="contributions are welcome" alt="contributions are welcome"
src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat" src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"
@@ -18,13 +18,13 @@
</a> </a>
<!-- Code quality --> <!-- Code quality -->
<br /> <br />
<a href="https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript"> <a href="https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript" target="_blank" rel="noopener noreferrer">
<img <img
alt="Language grade: JavaScript/TypeScript" alt="Language grade: JavaScript/TypeScript"
src="https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18" src="https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18"
/> />
</a> </a>
<a href="https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability"> <a href="https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability" target="_blank" rel="noopener noreferrer">
<img <img
alt="Maintainability" alt="Maintainability"
src="https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability" src="https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability"
@@ -32,19 +32,19 @@
</a> </a>
<!-- Tests --> <!-- Tests -->
<br /> <br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.unit.yaml"> <a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.unit.yaml" target="_blank" rel="noopener noreferrer">
<img <img
alt="Unit tests status" alt="Unit tests status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/unit-tests/badge.svg" src="https://github.com/undergroundwires/privacy.sexy/workflows/unit-tests/badge.svg"
/> />
</a> </a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.integration.yaml"> <a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.integration.yaml" target="_blank" rel="noopener noreferrer">
<img <img
alt="Integration tests status" alt="Integration tests status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/integration-tests/badge.svg" src="https://github.com/undergroundwires/privacy.sexy/workflows/integration-tests/badge.svg"
/> />
</a> </a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.e2e.yaml"> <a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.e2e.yaml" target="_blank" rel="noopener noreferrer">
<img <img
alt="E2E tests status" alt="E2E tests status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/e2e-tests/badge.svg" src="https://github.com/undergroundwires/privacy.sexy/workflows/e2e-tests/badge.svg"
@@ -52,19 +52,19 @@
</a> </a>
<!-- Checks --> <!-- Checks -->
<br /> <br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml"> <a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
<img <img
alt="Quality checks status" alt="Quality checks status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg" src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
/> />
</a> </a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.yaml"> <a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.yaml" target="_blank" rel="noopener noreferrer">
<img <img
alt="Security checks status" alt="Security checks status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/security-checks/badge.svg" src="https://github.com/undergroundwires/privacy.sexy/workflows/security-checks/badge.svg"
/> />
</a> </a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml"> <a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
<img <img
alt="Build checks status" alt="Build checks status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg" src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
@@ -72,19 +72,19 @@
</a> </a>
<!-- Release --> <!-- Release -->
<br /> <br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml"> <a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml" target="_blank" rel="noopener noreferrer">
<img <img
alt="Git release status" alt="Git release status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-git/badge.svg" src="https://github.com/undergroundwires/privacy.sexy/workflows/release-git/badge.svg"
/> />
</a> </a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.site.yaml"> <a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.site.yaml" target="_blank" rel="noopener noreferrer">
<img <img
alt="Site release status" alt="Site release status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-site/badge.svg" src="https://github.com/undergroundwires/privacy.sexy/workflows/release-site/badge.svg"
/> />
</a> </a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.desktop.yaml"> <a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.desktop.yaml" target="_blank" rel="noopener noreferrer">
<img <img
alt="Desktop application release status" alt="Desktop application release status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-desktop/badge.svg" src="https://github.com/undergroundwires/privacy.sexy/workflows/release-desktop/badge.svg"
@@ -92,7 +92,7 @@
</a> </a>
<!-- Others --> <!-- Others -->
<br /> <br />
<a href="https://github.com/undergroundwires/bump-everywhere"> <a href="https://github.com/undergroundwires/bump-everywhere" target="_blank" rel="noopener noreferrer">
<img <img
alt="Auto-versioned by bump-everywhere" alt="Auto-versioned by bump-everywhere"
src="https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true" src="https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true"
@@ -120,15 +120,15 @@ Online version does not require to run any software on your computer. Offline ve
- **Reversible**. Revert if something feels wrong. - **Reversible**. Revert if something feels wrong.
- **Accessible**. No need to run any compiled software on your computer with web version. - **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). - **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. - **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). - **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. - **Portable and simple**. Every script is independently executable without cross-dependencies.
## Support ## Support
**Sponsor 💕**. This project is free, and it might not be tempting to donate since you don't have to pay. But your donations will ensure that this project stays alive. A monthly coffee from you would make a difference. Recurring donations allow me to spend more time and resources on this project. 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). **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).
**Star 🤩**. I know that not everyone can afford donating a coffee to show support. In this case, feel free to give it a star ⭐ . It helps me to see that you appreciate the project. **Star 🤩**. Feel free to give it a star ⭐ .
**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). **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).

View File

@@ -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).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 740 B

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 353 KiB

15
cypress.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'cypress';
import setupPlugins from './tests/e2e/plugins/index.js';
export default defineConfig({
fixturesFolder: 'tests/e2e/fixtures',
screenshotsFolder: 'tests/e2e/screenshots',
videosFolder: 'tests/e2e/videos',
e2e: {
setupNodeEvents(on, config) {
return setupPlugins(on, config);
},
specPattern: 'tests/e2e/specs/**/*.cy.{js,jsx,ts,tsx}',
supportFile: 'tests/e2e/support/index.js',
},
});

View File

@@ -1,3 +0,0 @@
{
"pluginsFile": "tests/e2e/plugins/index.js"
}

View File

@@ -27,6 +27,8 @@ Application layer depends on and consumes domain layer. [Presentation layer](./p
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. 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. Each layer treat application layer differently.
![State](./../img/architecture/app-state.png) ![State](./../img/architecture/app-state.png)
@@ -45,7 +47,7 @@ Each layer treat application layer differently.
- So state is mutable, and fires related events when mutated. - So state is mutable, and fires related events when mutated.
- 📖 Read more: [application.md | Application state](./application.md#application-state). - 📖 Read more: [application.md | Application state](./application.md#application-state).
It's comparable with flux ([`redux`](https://redux.js.org/)) or flux-like ([`vuex`](https://vuex.vuejs.org/)) patterns. Flux component "view" is [presentation layer](./presentation.md) in Vue. Flux functions "dispatcher", "store" and "action creation" functions lie in the [application layer](./application.md). A difference is that application state in privacy.sexy is mutable and lies in single flux "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). 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 ## AWS infrastructure

View File

@@ -12,9 +12,9 @@ Everything that's merged in the master goes directly to production.
privacy.sexy uses [GitHub actions](https://github.com/features/actions) to define and run pipelines as code. 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] . GitHub workflows i.e. pipelines exist in [`/.github/workflows/`](./../.github/workflows/) folder without any subfolders due to GitHub actions requirements [1] .
[1]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows Local GitHub actions are defined in [`/.github/actions/`](./../.github/actions/) and used to reuse same workflow steps.
## Pipeline types ## Pipeline types

View File

@@ -2,8 +2,8 @@
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/) - privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
- 💡 Best practices - 💡 Best practices
- If you repeat yourself, try to utilize [YAML-defined functions](#Function) - If you repeat yourself, try to utilize [YAML-defined functions](#function)
- Always try to add documentation and a way to revert a tweak in [scripts](#Script) - Always try to add documentation and a way to revert a tweak in [scripts](#script)
- 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts) - 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
## Objects ## Objects
@@ -13,19 +13,19 @@
- A collection simply defines: - A collection simply defines:
- different categories and their scripts in a tree structure - different categories and their scripts in a tree structure
- OS specific details - OS specific details
- Also allows defining common [function](#Function)s to be used throughout the collection if you'd like different scripts to share same code. - Also allows defining common [function](#function)s to be used throughout the collection if you'd like different scripts to share same code.
#### `Collection` syntax #### `Collection` syntax
- `os:` *`string`* (**required**) - `os:` *`string`* (**required**)
- Operating system that the [Collection](#collection) is written for. - Operating system that the [Collection](#collection) is written for.
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values. - 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
- `actions: [` ***[`Category`](#Category)*** `, ... ]` **(required)** - `actions: [` ***[`Category`](#category)*** `, ... ]` **(required)**
- Each [category](#category) is rendered as different cards in card presentation. - Each [category](#category) is rendered as different cards in card presentation.
- ❗ A [Collection](#collection) must consist of at least one category. - ❗ A [Collection](#collection) must consist of at least one category.
- `functions: [` ***[`Function`](#Function)*** `, ... ]` - `functions: [` ***[`Function`](#function)*** `, ... ]`
- Functions are optionally defined to re-use the same code throughout different scripts. - Functions are optionally defined to re-use the same code throughout different scripts.
- `scripting:` ***[`ScriptingDefinition`](#ScriptingDefinition)*** **(required)** - `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
- Defines the scripting language that the code of other action uses. - Defines the scripting language that the code of other action uses.
### `Category` ### `Category`
@@ -38,9 +38,12 @@
- `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`
@@ -67,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
@@ -120,7 +123,7 @@
- Convention is to use camelCase, and be verbs. - Convention is to use camelCase, and be verbs.
- E.g. `uninstallStoreApp` - E.g. `uninstallStoreApp`
- ❗ Function names must be unique - ❗ Function names must be unique
- `parameters`: `[` ***[`FunctionParameter`](#FunctionParameter)*** `, ... ]` - `parameters`: `[` ***[`FunctionParameter`](#functionparameter)*** `, ... ]`
- List of parameters that function code refers to. - List of parameters that function code refers to.
- ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions) - ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
`code`: *`string`* (**required** if `call` is undefined) `code`: *`string`* (**required** if `call` is undefined)
@@ -133,7 +136,7 @@
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1` - E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0` - then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in code - 💡 [Expressions (templating)](./templating.md#expressions) can be used in code
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**) - `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**)
- A shared function or sequence of functions to call (called in order) - A shared function or sequence of functions to call (called in order)
- The parameter values that are sent can use [expressions (templating)](./templating.md#expressions) - The parameter values that are sent can use [expressions (templating)](./templating.md#expressions)
- ❗ If not defined `code` must be defined - ❗ If not defined `code` must be defined
@@ -141,7 +144,7 @@
### `FunctionParameter` ### `FunctionParameter`
- Defines a parameter that function requires optionally or mandatory. - Defines a parameter that function requires optionally or mandatory.
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#FunctionCall). - Its arguments are provided by a [Script](#script) through a [FunctionCall](#functioncall).
#### `FunctionParameter` syntax #### `FunctionParameter` syntax

View File

@@ -45,6 +45,14 @@ You could run other types of tests as well, but they may take longer time and ov
- Build web application: `npm run build` - Build web application: `npm run build`
- Build desktop application: `npm run electron:build` - Build desktop application: `npm run electron:build`
- (Re)create icons (see [documentation](../img/README.md)): `npm run create-icons`
### 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 ## Recommended extensions

View File

@@ -1,8 +1,10 @@
# Presentation layer # Presentation layer
Presentation layer consists of UI-related code. It uses Vue.js as JavaScript framework and includes Vue.js components. It also includes [Electron](https://www.electronjs.org/) to provide functionality to desktop application. The presentation layer handles UI concerns using Vue as JavaScript framework and Electron to provide desktop functionality.
It's designed event-driven from bottom to top. It listens user events (from top) and state events (from bottom) to update state or the GUI. 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. 📖 Refer to [architecture.md (Layered Application)](./architecture.md#layered-application) to read more about the layered architecture.
@@ -12,6 +14,7 @@ It's designed event-driven from bottom to top. It listens user events (from top)
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins. - [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes. - [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that other components share. - [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that other components share.
- [**`hooks`**](../src/presentation/components/Shared/Hooks): Shared hooks for state access
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that webpack will process. - [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that webpack will process.
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts - [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components. - [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
@@ -22,13 +25,17 @@ It's designed event-driven from bottom to top. It listens user events (from top)
- [**`electron/`**](./../src/presentation/electron/): Electron configuration for the desktop application. - [**`electron/`**](./../src/presentation/electron/): Electron configuration for the desktop application.
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts. - [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
- [**`/public/`**](./../public/): Contains static assets that are directly copied and do not go through webpack. - [**`/public/`**](./../public/): Contains static assets that are directly copied and do not go through webpack.
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`. - [**`/vue.config.cjs`**](./../vue.config.cjs): Global Vue CLI configurations loaded by `@vue/cli-service`.
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations used by Vue CLI internally. - [**`/postcss.config.cjs`**](./../postcss.config.cjs): PostCSS configurations used by Vue CLI internally.
- [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`. - [**`/babel.config.cjs`**](./../babel.config.cjs): Babel configurations for polyfills used by `@vue/cli-plugin-babel`.
## Visual design best-practices
Add visual clues for clickable items. It should be as clear as possible that they're interactable at first look without hovering. They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility.
## Application data ## Application data
Components (should) use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again. 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) is an immutable domain model that represents application state. It includes: [Application.ts](../src/domain/Application.ts) is an immutable domain model that represents application state. It includes:
@@ -39,32 +46,45 @@ You can read more about how application layer provides application data to he pr
## Application state ## Application state
Inheritance of a Vue components marks whether it uses application state . Components that does not handle application state extends `Vue`. Stateful components mutate or/and react to state changes (such as user selection or search queries) in [ApplicationContext](./../src/application/Context/ApplicationContext.ts) extend [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) class to access the context / state. This project uses a singleton instance of the application state, making it available to all Vue components.
[`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) functions include: 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.
- Creating a singleton of the state and makes it available to presentation layer as single source of truth. 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.
- Providing virtual abstract `handleCollectionState` callback that it calls when
- the Vue loads the component,
- and also every time when state changes.
- Providing `events` member to make lifecycling of state subscriptions events easier because it ensures that components unsubscribe from listening to state events when
- the component is no longer used (destroyed),
- an if [ApplicationContext](./../src/application/Context/ApplicationContext.ts) changes the active [collection](./collection-files.md) to a different one.
📖 Refer to [architecture.md | Application State](./architecture.md#application-state) to get an overview of event handling and [application.md | Application State](./presentation.md#application-state) for deeper look into how the application layer manages state. [`UseCollectionState.ts`](./../src/presentation/components/Shared/Hooks/UseCollectionState.ts) provides several functionalities including:
## Modals - **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).
[Dialog.vue](./../src/presentation/components/Shared/Dialog.vue) is a shared component that other components used to show modal windows. 📖 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.
You can use it by wrapping the content inside of its `slot` and call `.show()` function on its reference. For example: ## Dependency injections
```html The presentation layer uses Vue's native dependency injection system to increase testability and decouple components.
<Dialog ref="testDialog">
<div>Hello world</div> To add a new dependency:
</Dialog>
<div @click="$refs.testDialog.show()">Show dialog</div> 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.
## Sass naming convention ## Sass naming convention

View File

@@ -10,10 +10,19 @@
- Expressions start and end with mustaches (double brackets, `{{` and `}}`). - Expressions start and end with mustaches (double brackets, `{{` and `}}`).
- E.g. `Hello {{ $name }} !` - E.g. `Hello {{ $name }} !`
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) that has inspired this templating language. - Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
- Functions enables usage of expressions. - Functions enables usage of expressions.
- In script definition parts of a function, see [`Function`](./collection-files.md#Function). - In script definition parts of a function, see [`Function`](./collection-files.md#Function).
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function). - When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
- Expressions inside expressions (nested templates) are supported.
- An expression can output another expression that will also be compiled.
- E.g. following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output.
```go
{{ with $condition }}
echo {{ $text }}
{{ end }}
```
### Parameter substitution ### Parameter substitution
@@ -56,9 +65,31 @@ A function can call other functions such as:
### with ### with
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions. E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}`. 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..
Binds its context (`.`) value of provided argument for the parameter if provided one. E.g. `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`. 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 }}`. 💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.

View File

@@ -18,6 +18,7 @@ Common aspects for all tests:
- Unit tests test each component in isolation. - Unit tests test each component in isolation.
- All unit tests goes under [`./tests/unit`](./../tests/unit). - All unit tests goes under [`./tests/unit`](./../tests/unit).
- They rely on [stubs](./../tests/unit/shared/Stubs) for isolation. - They rely on [stubs](./../tests/unit/shared/Stubs) for isolation.
- Unit tests include also Vue component tests using `@vue/test-utils`.
### Unit tests structure ### Unit tests structure
@@ -72,7 +73,7 @@ Common aspects for all tests:
- Vue CLI plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) configures E2E tests. - Vue CLI plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) configures E2E tests.
- Test names and folders have logical structure based on tests executed. - Test names and folders have logical structure based on tests executed.
- The structure is following: - The structure is following:
- [`cypress.json`](./../cypress.json): Cypress configuration file. - [`cypress.config.ts`](./../cypress.config.ts): Cypress configuration file.
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder. - [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
- [`/specs/`](./../tests/e2e/specs/): Test files named with `.spec.js` extension. - [`/specs/`](./../tests/e2e/specs/): Test files named with `.spec.js` extension.
- [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before loading project. - [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before loading project.

12
img/README.md Normal file
View File

@@ -0,0 +1,12 @@
# img
This folder contains image files and other resources related to images.
## logo.svg
[logo.svg](./logo.svg) is the master logo from which all other icons or images are created from.
It should be the only file that will be changed manually.
[`logo-update.mjs`](./logo-update.mjs) script in this folder updates all the logo files.
It should be executed everytime the logo is changed.
It automates recreation of logo files in different formats.

127
img/logo-update.mjs Normal file
View 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, '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();

56
img/logo.svg Normal file
View 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

35570
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,92 +1,107 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.11.3", "version": "0.12.0",
"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", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit", "test:unit": "vue-cli-service test:unit --include ./tests/bootstrap/setup.ts",
"test:e2e": "vue-cli-service test:e2e", "test:e2e": "vue-cli-service test:e2e",
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml", "lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
"create-icons": "node img/logo-update.mjs",
"electron:build": "vue-cli-service electron:build", "electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve", "electron:serve": "vue-cli-service electron:serve",
"lint:eslint": "vue-cli-service lint --no-fix --mode production",
"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:eslint": "vue-cli-service lint --no-fix --mode production",
"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",
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\"" "test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\" --include ./tests/bootstrap/setup.ts"
}, },
"main": "background.js", "main": "index.js",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^5.15.4", "@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^2.0.6", "@fortawesome/vue-fontawesome": "^2.0.9",
"@juggle/resize-observer": "^3.3.1", "@juggle/resize-observer": "^3.4.0",
"ace-builds": "^1.4.13", "ace-builds": "^1.23.4",
"core-js": "^3.18.3", "core-js": "^3.32.0",
"cross-fetch": "^3.1.4", "cross-fetch": "^4.0.0",
"electron-progressbar": "^2.0.1", "electron-progressbar": "^2.1.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"install": "^0.13.0", "install": "^0.13.0",
"liquor-tree": "^0.2.70", "liquor-tree": "^0.2.70",
"npm": "^8.1.1", "markdown-it": "^13.0.1",
"npm": "^9.8.1",
"v-tooltip": "2.1.3", "v-tooltip": "2.1.3",
"vue": "^2.6.14", "vue": "^2.7.14"
"vue-class-component": "^7.2.6",
"vue-js-modal": "^2.0.1",
"vue-property-decorator": "^9.1.2"
}, },
"devDependencies": { "devDependencies": {
"@types/ace": "0.0.47", "@rushstack/eslint-patch": "^1.3.2",
"@types/chai": "^4.2.22", "@types/ace": "^0.0.48",
"@types/file-saver": "^2.0.3", "@types/chai": "^4.3.5",
"@types/mocha": "^9.0.0", "@types/file-saver": "^2.0.5",
"@typescript-eslint/eslint-plugin": "^5.4.0", "@types/mocha": "^10.0.1",
"@typescript-eslint/parser": "^5.4.0", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@vue/cli-plugin-babel": "~5.0.0-rc.1", "@typescript-eslint/parser": "^5.62.0",
"@vue/cli-plugin-e2e-cypress": "~5.0.0-rc.1", "@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.0-rc.1", "@vue/cli-plugin-e2e-cypress": "~5.0.8",
"@vue/cli-plugin-typescript": "~5.0.0-rc.1", "@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-unit-mocha": "~5.0.0-rc.1", "@vue/cli-plugin-typescript": "~5.0.8",
"@vue/cli-service": "~5.0.0-rc.1", "@vue/cli-plugin-unit-mocha": "~5.0.8",
"@vue/eslint-config-airbnb": "^6.0.0", "@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^9.1.0", "@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
"@vue/test-utils": "1.2.2", "@vue/eslint-config-typescript": "^11.0.3",
"chai": "^4.3.4", "@vue/test-utils": "^1.3.6",
"cypress": "^8.3.0", "chai": "^4.3.7",
"electron": "^15.3.0", "cypress": "^12.17.2",
"electron-builder": "^22.14.13", "electron": "^25.3.2",
"electron-builder": "^24.6.3",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-log": "^4.4.1", "electron-icon-builder": "^2.0.1",
"electron-updater": "^4.3.9", "electron-log": "^4.4.8",
"eslint": "^7.32.0", "electron-updater": "^6.1.4",
"eslint-plugin-import": "^2.25.3", "eslint": "^8.46.0",
"eslint-plugin-vue": "^8.0.3", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-vuejs-accessibility": "^1.1.0", "eslint-plugin-vue": "^9.6.0",
"eslint-plugin-vuejs-accessibility": "^1.2.0",
"icon-gen": "^3.0.1",
"js-yaml-loader": "^1.2.2", "js-yaml-loader": "^1.2.2",
"markdownlint-cli": "^0.29.0", "markdownlint-cli": "^0.35.0",
"remark-cli": "^10.0.0", "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": "^5.1.0", "remark-preset-lint-consistent": "^5.1.2",
"remark-validate-links": "^11.0.1", "remark-validate-links": "^12.1.1",
"sass": "^1.43.3", "sass": "^1.64.1",
"sass-loader": "10.2.0", "sass-loader": "^13.3.2",
"ts-loader": "9.0.1", "svgexport": "^0.4.2",
"tslib": "^2.3.1", "ts-loader": "^9.4.4",
"typescript": "^4.4.4", "tslib": "~2.4.0",
"vue-cli-plugin-electron-builder": "^2.1.1", "typescript": "~4.6.2",
"vue-template-compiler": "^2.6.14", "vue-cli-plugin-electron-builder": "^3.0.0-alpha.4",
"yaml-lint": "^1.2.4" "yaml-lint": "^1.7.0"
},
"overrides": {
"vue-cli-plugin-electron-builder": {
"electron-builder": "^24.6.3"
}
}, },
"//devDependencies": { "//devDependencies": {
"ts-loader": "Here as workaround for vue-cli-plugin-electron-builder using older webpack 4" "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": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -2,9 +2,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows and macOS</title> <title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows, macOS and Linux</title>
<meta name="robots" content="index,follow" /> <meta name="robots" content="index,follow" />
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/> <meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">

74
scripts/configure-vscode.sh Executable file
View 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
View 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"

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

View File

@@ -1,6 +1,5 @@
import { ICodeBuilder } from './ICodeBuilder'; import { ICodeBuilder } from './ICodeBuilder';
const NewLine = '\n';
const TotalFunctionSeparatorChars = 58; const TotalFunctionSeparatorChars = 58;
export abstract class CodeBuilder implements ICodeBuilder { export abstract class CodeBuilder implements ICodeBuilder {
@@ -59,10 +58,12 @@ export abstract class CodeBuilder implements ICodeBuilder {
} }
public toString(): string { public toString(): string {
return this.lines.join(NewLine); return this.lines.join(this.getNewLineTerminator());
} }
protected abstract getCommentDelimiter(): string; protected abstract getCommentDelimiter(): string;
protected abstract writeStandardOut(text: string): string; protected abstract writeStandardOut(text: string): string;
protected abstract getNewLineTerminator(): string;
} }

View File

@@ -8,6 +8,10 @@ export class BatchBuilder extends CodeBuilder {
protected writeStandardOut(text: string): string { protected writeStandardOut(text: string): string {
return `echo ${escapeForEcho(text)}`; return `echo ${escapeForEcho(text)}`;
} }
protected getNewLineTerminator(): string {
return '\r\n';
}
} }
function escapeForEcho(text: string) { function escapeForEcho(text: string) {

View File

@@ -8,6 +8,10 @@ export class ShellBuilder extends CodeBuilder {
protected writeStandardOut(text: string): string { protected writeStandardOut(text: string): string {
return `echo '${escapeForEcho(text)}'`; return `echo '${escapeForEcho(text)}'`;
} }
protected getNewLineTerminator(): string {
return '\n';
}
} }
function escapeForEcho(text: string) { function escapeForEcho(text: string) {

View File

@@ -0,0 +1,4 @@
export enum FilterActionType {
Apply,
Clear,
}

View File

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

View File

@@ -0,0 +1,14 @@
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { FilterActionType } from './FilterActionType';
export interface IFilterChangeDetails {
readonly actionType: FilterActionType;
readonly filter?: IFilterResult;
visit(visitor: IFilterChangeDetailsVisitor): void;
}
export interface IFilterChangeDetailsVisitor {
onClear(): void;
onApply(filter: IFilterResult): void;
}

View File

@@ -1,13 +1,13 @@
import { IEventSource } from '@/infrastructure/Events/IEventSource'; import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IFilterResult } from './IFilterResult'; import { IFilterResult } from './IFilterResult';
import { IFilterChangeDetails } from './Event/IFilterChangeDetails';
export interface IReadOnlyUserFilter { export interface IReadOnlyUserFilter {
readonly currentFilter: IFilterResult | undefined; readonly currentFilter: IFilterResult | undefined;
readonly filtered: IEventSource<IFilterResult>; readonly filterChanged: IEventSource<IFilterChangeDetails>;
readonly filterRemoved: IEventSource<void>;
} }
export interface IUserFilter extends IReadOnlyUserFilter { export interface IUserFilter extends IReadOnlyUserFilter {
setFilter(filter: string): void; applyFilter(filter: string): void;
removeFilter(): void; clearFilter(): void;
} }

View File

@@ -4,11 +4,11 @@ import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { FilterResult } from './FilterResult'; import { FilterResult } from './FilterResult';
import { IFilterResult } from './IFilterResult'; import { IFilterResult } from './IFilterResult';
import { IUserFilter } from './IUserFilter'; import { IUserFilter } from './IUserFilter';
import { IFilterChangeDetails } from './Event/IFilterChangeDetails';
import { FilterChange } from './Event/FilterChange';
export class UserFilter implements IUserFilter { export class UserFilter implements IUserFilter {
public readonly filtered = new EventSource<IFilterResult>(); public readonly filterChanged = new EventSource<IFilterChangeDetails>();
public readonly filterRemoved = new EventSource<void>();
public currentFilter: IFilterResult | undefined; public currentFilter: IFilterResult | undefined;
@@ -16,9 +16,9 @@ export class UserFilter implements IUserFilter {
} }
public setFilter(filter: string): void { public applyFilter(filter: string): void {
if (!filter) { if (!filter) {
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter'); throw new Error('Filter must be defined and not empty. Use clearFilter() to remove the filter');
} }
const filterLowercase = filter.toLocaleLowerCase(); const filterLowercase = filter.toLocaleLowerCase();
const filteredScripts = this.collection.getAllScripts().filter( const filteredScripts = this.collection.getAllScripts().filter(
@@ -33,12 +33,12 @@ export class UserFilter implements IUserFilter {
filter, filter,
); );
this.currentFilter = matches; this.currentFilter = matches;
this.filtered.notify(matches); this.filterChanged.notify(FilterChange.forApply(this.currentFilter));
} }
public removeFilter(): void { public clearFilter(): void {
this.currentFilter = undefined; this.currentFilter = undefined;
this.filterRemoved.notify(); this.filterChanged.notify(FilterChange.forClear());
} }
} }

View File

@@ -4,6 +4,7 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import WindowsData from '@/application/collections/windows.yaml'; import WindowsData from '@/application/collections/windows.yaml';
import MacOsData from '@/application/collections/macos.yaml'; import MacOsData from '@/application/collections/macos.yaml';
import LinuxData from '@/application/collections/linux.yaml';
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser'; import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
import { Application } from '@/domain/Application'; import { Application } from '@/domain/Application';
import { parseCategoryCollection } from './CategoryCollectionParser'; import { parseCategoryCollection } from './CategoryCollectionParser';
@@ -28,7 +29,7 @@ const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
}; };
const PreParsedCollections: readonly CollectionData [] = [ const PreParsedCollections: readonly CollectionData [] = [
WindowsData, MacOsData, WindowsData, MacOsData, LinuxData,
]; ];
function validateCollectionsData(collections: readonly CollectionData[]) { function validateCollectionsData(collections: readonly CollectionData[]) {

View File

@@ -3,7 +3,9 @@ import type {
} from '@/application/collections/'; } from '@/application/collections/';
import { Script } from '@/domain/Script'; import { Script } from '@/domain/Script';
import { Category } from '@/domain/Category'; import { Category } from '@/domain/Category';
import { parseDocUrls } from './DocumentationParser'; import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
import { parseDocs } from './DocumentationParser';
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext'; import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
import { parseScript } from './Script/ScriptParser'; import { parseScript } from './Script/ScriptParser';
@@ -12,35 +14,67 @@ let categoryIdCounter = 0;
export function parseCategory( export function parseCategory(
category: CategoryData, category: CategoryData,
context: ICategoryCollectionParseContext, context: ICategoryCollectionParseContext,
factory: CategoryFactoryType = CategoryFactory,
): Category { ): Category {
if (!context) { throw new Error('missing context'); } if (!context) { throw new Error('missing context'); }
ensureValid(category); return parseCategoryRecursively({
categoryData: category,
context,
factory,
});
}
interface ICategoryParseContext {
readonly categoryData: CategoryData,
readonly context: ICategoryCollectionParseContext,
readonly factory: CategoryFactoryType,
readonly parentCategory?: CategoryData,
}
// eslint-disable-next-line consistent-return
function parseCategoryRecursively(context: ICategoryParseContext): Category {
ensureValidCategory(context.categoryData, context.parentCategory);
const children: ICategoryChildren = { const children: ICategoryChildren = {
subCategories: new Array<Category>(), subCategories: new Array<Category>(),
subScripts: new Array<Script>(), subScripts: new Array<Script>(),
}; };
for (const data of category.children) { for (const data of context.categoryData.children) {
parseCategoryChild(data, children, category, context); parseNode({
nodeData: data,
children,
parent: context.categoryData,
factory: context.factory,
context: context.context,
});
}
try {
return context.factory(
/* id: */ categoryIdCounter++,
/* name: */ context.categoryData.category,
/* docs: */ parseDocs(context.categoryData),
/* categories: */ children.subCategories,
/* scripts: */ children.subScripts,
);
} catch (err) {
new NodeValidator({
type: NodeType.Category,
selfNode: context.categoryData,
parentNode: context.parentCategory,
}).throw(err.message);
} }
return new Category(
/* id: */ categoryIdCounter++,
/* name: */ category.category,
/* docs: */ parseDocUrls(category),
/* categories: */ children.subCategories,
/* scripts: */ children.subScripts,
);
} }
function ensureValid(category: CategoryData) { function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
if (!category) { new NodeValidator({
throw Error('missing category'); type: NodeType.Category,
} selfNode: category,
if (!category.children || category.children.length === 0) { parentNode: parentCategory,
throw Error(`category has no children: "${category.category}"`); })
} .assertDefined(category)
if (!category.category || category.category.length === 0) { .assertValidName(category.category)
throw Error('category has no name'); .assert(
} () => category.children && category.children.length > 0,
`"${category.category}" has no children.`,
);
} }
interface ICategoryChildren { interface ICategoryChildren {
@@ -48,22 +82,29 @@ interface ICategoryChildren {
subScripts: Script[]; subScripts: Script[];
} }
function parseCategoryChild( interface INodeParseContext {
data: CategoryOrScriptData, readonly nodeData: CategoryOrScriptData;
children: ICategoryChildren, readonly children: ICategoryChildren;
parent: CategoryData, readonly parent: CategoryData;
context: ICategoryCollectionParseContext, readonly factory: CategoryFactoryType;
) { readonly context: ICategoryCollectionParseContext;
if (isCategory(data)) { }
const subCategory = parseCategory(data as CategoryData, context); function parseNode(context: INodeParseContext) {
children.subCategories.push(subCategory); const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
} else if (isScript(data)) { validator.assertDefined(context.nodeData);
const scriptData = data as ScriptData; if (isCategory(context.nodeData)) {
const script = parseScript(scriptData, context); const subCategory = parseCategoryRecursively({
children.subScripts.push(script); categoryData: context.nodeData as CategoryData,
context: context.context,
factory: context.factory,
parentCategory: context.parent,
});
context.children.subCategories.push(subCategory);
} else if (isScript(context.nodeData)) {
const script = parseScript(context.nodeData as ScriptData, context.context);
context.children.subScripts.push(script);
} else { } else {
throw new Error(`Child element is neither a category or a script. validator.throw('Node is neither a category or a script.');
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
} }
} }
@@ -73,14 +114,22 @@ function isScript(data: CategoryOrScriptData): data is ScriptData {
} }
function isCategory(data: CategoryOrScriptData): data is CategoryData { function isCategory(data: CategoryOrScriptData): data is CategoryData {
const { category } = data as CategoryData; return hasProperty(data, 'category');
return category && category.length > 0;
} }
function hasCode(holder: InstructionHolder): boolean { function hasCode(data: InstructionHolder): boolean {
return holder.code && holder.code.length > 0; return hasProperty(data, 'code');
} }
function hasCall(holder: InstructionHolder) { function hasCall(data: InstructionHolder) {
return holder.call !== undefined; return hasProperty(data, 'call');
} }
function hasProperty(object: unknown, propertyName: string) {
return Object.prototype.hasOwnProperty.call(object, propertyName);
}
export type CategoryFactoryType = (
...parameters: ConstructorParameters<typeof Category>) => Category;
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);

View File

@@ -1,64 +1,58 @@
import type { DocumentableData, DocumentationUrlsData } from '@/application/collections/'; import type { DocumentableData, DocumentationData } from '@/application/collections/';
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> { export function parseDocs(documentable: DocumentableData): readonly string[] {
if (!documentable) { if (!documentable) {
throw new Error('missing documentable'); throw new Error('missing documentable');
} }
const { docs } = documentable; const { docs } = documentable;
if (!docs || !docs.length) { if (!docs) {
return []; return [];
} }
let result = new DocumentationUrlContainer(); let result = new DocumentationContainer();
result = addDocs(docs, result); result = addDocs(docs, result);
return result.getAll(); return result.getAll();
} }
function addDocs( function addDocs(
docs: DocumentationUrlsData, docs: DocumentationData,
urls: DocumentationUrlContainer, container: DocumentationContainer,
): DocumentationUrlContainer { ): DocumentationContainer {
if (docs instanceof Array) { if (docs instanceof Array) {
urls.addUrls(docs); if (docs.length > 0) {
container.addParts(docs);
}
} else if (typeof docs === 'string') { } else if (typeof docs === 'string') {
urls.addUrl(docs); container.addPart(docs);
} else { } else {
throw new Error('Docs field (documentation url) must a string or array of strings'); throwInvalidType();
} }
return urls; return container;
} }
class DocumentationUrlContainer { class DocumentationContainer {
private readonly urls = new Array<string>(); private readonly parts = new Array<string>();
public addUrl(url: string) { public addPart(documentation: string) {
validateUrl(url); if (!documentation) {
this.urls.push(url); throw Error('missing documentation');
}
if (typeof documentation !== 'string') {
throwInvalidType();
}
this.parts.push(documentation);
} }
public addUrls(urls: readonly string[]) { public addParts(parts: readonly string[]) {
for (const url of urls) { for (const part of parts) {
if (typeof url !== 'string') { this.addPart(part);
throw new Error('Docs field (documentation url) must be an array of strings');
}
this.addUrl(url);
} }
} }
public getAll(): ReadonlyArray<string> { public getAll(): ReadonlyArray<string> {
return this.urls; return this.parts;
} }
} }
function validateUrl(docUrl: string): void { function throwInvalidType() {
if (!docUrl) { throw new Error('docs field (documentation) must be an array of strings');
throw new Error('Documentation url is null or empty');
}
if (docUrl.includes('\n')) {
throw new Error('Documentation url cannot be multi-lined.');
}
const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
const res = docUrl.match(validUrlRegex);
if (res == null) {
throw new Error(`Invalid documentation url: ${docUrl}`);
}
} }

View File

@@ -0,0 +1,3 @@
import type { ScriptData, CategoryData } from '@/application/collections/';
export type NodeData = CategoryData | ScriptData;

View File

@@ -0,0 +1,34 @@
import { CustomError } from '@/application/Common/CustomError';
import { NodeType } from './NodeType';
import { NodeData } from './NodeData';
export class NodeDataError extends CustomError {
constructor(message: string, public readonly context: INodeDataErrorContext) {
super(createMessage(message, context));
}
}
export interface INodeDataErrorContext {
readonly type?: NodeType;
readonly selfNode: NodeData;
readonly parentNode?: NodeData;
}
function createMessage(errorMessage: string, context: INodeDataErrorContext) {
let message = '';
if (context.type !== undefined) {
message += `${NodeType[context.type]}: `;
}
message += errorMessage;
message += `\n${dump(context)}`;
return message;
}
function dump(context: INodeDataErrorContext): string {
const printJson = (obj: unknown) => JSON.stringify(obj, undefined, 2);
let output = `Self: ${printJson(context.selfNode)}`;
if (context.parentNode) {
output += `\nParent: ${printJson(context.parentNode)}`;
}
return output;
}

View File

@@ -0,0 +1,4 @@
export enum NodeType {
Script,
Category,
}

View File

@@ -0,0 +1,38 @@
import { INodeDataErrorContext, NodeDataError } from './NodeDataError';
import { NodeData } from './NodeData';
export class NodeValidator {
constructor(private readonly context: INodeDataErrorContext) {
}
public assertValidName(nameValue: string) {
return this
.assert(
() => Boolean(nameValue),
'missing name',
)
.assert(
() => typeof nameValue === 'string',
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
);
}
public assertDefined(node: NodeData) {
return this.assert(
() => node !== undefined && node !== null && Object.keys(node).length > 0,
'missing node data',
);
}
public assert(validationPredicate: () => boolean, errorMessage: string) {
if (!validationPredicate()) {
this.throw(errorMessage);
}
return this;
}
public throw(errorMessage: string) {
throw new NodeDataError(errorMessage, this.context);
}
}

View File

@@ -3,13 +3,26 @@ import { ProjectInformation } from '@/domain/ProjectInformation';
import { Version } from '@/domain/Version'; import { Version } from '@/domain/Version';
export function parseProjectInformation( export function parseProjectInformation(
environment: NodeJS.ProcessEnv, environment: NodeJS.ProcessEnv | VueAppEnvironment,
): IProjectInformation { ): IProjectInformation {
const version = new Version(environment.VUE_APP_VERSION); const version = new Version(environment[VueAppEnvironmentKeys.VUE_APP_VERSION]);
return new ProjectInformation( return new ProjectInformation(
environment.VUE_APP_NAME, environment[VueAppEnvironmentKeys.VUE_APP_NAME],
version, version,
environment.VUE_APP_REPOSITORY_URL, environment[VueAppEnvironmentKeys.VUE_APP_SLOGAN],
environment.VUE_APP_HOMEPAGE_URL, environment[VueAppEnvironmentKeys.VUE_APP_REPOSITORY_URL],
environment[VueAppEnvironmentKeys.VUE_APP_HOMEPAGE_URL],
); );
} }
export const VueAppEnvironmentKeys = {
VUE_APP_VERSION: 'VUE_APP_VERSION',
VUE_APP_NAME: 'VUE_APP_NAME',
VUE_APP_SLOGAN: 'VUE_APP_SLOGAN',
VUE_APP_REPOSITORY_URL: 'VUE_APP_REPOSITORY_URL',
VUE_APP_HOMEPAGE_URL: 'VUE_APP_HOMEPAGE_URL',
} as const;
export type VueAppEnvironment = {
[K in keyof typeof VueAppEnvironmentKeys]: string;
};

View File

@@ -1,11 +1,11 @@
import type { FunctionData } from '@/application/collections/'; import type { FunctionData } from '@/application/collections/';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { IScriptCompiler } from './Compiler/IScriptCompiler'; import { IScriptCompiler } from './Compiler/IScriptCompiler';
import { ScriptCompiler } from './Compiler/ScriptCompiler'; import { ScriptCompiler } from './Compiler/ScriptCompiler';
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext'; import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
import { SyntaxFactory } from './Syntax/SyntaxFactory'; import { SyntaxFactory } from './Validation/Syntax/SyntaxFactory';
import { ISyntaxFactory } from './Syntax/ISyntaxFactory'; import { ISyntaxFactory } from './Validation/Syntax/ISyntaxFactory';
import { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext { export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
public readonly compiler: IScriptCompiler; public readonly compiler: IScriptCompiler;

View File

@@ -13,4 +13,22 @@ export class ExpressionPosition {
throw Error(`negative start position: ${start}`); throw Error(`negative start position: ${start}`);
} }
} }
public isInInsideOf(potentialParent: ExpressionPosition): boolean {
if (this.isSame(potentialParent)) {
return false;
}
return potentialParent.start <= this.start
&& potentialParent.end >= this.end;
}
public isSame(other: ExpressionPosition): boolean {
return other.start === this.start
&& other.end === this.end;
}
public isIntersecting(other: ExpressionPosition): boolean {
return (other.start < this.end && other.end > this.start)
|| (this.end > other.start && other.start >= this.start);
}
} }

View File

@@ -20,21 +20,59 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
if (!code) { if (!code) {
return code; return code;
} }
const expressions = this.extractor.findExpressions(code);
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
const context = new ExpressionEvaluationContext(args); const context = new ExpressionEvaluationContext(args);
const compiledCode = compileExpressions(expressions, code, context); const compiledCode = compileRecursively(code, context, this.extractor);
return compiledCode; return compiledCode;
} }
} }
function compileRecursively(
code: string,
context: IExpressionEvaluationContext,
extractor: IExpressionParser,
): string {
/*
Instead of compiling code at once and returning we compile expressions from the code.
And recompile expressions from resulting code recursively.
This allows using expressions inside expressions blocks. E.g.:
```
{{ with $condition }}
echo '{{ $text }}'
{{ end }}
```
Without recursing parameter substitution for '{{ $text }}' is skipped once the outer
{{ with $condition }} is rendered.
A more optimized alternative to recursion would be to a parse an expression tree
instead of linear expression lists.
*/
if (!code) {
return code;
}
const expressions = extractor.findExpressions(code);
if (expressions.length === 0) {
return code;
}
const compiledCode = compileExpressions(expressions, code, context);
return compileRecursively(compiledCode, context, extractor);
}
function compileExpressions( function compileExpressions(
expressions: readonly IExpression[], expressions: readonly IExpression[],
code: string, code: string,
context: IExpressionEvaluationContext, context: IExpressionEvaluationContext,
) { ) {
ensureValidExpressions(expressions, code, context);
let compiledCode = ''; let compiledCode = '';
const sortedExpressions = expressions const outerExpressions = expressions.filter(
(expression) => expressions
.filter((otherExpression) => otherExpression !== expression)
.every((otherExpression) => !expression.position.isInInsideOf(otherExpression.position)),
);
/*
This logic will only compile outer expressions if there were nested expressions.
So the output of this compilation may result in new uncompiled expressions.
*/
const sortedExpressions = outerExpressions
.slice() // copy the array to not mutate the parameter .slice() // copy the array to not mutate the parameter
.sort((a, b) => b.position.start - a.position.start); .sort((a, b) => b.position.start - a.position.start);
let index = 0; let index = 0;
@@ -65,6 +103,43 @@ function extractRequiredParameterNames(
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates .filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
} }
function printList(list: readonly string[]): string {
return `"${list.join('", "')}"`;
}
function ensureValidExpressions(
expressions: readonly IExpression[],
code: string,
context: IExpressionEvaluationContext,
) {
ensureParamsUsedInCodeHasArgsProvided(expressions, context.args);
ensureExpressionsDoesNotExtendCodeLength(expressions, code);
ensureNoExpressionsAtSamePosition(expressions);
ensureNoInvalidIntersections(expressions);
}
function ensureExpressionsDoesNotExtendCodeLength(
expressions: readonly IExpression[],
code: string,
) {
const expectedMax = code.length;
const expressionsOutOfRange = expressions
.filter((expression) => expression.position.end > expectedMax);
if (expressionsOutOfRange.length > 0) {
throw new Error(`Expressions out of range:\n${JSON.stringify(expressionsOutOfRange)}`);
}
}
function ensureNoExpressionsAtSamePosition(expressions: readonly IExpression[]) {
const instructionsAtSamePosition = expressions.filter(
(expression) => expressions
.filter((other) => expression.position.isSame(other.position)).length > 1,
);
if (instructionsAtSamePosition.length > 0) {
throw new Error(`Instructions at same position:\n${JSON.stringify(instructionsAtSamePosition)}`);
}
}
function ensureParamsUsedInCodeHasArgsProvided( function ensureParamsUsedInCodeHasArgsProvided(
expressions: readonly IExpression[], expressions: readonly IExpression[],
providedArgs: IReadOnlyFunctionCallArgumentCollection, providedArgs: IReadOnlyFunctionCallArgumentCollection,
@@ -80,6 +155,16 @@ function ensureParamsUsedInCodeHasArgsProvided(
} }
} }
function printList(list: readonly string[]): string { function ensureNoInvalidIntersections(expressions: readonly IExpression[]) {
return `"${list.join('", "')}"`; const intersectingInstructions = expressions.filter(
(expression) => expressions
.filter((other) => expression.position.isIntersecting(other.position))
.filter((other) => !expression.position.isSame(other.position))
.filter((other) => !expression.position.isInInsideOf(other.position))
.filter((other) => !other.position.isInInsideOf(expression.position))
.length > 0,
);
if (intersectingInstructions.length > 0) {
throw new Error(`Instructions intersecting unexpectedly:\n${JSON.stringify(intersectingInstructions)}`);
}
} }

View File

@@ -25,10 +25,10 @@ export class ExpressionRegexBuilder {
.addRawRegex('([^|\\s]+)'); .addRawRegex('([^|\\s]+)');
} }
public matchAnythingExceptSurroundingWhitespaces() { public matchMultilineAnythingExceptSurroundingWhitespaces() {
return this return this
.expectZeroOrMoreWhitespaces() .expectZeroOrMoreWhitespaces()
.addRawRegex('(.+?)') .addRawRegex('([\\S\\s]+?)')
.expectZeroOrMoreWhitespaces(); .expectZeroOrMoreWhitespaces();
} }

View File

@@ -8,7 +8,7 @@ export class EscapeDoubleQuotes implements IPipe {
return raw; return raw;
} }
return raw.replaceAll('"', '"^""'); return raw.replaceAll('"', '"^""');
/* eslint-disable max-len */ /* eslint-disable vue/max-len */
/* /*
"^"" is the most robust and stable choice. "^"" is the most robust and stable choice.
Other options: Other options:
@@ -28,6 +28,6 @@ export class EscapeDoubleQuotes implements IPipe {
Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"` Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"`
A good explanation: https://stackoverflow.com/a/31413730 A good explanation: https://stackoverflow.com/a/31413730
*/ */
/* eslint-enable max-len */ /* eslint-enable vue/max-len */
} }
} }

View File

@@ -12,7 +12,7 @@ export class WithParser extends RegexParser {
.matchUntilFirstWhitespace() // First match: parameter name .matchUntilFirstWhitespace() // First match: parameter name
.expectExpressionEnd() .expectExpressionEnd()
// ... // ...
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text .matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
// {{ end }} // {{ end }}
.expectExpressionStart() .expectExpressionStart()
.expectCharacters('end') .expectCharacters('end')

View File

@@ -81,7 +81,7 @@ function compileCode(
compiler: IExpressionsCompiler, compiler: IExpressionsCompiler,
): ICompiledFunctionCall { ): ICompiledFunctionCall {
return { return {
code: compiler.compileExpressions(code.do, args), code: compiler.compileExpressions(code.execute, args),
revertCode: compiler.compileExpressions(code.revert, args), revertCode: compiler.compileExpressions(code.revert, args),
}; };
} }

View File

@@ -19,6 +19,6 @@ export enum FunctionBodyType {
} }
export interface IFunctionCode { export interface IFunctionCode {
readonly do: string; readonly execute: string;
readonly revert?: string; readonly revert?: string;
} }

View File

@@ -1,6 +1,10 @@
import type { FunctionData } from '@/application/collections/'; import type { FunctionData } from '@/application/collections/';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { ISharedFunctionCollection } from './ISharedFunctionCollection'; import { ISharedFunctionCollection } from './ISharedFunctionCollection';
export interface ISharedFunctionsParser { export interface ISharedFunctionsParser {
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection; parseFunctions(
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
): ISharedFunctionCollection;
} }

View File

@@ -1,4 +1,5 @@
import { IFunctionCall } from './Call/IFunctionCall'; import { IFunctionCall } from './Call/IFunctionCall';
import { import {
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody, FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
} from './ISharedFunction'; } from './ISharedFunction';
@@ -25,7 +26,7 @@ export function createFunctionWithInlineCode(
throw new Error(`undefined code in function "${name}"`); throw new Error(`undefined code in function "${name}"`);
} }
const content: IFunctionCode = { const content: IFunctionCode = {
do: code, execute: code,
revert: revertCode, revert: revertCode,
}; };
return new SharedFunction(name, parameters, content, FunctionBodyType.Code); return new SharedFunction(name, parameters, content, FunctionBodyType.Code);

View File

@@ -1,4 +1,9 @@
import type { FunctionData, InstructionHolder } from '@/application/collections/'; import type { FunctionData, InstructionHolder } from '@/application/collections/';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction'; import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
import { SharedFunctionCollection } from './SharedFunctionCollection'; import { SharedFunctionCollection } from './SharedFunctionCollection';
import { ISharedFunctionCollection } from './ISharedFunctionCollection'; import { ISharedFunctionCollection } from './ISharedFunctionCollection';
@@ -12,16 +17,20 @@ import { parseFunctionCalls } from './Call/FunctionCallParser';
export class SharedFunctionsParser implements ISharedFunctionsParser { export class SharedFunctionsParser implements ISharedFunctionsParser {
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser(); public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
constructor(private readonly codeValidator: ICodeValidator = CodeValidator.instance) { }
public parseFunctions( public parseFunctions(
functions: readonly FunctionData[], functions: readonly FunctionData[],
syntax: ILanguageSyntax,
): ISharedFunctionCollection { ): ISharedFunctionCollection {
if (!syntax) { throw new Error('missing syntax'); }
const collection = new SharedFunctionCollection(); const collection = new SharedFunctionCollection();
if (!functions || !functions.length) { if (!functions || !functions.length) {
return collection; return collection;
} }
ensureValidFunctions(functions); ensureValidFunctions(functions);
return functions return functions
.map((func) => parseFunction(func)) .map((func) => parseFunction(func, syntax, this.codeValidator))
.reduce((acc, func) => { .reduce((acc, func) => {
acc.addFunction(func); acc.addFunction(func);
return acc; return acc;
@@ -29,10 +38,15 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
} }
} }
function parseFunction(data: FunctionData): ISharedFunction { function parseFunction(
data: FunctionData,
syntax: ILanguageSyntax,
validator: ICodeValidator,
): ISharedFunction {
const { name } = data; const { name } = data;
const parameters = parseParameters(data); const parameters = parseParameters(data);
if (hasCode(data)) { if (hasCode(data)) {
validateCode(data, syntax, validator);
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode); return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
} }
// Has call // Has call
@@ -40,6 +54,19 @@ function parseFunction(data: FunctionData): ISharedFunction {
return createCallerFunction(name, parameters, calls); return createCallerFunction(name, parameters, calls);
} }
function validateCode(
data: FunctionData,
syntax: ILanguageSyntax,
validator: ICodeValidator,
): void {
[data.code, data.revertCode].forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
}
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection { function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
return (data.parameters || []) return (data.parameters || [])
.map((parameter) => { .map((parameter) => {

View File

@@ -1,6 +1,10 @@
import type { FunctionData, ScriptData } from '@/application/collections/'; import type { FunctionData, ScriptData } from '@/application/collections/';
import { IScriptCode } from '@/domain/IScriptCode'; import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode'; import { ScriptCode } from '@/domain/ScriptCode';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
import { IScriptCompiler } from './IScriptCompiler'; import { IScriptCompiler } from './IScriptCompiler';
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection'; import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler'; import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
@@ -8,18 +12,20 @@ import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompi
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser'; import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
import { SharedFunctionsParser } from './Function/SharedFunctionsParser'; import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
import { parseFunctionCalls } from './Function/Call/FunctionCallParser'; import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
import { ICompiledCode } from './Function/Call/Compiler/ICompiledCode';
export class ScriptCompiler implements IScriptCompiler { export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection; private readonly functions: ISharedFunctionCollection;
constructor( constructor(
functions: readonly FunctionData[] | undefined, functions: readonly FunctionData[] | undefined,
private readonly syntax: ILanguageSyntax, syntax: ILanguageSyntax,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance, sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
) { ) {
if (!syntax) { throw new Error('missing syntax'); } if (!syntax) { throw new Error('missing syntax'); }
this.functions = sharedFunctionsParser.parseFunctions(functions); this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
} }
public canCompile(script: ScriptData): boolean { public canCompile(script: ScriptData): boolean {
@@ -35,13 +41,19 @@ export class ScriptCompiler implements IScriptCompiler {
try { try {
const calls = parseFunctionCalls(script.call); const calls = parseFunctionCalls(script.call);
const compiledCode = this.callCompiler.compileCall(calls, this.functions); const compiledCode = this.callCompiler.compileCall(calls, this.functions);
validateCompiledCode(compiledCode, this.codeValidator);
return new ScriptCode( return new ScriptCode(
compiledCode.code, compiledCode.code,
compiledCode.revertCode, compiledCode.revertCode,
this.syntax,
); );
} catch (error) { } catch (error) {
throw Error(`Script "${script.name}" ${error.message}`); throw Error(`Script "${script.name}" ${error.message}`);
} }
} }
} }
function validateCompiledCode(compiledCode: ICompiledCode, validator: ICodeValidator): void {
[compiledCode.code, compiledCode.revertCode].forEach(
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
);
}

View File

@@ -1,5 +1,5 @@
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { IScriptCompiler } from './Compiler/IScriptCompiler'; import { IScriptCompiler } from './Compiler/IScriptCompiler';
import { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
export interface ICategoryCollectionParseContext { export interface ICategoryCollectionParseContext {
readonly compiler: IScriptCompiler; readonly compiler: IScriptCompiler;

View File

@@ -1,26 +1,41 @@
import type { ScriptData } from '@/application/collections/'; import type { ScriptData } from '@/application/collections/';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { Script } from '@/domain/Script'; import { Script } from '@/domain/Script';
import { RecommendationLevel } from '@/domain/RecommendationLevel'; import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { IScriptCode } from '@/domain/IScriptCode'; import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode'; import { ScriptCode } from '@/domain/ScriptCode';
import { parseDocUrls } from '../DocumentationParser'; import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
import { parseDocs } from '../DocumentationParser';
import { createEnumParser, IEnumParser } from '../../Common/Enum'; import { createEnumParser, IEnumParser } from '../../Common/Enum';
import { NodeType } from '../NodeValidation/NodeType';
import { NodeValidator } from '../NodeValidation/NodeValidator';
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext'; import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
import { CodeValidator } from './Validation/CodeValidator';
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
// eslint-disable-next-line consistent-return
export function parseScript( export function parseScript(
data: ScriptData, data: ScriptData,
context: ICategoryCollectionParseContext, context: ICategoryCollectionParseContext,
levelParser = createEnumParser(RecommendationLevel), levelParser = createEnumParser(RecommendationLevel),
scriptFactory: ScriptFactoryType = ScriptFactory,
codeValidator: ICodeValidator = CodeValidator.instance,
): Script { ): Script {
validateScript(data); const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
validateScript(data, validator);
if (!context) { throw new Error('missing context'); } if (!context) { throw new Error('missing context'); }
const script = new Script( try {
/* name: */ data.name, const script = scriptFactory(
/* code: */ parseCode(data, context), /* name: */ data.name,
/* docs: */ parseDocUrls(data), /* code: */ parseCode(data, context, codeValidator),
/* level: */ parseLevel(data.recommend, levelParser), /* docs: */ parseDocs(data),
); /* level: */ parseLevel(data.recommend, levelParser),
return script; );
return script;
} catch (err) {
validator.throw(err.message);
}
} }
function parseLevel( function parseLevel(
@@ -33,28 +48,50 @@ function parseLevel(
return parser.parseEnum(level, 'level'); return parser.parseEnum(level, 'level');
} }
function parseCode(script: ScriptData, context: ICategoryCollectionParseContext): IScriptCode { function parseCode(
script: ScriptData,
context: ICategoryCollectionParseContext,
codeValidator: ICodeValidator,
): IScriptCode {
if (context.compiler.canCompile(script)) { if (context.compiler.canCompile(script)) {
return context.compiler.compile(script); return context.compiler.compile(script);
} }
return new ScriptCode(script.code, script.revertCode, context.syntax); const code = new ScriptCode(script.code, script.revertCode);
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
return code;
} }
function ensureNotBothCallAndCode(script: ScriptData) { function validateHardcodedCodeWithoutCalls(
if (script.code && script.call) { scriptCode: ScriptCode,
throw new Error('cannot define both "call" and "code"'); codeValidator: ICodeValidator,
} syntax: ILanguageSyntax,
if (script.revertCode && script.call) { ) {
throw new Error('cannot define "revertCode" if "call" is defined'); [scriptCode.execute, scriptCode.revert].forEach(
} (code) => codeValidator.throwIfInvalid(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
} }
function validateScript(script: ScriptData) { function validateScript(script: ScriptData, validator: NodeValidator) {
if (!script) { validator
throw new Error('missing script'); .assertDefined(script)
} .assertValidName(script.name)
if (!script.code && !script.call) { .assert(
throw new Error('must define either "call" or "code"'); () => Boolean(script.code || script.call),
} 'Must define either "call" or "code".',
ensureNotBothCallAndCode(script); )
.assert(
() => !(script.code && script.call),
'Cannot define both "call" and "code".',
)
.assert(
() => !(script.revertCode && script.call),
'Cannot define "revertCode" if "call" is defined.',
);
} }
export type ScriptFactoryType = (...parameters: ConstructorParameters<typeof Script>) => Script;
const ScriptFactory: ScriptFactoryType = (...parameters) => new Script(...parameters);

View File

@@ -1,7 +0,0 @@
import { ILanguageSyntax } from '@/domain/ScriptCode';
export class ShellScriptSyntax implements ILanguageSyntax {
public readonly commentDelimiters = ['#'];
public readonly commonCodeParts = ['(', ')', 'else', 'fi'];
}

View File

@@ -0,0 +1,46 @@
import { ICodeValidationRule, IInvalidCodeLine } from './ICodeValidationRule';
import { ICodeValidator } from './ICodeValidator';
import { ICodeLine } from './ICodeLine';
export class CodeValidator implements ICodeValidator {
public static readonly instance: ICodeValidator = new CodeValidator();
public throwIfInvalid(
code: string,
rules: readonly ICodeValidationRule[],
): void {
if (!rules || rules.length === 0) { throw new Error('missing rules'); }
if (!code) {
return;
}
const lines = extractLines(code);
const invalidLines = rules.flatMap((rule) => rule.analyze(lines));
if (invalidLines.length === 0) {
return;
}
const errorText = `Errors with the code.\n${printLines(lines, invalidLines)}`;
throw new Error(errorText);
}
}
function extractLines(code: string): ICodeLine[] {
return code
.split(/\r\n|\r|\n/)
.map((lineText, lineIndex): ICodeLine => ({
index: lineIndex + 1,
text: lineText,
}));
}
function printLines(
lines: readonly ICodeLine[],
invalidLines: readonly IInvalidCodeLine[],
): string {
return lines.map((line) => {
const badLine = invalidLines.find((invalidLine) => invalidLine.index === line.index);
if (!badLine) {
return `[${line.index}] ✅ ${line.text}`;
}
return `[${badLine.index}] ❌ ${line.text}\n\t⟶ ${badLine.error}`;
}).join('\n');
}

View File

@@ -0,0 +1,4 @@
export interface ICodeLine {
readonly index: number;
readonly text: string;
}

View File

@@ -0,0 +1,10 @@
import { ICodeLine } from './ICodeLine';
export interface IInvalidCodeLine {
readonly index: number;
readonly error: string;
}
export interface ICodeValidationRule {
analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[];
}

View File

@@ -0,0 +1,8 @@
import { ICodeValidationRule } from './ICodeValidationRule';
export interface ICodeValidator {
throwIfInvalid(
code: string,
rules: readonly ICodeValidationRule[],
): void;
}

View File

@@ -0,0 +1,47 @@
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { ICodeLine } from '../ICodeLine';
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
export class NoDuplicatedLines implements ICodeValidationRule {
constructor(private readonly syntax: ILanguageSyntax) {
if (!syntax) { throw new Error('missing syntax'); }
}
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
return lines
.map((line): IDuplicateAnalyzedLine => ({
index: line.index,
isIgnored: shouldIgnoreLine(line.text, this.syntax),
occurrenceIndices: lines
.filter((other) => other.text === line.text)
.map((duplicatedLine) => duplicatedLine.index),
}))
.filter((line) => hasInvalidDuplicates(line))
.map((line): IInvalidCodeLine => ({
index: line.index,
error: `Line is duplicated at line numbers ${line.occurrenceIndices.join(',')}.`,
}));
}
}
interface IDuplicateAnalyzedLine {
readonly index: number;
readonly occurrenceIndices: readonly number[];
readonly isIgnored: boolean;
}
function hasInvalidDuplicates(line: IDuplicateAnalyzedLine): boolean {
return !line.isIgnored && line.occurrenceIndices.length > 1;
}
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
const lowerCaseCodeLine = codeLine.toLowerCase();
const isCommentLine = () => syntax.commentDelimiters.some(
(delimiter) => lowerCaseCodeLine.startsWith(delimiter),
);
const consistsOfFrequentCommands = () => {
const trimmed = lowerCaseCodeLine.trim().split(' ');
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
};
return isCommentLine() || consistsOfFrequentCommands();
}

View File

@@ -0,0 +1,21 @@
import { ICodeLine } from '../ICodeLine';
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
export class NoEmptyLines implements ICodeValidationRule {
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
return lines
.filter((line) => (line.text?.trim().length ?? 0) === 0)
.map((line): IInvalidCodeLine => ({
index: line.index,
error: (() => {
if (!line.text) {
return 'Empty line';
}
const markedText = line.text
.replaceAll(' ', '{whitespace}')
.replaceAll('\t', '{tab}');
return `Empty line: "${markedText}"`;
})(),
}));
}
}

View File

@@ -1,4 +1,4 @@
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
const BatchFileCommonCodeParts = ['(', ')', 'else', '||']; const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
const PowerShellCommonCodeParts = ['{', '}']; const PowerShellCommonCodeParts = ['{', '}'];

View File

@@ -0,0 +1,4 @@
export interface ILanguageSyntax {
readonly commentDelimiters: string[];
readonly commonCodeParts: string[];
}

View File

@@ -1,4 +1,4 @@
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory'; import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
import { ILanguageSyntax } from './ILanguageSyntax';
export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>; export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;

View File

@@ -0,0 +1,7 @@
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
export class ShellScriptSyntax implements ILanguageSyntax {
public readonly commentDelimiters = ['#'];
public readonly commonCodeParts = ['(', ')', 'else', 'fi', 'done'];
}

View File

@@ -1,6 +1,6 @@
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory'; import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { BatchFileSyntax } from './BatchFileSyntax'; import { BatchFileSyntax } from './BatchFileSyntax';
import { ShellScriptSyntax } from './ShellScriptSyntax'; import { ShellScriptSyntax } from './ShellScriptSyntax';
import { ISyntaxFactory } from './ISyntaxFactory'; import { ISyntaxFactory } from './ISyntaxFactory';

View File

@@ -12,10 +12,10 @@ declare module '@/application/collections/*' {
} }
export type CategoryOrScriptData = CategoryData | ScriptData; export type CategoryOrScriptData = CategoryData | ScriptData;
export type DocumentationUrlsData = ReadonlyArray<string> | string; export type DocumentationData = ReadonlyArray<string> | string;
export interface DocumentableData { export interface DocumentableData {
readonly docs?: DocumentationUrlsData; readonly docs?: DocumentationData;
} }
export interface InstructionHolder { export interface InstructionHolder {

File diff suppressed because it is too large Load Diff

View File

@@ -510,35 +510,81 @@ actions:
parameters: parameters:
configuration: export POWERSHELL_TELEMETRY_OPTOUT=1 configuration: export POWERSHELL_TELEMETRY_OPTOUT=1
- -
category: Configure Parallels category: Configure Parallels Desktop
docs: |-
Parallels Desktop for Mac is software providing hardware virtualization for macOS [1].
When you use it, it collects and share your personal data to third parties [2]. Personal
data include IP address of your device, your broad geographical location (country, state
(if applicable), and city) and used product [2].
It includes third-party ads [3] and automatic check for updates [4] by default. Both of these
behaviors communicate with online services that reveal data about you.
[1]: https://web.archive.org/web/20221012155943/https://en.wikipedia.org/wiki/Parallels_Desktop_for_Mac "Parallels Desktop for Mac - Wikipedia | en.wikipedia.org"
[2]: https://web.archive.org/web/20221012155829/https://www.parallels.com/about/legal/privacy/ "Privacy Statement | parallels.com"
[3]: https://web.archive.org/web/20221012151800/https://kb.parallels.com/114422 "How do I turn off notifications in Parallels Desktop and Parallels Access? | Knowledge Base | parallels.com"
[4]: https://web.archive.org/web/20221012151953/http://download.parallels.com/stm/docs/en/Parallels_Desktop_Users_Guide/22220.htm "Automatic Updating | Parallels Desktop Users Guide | download.parallels.com"
children: children:
- -
name: Turn off ads in Parallels name: Turn off ads in Parallels Desktop
docs: https://hints.macworld.com/article.php?story=20120724235352514 recommend: standard
# Check: defaults read 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' docs: |-
code: defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool yes Parallels Desktop in-product notifications to show ads from Parallels or other third
# Default: 0 (no) party companies [1].
revertCode: defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool no
# There's more settings but they're off (has value "1" by default): The main setting is `ProductPromo.ForcePromoOff` [1] that you can check using:
# WelcomeScreenPromo.PromoOff (default 1)
# NotificationPromo.6635.PromoOff (default 1) 1. `defaults read 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff'`
2. `defaults read 'com.parallels.Parallels Desktop' 'WelcomeScreenPromo.PromoOff'`
By default, on clean installations the value is `0` which is equivalent of `no`.
There is also `WelcomeScreenPromo.PromoOff` setting that's pre-configured to `1` (`no` as
default). It's undocumented but still kept disabled by this script.
[1]: https://web.archive.org/save/https://forum.parallels.com/threads/unable-to-process-the-upgrade-request.345603/ "Unable to process the upgrade request | Parallels Forums | forum.parallels.com"
[2]: https://web.archive.org/web/20221012151800/https://kb.parallels.com/114422 "How do I turn off notifications in Parallels Desktop and Parallels Access? | Knowledge Base | parallels.com"
code: |-
defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool yes
defaults write 'com.parallels.Parallels Desktop' 'WelcomeScreenPromo.PromoOff' -bool yes
revertCode: |-
defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool no
defaults write 'com.parallels.Parallels Desktop' 'WelcomeScreenPromo.PromoOff' -bool yes
- -
category: Disable Parallels auto-updates category: Disable Parallels Desktop auto-updates
docs: |-
Parallels Desktop by default checks for updates frequently and automatically downloads them [1].
This reveal personal data about [2] you without your control.
[1]: https://web.archive.org/web/20221012151953/http://download.parallels.com/stm/docs/en/Parallels_Desktop_Users_Guide/22220.htm "Automatic Updating | Parallels Desktop Users Guide | download.parallels.com"
[2]: https://web.archive.org/web/20221012155829/https://www.parallels.com/about/legal/privacy/ "Privacy Statement | parallels.com"
children: children:
- -
name: Disable automatically downloading Parallels updates name: Disable automatically downloading Parallels Desktop updates
docs: https://download.parallels.com/desktop/v17/docs/en_US/Parallels-Desktop-Business-Edition-Administrators-Guide/37744.htm docs: |-
# Check: defaults read 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' Automatic downloads are enabled by default, and this script disables automatic downloads.
Automatic downloads are configured using the `Application preferences.Download updates automatically` property [1].
- Check: `defaults read 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically'`
- Values: 0 - Disabled, 1 - Enabled (default)
[1]: https://web.archive.org/web/20221012153810/https://download.parallels.com/desktop/v18/docs/en_US/Parallels-Desktop-Business-Edition-Administrators-Guide/37744.htm "Parallels Desktop Business Edition Administrator's Guide v18 - Configuring individual Macs | download.parallels.com"
code: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool no code: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool no
# Default: 1 (enabled)
revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool yes revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool yes
- -
name: Disable automatically checking for Parallels updates name: Disable automatically checking for Parallels Desktop updates
docs: https://download.parallels.com/desktop/v17/docs/en_US/Parallels-Desktop-Business-Edition-Administrators-Guide/37744.htm docs: |-
# Check: defaults read 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' Automatic checks are weekly by default, and this script disables the checks completely.
# Values: 0 - Never, 1 - Once a day, 2 - Once a week, 3 - Once a month
Frequency to check for updates can be configured using `Application preferences.Check for updates` property [1].
- Check: `defaults read 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates'`
- Values: 0 - Never, 1 - Once a day, 2 - Once a week (default), 3 - Once a month
[1]: https://web.archive.org/web/20221012153810/https://download.parallels.com/desktop/v18/docs/en_US/Parallels-Desktop-Business-Edition-Administrators-Guide/37744.htm "Parallels Desktop Business Edition Administrator's Guide v18 - Configuring individual Macs | download.parallels.com"
code: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 0 code: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 0
# Default: 2 (once a week)
revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 2 revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 2
- -
category: Configure OS category: Configure OS
@@ -670,51 +716,57 @@ actions:
code: sudo mdutil -i off -d / code: sudo mdutil -i off -d /
revertCode: sudo mdutil -i on / revertCode: sudo mdutil -i on /
- -
category: Configure crash reporting (quit dialog after an application crash) name: Disable Personalized advertisements and identifier collection
# Prompts for sending data to Apple recommend: standard
children: docs: |-
- This script enhances your privacy by deactivating Personalized Ads and disabling the collection
name: Disable Crash reporting of identifiers related to your device. The process involves modifying certain key configurations,
# Quit dialog after an application crash which prevents Apple's advertising platform from using your personal information to deliver targeted
# "The application <application> has unexpectedly quit" alert ads [1].
docs:
# Since 10.4 (Basic, Developer, and Server) When Personalized Ads is enabled, your information may be used to provide ads that closely align
- https://web.archive.org/web/20090411195107/http://developer.apple.com/qa/qa2001/qa1288.html with your interests [1]. You might occasionally encounter such targeted ads in Apple News, Stocks,
# Before 10.4 and the Mac App Store [2]. Disabling Personalized Ads will prevent Apple from using your data for
# "none" = Don't show any dialog at all (crash reports are still silently written to disk) ad targeting [2]. Although this does not necessarily decrease the quantity of ads you receive,
# "prompt" = show the unexpectedly quit dialog and prompt to see if the user wants to submit/view the crashreport it may result in the ads being less relevant to your interests [2].
# "crashreport" = don't show the unexpectedly quit dialog - instead immediately show the crashreport/submission screen.
- https://web.archive.org/web/20040816171016/http://developer.apple.com/qa/qa2001/qa1288.html The primary keys to deactivating personalized ads are:
# Description for crashreporter preferences (basic, developer, server), stating basic is the default one
# https://web.archive.org/web/20090228102631/http://developer.apple.com/technotes/tn2004/tn2123.html#SECCRASHREPORTERPREFS - **`allowApplePersonalizedAdvertising`**: If set to false, this restricts Apple's personalized
# Check: defaults read 'com.apple.CrashReporter' 'DialogType' advertising [3]. This is applicable on macOS 12 and subsequent versions [3].
# Values: none|basic (default)|developer|server (before 10.4: crashreport|none) - **`allowIdentifierForAdvertising`**: The `advertisingIdentifier` is a unique string assigned
code: defaults write 'com.apple.CrashReporter' 'DialogType' -string 'none' to each device [5]. Apple uses this identifier and recommends its use in third-party
revertCode: |- # TODO: Or delete? Since monterey com.apple.CrashReporter is empty applications for tasks like frequency capping, attribution, conversion events, estimating the
os_major_ver=$(sw_vers -productVersion | awk -F "." '{print $1}') number of unique users, detecting advertising fraud, and debugging [5]. Although there is no
os_minor_ver=$(sw_vers -productVersion | awk -F "." '{print $2}') official documentation on it, a discussion on JAMF.com corroborates its existence [6].
# Older (before 10.4): prompt|crashreport
if [[ $os_major_ver -le 10 \ My tests show that disabling any of the keys mentioned above results in the
|| ( $os_major_ver -eq 10 && $os_minor_ver -le 4 ) \ "System Preferences > Apple Advertising > Personalized ads" option being deactivated in the GUI,
]]; then starting from macOS Monterey.
defaults write 'com.apple.CrashReporter' 'DialogType' -string 'prompt'
else Please note: The `forceLimitAdTracking` key limits ad tracking [3] [4] and is found in CIS
# Newer (since 10.4): basic|developer|server benchmarks for macOS [4]. However, the official macOS documentation specifies that it is
defaults write 'com.apple.CrashReporter' 'DialogType' -string 'basic' applicable only to iOS 7 and later versions, not to macOS [3]. The key does not exist on the OS
fi by default.
-
name: Use notification instead of report after crash [1]: https://web.archive.org/web/20230731152633/https://www.apple.com/legal/privacy/data/en/apple-advertising/ "Legal - Apple Advertising & Privacy - Apple"
docs: [2]: https://web.archive.org/web/20220805052411/https://support.apple.com/en-sg/guide/mac-help/mh32356/mac: "Change Privacy preferences on Mac - Apple Support (SG)"
# Removing the Crash Reporter may be overkill for some users, so another option is to [3]: https://web.archive.org/web/20230731155827/https://developer.apple.com/documentation/devicemanagement/restrictions "Restrictions | Apple Developer Documentation"
# change this to a notification instead. [4]: https://web.archive.org/web/20230731155653/https://paper.bobylive.com/Security/CIS/CIS_Apple_macOS_11_0_Big_Sur_Benchmark_v2_0_0.pdf "CIS Apple macOS 11.0 Big Sur Benchmark"
# The advantage of this is you still get notified if an app has crashed, but you don't [5]: https://web.archive.org/web/20230731155131/https://developer.apple.com/documentation/adsupport/asidentifiermanager/1614151-advertisingidentifier "advertisingIdentifier | Apple Developer Documentation"
# have to respond in any way (nothing to click on). To undo this change you would use the [6]: https://web.archive.org/web/20230731154840/https://community.jamf.com/t5/jamf-pro/macos-quot-limit-ad-tracking-quot/td-p/217001 'Solved: macOS "Limit Ad Tracking" - Jamf Nation Community - 217001'
# same entry but replace the 1 with a 0. code: |-
- https://www.defaults-write.com/os-x-make-crash-reporter-appear-as-a-notification/ defaults write com.apple.AdLib allowIdentifierForAdvertising -bool false
- https://osxdaily.com/2015/10/13/set-crash-reporter-as-notification-mac-os-x/ defaults write com.apple.AdLib allowApplePersonalizedAdvertising -bool false
code: defaults write 'com.apple.CrashReporter' 'UseUNC' 1 defaults write com.apple.AdLib forceLimitAdTracking -bool true
revertCode: |- # TODO: Or delete? Since monterey com.apple.CrashReporter is empty # Default: (`defaults read com.apple.AdLib`)
defaults write 'com.apple.CrashReporter' 'UseUNC' 1 # - `defaults read com.apple.AdLib allowApplePersonalizedAdvertising`: true (1)
# - `defaults read com.apple.AdLib allowIdentifierForAdvertising`: true (1)
# - `defaults read com.apple.AdLib forceLimitAdTracking`: non-existing
revertCode: |-
defaults write com.apple.AdLib allowIdentifierForAdvertising -bool true
defaults write com.apple.AdLib allowApplePersonalizedAdvertising -bool true
sudo defaults delete com.apple.AdLib forceLimitAdTracking
- -
category: Security improvements category: Security improvements
children: children:
@@ -1153,106 +1205,6 @@ actions:
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool true sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool true
# Trigger background check with normal scan (critical updates only) # Trigger background check with normal scan (critical updates only)
sudo softwareupdate --background-critical sudo softwareupdate --background-critical
-
category: UI for privacy
children:
-
name: Show hidden files in Finder
recommend: strict # Because NIST recommends it
docs:
# Disables hiding files `.htaccess` in Finder
- https://macos-defaults.com/finder/appleshowallfiles.html
# Securing Apple OSX X 10.10 Systems: NIST Security Configuration Checklist:
# Finder should be configured to not show hidden files and folders; this is already configured by
# default. Finder should also be configured to show file extensions, to show a warning before
# changing a file extension or emptying the trash, and to search this system when performing a
# search. Administrators with intimate knowledge of the OS X system could notice unusual hidden
# files and would benefit from their visibility. Consequently, hidden files should be displayed in an
# SSLF environment. These options can improve defenses against malware. To configure these
# options, go to Finder / Preferences / Advanced; then enable the corresponding options.
- https://nvlpubs.nist.gov/nistpubs/specialpublications/nist.sp.800-179.pdf
# Check: defaults read 'com.apple.finder' 'AppleShowAllFiles' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
code: |-
killall cfprefsd
defaults write 'com.apple.finder' 'AppleShowAllFiles' -bool true
killall 'Finder'
revertCode: |-
killall cfprefsd
defaults delete 'com.apple.finder' 'AppleShowAllFiles'
killall 'Finder'
-
name: Hide Desktop icons
docs: https://www.defaults-write.com/os-x-how-to-quickly-hide-the-desktop-icons/
# When doing presentations, it can be useful to hide desktop icons
# Check: defaults read 'com.apple.finder' 'CreateDesktop' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
code: |-
killall cfprefsd
defaults write 'com.apple.finder' 'CreateDesktop' -bool false
killall Finder
revertCode: |-
killall cfprefsd
defaults delete 'com.apple.finder' 'CreateDesktop'
killall Finder
# TODO: https://github.com/mathiasbynens/dotfiles/blob/main/.macos
# defaults write com.apple.finder ShowExternalHardDrivesOnDesktop -bool false
# defaults write com.apple.finder ShowHardDrivesOnDesktop -bool false
# defaults write com.apple.finder ShowMountedServersOnDesktop -bool false
# defaults write com.apple.finder ShowRemovableMediaOnDesktop -bool false
-
name: Show all filename extensions # TODO: docs from https://nvlpubs.nist.gov/nistpubs/specialpublications/nist.sp.800-179.pdf
# Check: defaults read 'NSGlobalDomain' 'AppleShowAllExtensions' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
code: |-
killall cfprefsd
defaults write 'NSGlobalDomain' 'AppleShowAllExtensions' -bool true
killall Finder
revertCode: |-
killall cfprefsd
defaults delete 'NSGlobalDomain' 'AppleShowAllExtensions'
killall Finder
-
name: show path bar
# TODO: defaults write com.apple.finder ShowPathbar -bool true
-
category: Disable creation of metadata files (`.DS_Store`) # TODO: A better category
# macOS creates metadata files as and when files are saved to the hard drive.
# These metadata files can also be viewed further giving malicious actors the extra edge.
# It is recommended that Mac OS X users disable creation of Metadata Files to further boost the
# privacy levels.
children:
-
name: Disable metadata files (`.DS_Store`) on Network Volumes
recommend: strict
docs:
- https://support.apple.com/en-us/HT208209
- https://web.archive.org/web/20190919161732/https://support.apple.com/en-gb/HT1629
# Check: defaults read 'com.apple.desktopservices' 'DSDontWriteNetworkStores' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
code: defaults write 'com.apple.desktopservices' 'DSDontWriteNetworkStores' -bool true
revertCode: defaults delete 'com.apple.desktopservices' 'DSDontWriteNetworkStores'
-
name: Disable metadata files (`.DS_Store`) on USB Volumes
recommend: strict
docs: https://krypted.com/mac-security/disable-dsstore-files-on-usb-drives/
# Check: defaults read 'com.apple.desktopservices' 'DSDontWriteUSBStores' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
code: defaults write 'com.apple.desktopservices' 'DSDontWriteUSBStores' -bool true
revertCode: defaults delete 'com.apple.desktopservices' 'DSDontWriteUSBStores'
-
name: Clear created metadata files # TODO: Not tested
code: |- # "fstype local to search only on the local mounted file system and ignore any other mounted disk under
find ~ \
-type f \
-name .DS_Store \
-fstype local \
-exec \
sh -c \
'
file="{}"
rm -fv "$file"
' \
{} \;
functions: functions:
- -
name: PersistUserEnvironmentConfiguration name: PersistUserEnvironmentConfiguration
@@ -1283,9 +1235,3 @@ functions:
echo "[$profile_file] No need for any action, configuration does not exist" echo "[$profile_file] No need for any action, configuration does not exist"
fi fi
done done
# TODO:
# Disable disk image verification (or do enable)
# defaults write com.apple.frameworks.diskimages skip-verify -bool true
# defaults write com.apple.frameworks.diskimages skip-verify-locked -bool true
# defaults write com.apple.frameworks.diskimages skip-verify-remote -bool true

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ export class Category extends BaseEntity<number> implements ICategory {
constructor( constructor(
id: number, id: number,
public readonly name: string, public readonly name: string,
public readonly documentationUrls: ReadonlyArray<string>, public readonly docs: ReadonlyArray<string>,
public readonly subCategories?: ReadonlyArray<ICategory>, public readonly subCategories?: ReadonlyArray<ICategory>,
public readonly scripts?: ReadonlyArray<IScript>, public readonly scripts?: ReadonlyArray<IScript>,
) { ) {

View File

@@ -1,3 +1,3 @@
export interface IDocumentable { export interface IDocumentable {
readonly documentationUrls: ReadonlyArray<string>; readonly docs: ReadonlyArray<string>;
} }

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