Compare commits

..

11 Commits
0.9.1 ... 0.9.2

Author SHA1 Message Date
zy26
575636e6b7 correct the typo in application.md (#60) 2021-02-13 11:57:57 +01:00
undergroundwires
daa997b21b add GitHub issue templates 2021-02-10 21:16:56 +01:00
undergroundwires
5934b17283 refactor and add tests for NonCollapsingDirective 2021-02-09 08:53:29 +01:00
undergroundwires
d7de420d5c add test to ensure correct shared functions are being parsed 2021-02-08 07:10:41 +01:00
undergroundwires
df273f7f63 refactor state handling to make application available independent of the state 2021-02-07 12:32:05 +01:00
undergroundwires
67b2d1c11c refactor vscode configuration scripts using functions #41 2021-02-06 11:37:45 +01:00
undergroundwires
15353d0e25 make compiler throw if a function call includes an unexpected parameter 2021-02-05 13:27:40 +01:00
undergroundwires
f1e21babbf refactor event handling to consume base class for lifecycling 2021-02-04 19:58:09 +01:00
undergroundwires
34b8822ac8 fix wrong path for NvTelemtry file in NVIDIA script 2021-01-27 08:07:33 +01:00
undergroundwires
73e0520de7 do not compile with unused locals vuejs/vetur#1063 2021-01-26 06:00:19 +01:00
undergroundwires-bot
fbc3b109b9 ⬆️ bump everywhere to 0.9.1 2021-01-24 06:22:52 +00:00
61 changed files with 821 additions and 479 deletions

View File

@@ -0,0 +1,36 @@
---
name: Bug report (script bug or unexpected script behavior)
about: Create a bug report for generated scripts to help privacy.sexy improve
labels: bug
title: '[BUG]: '
---
<!--
Thank you for reporting an issue with generated script(s).
Please fill in as much of the template below as you're able.
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
-->
### Describe the bug
<!-- A clear and concise description of what the bug is. -->
### OS
<!--
Which OS are you using? What version of OS you were using?
On Windows you can find it using "Start button" > "Settings" > "System" > "About".
On macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
-->
### Screenshots
<!-- If applicable, add screenshots to help explain your problem. -->
### Scripts
<!-- Which scripts did you execute? If applicable, please paste the executed scripts or attach the generated privacy.sexy file . -->
### Additional information
<!-- Add any other context about the problem here. -->

View File

@@ -0,0 +1,52 @@
---
name: Bug report (unrelated to generated scripts)
about: Create a bug report that's not related to generated scripts to help privacy.sexy improve
labels: bug
title: '[BUG]: '
---
<!--
Thank you for reporting an issue.
Please fill in as much of the template below as you're able.
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
-->
### Describe the bug
<!-- A clear and concise description of what the bug is. -->
### To Reproduce
<!--
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
-->
### Expected behavior
<!--
A clear and concise description of what you expected to happen.
-->
### Screenshots
<!--
If applicable, add screenshots to help explain your problem.
-->
### Distribution
<!--
If applicable, mention how you were using privacy.sexy when the bug was encountered:
- Web (on Desktop or mobile?)
- Or desktop (Windows, macOS or Linux?)
-->
### Additional context
<!--
Add any other context about the problem here.
-->

View File

@@ -0,0 +1,27 @@
---
name: Feature request
about: Suggest an idea for privacy.sexy
labels: enhancement
---
<!--
Thank you for suggesting an idea to make privacy better. 🤗
Please fill in as much of the template below as you're able.
-->
### Problem Description
<!-- Please add a clear and concise description of the problem you are seeking to solve with this feature request. Ex. I'm always frustrated when [...] -->
### Proposed solution
<!-- Describe the solution you'd like in a clear and concise manner. -->
### Alternatives considered
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
### Additional information
<!-- Add any other context or screenshots about the feature request here. -->

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: true

View File

@@ -1,5 +1,17 @@
# Changelog # Changelog
## 0.9.1 (2021-01-23)
* in CI/CD, allow publishing to github if release is more than 2 hours old electron-userland/electron-builder#2074 | [cf907d0](https://github.com/undergroundwires/privacy.sexy/commit/cf907d029a6d80682ba78ec887a9c4fab639db51)
* in CI/CD, publish packages for other OSes if single one fails | [4015e2c](https://github.com/undergroundwires/privacy.sexy/commit/4015e2ccd8492e0693365b70fbfe3bd0ac7a6ea2)
* specify desktop publish targets as defaults (may) change | [2316e3f](https://github.com/undergroundwires/privacy.sexy/commit/2316e3fb6867e5d765eafcf675b77f88bd2a0f52)
* fix selection state indicator on cards not showing up | [8b0e47d](https://github.com/undergroundwires/privacy.sexy/commit/8b0e47da38c49cfe2645d7d25970c448ecd200f8)
* transpile using babel for legacy browser support | [7930bef](https://github.com/undergroundwires/privacy.sexy/commit/7930bef48c4e9a4fe0823673958ed8377f5ee533)
* fix node APIs no longer working on desktop nklayman/vue-cli-plugin-electron-builder#610, nklayman/vue-cli-plugin-electron-builder#742 | [d7f9ef1](https://github.com/undergroundwires/privacy.sexy/commit/d7f9ef1cbebe911aa19f29be8c5fa9360550793e)
* improve explanation for selections | [229c13a](https://github.com/undergroundwires/privacy.sexy/commit/229c13a195dee92e4a31731b7b41c319273a16f1)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.9.0...0.9.1)
## 0.9.0 (2021-01-15) ## 0.9.0 (2021-01-15)
* refactor application.yaml to become an os definition #40 | [f7557bc](https://github.com/undergroundwires/privacy.sexy/commit/f7557bcc0faf44e8395b68c7eb14c5f715f07b92) * refactor application.yaml to become an os definition #40 | [f7557bc](https://github.com/undergroundwires/privacy.sexy/commit/f7557bcc0faf44e8395b68c7eb14c5f715f07b92)

View File

@@ -23,17 +23,6 @@
- ❗ DON'T - ❗ DON'T
- Do not update the versions, current version is only [set by the maintainer](./img/architecture/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) - Do not update the versions, current version is only [set by the maintainer](./img/architecture/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
## Guidelines
### Handle the state in presentation layer
- There are two types of components:
- **Stateless**, extends `Vue`
- **Stateful**, extends [`StatefulVue`](./src/presentation/StatefulVue.ts)
- The source of truth for the state lies in application layer ([`./src/application/`](src/application/)) and must be updated from the views if they're mutating the state
- They mutate or/and react to state changes in [ApplicationContext](src/application/Context/ApplicationContext.ts).
- You can react by getting the state and listening to it and update the view accordingly in [`mounted()`](https://vuejs.org/v2/api/#mounted) method.
## License ## License
By contributing, you agree that your contributions will be licensed under its GNU General Public License v3.0. By contributing, you agree that your contributions will be licensed under its GNU General Public License v3.0.

View File

@@ -15,7 +15,7 @@
## Get started ## Get started
- Online version: [https://privacy.sexy](https://privacy.sexy) - Online version: [https://privacy.sexy](https://privacy.sexy)
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.0/privacy.sexy-Setup-0.9.0.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.0/privacy.sexy-0.9.0.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.0/privacy.sexy-0.9.0.dmg) - or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.1/privacy.sexy-Setup-0.9.1.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.1/privacy.sexy-0.9.1.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.1/privacy.sexy-0.9.1.dmg)
- 💡 Come back regularly to apply latest version for stronger privacy and security. - 💡 Come back regularly to apply latest version for stronger privacy and security.
[![privacy.sexy application](img/screenshot.png)](https://privacy.sexy) [![privacy.sexy application](img/screenshot.png)](https://privacy.sexy)
@@ -51,29 +51,17 @@
- 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.9.0 .` 1. Build: `docker build -t undergroundwires/privacy.sexy:0.9.1 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.9.0 undergroundwires/privacy.sexy:0.9.0` 2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.9.1 undergroundwires/privacy.sexy:0.9.1`
## Architecture ## Architecture overview
### Application ### Application
- Powered by **TypeScript**, **Vue.js** and **Electron** 💪 - Powered by **TypeScript**, **Vue.js** and **Electron** 💪
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts. - and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
- Application uses highly decoupled models & services in different DDD layers. - Application uses highly decoupled models & services in different DDD layers.
- **Domain layer** is where the application is modelled with validation logic. - 📖 Read more on • [Presentation](./docs/presentation.md) • [Application](./docs/application.md)
- **Presentation Layer**
- Consists of Vue.js components and other UI-related code.
- Desktop application is created using [Electron](https://www.electronjs.org/).
- Event driven as in components simply listens to events from the state and act accordingly.
- **Application Layer**
- Keeps the application state using [state pattern](https://en.wikipedia.org/wiki/State_pattern)
- [ApplicationContext](src/application/Context/ApplicationContext.ts)
- Holds the [CategoryCollectionState](src/application/Context/State/CategoryCollectionState.ts)] for each OS
- Same instance is shared throughout the application
- The scripts are defined and controlled in [yaml files](src/application/collections/) per OS
- Uses [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming)
- 📖 See [extend scripts](#extend-scripts) to read about how to extend them.
![DDD + vue.js](img/architecture/app-ddd.png) ![DDD + vue.js](img/architecture/app-ddd.png)

22
docs/application.md Normal file
View File

@@ -0,0 +1,22 @@
# Application
- It's mainly responsible for
- creating and event based [application state](#application-state)
- parsing and compiling [application data](#application-data)
## Application state
- [ApplicationContext.ts](./../src/application/Context/ApplicationContext.ts) holds the [CategoryCollectionState](./../src/application/Context/State/CategoryCollectionState.ts) for each OS
- Uses [state pattern](https://en.wikipedia.org/wiki/State_pattern)
- Same instance is shared throughout the application to ensure consistent state
- 📖 See [Application State | Presentation layer](./presentation.md#application-state) to read more about how the state should be managed by the presentation layer.
- 📖 See [ApplicationContext.ts](./../src/application/Context/ApplicationContext.ts) to start diving into the state code.
## Application data
- Compiled to `Application` domain object.
- The scripts are defined and controlled in different data files per OS
- Enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) and easier contributions
- Application data is defined in collection files and
- 📖 See [Application data | Presentation layer](./presentation.md#application-data) to read how the application data is read by the presentation layer.
- 📖 See [collection files documentation](./collection-files.md) to read more about how the data files are structured/defined and see [collection yaml files](./../src/application/collections/) to directly check the code.

24
docs/presentation.md Normal file
View File

@@ -0,0 +1,24 @@
# Presentation layer
- Consists of Vue.js components and other UI-related code.
- Desktop application is created using [Electron](https://www.electronjs.org/).
- Event driven as in components simply listens to events from the state and act accordingly.
## Application data
- Components and should use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain.
- [Application.ts](../src/domain/Application.ts) domain model is the stateless application representation including
- available scripts, collections as defined in [collection files](./collection-files.md)
- package information as defined in [`package.json`](./../package.json)
- 📖 See [Application data | Application layer](./presentation.md#application-data) where application data is parsed and compiled.
## Application state
- Stateful components mutate or/and react to state changes in [ApplicationContext](./../src/application/Context/ApplicationContext.ts).
- Stateless components that does not handle state extends `Vue`
- Stateful components that depends on the collection state such as user selection, search queries and more extends [`StatefulVue`](./../src/presentation/StatefulVue.ts)
- The single source of truth is a singleton of the state created and made available to presentation layer by [`StatefulVue`](./../src/presentation/StatefulVue.ts)
- `StatefulVue` includes abstract `handleCollectionState` that is fired once the component is loaded and also each time [collection](./collection-files.md) is changed.
- Do not forget to subscribe from events when component is destroyed or if needed [collection](./collection-files.md) is changed.
- 💡 `events` in base class [`StatefulVue`](./../src/presentation/StatefulVue.ts) makes lifecycling easier
- 📖 See [Application state | Application layer](./presentation.md#application-state) where the state is implemented using using state pattern.

View File

@@ -1 +1 @@
<mxfile host="www.draw.io" modified="2019-12-27T03:04:27.829Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" etag="O-1eaon4mqUmgvki0auB" version="12.4.3" pages="1"><diagram id="rhL8jzEM8kVVyiS98U7u" name="Page-1">3Zpdk6I4FIZ/jZdakPDlpa3tzlTNbnVVV83OzE1XhACZAUKF2Or8+k0wCBhau1f8GrpayUkC5HlzTg7IAE7T9V8M5fHfNMDJABjBegBnAwA8YIpPadhsDS50t4aIkWBrMmvDM/mNldFQ1iUJcNFqyClNOMnbRp9mGfZ5y4YYo6t2s5Am7bPmKMKa4dlHiW79lwQ8VsOyjdr+CZMors5sGqomRVVjZShiFNBVwwQfB3DKKOXbvXQ9xYlkV3HZ9pu/Ubu7MIYz/p4OD0/efJxMXmZfs6kd/vj2Txp+HlrVxfFNNWIcCACqSBmPaUQzlDzW1gdGl1mA5WENUarbfKE0F0ZTGH9izjdKTbTkVJhiniaqFq8J/ya7j2xV+t6oma3VkcvCpipknG0anWTxe7Ou7laWqn4FZ/QXntKEsnJ8cFxuomY7cjncN4kqU0GXzMcHMFYzE7EI8wPtwE534S+YplhcqejHcII4eW1fB1IzN9q1q8UVO0rfj2gNrqL1TreWarWI96AbPFG3suuEMbRpNMgpyXjROPKTNIgGKnZC5Zsqctp77n24NTDtvQmzPX09fXbjOGFGwatGj0bsMEbAfmf4uONpaF0zfKjjvqJkqc70xHAhxirOTTNtIrRlXsWE4+cclRhWIlNoSxqSJGkwDmzsBVYXfQ8soOPIHjTj1TyrrgwzjteHxdDhqQ6O23Yf5U2res2H1coeN9Z7xzgTbehc07XMlmu907PMtmeBm3AtcCHX+n8RftwO2iZsBe2j7YHrnj/IQ+9eUsSbmlCnpgwnRQ+gxepJnifE7yNUtwKvFrpDW/51ieGUmzpCw77ddiKdFMXtdhTf3Yw1wzjoCOPu2cK4fS/Oc9P3V/AeEiSoOd2Mpoj0mxotzCAIO7mbhgvH+Ayp0XjPqca6UwGrw6nsczmVpYH+nIUMCSJLny8Z7hV46PnY97uALzzbso0zAHf3clGnA7h3ySjmaMDxK1aZzRugzY+DDrHTDTpwxwvD6GeF8PbYujpbtwMtOBdaV0PLcE4Lwqk6+F3jhR0L8EXxenqoeMw44ffIdj8uXB3uWIPLY4ZRQLLoD8B79dBg6ll8Kn9e6TfsLpDvBbALLoCWZQf9wBXJSRuuYelw7ZHZgRecC6+er4ldEhy7R7pVxMA2R+Obg2xrkAuO+KEU7R18e6C15+xm9ZTjiqjOk2X1MbOc47DMrqej52Olp02o+YDDSGhE/NsDZ+pLymXBVWPoBjfaiNFfG9rtMdMXCg1SEaNc7pK0fJ+giUTCEHyTSUKiTNi4fIizs35BC5w8yYRfTlw4W1DOaSoaJLLiAfm/olKAVjokN9GkPNmkyLfvPcg0CFWFkKylZA/qemYx5/KFiYkkAeZ+kFkj4tMsJEJaNvLFGcFcrHxIfEm7iDlz4UK0GKIsGC6Y+JQmW6Ykc+i4L1+X+GfxIpuINMUb5VXG1+t9r60Jb1/yMQPQ16771x1+WPchCugC79QHnvfyjIqiR91NcGPC6yvx/QtvHhV+ka3Ep5B1Lf7FnlgYpFk+3Sp6Uhq4+zkqBCNdbQh1tStb/2rrucT9q20cVTukLMLZsMBU/rI/d8Stwjwn5fPTYkhzTlLyu0wKenR0WN0O7MTXpbc6pLc+LL0o1m/WbX8JrV9PhI//AQ==</diagram></mxfile> <mxfile host="Electron" modified="2021-01-31T12:32:01.751Z" agent="5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.1.8 Chrome/87.0.4280.88 Electron/11.1.1 Safari/537.36" etag="OTbSPW1ZOLwiPL6mt-j9" version="14.1.8" type="device"><diagram id="rhL8jzEM8kVVyiS98U7u" name="Page-1">3VtZd6JKF/01/dhZjA6PREjCXRbGFpM2L70QCYIoLsUI/PpvnwKcMD3cazrJl6wEqfHU3vvsKoz5Infm6e3KWU5ZPPGiL5IwSb/I+hdJUsUmflNBVhQoDaEo8FfBpCgS9wWDIPfKwqrZJph466OGSRxHSbA8LnTjxcJzk6MyZ7WKt8fNnuPoeNal43u1goHrRPXSx2CSTIvSlirsy++8wJ9WM4tCWTN3qsZlwXrqTOLtQZFsfJE7qzhOilfztONFhF2FS9Hv5pXaXWArb5H8Tofr+9ZNO9J+6A+Ljvr89N2aP5tflSq4JKtW7E0AQHkbr5Jp7McLJzL2pdereLOYeDSsgLt9m24cL1EoojD0kiQr2XQ2SYyiaTKPylovDZLv1P1KLe9GBzV6Wo7Mb7LqZpGssoNOdDs6rNt343dVv3WyimdeJ47iFV+f3OZfqKkDWGK6jjcr1/sJapUQnZXvJT9pJxXtCNGDCUp6br147iFSNFh5kZMEL8eSc0rl+rt2e3LxouT3T7iW3oXrHW9HrO1J/IC8yZfmjXfVVisnO2iwjINFsj4Y+Z4K0KD0TrnMzdI51ZP0/nlrSVRPBFNMv5fPbh3/QVHyu7rHgXcIV5L6m/bxeWSofCj7KMd9caJNOdP9yltj8Zg7XtSEcEzzdhok3mDpcFy2OCkcU/ocRNEBxhPVa02Uc+i3pLHcaFCPeJFUOjuzFVbBeqvES38KXlnbaB6nT5lN2/2eL1c7+/Rgv69OMxdHW268Z2qJR6n1m5klHmeW9B6pJb1Xav07h28fm7YoH5n2L9tLzebbm7zc+ixHxPcU1MWPDP/JPaSaV2vLZRS4l7DqI+OtWfezSt/nyGjwr3KEg/Li62ck/b6Lq8cuvnsYO7Rx6YyNN9/MxtXPkjwf6flK/pQHJLmWdHo8d4LLHo3G4mTyfBZ3UWjKbe8Njkbtk6Rq15NKUs4klfpWSaXUgDYXzysHiGzcZLPyLgr4c8v1XPcc4OOWqqjCGwDePDmLNs4A3vqbLtaoAe69eOXJ5hWgxT8H+tlrnAd60myPBeEyO0TrBNtmHdvmGWilt4K2WYN25S3jdZDE5eCfGl75zAb8V+Ft1a3CWCRB8hmxPfWFdwe3XQM3ma48ZxIs/P8DeN/dGsT6KX5Of165rO2OBU/2GufAFbyW0GpdBlwcTo7BFZQ6uOqVeAZe6a3grZ/X8DKY/OoZ6aNCLKniVfvDgazWQF4nTvKzI9qf42vo9H0ZFE9MQKze/XhHCP/K6euSGJ6+mXoORPHcu6lvh2H9mOUcviEiRLEfuJ8HULG+Nf1dQHdr2AOazamXG0eR5xKqH1efHw/N+lZUA289dZb0MpjzTywcQkUrh5QjLQr8BcoSeptoV9p1xl50T48UJHVZH8dJEs/RIKKKa8ed+ZyYowMXfaEJn0xbL4tPVtBBy6lunoOUqLwu49GnSUIfydAICenGnSyUq8CNF88BKF9duZhRusHe6uBC5VDHDZIuXn91FpOv4xV+U5FKh54budH88bDxwvUPaoKDUOtqWZ0pL/pk3aa/UpxyryhXap38fenl6a9vkp+ffvmP6f/qTOKxtxOB1Gr9GDjr9QXpl9TWVVP9PQlI5zUgvZkI6tv85xeB+EsRjBdb/AbFKX7wCtsyFdNbausLsS7LyhHlcuOqVT+QSO0zht9+K7Lr55HPT7bwS7Kf45XvLb6uvZg+TXDTAC83y4C/Z7v+Gi+TYB7k/ER2wZzfvUtSsS/XqK/0cUh9VfZfqH/Y3Hrj5vegr7fv2awhLwLh8cyf5n7J/CWIPS+Z8+Qe8V/RzamUteJWulm/gJzrlM58nfs7S3rKrpXxY7pxcyFw7r4Jrh6/dOWJPMlUmWXqizt3X1iobVmnnU/mbmDeTZZPd9/i+4GZWfYoMG+nkfM4iSe6ELBwKJnBteQ8Psj9eVtBm62paz4v103RCkzMfX/r+k/zaD1Gj/G8vXkamMV9R8wmj2l0P/gnmswfNmPp28wMldZIirKRlEbm7dNyfLttmwHL+uE/N0wwMLtFs6Qs9Om1unt9Z7bN0Mh6HfPlPkwXB33Vb7OH62/hLCjLEnfxsH6yi1i8+UM2zniUd9fTya3vPyFK2zawfkVkuulbubGxwijs2qbUBS7WQBC6oZtbgSBYgSJaobthoZl27SGVSyzQUqujCMweJqhPcfWZrW0se5Z17f6G5SMZ2KDtKLcGGq6mYOpMxb1gzY0MV8nKtOoKPA2fdajdKMOcVC4yiSVWpsiWbSbMZhjbxNgzxDFCHKZIYzEepylYmZAjnhx1PuJRmO6jD9aUz1LiqBsaQq8jpKjLMJZv6RjH9uWubaTd0M+tx3MxaRTTpmcPJawb8/Zlby4kWBvKZqqpu2jLtpbEUo4jxwBz2kOxWLshW4GWsUDJe3bfZ/kM8ZBmGOZkSq+j5bQ+FjJoiWL1wYO76el+jv4y4tiygaaijYR+GBu42po0sk1wxfEtr4hzoG27wIjpmkj4WzraZVgvjU9zow/TXQXrABY+8Qeeh4SJivi2TGcJHz830MYgvMCvoIBrzkcP2Fq8DeFPPDDU9WX0zxnVvcqpkHfDmQAN4DpUvYArdzv6/i02b/ttcyakPYo3NGl9W8xHvFPcWyg+QY5izQblGcadIXcFgc+n+xwP8CsWmhymLBBIu+grEB8CcEW8Ptqwck0zWgv0CDz1IepGGLt/Hs8O8BwoWyuc+Wifs9wKGdeARtio4C4j3Vvh0IfuVHALfg1gPEOsBtbaV0x9RPFluE8pZ4ABrn3oxkQe+FuKE30zWgt+gP1MfY1bU+8Tf9Ay8kDvS6RDi17nlAsjhWvtkdY7pHFIY4h1poInmluxOhrmdIUe8gXYiozj7UNrTCy4nAms0KqAssQKSQt96Jj0MiIfkICpwmwXGoWOwdPrsRJOrgz8ZCt/CLkubZYjZ0n3AmLJMJZq6W5i0Rj5CJgMEYNJmMiESY+0w/EeIUcpTkMs8sYAf8CNuAk06Arj5CbnGXy/ih/4Jx3JRR4b4hkdKtxrMi3t6S7laT7KMa+tCUUu+6RB4lXqwc8QP7h3eU739D7mIe0apPuUUUz6kDREeKo9e5T0qL3OuEYseFGXvIzy5/bVvCFNy/C3jPN2W3ohjYs1kkd0aT2hKUKLaW+gKD3ySfJpvQ//MEhjyG3kAvyD8XgE0qxI8XA/DYeUx7S2DJiCY7STXo0HOsB6uIZd1ZKXCTiBRkzu9z19BLwoj42kyEsT+qdYfLHUYIo4Ea+m0N6CeAXue1inRdjkM65dC+OAb8xtSow8PSM9ukmP+1c/ezVXqd4eCZQnaC+McoPvNSzTuIfhWuwPpF/eZkQ5grWT12jEPfj0aayUv87dpPBt4piwYrTPUR5R/iiv40T+oEHPtA/McuSpCK1k2DtoPInZ/4RYM+FAuSgSLvBZeBe45X7F84G4zfg+m1VtuYbLMu20n1/1IzzIO7G/kW/tvL+o2/Xf9ava0rqrGE777eYF5zx/4Iamfj0lbjAW4uvnZZ7Bi4wyV3gcx/WYAzGU9dcN2+7THqLynON7Vz8d5aSZ4UE8/dN4hAMcgAnXu0px4QwjHuC2679fB28LXWrJHhPe73DMPaaFXgn7Au+BdgbvPTZFjgAdySjXDB/RWbVu0jr5vQLfOq2vtFPUL+Kgmyst9/ZGcDrXM5wcLcxH5wHSqkjnLkv/Fv4Rd3WshDpW7BQr9V9j1flbWGEs7rczn2so9zOce6CxmVLstxo8XeBeWJwrNRWemvL9Jx8mHMP8aQo/ynCu86trNU9RT35J50BXIo/r2eRptNe78B/yf9pvyZ/orGls6ZxSxK3RtYxBK2OqYtCKmIr85vtOEdNTwwxadFLvtBf34fblCUroyniiyRV6hrrMnz8l+Uo9fuNJlurPoY0zz6GNP34Oxe3+v8qKTwHv/zVPNv4H</diagram></mxfile>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 29 KiB

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.9.0", "version": "0.9.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.9.0", "version": "0.9.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",

View File

@@ -0,0 +1,21 @@
import { IApplication } from '@/domain/IApplication';
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
import { IApplicationFactory } from './IApplicationFactory';
import { parseApplication } from './Parser/ApplicationParser';
export type ApplicationGetter = () => IApplication;
const ApplicationGetter: ApplicationGetter = parseApplication;
export class ApplicationFactory implements IApplicationFactory {
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
private readonly getter: AsyncLazy<IApplication>;
protected constructor(costlyGetter: ApplicationGetter) {
if (!costlyGetter) {
throw new Error('undefined getter');
}
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
}
public getAppAsync(): Promise<IApplication> {
return this.getter.getValueAsync();
}
}

View File

@@ -4,12 +4,12 @@ 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 { Signal } from '@/infrastructure/Events/Signal'; import { EventSource } from '@/infrastructure/Events/EventSource';
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>; type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
export class ApplicationContext implements IApplicationContext { export class ApplicationContext implements IApplicationContext {
public readonly contextChanged = new Signal<IApplicationContextChangedEvent>(); public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
public collection: ICategoryCollection; public collection: ICategoryCollection;
public currentOs: OperatingSystem; public currentOs: OperatingSystem;

View File

@@ -4,15 +4,15 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
import { Environment } from '../Environment/Environment'; import { Environment } from '../Environment/Environment';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { IEnvironment } from '../Environment/IEnvironment'; import { IEnvironment } from '../Environment/IEnvironment';
import { parseApplication } from '../Parser/ApplicationParser'; import { IApplicationFactory } from '../IApplicationFactory';
import { ApplicationFactory } from '../ApplicationFactory';
export type ApplicationParserType = () => IApplication; export async function buildContextAsync(
const ApplicationParser: ApplicationParserType = parseApplication; factory: IApplicationFactory = ApplicationFactory.Current,
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
export function buildContext( if (!factory) { throw new Error('undefined factory'); }
parser = ApplicationParser, if (!environment) { throw new Error('undefined environment'); }
environment = Environment.CurrentEnvironment): IApplicationContext { const app = await factory.getAppAsync();
const app = parser();
const os = getInitialOs(app, environment); const os = getInitialOs(app, environment);
return new ApplicationContext(app, os); return new ApplicationContext(app, os);
} }

View File

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

View File

@@ -4,13 +4,13 @@ import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection'; import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
import { UserScriptGenerator } from './Generation/UserScriptGenerator'; import { UserScriptGenerator } from './Generation/UserScriptGenerator';
import { Signal } from '@/infrastructure/Events/Signal'; 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'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
export class ApplicationCode implements IApplicationCode { export class ApplicationCode implements IApplicationCode {
public readonly changed = new Signal<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>();

View File

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

View File

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

View File

@@ -2,12 +2,12 @@ import { IScript } from '@/domain/IScript';
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 { Signal } from '@/infrastructure/Events/Signal'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
export class UserFilter implements IUserFilter { export class UserFilter implements IUserFilter {
public readonly filtered = new Signal<IFilterResult>(); public readonly filtered = new EventSource<IFilterResult>();
public readonly filterRemoved = new Signal<void>(); public readonly filterRemoved = new EventSource<void>();
public currentFilter: IFilterResult | undefined; public currentFilter: IFilterResult | undefined;
constructor(private collection: ICategoryCollection) { constructor(private collection: ICategoryCollection) {

View File

@@ -1,10 +1,10 @@
import { SelectedScript } from './SelectedScript'; 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 { ISignal } from '@/infrastructure/Events/ISignal'; import { IEventSource } from '@/infrastructure/Events/IEventSource';
export interface IUserSelection { export interface IUserSelection {
readonly changed: ISignal<ReadonlyArray<SelectedScript>>; readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>; readonly selectedScripts: ReadonlyArray<SelectedScript>;
readonly totalSelected: number; readonly totalSelected: number;
areAllSelected(category: ICategory): boolean; areAllSelected(category: ICategory): boolean;

View File

@@ -2,13 +2,13 @@ import { SelectedScript } from './SelectedScript';
import { IUserSelection } from './IUserSelection'; 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 { Signal } from '@/infrastructure/Events/Signal'; 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';
export class UserSelection implements IUserSelection { export class UserSelection implements IUserSelection {
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>(); public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: IRepository<string, SelectedScript>; private readonly scripts: IRepository<string, SelectedScript>;
constructor( constructor(

View File

@@ -0,0 +1,5 @@
import { IApplication } from '@/domain/IApplication';
export interface IApplicationFactory {
getAppAsync(): Promise<IApplication>;
}

View File

@@ -30,6 +30,7 @@ export class ScriptCompiler implements IScriptCompiler {
calls.forEach((currentCall, currentCallIndex) => { calls.forEach((currentCall, currentCallIndex) => {
ensureValidCall(currentCall, script.name); ensureValidCall(currentCall, script.name);
const commonFunction = this.getFunctionByName(currentCall.function); const commonFunction = this.getFunctionByName(currentCall.function);
ensureExpectedParameters(commonFunction, currentCall);
let functionCode = compileCode(commonFunction, currentCall.parameters); let functionCode = compileCode(commonFunction, currentCall.parameters);
if (currentCallIndex !== calls.length - 1) { if (currentCallIndex !== calls.length - 1) {
functionCode = appendLine(functionCode); functionCode = appendLine(functionCode);
@@ -57,6 +58,18 @@ export class ScriptCompiler implements IScriptCompiler {
} }
} }
function ensureExpectedParameters(func: FunctionData, call: FunctionCallData) {
if (!func.parameters && !call.parameters) {
return;
}
const unexpectedParameters = Object.keys(call.parameters || {})
.filter((callParam) => !func.parameters.includes(callParam));
if (unexpectedParameters.length) {
throw new Error(
`function "${func.name}" has unexpected parameter(s) provided: "${unexpectedParameters.join('", "')}"`);
}
}
function getDuplicates(texts: readonly string[]): string[] { function getDuplicates(texts: readonly string[]): string[] {
return texts.filter((item, index) => texts.indexOf(item) !== index); return texts.filter((item, index) => texts.indexOf(item) !== index);
} }

View File

@@ -1467,7 +1467,7 @@ actions:
name: Delete NVIDIA residual telemetry files name: Delete NVIDIA residual telemetry files
recommend: standard recommend: standard
code: |- code: |-
del /s %systemdrive%\System32\DriverStore\FileRepository\NvTelemetry*.dll del /s %SystemRoot%\System32\DriverStore\FileRepository\NvTelemetry*.dll
rmdir /s /q "%ProgramFiles(x86)%\NVIDIA Corporation\NvTelemetry" 2>nul rmdir /s /q "%ProgramFiles(x86)%\NVIDIA Corporation\NvTelemetry" 2>nul
rmdir /s /q "%ProgramFiles%\NVIDIA Corporation\NvTelemetry" 2>nul rmdir /s /q "%ProgramFiles%\NVIDIA Corporation\NvTelemetry" 2>nul
- -
@@ -1508,45 +1508,72 @@ actions:
name: Disable Visual Studio Code telemetry name: Disable Visual Studio Code telemetry
docs: https://code.visualstudio.com/docs/getstarted/telemetry docs: https://code.visualstudio.com/docs/getstarted/telemetry
recommend: standard recommend: standard
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'telemetry.enableTelemetry' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;" call:
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json; $json.PSObject.Properties.Remove('telemetry.enableTelemetry'); $json | ConvertTo-Json | Set-Content $jsonfile;" function: SetVsCodeSetting
parameters:
setting: telemetry.enableTelemetry
powerShellValue: $false
- -
name: Disable Visual Studio Code crash reporting name: Disable Visual Studio Code crash reporting
docs: https://code.visualstudio.com/docs/getstarted/telemetry docs: https://code.visualstudio.com/docs/getstarted/telemetry
recommend: standard recommend: standard
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'telemetry.enableCrashReporter' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;" call:
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('telemetry.enableCrashReporter'); $json | ConvertTo-Json | Set-Content $jsonfile;" function: SetVsCodeSetting
parameters:
setting: telemetry.enableCrashReporter
powerShellValue: $false
- -
name: Do not run Microsoft online experiments name: Do not run Microsoft online experiments
docs: https://github.com/Microsoft/vscode/blob/1aee0c194cff72d179b9f8ef324e47f34555a07d/src/vs/workbench/contrib/experiments/node/experimentService.ts#L173 docs: https://github.com/Microsoft/vscode/blob/1aee0c194cff72d179b9f8ef324e47f34555a07d/src/vs/workbench/contrib/experiments/node/experimentService.ts#L173
recommend: standard recommend: standard
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'workbench.enableExperiments' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;" call:
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('workbench.enableExperiments'); $json | ConvertTo-Json | Set-Content $jsonfile;" function: SetVsCodeSetting
parameters:
setting: workbench.enableExperiments
powerShellValue: $false
- -
name: Choose manual updates over automatic updates name: Choose manual updates over automatic updates
docs: https://github.com/Microsoft/vscode/blob/1aee0c194cff72d179b9f8ef324e47f34555a07d/src/vs/workbench/contrib/experiments/node/experimentService.ts#L173 call:
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'update.mode' -Value \"manual\" -Force; $json | ConvertTo-Json | Set-Content $jsonfile;" function: SetVsCodeSetting
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('update.mode'); $json | ConvertTo-Json | Set-Content $jsonfile;" parameters:
setting: update.mode
powerShellValue: >-
'manual'
- -
name: Show Release Notes from Microsoft online service after an update name: Show Release Notes from Microsoft online service after an update
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'update.showReleaseNotes' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;" call:
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('update.showReleaseNotes'); $json | ConvertTo-Json | Set-Content $jsonfile;" function: SetVsCodeSetting
parameters:
setting: update.showReleaseNotes
powerShellValue: $false
- -
name: Automatically check extensions from Microsoft online service name: Automatically check extensions from Microsoft online service
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'extensions.autoCheckUpdates' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;" call:
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('extensions.autoCheckUpdates'); $json | ConvertTo-Json | Set-Content $jsonfile;" function: SetVsCodeSetting
parameters:
setting: extensions.autoCheckUpdates
powerShellValue: $false
- -
name: Fetch recommendations from a Microsoft online service name: Fetch recommendations from Microsoft only on demand
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'extensions.showRecommendationsOnlyOnDemand' -Value $true -Force; $json | ConvertTo-Json | Set-Content $jsonfile;" call:
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('extensions.showRecommendationsOnlyOnDemand'); $json | ConvertTo-Json | Set-Content $jsonfile;" function: SetVsCodeSetting
parameters:
setting: extensions.showRecommendationsOnlyOnDemand
powerShellValue: $true
- -
name: Automatically fetch git commits from remote repository name: Automatically fetch git commits from remote repository
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'git.autofetch' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;" call:
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('git.autofetch'); $json | ConvertTo-Json | Set-Content $jsonfile;" function: SetVsCodeSetting
parameters:
setting: git.autofetch
powerShellValue: $false
- -
name: Fetch package information from NPM and Bower name: Fetch package information from NPM and Bower
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'npm.fetchOnlinePackageInfo' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;" call:
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('npm.fetchOnlinePackageInfo'); $json | ConvertTo-Json | Set-Content $jsonfile;" function: SetVsCodeSetting
parameters:
setting: npm.fetchOnlinePackageInfo
powerShellValue: $false
- -
category: Disable Microsoft Office telemetry category: Disable Microsoft Office telemetry
docs: https://docs.microsoft.com/en-us/deployoffice/compat/manage-the-privacy-of-data-monitored-by-telemetry-in-office docs: https://docs.microsoft.com/en-us/deployoffice/compat/manage-the-privacy-of-data-monitored-by-telemetry-in-office
@@ -4220,3 +4247,18 @@ functions:
) else ( ) else (
echo Could not find backup file "{{ $filePath }}.OLD" 1>&2 echo Could not find backup file "{{ $filePath }}.OLD" 1>&2
) )
-
name: SetVsCodeSetting
parameters: [ setting, powerShellValue ]
code:
Powershell -Command "
$jsonfile = \"$env:APPDATA\Code\User\settings.json\";
$json = Get-Content $jsonfile | Out-String | ConvertFrom-Json;
$json | Add-Member -Type NoteProperty -Name '{{ $setting }}' -Value {{ $powerShellValue }} -Force;
$json | ConvertTo-Json | Set-Content $jsonfile;"
revertCode:
Powershell -Command "
$jsonfile = \"$env:APPDATA\Code\User\settings.json\";
$json = Get-Content $jsonfile | ConvertFrom-Json;
$json.PSObject.Properties.Remove('{{ $setting }}');
$json | ConvertTo-Json | Set-Content $jsonfile;"

View File

@@ -1,7 +1,6 @@
import { EventHandler, ISignal } from './ISignal'; import { EventHandler, IEventSource, IEventSubscription } from './IEventSource';
import { IEventSubscription } from './ISubscription';
export class Signal<T> implements ISignal<T> { export class EventSource<T> implements IEventSource<T> {
private handlers = new Map<number, EventHandler<T>>(); private handlers = new Map<number, EventHandler<T>>();
public on(handler: EventHandler<T>): IEventSubscription { public on(handler: EventHandler<T>): IEventSubscription {

View File

@@ -0,0 +1,12 @@
import { IEventSubscription } from './IEventSource';
export class EventSubscriptionCollection {
private readonly subscriptions = new Array<IEventSubscription>();
public register(...subscriptions: IEventSubscription[]) {
this.subscriptions.push(...subscriptions);
}
public unsubscribeAll() {
this.subscriptions.forEach((listener) => listener.unsubscribe());
this.subscriptions.splice(0, this.subscriptions.length);
}
}

View File

@@ -1,6 +1,11 @@
import { IEventSubscription } from './ISubscription'; export interface IEventSource<T> {
export interface ISignal<T> {
on(handler: EventHandler<T>): IEventSubscription; on(handler: EventHandler<T>): IEventSubscription;
} }
export interface IEventSubscription {
unsubscribe(): void;
}
export type EventHandler<T> = (data: T) => void; export type EventHandler<T> = (data: T) => void;

View File

@@ -1,3 +0,0 @@
export interface IEventSubscription {
unsubscribe(): void;
}

View File

@@ -1,7 +1,7 @@
import { Signal } from '../Events/Signal'; import { EventSource } from '../Events/EventSource';
export class AsyncLazy<T> { export class AsyncLazy<T> {
private valueCreated = new Signal(); private valueCreated = new EventSource();
private isValueCreated = false; private isValueCreated = false;
private isCreatingValue = false; private isCreatingValue = false;
private value: T | undefined; private value: T | undefined;
@@ -15,7 +15,7 @@ export class AsyncLazy<T> {
public async getValueAsync(): Promise<T> { public async getValueAsync(): Promise<T> {
// If value is already created, return the value directly // If value is already created, return the value directly
if (this.isValueCreated) { if (this.isValueCreated) {
return Promise.resolve(this.value as T); return Promise.resolve(this.value);
} }
// If value is being created, wait until the value is created and then return it. // If value is being created, wait until the value is created and then return it.
if (this.isCreatingValue) { if (this.isCreatingValue) {

View File

@@ -80,29 +80,26 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import Code from './Code.vue'; import Code from './Code.vue';
import { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ApplicationFactory } from '@/application/ApplicationFactory';
@Component({ @Component({
components: { components: {
Code, Code,
}, },
}) })
export default class MacOsInstructions extends StatefulVue { export default class MacOsInstructions extends Vue {
@Prop() public fileName: string; @Prop() public fileName: string;
public appName = ''; public appName = '';
public macOsDownloadUrl = ''; public macOsDownloadUrl = '';
protected initialize(app: IApplication): void { public async created() {
const app = await ApplicationFactory.Current.getAppAsync();
this.appName = app.info.name; this.appName = app.info.name;
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS); this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
} }
protected handleCollectionState(): void {
return;
}
} }
</script> </script>

View File

@@ -35,9 +35,7 @@ import MacOsInstructions from './MacOsInstructions.vue';
import { Environment } from '@/application/Environment/Environment'; import { Environment } from '@/application/Environment/Environment';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IApplication } from '@/domain/IApplication';
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode'; import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
@@ -55,8 +53,6 @@ export default class TheCodeButtons extends StatefulVue {
public isMacOsCollection = false; public isMacOsCollection = false;
public fileName = ''; public fileName = '';
private codeListener: IEventSubscription;
public async copyCodeAsync() { public async copyCodeAsync() {
const code = await this.getCurrentCodeAsync(); const code = await this.getCurrentCodeAsync();
Clipboard.copyText(code.current); Clipboard.copyText(code.current);
@@ -68,15 +64,7 @@ export default class TheCodeButtons extends StatefulVue {
this.$modal.show(this.macOsModalName); this.$modal.show(this.macOsModalName);
} }
} }
public destroyed() {
if (this.codeListener) {
this.codeListener.unsubscribe();
}
}
protected initialize(app: IApplication): void {
return;
}
protected handleCollectionState(newState: ICategoryCollectionState): void { protected handleCollectionState(newState: ICategoryCollectionState): void {
this.isMacOsCollection = newState.collection.os === OperatingSystem.macOS; this.isMacOsCollection = newState.collection.os === OperatingSystem.macOS;
this.fileName = buildFileName(newState.collection.scripting); this.fileName = buildFileName(newState.collection.scripting);
@@ -90,12 +78,10 @@ export default class TheCodeButtons extends StatefulVue {
} }
private async react(code: IApplicationCode) { private async react(code: IApplicationCode) {
this.hasCode = code.current && code.current.length > 0; this.hasCode = code.current && code.current.length > 0;
if (this.codeListener) { this.events.unsubscribeAll();
this.codeListener.unsubscribe(); this.events.register(code.changed.on((newCode) => {
}
this.codeListener = code.changed.on((newCode) => {
this.hasCode = newCode && newCode.code.length > 0; this.hasCode = newCode && newCode.code.length > 0;
}); }));
} }
} }

View File

@@ -22,7 +22,6 @@ import { StatefulVue } from '@/presentation/StatefulVue';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { hasDirective } from './NonCollapsingDirective'; import { hasDirective } from './NonCollapsingDirective';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IApplication } from '@/domain/IApplication';
@Component({ @Component({
components: { components: {
@@ -45,9 +44,6 @@ export default class CardList extends StatefulVue {
this.activeCategoryId = isExpanded ? categoryId : undefined; this.activeCategoryId = isExpanded ? categoryId : undefined;
} }
protected initialize(app: IApplication): void {
return;
}
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void { protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
this.setCategories(newState.collection.actions); this.setCategories(newState.collection.actions);
this.activeCategoryId = undefined; this.activeCategoryId = undefined;

View File

@@ -35,7 +35,6 @@
import { Component, Prop, Watch, Emit } from 'vue-property-decorator'; import { Component, Prop, Watch, Emit } from 'vue-property-decorator';
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue'; import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
@Component({ @Component({
components: { components: {
@@ -50,17 +49,11 @@ export default class CardListItem extends StatefulVue {
public isAnyChildSelected = false; public isAnyChildSelected = false;
public areAllChildrenSelected = false; public areAllChildrenSelected = false;
private selectionChangedListener: IEventSubscription;
public async mounted() { public async mounted() {
this.updateStateAsync(this.categoryId);
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
this.selectionChangedListener = context.state.selection.changed.on(() => this.updateStateAsync(this.categoryId)); this.events.register(context.state.selection.changed.on(
} () => this.updateSelectionIndicatorsAsync(this.categoryId)));
public destroyed() { await this.updateStateAsync(this.categoryId);
if (this.selectionChangedListener) {
this.selectionChangedListener.unsubscribe();
}
} }
@Emit('selected') @Emit('selected')
public onSelected(isExpanded: boolean) { public onSelected(isExpanded: boolean) {
@@ -81,19 +74,22 @@ export default class CardListItem extends StatefulVue {
@Watch('categoryId') @Watch('categoryId')
public async updateStateAsync(value: |number) { public async updateStateAsync(value: |number) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
const category = !value ? undefined : context.state.collection.findCategory(this.categoryId); const category = !value ? undefined : context.state.collection.findCategory(value);
this.cardTitle = category ? category.name : undefined; this.cardTitle = category ? category.name : undefined;
const currentSelection = context.state.selection; await this.updateSelectionIndicatorsAsync(value);
this.isAnyChildSelected = category ? currentSelection.isAnySelected(category) : false;
this.areAllChildrenSelected = category ? currentSelection.areAllSelected(category) : false;
}
protected initialize(): void {
return;
} }
protected handleCollectionState(): void { protected handleCollectionState(): void {
// No need, as categoryId will be updated instead
return; return;
} }
private async updateSelectionIndicatorsAsync(categoryId: number) {
const context = await this.getCurrentContextAsync();
const selection = context.state.selection;
const category = context.state.collection.findCategory(categoryId);
this.isAnyChildSelected = category ? selection.isAnySelected(category) : false;
this.areAllChildrenSelected = category ? selection.areAllSelected(category) : false;
}
} }
</script> </script>

View File

@@ -1,6 +1,6 @@
import { DirectiveOptions } from 'vue'; import { DirectiveOptions } from 'vue';
const attributeName = 'data-interactionDoesNotCollapse'; const attributeName = 'data-interaction-does-not-collapse';
export function hasDirective(el: Element): boolean { export function hasDirective(el: Element): boolean {
if (el.hasAttribute(attributeName)) { if (el.hasAttribute(attributeName)) {

View File

@@ -27,8 +27,6 @@ import SelectableTree from './SelectableTree/SelectableTree.vue';
import { INode, NodeType } from './SelectableTree/Node/INode'; import { INode, NodeType } from './SelectableTree/Node/INode';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent'; import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
import { IApplication } from '@/domain/IApplication';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
@Component({ @Component({
components: { components: {
@@ -43,7 +41,6 @@ export default class ScriptsTree extends StatefulVue {
public filterText?: string = null; public filterText?: string = null;
private filtered?: IFilterResult; private filtered?: IFilterResult;
private listeners = new Array<IEventSubscription>();
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) { public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
@@ -75,31 +72,24 @@ export default class ScriptsTree extends StatefulVue {
|| this.filtered.categoryMatches.some( || this.filtered.categoryMatches.some(
(category: ICategory) => node.id === getCategoryNodeId(category)); (category: ICategory) => node.id === getCategoryNodeId(category));
} }
public destroyed() {
this.unsubscribeAll();
}
protected initialize(app: IApplication): void {
return;
}
protected async handleCollectionState(newState: ICategoryCollectionState) { protected async handleCollectionState(newState: ICategoryCollectionState) {
this.setCurrentFilter(newState.filter.currentFilter); this.setCurrentFilter(newState.filter.currentFilter);
if (!this.categoryId) { if (!this.categoryId) {
this.nodes = parseAllCategories(newState.collection); this.nodes = parseAllCategories(newState.collection);
} }
this.unsubscribeAll(); this.events.unsubscribeAll();
this.subscribe(newState); this.subscribeState(newState);
} }
private subscribe(state: ICategoryCollectionState) { private subscribeState(state: ICategoryCollectionState) {
this.listeners.push(state.selection.changed.on(this.handleSelectionChanged)); this.events.register(
this.listeners.push(state.filter.filterRemoved.on(this.handleFilterRemoved)); state.selection.changed.on(this.handleSelectionChanged),
this.listeners.push(state.filter.filtered.on(this.handleFiltered)); state.filter.filterRemoved.on(this.handleFilterRemoved),
} state.filter.filtered.on(this.handleFiltered),
private unsubscribeAll() { );
this.listeners.forEach((listener) => listener.unsubscribe());
this.listeners.splice(0, this.listeners.length);
} }
private setCurrentFilter(currentFilter: IFilterResult | undefined) { private setCurrentFilter(currentFilter: IFilterResult | undefined) {
if (!currentFilter) { if (!currentFilter) {
this.handleFilterRemoved(); this.handleFilterRemoved();

View File

@@ -13,49 +13,40 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Watch } from 'vue-property-decorator'; import { Component, Prop, Watch } from 'vue-property-decorator';
import { IReverter } from './Reverter/IReverter'; import { IReverter } from './Reverter/IReverter';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { INode } from './INode'; import { INode } from './INode';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { getReverter } from './Reverter/ReverterFactory'; import { getReverter } from './Reverter/ReverterFactory';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IApplication } from '@/domain/IApplication';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
@Component @Component
export default class RevertToggle extends StatefulVue { export default class RevertToggle extends StatefulVue {
@Prop() public node: INode; @Prop() public node: INode;
public isReverted = false; public isReverted = false;
private handler: IReverter; private handler: IReverter;
private selectionChangeListener: IEventSubscription;
@Watch('node', {immediate: true}) public async onNodeChangedAsync(node: INode) { @Watch('node', {immediate: true}) public async onNodeChangedAsync(node: INode) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
this.handler = getReverter(node, context.state.collection); this.handler = getReverter(node, context.state.collection);
}
public async onRevertToggledAsync() {
const context = await this.getCurrentContextAsync();
this.handler.selectWithRevertState(this.isReverted, context.state.selection);
}
protected initialize(app: IApplication): void {
return;
}
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
this.updateStatus(newState.selection.selectedScripts);
if (this.selectionChangeListener) {
this.selectionChangeListener.unsubscribe();
}
this.selectionChangeListener = newState.selection.changed.on(
(scripts) => this.updateStatus(scripts));
}
private updateStatus(scripts: ReadonlyArray<SelectedScript>) {
this.isReverted = this.handler.getState(scripts);
}
} }
public async onRevertToggledAsync() {
const context = await this.getCurrentContextAsync();
this.handler.selectWithRevertState(this.isReverted, context.state.selection);
}
protected handleCollectionState(newState: ICategoryCollectionState): void {
this.updateStatus(newState.selection.selectedScripts);
this.events.unsubscribeAll();
this.events.register(newState.selection.changed.on((scripts) => this.updateStatus(scripts)));
}
private updateStatus(scripts: ReadonlyArray<SelectedScript>) {
this.isReverted = this.handler.getState(scripts);
}
}
</script> </script>

View File

@@ -56,7 +56,6 @@ import { IScript } from '@/domain/IScript';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { RecommendationLevel } from '@/domain/RecommendationLevel'; import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IApplication } from '@/domain/IApplication';
enum SelectionState { enum SelectionState {
Standard, Standard,
@@ -82,9 +81,6 @@ export default class TheSelector extends StatefulVue {
selectType(context.state, type); selectType(context.state, type);
} }
protected initialize(app: IApplication): void {
return;
}
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void { protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
this.updateSelections(newState); this.updateSelections(newState);
newState.selection.changed.on(() => this.updateSelections(newState)); newState.selection.changed.on(() => this.updateSelections(newState));

View File

@@ -16,23 +16,24 @@ import { Component } from 'vue-property-decorator';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IApplication } from '@/domain/IApplication'; import { ApplicationFactory } from '@/application/ApplicationFactory';
@Component @Component
export default class TheOsChanger extends StatefulVue { export default class TheOsChanger extends StatefulVue {
public allOses: Array<{ name: string, os: OperatingSystem }> = []; public allOses: Array<{ name: string, os: OperatingSystem }> = [];
public currentOs: OperatingSystem = undefined; public currentOs: OperatingSystem = OperatingSystem.Unknown;
public async created() {
const app = await ApplicationFactory.Current.getAppAsync();
this.allOses = app.getSupportedOsList()
.map((os) => ({ os, name: renderOsName(os) }));
}
public async changeOsAsync(newOs: OperatingSystem) { public async changeOsAsync(newOs: OperatingSystem) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
context.changeContext(newOs); context.changeContext(newOs);
} }
protected initialize(app: IApplication): void { protected handleCollectionState(newState: ICategoryCollectionState): void {
this.allOses = app.getSupportedOsList()
.map((os) => ({ os, name: renderOsName(os) }));
}
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
this.currentOs = newState.os; this.currentOs = newState.os;
this.$forceUpdate(); // v-bind:class is not updated otherwise this.$forceUpdate(); // v-bind:class is not updated otherwise
} }

View File

@@ -49,8 +49,7 @@ import { StatefulVue } from '@/presentation/StatefulVue';
import { Grouping } from './Grouping/Grouping'; import { Grouping } from './Grouping/Grouping';
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IApplication } from '@/domain/IApplication'; import { ApplicationFactory } from '@/application/ApplicationFactory';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
/** Shows content of single category or many categories */ /** Shows content of single category or many categories */
@Component({ @Component({
@@ -79,10 +78,9 @@ export default class TheScripts extends StatefulVue {
public isSearching = false; public isSearching = false;
public searchHasMatches = false; public searchHasMatches = false;
private listeners = new Array<IEventSubscription>(); public async created() {
const app = await ApplicationFactory.Current.getAppAsync();
public destroyed() { this.repositoryUrl = app.info.repositoryWebUrl;
this.unsubscribeAll();
} }
public async clearSearchQueryAsync() { public async clearSearchQueryAsync() {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
@@ -93,27 +91,22 @@ export default class TheScripts extends StatefulVue {
this.currentGrouping = group; this.currentGrouping = group;
} }
protected initialize(app: IApplication): void {
this.repositoryUrl = app.info.repositoryWebUrl;
}
protected handleCollectionState(newState: ICategoryCollectionState): void { protected handleCollectionState(newState: ICategoryCollectionState): void {
this.unsubscribeAll(); this.events.unsubscribeAll();
this.subscribe(newState); this.subscribeState(newState);
} }
private subscribe(state: ICategoryCollectionState) { private subscribeState(state: ICategoryCollectionState) {
this.listeners.push(state.filter.filterRemoved.on(() => { this.events.register(
this.isSearching = false; state.filter.filterRemoved.on(() => {
})); this.isSearching = false;
state.filter.filtered.on((result: IFilterResult) => { }),
this.searchQuery = result.query; state.filter.filtered.on((result: IFilterResult) => {
this.isSearching = true; this.searchQuery = result.query;
this.searchHasMatches = result.hasAnyMatches(); this.isSearching = true;
}); this.searchHasMatches = result.hasAnyMatches();
} }),
private unsubscribeAll() { );
this.listeners.forEach((listener) => listener.unsubscribe());
this.listeners.splice(0, this.listeners.length);
} }
} }
</script> </script>

View File

@@ -1,33 +1,30 @@
import { Component, Vue } from 'vue-property-decorator'; import { Component, Vue } from 'vue-property-decorator';
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy'; import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
import { IApplicationContext } from '@/application/Context/IApplicationContext'; import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { buildContext } from '@/application/Context/ApplicationContextProvider'; import { buildContextAsync } from '@/application/Context/ApplicationContextFactory';
import { IApplicationContextChangedEvent } from '../application/Context/IApplicationContext'; import { IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext';
import { IApplication } from '@/domain/IApplication'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { ICategoryCollectionState } from '../application/Context/State/ICategoryCollectionState'; import { EventSubscriptionCollection } from '../infrastructure/Events/EventSubscriptionCollection';
import { IEventSubscription } from '../infrastructure/Events/ISubscription';
// @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91 // @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91
@Component @Component
export abstract class StatefulVue extends Vue { export abstract class StatefulVue extends Vue {
public static instance = new AsyncLazy<IApplicationContext>( private static readonly instance = new AsyncLazy<IApplicationContext>(() => buildContextAsync());
() => Promise.resolve(buildContext()));
private listener: IEventSubscription; protected readonly events = new EventSubscriptionCollection();
private readonly ownEvents = new EventSubscriptionCollection();
public async mounted() { public async mounted() {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
this.listener = context.contextChanged.on((event) => this.handleStateChangedEvent(event)); this.ownEvents.register(context.contextChanged.on((event) => this.handleStateChangedEvent(event)));
this.initialize(context.app);
this.handleCollectionState(context.state, undefined); this.handleCollectionState(context.state, undefined);
} }
public destroyed() { public destroyed() {
if (this.listener) { this.ownEvents.unsubscribeAll();
this.listener.unsubscribe(); this.events.unsubscribeAll();
}
} }
protected abstract initialize(app: IApplication): void;
protected abstract handleCollectionState( protected abstract handleCollectionState(
newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined): void; newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined): void;
protected getCurrentContextAsync(): Promise<IApplicationContext> { protected getCurrentContextAsync(): Promise<IApplicationContext> {

View File

@@ -11,9 +11,6 @@ import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeC
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IApplication } from '@/domain/IApplication';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory'; import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
@Component @Component
@@ -22,35 +19,22 @@ export default class TheCodeArea extends StatefulVue {
private editor!: ace.Ace.Editor; private editor!: ace.Ace.Editor;
private currentMarkerId?: number; private currentMarkerId?: number;
private codeListener: IEventSubscription;
@Prop() private theme!: string; @Prop() private theme!: string;
public destroyed() { public destroyed() {
this.unsubscribeCodeListening();
this.destroyEditor(); this.destroyEditor();
} }
protected initialize(app: IApplication): void {
return;
}
protected handleCollectionState(newState: ICategoryCollectionState): void { protected handleCollectionState(newState: ICategoryCollectionState): void {
this.destroyEditor(); this.destroyEditor();
this.editor = initializeEditor(this.theme, this.editorId, newState.collection.scripting.language); this.editor = initializeEditor(this.theme, this.editorId, newState.collection.scripting.language);
const appCode = newState.code; const appCode = newState.code;
this.editor.setValue(appCode.current || getDefaultCode(newState.collection.scripting.language), 1); this.editor.setValue(appCode.current || getDefaultCode(newState.collection.scripting.language), 1);
this.unsubscribeCodeListening(); this.events.unsubscribeAll();
this.subscribe(appCode); this.events.register(appCode.changed.on((code) => this.updateCodeAsync(code)));
} }
private subscribe(appCode: IApplicationCode) {
this.codeListener = appCode.changed.on((code) => this.updateCodeAsync(code));
}
private unsubscribeCodeListening() {
if (this.codeListener) {
this.codeListener.unsubscribe();
}
}
private async updateCodeAsync(event: ICodeChangedEvent) { private async updateCodeAsync(event: ICodeChangedEvent) {
this.removeCurrentHighlighting(); this.removeCurrentHighlighting();
if (event.isEmpty()) { if (event.isEmpty()) {

View File

@@ -9,15 +9,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Watch } from 'vue-property-decorator'; import { Component, Prop, Watch, Vue } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment'; import { Environment } from '@/application/Environment/Environment';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ApplicationFactory } from '@/application/ApplicationFactory';
import { IApplication } from '@/domain/IApplication';
@Component @Component
export default class DownloadUrlListItem extends StatefulVue { export default class DownloadUrlListItem extends Vue {
@Prop() public operatingSystem!: OperatingSystem; @Prop() public operatingSystem!: OperatingSystem;
public downloadUrl: string = ''; public downloadUrl: string = '';
@@ -38,16 +36,9 @@ export default class DownloadUrlListItem extends StatefulVue {
this.hasCurrentOsDesktopVersion = hasDesktopVersion(currentOs); this.hasCurrentOsDesktopVersion = hasDesktopVersion(currentOs);
} }
protected initialize(app: IApplication): void {
return;
}
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
return;
}
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> { private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
const context = await this.getCurrentContextAsync(); const context = await ApplicationFactory.Current.getAppAsync();
return context.app.info.getDownloadUrl(os); return context.info.getDownloadUrl(os);
} }
} }

View File

@@ -31,25 +31,27 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component } from 'vue-property-decorator'; import { Component, Vue } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment'; import { Environment } from '@/application/Environment/Environment';
import { ApplicationFactory } from '@/application/ApplicationFactory';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
@Component @Component
export default class PrivacyPolicy extends StatefulVue { export default class PrivacyPolicy extends Vue {
public repositoryUrl: string = ''; public repositoryUrl: string = '';
public feedbackUrl: string = ''; public feedbackUrl: string = '';
public isDesktop = Environment.CurrentEnvironment.isDesktop; public isDesktop = Environment.CurrentEnvironment.isDesktop;
protected initialize(app: IApplication): void { public async created() {
const app = await ApplicationFactory.Current.getAppAsync();
this.initialize(app);
}
private initialize(app: IApplication) {
const info = app.info; const info = app.info;
this.repositoryUrl = info.repositoryWebUrl; this.repositoryUrl = info.repositoryWebUrl;
this.feedbackUrl = info.feedbackUrl; this.feedbackUrl = info.feedbackUrl;
} }
protected handleCollectionState(): void {
return;
}
} }
</script> </script>

View File

@@ -47,22 +47,21 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component } from 'vue-property-decorator'; import { Component, Vue } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment'; import { Environment } from '@/application/Environment/Environment';
import PrivacyPolicy from './PrivacyPolicy.vue'; import PrivacyPolicy from './PrivacyPolicy.vue';
import DownloadUrlList from './DownloadUrlList.vue'; import DownloadUrlList from './DownloadUrlList.vue';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { ApplicationFactory } from '@/application/ApplicationFactory';
@Component({ @Component({
components: { components: {
PrivacyPolicy, DownloadUrlList, PrivacyPolicy, DownloadUrlList,
}, },
}) })
export default class TheFooter extends StatefulVue { export default class TheFooter extends Vue {
public readonly modalName = 'privacy-policy'; public readonly modalName = 'privacy-policy';
public readonly isDesktop: boolean; public readonly isDesktop = Environment.CurrentEnvironment.isDesktop;
public version: string = ''; public version: string = '';
public repositoryUrl: string = ''; public repositoryUrl: string = '';
@@ -70,12 +69,12 @@ export default class TheFooter extends StatefulVue {
public feedbackUrl: string = ''; public feedbackUrl: string = '';
public homepageUrl: string = ''; public homepageUrl: string = '';
constructor() { public async created() {
super(); const app = await ApplicationFactory.Current.getAppAsync();
this.isDesktop = Environment.CurrentEnvironment.isDesktop; this.initialize(app);
} }
protected initialize(app: IApplication): void { private initialize(app: IApplication) {
const info = app.info; const info = app.info;
this.version = info.version; this.version = info.version;
this.homepageUrl = info.homepage; this.homepageUrl = info.homepage;
@@ -83,10 +82,6 @@ export default class TheFooter extends StatefulVue {
this.releaseUrl = info.releaseUrl; this.releaseUrl = info.releaseUrl;
this.feedbackUrl = info.feedbackUrl; this.feedbackUrl = info.feedbackUrl;
} }
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
return;
}
} }
</script> </script>

View File

@@ -1,27 +1,23 @@
<template> <template>
<div id="container"> <div id="container">
<h1 class="child title" >{{ title }}</h1> <h1 class="child title" >{{ title }}</h1>
<h2 class="child subtitle">Enforce privacy & security on Windows and macOS</h2> <h2 class="child subtitle">Enforce privacy &amp; security on Windows and macOS</h2>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ApplicationFactory } from '@/application/ApplicationFactory';
import { IApplication } from '@/domain/IApplication'; import { Component, Vue } from 'vue-property-decorator';
import { Component } from 'vue-property-decorator';
import { StatefulVue } from './StatefulVue';
@Component @Component
export default class TheHeader extends StatefulVue { export default class TheHeader extends Vue {
public title = ''; public title = '';
public subtitle = ''; public subtitle = '';
protected initialize(app: IApplication): void { public async created() {
const app = await ApplicationFactory.Current.getAppAsync();
this.title = app.info.name; this.title = app.info.name;
} }
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
return;
}
} }
</script> </script>

View File

@@ -15,9 +15,7 @@ import { StatefulVue } from './StatefulVue';
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective'; import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter'; import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { IApplication } from '@/domain/IApplication';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
@Component( { @Component( {
directives: { NonCollapsing }, directives: { NonCollapsing },
@@ -27,8 +25,6 @@ export default class TheSearchBar extends StatefulVue {
public searchPlaceHolder = 'Search'; public searchPlaceHolder = 'Search';
public searchQuery = ''; public searchQuery = '';
private readonly listeners = new Array<IEventSubscription>();
@Watch('searchQuery') @Watch('searchQuery')
public async updateFilterAsync(newFilter: |string) { public async updateFilterAsync(newFilter: |string) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
@@ -39,28 +35,18 @@ export default class TheSearchBar extends StatefulVue {
filter.setFilter(newFilter); filter.setFilter(newFilter);
} }
} }
public destroyed() {
this.unsubscribeAll();
}
protected initialize(app: IApplication): void {
return;
}
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined) { protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined) {
const totalScripts = newState.collection.totalScripts; const totalScripts = newState.collection.totalScripts;
this.searchPlaceHolder = `Search in ${totalScripts} scripts`; this.searchPlaceHolder = `Search in ${totalScripts} scripts`;
this.searchQuery = newState.filter.currentFilter ? newState.filter.currentFilter.query : ''; this.searchQuery = newState.filter.currentFilter ? newState.filter.currentFilter.query : '';
this.unsubscribeAll(); this.events.unsubscribeAll();
this.subscribe(newState.filter); this.subscribeFilter(newState.filter);
} }
private subscribe(filter: IUserFilter) { private subscribeFilter(filter: IUserFilter) {
this.listeners.push(filter.filtered.on((result) => this.handleFiltered(result))); this.events.register(filter.filtered.on((result) => this.handleFiltered(result)));
this.listeners.push(filter.filterRemoved.on(() => this.handleFilterRemoved())); this.events.register(filter.filterRemoved.on(() => this.handleFilterRemoved()));
}
private unsubscribeAll() {
this.listeners.forEach((listener) => listener.unsubscribe());
this.listeners.splice(0, this.listeners.length);
} }
private handleFiltered(result: IFilterResult) { private handleFiltered(result: IFilterResult) {
this.searchQuery = result.query; this.searchQuery = result.query;

View File

@@ -0,0 +1,60 @@
import 'mocha';
import { expect } from 'chai';
import { ApplicationFactory, ApplicationGetter } from '@/application/ApplicationFactory';
import { ApplicationStub } from '../stubs/ApplicationStub';
describe('ApplicationFactory', () => {
describe('ctor', () => {
it('throws if getter is undefined', () => {
// arrange
const expectedError = 'undefined getter';
const getter = undefined;
// act
const act = () => new SystemUnderTest(getter);
// assert
expect(act).to.throw(expectedError);
});
});
describe('getAppAsync', () => {
it('returns result from the getter', async () => {
// arrange
const expected = new ApplicationStub();
const getter: ApplicationGetter = () => expected;
const sut = new SystemUnderTest(getter);
// act
const actual = await Promise.all( [
sut.getAppAsync(),
sut.getAppAsync(),
sut.getAppAsync(),
sut.getAppAsync(),
]);
// assert
expect(actual.every((value) => value === expected));
});
it('only executes getter once', async () => {
// arrange
let totalExecution = 0;
const expected = new ApplicationStub();
const getter: ApplicationGetter = () => {
totalExecution++;
return expected;
};
const sut = new SystemUnderTest(getter);
// act
await Promise.all( [
sut.getAppAsync(),
sut.getAppAsync(),
sut.getAppAsync(),
sut.getAppAsync(),
]);
// assert
expect(totalExecution).to.equal(1);
});
});
});
class SystemUnderTest extends ApplicationFactory {
public constructor(costlyGetter: ApplicationGetter) {
super(costlyGetter);
}
}

View File

@@ -0,0 +1,83 @@
import 'mocha';
import { expect } from 'chai';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub';
import { EnvironmentStub } from '../../stubs/EnvironmentStub';
import { ApplicationStub } from '../../stubs/ApplicationStub';
import { buildContextAsync } from '@/application/Context/ApplicationContextFactory';
import { IApplicationFactory } from '@/application/IApplicationFactory';
import { IApplication } from '@/domain/IApplication';
describe('ApplicationContextFactory', () => {
describe('buildContextAsync', () => {
describe('factory', () => {
it('sets application from factory', async () => {
// arrange
const expected = new ApplicationStub().withCollection(
new CategoryCollectionStub().withOs(OperatingSystem.macOS));
const factoryMock = mockFactoryWithApp(expected);
// act
const context = await buildContextAsync(factoryMock);
// assert
expect(expected).to.equal(context.app);
});
});
describe('environment', () => {
describe('sets initial OS as expected', () => {
it('returns currentOs if it is supported', async () => {
// arrange
const expected = OperatingSystem.Windows;
const environment = new EnvironmentStub().withOs(expected);
const collection = new CategoryCollectionStub().withOs(expected);
const factoryMock = mockFactoryWithCollection(collection);
// act
const context = await buildContextAsync(factoryMock, environment);
// assert
const actual = context.state.os;
expect(expected).to.equal(actual);
});
it('fallbacks to other os if OS in environment is not supported', async () => {
// arrange
const expected = OperatingSystem.Windows;
const currentOs = OperatingSystem.macOS;
const environment = new EnvironmentStub().withOs(currentOs);
const collection = new CategoryCollectionStub().withOs(expected);
const factoryMock = mockFactoryWithCollection(collection);
// act
const context = await buildContextAsync(factoryMock, environment);
// assert
const actual = context.state.os;
expect(expected).to.equal(actual);
});
it('fallbacks to most supported os if current os is not supported', async () => {
// arrange
const expectedOs = OperatingSystem.Android;
const allCollections = [
new CategoryCollectionStub().withOs(OperatingSystem.Linux).withTotalScripts(3),
new CategoryCollectionStub().withOs(expectedOs).withTotalScripts(5),
new CategoryCollectionStub().withOs(OperatingSystem.Windows).withTotalScripts(4),
];
const environment = new EnvironmentStub().withOs(OperatingSystem.macOS);
const app = new ApplicationStub().withCollections(...allCollections);
const factoryMock = mockFactoryWithApp(app);
// act
const context = await buildContextAsync(factoryMock, environment);
// assert
const actual = context.state.os;
expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`);
});
});
});
});
});
function mockFactoryWithCollection(result: ICategoryCollection): IApplicationFactory {
return mockFactoryWithApp(new ApplicationStub().withCollection(result));
}
function mockFactoryWithApp(app: IApplication): IApplicationFactory {
return {
getAppAsync: () => Promise.resolve(app),
};
}

View File

@@ -1,69 +0,0 @@
import 'mocha';
import { expect } from 'chai';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { ApplicationParserType, buildContext } from '@/application/Context/ApplicationContextProvider';
import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub';
import { EnvironmentStub } from '../../stubs/EnvironmentStub';
import { ApplicationStub } from '../../stubs/ApplicationStub';
describe('ApplicationContextProvider', () => {
describe('buildContext', () => {
it('sets application from parser', () => {
// arrange
const expected = new ApplicationStub().withCollection(
new CategoryCollectionStub().withOs(OperatingSystem.macOS));
const parserMock: ApplicationParserType = () => expected;
// act
const context = buildContext(parserMock);
// assert
expect(expected).to.equal(context.app);
});
describe('sets initial OS as expected', () => {
it('returns currentOs if it is supported', () => {
// arrange
const expected = OperatingSystem.Windows;
const environment = new EnvironmentStub().withOs(expected);
const parser = mockParser(new CategoryCollectionStub().withOs(expected));
// act
const context = buildContext(parser, environment);
// assert
const actual = context.state.os;
expect(expected).to.equal(actual);
});
it('fallbacks to other os if OS in environment is not supported', () => {
// arrange
const expected = OperatingSystem.Windows;
const currentOs = OperatingSystem.macOS;
const environment = new EnvironmentStub().withOs(currentOs);
const parser = mockParser(new CategoryCollectionStub().withOs(expected));
// act
const context = buildContext(parser, environment);
// assert
const actual = context.state.os;
expect(expected).to.equal(actual);
});
it('fallbacks to most supported os if current os is not supported', () => {
// arrange
const expectedOs = OperatingSystem.Android;
const allCollections = [
new CategoryCollectionStub().withOs(OperatingSystem.Linux).withTotalScripts(3),
new CategoryCollectionStub().withOs(expectedOs).withTotalScripts(5),
new CategoryCollectionStub().withOs(OperatingSystem.Windows).withTotalScripts(4),
];
const environment = new EnvironmentStub().withOs(OperatingSystem.macOS);
const app = new ApplicationStub().withCollections(...allCollections);
const parser: ApplicationParserType = () => app;
// act
const context = buildContext(parser, environment);
// assert
const actual = context.state.os;
expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`);
});
});
});
});
function mockParser(result: ICategoryCollection): ApplicationParserType {
return () => new ApplicationStub().withCollection(result);
}

View File

@@ -10,6 +10,10 @@ import { mockEnumParser } from '../../stubs/EnumParserStub';
import { ProjectInformationStub } from '../../stubs/ProjectInformationStub'; import { ProjectInformationStub } from '../../stubs/ProjectInformationStub';
import { getCategoryStub, CollectionDataStub } from '../../stubs/CollectionDataStub'; import { getCategoryStub, CollectionDataStub } from '../../stubs/CollectionDataStub';
import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub'; import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
import { CategoryDataStub } from '../../stubs/CategoryDataStub';
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
import { FunctionDataStub } from '../../stubs/FunctionDataStub';
import { RecommendationLevel } from '../../../../src/domain/RecommendationLevel';
describe('CategoryCollectionParser', () => { describe('CategoryCollectionParser', () => {
describe('parseCategoryCollection', () => { describe('parseCategoryCollection', () => {
@@ -93,5 +97,35 @@ describe('CategoryCollectionParser', () => {
expect(actual.os).to.equal(expectedOs); expect(actual.os).to.equal(expectedOs);
}); });
}); });
describe('functions', () => {
it('compiles script call with given function', () => {
// arrange
const expectedCode = 'code-from-the-function';
const functionName = 'function-name';
const scriptName = 'script-name';
const script = ScriptDataStub.createWithCall({ function: functionName })
.withName(scriptName);
const func = new FunctionDataStub()
.withName(functionName)
.withCode(expectedCode);
const category = new CategoryDataStub()
.withChildren([ script,
ScriptDataStub.createWithCode().withName('2')
.withRecommendationLevel(RecommendationLevel.Standard),
ScriptDataStub.createWithCode()
.withName('3').withRecommendationLevel(RecommendationLevel.Strict),
]);
const collection = new CollectionDataStub()
.withActions([ category ])
.withFunctions([ func ]);
const info = new ProjectInformationStub();
// act
const actual = parseCategoryCollection(collection, info);
// assert
const actualScript = actual.findScript(scriptName);
const actualCode = actualScript.code.execute;
expect(actualCode).to.equal(expectedCode);
});
});
}); });
}); });

View File

@@ -1,13 +1,13 @@
import 'mocha'; import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { parseCategory } from '@/application/Parser/CategoryParser'; import { parseCategory } from '@/application/Parser/CategoryParser';
import { CategoryData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
import { parseScript } from '@/application/Parser/Script/ScriptParser'; import { parseScript } from '@/application/Parser/Script/ScriptParser';
import { parseDocUrls } from '@/application/Parser/DocumentationParser'; import { parseDocUrls } from '@/application/Parser/DocumentationParser';
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub'; import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
import { ScriptDataStub } from '../../stubs/ScriptDataStub'; import { ScriptDataStub } from '../../stubs/ScriptDataStub';
import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub'; import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
import { LanguageSyntaxStub } from '../../stubs/LanguageSyntaxStub'; import { LanguageSyntaxStub } from '../../stubs/LanguageSyntaxStub';
import { CategoryDataStub } from '../../stubs/CategoryDataStub';
describe('CategoryParser', () => { describe('CategoryParser', () => {
describe('parseCategory', () => { describe('parseCategory', () => {
@@ -26,10 +26,9 @@ describe('CategoryParser', () => {
// arrange // arrange
const categoryName = 'test'; const categoryName = 'test';
const expectedMessage = `category has no children: "${categoryName}"`; const expectedMessage = `category has no children: "${categoryName}"`;
const category: CategoryData = { const category = new CategoryDataStub()
category: categoryName, .withName(categoryName)
children: [], .withChildren([]);
};
const context = new CategoryCollectionParseContextStub(); const context = new CategoryCollectionParseContextStub();
// act // act
const act = () => parseCategory(category, context); const act = () => parseCategory(category, context);
@@ -40,10 +39,9 @@ describe('CategoryParser', () => {
// arrange // arrange
const categoryName = 'test'; const categoryName = 'test';
const expectedMessage = `category has no children: "${categoryName}"`; const expectedMessage = `category has no children: "${categoryName}"`;
const category: CategoryData = { const category = new CategoryDataStub()
category: categoryName, .withName(categoryName)
children: undefined, .withChildren(undefined);
};
const context = new CategoryCollectionParseContextStub(); const context = new CategoryCollectionParseContextStub();
// act // act
const act = () => parseCategory(category, context); const act = () => parseCategory(category, context);
@@ -55,10 +53,8 @@ describe('CategoryParser', () => {
const expectedMessage = 'category has no name'; const expectedMessage = 'category has no name';
const invalidNames = ['', undefined]; const invalidNames = ['', undefined];
invalidNames.forEach((invalidName) => { invalidNames.forEach((invalidName) => {
const category: CategoryData = { const category = new CategoryDataStub()
category: invalidName, .withName(invalidName);
children: getTestChildren(),
};
const context = new CategoryCollectionParseContextStub(); const context = new CategoryCollectionParseContextStub();
// act // act
const act = () => parseCategory(category, context); const act = () => parseCategory(category, context);
@@ -71,7 +67,7 @@ describe('CategoryParser', () => {
// arrange // arrange
const expectedError = 'undefined context'; const expectedError = 'undefined context';
const context = undefined; const context = undefined;
const category = getValidCategory(); const category = new CategoryDataStub();
// act // act
const act = () => parseCategory(category, context); const act = () => parseCategory(category, context);
// assert // assert
@@ -81,11 +77,8 @@ describe('CategoryParser', () => {
// arrange // arrange
const url = 'https://privacy.sexy'; const url = 'https://privacy.sexy';
const expected = parseDocUrls({ docs: url }); const expected = parseDocUrls({ docs: url });
const category: CategoryData = { const category = new CategoryDataStub()
category: 'category name', .withDocs(url);
children: getTestChildren(),
docs: url,
};
const context = new CategoryCollectionParseContextStub(); const context = new CategoryCollectionParseContextStub();
// act // act
const actual = parseCategory(category, context).documentationUrls; const actual = parseCategory(category, context).documentationUrls;
@@ -98,10 +91,8 @@ describe('CategoryParser', () => {
const script = ScriptDataStub.createWithCode(); const script = ScriptDataStub.createWithCode();
const context = new CategoryCollectionParseContextStub(); const context = new CategoryCollectionParseContextStub();
const expected = [ parseScript(script, context) ]; const expected = [ parseScript(script, context) ];
const category: CategoryData = { const category = new CategoryDataStub()
category: 'category name', .withChildren([ script ]);
children: [ script ],
};
// act // act
const actual = parseCategory(category, context).scripts; const actual = parseCategory(category, context).scripts;
// assert // assert
@@ -115,10 +106,8 @@ describe('CategoryParser', () => {
const context = new CategoryCollectionParseContextStub() const context = new CategoryCollectionParseContextStub()
.withCompiler(compiler); .withCompiler(compiler);
const expected = [ parseScript(script, context) ]; const expected = [ parseScript(script, context) ];
const category: CategoryData = { const category = new CategoryDataStub()
category: 'category name', .withChildren([ script ]);
children: [ script ],
};
// act // act
const actual = parseCategory(category, context).scripts; const actual = parseCategory(category, context).scripts;
// assert // assert
@@ -128,10 +117,8 @@ describe('CategoryParser', () => {
// arrange // arrange
const callableScript = ScriptDataStub.createWithCall(); const callableScript = ScriptDataStub.createWithCall();
const scripts = [ callableScript, ScriptDataStub.createWithCode() ]; const scripts = [ callableScript, ScriptDataStub.createWithCode() ];
const category: CategoryData = { const category = new CategoryDataStub()
category: 'category name', .withChildren(scripts);
children: scripts,
};
const compiler = new ScriptCompilerStub() const compiler = new ScriptCompilerStub()
.withCompileAbility(callableScript); .withCompileAbility(callableScript);
const context = new CategoryCollectionParseContextStub() const context = new CategoryCollectionParseContextStub()
@@ -148,19 +135,16 @@ describe('CategoryParser', () => {
const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`; const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
const parseContext = new CategoryCollectionParseContextStub() const parseContext = new CategoryCollectionParseContextStub()
.withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter)); .withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter));
const category: CategoryData = { const category = new CategoryDataStub()
category: 'category name', .withChildren([
children: [ new CategoryDataStub()
{ .withName('sub-category')
category: 'sub-category', .withChildren([
children: [
ScriptDataStub ScriptDataStub
.createWithoutCallOrCodes() .createWithoutCallOrCodes()
.withCode(duplicatedCode), .withCode(duplicatedCode),
], ]),
}, ]);
],
};
// act // act
const act = () => parseCategory(category, parseContext).scripts; const act = () => parseCategory(category, parseContext).scripts;
// assert // assert
@@ -169,14 +153,13 @@ describe('CategoryParser', () => {
}); });
it('returns expected subcategories', () => { it('returns expected subcategories', () => {
// arrange // arrange
const expected: CategoryData[] = [ { const expected = [ new CategoryDataStub()
category: 'test category', .withName('test category')
children: [ ScriptDataStub.createWithCode() ], .withChildren([ ScriptDataStub.createWithCode() ]),
}]; ];
const category: CategoryData = { const category = new CategoryDataStub()
category: 'category name', .withName('category name')
children: expected, .withChildren(expected);
};
const context = new CategoryCollectionParseContextStub(); const context = new CategoryCollectionParseContextStub();
// act // act
const actual = parseCategory(category, context).subCategories; const actual = parseCategory(category, context).subCategories;
@@ -187,17 +170,3 @@ describe('CategoryParser', () => {
}); });
}); });
}); });
function getValidCategory(): CategoryData {
return {
category: 'category name',
children: getTestChildren(),
docs: undefined,
};
}
function getTestChildren(): ReadonlyArray<CategoryOrScriptData> {
return [
ScriptDataStub.createWithCode(),
];
}

View File

@@ -206,6 +206,26 @@ describe('ScriptCompiler', () => {
expect(act).to.throw(expectedError); expect(act).to.throw(expectedError);
}); });
}); });
it('throws if provided parameters does not match given ones', () => {
// arrange
const unexpectedParameterName = 'unexpected-parameter-name';
const functionName = 'test-function-name';
const expectedError = `function "${functionName}" has unexpected parameter(s) provided: "${unexpectedParameterName}"`;
const sut = new ScriptCompilerBuilder()
.withFunctions(
new FunctionDataStub()
.withName(functionName)
.withParameters('another-parameter'))
.build();
const params: FunctionCallParametersData = {};
params[unexpectedParameterName] = 'unexpected-parameter-value';
const call: ScriptFunctionCallData = { function: functionName, parameters: params };
const script = ScriptDataStub.createWithCall(call);
// act
const act = () => sut.compile(script);
// assert
expect(act).to.throw(expectedError);
});
}); });
describe('builds code as expected', () => { describe('builds code as expected', () => {
it('creates code with expected syntax', () => { // test through script validation logic it('creates code with expected syntax', () => { // test through script validation logic
@@ -304,7 +324,7 @@ describe('ScriptCompiler', () => {
expect(actual).to.deep.equal(expected); expect(actual).to.deep.equal(expected);
}); });
}); });
it('throws when parameters is undefined', () => { it('throws when parameters are undefined', () => {
// arrange // arrange
const env = new TestEnvironment({ const env = new TestEnvironment({
code: '{{ $parameter }} {{ $parameter }}!', code: '{{ $parameter }} {{ $parameter }}!',

View File

@@ -1,45 +1,43 @@
import { ISignal } from '@/infrastructure/Events/ISignal'; import { EventHandler, IEventSource, IEventSubscription } from '@/infrastructure/Events/IEventSource';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { Signal } from '@/infrastructure/Events/Signal';
import { expect } from 'chai'; import { expect } from 'chai';
import { EventHandler } from '@/infrastructure/Events/ISignal'; import 'mocha';
describe('EventSource', () => {
describe('Signal', () => {
class ObserverMock { class ObserverMock {
public readonly onReceiveCalls = new Array<number>(); public readonly onReceiveCalls = new Array<number>();
public readonly callbacks = new Array<EventHandler<number>>(); public readonly callbacks = new Array<EventHandler<number>>();
public readonly subscription: IEventSubscription; public readonly subscription: IEventSubscription;
constructor(subject: ISignal<number>) { constructor(subject: IEventSource<number>) {
this.callbacks.push((arg) => this.onReceiveCalls.push(arg)); this.callbacks.push((arg) => this.onReceiveCalls.push(arg));
this.subscription = subject.on((arg) => this.callbacks.forEach((action) => action(arg))); this.subscription = subject.on((arg) => this.callbacks.forEach((action) => action(arg)));
} }
} }
let signal: Signal<number>; let sut: EventSource<number>;
beforeEach(() => signal = new Signal()); beforeEach(() => sut = new EventSource());
describe('single observer', () => { describe('single observer', () => {
// arrange // arrange
let observer: ObserverMock; let observer: ObserverMock;
beforeEach(() => { beforeEach(() => {
observer = new ObserverMock(signal); observer = new ObserverMock(sut);
}); });
it('notify() executes the callback', () => { it('notify() executes the callback', () => {
// act // act
signal.notify(5); sut.notify(5);
// assert // assert
expect(observer.onReceiveCalls).to.have.length(1); expect(observer.onReceiveCalls).to.have.length(1);
}); });
it('notify() executes the callback with the payload', () => { it('notify() executes the callback with the payload', () => {
const expected = 5; const expected = 5;
// act // act
signal.notify(expected); sut.notify(expected);
// assert // assert
expect(observer.onReceiveCalls).to.deep.equal([expected]); expect(observer.onReceiveCalls).to.deep.equal([expected]);
}); });
it('notify() does not call callback when unsubscribed', () => { it('notify() does not call callback when unsubscribed', () => {
// act // act
observer.subscription.unsubscribe(); observer.subscription.unsubscribe();
signal.notify(5); sut.notify(5);
// assert // assert
expect(observer.onReceiveCalls).to.have.lengthOf(0); expect(observer.onReceiveCalls).to.have.lengthOf(0);
}); });
@@ -50,13 +48,13 @@ describe('Signal', () => {
let observers: ObserverMock[]; let observers: ObserverMock[];
beforeEach(() => { beforeEach(() => {
observers = [ observers = [
new ObserverMock(signal), new ObserverMock(signal), new ObserverMock(sut), new ObserverMock(sut),
new ObserverMock(signal), new ObserverMock(signal), new ObserverMock(sut), new ObserverMock(sut),
]; ];
}); });
it('notify() should execute all callbacks', () => { it('notify() should execute all callbacks', () => {
// act // act
signal.notify(5); sut.notify(5);
// assert // assert
observers.forEach((observer) => { observers.forEach((observer) => {
expect(observer.onReceiveCalls).to.have.length(1); expect(observer.onReceiveCalls).to.have.length(1);
@@ -65,7 +63,7 @@ describe('Signal', () => {
it('notify() should execute all callbacks with payload', () => { it('notify() should execute all callbacks with payload', () => {
const expected = 5; const expected = 5;
// act // act
signal.notify(expected); sut.notify(expected);
// assert // assert
observers.forEach((observer) => { observers.forEach((observer) => {
expect(observer.onReceiveCalls).to.deep.equal([expected]); expect(observer.onReceiveCalls).to.deep.equal([expected]);
@@ -79,7 +77,7 @@ describe('Signal', () => {
observers[i].callbacks.push(() => actualSequence.push(i)); observers[i].callbacks.push(() => actualSequence.push(i));
} }
// act // act
signal.notify(5); sut.notify(5);
// assert // assert
expect(actualSequence).to.deep.equal(expectedSequence); expect(actualSequence).to.deep.equal(expectedSequence);
}); });

View File

@@ -0,0 +1,22 @@
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
import { expect } from 'chai';
import 'mocha';
describe('EventSubscriptionCollection', () => {
it('unsubscribeAll unsubscribes from all registered subscriptions', () => {
// arrange
const sut = new EventSubscriptionCollection();
const expected = [ 'unsubscribed1', 'unsubscribed2'];
const actual = new Array<string>();
const subscriptions: IEventSubscription[] = [
{ unsubscribe: () => actual.push(expected[0]) },
{ unsubscribe: () => actual.push(expected[1]) },
];
// act
sut.register(...subscriptions);
sut.unsubscribeAll();
// assert
expect(actual).to.deep.equal(expected);
});
});

View File

@@ -0,0 +1,54 @@
import 'mocha';
import { expect } from 'chai';
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
import { hasDirective } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
const expectedAttributeName = 'data-interaction-does-not-collapse';
describe('NonCollapsingDirective', () => {
describe('NonCollapsing', () => {
it('adds expected attribute to the element when inserted', () => {
// arrange
const element = getElementMock();
// act
NonCollapsing.inserted(element, undefined, undefined, undefined);
// assert
expect(element.hasAttribute(expectedAttributeName));
});
});
describe('hasDirective', () => {
it('returns true if the element has expected attribute', () => {
// arrange
const element = getElementMock();
element.setAttribute(expectedAttributeName, undefined);
// act
const actual = hasDirective(element);
// assert
expect(actual).to.equal(true);
});
it('returns true if the element has a parent with expected attribute', () => {
// arrange
const parent = getElementMock();
const element = getElementMock();
parent.appendChild(element);
element.setAttribute(expectedAttributeName, undefined);
// act
const actual = hasDirective(element);
// assert
expect(actual).to.equal(true);
});
it('returns false if nor the element or its parent has expected attribute', () => {
// arrange
const element = getElementMock();
// act
const actual = hasDirective(element);
// assert
expect(actual).to.equal(false);
});
});
});
function getElementMock(): HTMLElement {
const element = document.createElement('div');
return element;
}

View File

@@ -0,0 +1,21 @@
import { CategoryData, CategoryOrScriptData, DocumentationUrlsData } from 'js-yaml-loader!@/*';
import { ScriptDataStub } from './ScriptDataStub';
export class CategoryDataStub implements CategoryData {
public children: readonly CategoryOrScriptData[] = [ ScriptDataStub.createWithCode() ];
public category = 'category name';
public docs?: DocumentationUrlsData;
public withChildren(children: readonly CategoryOrScriptData[]) {
this.children = children;
return this;
}
public withName(name: string) {
this.category = name;
return this;
}
public withDocs(docs: DocumentationUrlsData) {
this.docs = docs;
return this;
}
}

View File

@@ -1,26 +1,29 @@
import { RecommendationLevel } from '@/domain/RecommendationLevel'; import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { CategoryData, ScriptData, CollectionData, ScriptingDefinitionData } from 'js-yaml-loader!@/*'; import { CategoryData, ScriptData, CollectionData, ScriptingDefinitionData, FunctionData } from 'js-yaml-loader!@/*';
export class CollectionDataStub implements CollectionData { export class CollectionDataStub implements CollectionData {
public os = 'windows'; public os = 'windows';
public actions: readonly CategoryData[] = [ getCategoryStub() ]; public actions: readonly CategoryData[] = [ getCategoryStub() ];
public scripting: ScriptingDefinitionData = getTestDefinitionStub(); public scripting: ScriptingDefinitionData = getTestDefinitionStub();
public functions?: ReadonlyArray<FunctionData>;
public withActions(actions: readonly CategoryData[]): CollectionDataStub { public withActions(actions: readonly CategoryData[]): CollectionDataStub {
this.actions = actions; this.actions = actions;
return this; return this;
} }
public withOs(os: string): CollectionDataStub { public withOs(os: string): CollectionDataStub {
this.os = os; this.os = os;
return this; return this;
} }
public withScripting(scripting: ScriptingDefinitionData): CollectionDataStub { public withScripting(scripting: ScriptingDefinitionData): CollectionDataStub {
this.scripting = scripting; this.scripting = scripting;
return this; return this;
} }
public withFunctions(functions: ReadonlyArray<FunctionData>) {
this.functions = functions;
return this;
}
} }
export function getCategoryStub(scriptPrefix = 'testScript'): CategoryData { export function getCategoryStub(scriptPrefix = 'testScript'): CategoryData {

View File

@@ -55,4 +55,8 @@ export class ScriptDataStub implements ScriptData {
this.recommend = recommend; this.recommend = recommend;
return this; return this;
} }
public withRecommendationLevel(level: RecommendationLevel): ScriptDataStub {
this.recommend = RecommendationLevel[level].toLowerCase();
return this;
}
} }

View File

@@ -13,6 +13,7 @@
"moduleResolution": "node", "moduleResolution": "node",
"experimentalDecorators": true, "experimentalDecorators": true,
"esModuleInterop": true, "esModuleInterop": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": ".",