Compare commits

..

1 Commits

Author SHA1 Message Date
undergroundwires
0c8412c467 Improve disabling of Windows services
- Unify way of disabling Windows services using templating.
- Remove false-positive error messages.
- Use smarter logic to start/stop service.

Logic improvements include:
  - Check if service is running before stopping/starting the service.
  - Do not start the service it's not an Automatic service.
  - Do not write stderr if service cannot be stopped/started as it's not
    not the main goal of the function.
  - Check whether service is already disabled.

Add more documentation about the disabled Windows service.

Script: Disable diagnostics telemetry
 - Add missing revert script
 - Add more granular control for each service.
2021-11-21 20:58:44 +01:00
366 changed files with 31755 additions and 37563 deletions

View File

@@ -1,7 +0,0 @@
[*.{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

View File

@@ -1,115 +0,0 @@
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
View File

@@ -1,6 +0,0 @@
# 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

View File

@@ -1,4 +1,4 @@
name: release-git name: Bump & release
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,59 +0,0 @@
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: release-desktop name: Deploy desktop
on: on:
release: release:

View File

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

View File

@@ -1,4 +1,4 @@
name: quality-checks name: Quality checks
on: [ push, pull_request ] on: [ push, pull_request ]
@@ -8,13 +8,12 @@ jobs:
strategy: strategy:
matrix: matrix:
lint-command: lint-command:
- npm run lint:eslint - npm run lint:vue
- npm run lint:yaml - npm run lint:yaml
- npm run lint:md - npm run lint:md
- npm run lint:md:relative-urls - npm run lint:md:relative-urls
- npm run lint:md:consistency - npm run lint:md:consistency
os: [ macos, ubuntu, windows ] fail-fast: false # So it continues with other commands if one fails
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,9 +1,9 @@
name: integration-tests name: Test
on: on:
push: push:
pull_request: pull_request:
schedule: # To get notified about problems from third party dependencies schedule: # for integration tests
- cron: '0 0 * * 0' # at 00:00 on every Sunday - cron: '0 0 * * 0' # at 00:00 on every Sunday
jobs: jobs:
@@ -25,6 +25,9 @@ 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

View File

@@ -1,28 +0,0 @@
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,28 +0,0 @@
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,6 +4,3 @@ dist/
.vscode .vscode
#Electron-builder output #Electron-builder output
/dist_electron /dist_electron
# Cypress
/tests/e2e/screenshots
/tests/e2e/videos

View File

@@ -1,20 +1,5 @@
# 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,104 +2,21 @@
> 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 🍑🍆
<!-- markdownlint-disable MD033 --> [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](./CONTRIBUTING.md)
<p align="center"> [![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)
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md"> [![Maintainability](https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability)](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
<img [![Tests status](https://github.com/undergroundwires/privacy.sexy/workflows/Test/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
alt="contributions are welcome" [![Quality checks status](https://github.com/undergroundwires/privacy.sexy/workflows/Quality%20checks/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat" [![Security checks status](https://github.com/undergroundwires/privacy.sexy/workflows/Security%20checks/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
/> [![Bump & release status](https://github.com/undergroundwires/privacy.sexy/workflows/Bump%20&%20release/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
</a> [![Deploy status](https://github.com/undergroundwires/privacy.sexy/workflows/Build%20&%20deploy/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
<!-- Code quality --> [![Auto-versioned by bump-everywhere](https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true)](https://github.com/undergroundwires/bump-everywhere)
<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.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). - 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).
- 💡 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.
@@ -132,9 +49,6 @@
- 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`
@@ -143,8 +57,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.2 .` 1. Build: `docker build -t 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` 2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.1 undergroundwires/privacy.sexy:0.11.1`
## Architecture overview ## Architecture overview
@@ -170,6 +84,5 @@
- 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'
], ]
}; }

View File

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

View File

@@ -1,19 +0,0 @@
# 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,59 +3,41 @@
- 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: 255 KiB

After

Width:  |  Height:  |  Size: 579 KiB

37031
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.2", "version": "0.11.1",
"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,18 +8,17 @@
"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:e2e": "vue-cli-service test:e2e", "test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\"",
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml", "lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
"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:eslint": "vue-cli-service lint --no-fix --mode production", "lint:vue": "vue-cli-service lint --no-fix",
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml", "lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps", "postuninstall": "electron-builder install-app-deps"
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\""
}, },
"main": "background.js", "main": "background.js",
"dependencies": { "dependencies": {
@@ -48,27 +47,16 @@
"@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",
"@typescript-eslint/eslint-plugin": "^5.4.0", "@vue/cli-plugin-babel": "^4.5.14",
"@typescript-eslint/parser": "^5.4.0", "@vue/cli-plugin-typescript": "^4.5.14",
"@vue/cli-plugin-babel": "~5.0.0-rc.1", "@vue/cli-plugin-unit-mocha": "^4.5.14",
"@vue/cli-plugin-e2e-cypress": "~5.0.0-rc.1", "@vue/cli-service": "^4.5.14",
"@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",
@@ -78,16 +66,12 @@
"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,16 +8,13 @@ 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,25 +1,20 @@
// 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> export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
= { [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>, enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<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>, enumVariable: EnumVariable<T, TEnumValue>): TEnumValue {
): TEnumValue {
if (!value) { if (!value) {
throw new Error(`undefined ${enumName}`); throw new Error(`undefined ${enumName}`);
} }
@@ -34,26 +29,22 @@ 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 export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
<T extends EnumType, TEnumValue extends EnumType>( enumVariable: EnumVariable<T, TEnumValue>): string[] {
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>, enumVariable: EnumVariable<T, TEnumValue>): 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 { assertInRange } from '@/application/Common/Enum';
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory'; import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
import { assertInRange } from '@/application/Common/Enum';
type Getter<T> = () => T; type Getter<T> = () => T;
@@ -27,4 +27,5 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
} }
this.getters.set(language, getter); this.getters.set(language, getter);
} }
} }

View File

@@ -1,19 +1,17 @@
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 {
@@ -21,11 +19,9 @@ 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,16 +1,15 @@
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 { IApplication } from '@/domain/IApplication';
import { Environment } from '../Environment/Environment'; import { Environment } from '../Environment/Environment';
import { IApplication } from '@/domain/IApplication';
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, environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
): 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,16 +1,12 @@
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 IReadOnlyApplicationContext { export interface IApplicationContext {
readonly app: IApplication; readonly app: IApplication;
readonly state: IReadOnlyCategoryCollectionState;
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
}
export interface IApplicationContext extends IReadOnlyApplicationContext {
readonly state: ICategoryCollectionState; readonly state: ICategoryCollectionState;
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
changeContext(os: OperatingSystem): void; changeContext(os: OperatingSystem): void;
} }

View File

@@ -1,5 +1,3 @@
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';
@@ -7,14 +5,13 @@ 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,26 +1,24 @@
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: IReadOnlyUserSelection, userSelection: IUserSelection,
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,15 +1,12 @@
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>;
@@ -17,8 +14,7 @@ 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());
@@ -42,19 +38,17 @@ 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;
const missingPositions = positions.filter((position) => position.endLine > totalLines); for (const position of positions) {
if (missingPositions.length > 0) { if (position.endLine > totalLines) {
throw new Error( throw new Error(`script end line (${position.endLine}) is out of range.` +
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"` `(total code lines: ${totalLines}`);
+ `(total code lines: ${totalLines}).`, }
);
} }
} }
function getChangedScripts( function getChangedScripts(
oldScripts: ReadonlyArray<SelectedScript>, oldScripts: ReadonlyArray<SelectedScript>,
newScripts: ReadonlyArray<SelectedScript>, newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
): 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 ))
@@ -63,8 +57,7 @@ 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,13 +17,14 @@ export abstract class CodeBuilder implements ICodeBuilder {
return this; return this;
} }
const lines = code.match(/[^\r\n]+/g); const lines = code.match(/[^\r\n]+/g);
this.lines.push(...lines); for (const line of lines) {
this.lines.push(line);
}
return this; return this;
} }
public appendTrailingHyphensCommentLine( public appendTrailingHyphensCommentLine(
totalRepeatHyphens: number = TotalFunctionSeparatorChars, totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
): CodeBuilder {
return this.appendCommentLine('-'.repeat(totalRepeatHyphens)); return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
} }
@@ -44,8 +45,7 @@ export abstract class CodeBuilder implements ICodeBuilder {
public appendCommentLineWithHyphensAround( public appendCommentLineWithHyphensAround(
sectionName: string, sectionName: string,
totalRepeatHyphens: number = TotalFunctionSeparatorChars, totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
): 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,6 +63,5 @@ 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,9 +5,7 @@ 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 export class CodeBuilderFactory extends ScriptingLanguageFactory<ICodeBuilder> implements ICodeBuilderFactory {
extends ScriptingLanguageFactory<ICodeBuilder>
implements ICodeBuilderFactory {
constructor() { constructor() {
super(); super();
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder()); this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());

View File

@@ -1,4 +1,5 @@
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
import { ICodeBuilder } from './ICodeBuilder'; import { ICodeBuilder } from './ICodeBuilder';
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
export type ICodeBuilderFactory = IScriptingLanguageFactory<ICodeBuilder>; export interface ICodeBuilderFactory extends 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 { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { IUserScript } from './IUserScript'; import { IUserScript } from './IUserScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
export interface IUserScriptGenerator { export interface IUserScriptGenerator {
buildCode( buildCode(

View File

@@ -4,7 +4,6 @@ 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,7 +4,6 @@ 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 { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { CodePosition } from '../Position/CodePosition';
import { IUserScriptGenerator } from './IUserScriptGenerator'; import { IUserScriptGenerator } from './IUserScriptGenerator';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { CodePosition } from '../Position/CodePosition';
import { IUserScript } from './IUserScript'; import { IUserScript } from './IUserScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
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,21 +12,20 @@ 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, scriptingDefinition: IScriptingDefinition): IUserScript {
): 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: new Map<SelectedScript, ICodePosition>() }; return { code: '', scriptPositions };
} }
let builder = this.codeBuilderFactory.create(scriptingDefinition.language); let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
builder = initializeCode(scriptingDefinition.startCode, builder); builder = initializeCode(scriptingDefinition.startCode, builder);
const scriptPositions = selectedScripts.reduce((result, selection) => { for (const selection of selectedScripts) {
return appendSelection(selection, result, builder); scriptPositions = appendSelection(selection, scriptPositions, builder);
}, new Map<SelectedScript, ICodePosition>()); }
const code = finalizeCode(builder, scriptingDefinition.endCode); const code = finalizeCode(builder, scriptingDefinition.endCode);
return { code, scriptPositions }; return { code, scriptPositions };
} }
@@ -53,11 +52,9 @@ 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, builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
): Map<SelectedScript, ICodePosition> { const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts
// Start from next line because first line will be empty to separate scripts builder = appendCode(selection, builder);
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);
@@ -66,9 +63,8 @@ function appendSelection(
} }
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder { function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
const { script } = selection; const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
const name = selection.revert ? `${script.name} (revert)` : script.name; const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
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 { IEventSource } from '@/infrastructure/Events/IEventSource';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { IEventSource } from '@/infrastructure/Events/IEventSource';
export interface IApplicationCode { export interface IApplicationCode {
readonly changed: IEventSource<ICodeChangedEvent>; readonly changed: IEventSource<ICodeChangedEvent>;

View File

@@ -7,8 +7,7 @@ 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,18 +1,16 @@
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,13 +1,10 @@
import { IEventSource } from '@/infrastructure/Events/IEventSource'; import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IFilterResult } from './IFilterResult'; import { IFilterResult } from './IFilterResult';
export interface IReadOnlyUserFilter { export interface IUserFilter {
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,15 +1,13 @@
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) {
@@ -22,11 +20,9 @@ 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,18 +1,13 @@
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 IReadOnlyCategoryCollectionState { export interface ICategoryCollectionState {
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,23 +1,20 @@
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 IReadOnlyUserSelection { export interface IUserSelection {
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,9 +30,7 @@ export class UserSelection implements IUserSelection {
if (this.selectedScripts.length < scripts.length) { if (this.selectedScripts.length < scripts.length) {
return false; return false;
} }
return scripts.every( return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id));
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
);
} }
public isAnySelected(category: ICategory): boolean { public isAnySelected(category: ICategory): boolean {
@@ -55,20 +53,19 @@ export class UserSelection implements IUserSelection {
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
public addOrUpdateAllInCategory(categoryId: number, revert = false): void { public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
const scriptsToAddOrUpdate = this.collection const category = this.collection.findCategory(categoryId);
.findCategory(categoryId) const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
.getAllScriptsRecursively() .filter((script) =>
.filter( !this.scripts.exists(script.id)
(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) {
this.scripts.addOrUpdateItem(script); const selectedScript = new SelectedScript(script, revert);
this.scripts.addOrUpdateItem(selectedScript);
} }
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
@@ -105,12 +102,11 @@ export class UserSelection implements IUserSelection {
} }
public selectAll(): void { public selectAll(): void {
const scriptsToSelect = this.collection for (const script of this.collection.getAllScripts()) {
.getAllScripts() if (!this.scripts.exists(script.id)) {
.filter((script) => !this.scripts.exists(script.id)) const selection = new SelectedScript(script, false);
.map((script) => new SelectedScript(script, false)); this.scripts.addItem(selection);
for (const script of scriptsToSelect) { }
this.scripts.addItem(script);
} }
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
@@ -135,11 +131,10 @@ 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 const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new SelectedScript(script, false));
for (const toSelect of unselectedScripts) { for (const toSelect of unselectedScripts) {
this.scripts.addItem(toSelect); const selection = new SelectedScript(toSelect, false);
this.scripts.addItem(selection);
} }
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }

View File

@@ -4,7 +4,6 @@ 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;
@@ -20,37 +19,35 @@ 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 [
.mustInclude('KAIOS')), define(OperatingSystem.KaiOS, (b) =>
define(OperatingSystem.ChromeOS, (b) => b b.mustInclude('KAIOS')),
.mustInclude('CrOS')), define(OperatingSystem.ChromeOS, (b) =>
define(OperatingSystem.BlackBerryOS, (b) => b b.mustInclude('CrOS')),
.mustInclude('BlackBerry')), define(OperatingSystem.BlackBerryOS, (b) =>
define(OperatingSystem.BlackBerryTabletOS, (b) => b b.mustInclude('BlackBerry')),
.mustInclude('RIM Tablet OS')), define(OperatingSystem.BlackBerryTabletOS, (b) =>
define(OperatingSystem.BlackBerry, (b) => b b.mustInclude('RIM Tablet OS')),
.mustInclude('BB10')), define(OperatingSystem.BlackBerry, (b) =>
define(OperatingSystem.Android, (b) => b b.mustInclude('BB10')),
.mustInclude('Android').mustNotInclude('Windows Phone')), define(OperatingSystem.Android, (b) =>
define(OperatingSystem.Android, (b) => b b.mustInclude('Android').mustNotInclude('Windows Phone')),
.mustInclude('Adr').mustNotInclude('Windows Phone')), define(OperatingSystem.Android, (b) =>
define(OperatingSystem.iOS, (b) => b b.mustInclude('Adr').mustNotInclude('Windows Phone')),
.mustInclude('like Mac OS X')), define(OperatingSystem.iOS, (b) =>
define(OperatingSystem.Linux, (b) => b b.mustInclude('like Mac OS X')),
.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')), define(OperatingSystem.Linux, (b) =>
define(OperatingSystem.Windows, (b) => b b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
.mustInclude('Windows').mustNotInclude('Windows Phone')), define(OperatingSystem.Windows, (b) =>
define(OperatingSystem.WindowsPhone, (b) => b b.mustInclude('Windows').mustNotInclude('Windows Phone')),
.mustInclude('Windows Phone')), define(OperatingSystem.WindowsPhone, (b) =>
define(OperatingSystem.macOS, (b) => b b.mustInclude('Windows Phone')),
.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')), define(OperatingSystem.macOS, (b) =>
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
]; ];
function define( function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
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,9 +1,8 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IBrowserOsDetector } from './IBrowserOsDetector'; import { IBrowserOsDetector } from './IBrowserOsDetector';
import { OperatingSystem } from '@/domain/OperatingSystem';
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';
export interface IEnvironmentVariables { 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,28 +12,21 @@ export 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: typeof process !== 'undefined' ? process /* electron only */ : undefined, process,
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);
if (this.isDesktop) { this.os = this.isDesktop ?
this.os = getDesktopOsType(getProcessPlatform(variables)); getDesktopOsType(getProcessPlatform(variables))
} else { : browserOsDetector.detect(getUserAgent(variables));
const userAgent = getUserAgent(variables);
this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent);
}
} }
} }
@@ -53,16 +46,14 @@ 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
switch (processPlatform) { if (processPlatform === 'darwin') {
case 'darwin':
return OperatingSystem.macOS; return OperatingSystem.macOS;
case 'win32': } else if (processPlatform === 'win32') {
return OperatingSystem.Windows; return OperatingSystem.Windows;
case 'linux': } else if (processPlatform === 'linux') {
return OperatingSystem.Linux; return OperatingSystem.Linux;
default:
return undefined;
} }
return undefined;
} }
function isDesktop(variables: IEnvironmentVariables): boolean { function isDesktop(variables: IEnvironmentVariables): boolean {

View File

@@ -1,18 +1,17 @@
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, collectionsData = PreParsedCollections): IApplication {
): 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));
@@ -23,13 +22,11 @@ export function parseApplication(
export type CategoryCollectionParserType export type CategoryCollectionParserType
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection; = (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => { const CategoryCollectionParser: CategoryCollectionParserType
return parseCategoryCollection(file, info); = (file, info) => 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,29 +1,32 @@
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), osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
): 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 = content.actions.map((action) => parseCategory(action, context)); const categories = new Array<Category>();
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 { import { CategoryData, ScriptData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
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 = 0; let categoryIdCounter: number = 0;
export function parseCategory( interface ICategoryChildren {
category: CategoryData, subCategories: Category[];
context: ICategoryCollectionParseContext, subScripts: Script[];
): 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(
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,17 +43,11 @@ 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);
@@ -67,20 +61,11 @@ function parseCategoryChild(
} }
} }
function isScript(data: CategoryOrScriptData): data is ScriptData { function isScript(data: any): boolean {
const holder = (data as InstructionHolder); return (data.code && data.code.length > 0)
return hasCode(holder) || hasCall(holder); || data.call;
} }
function isCategory(data: CategoryOrScriptData): data is CategoryData { function isCategory(data: any): boolean {
const { category } = data as CategoryData; return data.category && data.category.length > 0;
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; const docs = documentable.docs;
if (!docs || !docs.length) { if (!docs || !docs.length) {
return []; return [];
} }
@@ -13,10 +13,7 @@ export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<stri
return result.getAll(); return result.getAll();
} }
function addDocs( function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer {
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') {
@@ -35,7 +32,7 @@ class DocumentationUrlContainer {
this.urls.push(url); this.urls.push(url);
} }
public addUrls(urls: readonly string[]) { public addUrls(urls: readonly any[]) {
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');
@@ -56,8 +53,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 validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g; const res = docUrl.match(
const res = docUrl.match(validUrlRegex); /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g);
if (res == null) { if (res == null) {
throw new Error(`Invalid documentation url: ${docUrl}`); throw new Error(`Invalid documentation url: ${docUrl}`);
} }

View File

@@ -2,8 +2,7 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
import { ProjectInformation } from '@/domain/ProjectInformation'; import { ProjectInformation } from '@/domain/ProjectInformation';
export function parseProjectInformation( export function parseProjectInformation(
environment: NodeJS.ProcessEnv, environment: NodeJS.ProcessEnv): IProjectInformation {
): 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,14 +9,12 @@ 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,19 +1,18 @@
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection'; import { ExpressionPosition } from './ExpressionPosition';
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 { IExpression } from './IExpression'; import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
import { ExpressionPosition } from './ExpressionPosition'; import { ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
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 public readonly parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection()) {
: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(),
) {
if (!position) { if (!position) {
throw new Error('undefined position'); throw new Error('undefined position');
} }
@@ -21,15 +20,14 @@ 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);
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler); context = new ExpressionEvaluationContext(args, context.pipelineCompiler);
return this.evaluator(filteredContext); return this.evaluator(context);
} }
} }
@@ -45,19 +43,20 @@ 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, allFunctionArgs: IReadOnlyFunctionCallArgumentCollection): IReadOnlyFunctionCallArgumentCollection {
): IReadOnlyFunctionCallArgumentCollection {
const specificCallArgs = new FunctionCallArgumentCollection(); const specificCallArgs = new FunctionCallArgumentCollection();
parameters.all for (const parameter of parameters.all) {
.filter((parameter) => allFunctionArgs.hasArgument(parameter.name)) if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) {
.map((parameter) => allFunctionArgs.getArgument(parameter.name)) continue; // Optional parameter is not necessarily provided
.forEach((argument) => specificCallArgs.addArgument(argument)); }
const arg = allFunctionArgs.getArgument(parameter.name);
specificCallArgs.addArgument(arg);
}
return specificCallArgs; return specificCallArgs;
} }

View File

@@ -10,8 +10,7 @@ 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,8 +1,7 @@
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 { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
import { ExpressionPosition } from './ExpressionPosition'; import { ExpressionPosition } from './ExpressionPosition';
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext'; import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
export interface IExpression { export interface IExpression {

View File

@@ -1,25 +1,20 @@
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 | undefined, code: string,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection): string {
): 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);
@@ -31,8 +26,7 @@ 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
@@ -54,21 +48,20 @@ function compileExpressions(
} }
function extractRequiredParameterNames( function extractRequiredParameterNames(
expressions: readonly IExpression[], expressions: readonly IExpression[]): string[] {
): string[] { const usedParameterNames = expressions
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(Boolean) // Remove empty or undefined .filter((p) => p)
.flat() .flat();
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates const uniqueParameterNames = Array.from(new Set(usedParameterNames));
return uniqueParameterNames;
} }
function ensureParamsUsedInCodeHasArgsProvided( function ensureParamsUsedInCodeHasArgsProvided(
expressions: readonly IExpression[], expressions: readonly IExpression[],
providedArgs: IReadOnlyFunctionCallArgumentCollection, providedArgs: IReadOnlyFunctionCallArgumentCollection): void {
): 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 | undefined, code: string,
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,10 +14,14 @@ 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[] {
return this.leafs.flatMap( const expressions = new Array<IExpression>();
(parser) => parser.findExpressions(code) || [], for (const parser of this.leafs) {
); const newExpressions = parser.findExpressions(code);
if (newExpressions && newExpressions.length) {
expressions.push(...newExpressions);
}
}
return expressions;
} }
} }

View File

@@ -52,7 +52,6 @@ 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,45 +15,34 @@ 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> {
if (!code) { const matches = Array.from(code.matchAll(this.regex));
throw new Error('undefined code');
}
const matches = code.matchAll(this.regex);
for (const match of matches) { for (const match of matches) {
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 primitiveExpression = this.buildExpression(match); const primitiveExpression = this.buildExpression(match);
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code); const parameters = getParameters(primitiveExpression);
const parameters = createParameters(primitiveExpression);
const expression = new Expression(position, primitiveExpression.evaluator, parameters); const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression; yield expression;
} }
} }
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,10 +2,8 @@ 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:
@@ -13,18 +11,17 @@ 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,19 +2,18 @@ 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;
} }
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent" code = inlineComments(code);
inlineComments, code = mergeLinesWithBacktick(code);
mergeLinesWithBacktick, code = mergeHereStrings(code);
mergeHereStrings, const lines = getLines(code)
mergeNewLines, .map((line) => line.trim())
]).reduce((a, b) => (data) => b(a(data))); .filter((line) => line.length > 0);
const newCode = processor(code); return lines
return newCode; .join('; ');
} }
} }
@@ -113,18 +112,15 @@ 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 {
@@ -157,10 +153,3 @@ 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,7 +13,6 @@ 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');
@@ -22,7 +21,6 @@ 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)) {
@@ -30,7 +28,6 @@ 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));
return pipes.reduce((previousValue, pipe) => { for (const pipe of pipes) {
return pipe.apply(previousValue); value = pipe.apply(value);
}, value); }
return value;
} }
} }

View File

@@ -1,5 +1,5 @@
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser'; import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
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); const argumentValue = context.args.getArgument(parameterName).argumentValue;
if (!pipeline) { if (!pipeline) {
return argumentValue; return argumentValue;
} }

View File

@@ -1,5 +1,5 @@
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser'; import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
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,8 +51,7 @@ 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, // Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those
// 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,11 +1,10 @@
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
import { IFunctionCallArgument } from './IFunctionCallArgument'; import { IFunctionCallArgument } from './IFunctionCallArgument';
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
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,7 +3,6 @@ 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');
@@ -13,18 +12,15 @@ 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,27 +1,25 @@
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall'; import { ICompiledCode } from './ICompiledCode';
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 { IFunctionCallCompiler } from './IFunctionCallCompiler'; import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
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, functions: ISharedFunctionCollection): ICompiledCode {
): 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'); }
@@ -47,25 +45,24 @@ interface ICompiledFunctionCall {
} }
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall { function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
const compiledFunctions = context.callSequence const compiledFunctions = new Array<ICompiledFunctionCall>();
.flatMap((call) => compileSingleCall(call, context)); for (const call of context.callSequence) {
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( function compileSingleCall(call: IFunctionCall, context: ICompilationContext): ICompiledFunctionCall[] {
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);
@@ -74,12 +71,12 @@ function compileSingleCall(
}) })
.flat(); .flat();
} }
}
function compileCode( function compileCode(
code: IFunctionCode, code: IFunctionCode,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler, compiler: IExpressionsCompiler): ICompiledFunctionCall {
): 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),
@@ -91,17 +88,14 @@ function compileArgs(
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler, compiler: IExpressionsCompiler,
): IReadOnlyFunctionCallArgumentCollection { ): IReadOnlyFunctionCallArgumentCollection {
return argsToCompile const compiledArgs = new FunctionCallArgumentCollection();
.getAllParameterNames() for (const parameterName of argsToCompile.getAllParameterNames()) {
.map((parameterName) => { const argumentValue = argsToCompile.getArgument(parameterName).argumentValue;
const { argumentValue } = argsToCompile.getArgument(parameterName);
const compiledValue = compiler.compileExpressions(argumentValue, args); const compiledValue = compiler.compileExpressions(argumentValue, args);
return new FunctionCallArgument(parameterName, compiledValue); const newArgument = 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 {
@@ -112,8 +106,7 @@ function merge(codeParts: readonly string[]): string {
function ensureThatCallArgumentsExistInParameterDefinition( function ensureThatCallArgumentsExistInParameterDefinition(
func: ISharedFunction, func: ISharedFunction,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection): void {
): 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);
@@ -122,8 +115,7 @@ function ensureThatCallArgumentsExistInParameterDefinition(
function findUnexpectedParameters( function findUnexpectedParameters(
callArgumentNames: string[], callArgumentNames: string[],
functionParameterNames: string[], functionParameterNames: string[]): string[] {
): string[] {
if (!callArgumentNames.length && !functionParameterNames.length) { if (!callArgumentNames.length && !functionParameterNames.length) {
return []; return [];
} }
@@ -134,16 +126,14 @@ 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(
// eslint-disable-next-line prefer-template `Function "${functionName}" has unexpected parameter(s) provided: ` +
`Function "${functionName}" has unexpected parameter(s) provided: ` `"${unexpectedParameters.join('", "')}"` +
+ `"${unexpectedParameters.join('", "')}"` '. Expected parameter(s): ' +
+ '. Expected parameter(s): ' (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
+ (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,8 +4,7 @@ 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, FunctionCallParametersData } from 'js-yaml-loader!@/*'; import { FunctionCallData, FunctionCallsData } 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 === undefined) { if (!calls) {
throw new Error('undefined call data'); throw new Error('undefined call data');
} }
const sequence = getCallSequence(calls); const sequence = getCallSequence(calls);
@@ -24,19 +24,12 @@ 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 callArgs = parseArgs(call.parameters); const args = new FunctionCallArgumentCollection();
return new FunctionCall(call.function, callArgs); for (const parameterName of Object.keys(call.parameters || {})) {
} 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; }
}, new FunctionCallArgumentCollection()); return new FunctionCall(call.function, args);
} }

View File

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

View File

@@ -1,11 +1,10 @@
import { ensureValidParameterName } from '../Shared/ParameterNameValidator';
import { IFunctionParameter } from './IFunctionParameter'; import { IFunctionParameter } from './IFunctionParameter';
import { ensureValidParameterName } from '../Shared/ParameterNameValidator';
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,7 +7,6 @@ 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);
@@ -16,7 +15,6 @@ 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,14 +1,11 @@
import { IFunctionCall } from './Call/IFunctionCall'; import { IFunctionCall } from '../Function/Call/IFunctionCall';
import { import { FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody } from './ISharedFunction';
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[], callSequence: readonly IFunctionCall[]): ISharedFunction {
): ISharedFunction {
if (!callSequence) { if (!callSequence) {
throw new Error(`undefined call sequence in function "${name}"`); throw new Error(`undefined call sequence in function "${name}"`);
} }
@@ -22,8 +19,7 @@ export function createFunctionWithInlineCode(
name: string, name: string,
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
code: string, code: string,
revertCode?: string, revertCode?: string): ISharedFunction {
): ISharedFunction {
if (!code) { if (!code) {
throw new Error(`undefined code in function "${name}"`); throw new Error(`undefined code in function "${name}"`);
} }
@@ -36,7 +32,6 @@ 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,
@@ -44,7 +39,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,51 +11,47 @@ 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[], functions: readonly FunctionData[]): ISharedFunctionCollection {
): 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);
return functions for (const func of functions) {
.map((func) => parseFunction(func)) const sharedFunction = parseFunction(func);
.reduce((acc, func) => { collection.addFunction(sharedFunction);
acc.addFunction(func); }
return acc; return collection;
}, collection);
} }
} }
function parseFunction(data: FunctionData): ISharedFunction { function parseFunction(data: FunctionData): ISharedFunction {
const { name } = data; const name = data.name;
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 {
return (data.parameters || []) const parameters = new FunctionParameterCollection();
.map((parameter) => { if (!data.parameters) {
return parameters;
}
for (const parameterData of data.parameters) {
const isOptional = parameterData.optional || false;
try { try {
return new FunctionParameter( const parameter = new FunctionParameter(parameterData.name, isOptional);
parameter.name, parameters.addParameter(parameter);
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 {
@@ -100,7 +96,7 @@ function ensureExpectedParametersType(functions: readonly FunctionData[]) {
} }
} }
function isArrayOfObjects(value: unknown): boolean { function isArrayOfObjects(value: any): boolean {
return Array.isArray(value) return Array.isArray(value)
&& value.every((item) => typeof item === 'object'); && value.every((item) => typeof item === 'object');
} }
@@ -119,14 +115,15 @@ 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 { ScriptData } from 'js-yaml-loader!@/*';
import { IScriptCode } from '@/domain/IScriptCode'; import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptData } from 'js-yaml-loader!@/*';
export interface IScriptCompiler { export interface IScriptCompiler {
canCompile(script: ScriptData): boolean; canCompile(script: ScriptData): boolean;

View File

@@ -1,6 +1,7 @@
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
import { IScriptCode } from '@/domain/IScriptCode'; import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode'; import { ScriptCode } from '@/domain/ScriptCode';
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
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';
@@ -11,17 +12,15 @@ 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,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance, sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.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) {
@@ -29,7 +28,6 @@ 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 {
@@ -38,8 +36,7 @@ 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,32 +1,26 @@
import { ScriptData } from 'js-yaml-loader!@/*';
import { Script } from '@/domain/Script'; import { Script } from '@/domain/Script';
import { ScriptData } from 'js-yaml-loader!@/*';
import { parseDocUrls } from '../DocumentationParser';
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, data: ScriptData, context: ICategoryCollectionParseContext,
context: ICategoryCollectionParseContext, levelParser = createEnumParser(RecommendationLevel)): Script {
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( function parseLevel(level: string, parser: IEnumParser<RecommendationLevel>): RecommendationLevel | undefined {
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,4 +1,5 @@
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 type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>; export interface ISyntaxFactory extends IScriptingLanguageFactory<ILanguageSyntax> {
}

View File

@@ -2,6 +2,5 @@ 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,9 +5,7 @@ import { BatchFileSyntax } from './BatchFileSyntax';
import { ShellScriptSyntax } from './ShellScriptSyntax'; import { ShellScriptSyntax } from './ShellScriptSyntax';
import { ISyntaxFactory } from './ISyntaxFactory'; import { ISyntaxFactory } from './ISyntaxFactory';
export class SyntaxFactory export class SyntaxFactory extends ScriptingLanguageFactory<ILanguageSyntax> implements ISyntaxFactory {
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,13 +14,12 @@ 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) => args const substitute = (name: string, value: string) =>
.addArgument(new FunctionCallArgument(name, value)); args.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 { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
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,11 +13,9 @@ 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, info: IProjectInformation): IScriptingDefinition {
): 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');
@@ -30,3 +28,4 @@ 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,10 +4,7 @@ import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from './OperatingSystem';
export class Application implements IApplication { export class Application implements IApplication {
constructor( constructor(public info: IProjectInformation, public collections: readonly ICategoryCollection[]) {
public info: IProjectInformation,
public collections: readonly ICategoryCollection[],
) {
validateInformation(info); validateInformation(info);
validateCollections(collections); validateCollections(collections);
} }
@@ -40,8 +37,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,8 +10,7 @@ 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);
} }
@@ -21,10 +20,7 @@ export class Category extends BaseEntity<number> implements ICategory {
} }
public getAllScriptsRecursively(): readonly IScript[] { public getAllScriptsRecursively(): readonly IScript[] {
if (!this.allSubScripts) { return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this));
this.allSubScripts = parseScriptsRecursively(this);
}
return this.allSubScripts;
} }
} }
@@ -39,10 +35,8 @@ 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 ( if ((!category.subCategories || category.subCategories.length === 0) &&
(!category.subCategories || category.subCategories.length === 0) (!category.scripts || category.scripts.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 { getEnumValues, assertInRange } from '@/application/Common/Enum'; import { getEnumNames, 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,7 +9,6 @@ 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;
@@ -17,8 +16,7 @@ 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');
} }
@@ -34,7 +32,7 @@ export class CategoryCollection implements ICategoryCollection {
} }
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] { public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
if (level === undefined) { if (isNaN(level)) {
throw new Error('undefined level'); throw new Error('undefined level');
} }
if (!(level in RecommendationLevel)) { if (!(level in RecommendationLevel)) {
@@ -57,17 +55,20 @@ export class CategoryCollection implements ICategoryCollection {
} }
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) { function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
const isUniqueInArray = (id: TKey, index: number, array: readonly TKey[]) => array const totalOccurrencesById = new Map<TKey, number>();
.findIndex((otherId) => otherId === id) !== index; for (const entity of entities) {
const duplicatedIds = entities totalOccurrencesById.set(entity.id, (totalOccurrencesById.get(entity.id) || 0) + 1);
.map((entity) => entity.id) }
.filter((id, index, array) => !isUniqueInArray(id, index, array)) const duplicatedIds = new Array<TKey>();
.filter(isUniqueInArray); totalOccurrencesById.forEach((index, id) => {
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}`);
);
} }
} }
@@ -92,42 +93,50 @@ 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');
} }
const missingRecommendationLevels = getEnumValues(RecommendationLevel) for (const level of getEnumValues(RecommendationLevel)) {
.filter((level) => allScripts.every((script) => script.level !== level)); if (allScripts.every((script) => script.level !== level)) {
if (missingRecommendationLevels.length > 0) { throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`);
throw new Error('none of the scripts are recommended as' }
+ ` "${missingRecommendationLevels.map((level) => RecommendationLevel[level]).join(', "')}".`);
} }
} }
function flattenApplication( function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[], IScript[]] {
const allCategories = new Array<ICategory>();
const allScripts = new Array<IScript>();
flattenCategories(categories, allCategories, allScripts);
return [
allCategories,
allScripts,
];
}
function flattenCategories(
categories: ReadonlyArray<ICategory>, categories: ReadonlyArray<ICategory>,
): [ICategory[], IScript[]] { allCategories: ICategory[],
const [subCategories, subScripts] = (categories || []) allScripts: IScript[]): IQueryableCollection {
// Parse children if (!categories || categories.length === 0) {
.map((category) => flattenApplication(category.subCategories)) return;
// Flatten results }
.reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => { for (const category of categories) {
return [ allCategories.push(category);
[...previousCategories, ...currentCategories], flattenScripts(category.scripts, allScripts);
[...previousScripts, ...currentScripts], flattenCategories(category.subCategories, allCategories, allScripts);
]; }
}, [new Array<ICategory>(), new Array<IScript>()]); }
return [
[ function flattenScripts(
...(categories || []), scripts: ReadonlyArray<IScript>,
...subCategories, allScripts: IScript[]): IScript[] {
], if (!scripts) {
[ return;
...(categories || []).flatMap((category) => category.scripts || []), }
...subScripts, for (const script of scripts) {
], allScripts.push(script);
]; }
} }
function makeQueryable( function makeQueryable(
actions: ReadonlyArray<ICategory>, actions: ReadonlyArray<ICategory>): IQueryableCollection {
): IQueryableCollection {
const flattened = flattenApplication(actions); const flattened = flattenApplication(actions);
return { return {
allCategories: flattened[0], allCategories: flattened[0],
@@ -136,18 +145,12 @@ function makeQueryable(
}; };
} }
function groupByLevel( function groupByLevel(allScripts: readonly IScript[]): Map<RecommendationLevel, readonly IScript[]> {
allScripts: readonly IScript[], const map = new Map<RecommendationLevel, readonly IScript[]>();
): Map<RecommendationLevel, readonly IScript[]> { for (const levelName of getEnumNames(RecommendationLevel)) {
return getEnumValues(RecommendationLevel) const level = RecommendationLevel[levelName];
.map((level) => ({ const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level);
level, map.set(level, scripts);
scripts: allScripts.filter( }
(script) => script.level !== undefined && script.level <= level, return map;
),
}))
.reduce((map, group) => {
map.set(group.level, group.scripts);
return map;
}, new Map<RecommendationLevel, readonly IScript[]>());
} }

View File

@@ -1,5 +1,4 @@
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,10 +1,9 @@
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,
@@ -25,15 +24,12 @@ 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