Compare commits

...

7 Commits

Author SHA1 Message Date
undergroundwires
0db8cc4206 Fix website not loading on Safari
It's caused by lookahead regex used in dash comment regex for inlining
PowerShell. This commit changes dash comment inlining.

- Change regex to one without lookahead.
- Add more test cases for inlining dash comment in tricky situations.
- Refactor makeInlineComment to be it's own function to easily test
  other regex options.
- Document all regex alternatives.
- Remove redundant null check (`||`) with adding safe navigation
  operator  (`?`) to allow variable before check to be null instead of
  throwing exception.
2021-11-04 18:42:44 +01:00
undergroundwires
97ddc027cb Fix dead URLs and use forks as GitHub references
Change all GitHub URLs with forks so they survive if their maintainer
decides to remove them.

Fix dead URLs in:
  - "Windows Push Notification Service" (#101)
  - "Limit CPU usage during scans to minimum"
  - "Disable NVIDIA telemetry"
2021-11-03 20:08:56 +01:00
undergroundwires
82c43ba2e3 Refactor to remove "Async" function name suffix
Remove convention where Async suffix is added to functions that returns
a Promise. It was a habit from C#, but is not widely used in JavaScript
/ TypeScript world, also bloats the code. The code is more consistent
with third party dependencies/frameworks without the suffix.
2021-11-01 19:02:22 +01:00
undergroundwires
799fb091b8 Fix failing URL status checking integration tests
Implement following redirects over `fetch` supporting cookies.
`node-fetch` does not support sending cookies during redirect. However,
this is needed to not end-up in a redirect loop for a sign-in callback.

Fix integration tests failing due to redirects and 403 errors:
  - Many redirects from `answers.microsoft.com` was throwing: throwing
    `FetchError: maximum redirect reached` error. It was caused by not
    having cookies when following redirects therefore having an infinite
    sign-in callback for the webpage.
  - Fixes integration tests failing due to additional referer header being
    sent by the application. It adds support for making exceptions to
    additional header sending through a list of regexes.

Add in-depth documentation for URL status checking.
2021-10-30 16:19:10 +01:00
undergroundwires
5ead1a087d Fix, document, unrecommend Windows browser cleanup
The main goal is to highlight and exclude scripts that clears user data
(such as Chrome bookmarks) from standard recommendation, thus allowing
more granular and intentional user selection. Because scripts that are
recommended as "standard" should be non-breaking.

Standard: Recommend only clearing data that would not be noticable by
user. E.g. caches and logs.
Strict	: Recommend clearing data that may be noticable by user, but
does not affect stored consciously data by user. E.g. cookies.
Do not recommend if data is stored consciously by user. E.g. favorites
/ bookmarks.

[General]
  - Change wording from "Clear xx traces" to "Clean xx history" to make
  it more clear and unify the naming with macOS scripts.
  - More documentation both in code and both as more references.

[Chrome]
  - Unrecommend deleting Chrome user profile.
  - Document what each chrome clean-up script is doing in more detail.

[Internet Explorer]
  - Document IE scripts better.
  - For Cookie cleanup, add solutions for later Windows version.
  - Unrecommend some from standard.
  - Remove undocumented `Local Settings\Traces` folder.
  - Take ownership before deleting Temporary Internet Files. Fixes
    permission error.
  - Remove `INetCookies\PrivacIE` script because it's undocumented and
    we already have cleanup for its parent folder (`INetCookies`).
  - Remove "%USERPROFILE%\Local Settings\Traces" due to lack of
    documentation.

[Safari]
  - Remove cleanup for undocumented traces folders `Safari\Traces`.
  - Document with subcategories and references.
  - Fix clearing all data not pointing to `localappdata`.
  - Unrecomend clearing all data.

[Opera]
  - Rename to "Clear all.." to show intent.
  - Unrecommend as it removes everything.
2021-10-28 17:43:04 +01:00
undergroundwires
64631a4552 Update dependencies
- Bump dependencies to latest.
- Remove unused inversify dependency.
- Lock sass-loader to a version that's compatible to 10. Because later
  versions (>=11) require Webpack v5 while Vue CLI v4 uses Webpack v4.
- Changes slashes as division to `math.div` as it's depreciated by SASS
  https://sass-lang.com/documentation/breaking-changes/slash
2021-10-23 20:25:03 +01:00
undergroundwires-bot
f47cb04860 ⬆️ bump everywhere to 0.11.0 2021-10-21 14:57:54 +00:00
45 changed files with 11489 additions and 3027 deletions

View File

@@ -1,5 +1,38 @@
# Changelog # Changelog
## 0.11.0 (2021-10-21)
* Change "grouping" to "view" | [c0c475f](https://github.com/undergroundwires/privacy.sexy/commit/c0c475ff564b23a4dabcc03ac2909207a8eb61ce)
* Tighten parameter substitution tolerance | [dcccb61](https://github.com/undergroundwires/privacy.sexy/commit/dcccb617813625c224a28242c5b965bb4cd6f189)
* Add optionality for parameters | [6a89c62](https://github.com/undergroundwires/privacy.sexy/commit/6a89c6224bdef5eb96980471f3b3935b9351b197)
* Do not collapse cards on links and code area #88 | [e73c0ad](https://github.com/undergroundwires/privacy.sexy/commit/e73c0ad1bf922b1dd3360fc5aafc3434951fa63c)
* Add scripts to disable, hide and opt-out from Siri | [c92dc1e](https://github.com/undergroundwires/privacy.sexy/commit/c92dc1e25387c65a3a41ca64d2a23cf8131b4c86)
* Improve macOS scripts for cleaning OS logs | [6c3c2e6](https://github.com/undergroundwires/privacy.sexy/commit/6c3c2e6709ec84f8e0411f19c024bab2c7e5753b)
* Add "with" expression for templating #53 | [862914b](https://github.com/undergroundwires/privacy.sexy/commit/862914b06ea9ef74c4b58a9a4164a10a38273638)
* Add support for pipes in templates #53 | [4d7ff7e](https://github.com/undergroundwires/privacy.sexy/commit/4d7ff7edc5a96cc0d99d3c1ca4fdf9bbdace3fd2)
* Bump node environment to 15.x | [2f0321f](https://github.com/undergroundwires/privacy.sexy/commit/2f0321f315ac0da8c713dd50e37032f1de194942)
* Add new UX for optionally downloading updates | [ddf417a](https://github.com/undergroundwires/privacy.sexy/commit/ddf417a16a79551b43576befab0541ea08487969)
* Add pipes to write pretty PowerShell #53 | [5217b0b](https://github.com/undergroundwires/privacy.sexy/commit/5217b0b7587ccfe509ba8adc3a7748b9bae14d7a)
* Improve alignment, padding/margin issues on UI | [c8cb7a5](https://github.com/undergroundwires/privacy.sexy/commit/c8cb7a5c28420557319606da82f56b011e88f470)
* Support disabling per-user services in Windows #16 | [4b23907](https://github.com/undergroundwires/privacy.sexy/commit/4b2390736ac1f9de2d5176b7b07da0e827112f9a)
* Add script to remove Meet Now icon in Windows | [f39ee76](https://github.com/undergroundwires/privacy.sexy/commit/f39ee76c0cda95f54502b19d5c49390fd0f12b5e)
* Add support for more depth in function calls | [20b7d28](https://github.com/undergroundwires/privacy.sexy/commit/20b7d283b02dd751dfbde18ef1fe334c6bf76e2b)
* Increase default screen width on desktop app | [9942df1](https://github.com/undergroundwires/privacy.sexy/commit/9942df16c8334ff041fb92f432a3a29e351c88df)
* Improve disabling of SmartScreen #74 | [0696ed8](https://github.com/undergroundwires/privacy.sexy/commit/0696ed8396e298a358bec17adb91c9145dd90418)
* Remove integration tests from deployments #90 | [37ad26a](https://github.com/undergroundwires/privacy.sexy/commit/37ad26a082851c02497c36e7fce40555b9480e11)
* Use a consistent color system | [b08a6b5](https://github.com/undergroundwires/privacy.sexy/commit/b08a6b5cecf4a53023053695292146edbd24b960)
* Add semi-automatic update support for macOS | [410bcd8](https://github.com/undergroundwires/privacy.sexy/commit/410bcd82445097c29c9fcf0eabf7af9ebcb93c1e)
* Add more ways to disable and clean Defender #74 | [2492f2d](https://github.com/undergroundwires/privacy.sexy/commit/2492f2d8141b3abdf590ccad59680b1f50ecb59e)
* Add privacy over security scripts for macOS #83 | [236a0f6](https://github.com/undergroundwires/privacy.sexy/commit/236a0f6c8241294fc397194cd1b20bdeccbbb50b)
* Change PowerShell double quotes escape | [9aa8166](https://github.com/undergroundwires/privacy.sexy/commit/9aa816689146ee6cd86d8262112677c38651c6bd)
* Change theme colors | [a8031d1](https://github.com/undergroundwires/privacy.sexy/commit/a8031d18d520dd3b0567f7b8cfe2dcd694b65073)
* Improve security hardening for macOS | [e6152fa](https://github.com/undergroundwires/privacy.sexy/commit/e6152fa76f5e7d23b0f79d5dd98713daaecbff90)
* Support disabling of protected services #74 | [ab8bce7](https://github.com/undergroundwires/privacy.sexy/commit/ab8bce768650a10677f0a13b3a9fae93c83802ff)
* Fix minor issues with Defender scripts | [739287a](https://github.com/undergroundwires/privacy.sexy/commit/739287ac71b3f8b04348fc101f1fa06f2d7d86a2)
* Update screenshot | [504fa05](https://github.com/undergroundwires/privacy.sexy/commit/504fa056d7d8b17fc20afd398f9a557495fca7e8)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.3...0.11.0)
## 0.10.3 (2021-08-27) ## 0.10.3 (2021-08-27)
* unrecommend VSS and document its breaking behavior | [7714898](https://github.com/undergroundwires/privacy.sexy/commit/77148980e08859f89c15c6604e55b56ce4f74358) * unrecommend VSS and document its breaking behavior | [7714898](https://github.com/undergroundwires/privacy.sexy/commit/77148980e08859f89c15c6604e55b56ce4f74358)

View File

@@ -16,7 +16,7 @@
- Online version at [https://privacy.sexy](https://privacy.sexy) - Online version at [https://privacy.sexy](https://privacy.sexy)
- 💡 No need to run any compiled software on your computer. - 💡 No need to run any compiled software on your computer.
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.3/privacy.sexy-Setup-0.10.3.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.3/privacy.sexy-0.10.3.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.3/privacy.sexy-0.10.3.AppImage). - Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.0/privacy.sexy-Setup-0.11.0.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.0/privacy.sexy-0.11.0.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.0/privacy.sexy-0.11.0.AppImage).
- 💡 Single click to execute your script. - 💡 Single click to execute your script.
- ❗ Come back regularly to apply latest version for stronger privacy and security. - ❗ Come back regularly to apply latest version for stronger privacy and security.
@@ -57,8 +57,8 @@
- Development: `npm run serve` to compile & hot-reload for development. - Development: `npm run serve` to compile & hot-reload for development.
- Production: `npm run build` to prepare files for distribution. - Production: `npm run build` to prepare files for distribution.
- Or run using Docker: - Or run using Docker:
1. Build: `docker build -t undergroundwires/privacy.sexy:0.10.3 .` 1. Build: `docker build -t undergroundwires/privacy.sexy:0.11.0 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.10.3 undergroundwires/privacy.sexy:0.10.3` 2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.0 undergroundwires/privacy.sexy:0.11.0`
## Architecture overview ## Architecture overview

13509
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.10.3", "version": "0.11.0",
"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",
@@ -22,52 +22,53 @@
}, },
"main": "background.js", "main": "background.js",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.3", "@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-regular-svg-icons": "^5.15.3", "@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^2.0.2", "@fortawesome/vue-fontawesome": "^2.0.6",
"@juggle/resize-observer": "^3.3.1", "@juggle/resize-observer": "^3.3.1",
"ace-builds": "^1.4.12", "ace-builds": "^1.4.13",
"core-js": "^3.12.1", "core-js": "^3.18.3",
"cross-fetch": "^3.1.4", "cross-fetch": "^3.1.4",
"electron-progressbar": "^2.0.1", "electron-progressbar": "^2.0.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"inversify": "^5.1.1", "install": "^0.13.0",
"liquor-tree": "^0.2.70", "liquor-tree": "^0.2.70",
"npm": "^8.1.1",
"v-tooltip": "2.1.3", "v-tooltip": "2.1.3",
"vue": "^2.6.12", "vue": "^2.6.14",
"vue-class-component": "^7.2.6", "vue-class-component": "^7.2.6",
"vue-js-modal": "^2.0.0-rc.6", "vue-js-modal": "^2.0.1",
"vue-property-decorator": "^9.1.2" "vue-property-decorator": "^9.1.2"
}, },
"devDependencies": { "devDependencies": {
"@types/ace": "0.0.45", "@types/ace": "0.0.47",
"@types/chai": "^4.2.18", "@types/chai": "^4.2.22",
"@types/file-saver": "^2.0.2", "@types/file-saver": "^2.0.3",
"@types/mocha": "^8.2.2", "@types/mocha": "^9.0.0",
"@vue/cli-plugin-babel": "^4.5.13", "@vue/cli-plugin-babel": "^4.5.14",
"@vue/cli-plugin-typescript": "^4.5.13", "@vue/cli-plugin-typescript": "^4.5.14",
"@vue/cli-plugin-unit-mocha": "^4.5.13", "@vue/cli-plugin-unit-mocha": "^4.5.14",
"@vue/cli-service": "^4.5.13", "@vue/cli-service": "^4.5.14",
"@vue/test-utils": "1.2.0", "@vue/test-utils": "1.2.2",
"chai": "^4.3.4", "chai": "^4.3.4",
"electron": "^12.0.7", "electron": "^15.3.0",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-log": "^4.3.5", "electron-log": "^4.4.1",
"electron-updater": "^4.3.8", "electron-updater": "^4.3.9",
"js-yaml-loader": "^1.2.2", "js-yaml-loader": "^1.2.2",
"markdownlint-cli": "^0.27.1", "markdownlint-cli": "^0.29.0",
"remark-cli": "^9.0.0", "remark-cli": "^10.0.0",
"remark-lint-no-dead-urls": "^1.1.0", "remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^4.0.0", "remark-preset-lint-consistent": "^5.1.0",
"remark-validate-links": "^10.0.4", "remark-validate-links": "^11.0.1",
"sass": "^1.32.12", "sass": "^1.43.3",
"sass-loader": "^10.0.1", "sass-loader": "10.2.0",
"tslib": "^2.2.0", "tslib": "^2.3.1",
"typescript": "^4.2.4", "typescript": "^4.4.4",
"vue-cli-plugin-electron-builder": "^2.0.0-rc.6", "vue-cli-plugin-electron-builder": "^2.1.1",
"vue-template-compiler": "^2.6.12", "vue-template-compiler": "^2.6.14",
"yaml-lint": "^1.2.4" "yaml-lint": "^1.2.4"
}, },
"homepage": "https://privacy.sexy", "homepage": "https://privacy.sexy",

View File

@@ -15,7 +15,7 @@ export class ApplicationFactory implements IApplicationFactory {
} }
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter())); this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
} }
public getAppAsync(): Promise<IApplication> { public getApp(): Promise<IApplication> {
return this.getter.getValueAsync(); return this.getter.getValue();
} }
} }

View File

@@ -7,12 +7,12 @@ import { IEnvironment } from '../Environment/IEnvironment';
import { IApplicationFactory } from '../IApplicationFactory'; import { IApplicationFactory } from '../IApplicationFactory';
import { ApplicationFactory } from '../ApplicationFactory'; import { ApplicationFactory } from '../ApplicationFactory';
export async function buildContextAsync( export async function buildContext(
factory: IApplicationFactory = ApplicationFactory.Current, factory: IApplicationFactory = ApplicationFactory.Current,
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> { environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
if (!factory) { throw new Error('undefined factory'); } if (!factory) { throw new Error('undefined factory'); }
if (!environment) { throw new Error('undefined environment'); } if (!environment) { throw new Error('undefined environment'); }
const app = await factory.getAppAsync(); const app = await factory.getApp();
const os = getInitialOs(app, environment); const os = getInitialOs(app, environment);
return new ApplicationContext(app, os); return new ApplicationContext(app, os);
} }

View File

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

View File

@@ -6,7 +6,7 @@ export class InlinePowerShell implements IPipe {
if (!code || !hasLines(code)) { if (!code || !hasLines(code)) {
return code; return code;
} }
code = replaceComments(code); code = inlineComments(code);
code = mergeLinesWithBacktick(code); code = mergeLinesWithBacktick(code);
code = mergeHereStrings(code); code = mergeHereStrings(code);
const lines = getLines(code) const lines = getLines(code)
@@ -25,18 +25,71 @@ function hasLines(text: string) {
Line comments using "#" are replaced with inline comment syntax <# comment.. #> Line comments using "#" are replaced with inline comment syntax <# comment.. #>
Otherwise single # comments out rest of the code Otherwise single # comments out rest of the code
*/ */
function replaceComments(code: string) { function inlineComments(code: string): string {
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (_$, match1 ) => { const makeInlineComment = (comment: string) => {
const value = match1?.trim(); const value = comment?.trim();
if (!value) { if (!value) {
return '<##>'; return '<##>';
} }
return `<# ${value} #>`; return `<# ${value} #>`;
};
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
if (captureComment === undefined) {
return match;
}
return makeInlineComment(captureComment);
}); });
/*
Other alternatives considered:
--------------------------
/#(?<!<#)(?![<>])(.*)$/gm
-------------------------
✅ Simple, yet matches and captures only what's necessary
❌ Fails to match some cases
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one`
❌ Uses lookbehind
Safari does not yet support lookbehind and syntax, leading application to not
load and throw "Invalid regular expression: invalid group specifier name"
https://caniuse.com/js-regexp-lookbehind
⏩ Usage
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (match, captureComment) => {
return makeInlineComment(captureComment)
});
----------------
/<#.*?#>|#(.*)/g
----------------
✅ Simple yet affective
❌ Matches all comments, but only captures dash comments
❌ Fails to match some cases
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
⏩ Usage
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
if (captureComment === undefined) {
return match;
}
return makeInlineComment(captureComment);
});
------------------------------------
/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
------------------------------------
✅ Covers all cases
❌ Matches every line, three capture groups are used to build result
⏩ Usage
return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
(match, captureLeft, captureDash, captureComment) => {
if (!captureDash) {
return match;
}
return captureLeft + makeInlineComment(captureComment);
});
*/
} }
function getLines(code: string) { function getLines(code: string): string [] {
return (code.split(/\r\n|\r|\n/) || []); return (code?.split(/\r\n|\r|\n/) || []);
} }
/* /*
@@ -59,7 +112,7 @@ interface IInlinedHereString {
readonly escapedQuotes: string; readonly escapedQuotes: string;
readonly separator: string; readonly separator: string;
} }
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable // We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
function getHereStringHandler(quotes: string): IInlinedHereString { function getHereStringHandler(quotes: string): IInlinedHereString {
const expandableNewLine = '`r`n'; const expandableNewLine = '`r`n';
switch (quotes) { switch (quotes) {

View File

@@ -72,7 +72,7 @@ actions:
name: Clear shared-cache strings data name: Clear shared-cache strings data
docs: docs:
- https://eclecticlight.co/2017/09/23/sierras-unified-log-evolves-more-persistent-and-a-valuable-log-log/ - https://eclecticlight.co/2017/09/23/sierras-unified-log-evolves-more-persistent-and-a-valuable-log-log/
- https://github.com/libyal/dtformats/blob/main/documentation/Apple%20Unified%20Logging%20and%20Activity%20Tracing%20formats.asciidoc - https://github.com/privacysexy-forks/dtformats/blob/main/documentation/Apple%20Unified%20Logging%20and%20Activity%20Tracing%20formats.asciidoc
code: |- code: |-
sudo rm -rfv /private/var/db/uuidtext/ sudo rm -rfv /private/var/db/uuidtext/
sudo rm -rfv /var/db/uuidtext/ sudo rm -rfv /var/db/uuidtext/
@@ -458,7 +458,7 @@ actions:
- -
name: Disable Firefox telemetry name: Disable Firefox telemetry
recommend: standard recommend: standard
docs: https://github.com/mozilla/policy-templates/blob/master/README.md docs: https://github.com/privacysexy-forks/policy-templates/blob/master/README.md
code: |- code: |-
# Enable Firefox policies so the telemetry can be configured. # Enable Firefox policies so the telemetry can be configured.
sudo defaults write /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled -bool TRUE sudo defaults write /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled -bool TRUE
@@ -503,7 +503,7 @@ actions:
- -
name: Disable PowerShell Core telemetry name: Disable PowerShell Core telemetry
recommend: standard recommend: standard
docs: https://github.com/PowerShell/PowerShell/blob/v7.1.0/README.md#telemetry docs: https://github.com/privacysexy-forks/PowerShell/blob/v7.1.5/README.md#telemetry
call: call:
- -
function: PersistUserEnvironmentConfiguration function: PersistUserEnvironmentConfiguration
@@ -576,7 +576,7 @@ actions:
name: Disable Siri voice feedback name: Disable Siri voice feedback
recommend: strict recommend: strict
docs: docs:
- https://github.com/joeyhoer/starter/blob/master/system/siri.sh - https://github.com/privacysexy-forks/starter/blob/master/system/siri.sh
- https://machippie.github.io/system/ - https://machippie.github.io/system/
code: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 3 code: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 3
revertCode: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 2 revertCode: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 2

View File

@@ -147,32 +147,109 @@ actions:
category: Clear browser history category: Clear browser history
children: children:
- -
name: Clear Internet Explorer traces category: Clear Internet Explorer history
recommend: standard children:
code: |- -
del /f /q "%localappdata%\Microsoft\Windows\INetCache\IE\*" name: Clear Internet Explorer caches
reg delete "HKCU\SOFTWARE\Microsoft\Internet Explorer\TypedURLs" /va /f recommend: standard
reg delete "HKCU\SOFTWARE\Microsoft\Internet Explorer\TypedURLsTime" /va /f docs:
rd /s /q "%localappdata%\Microsoft\Internet Explorer" # INetCache
rd /s /q "%APPDATA%\Microsoft\Windows\Cookies" - https://support.microsoft.com/en-us/help/260897/how-to-delete-the-contents-of-the-temporary-internet-files-folder
rd /s /q "%USERPROFILE%\Cookies" - https://docs.microsoft.com/en-us/troubleshoot/browsers/apps-access-admin-web-cache
rd /s /q "%USERPROFILE%\Local Settings\Traces" # WebCache
rd /s /q "%localappdata%\Temporary Internet Files" - https://docs.microsoft.com/en-us/troubleshoot/browsers/apps-access-admin-web-cache
rd /s /q "%localappdata%\Microsoft\Windows\Temporary Internet Files" code: |-
rd /s /q "%localappdata%\Microsoft\Windows\INetCookies\PrivacIE" del /f /q "%localappdata%\Microsoft\Windows\INetCache\IE\*"
rd /s /q "%localappdata%\Microsoft\Feeds Cache" rd /s /q "%localappdata%\Microsoft\Windows\WebCache"
rd /s /q "%localappdata%\Microsoft\InternetExplorer\DOMStore" -
name: Clear Internet Explorer recent URLs
recommend: strict
docs:
- https://web.archive.org/web/20160304232740/http://crucialsecurityblog.harris.com/2011/03/14/typedurls-part-1/
- https://web.archive.org/web/20160321221849/http://crucialsecurityblog.harris.com/2011/03/23/typedurls-part-2/
- https://web.archive.org/web/20150601014235/http://randomthoughtsofforensics.blogspot.com/2012/07/trouble-with-typedurlstime.html
- http://sketchymoose.blogspot.com/2014/02/typedurls-registry-key.html
code: |-
reg delete "HKCU\SOFTWARE\Microsoft\Internet Explorer\TypedURLs" /va /f
reg delete "HKCU\SOFTWARE\Microsoft\Internet Explorer\TypedURLsTime" /va /f
-
name: Clear Temporary Internet Files (browser cache)
recommend: standard
docs:
- https://en.wikipedia.org/wiki/Temporary_Internet_Files
- https://www.windows-commandline.com/delete-temporary-internet-files/ # %localappdata%\Temporary Internet Files
- https://www.thewindowsclub.com/temporary-internet-files-folder-location # %localappdata%\Microsoft\Windows\Temporary Internet Files and INetCache
code: |-
:: Windows XP
rd /s /q %userprofile%\Local Settings\Temporary Internet Files
:: Windows 7
rd /s /q "%localappdata%\Microsoft\Windows\Temporary Internet Files"
takeown /f "%localappdata%\Temporary Internet Files" /r /d y
icacls "%localappdata%\Temporary Internet Files" /grant administrators:F /t
rd /s /q "%localappdata%\Temporary Internet Files"
:: Windows 8 and above
rd /s /q "%localappdata%\Microsoft\Windows\INetCache"
-
name: Clear Internet Explorer Feeds Cache
recommend: standard
docs: https://kb.digital-detective.net/display/BF/Location+of+Internet+Explorer+11+Data
code: rd /s /q "%localappdata%\Microsoft\Feeds Cache"
-
name: Clear Internet Explorer cookies
recommend: strict
docs:
- https://docs.microsoft.com/en-us/windows/win32/wininet/managing-cookies
- https://docs.microsoft.com/en-us/internet-explorer/kb-support/ie-edge-faqs
- https://www.thewindowsclub.com/cookies-folder-location-windows
code: |-
:: Windows 7 browsers
rd /s /q "%APPDATA%\Microsoft\Windows\Cookies"
:: Windows 8 and higher
rd /s /q "%localappdata%\Microsoft\Windows\INetCookies"
-
name: Clear Internet Explorer DOMStore
recommend: standard
docs: https://web.archive.org/web/20100416135352/http://msdn.microsoft.com/en-us/library/cc197062(VS.85).aspx
code: rd /s /q "%localappdata%\Microsoft\InternetExplorer\DOMStore"
-
name: Clear all Internet Explorer user data
docs:
- https://kb.digital-detective.net/display/BF/Location+of+Internet+Explorer+Data
- https://kb.digital-detective.net/display/BF/Location+of+Internet+Explorer+11+Data
- https://www.forensafe.com/blogs/internetexplorer.html
# Includes Internet Explorer cache, tab recovery data, persistance storage (DOMStore, indexed DB etc.)
# Folders: CacheStorage\, Tracking Protection\, Tiles\, TabRoaming\, IECompatData\
# DOMStore\, Recovery\ (that includes browser history), DomainSuggestions\,
# VersionManager\, UrlBlockManager\, Indexed DB\, imagestore\, IEFlipAheadCache\
# EUPP\, EmieUserList\, EmieSiteList\, EmieBrowserModeList\
# Files: brndlog.txt, brndlog.bak, ie4uinit-ClearIconCache.log, ie4uinit-UserConfig.log,
# MSIMGSIZ.DAT
code: rd /s /q "%localappdata%\Microsoft\Internet Explorer"
- -
name: Clear Google Chrome traces category: Clear Google Chrome history
recommend: standard children:
code: |- -
del /f /q "%localappdata%\Google\Software Reporter Tool\*.log" name: Clear Google Chrome crash reports
rd /s /q "%USERPROFILE%\Local Settings\Application Data\Google\Chrome\User Data" recommend: standard
rd /s /q "%localappdata%\Google\Chrome\User Data" docs: https://www.chromium.org/developers/crash-reports
rd /s /q "%localappdata%\Google\CrashReports\"" code: |-
rd /s /q "%localappdata%\Google\Chrome\User Data\Crashpad\reports\"" rd /s /q "%localappdata%\Google\Chrome\User Data\Crashpad\reports\"
rd /s /q "%localappdata%\Google\CrashReports\"
-
name: Clear Software Reporter Tool logs
recommend: standard
docs: https://support.google.com/chrome/forum/AAAAP1KN0B0T8qnffV5gwM/
code: del /f /q "%localappdata%\Google\Software Reporter Tool\*.log"
-
name: Clear all Chrome user data
docs: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/user_data_dir.md
code: |-
:: Windows XP
rd /s /q "%USERPROFILE%\Local Settings\Application Data\Google\Chrome\User Data"
:: Windows Vista and later
rd /s /q "%localappdata%\Google\Chrome\User Data"
- -
category: Clear Firefox traces category: Clear Firefox history
children: children:
- -
name: Clear browsing history and caches name: Clear browsing history and caches
@@ -201,26 +278,57 @@ actions:
- -
name: Clear all Firefox user profiles, settings, and data name: Clear all Firefox user profiles, settings, and data
code: |- code: |-
rd /s /q "%LOCALAPPDATA%\Mozilla\Firefox\Profiles" rd /s /q "%localappdata%\Mozilla\Firefox\Profiles"
rd /s /q "%APPDATA%\Mozilla\Firefox\Profiles" rd /s /q "%APPDATA%\Mozilla\Firefox\Profiles"
- -
name: Clear Opera traces name: Clear all Opera data (user profiles, settings, and data)
recommend: standard
code: |- code: |-
rd /s /q "%USERPROFILE%\AppData\Local\Opera\Opera" :: Windows XP
rd /s /q "%APPDATA%\Opera\Opera"
rd /s /q "%USERPROFILE%\Local Settings\Application Data\Opera\Opera" rd /s /q "%USERPROFILE%\Local Settings\Application Data\Opera\Opera"
:: Windows Vista and later
rd /s /q "%localappdata%\Opera\Opera"
rd /s /q "%APPDATA%\Opera\Opera"
- -
name: Clear Safari traces category: Clear Safari history
recommend: standard children:
code: |- -
rd /s /q "%USERPROFILE%\AppData\Local\Apple Computer\Safari\Traces" name: Clear Webpage Icons
rd /s /q "%APPDATA%\Apple Computer\Safari" recommend: standard
del /q /s /f "%USERPROFILE%\AppData\Local\Apple Computer\Safari\Cache.db" docs: https://www.sans.org/blog/safari-browser-forensics/
del /q /s /f "%USERPROFILE%\AppData\Local\Apple Computer\Safari\WebpageIcons.db" code: |-
rd /s /q "%USERPROFILE%\Local Settings\Application Data\Apple Computer\Safari\Traces" :: Windows XP
del /q /s /f "%USERPROFILE%\Local Settings\Application Data\Apple Computer\Safari\Cache.db" del /q /s /f "%USERPROFILE%\Local Settings\Application Data\Safari\WebpageIcons.db"
del /q /s /f "%USERPROFILE%\Local Settings\Application Data\Safari\WebpageIcons.db" :: Windows Vista and later
del /q /s /f "%localappdata%\Apple Computer\Safari\WebpageIcons.db"
-
name: Clear Safari cache
recommend: standard
docs: https://forensicswiki.xyz/wiki/index.php?title=Apple_Safari
code: |-
:: Windows XP
del /q /s /f "%USERPROFILE%\Local Settings\Application Data\Apple Computer\Safari\Cache.db"
:: Windows Vista and later
del /q /s /f "%localappdata%\Apple Computer\Safari\Cache.db"
-
name: Clear Safari cookies
recommend: strict
docs: https://kb.digital-detective.net/display/BF/Location+of+Safari+Data
code: |-
:: Windows XP
del /q /s /f "%USERPROFILE%\Local Settings\Application Data\Apple Computer\Safari\Cookies.db"
:: Windows Vista and later
del /q /s /f "%localappdata%\Apple Computer\Safari\Cookies.db"
-
name: Clear all Safari data (user profiles, settings, and data)
docs:
- https://kb.digital-detective.net/display/BF/Location+of+Safari+Data
- https://forensicswiki.xyz/wiki/index.php?title=Apple_Safari
- https://zerosecurity.org/2013/04/safari-forensic-tutorial
code: |-
:: Windows XP
rd /s /q "%USERPROFILE%\Local Settings\Application Data\Apple Computer\Safari"
:: Windows Vista and later
rd /s /q "%AppData%\Apple Computer\Safari"
- -
category: Clear Windows logs & caches category: Clear Windows logs & caches
children: children:
@@ -1565,8 +1673,8 @@ actions:
- -
category: Disable NVIDIA telemetry category: Disable NVIDIA telemetry
docs: docs:
- https://github.com/CHEF-KOCH/nVidia-modded-Inf - https://github.com/privacysexy-forks/nVidia-modded-Inf
- https://github.com/NateShoffner/Disable-Nvidia-Telemetry - https://github.com/privacysexy-forks/Disable-Nvidia-Telemetry
- https://forum.palemoon.org/viewtopic.php?f=4&t=15686&sid=3d7982d3b9e89c713547f1a581ea44a2&start=20 - https://forum.palemoon.org/viewtopic.php?f=4&t=15686&sid=3d7982d3b9e89c713547f1a581ea44a2&start=20
children: children:
- -
@@ -1638,7 +1746,7 @@ actions:
powerShellValue: $false 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/privacysexy-forks/vscode/blob/1aee0c194cff72d179b9f8ef324e47f34555a07d/src/vs/workbench/contrib/experiments/node/experimentService.ts#L173
recommend: standard recommend: standard
call: call:
function: SetVsCodeSetting function: SetVsCodeSetting
@@ -1941,7 +2049,7 @@ actions:
- -
name: Disable Firefox metrics reporting name: Disable Firefox metrics reporting
recommend: standard recommend: standard
docs: https://github.com/mozilla/policy-templates#disabletelemetry docs: https://github.com/privacysexy-forks/policy-templates#disabletelemetry
code: reg add HKLM\SOFTWARE\Policies\Mozilla\Firefox /v DisableTelemetry /t REG_DWORD /d 1 /f code: reg add HKLM\SOFTWARE\Policies\Mozilla\Firefox /v DisableTelemetry /t REG_DWORD /d 1 /f
revertCode: reg add HKLM\SOFTWARE\Policies\Mozilla\Firefox /v DisableTelemetry /t REG_DWORD /d 0 /f revertCode: reg add HKLM\SOFTWARE\Policies\Mozilla\Firefox /v DisableTelemetry /t REG_DWORD /d 0 /f
- -
@@ -3070,7 +3178,7 @@ actions:
- -
name: Limit CPU usage during scans to minimum name: Limit CPU usage during scans to minimum
docs: docs:
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::Scan_AvgCPULoadFact - https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::Scan_AvgCPULoadFactor
- https://docs.microsoft.com/en-us/powershell/module/defender/set-mppreference - https://docs.microsoft.com/en-us/powershell/module/defender/set-mppreference
call: call:
- -
@@ -4210,7 +4318,7 @@ actions:
- -
name: Windows Push Notification Service name: Windows Push Notification Service
recommend: standard recommend: standard
docs: https://en.wikipedia.org/wiki/Windows_Push_Notification_Service#Privacy_Issue docs: https://en.wikipedia.org/w/index.php?title=Windows_Push_Notification_Service&oldid=1012335551#Privacy_Issue
code: sc stop "WpnService" & sc config "WpnService" start=disabled code: sc stop "WpnService" & sc config "WpnService" start=disabled
revertCode: sc config "WpnService" start=auto & sc start "WpnService" revertCode: sc config "WpnService" start=auto & sc start "WpnService"
- -
@@ -4790,7 +4898,7 @@ actions:
category: Uninstall system apps category: Uninstall system apps
docs: docs:
- https://docs.microsoft.com/en-us/windows/application-management/apps-in-windows-10#system-apps - https://docs.microsoft.com/en-us/windows/application-management/apps-in-windows-10#system-apps
- https://github.com/Sycnex/Windows10Debloater/blob/02963b6844cf7d13ed3fa64d75128f4e312689ca/Windows10Debloater.ps1#L43 - https://github.com/privacysexy-forks/Windows10Debloater/blob/d4ede6d3225e7def087b389c7e8cf6be0d5e2cd7/Windows10Debloater.ps1#L43-L47
children: children:
- -
name: File Picker app name: File Picker app

View File

@@ -10,7 +10,7 @@ export class CodeRunner {
private readonly node = getNodeJs(), private readonly node = getNodeJs(),
private readonly environment = Environment.CurrentEnvironment) { private readonly environment = Environment.CurrentEnvironment) {
} }
public async runCodeAsync(code: string, folderName: string, fileExtension: string): Promise<void> { public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
const dir = this.node.path.join(this.node.os.tmpdir(), folderName); const dir = this.node.path.join(this.node.os.tmpdir(), folderName);
await this.node.fs.promises.mkdir(dir, {recursive: true}); await this.node.fs.promises.mkdir(dir, {recursive: true});
const filePath = this.node.path.join(dir, `run.${fileExtension}`); const filePath = this.node.path.join(dir, `run.${fileExtension}`);

View File

@@ -12,7 +12,7 @@ export class AsyncLazy<T> {
this.valueFactory = valueFactory; this.valueFactory = valueFactory;
} }
public async getValueAsync(): Promise<T> { public async getValue(): 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); return Promise.resolve(this.value);

View File

@@ -1,5 +1,5 @@
export type SchedulerType = (callback: (...args: any[]) => void, ms: number) => void; export type SchedulerType = (callback: (...args: any[]) => void, ms: number) => void;
export function sleepAsync(time: number, scheduler: SchedulerType = setTimeout) { export function sleep(time: number, scheduler: SchedulerType = setTimeout) {
return new Promise((resolve) => scheduler(() => resolve(undefined), time)); return new Promise((resolve) => scheduler(() => resolve(undefined), time));
} }

View File

@@ -96,7 +96,7 @@ export default class MacOsInstructions extends Vue {
public macOsDownloadUrl = ''; public macOsDownloadUrl = '';
public async created() { public async created() {
const app = await ApplicationFactory.Current.getAppAsync(); const app = await ApplicationFactory.Current.getApp();
this.appName = app.info.name; this.appName = app.info.name;
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS); this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
} }

View File

@@ -3,18 +3,18 @@
<IconButton <IconButton
v-if="this.canRun" v-if="this.canRun"
text="Run" text="Run"
v-on:click="executeCodeAsync" v-on:click="executeCode"
icon-prefix="fas" icon-name="play"> icon-prefix="fas" icon-name="play">
</IconButton> </IconButton>
<IconButton <IconButton
:text="this.isDesktopVersion ? 'Save' : 'Download'" :text="this.isDesktopVersion ? 'Save' : 'Download'"
v-on:click="saveCodeAsync" v-on:click="saveCode"
icon-prefix="fas" icon-prefix="fas"
:icon-name="this.isDesktopVersion ? 'save' : 'file-download'"> :icon-name="this.isDesktopVersion ? 'save' : 'file-download'">
</IconButton> </IconButton>
<IconButton <IconButton
text="Copy" text="Copy"
v-on:click="copyCodeAsync" v-on:click="copyCode"
icon-prefix="fas" icon-name="copy"> icon-prefix="fas" icon-name="copy">
</IconButton> </IconButton>
<Dialog v-if="this.isMacOsCollection" ref="instructionsDialog"> <Dialog v-if="this.isMacOsCollection" ref="instructionsDialog">
@@ -54,20 +54,20 @@ export default class TheCodeButtons extends StatefulVue {
public isMacOsCollection = false; public isMacOsCollection = false;
public fileName = ''; public fileName = '';
public async copyCodeAsync() { public async copyCode() {
const code = await this.getCurrentCodeAsync(); const code = await this.getCurrentCode();
Clipboard.copyText(code.current); Clipboard.copyText(code.current);
} }
public async saveCodeAsync() { public async saveCode() {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
saveCode(this.fileName, context.state); saveCode(this.fileName, context.state);
if (this.isMacOsCollection) { if (this.isMacOsCollection) {
(this.$refs.instructionsDialog as any).show(); (this.$refs.instructionsDialog as any).show();
} }
} }
public async executeCodeAsync() { public async executeCode() {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
await executeCodeAsync(context); await executeCode(context);
} }
protected handleCollectionState(newState: ICategoryCollectionState): void { protected handleCollectionState(newState: ICategoryCollectionState): void {
@@ -77,8 +77,8 @@ export default class TheCodeButtons extends StatefulVue {
this.react(newState.code); this.react(newState.code);
} }
private async getCurrentCodeAsync(): Promise<IApplicationCode> { private async getCurrentCode(): Promise<IApplicationCode> {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
const code = context.state.code; const code = context.state.code;
return code; return code;
} }
@@ -115,9 +115,9 @@ function buildFileName(scripting: IScriptingDefinition) {
return fileName; return fileName;
} }
async function executeCodeAsync(context: IApplicationContext) { async function executeCode(context: IApplicationContext) {
const runner = new CodeRunner(); const runner = new CodeRunner();
await runner.runCodeAsync( await runner.runCode(
/*code*/ context.state.code.current, /*code*/ context.state.code.current,
/*appName*/ context.app.info.name, /*appName*/ context.app.info.name,
/*fileExtension*/ context.state.collection.scripting.fileExtension, /*fileExtension*/ context.state.collection.scripting.fileExtension,

View File

@@ -51,13 +51,13 @@ export default class TheCodeArea extends StatefulVue {
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.events.unsubscribeAll(); this.events.unsubscribeAll();
this.events.register(appCode.changed.on((code) => this.updateCodeAsync(code))); this.events.register(appCode.changed.on((code) => this.updateCode(code)));
} }
private async updateCodeAsync(event: ICodeChangedEvent) { private async updateCode(event: ICodeChangedEvent) {
this.removeCurrentHighlighting(); this.removeCurrentHighlighting();
if (event.isEmpty()) { if (event.isEmpty()) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
const defaultCode = getDefaultCode(context.state.collection.scripting.language); const defaultCode = getDefaultCode(context.state.collection.scripting.language);
this.editor.setValue(defaultCode, 1); this.editor.setValue(defaultCode, 1);
return; return;

View File

@@ -3,7 +3,7 @@
<MenuOptionListItem <MenuOptionListItem
v-for="os in this.allOses" :key="os.name" v-for="os in this.allOses" :key="os.name"
:enabled="currentOs !== os.os" :enabled="currentOs !== os.os"
@click="changeOsAsync(os.os)" @click="changeOs(os.os)"
:label="os.name" :label="os.name"
/> />
</MenuOptionList> </MenuOptionList>
@@ -29,12 +29,12 @@ export default class TheOsChanger extends StatefulVue {
public currentOs?: OperatingSystem = null; public currentOs?: OperatingSystem = null;
public async created() { public async created() {
const app = await ApplicationFactory.Current.getAppAsync(); const app = await ApplicationFactory.Current.getApp();
this.allOses = app.getSupportedOsList() this.allOses = app.getSupportedOsList()
.map((os) => ({ os, name: renderOsName(os) })); .map((os) => ({ os, name: renderOsName(os) }));
} }
public async changeOsAsync(newOs: OperatingSystem) { public async changeOs(newOs: OperatingSystem) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
context.changeContext(newOs); context.changeContext(newOs);
} }

View File

@@ -50,10 +50,10 @@ export default class CardListItem extends StatefulVue {
public areAllChildrenSelected = false; public areAllChildrenSelected = false;
public async mounted() { public async mounted() {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
this.events.register(context.state.selection.changed.on( this.events.register(context.state.selection.changed.on(
() => this.updateSelectionIndicatorsAsync(this.categoryId))); () => this.updateSelectionIndicators(this.categoryId)));
await this.updateStateAsync(this.categoryId); await this.updateState(this.categoryId);
} }
@Emit('selected') @Emit('selected')
public onSelected(isExpanded: boolean) { public onSelected(isExpanded: boolean) {
@@ -64,7 +64,7 @@ export default class CardListItem extends StatefulVue {
this.isExpanded = value === this.categoryId; this.isExpanded = value === this.categoryId;
} }
@Watch('isExpanded') @Watch('isExpanded')
public async onExpansionChangedAsync(newValue: number, oldValue: number) { public async onExpansionChanged(newValue: number, oldValue: number) {
if (!oldValue && newValue) { if (!oldValue && newValue) {
await new Promise((r) => setTimeout(r, 400)); await new Promise((r) => setTimeout(r, 400));
const focusElement = this.$refs.cardElement as HTMLElement; const focusElement = this.$refs.cardElement as HTMLElement;
@@ -72,19 +72,19 @@ export default class CardListItem extends StatefulVue {
} }
} }
@Watch('categoryId') @Watch('categoryId')
public async updateStateAsync(value: |number) { public async updateState(value: |number) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
const category = !value ? undefined : context.state.collection.findCategory(value); const category = !value ? undefined : context.state.collection.findCategory(value);
this.cardTitle = category ? category.name : undefined; this.cardTitle = category ? category.name : undefined;
await this.updateSelectionIndicatorsAsync(value); await this.updateSelectionIndicators(value);
} }
protected handleCollectionState(): void { protected handleCollectionState(): void {
return; return;
} }
private async updateSelectionIndicatorsAsync(categoryId: number) { private async updateSelectionIndicators(categoryId: number) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
const selection = context.state.selection; const selection = context.state.selection;
const category = context.state.collection.findCategory(categoryId); const category = context.state.collection.findCategory(categoryId);
this.isAnyChildSelected = category ? selection.isAnySelected(category) : false; this.isAnyChildSelected = category ? selection.isAnySelected(category) : false;

View File

@@ -6,7 +6,7 @@
:selectedNodeIds="selectedNodeIds" :selectedNodeIds="selectedNodeIds"
:filterPredicate="filterPredicate" :filterPredicate="filterPredicate"
:filterText="filterText" :filterText="filterText"
v-on:nodeSelected="toggleNodeSelectionAsync($event)" v-on:nodeSelected="toggleNodeSelection($event)"
> >
</SelectableTree> </SelectableTree>
</span> </span>
@@ -42,8 +42,8 @@ export default class ScriptsTree extends StatefulVue {
private filtered?: IFilterResult; private filtered?: IFilterResult;
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) { public async toggleNodeSelection(event: INodeSelectedEvent) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
switch (event.node.type) { switch (event.node.type) {
case NodeType.Category: case NodeType.Category:
toggleCategoryNodeSelection(event, context.state); toggleCategoryNodeSelection(event, context.state);
@@ -56,8 +56,8 @@ export default class ScriptsTree extends StatefulVue {
} }
} }
@Watch('categoryId', { immediate: true }) @Watch('categoryId', { immediate: true })
public async setNodesAsync(categoryId?: number) { public async setNodes(categoryId?: number) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
if (categoryId) { if (categoryId) {
this.nodes = parseSingleCategory(categoryId, context.state.collection); this.nodes = parseSingleCategory(categoryId, context.state.collection);
} else { } else {

View File

@@ -2,7 +2,7 @@
<div class="checkbox-switch" > <div class="checkbox-switch" >
<input type="checkbox" class="input-checkbox" <input type="checkbox" class="input-checkbox"
v-model="isReverted" v-model="isReverted"
@change="onRevertToggledAsync()" @change="onRevertToggled()"
v-on:click.stop> v-on:click.stop>
<div class="checkbox-animate"> <div class="checkbox-animate">
<span class="checkbox-off">revert</span> <span class="checkbox-off">revert</span>
@@ -28,12 +28,12 @@ export default class RevertToggle extends StatefulVue {
private handler: IReverter; private handler: IReverter;
@Watch('node', {immediate: true}) public async onNodeChangedAsync(node: INode) { @Watch('node', {immediate: true}) public async onNodeChanged(node: INode) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
this.handler = getReverter(node, context.state.collection); this.handler = getReverter(node, context.state.collection);
} }
public async onRevertToggledAsync() { public async onRevertToggled() {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
this.handler.selectWithRevertState(this.isReverted, context.state.selection); this.handler.selectWithRevertState(this.isReverted, context.state.selection);
} }
@@ -51,6 +51,7 @@ export default class RevertToggle extends StatefulVue {
<style scoped lang="scss"> <style scoped lang="scss">
@use 'sass:math';
@import "@/presentation/styles/colors.scss"; @import "@/presentation/styles/colors.scss";
$color-unchecked-bullet : $color-primary-darker; $color-unchecked-bullet : $color-primary-darker;
@@ -73,7 +74,7 @@ $size-height : 30px;
-webkit-border-radius: $size-height; -webkit-border-radius: $size-height;
border-radius: $size-height; border-radius: $size-height;
line-height: $size-height; line-height: $size-height;
font-size: $size-height / 2; font-size: math.div($size-height, 2);
display: inline-block; display: inline-block;
input.input-checkbox { input.input-checkbox {
@@ -122,7 +123,7 @@ $size-height : 30px;
background-color: $color-checked-bg; background-color: $color-checked-bg;
} }
+ .checkbox-animate:before { + .checkbox-animate:before {
left: ($size-width - $size-width/3.5); left: ($size-width - math.div($size-width, 3.5));
background-color: $color-checked-bullet; background-color: $color-checked-bullet;
} }
+ .checkbox-animate .checkbox-off { + .checkbox-animate .checkbox-off {
@@ -143,7 +144,7 @@ $size-height : 30px;
} }
.checkbox-off { .checkbox-off {
margin-left: $size-width / 3; margin-left: math.div($size-width, 3);
opacity: 1; opacity: 1;
color: $color-unchecked-text; color: $color-unchecked-text;
} }
@@ -151,7 +152,7 @@ $size-height : 30px;
.checkbox-on { .checkbox-on {
display: none; display: none;
float: right; float: right;
margin-right: $size-width / 3; margin-right: math.div($size-width, 3);
opacity: 0; opacity: 0;
color: $color-checked-text; color: $color-checked-text;
} }

View File

@@ -23,7 +23,7 @@ import Node from './Node/Node.vue';
import { INode } from './Node/INode'; import { INode } from './Node/INode';
import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator'; import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator';
import { INodeSelectedEvent } from './INodeSelectedEvent'; import { INodeSelectedEvent } from './INodeSelectedEvent';
import { sleepAsync } from '@/infrastructure/Threading/AsyncSleep'; import { sleep } from '@/infrastructure/Threading/AsyncSleep';
import { getNewState } from './LiquorTree/NodeWrapper/NodeStateUpdater'; import { getNewState } from './LiquorTree/NodeWrapper/NodeStateUpdater';
import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions'; import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions';
import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter'; import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter';
@@ -56,7 +56,7 @@ export default class SelectableTree extends Vue { // Keep it stateless to make i
} }
@Watch('initialNodes', { immediate: true }) @Watch('initialNodes', { immediate: true })
public async updateNodesAsync(nodes: readonly INode[]) { public async updateNodes(nodes: readonly INode[]) {
if (!nodes) { if (!nodes) {
throw new Error('undefined initial nodes'); throw new Error('undefined initial nodes');
} }
@@ -66,12 +66,12 @@ export default class SelectableTree extends Vue { // Keep it stateless to make i
(node) => node.state = updateState(node.state, node, this.selectedNodeIds)); (node) => node.state = updateState(node.state, node, this.selectedNodeIds));
} }
this.initialLiquourTreeNodes = initialNodes; this.initialLiquourTreeNodes = initialNodes;
const api = await this.getLiquorTreeApiAsync(); const api = await this.getLiquorTreeApi();
api.setModel(this.initialLiquourTreeNodes); // as liquor tree is not reactive to data after initialization api.setModel(this.initialLiquourTreeNodes); // as liquor tree is not reactive to data after initialization
} }
@Watch('filterText', { immediate: true }) @Watch('filterText', { immediate: true })
public async updateFilterTextAsync(filterText: |string) { public async updateFilterText(filterText: |string) {
const api = await this.getLiquorTreeApiAsync(); const api = await this.getLiquorTreeApi();
if (!filterText) { if (!filterText) {
api.clearFilter(); api.clearFilter();
} else { } else {
@@ -80,22 +80,22 @@ export default class SelectableTree extends Vue { // Keep it stateless to make i
} }
@Watch('selectedNodeIds') @Watch('selectedNodeIds')
public async setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) { public async setSelectedStatus(selectedNodeIds: ReadonlyArray<string>) {
if (!selectedNodeIds) { if (!selectedNodeIds) {
throw new Error('SelectedrecurseDown nodes are undefined'); throw new Error('SelectedrecurseDown nodes are undefined');
} }
const tree = await this.getLiquorTreeApiAsync(); const tree = await this.getLiquorTreeApi();
tree.recurseDown( tree.recurseDown(
(node) => node.states = updateState(node.states, node, selectedNodeIds), (node) => node.states = updateState(node.states, node, selectedNodeIds),
); );
} }
private async getLiquorTreeApiAsync(): Promise<ILiquorTree> { private async getLiquorTreeApi(): Promise<ILiquorTree> {
const accessor = (): ILiquorTree => { const accessor = (): ILiquorTree => {
const uiElement = this.$refs.treeElement; const uiElement = this.$refs.treeElement;
return uiElement ? (uiElement as any).tree : undefined; return uiElement ? (uiElement as any).tree : undefined;
}; };
const treeElement = await tryUntilDefinedAsync(accessor, 5, 20); // Wait for it to render const treeElement = await tryUntilDefined(accessor, 5, 20); // Wait for it to render
if (!treeElement) { if (!treeElement) {
throw Error('Referenced tree element cannot be found. Perhaps it\'s not yet rendered?'); throw Error('Referenced tree element cannot be found. Perhaps it\'s not yet rendered?');
} }
@@ -119,7 +119,7 @@ function recurseDown(
} }
} }
} }
async function tryUntilDefinedAsync<T>( async function tryUntilDefined<T>(
accessor: () => T | undefined, accessor: () => T | undefined,
delayInMs: number, maxTries: number): Promise<T | undefined> { delayInMs: number, maxTries: number): Promise<T | undefined> {
let triesLeft = maxTries; let triesLeft = maxTries;
@@ -130,7 +130,7 @@ async function tryUntilDefinedAsync<T>(
return value; return value;
} }
triesLeft--; triesLeft--;
await sleepAsync(delayInMs); await sleep(delayInMs);
} }
return value; return value;
} }

View File

@@ -13,7 +13,7 @@
<div class="search__query__close-button"> <div class="search__query__close-button">
<font-awesome-icon <font-awesome-icon
:icon="['fas', 'times']" :icon="['fas', 'times']"
v-on:click="clearSearchQueryAsync()"/> v-on:click="clearSearchQuery()"/>
</div> </div>
</div> </div>
<div v-if="!searchHasMatches" class="search-no-matches"> <div v-if="!searchHasMatches" class="search-no-matches">
@@ -65,11 +65,11 @@ export default class TheScriptsView extends StatefulVue {
public ViewType = ViewType; // Make it accessible from the view public ViewType = ViewType; // Make it accessible from the view
public async created() { public async created() {
const app = await ApplicationFactory.Current.getAppAsync(); const app = await ApplicationFactory.Current.getApp();
this.repositoryUrl = app.info.repositoryWebUrl; this.repositoryUrl = app.info.repositoryWebUrl;
} }
public async clearSearchQueryAsync() { public async clearSearchQuery() {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
const filter = context.state.filter; const filter = context.state.filter;
filter.removeFilter(); filter.removeFilter();
} }

View File

@@ -1,7 +1,7 @@
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 { buildContextAsync } from '@/application/Context/ApplicationContextFactory'; import { buildContext } from '@/application/Context/ApplicationContextFactory';
import { IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext'; import { IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection'; import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
@@ -9,14 +9,14 @@ import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscr
// @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 {
private static readonly instance = new AsyncLazy<IApplicationContext>(() => buildContextAsync()); private static readonly instance = new AsyncLazy<IApplicationContext>(() => buildContext());
protected readonly events = new EventSubscriptionCollection(); protected readonly events = new EventSubscriptionCollection();
private readonly ownEvents = new EventSubscriptionCollection(); private readonly ownEvents = new EventSubscriptionCollection();
public async mounted() { public async mounted() {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
this.ownEvents.register(context.contextChanged.on((event) => this.handleStateChangedEvent(event))); this.ownEvents.register(context.contextChanged.on((event) => this.handleStateChangedEvent(event)));
this.handleCollectionState(context.state, undefined); this.handleCollectionState(context.state, undefined);
} }
@@ -27,8 +27,8 @@ export abstract class StatefulVue extends Vue {
protected abstract handleCollectionState( protected abstract handleCollectionState(
newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined): void; newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined): void;
protected getCurrentContextAsync(): Promise<IApplicationContext> { protected getCurrentContext(): Promise<IApplicationContext> {
return StatefulVue.instance.getValueAsync(); return StatefulVue.instance.getValue();
} }
private handleStateChangedEvent(event: IApplicationContextChangedEvent) { private handleStateChangedEvent(event: IApplicationContextChangedEvent) {

View File

@@ -24,20 +24,20 @@ export default class DownloadUrlListItem extends Vue {
public hasCurrentOsDesktopVersion: boolean = false; public hasCurrentOsDesktopVersion: boolean = false;
public async mounted() { public async mounted() {
await this.onOperatingSystemChangedAsync(this.operatingSystem); await this.onOperatingSystemChanged(this.operatingSystem);
} }
@Watch('operatingSystem') @Watch('operatingSystem')
public async onOperatingSystemChangedAsync(os: OperatingSystem) { public async onOperatingSystemChanged(os: OperatingSystem) {
const currentOs = Environment.CurrentEnvironment.os; const currentOs = Environment.CurrentEnvironment.os;
this.isCurrentOs = os === currentOs; this.isCurrentOs = os === currentOs;
this.downloadUrl = await this.getDownloadUrlAsync(os); this.downloadUrl = await this.getDownloadUrl(os);
this.operatingSystemName = getOperatingSystemName(os); this.operatingSystemName = getOperatingSystemName(os);
this.hasCurrentOsDesktopVersion = hasDesktopVersion(currentOs); this.hasCurrentOsDesktopVersion = hasDesktopVersion(currentOs);
} }
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> { private async getDownloadUrl(os: OperatingSystem): Promise<string> {
const context = await ApplicationFactory.Current.getAppAsync(); const context = await ApplicationFactory.Current.getApp();
return context.info.getDownloadUrl(os); return context.info.getDownloadUrl(os);
} }
} }

View File

@@ -43,7 +43,7 @@ export default class PrivacyPolicy extends Vue {
public isDesktop = Environment.CurrentEnvironment.isDesktop; public isDesktop = Environment.CurrentEnvironment.isDesktop;
public async created() { public async created() {
const app = await ApplicationFactory.Current.getAppAsync(); const app = await ApplicationFactory.Current.getApp();
this.initialize(app); this.initialize(app);
} }

View File

@@ -65,7 +65,7 @@ export default class TheFooter extends Vue {
public homepageUrl: string = ''; public homepageUrl: string = '';
public async created() { public async created() {
const app = await ApplicationFactory.Current.getAppAsync(); const app = await ApplicationFactory.Current.getApp();
this.initialize(app); this.initialize(app);
} }

View File

@@ -15,7 +15,7 @@ export default class TheHeader extends Vue {
public subtitle = ''; public subtitle = '';
public async created() { public async created() {
const app = await ApplicationFactory.Current.getAppAsync(); const app = await ApplicationFactory.Current.getApp();
this.title = app.info.name; this.title = app.info.name;
} }
} }

View File

@@ -26,8 +26,8 @@ export default class TheSearchBar extends StatefulVue {
public searchQuery = ''; public searchQuery = '';
@Watch('searchQuery') @Watch('searchQuery')
public async updateFilterAsync(newFilter: |string) { public async updateFilter(newFilter: |string) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
const filter = context.state.filter; const filter = context.state.filter;
if (!newFilter) { if (!newFilter) {
filter.removeFilter(); filter.removeFilter();

View File

@@ -4,8 +4,8 @@ import { ProgressInfo } from 'electron-builder';
import { UpdateProgressBar } from './UpdateProgressBar'; import { UpdateProgressBar } from './UpdateProgressBar';
import log from 'electron-log'; import log from 'electron-log';
export async function handleAutoUpdateAsync() { export async function handleAutoUpdate() {
if (await askDownloadAndInstallAsync() === DownloadDialogResult.NotNow) { if (await askDownloadAndInstall() === DownloadDialogResult.NotNow) {
return; return;
} }
startHandlingUpdateProgress(); startHandlingUpdateProgress();
@@ -29,12 +29,12 @@ function startHandlingUpdateProgress() {
autoUpdater.on('update-downloaded', async (info: UpdateInfo) => { autoUpdater.on('update-downloaded', async (info: UpdateInfo) => {
log.info('@update-downloaded@\n', info); log.info('@update-downloaded@\n', info);
progressBar.close(); progressBar.close();
await handleUpdateDownloadedAsync(); await handleUpdateDownloaded();
}); });
} }
async function handleUpdateDownloadedAsync() { async function handleUpdateDownloaded() {
if (await askRestartAndInstallAsync() === InstallDialogResult.NotNow) { if (await askRestartAndInstall() === InstallDialogResult.NotNow) {
return; return;
} }
setTimeout(() => autoUpdater.quitAndInstall(), 1); setTimeout(() => autoUpdater.quitAndInstall(), 1);
@@ -44,7 +44,7 @@ enum DownloadDialogResult {
Install = 0, Install = 0,
NotNow = 1, NotNow = 1,
} }
async function askDownloadAndInstallAsync(): Promise<DownloadDialogResult> { async function askDownloadAndInstall(): Promise<DownloadDialogResult> {
const updateDialogResult = await dialog.showMessageBox({ const updateDialogResult = await dialog.showMessageBox({
type: 'question', type: 'question',
buttons: ['Install', 'Not now' ], buttons: ['Install', 'Not now' ],
@@ -61,7 +61,7 @@ enum InstallDialogResult {
InstallAndRestart = 0, InstallAndRestart = 0,
NotNow = 1, NotNow = 1,
} }
async function askRestartAndInstallAsync(): Promise<InstallDialogResult> { async function askRestartAndInstall(): Promise<InstallDialogResult> {
const installDialogResult = await dialog.showMessageBox({ const installDialogResult = await dialog.showMessageBox({
type: 'question', type: 'question',
buttons: ['Install and restart', 'Later'], buttons: ['Install and restart', 'Later'],

View File

@@ -12,8 +12,8 @@ export function requiresManualUpdate(): boolean {
return process.platform === 'darwin'; return process.platform === 'darwin';
} }
export async function handleManualUpdateAsync(info: UpdateInfo) { export async function handleManualUpdate(info: UpdateInfo) {
const result = await askForVisitingWebsiteForManualUpdateAsync(); const result = await askForVisitingWebsiteForManualUpdate();
if (result === ManualDownloadDialogResult.NoAction) { if (result === ManualDownloadDialogResult.NoAction) {
return; return;
} }
@@ -26,7 +26,7 @@ export async function handleManualUpdateAsync(info: UpdateInfo) {
if (result === ManualDownloadDialogResult.VisitReleasesPage) { if (result === ManualDownloadDialogResult.VisitReleasesPage) {
await shell.openExternal(project.releaseUrl); await shell.openExternal(project.releaseUrl);
} else if (result === ManualDownloadDialogResult.UpdateNow) { } else if (result === ManualDownloadDialogResult.UpdateNow) {
await downloadAsync(info, project); await download(info, project);
} }
} }
@@ -35,7 +35,7 @@ enum ManualDownloadDialogResult {
UpdateNow = 1, UpdateNow = 1,
VisitReleasesPage = 2, VisitReleasesPage = 2,
} }
async function askForVisitingWebsiteForManualUpdateAsync(): Promise<ManualDownloadDialogResult> { async function askForVisitingWebsiteForManualUpdate(): Promise<ManualDownloadDialogResult> {
const visitPageResult = await dialog.showMessageBox({ const visitPageResult = await dialog.showMessageBox({
type: 'info', type: 'info',
buttons: [ buttons: [
@@ -54,7 +54,7 @@ async function askForVisitingWebsiteForManualUpdateAsync(): Promise<ManualDownlo
return visitPageResult.response; return visitPageResult.response;
} }
async function downloadAsync(info: UpdateInfo, project: ProjectInformation) { async function download(info: UpdateInfo, project: ProjectInformation) {
log.info('Downloading update manually'); log.info('Downloading update manually');
const progressBar = new UpdateProgressBar(); const progressBar = new UpdateProgressBar();
progressBar.showIndeterminateState(); progressBar.showIndeterminateState();
@@ -69,7 +69,7 @@ async function downloadAsync(info: UpdateInfo, project: ProjectInformation) {
await fs.promises.mkdir(parentFolder, { recursive: true }); await fs.promises.mkdir(parentFolder, { recursive: true });
} }
const dmgFileUrl = project.getDownloadUrl(OperatingSystem.macOS); const dmgFileUrl = project.getDownloadUrl(OperatingSystem.macOS);
await downloadFileWithProgressAsync(dmgFileUrl, filePath, await downloadFileWithProgress(dmgFileUrl, filePath,
(percentage) => { progressBar.showPercentage(percentage); }); (percentage) => { progressBar.showPercentage(percentage); });
await shell.openPath(filePath); await shell.openPath(filePath);
progressBar.close(); progressBar.close();
@@ -81,7 +81,7 @@ async function downloadAsync(info: UpdateInfo, project: ProjectInformation) {
type ProgressCallback = (progress: number) => void; type ProgressCallback = (progress: number) => void;
async function downloadFileWithProgressAsync( async function downloadFileWithProgress(
url: string, filePath: string, progressHandler: ProgressCallback) { url: string, filePath: string, progressHandler: ProgressCallback) {
// We don't download through autoUpdater as it cannot download DMG but requires distributing ZIP // We don't download through autoUpdater as it cannot download DMG but requires distributing ZIP
log.info(`Fetching ${url}`); log.info(`Fetching ${url}`);
@@ -100,10 +100,10 @@ async function downloadFileWithProgressAsync(
if (!reader) { if (!reader) {
throw new Error('No response body'); throw new Error('No response body');
} }
await streamWithProgressAsync(contentLength, reader, writer, progressHandler); await streamWithProgress(contentLength, reader, writer, progressHandler);
} }
async function streamWithProgressAsync( async function streamWithProgress(
totalLength: number, totalLength: number,
readStream: NodeJS.ReadableStream, readStream: NodeJS.ReadableStream,
writeStream: fs.WriteStream, writeStream: fs.WriteStream,

View File

@@ -1,10 +1,10 @@
import { autoUpdater, UpdateInfo } from 'electron-updater'; import { autoUpdater, UpdateInfo } from 'electron-updater';
import log from 'electron-log'; import log from 'electron-log';
import { handleManualUpdateAsync, requiresManualUpdate } from './ManualUpdater'; import { handleManualUpdate, requiresManualUpdate } from './ManualUpdater';
import { handleAutoUpdateAsync } from './AutoUpdater'; import { handleAutoUpdate } from './AutoUpdater';
interface IUpdater { interface IUpdater {
checkForUpdatesAsync(): Promise<void>; checkForUpdates(): Promise<void>;
} }
export function setupAutoUpdater(): IUpdater { export function setupAutoUpdater(): IUpdater {
@@ -21,20 +21,20 @@ export function setupAutoUpdater(): IUpdater {
return; return;
} }
isAlreadyHandled = true; isAlreadyHandled = true;
await handleAvailableUpdateAsync(info); await handleAvailableUpdate(info);
}); });
return { return {
checkForUpdatesAsync: async () => { checkForUpdates: async () => {
// autoUpdater.emit('update-available'); // For testing // autoUpdater.emit('update-available'); // For testing
await autoUpdater.checkForUpdates(); await autoUpdater.checkForUpdates();
}, },
}; };
} }
async function handleAvailableUpdateAsync(info: UpdateInfo) { async function handleAvailableUpdate(info: UpdateInfo) {
if (requiresManualUpdate()) { if (requiresManualUpdate()) {
await handleManualUpdateAsync(info); await handleManualUpdate(info);
return; return;
} }
await handleAutoUpdateAsync(); await handleAutoUpdate();
} }

View File

@@ -124,7 +124,7 @@ function loadApplication(window: BrowserWindow) {
// Load the index.html when not in development // Load the index.html when not in development
loadUrlWithNodeWorkaround(win, 'app://./index.html'); loadUrlWithNodeWorkaround(win, 'app://./index.html');
const updater = setupAutoUpdater(); const updater = setupAutoUpdater();
updater.checkForUpdatesAsync(); updater.checkForUpdates();
} }
} }

View File

@@ -3,7 +3,7 @@ import { expect } from 'chai';
import { parseApplication } from '@/application/Parser/ApplicationParser'; import { parseApplication } from '@/application/Parser/ApplicationParser';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { IUrlStatus } from './StatusChecker/IUrlStatus'; import { IUrlStatus } from './StatusChecker/IUrlStatus';
import { getUrlStatusesInParallelAsync, IBatchRequestOptions } from './StatusChecker/BatchStatusChecker'; import { getUrlStatusesInParallel, IBatchRequestOptions } from './StatusChecker/BatchStatusChecker';
describe('collections', () => { describe('collections', () => {
// arrange // arrange
@@ -17,14 +17,17 @@ describe('collections', () => {
requestOptions: { requestOptions: {
retryExponentialBaseInMs: 3 /* sec */ * 1000, retryExponentialBaseInMs: 3 /* sec */ * 1000,
additionalHeaders: { referer: app.info.homepage }, additionalHeaders: { referer: app.info.homepage },
additionalHeadersUrlIgnore: [
'http://batcmd.com/', // Otherwise it responds with 403
],
}, },
}; };
const testTimeoutInMs = urls.length * 60000 /* 1 minute */; const testTimeoutInMs = urls.length * 60000 /* 1 minute */;
it('have no dead urls', async () => { it('have no dead urls', async () => {
// act // act
const results = await getUrlStatusesInParallelAsync(urls, options); const results = await getUrlStatusesInParallel(urls, options);
// assert // assert
const deadUrls = results.filter((r) => r.statusCode !== 200); const deadUrls = results.filter((r) => r.code !== 200);
expect(deadUrls).to.have.lengthOf(0, printUrls(deadUrls)); expect(deadUrls).to.have.lengthOf(0, printUrls(deadUrls));
}).timeout(testTimeoutInMs); }).timeout(testTimeoutInMs);
}); });
@@ -41,7 +44,7 @@ function printUrls(statuses: IUrlStatus[]): string {
return '\n' + return '\n' +
statuses.map((status) => statuses.map((status) =>
`- ${status.url}\n` + `- ${status.url}\n` +
(status.statusCode ? `\tResponse code: ${status.statusCode}` : '') + (status.code ? `\tResponse code: ${status.code}` : '') +
(status.error ? `\tException: ${JSON.stringify(status.error, null, '\t')}` : '')) (status.error ? `\tException: ${JSON.stringify(status.error, null, '\t')}` : ''))
.join(`\n`) .join(`\n`)
+ '\n'; + '\n';

View File

@@ -1,15 +1,16 @@
import { sleepAsync } from '@/infrastructure/Threading/AsyncSleep'; import { sleep } from '@/infrastructure/Threading/AsyncSleep';
import { IUrlStatus } from './IUrlStatus'; import { IUrlStatus } from './IUrlStatus';
import { getUrlStatusAsync, IRequestOptions } from './Requestor'; import { getUrlStatus, IRequestOptions } from './Requestor';
import { groupUrlsByDomain } from './UrlPerDomainGrouper'; import { groupUrlsByDomain } from './UrlPerDomainGrouper';
export async function getUrlStatusesInParallelAsync( export async function getUrlStatusesInParallel(
urls: string[], urls: string[],
options?: IBatchRequestOptions): Promise<IUrlStatus[]> { options?: IBatchRequestOptions): Promise<IUrlStatus[]> {
// urls = [ 'https://privacy.sexy' ]; // Here to comment out when testing
const uniqueUrls = Array.from(new Set(urls)); const uniqueUrls = Array.from(new Set(urls));
options = { ...DefaultOptions, ...options }; options = { ...DefaultOptions, ...options };
console.log('Options: ', options); // tslint:disable-line: no-console console.log('Options: ', options); // tslint:disable-line: no-console
const results = await requestAsync(uniqueUrls, options); const results = await request(uniqueUrls, options);
return results; return results;
} }
@@ -34,19 +35,19 @@ const DefaultOptions: IBatchRequestOptions = {
}, },
}; };
function requestAsync(urls: string[], options: IBatchRequestOptions): Promise<IUrlStatus[]> { function request(urls: string[], options: IBatchRequestOptions): Promise<IUrlStatus[]> {
if (!options.domainOptions.sameDomainParallelize) { if (!options.domainOptions.sameDomainParallelize) {
return runOnEachDomainWithDelayAsync( return runOnEachDomainWithDelay(
urls, urls,
(url) => getUrlStatusAsync(url, options.requestOptions), (url) => getUrlStatus(url, options.requestOptions),
options.domainOptions.sameDomainDelayInMs); options.domainOptions.sameDomainDelayInMs);
} else { } else {
return Promise.all( return Promise.all(
urls.map((url) => getUrlStatusAsync(url, options.requestOptions))); urls.map((url) => getUrlStatus(url, options.requestOptions)));
} }
} }
async function runOnEachDomainWithDelayAsync( async function runOnEachDomainWithDelay(
urls: string[], urls: string[],
action: (url: string) => Promise<IUrlStatus>, action: (url: string) => Promise<IUrlStatus>,
delayInMs: number): Promise<IUrlStatus[]> { delayInMs: number): Promise<IUrlStatus[]> {
@@ -57,7 +58,7 @@ async function runOnEachDomainWithDelayAsync(
const status = await action(url); const status = await action(url);
results.push(status); results.push(status);
if (results.length !== group.length) { if (results.length !== group.length) {
await sleepAsync(delayInMs); await sleep(delayInMs);
} }
} }
return results; return results;

View File

@@ -1,9 +1,9 @@
import { sleepAsync } from '@/infrastructure/Threading/AsyncSleep'; import { sleep } from '@/infrastructure/Threading/AsyncSleep';
import { IUrlStatus } from './IUrlStatus'; import { IUrlStatus } from './IUrlStatus';
const DefaultBaseRetryIntervalInMs = 5 /* sec */ * 1000; const DefaultBaseRetryIntervalInMs = 5 /* sec */ * 1000;
export async function retryWithExponentialBackOffAsync( export async function retryWithExponentialBackOff(
action: () => Promise<IUrlStatus>, action: () => Promise<IUrlStatus>,
baseRetryIntervalInMs: number = DefaultBaseRetryIntervalInMs, baseRetryIntervalInMs: number = DefaultBaseRetryIntervalInMs,
currentRetry = 1): Promise<IUrlStatus> { currentRetry = 1): Promise<IUrlStatus> {
@@ -14,8 +14,8 @@ export async function retryWithExponentialBackOffAsync(
const exponentialBackOffInMs = getRetryTimeoutInMs(currentRetry, baseRetryIntervalInMs); const exponentialBackOffInMs = getRetryTimeoutInMs(currentRetry, baseRetryIntervalInMs);
// tslint:disable-next-line: no-console // tslint:disable-next-line: no-console
console.log(`Retrying (${currentRetry}) in ${exponentialBackOffInMs / 1000} seconds`, status); console.log(`Retrying (${currentRetry}) in ${exponentialBackOffInMs / 1000} seconds`, status);
await sleepAsync(exponentialBackOffInMs); await sleep(exponentialBackOffInMs);
return retryWithExponentialBackOffAsync(action, baseRetryIntervalInMs, currentRetry + 1); return retryWithExponentialBackOff(action, baseRetryIntervalInMs, currentRetry + 1);
} }
} }
return status; return status;
@@ -25,8 +25,8 @@ function shouldRetry(status: IUrlStatus) {
if (status.error) { if (status.error) {
return true; return true;
} }
return isTransientError(status.statusCode) return isTransientError(status.code)
|| status.statusCode === 429; // Too Many Requests || status.code === 429; // Too Many Requests
} }
function isTransientError(statusCode: number) { function isTransientError(statusCode: number) {

View File

@@ -0,0 +1,66 @@
import fetch from 'cross-fetch';
export function fetchFollow(
url: string, fetchOptions: RequestInit, followOptions: IFollowOptions): Promise<Response> {
followOptions = { ...DefaultOptions, ...followOptions };
if (!followOptions.followRedirects
|| followOptions.maximumRedirectFollowDepth === 0) {
return fetch(url, fetchOptions);
}
fetchOptions = { ...fetchOptions, redirect: 'manual' /* handled manually */ };
const cookies = new CookieStorage(followOptions.enableCookies);
return followRecursivelyWithCookies(
url, fetchOptions, followOptions.maximumRedirectFollowDepth, cookies);
}
export interface IFollowOptions {
followRedirects?: boolean;
maximumRedirectFollowDepth?: number;
enableCookies?: boolean;
}
const DefaultOptions: IFollowOptions = {
followRedirects: true,
maximumRedirectFollowDepth: 20,
enableCookies: true,
};
async function followRecursivelyWithCookies(
url: string, options: RequestInit, followDepth: number, cookies: CookieStorage): Promise<Response> {
if (cookies.hasAny()) {
options = { ...options, headers: { ...options.headers, cookie: cookies.getHeader() } };
}
const response = await fetch(url, options);
if (!isRedirect(response.status)) {
return response;
}
if (--followDepth < 0) {
throw new Error(`[max-redirect] maximum redirect reached at: ${url}`);
}
const cookieHeader = response.headers.get('set-cookie');
cookies.addHeader(cookieHeader);
const nextUrl = response.headers.get('location');
return followRecursivelyWithCookies(nextUrl, options, followDepth, cookies);
}
function isRedirect(code: number): boolean {
return code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
}
class CookieStorage {
public cookies = new Array<string>();
constructor(private readonly enabled: boolean) {
}
public hasAny() {
return this.enabled && this.cookies.length > 0;
}
public addHeader(header: string) {
if (!this.enabled || !header) {
return;
}
this.cookies.push(header);
}
public getHeader() {
return this.cookies.join(' ; ');
}
}

View File

@@ -1,5 +1,5 @@
export interface IUrlStatus { export interface IUrlStatus {
url: string; url: string;
error?: any; error?: any;
statusCode?: number; code?: number;
} }

View File

@@ -0,0 +1,108 @@
# status-checker
CLI and SDK to check whether an external URL is alive.
🧐 Why?
- 🏃🏻 Batch checking status of URLs in parallel.
- 🤖 Zero-touch start, pre-configured for reliable results, still configurable.
- 🤞 Reliable, mimics a real web browser by following redirect, and cookie storage.
🍭 Sweets such as
- 😇 Queueing requests by domain to be nice to them
- 🔁 Retry pattern with exponential back-off
## CLI
Coming soon 🚧
## Programmatic usage
Programmatic usage is supported both on Node.js and browser.
### `getUrlStatusesInParallel`
```js
// Simple example
const statuses = await getUrlStatusesInParallel([ 'https://privacy.sexy', /* ... */ ]);
if(statuses.all((r) => r.code === 200)) {
console.log('All URLs are alive!');
} else {
console.log('Dead URLs:', statuses.filter((r) => r.code !== 200).map((r) => r.url));
}
// Fastest configuration
const statuses = await getUrlStatusesInParallel([ 'https://privacy.sexy', /* ... */ ], {
domainOptions: {
sameDomainParallelize: false,
}
});
```
#### Batch request options
- `domainOptions`:
- **`sameDomainParallelize`**, (*boolean*), default: `false`
- Determines whether the requests to URLs under same domain will be parallelize.
- Setting `false` parallelizes all requests.
- Setting `true` sends requests in queue for each unique domain, still parallelizing for different domains.
- Requests to different domains are always parallelized regardless of this option.
- 💡 This helps to avoid `429 Too Many Requests` and be nice to websites
- **`sameDomainDelayInMs`** (*boolean*), default: `3000` (3 seconds)
- Sets delay between requests to same host (domain) if same domain parallelization is disabled.
- `requestOptions` (*object*): See [request options](#request-options).
### `getUrlStatus`
Checks whether single URL is dead or alive.
```js
// Simple example
const status = await getUrlStatus('https://privacy.sexy');
console.log(`Status code: ${status.code}`);
```
#### Request options
- **`retryExponentialBaseInMs`** (*boolean*), default: `5000` (5 seconds)
- The based time that's multiplied by exponential value for exponential backoff and retry calculations
- The longer it is, the longer the delay between retries are.
- **`additionalHeaders`** (*boolean*), default: `false`
- Additional headers that will be sent alongside default headers mimicking browser.
- If default header are specified, additional headers override defaults.
- **`followOptions`** (*object*): See [follow options](#follow-options).
### `fetchFollow`
Gets response from single URL by following `3XX` redirect targets by sending necessary cookies.
Same fetch API except third parameter that specifies [follow options](#follow-options), `redirect: 'follow' | 'manual' | 'error'` is discarded in favor of the third parameter.
```js
const status = await fetchFollow('https://privacy.sexy', {
// First argument is same options as fetch API, except `redirect` options
// that's discarded in favor of next argument follow options
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0'
},
}, {
// Second argument sets the redirect behavior
followRedirects: true,
maximumRedirectFollowDepth: 20,
enableCookies: true,
}
);
console.log(`Status code: ${status.code}`);
```
#### Follow options
- **`followRedirects`** (*boolean*), default: `true`
- Determines whether redirects with `3XX` response code will be followed.
- **`maximumRedirectFollowDepth`** (*boolean*), default: `20`
- Determines maximum consequent redirects that will be followed.
- 💡 Helps to solve maximum redirect reached errors.
- **`enableCookies`** (*boolean*), default: `true`
- Saves cookies requested to store by webpages and sends them when redirected.
- 💡 Helps to over-come sign-in challenges with callbacks.

View File

@@ -1,37 +1,44 @@
import { retryWithExponentialBackOffAsync } from './ExponentialBackOffRetryHandler'; import { retryWithExponentialBackOff } from './ExponentialBackOffRetryHandler';
import { IUrlStatus } from './IUrlStatus'; import { IUrlStatus } from './IUrlStatus';
import fetch from 'cross-fetch'; import { fetchFollow, IFollowOptions } from './FetchFollow';
export async function getUrlStatus(
url: string,
options: IRequestOptions = DefaultOptions): Promise<IUrlStatus> {
options = { ...DefaultOptions, ...options };
const fetchOptions = getFetchOptions(url, options);
return retryWithExponentialBackOff(async () => {
console.log('Requesting', url); // tslint:disable-line: no-console
let result: IUrlStatus;
try {
const response = await fetchFollow(url, fetchOptions, options.followOptions);
result = { url, code: response.status };
} catch (err) {
result = { url, error: err };
}
return result;
}, options.retryExponentialBaseInMs);
}
export interface IRequestOptions { export interface IRequestOptions {
retryExponentialBaseInMs?: number; retryExponentialBaseInMs?: number;
additionalHeaders?: Record<string, string>; additionalHeaders?: Record<string, string>;
} additionalHeadersUrlIgnore?: string[];
followOptions?: IFollowOptions;
export async function getUrlStatusAsync(
url: string,
options: IRequestOptions = DefaultOptions): Promise<IUrlStatus> {
options = { ...DefaultOptions, ...options };
const fetchOptions = getFetchOptions(options);
return retryWithExponentialBackOffAsync(async () => {
console.log('Requesting', url); // tslint:disable-line: no-console
try {
const response = await fetch(url, fetchOptions);
return { url, statusCode: response.status};
} catch (err) {
return { url, error: err};
}
}, options.retryExponentialBaseInMs);
} }
const DefaultOptions: IRequestOptions = { const DefaultOptions: IRequestOptions = {
retryExponentialBaseInMs: 5000, retryExponentialBaseInMs: 5000,
additionalHeaders: {}, additionalHeaders: {},
additionalHeadersUrlIgnore: [],
}; };
function getFetchOptions(options: IRequestOptions) { function getFetchOptions(url: string, options: IRequestOptions): RequestInit {
const additionalHeaders = options.additionalHeadersUrlIgnore.some(
(ignorePattern) => url.match(ignorePattern)) ? {} : options.additionalHeaders;
return { return {
method: 'GET', method: 'GET',
headers: { ...DefaultHeaders, ...options.additionalHeaders }, headers: { ...DefaultHeaders, ...additionalHeaders },
}; };
} }

View File

@@ -15,7 +15,7 @@ describe('ApplicationFactory', () => {
expect(act).to.throw(expectedError); expect(act).to.throw(expectedError);
}); });
}); });
describe('getAppAsync', () => { describe('getApp', () => {
it('returns result from the getter', async () => { it('returns result from the getter', async () => {
// arrange // arrange
const expected = new ApplicationStub(); const expected = new ApplicationStub();
@@ -23,10 +23,10 @@ describe('ApplicationFactory', () => {
const sut = new SystemUnderTest(getter); const sut = new SystemUnderTest(getter);
// act // act
const actual = await Promise.all( [ const actual = await Promise.all( [
sut.getAppAsync(), sut.getApp(),
sut.getAppAsync(), sut.getApp(),
sut.getAppAsync(), sut.getApp(),
sut.getAppAsync(), sut.getApp(),
]); ]);
// assert // assert
expect(actual.every((value) => value === expected)); expect(actual.every((value) => value === expected));
@@ -42,10 +42,10 @@ describe('ApplicationFactory', () => {
const sut = new SystemUnderTest(getter); const sut = new SystemUnderTest(getter);
// act // act
await Promise.all( [ await Promise.all( [
sut.getAppAsync(), sut.getApp(),
sut.getAppAsync(), sut.getApp(),
sut.getAppAsync(), sut.getApp(),
sut.getAppAsync(), sut.getApp(),
]); ]);
// assert // assert
expect(totalExecution).to.equal(1); expect(totalExecution).to.equal(1);

View File

@@ -2,7 +2,7 @@ import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { buildContextAsync } from '@/application/Context/ApplicationContextFactory'; import { buildContext } from '@/application/Context/ApplicationContextFactory';
import { IApplicationFactory } from '@/application/IApplicationFactory'; import { IApplicationFactory } from '@/application/IApplicationFactory';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { EnvironmentStub } from '@tests/unit/stubs/EnvironmentStub'; import { EnvironmentStub } from '@tests/unit/stubs/EnvironmentStub';
@@ -10,7 +10,7 @@ import { ApplicationStub } from '@tests/unit/stubs/ApplicationStub';
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub'; import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
describe('ApplicationContextFactory', () => { describe('ApplicationContextFactory', () => {
describe('buildContextAsync', () => { describe('buildContext', () => {
describe('factory', () => { describe('factory', () => {
it('sets application from factory', async () => { it('sets application from factory', async () => {
// arrange // arrange
@@ -18,7 +18,7 @@ describe('ApplicationContextFactory', () => {
new CategoryCollectionStub().withOs(OperatingSystem.macOS)); new CategoryCollectionStub().withOs(OperatingSystem.macOS));
const factoryMock = mockFactoryWithApp(expected); const factoryMock = mockFactoryWithApp(expected);
// act // act
const context = await buildContextAsync(factoryMock); const context = await buildContext(factoryMock);
// assert // assert
expect(expected).to.equal(context.app); expect(expected).to.equal(context.app);
}); });
@@ -32,7 +32,7 @@ describe('ApplicationContextFactory', () => {
const collection = new CategoryCollectionStub().withOs(expected); const collection = new CategoryCollectionStub().withOs(expected);
const factoryMock = mockFactoryWithCollection(collection); const factoryMock = mockFactoryWithCollection(collection);
// act // act
const context = await buildContextAsync(factoryMock, environment); const context = await buildContext(factoryMock, environment);
// assert // assert
const actual = context.state.os; const actual = context.state.os;
expect(expected).to.equal(actual); expect(expected).to.equal(actual);
@@ -45,7 +45,7 @@ describe('ApplicationContextFactory', () => {
const collection = new CategoryCollectionStub().withOs(expected); const collection = new CategoryCollectionStub().withOs(expected);
const factoryMock = mockFactoryWithCollection(collection); const factoryMock = mockFactoryWithCollection(collection);
// act // act
const context = await buildContextAsync(factoryMock, environment); const context = await buildContext(factoryMock, environment);
// assert // assert
const actual = context.state.os; const actual = context.state.os;
expect(expected).to.equal(actual); expect(expected).to.equal(actual);
@@ -62,7 +62,7 @@ describe('ApplicationContextFactory', () => {
const app = new ApplicationStub().withCollections(...allCollections); const app = new ApplicationStub().withCollections(...allCollections);
const factoryMock = mockFactoryWithApp(app); const factoryMock = mockFactoryWithApp(app);
// act // act
const context = await buildContextAsync(factoryMock, environment); const context = await buildContext(factoryMock, environment);
// assert // assert
const actual = context.state.os; const actual = context.state.os;
expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`); expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`);
@@ -78,6 +78,6 @@ function mockFactoryWithCollection(result: ICategoryCollection): IApplicationFac
function mockFactoryWithApp(app: IApplication): IApplicationFactory { function mockFactoryWithApp(app: IApplication): IApplicationFactory {
return { return {
getAppAsync: () => Promise.resolve(app), getApp: () => Promise.resolve(app),
}; };
} }

View File

@@ -283,7 +283,7 @@ function getCommentCases(): IPipeTestCase[] {
), ),
}, },
{ {
name: 'can convert comment with inline comment parts', name: 'can convert comment with inline comment parts inside',
input: getWindowsLines( input: getWindowsLines(
'$text+= #Comment with < inside', '$text+= #Comment with < inside',
'$text+= #Comment ending with >', '$text+= #Comment ending with >',
@@ -295,14 +295,28 @@ function getCommentCases(): IPipeTestCase[] {
'$text+= <# Comment with <# inline comment #> #>', '$text+= <# Comment with <# inline comment #> #>',
), ),
}, },
{
name: 'can convert comment with inline comment parts around', // Pretty uncommon
input: getWindowsLines(
'Write-Host "hi" # Comment ending line inline comment but not one #>',
'Write-Host "hi" #>Comment starting like inline comment end but not one',
// Following line does not compile as valid PowerShell referring to missing #> for inline comment
'Write-Host "hi" <#Comment starting like inline comment start but not one',
),
expectedOutput: getSingleLinedOutput(
'Write-Host "hi" <# Comment ending line inline comment but not one #> #>',
'Write-Host "hi" <# >Comment starting like inline comment end but not one #>',
'Write-Host "hi" <<# Comment starting like inline comment start but not one #>',
),
},
{ {
name: 'converts empty hash comment', name: 'converts empty hash comment',
input: getWindowsLines( input: getWindowsLines(
'Write-Host "Lorem ipsus" #', 'Write-Host "Comment without text" #',
'Write-Host "Non-empty line"', 'Write-Host "Non-empty line"',
), ),
expectedOutput: getSingleLinedOutput( expectedOutput: getSingleLinedOutput(
'Write-Host "Lorem ipsus" <##>', 'Write-Host "Comment without text" <##>',
'Write-Host "Non-empty line"', 'Write-Host "Non-empty line"',
), ),
}, },
@@ -318,7 +332,7 @@ function getCommentCases(): IPipeTestCase[] {
), ),
}, },
{ {
name: 'trims whitespaces around from match', name: 'trims whitespaces around comment',
input: getWindowsLines( input: getWindowsLines(
'# Comment with whitespaces around ', '# Comment with whitespaces around ',
'#\tComment with tabs around\t\t', '#\tComment with tabs around\t\t',

View File

@@ -5,7 +5,7 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
import { CodeRunner } from '@/infrastructure/CodeRunner'; import { CodeRunner } from '@/infrastructure/CodeRunner';
describe('CodeRunner', () => { describe('CodeRunner', () => {
describe('runCodeAsync', () => { describe('runCode', () => {
it('creates temporary directory recursively', async () => { it('creates temporary directory recursively', async () => {
// arrange // arrange
const expectedDir = 'expected-dir'; const expectedDir = 'expected-dir';
@@ -17,7 +17,7 @@ describe('CodeRunner', () => {
// act // act
await context await context
.withFolderName(folderName) .withFolderName(folderName)
.runCodeAsync(); .runCode();
// assert // assert
expect(context.mocks.fs.mkdirHistory.length).to.equal(1); expect(context.mocks.fs.mkdirHistory.length).to.equal(1);
@@ -42,7 +42,7 @@ describe('CodeRunner', () => {
.withCode(expectedCode) .withCode(expectedCode)
.withFolderName(folderName) .withFolderName(folderName)
.withExtension(extension) .withExtension(extension)
.runCodeAsync(); .runCode();
// assert // assert
expect(context.mocks.fs.writeFileHistory.length).to.equal(1); expect(context.mocks.fs.writeFileHistory.length).to.equal(1);
@@ -66,7 +66,7 @@ describe('CodeRunner', () => {
await context await context
.withFolderName(folderName) .withFolderName(folderName)
.withExtension(extension) .withExtension(extension)
.runCodeAsync(); .runCode();
// assert // assert
expect(context.mocks.fs.chmodCallHistory.length).to.equal(1); expect(context.mocks.fs.chmodCallHistory.length).to.equal(1);
@@ -93,7 +93,7 @@ describe('CodeRunner', () => {
// act // act
await context await context
.withOs(data.os) .withOs(data.os)
.runCodeAsync(); .runCode();
// assert // assert
expect(context.mocks.child_process.executionHistory.length).to.equal(1); expect(context.mocks.child_process.executionHistory.length).to.equal(1);
@@ -109,7 +109,7 @@ describe('CodeRunner', () => {
context.mocks.path.setupJoinSequence('non-important-folder-name1', 'non-important-folder-name2'); context.mocks.path.setupJoinSequence('non-important-folder-name1', 'non-important-folder-name2');
// act // act
await context.runCodeAsync(); await context.runCode();
// assert // assert
const actualOrder = context.mocks.commandHistory.filter((command) => expectedOrder.includes(command)); const actualOrder = context.mocks.commandHistory.filter((command) => expectedOrder.includes(command));
@@ -126,9 +126,9 @@ class TestContext {
private fileExtension: string = 'fileExtension'; private fileExtension: string = 'fileExtension';
private env = mockEnvironment(OperatingSystem.Windows); private env = mockEnvironment(OperatingSystem.Windows);
public async runCodeAsync(): Promise<void> { public async runCode(): Promise<void> {
const runner = new CodeRunner(this.mocks, this.env); const runner = new CodeRunner(this.mocks, this.env);
await runner.runCodeAsync(this.code, this.folderName, this.fileExtension); await runner.runCode(this.code, this.folderName, this.fileExtension);
} }
public withOs(os: OperatingSystem) { public withOs(os: OperatingSystem) {
this.env = mockEnvironment(os); this.env = mockEnvironment(os);

View File

@@ -1,7 +1,7 @@
import 'mocha'; import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy'; import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
import { sleepAsync } from '@/infrastructure/Threading/AsyncSleep'; import { sleep } from '@/infrastructure/Threading/AsyncSleep';
describe('AsyncLazy', () => { describe('AsyncLazy', () => {
it('returns value from lambda', async () => { it('returns value from lambda', async () => {
@@ -10,7 +10,7 @@ describe('AsyncLazy', () => {
const lambda = () => Promise.resolve(expected); const lambda = () => Promise.resolve(expected);
const sut = new AsyncLazy(lambda); const sut = new AsyncLazy(lambda);
// act // act
const actual = await sut.getValueAsync(); const actual = await sut.getValue();
// assert // assert
expect(actual).to.equal(expected); expect(actual).to.equal(expected);
}); });
@@ -26,7 +26,7 @@ describe('AsyncLazy', () => {
}); });
const results = new Array<number>(); const results = new Array<number>();
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
results.push(await sut.getValueAsync()); results.push(await sut.getValue());
} }
// assert // assert
expect(totalExecuted).to.equal(1); expect(totalExecuted).to.equal(1);
@@ -35,16 +35,16 @@ describe('AsyncLazy', () => {
it('when running long-running task in parallel', async () => { it('when running long-running task in parallel', async () => {
// act // act
const sut = new AsyncLazy(async () => { const sut = new AsyncLazy(async () => {
await sleepAsync(100); await sleep(100);
totalExecuted++; totalExecuted++;
return Promise.resolve(totalExecuted); return Promise.resolve(totalExecuted);
}); });
const results = await Promise.all([ const results = await Promise.all([
sut.getValueAsync(), sut.getValue(),
sut.getValueAsync(), sut.getValue(),
sut.getValueAsync(), sut.getValue(),
sut.getValueAsync(), sut.getValue(),
sut.getValueAsync()]); sut.getValue()]);
// assert // assert
expect(totalExecuted).to.equal(1); expect(totalExecuted).to.equal(1);
expect(results).to.deep.equal([1, 1, 1, 1, 1]); expect(results).to.deep.equal([1, 1, 1, 1, 1]);

View File

@@ -1,33 +1,35 @@
import 'mocha'; import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { sleepAsync, SchedulerType } from '@/infrastructure/Threading/AsyncSleep'; import { sleep, SchedulerType } from '@/infrastructure/Threading/AsyncSleep';
describe('AsyncSleep', () => { describe('AsyncSleep', () => {
it('fulfills after delay', async () => { describe('sleep', () => {
// arrange it('fulfills after delay', async () => {
const delayInMs = 10; // arrange
const scheduler = new SchedulerMock(); const delayInMs = 10;
// act const scheduler = new SchedulerMock();
const sleep = sleepAsync(delayInMs, scheduler.mock); // act
const promiseState = watchPromiseState(sleep); const promise = sleep(delayInMs, scheduler.mock);
scheduler.tickNext(delayInMs); const promiseState = watchPromiseState(promise);
await flushPromiseResolutionQueue(); scheduler.tickNext(delayInMs);
// assert await flushPromiseResolutionQueue();
const actual = promiseState.isFulfilled(); // assert
expect(actual).to.equal(true); const actual = promiseState.isFulfilled();
}); expect(actual).to.equal(true);
it('pending before delay', async () => { });
// arrange it('pending before delay', async () => {
const delayInMs = 10; // arrange
const scheduler = new SchedulerMock(); const delayInMs = 10;
// act const scheduler = new SchedulerMock();
const sleep = sleepAsync(delayInMs, scheduler.mock); // act
const promiseState = watchPromiseState(sleep); const promise = sleep(delayInMs, scheduler.mock);
scheduler.tickNext(delayInMs / 5); const promiseState = watchPromiseState(promise);
await flushPromiseResolutionQueue(); scheduler.tickNext(delayInMs / 5);
// assert await flushPromiseResolutionQueue();
const actual = promiseState.isPending(); // assert
expect(actual).to.equal(true); const actual = promiseState.isPending();
expect(actual).to.equal(true);
});
}); });
}); });