Compare commits

...

21 Commits

Author SHA1 Message Date
undergroundwires
d11a674a3c win: unrecommend and document Live ID service #100
Rename service to its newer name. Mention breaking behavior in its name
and add more documentation.

Unrecommended from "Standard" pool because it breaks a lot of
functionality, but still recomended in "Stricts" because it's used to
identify personal information that leads to less privacy.
2022-01-05 19:26:30 +01:00
undergroundwires
31f70913a2 Refactor to improve iterations
- Use function abstractions (such as map, reduce, filter etc.) over
  for-of loops to gain benefits of having less side effects and easier
  readability.
- Enable `downLevelIterations` for writing modern code with lazy evaluation.
- Refactor for of loops to named abstractions to clearly express their
  intentions without needing to analyse the loop itself.
- Add missing cases for changes that had no tests.
2022-01-04 21:45:22 +01:00
undergroundwires
bd23faa28f Fix mutated line endings on Windows
It prevents Git from modifying files on checkout. By default, it
converts LF line-endings to CRLF on Windows which leads to inconsistency
and ESLint `linebreak-style` with `unix` (LF) value to fail.

It also solves failed builds in GitHub actions agents actions/checkout#135.
2022-01-03 21:28:24 +01:00
undergroundwires
5b1fbe1e2f Refactor code to comply with ESLint rules
Major refactoring using ESLint with rules from AirBnb and Vue.

Enable most of the ESLint rules and do necessary linting in the code.
Also add more information for rules that are disabled to describe what
they are and why they are disabled.

Allow logging (`console.log`) in test files, and in development mode
(e.g. when working with `npm run serve`), but disable it when
environment is production (as pre-configured by Vue). Also add flag
(`--mode production`) in `lint:eslint` command so production linting is
executed earlier in lifecycle.

Disable rules that requires a separate work. Such as ESLint rules that
are broken in TypeScript: no-useless-constructor (eslint/eslint#14118)
and no-shadow (eslint/eslint#13014).
2022-01-02 18:20:14 +01:00
undergroundwires
96265b75de Upgrade to Vue CLI 5 (and webpack 5)
Upgrade to v5.x using `vue upgrade --next`.

Update `vue.config.js` to import and use `defineConfig`, because it
provides type safety and created by Vue CLI 5 as default.

Vue CLI 5.x upgrades from webpack 4 to 5. It causes some issues that this
commit attemps to fix:

1. Fail due to webpack resolving of Ace.
   Third-party dependency (code editor) Ace uses legacy `file-loader`
   for webpack resolving. It's not supported in webpack 5. So change it
   with manual imports.
   Refs: ajaxorg/ace-builds#211, ajaxorg/ace-builds#221.

2. Wehpack drops polyfilling node core modules (`path`, `fs`, etc.).
   Webpack does not polyfill those modules by default anymore. This is
   good because they did not need browser polyfilling as they are
   used in desktop version only and resolved already by Electron.
   To resolve errors (using webpack recommendations):
    - Add typeof check around `process` variable.
    - Tell webpack explicitly to ignore used node modules.

3. Fail due to legacy dependency of vue-cli-plugin-electron-builder.
   This plugin is used for electron builds and development. It still
   uses webpack 4 that leads to failed builds.
   Downgrading `ts-loader` to latest version which has support for
   `loader-utils` solves the problem (typestrong/ts-loader#1288).
   Related issue: nklayman/vue-cli-plugin-electron-builder#1625

4. Compilation fails due to webpack loading of `fsevents` on macOS.
   This happens only when running `vue-cli-service test:unit` command
   (used in integration tests and unit tests). Other builds work fine.
   Refs: yan-foto/electron-reload#71,
     nklayman/vue-cli-plugin-electron-builder#712,
     nklayman/vue-cli-plugin-electron-builder#1333
2021-12-31 21:23:36 +01:00
undergroundwires
17298f0b2c Add build checks and improve existing CI/CD checks
Run quality checks for every possible OS because behavior of linting
rules may change per OS (e.g. `linebreak-style` ESLint assertment varies
by Unix-like vs Windows).

Add a new check to ensure project can be built:
  1. As both web and desktop applications.
    Different jobs are used due to nonidentical environment/mode support.
    Reference: nklayman/vue-cli-plugin-electron-builder#1627.
  2. Targeting all possible modes.
    The modes are configured using `--mode` but electron CLI checks
    `NODE_ENV` so it's set as well.
    Reference: nklayman/vue-cli-plugin-electron-builder#1626.

  3. On and for different operating systems.

Fix typo "Run units" instead of "Run unit tests".

Link to specific GitHub actions page for workflow runs.

Update documentation to match new structure, and change nontransparent
icons from the diagram and consistently use imperative for actions.

Rename `release-site` , `release-git`, `release-desktop` to
`site-release` , `git-release` and `desktop-release` to not be Yoda.
2021-12-31 00:39:06 +01:00
undergroundwires
61b475fa8d Migrate from TSLint to ESLint
TSLint deprecated and is being replaced by ESLint.

Add Vue CLI plugin (@vue/cli-plugin-eslint) using:
`vue add @vue/cli-plugin-eslint`. It also adds `.eslintrc.js` manually
for Cypress since Vue CLI for ESLint misses it (vuejs/vue-cli#6892).

Also rename `npm run lint:vue` to `npm run lint:eslint` for better
clarification.

This commit disables all rules that the current code is not compliant
with. This allows for enabling them gradually and separating commits
instead of mixing ESLint introduction with other code changes.

AirBnb is chosen as base configuration.

"Standard" is not chosen due to its poor defaults. It makes code cleaner
but harder to maintain:
  - It converts interfaces to types which is harder to read.
  - Removes semicolons that helps to eliminate some ambigious code.

"Airbnb" on the other hand helps for easier future changes and
maintinability:
  - Includes more useful rules.
  - Keeps the semicolons and interfaces.
  - Enforces trailing commas that makes it easier to delete lines later on.
  - Delete branches: standard, prettier.
2021-12-27 22:42:27 +01:00
undergroundwires
455084c17b Document and unrecommend AAD app removal #24, #54
- Document breaking of night light settings (#54).
- Document breaking of language selection from taskbar (#24).
- Unrecommend from strict pool.
2021-12-25 19:12:25 +01:00
undergroundwires
c3c5b897f3 Refactor to add readonly interfaces
Using more granular interfaces adds to expressiveness of the code.
Knowing what needs to mutate the state explicitly helps easier
understanding of the code and therefore increases the maintainability.
2021-12-24 21:14:27 +01:00
Weigurde
a1871a2982 Fix typos in privacy modal #109 2021-12-22 18:24:59 +01:00
undergroundwires
87de017afd Fix failing of functions without revert code
Add missing empty/undefined handling to fix a bug where defining new
functions without `revertCode:` fails.
2021-12-21 06:01:16 +01:00
undergroundwires
5a2c263af3 Restructure pipelines and badges
- Seperate test pipeline into E2E, integration and unit test pipelines.
- Improve documenetation for pipelines (ci-cd.md).
- Introduce naming convention for worklow files and names.
- Center badges with multiple files on README file.
2021-12-19 01:48:29 +01:00
undergroundwires
ddd2e704db Add initial e2e testing with cypress
Add e2e tests with cypress using `vue add e2e-cypress`.

Vue CLI does not support creating typescript tests at this moment
(vuejs/vue-cli#1350).
2021-12-16 22:37:58 +01:00
undergroundwires
9b5e0b0591 Document and unrecommend Cloud Experience Host
Removing Cloud Experience Host has caused many unexpected issues
for users (see #99, #64, #67). It's now excluded from "Strict"
recommendation pool until a better warning mechanism is implemented.
2021-12-13 23:11:13 +01:00
undergroundwires
9b6636e21a Fix clearing Windows product key showing dialog
It's now handled silently instead of prompting user.
2021-12-12 12:21:50 +01:00
undergroundwires
a8358b8e7a Fix OS desktop detection tests and edge cases
- Fix test cases not running for desktop OS detection.
- Fixes application throwing error when user agent is undefined.
- Refactor by making os property optional in Environment to explicit
describe its potential undefined state.
2021-12-11 11:55:43 +01:00
undergroundwires
5f091bb6ab Fix double backlashes in Windows vscode scripts 2021-12-06 20:07:42 +01:00
undergroundwires-bot
17b334aaad ⬆️ bump everywhere to 0.11.2 2021-12-04 16:06:15 +00:00
undergroundwires
c65209e6a9 Unrecommend and complete Windows Push Notif. #101
- Add more script documentation in code and reference URLs.
- Unrecommend as "Standard" recommend as "Strict" due to lack of
  documentation for its privacy intrusive behavior.
- Add mising WpnUserService for disabling it completely.
2021-12-03 01:08:55 +01:00
undergroundwires
d2518b11a7 Improve Windows defender docs and errors #104
- Improve error messages with cause of the problem and suggested solution.
- Document:
  * Disabling `WinDefend` breaks `Set-MpPreference` and Microsoft Store
    (as reported in #104).
  * Document services that `netsh advfirewall` depends on.
- Fix some bad whitespace character in documentation.
2021-11-27 20:22:18 +01:00
undergroundwires
70cdf3865a Improve and unify disabling of Windows services
Refactor, unify and improve the logic to to start/stop and
enable/disable services, and also add more documentation.

Rework functions:
  - Unify way of disabling Windows services using templating.
  - Capitalize as `startupMode` (where startup is single word) everywhere.
  - Use also text parameters (automatic, manual..) instead of numeric
    values (2,3...) when providing parameters to any service disable
    function.

Improve documentation:
  - Add reference URLs about disabled services.
  - Add more code documentation for querying status and allowed values.

Logic improvements include:
  - Check if service is running before stopping/starting the service.
  - Do not start the service it's not an Automatic service.
  - Check whether service is already disabled.
  - When reverting, start the service if it has Automatic startup. But
    do not start the service it has different startup (e.g. manual).
    Also starts the service even though start up is configured as
    desired (before it quit before doing service start).

Improve outputs (logs):
  - Remove false-positive error messages.
  - When a service cannot be stopped/start; mention in output that the
    service will be started/stopped after reboot.
  - Show success message once service is enabled/disabled.
  - Fix reboot messages when enabling/disabling services,
  - Do not write stderr if service cannot be stopped/started as it's not
    not the main goal of the function.

Add missing revert code for the ones missing them:
  - Disable diagnostics telemetry
  - Disable Windows Media Player Network Sharing Service

> Function: DisableServiceInRegistry
- Fix not exitting if service does not exist when reverting
- Show success message once service is enabled/disabled
- Fix double "Enabled.." messages
- Fix unintended registry addition

> Function: DisablePerUserService
- Change implementation to call DisableServiceInRegistry.
- Fix both services are skipped if one of them fails.
- Fix reverting a service sets wrong startup mode.
2021-11-25 21:34:15 +01:00
366 changed files with 37880 additions and 31829 deletions

7
.editorconfig Normal file
View File

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

115
.eslintrc.js Normal file
View File

@@ -0,0 +1,115 @@
const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
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: ['**/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"
'@/**', // @/..
'@tests/**', // @tests/.. (not matching anything after @** because there can be third parties as well)
'js-yaml-loader!@/**', // E.g. js-yaml-loader!@/..
].map((pattern) => ({ pattern, group: 'internal' })),
},
],
};
}
function getTodoRules() { // Should be worked on separate future commits
return {
'import/no-extraneous-dependencies': 'off',
// Requires webpack configuration change with import '..yaml' files.
'import/no-webpack-loader-syntax': 'off',
'import/extensions': 'off',
'import/no-unresolved': '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'),
],
};
}

6
.gitattributes vendored Normal file
View File

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

59
.github/workflows/checks.build.yaml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: build-checks
on:
push:
pull_request:
jobs:
build-web:
strategy:
matrix:
os: [ macos, ubuntu, windows ]
mode: [ development, test, production ]
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: actions/setup-node@v1
with:
node-version: 15.x
-
name: Install dependencies
run: npm ci
-
name: Build
run: npm run build -- --mode ${{ matrix.mode }}
# A new job is used due to environments/modes different from Vue CLI, https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
build-desktop:
strategy:
matrix:
os: [ macos, ubuntu, windows ]
mode: [ development, production ] # "test" is not supported https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1627
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: actions/setup-node@v1
with:
node-version: 15.x
-
name: Install dependencies
run: npm ci
-
name: Install cross-env
# Used to set NODE_ENV due to https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
run: npm install --global cross-env
-
name: Build
run: |-
cross-env-shell NODE_ENV=${{ matrix.mode }}
npm run electron:build -- --publish never --mode ${{ matrix.mode }}

View File

@@ -1,4 +1,4 @@
name: Quality checks name: quality-checks
on: [ push, pull_request ] on: [ push, pull_request ]
@@ -8,12 +8,13 @@ jobs:
strategy: strategy:
matrix: matrix:
lint-command: lint-command:
- npm run lint:vue - npm run lint:eslint
- npm run lint:yaml - npm run lint:yaml
- npm run lint:md - npm run lint:md
- npm run lint:md:relative-urls - npm run lint:md:relative-urls
- npm run lint:md:consistency - npm run lint:md:consistency
fail-fast: false # So it continues with other commands if one fails os: [ macos, ubuntu, windows ]
fail-fast: false # Still interested to see results from other combinations
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -1,4 +1,4 @@
name: Security checks name: security-checks
on: on:
push: push:

View File

@@ -1,4 +1,4 @@
name: Deploy desktop name: release-desktop
on: on:
release: release:

View File

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

View File

@@ -1,4 +1,4 @@
name: Deploy site name: release-site
on: on:
release: release:

28
.github/workflows/tests.e2e.yaml vendored Normal file
View File

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

View File

@@ -1,9 +1,9 @@
name: Test name: integration-tests
on: on:
push: push:
pull_request: pull_request:
schedule: # for integration tests schedule: # To get notified about problems from third party dependencies
- cron: '0 0 * * 0' # at 00:00 on every Sunday - cron: '0 0 * * 0' # at 00:00 on every Sunday
jobs: jobs:
@@ -25,9 +25,6 @@ jobs:
- -
name: Install dependencies name: Install dependencies
run: npm ci run: npm ci
-
name: Run unit tests
run: npm run test:unit
- -
name: Run integration tests name: Run integration tests
run: npm run test:integration run: npm run test:integration

28
.github/workflows/tests.unit.yaml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: unit-tests
on:
push:
pull_request:
jobs:
run-tests:
strategy:
matrix:
os: [macos, ubuntu, windows]
fail-fast: false # So it still runs on other OSes if one of them fails
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Setup node
uses: actions/setup-node@v1
with:
node-version: 15.x
-
name: Install dependencies
run: npm ci
-
name: Run unit tests
run: npm run test:unit

3
.gitignore vendored
View File

@@ -4,3 +4,6 @@ dist/
.vscode .vscode
#Electron-builder output #Electron-builder output
/dist_electron /dist_electron
# Cypress
/tests/e2e/screenshots
/tests/e2e/videos

View File

@@ -1,5 +1,20 @@
# Changelog # Changelog
## 0.11.2 (2021-12-03)
* Fix Windows TrustedInstaller session errors | [20a0071](https://github.com/undergroundwires/privacy.sexy/commit/20a0071c0d3d769a8f31218abdbfc4cafa25c6ff)
* Improve tests for `UserSelection` | [2f90cac](https://github.com/undergroundwires/privacy.sexy/commit/2f90cac52ab9e57615aeec41f9daa842bce770a5)
* Fix disabling/enabling Defender on Windows #104 | [2e08293](https://github.com/undergroundwires/privacy.sexy/commit/2e082932c952b0849ab2b8709ff0c75293b88e95)
* Refactor Saas naming, structure and modules | [bf83c58](https://github.com/undergroundwires/privacy.sexy/commit/bf83c58982ffa178facc6d35e50c7f1eac7ff236)
* Fix Defender features errors in Windows #104 | [d7761ab](https://github.com/undergroundwires/privacy.sexy/commit/d7761ab30e7f1e10a2919c196804d67511d6163a)
* Fix unintendedly inlined Windows scripts | [f2d9881](https://github.com/undergroundwires/privacy.sexy/commit/f2d988138257ff184884e4adc83c39e3bc247e9b)
* Fix Defender error due to non-english Windows #104 | [7c02ffb](https://github.com/undergroundwires/privacy.sexy/commit/7c02ffb6c95382b94f0b05e6f259cc418ec91c93)
* Improve and unify disabling of Windows services | [70cdf38](https://github.com/undergroundwires/privacy.sexy/commit/70cdf3865a0de3214fc9e26fbdada4b0cb413c46)
* Improve Windows defender docs and errors #104 | [d2518b1](https://github.com/undergroundwires/privacy.sexy/commit/d2518b11a7774ec58b9b46a691e2f013855bf0f9)
* Unrecommend and complete Windows Push Notif. #101 | [c65209e](https://github.com/undergroundwires/privacy.sexy/commit/c65209e6a99230f15ace8955e8d5a6f3333d146b)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.1...0.11.2)
## 0.11.1 (2021-11-04) ## 0.11.1 (2021-11-04)
* Update dependencies | [64631a4](https://github.com/undergroundwires/privacy.sexy/commit/64631a4552fad7f7b06286aba8d3ca2d731f9342) * Update dependencies | [64631a4](https://github.com/undergroundwires/privacy.sexy/commit/64631a4552fad7f7b06286aba8d3ca2d731f9342)

111
README.md
View File

@@ -2,21 +2,104 @@
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆 > Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](./CONTRIBUTING.md) <!-- markdownlint-disable MD033 -->
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript) <p align="center">
[![Maintainability](https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability)](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability) <a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md">
[![Tests status](https://github.com/undergroundwires/privacy.sexy/workflows/Test/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions) <img
[![Quality checks status](https://github.com/undergroundwires/privacy.sexy/workflows/Quality%20checks/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions) alt="contributions are welcome"
[![Security checks status](https://github.com/undergroundwires/privacy.sexy/workflows/Security%20checks/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions) src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"
[![Bump & release status](https://github.com/undergroundwires/privacy.sexy/workflows/Bump%20&%20release/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions) />
[![Deploy status](https://github.com/undergroundwires/privacy.sexy/workflows/Build%20&%20deploy/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions) </a>
[![Auto-versioned by bump-everywhere](https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true)](https://github.com/undergroundwires/bump-everywhere) <!-- Code quality -->
<br />
<a href="https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript">
<img
alt="Language grade: JavaScript/TypeScript"
src="https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18"
/>
</a>
<a href="https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability">
<img
alt="Maintainability"
src="https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability"
/>
</a>
<!-- Tests -->
<br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.unit.yaml">
<img
alt="Unit tests status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/unit-tests/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.integration.yaml">
<img
alt="Integration tests status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/integration-tests/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.e2e.yaml">
<img
alt="E2E tests status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/e2e-tests/badge.svg"
/>
</a>
<!-- Checks -->
<br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml">
<img
alt="Quality checks status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.yaml">
<img
alt="Security checks status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/security-checks/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml">
<img
alt="Build checks status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
/>
</a>
<!-- Release -->
<br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml">
<img
alt="Git release status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-git/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.site.yaml">
<img
alt="Site release status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-site/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.desktop.yaml">
<img
alt="Desktop application release status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-desktop/badge.svg"
/>
</a>
<!-- Others -->
<br />
<a href="https://github.com/undergroundwires/bump-everywhere">
<img
alt="Auto-versioned by bump-everywhere"
src="https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true"
/>
</a>
</p>
<!-- markdownlint-restore -->
## Get started ## Get started
- Online version at [https://privacy.sexy](https://privacy.sexy) - Online version at [https://privacy.sexy](https://privacy.sexy)
- 💡 No need to run any compiled software on your computer. - 💡 No need to run any compiled software on your computer.
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-Setup-0.11.1.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-0.11.1.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-0.11.1.AppImage). - Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-Setup-0.11.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.AppImage).
- 💡 Single click to execute your script. - 💡 Single click to execute your script.
- ❗ Come back regularly to apply latest version for stronger privacy and security. - ❗ Come back regularly to apply latest version for stronger privacy and security.
@@ -49,6 +132,9 @@
- Testing - Testing
- Run unit tests: `npm run test:unit` - Run unit tests: `npm run test:unit`
- Run integration tests: `npm run test:integration` - Run integration tests: `npm run test:integration`
- Run e2e (end-to-end) tests
- Interactive mode with GUI: `npm run test:e2e`
- Headless mode without GUI: `npm run test:e2e -- --headless`
- Lint: `npm run lint` - Lint: `npm run lint`
- **Desktop app** - **Desktop app**
- Development: `npm run electron:serve` - Development: `npm run electron:serve`
@@ -57,8 +143,8 @@
- Development: `npm run serve` to compile & hot-reload for development. - Development: `npm run serve` to compile & hot-reload for development.
- Production: `npm run build` to prepare files for distribution. - Production: `npm run build` to prepare files for distribution.
- Or run using Docker: - Or run using Docker:
1. Build: `docker build -t undergroundwires/privacy.sexy:0.11.1 .` 1. Build: `docker build -t undergroundwires/privacy.sexy:0.11.2 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.1 undergroundwires/privacy.sexy:0.11.1` 2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.2 undergroundwires/privacy.sexy:0.11.2`
## Architecture overview ## Architecture overview
@@ -84,5 +170,6 @@
- CI/CD is fully automated for this repo using different GIT events & GitHub actions. - CI/CD is fully automated for this repo using different GIT events & GitHub actions.
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action - Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
- Everything that's merged in the master goes directly to production. - Everything that's merged in the master goes directly to production.
- 📖 Read more on [CI/CD pipelines](./docs/ci-cd.md)
[![CI/CD to AWS with GitHub Actions](img/architecture/gitops.png)](.github/workflows/) [![CI/CD to AWS with GitHub Actions](img/architecture/gitops.png)](.github/workflows/)

View File

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

3
cypress.json Normal file
View File

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

19
docs/ci-cd.md Normal file
View File

@@ -0,0 +1,19 @@
# Pipelines
Pipelines are found under [`.github/workflows`](./../.github/workflows).
## Pipeline types
They are categorized based on their type:
- `tests`: Different types of tests to verify functionality.
- `checks`: Other controls such as vulnerability scans or styling checks.
- `release`: Pipelines used for release of deployment such as building and testing.
## Naming conventions
Pipeline files are named using: **`<type>.<name>.yaml`**.
**`type`**: Sub-folders do not work for GitHub workflows so that's why `<type>.` prefix is used. See also [pipeline types](#pipeline-types).
**`name`**: Pipeline themselves are named using kebab case. It allows for easier URL references for their status badges. E.g. file name `tests.unit.yaml`, pipeline name: `name: unit-tests`

View File

@@ -3,41 +3,59 @@
- There are two different types of tests executed: - There are two different types of tests executed:
1. [Unit tests](#unit-tests) 1. [Unit tests](#unit-tests)
2. [Integration tests](#integration-tests) 2. [Integration tests](#integration-tests)
3. [End-to-end (E2E) tests](#e2e-tests)
- All tests
- Uses [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/).
- Are written in files that includes `.spec` extension.
- 💡 You can use path/module alias `@/tests` in import statements. - 💡 You can use path/module alias `@/tests` in import statements.
## Unit tests ## Unit tests
- Tests each component in isolation - Tests each component in isolation.
- Defined in [`./tests/unit`](./../tests/unit) - Defined in [`./tests/unit`](./../tests/unit).
- They follow same folder structure as [`./src`](./../src) - They follow same folder structure as [`./src`](./../src).
### Naming ### Naming
- Each test suite first describe the system under test - Each test suite first describe the system under test.
- E.g. tests for class `Application` is categorized under `Application` - E.g. tests for class `Application` is categorized under `Application`.
- Tests for specific methods are categorized under method name (if applicable) - Tests for specific methods are categorized under method name (if applicable).
- E.g. test for `run()` is categorized under `run` - E.g. test for `run()` is categorized under `run`.
### Act, arrange, assert ### Act, arrange, assert
- Tests use act, arrange and assert (AAA) pattern when applicable - Tests use act, arrange and assert (AAA) pattern when applicable.
- **Arrange** - **Arrange**
- Should set up the test case - Should set up the test case.
- Starts with comment line `// arrange` - Starts with comment line `// arrange`.
- **Act** - **Act**
- Should cover the main thing to be tested - Should cover the main thing to be tested.
- Starts with comment line `// act` - Starts with comment line `// act`.
- **Assert** - **Assert**
- Should elicit some sort of response - Should elicit some sort of response.
- Starts with comment line `// assert` - Starts with comment line `// assert`.
### Stubs ### Stubs
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs) - Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs).
- They implement dummy behavior to be functional - They implement dummy behavior to be functional.
## Integration tests ## Integration tests
- Tests functionality of a component in combination with others (not isolated) - Tests functionality of a component in combination with others (not isolated).
- Ensure dependencies to third parties work as expected - Ensure dependencies to third parties work as expected.
- Defined in [`./tests/integration`](./../tests/integration) - Defined in [`./tests/integration`](./../tests/integration).
## E2E tests
- Test the functionality and performance of a running application.
- E2E tests are configured by vue plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) for Vue CLI.
- Names and folders are structured logically based on tests.
- The structure is following:
- [`cypress.json`](./../cypress.json): Cypress configuration file.
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
- [`/specs/`](./../tests/e2e/specs/): Test files, test are named with `.spec.js` extension.
- [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before project is loaded.
- [`/support/index.js`](./../tests/e2e/support/index.js): Support file, runs before every single spec file.
- *(Ignored)* `/videos`: Asset folder for videos taken during tests.
- *(Ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 579 KiB

After

Width:  |  Height:  |  Size: 255 KiB

37001
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.11.1", "version": "0.11.2",
"private": true, "private": true,
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆", "description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
"author": "undergroundwires", "author": "undergroundwires",
@@ -8,17 +8,18 @@
"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",
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\"", "test:e2e": "vue-cli-service test:e2e",
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency", "lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
"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:md": "markdownlint **/*.md --ignore node_modules", "lint:md": "markdownlint **/*.md --ignore node_modules",
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent", "lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
"lint:md:relative-urls": "remark . --frail --use remark-validate-links", "lint:md:relative-urls": "remark . --frail --use remark-validate-links",
"lint:vue": "vue-cli-service lint --no-fix", "lint: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\""
}, },
"main": "background.js", "main": "background.js",
"dependencies": { "dependencies": {
@@ -47,16 +48,27 @@
"@types/chai": "^4.2.22", "@types/chai": "^4.2.22",
"@types/file-saver": "^2.0.3", "@types/file-saver": "^2.0.3",
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
"@vue/cli-plugin-babel": "^4.5.14", "@typescript-eslint/eslint-plugin": "^5.4.0",
"@vue/cli-plugin-typescript": "^4.5.14", "@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-unit-mocha": "^4.5.14", "@vue/cli-plugin-babel": "~5.0.0-rc.1",
"@vue/cli-service": "^4.5.14", "@vue/cli-plugin-e2e-cypress": "~5.0.0-rc.1",
"@vue/cli-plugin-eslint": "~5.0.0-rc.1",
"@vue/cli-plugin-typescript": "~5.0.0-rc.1",
"@vue/cli-plugin-unit-mocha": "~5.0.0-rc.1",
"@vue/cli-service": "~5.0.0-rc.1",
"@vue/eslint-config-airbnb": "^6.0.0",
"@vue/eslint-config-typescript": "^9.1.0",
"@vue/test-utils": "1.2.2", "@vue/test-utils": "1.2.2",
"chai": "^4.3.4", "chai": "^4.3.4",
"cypress": "^8.3.0",
"electron": "^15.3.0", "electron": "^15.3.0",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-log": "^4.4.1", "electron-log": "^4.4.1",
"electron-updater": "^4.3.9", "electron-updater": "^4.3.9",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-vue": "^8.0.3",
"eslint-plugin-vuejs-accessibility": "^1.1.0",
"js-yaml-loader": "^1.2.2", "js-yaml-loader": "^1.2.2",
"markdownlint-cli": "^0.29.0", "markdownlint-cli": "^0.29.0",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
@@ -66,12 +78,16 @@
"remark-validate-links": "^11.0.1", "remark-validate-links": "^11.0.1",
"sass": "^1.43.3", "sass": "^1.43.3",
"sass-loader": "10.2.0", "sass-loader": "10.2.0",
"ts-loader": "9.0.1",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"typescript": "^4.4.4", "typescript": "^4.4.4",
"vue-cli-plugin-electron-builder": "^2.1.1", "vue-cli-plugin-electron-builder": "^2.1.1",
"vue-template-compiler": "^2.6.14", "vue-template-compiler": "^2.6.14",
"yaml-lint": "^1.2.4" "yaml-lint": "^1.2.4"
}, },
"//devDependencies": {
"ts-loader": "Here as workaround for vue-cli-plugin-electron-builder using older webpack 4"
},
"homepage": "https://privacy.sexy", "homepage": "https://privacy.sexy",
"repository": { "repository": {
"type": "git", "type": "git",

View File

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

View File

@@ -8,13 +8,16 @@ const ApplicationGetter: ApplicationGetter = parseApplication;
export class ApplicationFactory implements IApplicationFactory { export class ApplicationFactory implements IApplicationFactory {
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter); public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
private readonly getter: AsyncLazy<IApplication>; private readonly getter: AsyncLazy<IApplication>;
protected constructor(costlyGetter: ApplicationGetter) { protected constructor(costlyGetter: ApplicationGetter) {
if (!costlyGetter) { if (!costlyGetter) {
throw new Error('undefined getter'); throw new Error('undefined getter');
} }
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter())); this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
} }
public getApp(): Promise<IApplication> { public getApp(): Promise<IApplication> {
return this.getter.getValue(); return this.getter.getValue();
} }

View File

@@ -1,20 +1,25 @@
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611 // Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
export type EnumType = number | string; export type EnumType = number | string;
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue }; export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
= { [key in T]: TEnumValue };
export interface IEnumParser<TEnum> { export interface IEnumParser<TEnum> {
parseEnum(value: string, propertyName: string): TEnum; parseEnum(value: string, propertyName: string): TEnum;
} }
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>( export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> { enumVariable: EnumVariable<T, TEnumValue>,
): IEnumParser<TEnumValue> {
return { return {
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable), parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
}; };
} }
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>( function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
value: string, value: string,
enumName: string, enumName: string,
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue { enumVariable: EnumVariable<T, TEnumValue>,
): TEnumValue {
if (!value) { if (!value) {
throw new Error(`undefined ${enumName}`); throw new Error(`undefined ${enumName}`);
} }
@@ -29,22 +34,26 @@ function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
return enumVariable[casedValue as keyof typeof enumVariable]; return enumVariable[casedValue as keyof typeof enumVariable];
} }
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>( export function getEnumNames
enumVariable: EnumVariable<T, TEnumValue>): string[] { <T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>,
): string[] {
return Object return Object
.values(enumVariable) .values(enumVariable)
.filter((enumMember) => typeof enumMember === 'string') as string[]; .filter((enumMember) => typeof enumMember === 'string') as string[];
} }
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>( export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] { enumVariable: EnumVariable<T, TEnumValue>,
): TEnumValue[] {
return getEnumNames(enumVariable) return getEnumNames(enumVariable)
.map((level) => enumVariable[level]) as TEnumValue[]; .map((level) => enumVariable[level]) as TEnumValue[];
} }
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>( export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
value: TEnumValue, value: TEnumValue,
enumVariable: EnumVariable<T, TEnumValue>) { enumVariable: EnumVariable<T, TEnumValue>,
) {
if (value === undefined) { if (value === undefined) {
throw new Error('undefined enum value'); throw new Error('undefined enum value');
} }

View File

@@ -1,6 +1,6 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
import { assertInRange } from '@/application/Common/Enum'; import { assertInRange } from '@/application/Common/Enum';
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
type Getter<T> = () => T; type Getter<T> = () => T;
@@ -27,5 +27,4 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
} }
this.getters.set(language, getter); this.getters.set(language, getter);
} }
} }

View File

@@ -1,17 +1,19 @@
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
import { CategoryCollectionState } from './State/CategoryCollectionState';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { assertInRange } from '@/application/Common/Enum'; import { assertInRange } from '@/application/Common/Enum';
import { CategoryCollectionState } from './State/CategoryCollectionState';
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>; type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
export class ApplicationContext implements IApplicationContext { export class ApplicationContext implements IApplicationContext {
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>(); public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
public collection: ICategoryCollection; public collection: ICategoryCollection;
public currentOs: OperatingSystem; public currentOs: OperatingSystem;
public get state(): ICategoryCollectionState { public get state(): ICategoryCollectionState {
@@ -19,9 +21,11 @@ export class ApplicationContext implements IApplicationContext {
} }
private readonly states: StateMachine; private readonly states: StateMachine;
public constructor( public constructor(
public readonly app: IApplication, public readonly app: IApplication,
initialContext: OperatingSystem) { initialContext: OperatingSystem,
) {
validateApp(app); validateApp(app);
assertInRange(initialContext, OperatingSystem); assertInRange(initialContext, OperatingSystem);
this.states = initializeStates(app); this.states = initializeStates(app);

View File

@@ -1,15 +1,16 @@
import { ApplicationContext } from './ApplicationContext';
import { IApplicationContext } from '@/application/Context/IApplicationContext'; import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { Environment } from '../Environment/Environment';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { Environment } from '../Environment/Environment';
import { IEnvironment } from '../Environment/IEnvironment'; import { IEnvironment } from '../Environment/IEnvironment';
import { IApplicationFactory } from '../IApplicationFactory'; import { IApplicationFactory } from '../IApplicationFactory';
import { ApplicationFactory } from '../ApplicationFactory'; import { ApplicationFactory } from '../ApplicationFactory';
import { ApplicationContext } from './ApplicationContext';
export async function buildContext( export async function buildContext(
factory: IApplicationFactory = ApplicationFactory.Current, factory: IApplicationFactory = ApplicationFactory.Current,
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> { environment = Environment.CurrentEnvironment,
): Promise<IApplicationContext> {
if (!factory) { throw new Error('undefined factory'); } if (!factory) { throw new Error('undefined factory'); }
if (!environment) { throw new Error('undefined environment'); } if (!environment) { throw new Error('undefined environment'); }
const app = await factory.getApp(); const app = await factory.getApp();

View File

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

View File

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

View File

@@ -1,24 +1,26 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { CodeChangedEvent } from './Event/CodeChangedEvent'; import { CodeChangedEvent } from './Event/CodeChangedEvent';
import { CodePosition } from './Position/CodePosition'; import { CodePosition } from './Position/CodePosition';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
import { UserScriptGenerator } from './Generation/UserScriptGenerator'; import { UserScriptGenerator } from './Generation/UserScriptGenerator';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IApplicationCode } from './IApplicationCode'; import { IApplicationCode } from './IApplicationCode';
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator'; import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
export class ApplicationCode implements IApplicationCode { export class ApplicationCode implements IApplicationCode {
public readonly changed = new EventSource<ICodeChangedEvent>(); public readonly changed = new EventSource<ICodeChangedEvent>();
public current: string; public current: string;
private scriptPositions = new Map<SelectedScript, CodePosition>(); private scriptPositions = new Map<SelectedScript, CodePosition>();
constructor( constructor(
userSelection: IUserSelection, userSelection: IReadOnlyUserSelection,
private readonly scriptingDefinition: IScriptingDefinition, private readonly scriptingDefinition: IScriptingDefinition,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) { private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
) {
if (!userSelection) { throw new Error('userSelection is null or undefined'); } if (!userSelection) { throw new Error('userSelection is null or undefined'); }
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); } if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
if (!generator) { throw new Error('generator is null or undefined'); } if (!generator) { throw new Error('generator is null or undefined'); }

View File

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

View File

@@ -17,14 +17,13 @@ export abstract class CodeBuilder implements ICodeBuilder {
return this; return this;
} }
const lines = code.match(/[^\r\n]+/g); const lines = code.match(/[^\r\n]+/g);
for (const line of lines) { this.lines.push(...lines);
this.lines.push(line);
}
return this; return this;
} }
public appendTrailingHyphensCommentLine( public appendTrailingHyphensCommentLine(
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder { totalRepeatHyphens: number = TotalFunctionSeparatorChars,
): CodeBuilder {
return this.appendCommentLine('-'.repeat(totalRepeatHyphens)); return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
} }
@@ -45,7 +44,8 @@ export abstract class CodeBuilder implements ICodeBuilder {
public appendCommentLineWithHyphensAround( public appendCommentLineWithHyphensAround(
sectionName: string, sectionName: string,
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder { totalRepeatHyphens: number = TotalFunctionSeparatorChars,
): CodeBuilder {
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); } if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
if (sectionName.length >= totalRepeatHyphens) { if (sectionName.length >= totalRepeatHyphens) {
return this.appendCommentLine(sectionName); return this.appendCommentLine(sectionName);
@@ -63,5 +63,6 @@ export abstract class CodeBuilder implements ICodeBuilder {
} }
protected abstract getCommentDelimiter(): string; protected abstract getCommentDelimiter(): string;
protected abstract writeStandardOut(text: string): string; protected abstract writeStandardOut(text: string): string;
} }

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IUserScript } from './IUserScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { IUserScript } from './IUserScript';
export interface IUserScriptGenerator { export interface IUserScriptGenerator {
buildCode( buildCode(

View File

@@ -4,6 +4,7 @@ export class BatchBuilder extends CodeBuilder {
protected getCommentDelimiter(): string { protected getCommentDelimiter(): string {
return '::'; return '::';
} }
protected writeStandardOut(text: string): string { protected writeStandardOut(text: string): string {
return `echo ${escapeForEcho(text)}`; return `echo ${escapeForEcho(text)}`;
} }

View File

@@ -4,6 +4,7 @@ export class ShellBuilder extends CodeBuilder {
protected getCommentDelimiter(): string { protected getCommentDelimiter(): string {
return '#'; return '#';
} }
protected writeStandardOut(text: string): string { protected writeStandardOut(text: string): string {
return `echo '${escapeForEcho(text)}'`; return `echo '${escapeForEcho(text)}'`;
} }

View File

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

View File

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

View File

@@ -7,7 +7,8 @@ export class CodePosition implements ICodePosition {
constructor( constructor(
public readonly startLine: number, public readonly startLine: number,
public readonly endLine: number) { public readonly endLine: number,
) {
if (startLine < 0) { if (startLine < 0) {
throw new Error('Code cannot start in a negative line'); throw new Error('Code cannot start in a negative line');
} }

View File

@@ -1,16 +1,18 @@
import { IFilterResult } from './IFilterResult';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { IFilterResult } from './IFilterResult';
export class FilterResult implements IFilterResult { export class FilterResult implements IFilterResult {
constructor( constructor(
public readonly scriptMatches: ReadonlyArray<IScript>, public readonly scriptMatches: ReadonlyArray<IScript>,
public readonly categoryMatches: ReadonlyArray<ICategory>, public readonly categoryMatches: ReadonlyArray<ICategory>,
public readonly query: string) { public readonly query: string,
) {
if (!query) { throw new Error('Query is empty or undefined'); } if (!query) { throw new Error('Query is empty or undefined'); }
if (!scriptMatches) { throw new Error('Script matches is undefined'); } if (!scriptMatches) { throw new Error('Script matches is undefined'); }
if (!categoryMatches) { throw new Error('Category matches is undefined'); } if (!categoryMatches) { throw new Error('Category matches is undefined'); }
} }
public hasAnyMatches(): boolean { public hasAnyMatches(): boolean {
return this.scriptMatches.length > 0 return this.scriptMatches.length > 0
|| this.categoryMatches.length > 0; || this.categoryMatches.length > 0;

View File

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

View File

@@ -1,13 +1,15 @@
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource';
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 { EventSource } from '@/infrastructure/Events/EventSource';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
export class UserFilter implements IUserFilter { export class UserFilter implements IUserFilter {
public readonly filtered = new EventSource<IFilterResult>(); public readonly filtered = new EventSource<IFilterResult>();
public readonly filterRemoved = new EventSource<void>(); public readonly filterRemoved = new EventSource<void>();
public currentFilter: IFilterResult | undefined; public currentFilter: IFilterResult | undefined;
constructor(private collection: ICategoryCollection) { constructor(private collection: ICategoryCollection) {
@@ -20,9 +22,11 @@ export class UserFilter implements IUserFilter {
} }
const filterLowercase = filter.toLocaleLowerCase(); const filterLowercase = filter.toLocaleLowerCase();
const filteredScripts = this.collection.getAllScripts().filter( const filteredScripts = this.collection.getAllScripts().filter(
(script) => isScriptAMatch(script, filterLowercase)); (script) => isScriptAMatch(script, filterLowercase),
);
const filteredCategories = this.collection.getAllCategories().filter( const filteredCategories = this.collection.getAllCategories().filter(
(category) => category.name.toLowerCase().includes(filterLowercase)); (category) => category.name.toLowerCase().includes(filterLowercase),
);
const matches = new FilterResult( const matches = new FilterResult(
filteredScripts, filteredScripts,
filteredCategories, filteredCategories,

View File

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

View File

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

View File

@@ -1,26 +1,26 @@
import { SelectedScript } from './SelectedScript';
import { IUserSelection } from './IUserSelection';
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { IRepository } from '@/infrastructure/Repository/IRepository'; import { IRepository } from '@/infrastructure/Repository/IRepository';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { IUserSelection } from './IUserSelection';
import { SelectedScript } from './SelectedScript';
export class UserSelection implements IUserSelection { export class UserSelection implements IUserSelection {
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>(); public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: IRepository<string, SelectedScript>; private readonly scripts: IRepository<string, SelectedScript>;
constructor( constructor(
private readonly collection: ICategoryCollection, private readonly collection: ICategoryCollection,
selectedScripts: ReadonlyArray<SelectedScript>) { selectedScripts: ReadonlyArray<SelectedScript>,
) {
this.scripts = new InMemoryRepository<string, SelectedScript>(); this.scripts = new InMemoryRepository<string, SelectedScript>();
if (selectedScripts && selectedScripts.length > 0) {
for (const script of selectedScripts) { for (const script of selectedScripts) {
this.scripts.addItem(script); this.scripts.addItem(script);
} }
} }
}
public areAllSelected(category: ICategory): boolean { public areAllSelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) { if (this.selectedScripts.length === 0) {
@@ -30,7 +30,9 @@ export class UserSelection implements IUserSelection {
if (this.selectedScripts.length < scripts.length) { if (this.selectedScripts.length < scripts.length) {
return false; return false;
} }
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id)); return scripts.every(
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
);
} }
public isAnySelected(category: ICategory): boolean { public isAnySelected(category: ICategory): boolean {
@@ -53,19 +55,20 @@ export class UserSelection implements IUserSelection {
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void { public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
const category = this.collection.findCategory(categoryId); const scriptsToAddOrUpdate = this.collection
const scriptsToAddOrUpdate = category.getAllScriptsRecursively() .findCategory(categoryId)
.filter((script) => .getAllScriptsRecursively()
!this.scripts.exists(script.id) .filter(
(script) => !this.scripts.exists(script.id)
|| this.scripts.getById(script.id).revert !== revert, || this.scripts.getById(script.id).revert !== revert,
); )
.map((script) => new SelectedScript(script, revert));
if (!scriptsToAddOrUpdate.length) { if (!scriptsToAddOrUpdate.length) {
return; return;
} }
for (const script of scriptsToAddOrUpdate) { for (const script of scriptsToAddOrUpdate) {
const selectedScript = new SelectedScript(script, revert); this.scripts.addOrUpdateItem(script);
this.scripts.addOrUpdateItem(selectedScript);
} }
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
@@ -102,11 +105,12 @@ export class UserSelection implements IUserSelection {
} }
public selectAll(): void { public selectAll(): void {
for (const script of this.collection.getAllScripts()) { const scriptsToSelect = this.collection
if (!this.scripts.exists(script.id)) { .getAllScripts()
const selection = new SelectedScript(script, false); .filter((script) => !this.scripts.exists(script.id))
this.scripts.addItem(selection); .map((script) => new SelectedScript(script, false));
} for (const script of scriptsToSelect) {
this.scripts.addItem(script);
} }
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
@@ -131,10 +135,11 @@ export class UserSelection implements IUserSelection {
.forEach((scriptId) => this.scripts.removeItem(scriptId)); .forEach((scriptId) => this.scripts.removeItem(scriptId));
} }
// Select from unselected scripts // Select from unselected scripts
const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id)); const unselectedScripts = scripts
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new SelectedScript(script, false));
for (const toSelect of unselectedScripts) { for (const toSelect of unselectedScripts) {
const selection = new SelectedScript(toSelect, false); this.scripts.addItem(toSelect);
this.scripts.addItem(selection);
} }
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }

View File

@@ -4,6 +4,7 @@ import { IBrowserOsDetector } from './IBrowserOsDetector';
export class BrowserOsDetector implements IBrowserOsDetector { export class BrowserOsDetector implements IBrowserOsDetector {
private readonly detectors = BrowserDetectors; private readonly detectors = BrowserDetectors;
public detect(userAgent: string): OperatingSystem | undefined { public detect(userAgent: string): OperatingSystem | undefined {
if (!userAgent) { if (!userAgent) {
return undefined; return undefined;
@@ -19,35 +20,37 @@ export class BrowserOsDetector implements IBrowserOsDetector {
} }
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304 // Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
const BrowserDetectors = const BrowserDetectors = [
[ define(OperatingSystem.KaiOS, (b) => b
define(OperatingSystem.KaiOS, (b) => .mustInclude('KAIOS')),
b.mustInclude('KAIOS')), define(OperatingSystem.ChromeOS, (b) => b
define(OperatingSystem.ChromeOS, (b) => .mustInclude('CrOS')),
b.mustInclude('CrOS')), define(OperatingSystem.BlackBerryOS, (b) => b
define(OperatingSystem.BlackBerryOS, (b) => .mustInclude('BlackBerry')),
b.mustInclude('BlackBerry')), define(OperatingSystem.BlackBerryTabletOS, (b) => b
define(OperatingSystem.BlackBerryTabletOS, (b) => .mustInclude('RIM Tablet OS')),
b.mustInclude('RIM Tablet OS')), define(OperatingSystem.BlackBerry, (b) => b
define(OperatingSystem.BlackBerry, (b) => .mustInclude('BB10')),
b.mustInclude('BB10')), define(OperatingSystem.Android, (b) => b
define(OperatingSystem.Android, (b) => .mustInclude('Android').mustNotInclude('Windows Phone')),
b.mustInclude('Android').mustNotInclude('Windows Phone')), define(OperatingSystem.Android, (b) => b
define(OperatingSystem.Android, (b) => .mustInclude('Adr').mustNotInclude('Windows Phone')),
b.mustInclude('Adr').mustNotInclude('Windows Phone')), define(OperatingSystem.iOS, (b) => b
define(OperatingSystem.iOS, (b) => .mustInclude('like Mac OS X')),
b.mustInclude('like Mac OS X')), define(OperatingSystem.Linux, (b) => b
define(OperatingSystem.Linux, (b) => .mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')), define(OperatingSystem.Windows, (b) => b
define(OperatingSystem.Windows, (b) => .mustInclude('Windows').mustNotInclude('Windows Phone')),
b.mustInclude('Windows').mustNotInclude('Windows Phone')), define(OperatingSystem.WindowsPhone, (b) => b
define(OperatingSystem.WindowsPhone, (b) => .mustInclude('Windows Phone')),
b.mustInclude('Windows Phone')), define(OperatingSystem.macOS, (b) => b
define(OperatingSystem.macOS, (b) => .mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
]; ];
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector { function define(
os: OperatingSystem,
applyRules: (builder: DetectorBuilder) => DetectorBuilder,
): IBrowserOsDetector {
const builder = new DetectorBuilder(os); const builder = new DetectorBuilder(os);
applyRules(builder); applyRules(builder);
return builder.build(); return builder.build();

View File

@@ -1,8 +1,9 @@
import { IBrowserOsDetector } from './IBrowserOsDetector';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { IBrowserOsDetector } from './IBrowserOsDetector';
export class DetectorBuilder { export class DetectorBuilder {
private readonly existingPartsInUserAgent = new Array<string>(); private readonly existingPartsInUserAgent = new Array<string>();
private readonly notExistingPartsInUserAgent = new Array<string>(); private readonly notExistingPartsInUserAgent = new Array<string>();
constructor(private readonly os: OperatingSystem) { } constructor(private readonly os: OperatingSystem) { }

View File

@@ -1,9 +1,9 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector'; import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector'; import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
import { IEnvironment } from './IEnvironment'; import { IEnvironment } from './IEnvironment';
import { OperatingSystem } from '@/domain/OperatingSystem';
interface IEnvironmentVariables { export interface IEnvironmentVariables {
readonly window: Window & typeof globalThis; readonly window: Window & typeof globalThis;
readonly process: NodeJS.Process; readonly process: NodeJS.Process;
readonly navigator: Navigator; readonly navigator: Navigator;
@@ -12,21 +12,28 @@ interface IEnvironmentVariables {
export class Environment implements IEnvironment { export class Environment implements IEnvironment {
public static readonly CurrentEnvironment: IEnvironment = new Environment({ public static readonly CurrentEnvironment: IEnvironment = new Environment({
window, window,
process, process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
navigator, navigator,
}); });
public readonly isDesktop: boolean; public readonly isDesktop: boolean;
public readonly os: OperatingSystem; public readonly os: OperatingSystem;
protected constructor( protected constructor(
variables: IEnvironmentVariables, variables: IEnvironmentVariables,
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) { browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
) {
if (!variables) { if (!variables) {
throw new Error('variables is null or empty'); throw new Error('variables is null or empty');
} }
this.isDesktop = isDesktop(variables); this.isDesktop = isDesktop(variables);
this.os = this.isDesktop ? if (this.isDesktop) {
getDesktopOsType(getProcessPlatform(variables)) this.os = getDesktopOsType(getProcessPlatform(variables));
: browserOsDetector.detect(getUserAgent(variables)); } else {
const userAgent = getUserAgent(variables);
this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent);
}
} }
} }
@@ -46,15 +53,17 @@ function getProcessPlatform(variables: IEnvironmentVariables): string {
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined { function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
// https://nodejs.org/api/process.html#process_process_platform // https://nodejs.org/api/process.html#process_process_platform
if (processPlatform === 'darwin') { switch (processPlatform) {
case 'darwin':
return OperatingSystem.macOS; return OperatingSystem.macOS;
} else if (processPlatform === 'win32') { case 'win32':
return OperatingSystem.Windows; return OperatingSystem.Windows;
} else if (processPlatform === 'linux') { case 'linux':
return OperatingSystem.Linux; return OperatingSystem.Linux;
} default:
return undefined; return undefined;
} }
}
function isDesktop(variables: IEnvironmentVariables): boolean { function isDesktop(variables: IEnvironmentVariables): boolean {
// More: https://github.com/electron/electron/issues/2288 // More: https://github.com/electron/electron/issues/2288

View File

@@ -1,17 +1,18 @@
import { CollectionData } from 'js-yaml-loader!@/*';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { IProjectInformation } from '@/domain/IProjectInformation'; import { IProjectInformation } from '@/domain/IProjectInformation';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { parseCategoryCollection } from './CategoryCollectionParser';
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml'; import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml'; import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
import { CollectionData } from 'js-yaml-loader!@/*';
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';
export function parseApplication( export function parseApplication(
parser = CategoryCollectionParser, parser = CategoryCollectionParser,
processEnv: NodeJS.ProcessEnv = process.env, processEnv: NodeJS.ProcessEnv = process.env,
collectionsData = PreParsedCollections): IApplication { collectionsData = PreParsedCollections,
): IApplication {
validateCollectionsData(collectionsData); validateCollectionsData(collectionsData);
const information = parseProjectInformation(processEnv); const information = parseProjectInformation(processEnv);
const collections = collectionsData.map((collection) => parser(collection, information)); const collections = collectionsData.map((collection) => parser(collection, information));
@@ -22,11 +23,13 @@ export function parseApplication(
export type CategoryCollectionParserType export type CategoryCollectionParserType
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection; = (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
const CategoryCollectionParser: CategoryCollectionParserType const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
= (file, info) => parseCategoryCollection(file, info); return parseCategoryCollection(file, info);
};
const PreParsedCollections: readonly CollectionData [] const PreParsedCollections: readonly CollectionData [] = [
= [ WindowsData, MacOsData ]; WindowsData, MacOsData,
];
function validateCollectionsData(collections: readonly CollectionData[]) { function validateCollectionsData(collections: readonly CollectionData[]) {
if (!collections.length) { if (!collections.length) {

View File

@@ -1,32 +1,29 @@
import { Category } from '@/domain/Category';
import { CollectionData } from 'js-yaml-loader!@/*'; import { CollectionData } from 'js-yaml-loader!@/*';
import { parseCategory } from './CategoryParser';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { createEnumParser } from '../Common/Enum';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategoryCollection } from '@/domain/CategoryCollection'; import { CategoryCollection } from '@/domain/CategoryCollection';
import { IProjectInformation } from '@/domain/IProjectInformation'; import { IProjectInformation } from '@/domain/IProjectInformation';
import { createEnumParser } from '../Common/Enum';
import { parseCategory } from './CategoryParser';
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext'; import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser'; import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
export function parseCategoryCollection( export function parseCategoryCollection(
content: CollectionData, content: CollectionData,
info: IProjectInformation, info: IProjectInformation,
osParser = createEnumParser(OperatingSystem)): ICategoryCollection { osParser = createEnumParser(OperatingSystem),
): ICategoryCollection {
validate(content); validate(content);
const scripting = new ScriptingDefinitionParser() const scripting = new ScriptingDefinitionParser()
.parse(content.scripting, info); .parse(content.scripting, info);
const context = new CategoryCollectionParseContext(content.functions, scripting); const context = new CategoryCollectionParseContext(content.functions, scripting);
const categories = new Array<Category>(); const categories = content.actions.map((action) => parseCategory(action, context));
for (const action of content.actions) {
const category = parseCategory(action, context);
categories.push(category);
}
const os = osParser.parseEnum(content.os, 'os'); const os = osParser.parseEnum(content.os, 'os');
const collection = new CategoryCollection( const collection = new CategoryCollection(
os, os,
categories, categories,
scripting); scripting,
);
return collection; return collection;
} }

View File

@@ -1,18 +1,18 @@
import { CategoryData, ScriptData, CategoryOrScriptData } from 'js-yaml-loader!@/*'; import {
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
} from 'js-yaml-loader!@/*';
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 { parseDocUrls } from './DocumentationParser';
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext'; import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
import { parseScript } from './Script/ScriptParser'; import { parseScript } from './Script/ScriptParser';
let categoryIdCounter: number = 0; let categoryIdCounter = 0;
interface ICategoryChildren { export function parseCategory(
subCategories: Category[]; category: CategoryData,
subScripts: Script[]; context: ICategoryCollectionParseContext,
} ): Category {
export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category {
if (!context) { throw new Error('undefined context'); } if (!context) { throw new Error('undefined context'); }
ensureValid(category); ensureValid(category);
const children: ICategoryChildren = { const children: ICategoryChildren = {
@@ -23,11 +23,11 @@ export function parseCategory(category: CategoryData, context: ICategoryCollecti
parseCategoryChild(data, children, category, context); parseCategoryChild(data, children, category, context);
} }
return new Category( return new Category(
/*id*/ categoryIdCounter++, /* id: */ categoryIdCounter++,
/*name*/ category.category, /* name: */ category.category,
/*docs*/ parseDocUrls(category), /* docs: */ parseDocUrls(category),
/*categories*/ children.subCategories, /* categories: */ children.subCategories,
/*scripts*/ children.subScripts, /* scripts: */ children.subScripts,
); );
} }
@@ -43,11 +43,17 @@ function ensureValid(category: CategoryData) {
} }
} }
interface ICategoryChildren {
subCategories: Category[];
subScripts: Script[];
}
function parseCategoryChild( function parseCategoryChild(
data: CategoryOrScriptData, data: CategoryOrScriptData,
children: ICategoryChildren, children: ICategoryChildren,
parent: CategoryData, parent: CategoryData,
context: ICategoryCollectionParseContext) { context: ICategoryCollectionParseContext,
) {
if (isCategory(data)) { if (isCategory(data)) {
const subCategory = parseCategory(data as CategoryData, context); const subCategory = parseCategory(data as CategoryData, context);
children.subCategories.push(subCategory); children.subCategories.push(subCategory);
@@ -61,11 +67,20 @@ function parseCategoryChild(
} }
} }
function isScript(data: any): boolean { function isScript(data: CategoryOrScriptData): data is ScriptData {
return (data.code && data.code.length > 0) const holder = (data as InstructionHolder);
|| data.call; return hasCode(holder) || hasCall(holder);
} }
function isCategory(data: any): boolean { function isCategory(data: CategoryOrScriptData): data is CategoryData {
return data.category && data.category.length > 0; const { category } = data as CategoryData;
return category && category.length > 0;
}
function hasCode(holder: InstructionHolder): boolean {
return holder.code && holder.code.length > 0;
}
function hasCall(holder: InstructionHolder) {
return holder.call !== undefined;
} }

View File

@@ -4,7 +4,7 @@ export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<stri
if (!documentable) { if (!documentable) {
throw new Error('documentable is null or undefined'); throw new Error('documentable is null or undefined');
} }
const docs = documentable.docs; const { docs } = documentable;
if (!docs || !docs.length) { if (!docs || !docs.length) {
return []; return [];
} }
@@ -13,7 +13,10 @@ export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<stri
return result.getAll(); return result.getAll();
} }
function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer { function addDocs(
docs: DocumentationUrlsData,
urls: DocumentationUrlContainer,
): DocumentationUrlContainer {
if (docs instanceof Array) { if (docs instanceof Array) {
urls.addUrls(docs); urls.addUrls(docs);
} else if (typeof docs === 'string') { } else if (typeof docs === 'string') {
@@ -32,7 +35,7 @@ class DocumentationUrlContainer {
this.urls.push(url); this.urls.push(url);
} }
public addUrls(urls: readonly any[]) { public addUrls(urls: readonly string[]) {
for (const url of urls) { for (const url of urls) {
if (typeof url !== 'string') { if (typeof url !== 'string') {
throw new Error('Docs field (documentation url) must be an array of strings'); throw new Error('Docs field (documentation url) must be an array of strings');
@@ -53,8 +56,8 @@ function validateUrl(docUrl: string): void {
if (docUrl.includes('\n')) { if (docUrl.includes('\n')) {
throw new Error('Documentation url cannot be multi-lined.'); throw new Error('Documentation url cannot be multi-lined.');
} }
const res = docUrl.match( const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
/(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) { if (res == null) {
throw new Error(`Invalid documentation url: ${docUrl}`); throw new Error(`Invalid documentation url: ${docUrl}`);
} }

View File

@@ -2,7 +2,8 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
import { ProjectInformation } from '@/domain/ProjectInformation'; import { ProjectInformation } from '@/domain/ProjectInformation';
export function parseProjectInformation( export function parseProjectInformation(
environment: NodeJS.ProcessEnv): IProjectInformation { environment: NodeJS.ProcessEnv,
): IProjectInformation {
return new ProjectInformation( return new ProjectInformation(
environment.VUE_APP_NAME, environment.VUE_APP_NAME,
environment.VUE_APP_VERSION, environment.VUE_APP_VERSION,

View File

@@ -1,6 +1,6 @@
import { FunctionData } from 'js-yaml-loader!@/*';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/domain/ScriptCode';
import { FunctionData } from 'js-yaml-loader!@/*';
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';
@@ -9,12 +9,14 @@ import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext { export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
public readonly compiler: IScriptCompiler; public readonly compiler: IScriptCompiler;
public readonly syntax: ILanguageSyntax; public readonly syntax: ILanguageSyntax;
constructor( constructor(
functionsData: ReadonlyArray<FunctionData> | undefined, functionsData: ReadonlyArray<FunctionData> | undefined,
scripting: IScriptingDefinition, scripting: IScriptingDefinition,
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) { syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
) {
if (!scripting) { throw new Error('undefined scripting'); } if (!scripting) { throw new Error('undefined scripting'); }
this.syntax = syntaxFactory.create(scripting.language); this.syntax = syntaxFactory.create(scripting.language);
this.compiler = new ScriptCompiler(functionsData, this.syntax); this.compiler = new ScriptCompiler(functionsData, this.syntax);

View File

@@ -1,18 +1,19 @@
import { ExpressionPosition } from './ExpressionPosition'; import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { IExpression } from './IExpression';
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection'; import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection'; import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext'; import { IExpression } from './IExpression';
import { ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext'; import { ExpressionPosition } from './ExpressionPosition';
import { ExpressionEvaluationContext, IExpressionEvaluationContext } from './ExpressionEvaluationContext';
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string; export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
export class Expression implements IExpression { export class Expression implements IExpression {
constructor( constructor(
public readonly position: ExpressionPosition, public readonly position: ExpressionPosition,
public readonly evaluator: ExpressionEvaluator, public readonly evaluator: ExpressionEvaluator,
public readonly parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection()) { public readonly parameters
: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(),
) {
if (!position) { if (!position) {
throw new Error('undefined position'); throw new Error('undefined position');
} }
@@ -20,14 +21,15 @@ export class Expression implements IExpression {
throw new Error('undefined evaluator'); throw new Error('undefined evaluator');
} }
} }
public evaluate(context: IExpressionEvaluationContext): string { public evaluate(context: IExpressionEvaluationContext): string {
if (!context) { if (!context) {
throw new Error('undefined context'); throw new Error('undefined context');
} }
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args); validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
const args = filterUnusedArguments(this.parameters, context.args); const args = filterUnusedArguments(this.parameters, context.args);
context = new ExpressionEvaluationContext(args, context.pipelineCompiler); const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
return this.evaluator(context); return this.evaluator(filteredContext);
} }
} }
@@ -43,20 +45,19 @@ function validateThatAllRequiredParametersAreSatisfied(
.filter((parameterName) => !args.hasArgument(parameterName)); .filter((parameterName) => !args.hasArgument(parameterName));
if (missingParameterNames.length) { if (missingParameterNames.length) {
throw new Error( throw new Error(
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`); `argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`,
);
} }
} }
function filterUnusedArguments( function filterUnusedArguments(
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection): IReadOnlyFunctionCallArgumentCollection { allFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
): IReadOnlyFunctionCallArgumentCollection {
const specificCallArgs = new FunctionCallArgumentCollection(); const specificCallArgs = new FunctionCallArgumentCollection();
for (const parameter of parameters.all) { parameters.all
if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) { .filter((parameter) => allFunctionArgs.hasArgument(parameter.name))
continue; // Optional parameter is not necessarily provided .map((parameter) => allFunctionArgs.getArgument(parameter.name))
} .forEach((argument) => specificCallArgs.addArgument(argument));
const arg = allFunctionArgs.getArgument(parameter.name);
specificCallArgs.addArgument(arg);
}
return specificCallArgs; return specificCallArgs;
} }

View File

@@ -10,7 +10,8 @@ export interface IExpressionEvaluationContext {
export class ExpressionEvaluationContext implements IExpressionEvaluationContext { export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
constructor( constructor(
public readonly args: IReadOnlyFunctionCallArgumentCollection, public readonly args: IReadOnlyFunctionCallArgumentCollection,
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler()) { public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
) {
if (!args) { if (!args) {
throw new Error('undefined args, send empty collection instead'); throw new Error('undefined args, send empty collection instead');
} }

View File

@@ -1,7 +1,8 @@
export class ExpressionPosition { export class ExpressionPosition {
constructor( constructor(
public readonly start: number, public readonly start: number,
public readonly end: number) { public readonly end: number,
) {
if (start === end) { if (start === end) {
throw new Error(`no length (start = end = ${start})`); throw new Error(`no length (start = end = ${start})`);
} }

View File

@@ -1,5 +1,5 @@
import { ExpressionPosition } from './ExpressionPosition';
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection'; import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
import { ExpressionPosition } from './ExpressionPosition';
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext'; import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
export interface IExpression { export interface IExpression {

View File

@@ -1,20 +1,25 @@
import { IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
import { IExpressionsCompiler } from './IExpressionsCompiler'; import { IExpressionsCompiler } from './IExpressionsCompiler';
import { IExpression } from './Expression/IExpression'; import { IExpression } from './Expression/IExpression';
import { IExpressionParser } from './Parser/IExpressionParser'; import { IExpressionParser } from './Parser/IExpressionParser';
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser'; import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
import { ExpressionEvaluationContext } from './Expression/ExpressionEvaluationContext';
import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
export class ExpressionsCompiler implements IExpressionsCompiler { export class ExpressionsCompiler implements IExpressionsCompiler {
public constructor( public constructor(
private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { } private readonly extractor: IExpressionParser = new CompositeExpressionParser(),
) { }
public compileExpressions( public compileExpressions(
code: string, code: string | undefined,
args: IReadOnlyFunctionCallArgumentCollection): string { args: IReadOnlyFunctionCallArgumentCollection,
): string {
if (!args) { if (!args) {
throw new Error('undefined args, send empty collection instead'); throw new Error('undefined args, send empty collection instead');
} }
if (!code) {
return code;
}
const expressions = this.extractor.findExpressions(code); const expressions = this.extractor.findExpressions(code);
ensureParamsUsedInCodeHasArgsProvided(expressions, args); ensureParamsUsedInCodeHasArgsProvided(expressions, args);
const context = new ExpressionEvaluationContext(args); const context = new ExpressionEvaluationContext(args);
@@ -26,7 +31,8 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
function compileExpressions( function compileExpressions(
expressions: readonly IExpression[], expressions: readonly IExpression[],
code: string, code: string,
context: IExpressionEvaluationContext) { context: IExpressionEvaluationContext,
) {
let compiledCode = ''; let compiledCode = '';
const sortedExpressions = expressions const sortedExpressions = expressions
.slice() // copy the array to not mutate the parameter .slice() // copy the array to not mutate the parameter
@@ -48,20 +54,21 @@ function compileExpressions(
} }
function extractRequiredParameterNames( function extractRequiredParameterNames(
expressions: readonly IExpression[]): string[] { expressions: readonly IExpression[],
const usedParameterNames = expressions ): string[] {
return expressions
.map((e) => e.parameters.all .map((e) => e.parameters.all
.filter((p) => !p.isOptional) .filter((p) => !p.isOptional)
.map((p) => p.name)) .map((p) => p.name))
.filter((p) => p) .filter(Boolean) // Remove empty or undefined
.flat(); .flat()
const uniqueParameterNames = Array.from(new Set(usedParameterNames)); .filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
return uniqueParameterNames;
} }
function ensureParamsUsedInCodeHasArgsProvided( function ensureParamsUsedInCodeHasArgsProvided(
expressions: readonly IExpression[], expressions: readonly IExpression[],
providedArgs: IReadOnlyFunctionCallArgumentCollection): void { providedArgs: IReadOnlyFunctionCallArgumentCollection,
): void {
const usedParameterNames = extractRequiredParameterNames(expressions); const usedParameterNames = extractRequiredParameterNames(expressions);
if (!usedParameterNames?.length) { if (!usedParameterNames?.length) {
return; return;

View File

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

View File

@@ -1,7 +1,7 @@
import { IExpression } from '../Expression/IExpression'; import { IExpression } from '../Expression/IExpression';
import { IExpressionParser } from './IExpressionParser';
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser'; import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
import { WithParser } from '../SyntaxParsers/WithParser'; import { WithParser } from '../SyntaxParsers/WithParser';
import { IExpressionParser } from './IExpressionParser';
const Parsers = [ const Parsers = [
new ParameterSubstitutionParser(), new ParameterSubstitutionParser(),
@@ -14,14 +14,10 @@ export class CompositeExpressionParser implements IExpressionParser {
throw new Error('undefined leaf'); throw new Error('undefined leaf');
} }
} }
public findExpressions(code: string): IExpression[] { public findExpressions(code: string): IExpression[] {
const expressions = new Array<IExpression>(); return this.leafs.flatMap(
for (const parser of this.leafs) { (parser) => parser.findExpressions(code) || [],
const newExpressions = parser.findExpressions(code); );
if (newExpressions && newExpressions.length) {
expressions.push(...newExpressions);
}
}
return expressions;
} }
} }

View File

@@ -52,6 +52,7 @@ export class ExpressionRegexBuilder {
return this return this
.addRawRegex('\\s*'); .addRawRegex('\\s*');
} }
private addRawRegex(regex: string) { private addRawRegex(regex: string) {
this.parts.push(regex); this.parts.push(regex);
return this; return this;

View File

@@ -15,34 +15,45 @@ export abstract class RegexParser implements IExpressionParser {
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression; protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
private* findRegexExpressions(code: string): Iterable<IExpression> { private* findRegexExpressions(code: string): Iterable<IExpression> {
const matches = Array.from(code.matchAll(this.regex)); if (!code) {
for (const match of matches) { throw new Error('undefined code');
const startPos = match.index;
const endPos = startPos + match[0].length;
let position: ExpressionPosition;
try {
position = new ExpressionPosition(startPos, endPos);
} catch (error) {
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
} }
const matches = code.matchAll(this.regex);
for (const match of matches) {
const primitiveExpression = this.buildExpression(match); const primitiveExpression = this.buildExpression(match);
const parameters = getParameters(primitiveExpression); const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
const parameters = createParameters(primitiveExpression);
const expression = new Expression(position, primitiveExpression.evaluator, parameters); const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression; yield expression;
} }
} }
private doOrRethrow<T>(action: () => T, errorText: string, code: string): T {
try {
return action();
} catch (error) {
throw new Error(`[${this.constructor.name}] ${errorText}: ${error.message}\nRegex: ${this.regex}\nCode: ${code}`);
}
}
}
function createPosition(match: RegExpMatchArray): ExpressionPosition {
const startPos = match.index;
const endPos = startPos + match[0].length;
return new ExpressionPosition(startPos, endPos);
}
function createParameters(
expression: IPrimitiveExpression,
): FunctionParameterCollection {
return (expression.parameters || [])
.reduce((parameters, parameter) => {
parameters.addParameter(parameter);
return parameters;
}, new FunctionParameterCollection());
} }
export interface IPrimitiveExpression { export interface IPrimitiveExpression {
evaluator: ExpressionEvaluator; evaluator: ExpressionEvaluator;
parameters?: readonly IFunctionParameter[]; parameters?: readonly IFunctionParameter[];
} }
function getParameters(
expression: IPrimitiveExpression): FunctionParameterCollection {
const parameters = new FunctionParameterCollection();
for (const parameter of expression.parameters || []) {
parameters.addParameter(parameter);
}
return parameters;
}

View File

@@ -2,8 +2,10 @@ import { IPipe } from '../IPipe';
export class EscapeDoubleQuotes implements IPipe { export class EscapeDoubleQuotes implements IPipe {
public readonly name: string = 'escapeDoubleQuotes'; public readonly name: string = 'escapeDoubleQuotes';
public apply(raw: string): string { public apply(raw: string): string {
return raw?.replaceAll('"', '"^""'); return raw?.replaceAll('"', '"^""');
/* eslint-disable max-len */
/* /*
"^"" is the most robust and stable choice. "^"" is the most robust and stable choice.
Other options: Other options:
@@ -11,17 +13,18 @@ export class EscapeDoubleQuotes implements IPipe {
Breaks, because it is fundamentally unsupported Breaks, because it is fundamentally unsupported
"""" """"
Does not work with consecutive double quotes. Does not work with consecutive double quotes.
E.g. PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";" E.g. `PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"`
Works when using: PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";" Works when using: `PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"`
\" \"
May break as they are interpreted by cmd.exe as metacharacters breaking the command May break as they are interpreted by cmd.exe as metacharacters breaking the command
E.g. PowerShell -Command "Write-Host 'Hello \"w&orld\"'" does not work due to unescaped "&" E.g. `PowerShell -Command "Write-Host 'Hello \"w&orld\"'"` does not work due to unescaped "&"
Works when using: PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'" Works when using: `PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"`
\"" \""
Normalizes interior whitespace Normalizes interior whitespace
E.g. PowerShell -Command "\""a& c\"".length", outputs 4 and discards one of two whitespaces E.g. `PowerShell -Command "\""a& c\"".length"`, outputs 4 and discards one of two whitespaces
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 */
} }
} }

View File

@@ -2,18 +2,19 @@ import { IPipe } from '../IPipe';
export class InlinePowerShell implements IPipe { export class InlinePowerShell implements IPipe {
public readonly name: string = 'inlinePowerShell'; public readonly name: string = 'inlinePowerShell';
public apply(code: string): string { public apply(code: string): string {
if (!code || !hasLines(code)) { if (!code || !hasLines(code)) {
return code; return code;
} }
code = inlineComments(code); const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
code = mergeLinesWithBacktick(code); inlineComments,
code = mergeHereStrings(code); mergeLinesWithBacktick,
const lines = getLines(code) mergeHereStrings,
.map((line) => line.trim()) mergeNewLines,
.filter((line) => line.length > 0); ]).reduce((a, b) => (data) => b(a(data)));
return lines const newCode = processor(code);
.join('; '); return newCode;
} }
} }
@@ -112,15 +113,18 @@ interface IInlinedHereString {
readonly escapedQuotes: string; readonly escapedQuotes: string;
readonly separator: string; readonly separator: string;
} }
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
function getHereStringHandler(quotes: string): IInlinedHereString { function getHereStringHandler(quotes: string): IInlinedHereString {
/*
We handle @' and @" differently.
Single quotes are interpreted literally and doubles are expandable.
*/
const expandableNewLine = '`r`n'; const expandableNewLine = '`r`n';
switch (quotes) { switch (quotes) {
case '\'': case '\'':
return { return {
quotesAround: '\'', quotesAround: '\'',
escapedQuotes: '\'\'', escapedQuotes: '\'\'',
separator: `\'+"${expandableNewLine}"+\'`, separator: `'+"${expandableNewLine}"+'`,
}; };
case '"': case '"':
return { return {
@@ -153,3 +157,10 @@ function mergeLinesWithBacktick(code: string) {
*/ */
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' '); return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
} }
function mergeNewLines(code: string) {
return getLines(code)
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join('; ');
}

View File

@@ -13,6 +13,7 @@ export interface IPipeFactory {
export class PipeFactory implements IPipeFactory { export class PipeFactory implements IPipeFactory {
private readonly pipes = new Map<string, IPipe>(); private readonly pipes = new Map<string, IPipe>();
constructor(pipes: readonly IPipe[] = RegisteredPipes) { constructor(pipes: readonly IPipe[] = RegisteredPipes) {
if (pipes.some((pipe) => !pipe)) { if (pipes.some((pipe) => !pipe)) {
throw new Error('undefined pipe in list'); throw new Error('undefined pipe in list');
@@ -21,6 +22,7 @@ export class PipeFactory implements IPipeFactory {
this.registerPipe(pipe); this.registerPipe(pipe);
} }
} }
public get(pipeName: string): IPipe { public get(pipeName: string): IPipe {
validatePipeName(pipeName); validatePipeName(pipeName);
if (!this.pipes.has(pipeName)) { if (!this.pipes.has(pipeName)) {
@@ -28,6 +30,7 @@ export class PipeFactory implements IPipeFactory {
} }
return this.pipes.get(pipeName); return this.pipes.get(pipeName);
} }
private registerPipe(pipe: IPipe): void { private registerPipe(pipe: IPipe): void {
validatePipeName(pipe.name); validatePipeName(pipe.name);
if (this.pipes.has(pipe.name)) { if (this.pipes.has(pipe.name)) {

View File

@@ -3,14 +3,14 @@ import { IPipelineCompiler } from './IPipelineCompiler';
export class PipelineCompiler implements IPipelineCompiler { export class PipelineCompiler implements IPipelineCompiler {
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { } constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
public compile(value: string, pipeline: string): string { public compile(value: string, pipeline: string): string {
ensureValidArguments(value, pipeline); ensureValidArguments(value, pipeline);
const pipeNames = extractPipeNames(pipeline); const pipeNames = extractPipeNames(pipeline);
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName)); const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
for (const pipe of pipes) { return pipes.reduce((previousValue, pipe) => {
value = pipe.apply(value); return pipe.apply(previousValue);
} }, value);
return value;
} }
} }

View File

@@ -1,5 +1,5 @@
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter'; import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
export class ParameterSubstitutionParser extends RegexParser { export class ParameterSubstitutionParser extends RegexParser {
@@ -17,7 +17,7 @@ export class ParameterSubstitutionParser extends RegexParser {
return { return {
parameters: [new FunctionParameter(parameterName, false)], parameters: [new FunctionParameter(parameterName, false)],
evaluator: (context) => { evaluator: (context) => {
const argumentValue = context.args.getArgument(parameterName).argumentValue; const { argumentValue } = context.args.getArgument(parameterName);
if (!pipeline) { if (!pipeline) {
return argumentValue; return argumentValue;
} }

View File

@@ -1,5 +1,5 @@
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter'; import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
export class WithParser extends RegexParser { export class WithParser extends RegexParser {
@@ -25,8 +25,8 @@ export class WithParser extends RegexParser {
return { return {
parameters: [new FunctionParameter(parameterName, true)], parameters: [new FunctionParameter(parameterName, true)],
evaluator: (context) => { evaluator: (context) => {
const argumentValue = context.args.hasArgument(parameterName) ? const argumentValue = context.args.hasArgument(parameterName)
context.args.getArgument(parameterName).argumentValue ? context.args.getArgument(parameterName).argumentValue
: undefined; : undefined;
if (!argumentValue) { if (!argumentValue) {
return ''; return '';
@@ -51,7 +51,8 @@ const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
.buildRegExp(); .buildRegExp();
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) { function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those // Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
// but instead letting the pipeline compiler to fail on those.
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => { return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
return replacer(match1); return replacer(match1);
}); });

View File

@@ -1,10 +1,11 @@
import { IFunctionCallArgument } from './IFunctionCallArgument';
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator'; import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
import { IFunctionCallArgument } from './IFunctionCallArgument';
export class FunctionCallArgument implements IFunctionCallArgument { export class FunctionCallArgument implements IFunctionCallArgument {
constructor( constructor(
public readonly parameterName: string, public readonly parameterName: string,
public readonly argumentValue: string) { public readonly argumentValue: string,
) {
ensureValidParameterName(parameterName); ensureValidParameterName(parameterName);
if (!argumentValue) { if (!argumentValue) {
throw new Error(`undefined argument value for "${parameterName}"`); throw new Error(`undefined argument value for "${parameterName}"`);

View File

@@ -3,6 +3,7 @@ import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollecti
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection { export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
private readonly arguments = new Map<string, IFunctionCallArgument>(); private readonly arguments = new Map<string, IFunctionCallArgument>();
public addArgument(argument: IFunctionCallArgument): void { public addArgument(argument: IFunctionCallArgument): void {
if (!argument) { if (!argument) {
throw new Error('undefined argument'); throw new Error('undefined argument');
@@ -12,15 +13,18 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
} }
this.arguments.set(argument.parameterName, argument); this.arguments.set(argument.parameterName, argument);
} }
public getAllParameterNames(): string[] { public getAllParameterNames(): string[] {
return Array.from(this.arguments.keys()); return Array.from(this.arguments.keys());
} }
public hasArgument(parameterName: string): boolean { public hasArgument(parameterName: string): boolean {
if (!parameterName) { if (!parameterName) {
throw new Error('undefined parameter name'); throw new Error('undefined parameter name');
} }
return this.arguments.has(parameterName); return this.arguments.has(parameterName);
} }
public getArgument(parameterName: string): IFunctionCallArgument { public getArgument(parameterName: string): IFunctionCallArgument {
if (!parameterName) { if (!parameterName) {
throw new Error('undefined parameter name'); throw new Error('undefined parameter name');

View File

@@ -1,25 +1,27 @@
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { ICompiledCode } from './ICompiledCode'; import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection'; import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler'; import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler'; import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction'; import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '../FunctionCall'; import { FunctionCall } from '../FunctionCall';
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; import { IFunctionCallCompiler } from './IFunctionCallCompiler';
import { ICompiledCode } from './ICompiledCode';
export class FunctionCallCompiler implements IFunctionCallCompiler { export class FunctionCallCompiler implements IFunctionCallCompiler {
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler(); public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
protected constructor( protected constructor(
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) { private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
) {
} }
public compileCall( public compileCall(
calls: IFunctionCall[], calls: IFunctionCall[],
functions: ISharedFunctionCollection): ICompiledCode { functions: ISharedFunctionCollection,
): ICompiledCode {
if (!functions) { throw new Error('undefined functions'); } if (!functions) { throw new Error('undefined functions'); }
if (!calls) { throw new Error('undefined calls'); } if (!calls) { throw new Error('undefined calls'); }
if (calls.some((f) => !f)) { throw new Error('undefined function call'); } if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
@@ -45,24 +47,25 @@ interface ICompiledFunctionCall {
} }
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall { function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
const compiledFunctions = new Array<ICompiledFunctionCall>(); const compiledFunctions = context.callSequence
for (const call of context.callSequence) { .flatMap((call) => compileSingleCall(call, context));
const compiledCode = compileSingleCall(call, context);
compiledFunctions.push(...compiledCode);
}
return { return {
code: merge(compiledFunctions.map((f) => f.code)), code: merge(compiledFunctions.map((f) => f.code)),
revertCode: merge(compiledFunctions.map((f) => f.revertCode)), revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
}; };
} }
function compileSingleCall(call: IFunctionCall, context: ICompilationContext): ICompiledFunctionCall[] { function compileSingleCall(
call: IFunctionCall,
context: ICompilationContext,
): ICompiledFunctionCall[] {
const func = context.allFunctions.getFunctionByName(call.functionName); const func = context.allFunctions.getFunctionByName(call.functionName);
ensureThatCallArgumentsExistInParameterDefinition(func, call.args); ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
if (func.body.code) { // Function with inline code if (func.body.code) { // Function with inline code
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler); const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
return [compiledCode]; return [compiledCode];
} else { // Function with inner calls }
// Function with inner calls
return func.body.calls return func.body.calls
.map((innerCall) => { .map((innerCall) => {
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler); const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
@@ -71,12 +74,12 @@ function compileSingleCall(call: IFunctionCall, context: ICompilationContext): I
}) })
.flat(); .flat();
} }
}
function compileCode( function compileCode(
code: IFunctionCode, code: IFunctionCode,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler): ICompiledFunctionCall { compiler: IExpressionsCompiler,
): ICompiledFunctionCall {
return { return {
code: compiler.compileExpressions(code.do, args), code: compiler.compileExpressions(code.do, args),
revertCode: compiler.compileExpressions(code.revert, args), revertCode: compiler.compileExpressions(code.revert, args),
@@ -88,14 +91,17 @@ function compileArgs(
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler, compiler: IExpressionsCompiler,
): IReadOnlyFunctionCallArgumentCollection { ): IReadOnlyFunctionCallArgumentCollection {
const compiledArgs = new FunctionCallArgumentCollection(); return argsToCompile
for (const parameterName of argsToCompile.getAllParameterNames()) { .getAllParameterNames()
const argumentValue = argsToCompile.getArgument(parameterName).argumentValue; .map((parameterName) => {
const { argumentValue } = argsToCompile.getArgument(parameterName);
const compiledValue = compiler.compileExpressions(argumentValue, args); const compiledValue = compiler.compileExpressions(argumentValue, args);
const newArgument = new FunctionCallArgument(parameterName, compiledValue); return new FunctionCallArgument(parameterName, compiledValue);
compiledArgs.addArgument(newArgument); })
} .reduce((compiledArgs, arg) => {
compiledArgs.addArgument(arg);
return compiledArgs; return compiledArgs;
}, new FunctionCallArgumentCollection());
} }
function merge(codeParts: readonly string[]): string { function merge(codeParts: readonly string[]): string {
@@ -106,7 +112,8 @@ function merge(codeParts: readonly string[]): string {
function ensureThatCallArgumentsExistInParameterDefinition( function ensureThatCallArgumentsExistInParameterDefinition(
func: ISharedFunction, func: ISharedFunction,
args: IReadOnlyFunctionCallArgumentCollection): void { args: IReadOnlyFunctionCallArgumentCollection,
): void {
const callArgumentNames = args.getAllParameterNames(); const callArgumentNames = args.getAllParameterNames();
const functionParameterNames = func.parameters.all.map((param) => param.name) || []; const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames); const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
@@ -115,7 +122,8 @@ function ensureThatCallArgumentsExistInParameterDefinition(
function findUnexpectedParameters( function findUnexpectedParameters(
callArgumentNames: string[], callArgumentNames: string[],
functionParameterNames: string[]): string[] { functionParameterNames: string[],
): string[] {
if (!callArgumentNames.length && !functionParameterNames.length) { if (!callArgumentNames.length && !functionParameterNames.length) {
return []; return [];
} }
@@ -126,14 +134,16 @@ function findUnexpectedParameters(
function throwIfNotEmpty( function throwIfNotEmpty(
functionName: string, functionName: string,
unexpectedParameters: string[], unexpectedParameters: string[],
expectedParameters: string[]) { expectedParameters: string[],
) {
if (!unexpectedParameters.length) { if (!unexpectedParameters.length) {
return; return;
} }
throw new Error( throw new Error(
`Function "${functionName}" has unexpected parameter(s) provided: ` + // eslint-disable-next-line prefer-template
`"${unexpectedParameters.join('", "')}"` + `Function "${functionName}" has unexpected parameter(s) provided: `
'. Expected parameter(s): ' + + `"${unexpectedParameters.join('", "')}"`
(expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'), + '. Expected parameter(s): '
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
); );
} }

View File

@@ -1,6 +1,6 @@
import { ICompiledCode } from './ICompiledCode';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { IFunctionCall } from '../IFunctionCall'; import { IFunctionCall } from '../IFunctionCall';
import { ICompiledCode } from './ICompiledCode';
export interface IFunctionCallCompiler { export interface IFunctionCallCompiler {
compileCall( compileCall(

View File

@@ -4,7 +4,8 @@ import { IFunctionCall } from './IFunctionCall';
export class FunctionCall implements IFunctionCall { export class FunctionCall implements IFunctionCall {
constructor( constructor(
public readonly functionName: string, public readonly functionName: string,
public readonly args: IReadOnlyFunctionCallArgumentCollection) { public readonly args: IReadOnlyFunctionCallArgumentCollection,
) {
if (!functionName) { if (!functionName) {
throw new Error('empty function name in function call'); throw new Error('empty function name in function call');
} }

View File

@@ -1,11 +1,11 @@
import { FunctionCallData, FunctionCallsData } from 'js-yaml-loader!@/*'; import { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
import { IFunctionCall } from './IFunctionCall'; import { IFunctionCall } from './IFunctionCall';
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
import { FunctionCallArgument } from './Argument/FunctionCallArgument'; import { FunctionCallArgument } from './Argument/FunctionCallArgument';
import { FunctionCall } from './FunctionCall'; import { FunctionCall } from './FunctionCall';
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] { export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
if (!calls) { if (calls === undefined) {
throw new Error('undefined call data'); throw new Error('undefined call data');
} }
const sequence = getCallSequence(calls); const sequence = getCallSequence(calls);
@@ -24,12 +24,19 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
function parseFunctionCall(call: FunctionCallData): IFunctionCall { function parseFunctionCall(call: FunctionCallData): IFunctionCall {
if (!call) { if (!call) {
throw new Error(`undefined function call`); throw new Error('undefined function call');
} }
const args = new FunctionCallArgumentCollection(); const callArgs = parseArgs(call.parameters);
for (const parameterName of Object.keys(call.parameters || {})) { return new FunctionCall(call.function, callArgs);
const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]); }
function parseArgs(
parameters: FunctionCallParametersData,
): FunctionCallArgumentCollection {
return Object.keys(parameters || {})
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
.reduce((args, arg) => {
args.addArgument(arg); args.addArgument(arg);
} return args;
return new FunctionCall(call.function, args); }, new FunctionCallArgumentCollection());
} }

View File

@@ -1,5 +1,5 @@
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
import { IFunctionCall } from '../Function/Call/IFunctionCall'; import { IFunctionCall } from './Call/IFunctionCall';
export interface ISharedFunction { export interface ISharedFunction {
readonly name: string; readonly name: string;

View File

@@ -1,10 +1,11 @@
import { IFunctionParameter } from './IFunctionParameter';
import { ensureValidParameterName } from '../Shared/ParameterNameValidator'; import { ensureValidParameterName } from '../Shared/ParameterNameValidator';
import { IFunctionParameter } from './IFunctionParameter';
export class FunctionParameter implements IFunctionParameter { export class FunctionParameter implements IFunctionParameter {
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly isOptional: boolean) { public readonly isOptional: boolean,
) {
ensureValidParameterName(name); ensureValidParameterName(name);
} }
} }

View File

@@ -7,6 +7,7 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
public get all(): readonly IFunctionParameter[] { public get all(): readonly IFunctionParameter[] {
return this.parameters; return this.parameters;
} }
public addParameter(parameter: IFunctionParameter) { public addParameter(parameter: IFunctionParameter) {
this.ensureValidParameter(parameter); this.ensureValidParameter(parameter);
this.parameters.push(parameter); this.parameters.push(parameter);
@@ -15,6 +16,7 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
private includesName(name: string) { private includesName(name: string) {
return this.parameters.find((existingParameter) => existingParameter.name === name); return this.parameters.find((existingParameter) => existingParameter.name === name);
} }
private ensureValidParameter(parameter: IFunctionParameter) { private ensureValidParameter(parameter: IFunctionParameter) {
if (!parameter) { if (!parameter) {
throw new Error('undefined parameter'); throw new Error('undefined parameter');

View File

@@ -1,11 +1,14 @@
import { IFunctionCall } from '../Function/Call/IFunctionCall'; import { IFunctionCall } from './Call/IFunctionCall';
import { FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody } from './ISharedFunction'; import {
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
} from './ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
export function createCallerFunction( export function createCallerFunction(
name: string, name: string,
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
callSequence: readonly IFunctionCall[]): ISharedFunction { callSequence: readonly IFunctionCall[],
): ISharedFunction {
if (!callSequence) { if (!callSequence) {
throw new Error(`undefined call sequence in function "${name}"`); throw new Error(`undefined call sequence in function "${name}"`);
} }
@@ -19,7 +22,8 @@ export function createFunctionWithInlineCode(
name: string, name: string,
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
code: string, code: string,
revertCode?: string): ISharedFunction { revertCode?: string,
): ISharedFunction {
if (!code) { if (!code) {
throw new Error(`undefined code in function "${name}"`); throw new Error(`undefined code in function "${name}"`);
} }
@@ -32,6 +36,7 @@ export function createFunctionWithInlineCode(
class SharedFunction implements ISharedFunction { class SharedFunction implements ISharedFunction {
public readonly body: ISharedFunctionBody; public readonly body: ISharedFunctionBody;
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly parameters: IReadOnlyFunctionParameterCollection, public readonly parameters: IReadOnlyFunctionParameterCollection,
@@ -39,7 +44,7 @@ class SharedFunction implements ISharedFunction {
bodyType: FunctionBodyType, bodyType: FunctionBodyType,
) { ) {
if (!name) { throw new Error('undefined function name'); } if (!name) { throw new Error('undefined function name'); }
if (!parameters) { throw new Error(`undefined parameters`); } if (!parameters) { throw new Error('undefined parameters'); }
this.body = { this.body = {
type: bodyType, type: bodyType,
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined, code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,

View File

@@ -11,47 +11,51 @@ 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();
public parseFunctions( public parseFunctions(
functions: readonly FunctionData[]): ISharedFunctionCollection { functions: readonly FunctionData[],
): ISharedFunctionCollection {
const collection = new SharedFunctionCollection(); const collection = new SharedFunctionCollection();
if (!functions || !functions.length) { if (!functions || !functions.length) {
return collection; return collection;
} }
ensureValidFunctions(functions); ensureValidFunctions(functions);
for (const func of functions) { return functions
const sharedFunction = parseFunction(func); .map((func) => parseFunction(func))
collection.addFunction(sharedFunction); .reduce((acc, func) => {
} acc.addFunction(func);
return collection; return acc;
}, collection);
} }
} }
function parseFunction(data: FunctionData): ISharedFunction { function parseFunction(data: FunctionData): ISharedFunction {
const name = data.name; const { name } = data;
const parameters = parseParameters(data); const parameters = parseParameters(data);
if (hasCode(data)) { if (hasCode(data)) {
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode); return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
} else { // has call }
// Has call
const calls = parseFunctionCalls(data.call); const calls = parseFunctionCalls(data.call);
return createCallerFunction(name, parameters, calls); return createCallerFunction(name, parameters, calls);
} }
}
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection { function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
const parameters = new FunctionParameterCollection(); return (data.parameters || [])
if (!data.parameters) { .map((parameter) => {
return parameters;
}
for (const parameterData of data.parameters) {
const isOptional = parameterData.optional || false;
try { try {
const parameter = new FunctionParameter(parameterData.name, isOptional); return new FunctionParameter(
parameters.addParameter(parameter); parameter.name,
parameter.optional || false,
);
} catch (err) { } catch (err) {
throw new Error(`"${data.name}": ${err.message}`); throw new Error(`"${data.name}": ${err.message}`);
} }
} })
.reduce((parameters, parameter) => {
parameters.addParameter(parameter);
return parameters; return parameters;
}, new FunctionParameterCollection());
} }
function hasCode(data: FunctionData): boolean { function hasCode(data: FunctionData): boolean {
@@ -96,7 +100,7 @@ function ensureExpectedParametersType(functions: readonly FunctionData[]) {
} }
} }
function isArrayOfObjects(value: any): boolean { function isArrayOfObjects(value: unknown): boolean {
return Array.isArray(value) return Array.isArray(value)
&& value.every((item) => typeof item === 'object'); && value.every((item) => typeof item === 'object');
} }
@@ -115,15 +119,14 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
function ensureNoUndefinedItem(functions: readonly FunctionData[]) { function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
if (functions.some((func) => !func)) { if (functions.some((func) => !func)) {
throw new Error(`some functions are undefined`); throw new Error('some functions are undefined');
} }
} }
function ensureNoDuplicateCode(functions: readonly FunctionData[]) { function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
const duplicateCodes = getDuplicates(functions const duplicateCodes = getDuplicates(functions
.map((func) => func.code) .map((func) => func.code)
.filter((code) => code), .filter((code) => code));
);
if (duplicateCodes.length > 0) { if (duplicateCodes.length > 0) {
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`); throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
} }

View File

@@ -1,5 +1,5 @@
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptData } from 'js-yaml-loader!@/*'; import { ScriptData } from 'js-yaml-loader!@/*';
import { IScriptCode } from '@/domain/IScriptCode';
export interface IScriptCompiler { export interface IScriptCompiler {
canCompile(script: ScriptData): boolean; canCompile(script: ScriptData): boolean;

View File

@@ -1,7 +1,6 @@
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode';
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*'; import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
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';
@@ -12,15 +11,17 @@ import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
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, private readonly syntax: ILanguageSyntax,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance, private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
) { ) {
if (!syntax) { throw new Error('undefined syntax'); } if (!syntax) { throw new Error('undefined syntax'); }
this.functions = sharedFunctionsParser.parseFunctions(functions); this.functions = sharedFunctionsParser.parseFunctions(functions);
} }
public canCompile(script: ScriptData): boolean { public canCompile(script: ScriptData): boolean {
if (!script) { throw new Error('undefined script'); } if (!script) { throw new Error('undefined script'); }
if (!script.call) { if (!script.call) {
@@ -28,6 +29,7 @@ export class ScriptCompiler implements IScriptCompiler {
} }
return true; return true;
} }
public compile(script: ScriptData): IScriptCode { public compile(script: ScriptData): IScriptCode {
if (!script) { throw new Error('undefined script'); } if (!script) { throw new Error('undefined script'); }
try { try {
@@ -36,7 +38,8 @@ export class ScriptCompiler implements IScriptCompiler {
return new ScriptCode( return new ScriptCode(
compiledCode.code, compiledCode.code,
compiledCode.revertCode, compiledCode.revertCode,
this.syntax); this.syntax,
);
} catch (error) { } catch (error) {
throw Error(`Script "${script.name}" ${error.message}`); throw Error(`Script "${script.name}" ${error.message}`);
} }

View File

@@ -1,26 +1,32 @@
import { Script } from '@/domain/Script';
import { ScriptData } from 'js-yaml-loader!@/*'; import { ScriptData } from 'js-yaml-loader!@/*';
import { parseDocUrls } from '../DocumentationParser'; 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 { createEnumParser, IEnumParser } from '../../Common/Enum'; import { createEnumParser, IEnumParser } from '../../Common/Enum';
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext'; import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
export function parseScript( export function parseScript(
data: ScriptData, context: ICategoryCollectionParseContext, data: ScriptData,
levelParser = createEnumParser(RecommendationLevel)): Script { context: ICategoryCollectionParseContext,
levelParser = createEnumParser(RecommendationLevel),
): Script {
validateScript(data); validateScript(data);
if (!context) { throw new Error('undefined context'); } if (!context) { throw new Error('undefined context'); }
const script = new Script( const script = new Script(
/* name */ data.name, /* name: */ data.name,
/* code */ parseCode(data, context), /* code: */ parseCode(data, context),
/* docs */ parseDocUrls(data), /* docs: */ parseDocUrls(data),
/* level */ parseLevel(data.recommend, levelParser)); /* level: */ parseLevel(data.recommend, levelParser),
);
return script; return script;
} }
function parseLevel(level: string, parser: IEnumParser<RecommendationLevel>): RecommendationLevel | undefined { function parseLevel(
level: string,
parser: IEnumParser<RecommendationLevel>,
): RecommendationLevel | undefined {
if (!level) { if (!level) {
return undefined; return undefined;
} }

View File

@@ -1,10 +1,10 @@
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/domain/ScriptCode';
const BatchFileCommonCodeParts = ['(', ')', 'else', '||']; const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
const PowerShellCommonCodeParts = ['{', '}']; const PowerShellCommonCodeParts = ['{', '}'];
export class BatchFileSyntax implements ILanguageSyntax { export class BatchFileSyntax implements ILanguageSyntax {
public readonly commentDelimiters = ['REM', '::']; public readonly commentDelimiters = ['REM', '::'];
public readonly commonCodeParts = [...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts]; public readonly commonCodeParts = [...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts];
} }

View File

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

View File

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

View File

@@ -5,7 +5,9 @@ import { BatchFileSyntax } from './BatchFileSyntax';
import { ShellScriptSyntax } from './ShellScriptSyntax'; import { ShellScriptSyntax } from './ShellScriptSyntax';
import { ISyntaxFactory } from './ISyntaxFactory'; import { ISyntaxFactory } from './ISyntaxFactory';
export class SyntaxFactory extends ScriptingLanguageFactory<ILanguageSyntax> implements ISyntaxFactory { export class SyntaxFactory
extends ScriptingLanguageFactory<ILanguageSyntax>
implements ISyntaxFactory {
constructor() { constructor() {
super(); super();
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax()); this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());

View File

@@ -3,9 +3,9 @@ import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compile
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser'; import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IProjectInformation } from '@/domain/IProjectInformation'; import { IProjectInformation } from '@/domain/IProjectInformation';
import { ICodeSubstituter } from './ICodeSubstituter';
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { ICodeSubstituter } from './ICodeSubstituter';
export class CodeSubstituter implements ICodeSubstituter { export class CodeSubstituter implements ICodeSubstituter {
constructor( constructor(
@@ -14,12 +14,13 @@ export class CodeSubstituter implements ICodeSubstituter {
) { ) {
} }
public substitute(code: string, info: IProjectInformation): string { public substitute(code: string, info: IProjectInformation): string {
if (!code) { throw new Error('undefined code'); } if (!code) { throw new Error('undefined code'); }
if (!info) { throw new Error('undefined info'); } if (!info) { throw new Error('undefined info'); }
const args = new FunctionCallArgumentCollection(); const args = new FunctionCallArgumentCollection();
const substitute = (name: string, value: string) => const substitute = (name: string, value: string) => args
args.addArgument(new FunctionCallArgument(name, value)); .addArgument(new FunctionCallArgument(name, value));
substitute('homepage', info.homepage); substitute('homepage', info.homepage);
substitute('version', info.version); substitute('version', info.version);
substitute('date', this.date.toUTCString()); substitute('date', this.date.toUTCString());

View File

@@ -1,5 +1,5 @@
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*'; import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ScriptingDefinition } from '@/domain/ScriptingDefinition'; import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IProjectInformation } from '@/domain/IProjectInformation'; import { IProjectInformation } from '@/domain/IProjectInformation';
@@ -13,9 +13,11 @@ export class ScriptingDefinitionParser {
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(), private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(),
) { ) {
} }
public parse( public parse(
definition: ScriptingDefinitionData, definition: ScriptingDefinitionData,
info: IProjectInformation): IScriptingDefinition { info: IProjectInformation,
): IScriptingDefinition {
if (!info) { throw new Error('undefined info'); } if (!info) { throw new Error('undefined info'); }
if (!definition) { throw new Error('undefined definition'); } if (!definition) { throw new Error('undefined definition'); }
const language = this.languageParser.parseEnum(definition.language, 'language'); const language = this.languageParser.parseEnum(definition.language, 'language');
@@ -28,4 +30,3 @@ export class ScriptingDefinitionParser {
); );
} }
} }

View File

@@ -955,7 +955,7 @@ actions:
sudo defaults write '/var/db/SystemPolicy-prefs' 'enabled' -string 'no' sudo defaults write '/var/db/SystemPolicy-prefs' 'enabled' -string 'no'
echo "Disabled Gatekeeper" echo "Disabled Gatekeeper"
else else
>&2 echo "Unknown gatekeeper status: $gatekeeper_status" >&2 echo "Unknown gatekeeper status: $gatekeeper_status"
fi fi
fi fi
revertCode: |- revertCode: |-
@@ -974,7 +974,7 @@ actions:
elif [ $gatekeeper_status = "enabled" ]; then elif [ $gatekeeper_status = "enabled" ]; then
echo "No action needed, Gatekeeper is already enabled" echo "No action needed, Gatekeeper is already enabled"
else else
>&2 echo "Unknown Gatekeeper status: $gatekeeper_status" >&2 echo "Unknown Gatekeeper status: $gatekeeper_status"
fi fi
fi fi
- -

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,10 @@ import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from './OperatingSystem';
export class Application implements IApplication { export class Application implements IApplication {
constructor(public info: IProjectInformation, public collections: readonly ICategoryCollection[]) { constructor(
public info: IProjectInformation,
public collections: readonly ICategoryCollection[],
) {
validateInformation(info); validateInformation(info);
validateCollections(collections); validateCollections(collections);
} }
@@ -37,8 +40,8 @@ function validateCollections(collections: readonly ICategoryCollection[]) {
const osList = collections.map((c) => c.os); const osList = collections.map((c) => c.os);
const duplicates = getDuplicates(osList); const duplicates = getDuplicates(osList);
if (duplicates.length > 0) { if (duplicates.length > 0) {
throw new Error('multiple collections with same os: ' + throw new Error(`multiple collections with same os: ${
duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "')); duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "')}`);
} }
} }

View File

@@ -10,7 +10,8 @@ export class Category extends BaseEntity<number> implements ICategory {
public readonly name: string, public readonly name: string,
public readonly documentationUrls: ReadonlyArray<string>, public readonly documentationUrls: ReadonlyArray<string>,
public readonly subCategories?: ReadonlyArray<ICategory>, public readonly subCategories?: ReadonlyArray<ICategory>,
public readonly scripts?: ReadonlyArray<IScript>) { public readonly scripts?: ReadonlyArray<IScript>,
) {
super(id); super(id);
validateCategory(this); validateCategory(this);
} }
@@ -20,7 +21,10 @@ export class Category extends BaseEntity<number> implements ICategory {
} }
public getAllScriptsRecursively(): readonly IScript[] { public getAllScriptsRecursively(): readonly IScript[] {
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this)); if (!this.allSubScripts) {
this.allSubScripts = parseScriptsRecursively(this);
}
return this.allSubScripts;
} }
} }
@@ -35,8 +39,10 @@ function validateCategory(category: ICategory) {
if (!category.name) { if (!category.name) {
throw new Error('undefined or empty name'); throw new Error('undefined or empty name');
} }
if ((!category.subCategories || category.subCategories.length === 0) && if (
(!category.scripts || category.scripts.length === 0)) { (!category.subCategories || category.subCategories.length === 0)
&& (!category.scripts || category.scripts.length === 0)
) {
throw new Error('A category must have at least one sub-category or script'); throw new Error('A category must have at least one sub-category or script');
} }
} }

View File

@@ -1,4 +1,4 @@
import { getEnumNames, getEnumValues, assertInRange } from '@/application/Common/Enum'; import { getEnumValues, assertInRange } from '@/application/Common/Enum';
import { IEntity } from '../infrastructure/Entity/IEntity'; import { IEntity } from '../infrastructure/Entity/IEntity';
import { ICategory } from './ICategory'; import { ICategory } from './ICategory';
import { IScript } from './IScript'; import { IScript } from './IScript';
@@ -9,6 +9,7 @@ import { ICategoryCollection } from './ICategoryCollection';
export class CategoryCollection implements ICategoryCollection { export class CategoryCollection implements ICategoryCollection {
public get totalScripts(): number { return this.queryable.allScripts.length; } public get totalScripts(): number { return this.queryable.allScripts.length; }
public get totalCategories(): number { return this.queryable.allCategories.length; } public get totalCategories(): number { return this.queryable.allCategories.length; }
private readonly queryable: IQueryableCollection; private readonly queryable: IQueryableCollection;
@@ -16,7 +17,8 @@ export class CategoryCollection implements ICategoryCollection {
constructor( constructor(
public readonly os: OperatingSystem, public readonly os: OperatingSystem,
public readonly actions: ReadonlyArray<ICategory>, public readonly actions: ReadonlyArray<ICategory>,
public readonly scripting: IScriptingDefinition) { public readonly scripting: IScriptingDefinition,
) {
if (!scripting) { if (!scripting) {
throw new Error('undefined scripting definition'); throw new Error('undefined scripting definition');
} }
@@ -32,7 +34,7 @@ export class CategoryCollection implements ICategoryCollection {
} }
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] { public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
if (isNaN(level)) { if (level === undefined) {
throw new Error('undefined level'); throw new Error('undefined level');
} }
if (!(level in RecommendationLevel)) { if (!(level in RecommendationLevel)) {
@@ -55,20 +57,17 @@ export class CategoryCollection implements ICategoryCollection {
} }
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) { function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
const totalOccurrencesById = new Map<TKey, number>(); const isUniqueInArray = (id: TKey, index: number, array: readonly TKey[]) => array
for (const entity of entities) { .findIndex((otherId) => otherId === id) !== index;
totalOccurrencesById.set(entity.id, (totalOccurrencesById.get(entity.id) || 0) + 1); const duplicatedIds = entities
} .map((entity) => entity.id)
const duplicatedIds = new Array<TKey>(); .filter((id, index, array) => !isUniqueInArray(id, index, array))
totalOccurrencesById.forEach((index, id) => { .filter(isUniqueInArray);
if (index > 1) {
duplicatedIds.push(id);
}
});
if (duplicatedIds.length > 0) { if (duplicatedIds.length > 0) {
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(','); const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
throw new Error( throw new Error(
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`); `Duplicate entities are detected with following id(s): ${duplicatedIdsText}`,
);
} }
} }
@@ -93,50 +92,42 @@ function ensureValidScripts(allScripts: readonly IScript[]) {
if (!allScripts || allScripts.length === 0) { if (!allScripts || allScripts.length === 0) {
throw new Error('must consist of at least one script'); throw new Error('must consist of at least one script');
} }
for (const level of getEnumValues(RecommendationLevel)) { const missingRecommendationLevels = getEnumValues(RecommendationLevel)
if (allScripts.every((script) => script.level !== level)) { .filter((level) => allScripts.every((script) => script.level !== level));
throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`); if (missingRecommendationLevels.length > 0) {
} throw new Error('none of the scripts are recommended as'
+ ` "${missingRecommendationLevels.map((level) => RecommendationLevel[level]).join(', "')}".`);
} }
} }
function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[], IScript[]] { function flattenApplication(
const allCategories = new Array<ICategory>(); categories: ReadonlyArray<ICategory>,
const allScripts = new Array<IScript>(); ): [ICategory[], IScript[]] {
flattenCategories(categories, allCategories, allScripts); const [subCategories, subScripts] = (categories || [])
// Parse children
.map((category) => flattenApplication(category.subCategories))
// Flatten results
.reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => {
return [ return [
allCategories, [...previousCategories, ...currentCategories],
allScripts, [...previousScripts, ...currentScripts],
];
}, [new Array<ICategory>(), new Array<IScript>()]);
return [
[
...(categories || []),
...subCategories,
],
[
...(categories || []).flatMap((category) => category.scripts || []),
...subScripts,
],
]; ];
} }
function flattenCategories(
categories: ReadonlyArray<ICategory>,
allCategories: ICategory[],
allScripts: IScript[]): IQueryableCollection {
if (!categories || categories.length === 0) {
return;
}
for (const category of categories) {
allCategories.push(category);
flattenScripts(category.scripts, allScripts);
flattenCategories(category.subCategories, allCategories, allScripts);
}
}
function flattenScripts(
scripts: ReadonlyArray<IScript>,
allScripts: IScript[]): IScript[] {
if (!scripts) {
return;
}
for (const script of scripts) {
allScripts.push(script);
}
}
function makeQueryable( function makeQueryable(
actions: ReadonlyArray<ICategory>): IQueryableCollection { actions: ReadonlyArray<ICategory>,
): IQueryableCollection {
const flattened = flattenApplication(actions); const flattened = flattenApplication(actions);
return { return {
allCategories: flattened[0], allCategories: flattened[0],
@@ -145,12 +136,18 @@ function makeQueryable(
}; };
} }
function groupByLevel(allScripts: readonly IScript[]): Map<RecommendationLevel, readonly IScript[]> { function groupByLevel(
const map = new Map<RecommendationLevel, readonly IScript[]>(); allScripts: readonly IScript[],
for (const levelName of getEnumNames(RecommendationLevel)) { ): Map<RecommendationLevel, readonly IScript[]> {
const level = RecommendationLevel[levelName]; return getEnumValues(RecommendationLevel)
const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level); .map((level) => ({
map.set(level, scripts); level,
} scripts: allScripts.filter(
(script) => script.level !== undefined && script.level <= level,
),
}))
.reduce((map, group) => {
map.set(group.level, group.scripts);
return map; return map;
}, new Map<RecommendationLevel, readonly IScript[]>());
} }

View File

@@ -1,4 +1,5 @@
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from './OperatingSystem';
export interface IProjectInformation { export interface IProjectInformation {
readonly name: string; readonly name: string;
readonly version: string; readonly version: string;

View File

@@ -1,9 +1,10 @@
import { assertInRange } from '@/application/Common/Enum';
import { IProjectInformation } from './IProjectInformation'; import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from './OperatingSystem';
import { assertInRange } from '@/application/Common/Enum';
export class ProjectInformation implements IProjectInformation { export class ProjectInformation implements IProjectInformation {
public readonly repositoryWebUrl: string; public readonly repositoryWebUrl: string;
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly version: string, public readonly version: string,
@@ -24,12 +25,15 @@ export class ProjectInformation implements IProjectInformation {
} }
this.repositoryWebUrl = getWebUrl(this.repositoryUrl); this.repositoryWebUrl = getWebUrl(this.repositoryUrl);
} }
public getDownloadUrl(os: OperatingSystem): string { public getDownloadUrl(os: OperatingSystem): string {
return `${this.repositoryWebUrl}/releases/download/${this.version}/${getFileName(os, this.version)}`; return `${this.repositoryWebUrl}/releases/download/${this.version}/${getFileName(os, this.version)}`;
} }
public get feedbackUrl(): string { public get feedbackUrl(): string {
return `${this.repositoryWebUrl}/issues`; return `${this.repositoryWebUrl}/issues`;
} }
public get releaseUrl(): string { public get releaseUrl(): string {
return `${this.repositoryWebUrl}/releases/tag/${this.version}`; return `${this.repositoryWebUrl}/releases/tag/${this.version}`;
} }

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