Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd41af466f | ||
|
|
970221b996 | ||
|
|
15004ff1f1 | ||
|
|
65226f3984 | ||
|
|
b0a7d0b53b | ||
|
|
ee43fd92a0 | ||
|
|
cf39e6d254 | ||
|
|
1260eea690 | ||
|
|
45a3669443 | ||
|
|
c9b91f6d8f | ||
|
|
9a6b903b92 | ||
|
|
7661575573 | ||
|
|
f1abd7682f |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.9.2 (2021-02-13)
|
||||||
|
|
||||||
|
* do not compile with unused locals vuejs/vetur#1063 | [73e0520](https://github.com/undergroundwires/privacy.sexy/commit/73e0520de70cdbaf0ecdc6e9be5e85f003fcfb79)
|
||||||
|
* fix wrong path for NvTelemtry file in NVIDIA script | [34b8822](https://github.com/undergroundwires/privacy.sexy/commit/34b8822ac821acb47e483e21b57e380551bcf455)
|
||||||
|
* refactor event handling to consume base class for lifecycling | [f1e21ba](https://github.com/undergroundwires/privacy.sexy/commit/f1e21babbfaac21903594a37e30163bfe3338279)
|
||||||
|
* make compiler throw if a function call includes an unexpected parameter | [15353d0](https://github.com/undergroundwires/privacy.sexy/commit/15353d0e2513c89ee4ffd9d9c5e9e83ef69b96b6)
|
||||||
|
* refactor vscode configuration scripts using functions #41 | [67b2d1c](https://github.com/undergroundwires/privacy.sexy/commit/67b2d1c11cd5b131dff93a4437db79d96ed8b3dc)
|
||||||
|
* refactor state handling to make application available independent of the state | [df273f7](https://github.com/undergroundwires/privacy.sexy/commit/df273f7f635ab156ac51a8dfb3fec66c4979f1c4)
|
||||||
|
* add test to ensure correct shared functions are being parsed | [d7de420](https://github.com/undergroundwires/privacy.sexy/commit/d7de420d5c91bd9ce64880cd4a4391ad3a0a5401)
|
||||||
|
* refactor and add tests for NonCollapsingDirective | [5934b17](https://github.com/undergroundwires/privacy.sexy/commit/5934b1728328c3b2ece1597b74dd87477d162175)
|
||||||
|
* add GitHub issue templates | [daa997b](https://github.com/undergroundwires/privacy.sexy/commit/daa997b21b624d133c6f5e4cd6b70214588f9144)
|
||||||
|
* correct the typo in application.md (#60) | [575636e](https://github.com/undergroundwires/privacy.sexy/commit/575636e6b728a2bdd1a9bd72c57bbf2752f10887)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.9.1...0.9.2)
|
||||||
|
|
||||||
## 0.9.1 (2021-01-23)
|
## 0.9.1 (2021-01-23)
|
||||||
|
|
||||||
* in CI/CD, allow publishing to github if release is more than 2 hours old electron-userland/electron-builder#2074 | [cf907d0](https://github.com/undergroundwires/privacy.sexy/commit/cf907d029a6d80682ba78ec887a9c4fab639db51)
|
* in CI/CD, allow publishing to github if release is more than 2 hours old electron-userland/electron-builder#2074 | [cf907d0](https://github.com/undergroundwires/privacy.sexy/commit/cf907d029a6d80682ba78ec887a9c4fab639db51)
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -14,11 +14,13 @@
|
|||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- Online version: [https://privacy.sexy](https://privacy.sexy)
|
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
||||||
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.1/privacy.sexy-Setup-0.9.1.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.1/privacy.sexy-0.9.1.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.1/privacy.sexy-0.9.1.dmg)
|
- 💡 No need to run any compiled software on your computer.
|
||||||
- 💡 Come back regularly to apply latest version for stronger privacy and security.
|
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-Setup-0.9.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-0.9.2.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-0.9.2.AppImage).
|
||||||
|
- 💡 Single click to execute your script.
|
||||||
|
- ❗ Come back regularly to apply latest version for stronger privacy and security.
|
||||||
|
|
||||||
[](https://privacy.sexy)
|
[](https://privacy.sexy)
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
@@ -51,8 +53,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.9.1 .`
|
1. Build: `docker build -t undergroundwires/privacy.sexy:0.9.2 .`
|
||||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.9.1 undergroundwires/privacy.sexy:0.9.1`
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.9.2 undergroundwires/privacy.sexy:0.9.2`
|
||||||
|
|
||||||
## Architecture overview
|
## Architecture overview
|
||||||
|
|
||||||
|
|||||||
@@ -101,11 +101,15 @@
|
|||||||
### `Function`
|
### `Function`
|
||||||
|
|
||||||
- Functions allow re-usable code throughout the defined scripts.
|
- Functions allow re-usable code throughout the defined scripts.
|
||||||
- Functions are templates compiled by privacy.sexy and uses special expressions.
|
- Functions are templates compiled by privacy.sexy and uses special [expressions](#expressions).
|
||||||
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
- Functions can call other functions by defining `call` property instead of `code`
|
||||||
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
||||||
|
|
||||||
#### Parameter substitution
|
#### Expressions
|
||||||
|
|
||||||
|
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
||||||
|
|
||||||
|
##### Parameter substitution
|
||||||
|
|
||||||
A simple function example
|
A simple function example
|
||||||
|
|
||||||
@@ -125,6 +129,22 @@ It would print "Hello world" if it's called in a [script](#script) as following:
|
|||||||
argument: World
|
argument: World
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A function can call other functions such as:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
-
|
||||||
|
function: CallerFunction
|
||||||
|
parameters: [ 'value' ]
|
||||||
|
call:
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
argument: {{ $value }}
|
||||||
|
-
|
||||||
|
function: EchoArgument
|
||||||
|
parameters: [ 'argument' ]
|
||||||
|
code: Hello {{ $argument }} !
|
||||||
|
```
|
||||||
|
|
||||||
#### `Function` syntax
|
#### `Function` syntax
|
||||||
|
|
||||||
- `name`: *`string`* (**required**)
|
- `name`: *`string`* (**required**)
|
||||||
@@ -135,15 +155,20 @@ It would print "Hello world" if it's called in a [script](#script) as following:
|
|||||||
- `parameters`: `[` *`string`* `, ... ]`
|
- `parameters`: `[` *`string`* `, ... ]`
|
||||||
- Name of the parameters that the function has.
|
- Name of the parameters that the function has.
|
||||||
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#FunctionCall)
|
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#FunctionCall)
|
||||||
- Parameter names must be defined to be used in expressions such as [parameter substitution](#parameter-substitution)
|
- Parameter names must be defined to be used in [expressions](#expressions)
|
||||||
- ❗ Parameter names must be unique
|
- ❗ Parameter names must be unique
|
||||||
`code`: *`string`* (**required**)
|
`code`: *`string`* (**required** if `call` is undefined)
|
||||||
- Batch file commands that will be executed
|
- Batch file commands that will be executed
|
||||||
- 💡 If defined, best practice to also define `revertCode`
|
- 💡 If defined, best practice to also define `revertCode`
|
||||||
|
- ❗ If not defined `call` must be defined
|
||||||
- `revertCode`: *`string`*
|
- `revertCode`: *`string`*
|
||||||
- Code that'll undo the change done by `code` property.
|
- Code that'll undo the change done by `code` property.
|
||||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
|
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
||||||
|
- A shared function or sequence of functions to call (called in order)
|
||||||
|
- The parameter values that are sent can use [expressions](#expressions)
|
||||||
|
- ❗ If not defined `code` must be defined
|
||||||
|
|
||||||
### `ScriptingDefinition`
|
### `ScriptingDefinition`
|
||||||
|
|
||||||
|
|||||||
29
docs/tests.md
Normal file
29
docs/tests.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Unit tests
|
||||||
|
|
||||||
|
- Unit tests are defined in [`./tests`](./../tests)
|
||||||
|
- They follow same folder structure as [`./src`](./../src)
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
- Each test suite first describe the system under test
|
||||||
|
- E.g. tests for class `Application` is categorized under `Application`
|
||||||
|
- Tests for specific methods are categorized under method name (if applicable)
|
||||||
|
- E.g. test for `run()` is categorized under `run`
|
||||||
|
|
||||||
|
## Act, arrange, assert
|
||||||
|
|
||||||
|
- Tests use act, arrange and assert (AAA) pattern when applicable
|
||||||
|
- **Arrange**
|
||||||
|
- Should set up the test case
|
||||||
|
- Starts with comment line `// arrange`
|
||||||
|
- **Act**
|
||||||
|
- Should cover the main thing to be tested
|
||||||
|
- Starts with comment line `// act`
|
||||||
|
- **Assert**
|
||||||
|
- Should elicit some sort of response
|
||||||
|
- Starts with comment line `// assert`
|
||||||
|
|
||||||
|
## Stubs
|
||||||
|
|
||||||
|
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs)
|
||||||
|
- They implement dummy behavior to be functional
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 89 KiB |
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.9.1",
|
"version": "0.9.2",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.9.1",
|
"version": "0.9.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"inversify": "^5.0.5",
|
"inversify": "^5.0.5",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"v-tooltip": "2.0.2",
|
"v-tooltip": "2.0.2",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
"vue-class-component": "^7.2.6",
|
"vue-class-component": "^7.2.6",
|
||||||
|
|||||||
19
src/App.vue
19
src/App.vue
@@ -3,8 +3,7 @@
|
|||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<TheHeader class="row" />
|
<TheHeader class="row" />
|
||||||
<TheSearchBar class="row" />
|
<TheSearchBar class="row" />
|
||||||
<TheScripts class="row"/>
|
<TheScriptArea class="row" />
|
||||||
<TheCodeArea class="row" theme="xcode" />
|
|
||||||
<TheCodeButtons class="row code-buttons" />
|
<TheCodeButtons class="row code-buttons" />
|
||||||
<TheFooter />
|
<TheFooter />
|
||||||
</div>
|
</div>
|
||||||
@@ -15,17 +14,15 @@
|
|||||||
import { Component, Vue } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
import TheHeader from '@/presentation/TheHeader.vue';
|
import TheHeader from '@/presentation/TheHeader.vue';
|
||||||
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
||||||
import TheCodeArea from '@/presentation/TheCodeArea.vue';
|
import TheCodeButtons from '@/presentation/Code/CodeButtons/TheCodeButtons.vue';
|
||||||
import TheCodeButtons from '@/presentation/CodeButtons/TheCodeButtons.vue';
|
import TheScriptArea from '@/presentation/Scripts/TheScriptArea.vue';
|
||||||
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
||||||
import TheScripts from '@/presentation/Scripts/TheScripts.vue';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
TheHeader,
|
TheHeader,
|
||||||
TheCodeArea,
|
|
||||||
TheCodeButtons,
|
TheCodeButtons,
|
||||||
TheScripts,
|
TheScriptArea,
|
||||||
TheSearchBar,
|
TheSearchBar,
|
||||||
TheFooter,
|
TheFooter,
|
||||||
},
|
},
|
||||||
@@ -38,6 +35,7 @@ export default class App extends Vue {
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "@/presentation/styles/colors.scss";
|
@import "@/presentation/styles/colors.scss";
|
||||||
@import "@/presentation/styles/fonts.scss";
|
@import "@/presentation/styles/fonts.scss";
|
||||||
|
@import "@/presentation/styles/media.scss";
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -49,12 +47,10 @@ body {
|
|||||||
color: $slate;
|
color: $slate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
max-width: 1500px;
|
max-width: 1600px;
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
margin: 0% 2% 0% 2%;
|
margin: 0% 2% 0% 2%;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
@@ -62,18 +58,15 @@ body {
|
|||||||
padding: 2%;
|
padding: 2%;
|
||||||
display:flex;
|
display:flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-buttons {
|
.code-buttons {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@import "@/presentation/styles/tooltip.scss";
|
@import "@/presentation/styles/tooltip.scss";
|
||||||
@import "@/presentation/styles/tree.scss";
|
@import "@/presentation/styles/tree.scss";
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ export class BatchBuilder extends CodeBuilder {
|
|||||||
return '::';
|
return '::';
|
||||||
}
|
}
|
||||||
protected writeStandardOut(text: string): string {
|
protected writeStandardOut(text: string): string {
|
||||||
return `echo ${text}`;
|
return `echo ${escapeForEcho(text)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeForEcho(text: string) {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '^&')
|
||||||
|
.replace(/%/g, '%%');
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ export class ShellBuilder extends CodeBuilder {
|
|||||||
return '#';
|
return '#';
|
||||||
}
|
}
|
||||||
protected writeStandardOut(text: string): string {
|
protected writeStandardOut(text: string): string {
|
||||||
return `echo '${text}'`;
|
return `echo '${escapeForEcho(text)}'`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeForEcho(text: string) {
|
||||||
|
return text
|
||||||
|
.replace(/'/g, '\'\\\'\'');
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { IExpressionsCompiler, ParameterValueDictionary } from './IExpressionsCompiler';
|
||||||
|
import { generateIlCode, IILCode } from './ILCode';
|
||||||
|
|
||||||
|
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||||
|
public static readonly instance: IExpressionsCompiler = new ExpressionsCompiler();
|
||||||
|
protected constructor() { }
|
||||||
|
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string {
|
||||||
|
let intermediateCode = generateIlCode(code);
|
||||||
|
intermediateCode = substituteParameters(intermediateCode, parameters);
|
||||||
|
return intermediateCode.compile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function substituteParameters(intermediateCode: IILCode, parameters: ParameterValueDictionary): IILCode {
|
||||||
|
const parameterNames = intermediateCode.getUniqueParameterNames();
|
||||||
|
ensureValuesProvided(parameterNames, parameters);
|
||||||
|
for (const parameterName of parameterNames) {
|
||||||
|
const parameterValue = parameters[parameterName];
|
||||||
|
intermediateCode = intermediateCode.substituteParameter(parameterName, parameterValue);
|
||||||
|
}
|
||||||
|
return intermediateCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValuesProvided(names: string[], nameValues: ParameterValueDictionary) {
|
||||||
|
nameValues = nameValues || {};
|
||||||
|
const notProvidedNames = names.filter((name) => !Boolean(nameValues[name]));
|
||||||
|
if (notProvidedNames.length) {
|
||||||
|
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedNames)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printList(list: readonly string[]): string {
|
||||||
|
return `"${list.join('", "')}"`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface ParameterValueDictionary { [parameterName: string]: string; }
|
||||||
|
|
||||||
|
export interface IExpressionsCompiler {
|
||||||
|
compileExpressions(code: string, parameters?: ParameterValueDictionary): string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { FunctionData, InstructionHolder } from 'js-yaml-loader!*';
|
||||||
|
import { SharedFunction } from './SharedFunction';
|
||||||
|
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||||
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
|
import { IFunctionCompiler } from './IFunctionCompiler';
|
||||||
|
import { IFunctionCallCompiler } from '../FunctionCall/IFunctionCallCompiler';
|
||||||
|
import { FunctionCallCompiler } from '../FunctionCall/FunctionCallCompiler';
|
||||||
|
|
||||||
|
export class FunctionCompiler implements IFunctionCompiler {
|
||||||
|
public static readonly instance: IFunctionCompiler = new FunctionCompiler();
|
||||||
|
protected constructor(
|
||||||
|
private readonly functionCallCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance) {
|
||||||
|
}
|
||||||
|
public compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection {
|
||||||
|
const collection = new SharedFunctionCollection();
|
||||||
|
if (!functions || !functions.length) {
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
ensureValidFunctions(functions);
|
||||||
|
functions
|
||||||
|
.filter((func) => hasCode(func))
|
||||||
|
.forEach((func) => {
|
||||||
|
const shared = new SharedFunction(func.name, func.parameters, func.code, func.revertCode);
|
||||||
|
collection.addFunction(shared);
|
||||||
|
});
|
||||||
|
functions
|
||||||
|
.filter((func) => hasCall(func))
|
||||||
|
.forEach((func) => {
|
||||||
|
const code = this.functionCallCompiler.compileCall(func.call, collection);
|
||||||
|
const shared = new SharedFunction(func.name, func.parameters, code.code, code.revertCode);
|
||||||
|
collection.addFunction(shared);
|
||||||
|
});
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCode(data: FunctionData): boolean {
|
||||||
|
return Boolean(data.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCall(data: FunctionData): boolean {
|
||||||
|
return Boolean(data.call);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||||
|
ensureNoUndefinedItem(functions);
|
||||||
|
ensureNoDuplicatesInFunctionNames(functions);
|
||||||
|
ensureNoDuplicatesInParameterNames(functions);
|
||||||
|
ensureNoDuplicateCode(functions);
|
||||||
|
ensureEitherCallOrCodeIsDefined(functions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printList(list: readonly string[]): string {
|
||||||
|
return `"${list.join('","')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
|
||||||
|
// Ensure functions do not define both call and code
|
||||||
|
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
||||||
|
if (withBothCallAndCode.length) {
|
||||||
|
throw new Error(`both "code" and "call" are defined in ${printNames(withBothCallAndCode)}`);
|
||||||
|
}
|
||||||
|
// Ensure functions have either code or call
|
||||||
|
const hasEitherCodeOrCall = holders.filter((holder) => !hasCode(holder) && !hasCall(holder));
|
||||||
|
if (hasEitherCodeOrCall.length) {
|
||||||
|
throw new Error(`neither "code" or "call" is defined in ${printNames(hasEitherCodeOrCall)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function printNames(holders: readonly InstructionHolder[]) {
|
||||||
|
return printList(holders.map((holder) => holder.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
||||||
|
const duplicateFunctionNames = getDuplicates(functions
|
||||||
|
.map((func) => func.name.toLowerCase()));
|
||||||
|
if (duplicateFunctionNames.length) {
|
||||||
|
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
||||||
|
if (functions.some((func) => !func)) {
|
||||||
|
throw new Error(`some functions are undefined`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
|
||||||
|
const functionsWithParameters = functions
|
||||||
|
.filter((func) => func.parameters && func.parameters.length > 0);
|
||||||
|
for (const func of functionsWithParameters) {
|
||||||
|
const duplicateParameterNames = getDuplicates(func.parameters);
|
||||||
|
if (duplicateParameterNames.length) {
|
||||||
|
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||||
|
const duplicateCodes = getDuplicates(functions
|
||||||
|
.map((func) => func.code)
|
||||||
|
.filter((code) => code),
|
||||||
|
);
|
||||||
|
if (duplicateCodes.length > 0) {
|
||||||
|
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||||
|
}
|
||||||
|
const duplicateRevertCodes = getDuplicates(functions
|
||||||
|
.filter((func) => func.revertCode)
|
||||||
|
.map((func) => func.revertCode));
|
||||||
|
if (duplicateRevertCodes.length > 0) {
|
||||||
|
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDuplicates(texts: readonly string[]): string[] {
|
||||||
|
return texts.filter((item, index) => texts.indexOf(item) !== index);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { FunctionData } from 'js-yaml-loader!*';
|
||||||
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
|
|
||||||
|
export interface IFunctionCompiler {
|
||||||
|
compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export interface ISharedFunction {
|
||||||
|
readonly name: string;
|
||||||
|
readonly parameters?: readonly string[];
|
||||||
|
readonly code: string;
|
||||||
|
readonly revertCode?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ISharedFunction } from './ISharedFunction';
|
||||||
|
|
||||||
|
export interface ISharedFunctionCollection {
|
||||||
|
getFunctionByName(name: string): ISharedFunction;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ISharedFunction } from './ISharedFunction';
|
||||||
|
|
||||||
|
export class SharedFunction implements ISharedFunction {
|
||||||
|
constructor(
|
||||||
|
public readonly name: string,
|
||||||
|
public readonly parameters: readonly string[],
|
||||||
|
public readonly code: string,
|
||||||
|
public readonly revertCode: string,
|
||||||
|
) {
|
||||||
|
if (!name) { throw new Error('undefined function name'); }
|
||||||
|
if (!code) { throw new Error(`undefined function ("${name}") code`); }
|
||||||
|
this.parameters = parameters || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ISharedFunction } from './ISharedFunction';
|
||||||
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
|
|
||||||
|
export class SharedFunctionCollection implements ISharedFunctionCollection {
|
||||||
|
private readonly functionsByName = new Map<string, ISharedFunction>();
|
||||||
|
|
||||||
|
public addFunction(func: ISharedFunction): void {
|
||||||
|
if (!func) { throw new Error('undefined function'); }
|
||||||
|
if (this.functionsByName.has(func.name)) {
|
||||||
|
throw new Error(`function with name ${func.name} already exists`);
|
||||||
|
}
|
||||||
|
this.functionsByName.set(func.name, func);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFunctionByName(name: string): ISharedFunction {
|
||||||
|
if (!name) { throw Error('undefined function name'); }
|
||||||
|
const func = this.functionsByName.get(name);
|
||||||
|
if (!func) {
|
||||||
|
throw new Error(`called function is not defined "${name}"`);
|
||||||
|
}
|
||||||
|
return func;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { FunctionCallData, FunctionCallParametersData, FunctionData, ScriptFunctionCallData } from 'js-yaml-loader!*';
|
||||||
|
import { ICompiledCode } from './ICompiledCode';
|
||||||
|
import { ISharedFunctionCollection } from '../Function/ISharedFunctionCollection';
|
||||||
|
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||||
|
import { IExpressionsCompiler } from '../Expressions/IExpressionsCompiler';
|
||||||
|
import { ExpressionsCompiler } from '../Expressions/ExpressionsCompiler';
|
||||||
|
|
||||||
|
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||||
|
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||||
|
protected constructor(
|
||||||
|
private readonly expressionsCompiler: IExpressionsCompiler = ExpressionsCompiler.instance) { }
|
||||||
|
public compileCall(
|
||||||
|
call: ScriptFunctionCallData,
|
||||||
|
functions: ISharedFunctionCollection): ICompiledCode {
|
||||||
|
if (!functions) { throw new Error('undefined functions'); }
|
||||||
|
if (!call) { throw new Error('undefined call'); }
|
||||||
|
const compiledCodes = new Array<ICompiledCode>();
|
||||||
|
const calls = getCallSequence(call);
|
||||||
|
calls.forEach((currentCall, currentCallIndex) => {
|
||||||
|
ensureValidCall(currentCall);
|
||||||
|
const commonFunction = functions.getFunctionByName(currentCall.function);
|
||||||
|
ensureExpectedParameters(commonFunction, currentCall);
|
||||||
|
let functionCode = compileCode(commonFunction, currentCall.parameters, this.expressionsCompiler);
|
||||||
|
if (currentCallIndex !== calls.length - 1) {
|
||||||
|
functionCode = appendLine(functionCode);
|
||||||
|
}
|
||||||
|
compiledCodes.push(functionCode);
|
||||||
|
});
|
||||||
|
const compiledCode = merge(compiledCodes);
|
||||||
|
return compiledCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureExpectedParameters(func: FunctionData, call: FunctionCallData) {
|
||||||
|
if (!func.parameters && !call.parameters) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const unexpectedParameters = Object.keys(call.parameters || {})
|
||||||
|
.filter((callParam) => !func.parameters.includes(callParam));
|
||||||
|
if (unexpectedParameters.length) {
|
||||||
|
throw new Error(
|
||||||
|
`function "${func.name}" has unexpected parameter(s) provided: "${unexpectedParameters.join('", "')}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge(codes: readonly ICompiledCode[]): ICompiledCode {
|
||||||
|
return {
|
||||||
|
code: codes.map((code) => code.code).join(''),
|
||||||
|
revertCode: codes.map((code) => code.revertCode).join(''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileCode(
|
||||||
|
func: FunctionData,
|
||||||
|
parameters: FunctionCallParametersData,
|
||||||
|
compiler: IExpressionsCompiler): ICompiledCode {
|
||||||
|
return {
|
||||||
|
code: compiler.compileExpressions(func.code, parameters),
|
||||||
|
revertCode: compiler.compileExpressions(func.revertCode, parameters),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCallSequence(call: ScriptFunctionCallData): FunctionCallData[] {
|
||||||
|
if (typeof call !== 'object') {
|
||||||
|
throw new Error('called function(s) must be an object');
|
||||||
|
}
|
||||||
|
if (call instanceof Array) {
|
||||||
|
return call as FunctionCallData[];
|
||||||
|
}
|
||||||
|
return [ call as FunctionCallData ];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidCall(call: FunctionCallData) {
|
||||||
|
if (!call) {
|
||||||
|
throw new Error(`undefined function call`);
|
||||||
|
}
|
||||||
|
if (!call.function) {
|
||||||
|
throw new Error(`empty function name called`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendLine(code: ICompiledCode): ICompiledCode {
|
||||||
|
const appendLineIfNotEmpty = (str: string) => str ? `${str}\n` : str;
|
||||||
|
return {
|
||||||
|
code: appendLineIfNotEmpty(code.code),
|
||||||
|
revertCode: appendLineIfNotEmpty(code.revertCode),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ICompiledCode {
|
||||||
|
readonly code: string;
|
||||||
|
readonly revertCode?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ScriptFunctionCallData } from 'js-yaml-loader!*';
|
||||||
|
import { ICompiledCode } from './ICompiledCode';
|
||||||
|
import { ISharedFunctionCollection } from '../Function/ISharedFunctionCollection';
|
||||||
|
|
||||||
|
export interface IFunctionCallCompiler {
|
||||||
|
compileCall(
|
||||||
|
call: ScriptFunctionCallData,
|
||||||
|
functions: ISharedFunctionCollection): ICompiledCode;
|
||||||
|
}
|
||||||
@@ -1,184 +1,42 @@
|
|||||||
import { generateIlCode, IILCode } from './ILCode';
|
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
import { ScriptData, FunctionData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
|
||||||
import { IScriptCompiler } from './IScriptCompiler';
|
import { IScriptCompiler } from './IScriptCompiler';
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||||
interface ICompiledCode {
|
import { IFunctionCallCompiler } from './FunctionCall/IFunctionCallCompiler';
|
||||||
readonly code: string;
|
import { FunctionCallCompiler } from './FunctionCall/FunctionCallCompiler';
|
||||||
readonly revertCode: string;
|
import { IFunctionCompiler } from './Function/IFunctionCompiler';
|
||||||
}
|
import { FunctionCompiler } from './Function/FunctionCompiler';
|
||||||
|
|
||||||
export class ScriptCompiler implements IScriptCompiler {
|
export class ScriptCompiler implements IScriptCompiler {
|
||||||
|
private readonly functions: ISharedFunctionCollection;
|
||||||
constructor(
|
constructor(
|
||||||
private readonly functions: readonly FunctionData[] | undefined,
|
functions: readonly FunctionData[] | undefined,
|
||||||
private syntax: ILanguageSyntax) {
|
private readonly syntax: ILanguageSyntax,
|
||||||
ensureValidFunctions(functions);
|
functionCompiler: IFunctionCompiler = FunctionCompiler.instance,
|
||||||
|
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||||
|
) {
|
||||||
if (!syntax) { throw new Error('undefined syntax'); }
|
if (!syntax) { throw new Error('undefined syntax'); }
|
||||||
|
this.functions = functionCompiler.compileFunctions(functions);
|
||||||
}
|
}
|
||||||
public canCompile(script: ScriptData): boolean {
|
public canCompile(script: ScriptData): boolean {
|
||||||
|
if (!script) { throw new Error('undefined script'); }
|
||||||
if (!script.call) {
|
if (!script.call) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
public compile(script: ScriptData): IScriptCode {
|
public compile(script: ScriptData): IScriptCode {
|
||||||
this.ensureCompilable(script.call);
|
if (!script) { throw new Error('undefined script'); }
|
||||||
const compiledCodes = new Array<ICompiledCode>();
|
try {
|
||||||
const calls = getCallSequence(script.call);
|
const compiledCode = this.callCompiler.compileCall(script.call, this.functions);
|
||||||
calls.forEach((currentCall, currentCallIndex) => {
|
return new ScriptCode(
|
||||||
ensureValidCall(currentCall, script.name);
|
compiledCode.code,
|
||||||
const commonFunction = this.getFunctionByName(currentCall.function);
|
compiledCode.revertCode,
|
||||||
ensureExpectedParameters(commonFunction, currentCall);
|
this.syntax);
|
||||||
let functionCode = compileCode(commonFunction, currentCall.parameters);
|
} catch (error) {
|
||||||
if (currentCallIndex !== calls.length - 1) {
|
throw Error(`Script "${script.name}" ${error.message}`);
|
||||||
functionCode = appendLine(functionCode);
|
|
||||||
}
|
|
||||||
compiledCodes.push(functionCode);
|
|
||||||
});
|
|
||||||
const scriptCode = merge(compiledCodes);
|
|
||||||
return new ScriptCode(scriptCode.code, scriptCode.revertCode, script.name, this.syntax);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFunctionByName(name: string): FunctionData {
|
|
||||||
const func = this.functions.find((f) => f.name === name);
|
|
||||||
if (!func) {
|
|
||||||
throw new Error(`called function is not defined "${name}"`);
|
|
||||||
}
|
|
||||||
return func;
|
|
||||||
}
|
|
||||||
private ensureCompilable(call: ScriptFunctionCallData) {
|
|
||||||
if (!this.functions || this.functions.length === 0) {
|
|
||||||
throw new Error('cannot compile without shared functions');
|
|
||||||
}
|
|
||||||
if (typeof call !== 'object') {
|
|
||||||
throw new Error('called function(s) must be an object');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureExpectedParameters(func: FunctionData, call: FunctionCallData) {
|
|
||||||
if (!func.parameters && !call.parameters) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const unexpectedParameters = Object.keys(call.parameters || {})
|
|
||||||
.filter((callParam) => !func.parameters.includes(callParam));
|
|
||||||
if (unexpectedParameters.length) {
|
|
||||||
throw new Error(
|
|
||||||
`function "${func.name}" has unexpected parameter(s) provided: "${unexpectedParameters.join('", "')}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDuplicates(texts: readonly string[]): string[] {
|
|
||||||
return texts.filter((item, index) => texts.indexOf(item) !== index);
|
|
||||||
}
|
|
||||||
|
|
||||||
function printList(list: readonly string[]): string {
|
|
||||||
return `"${list.join('","')}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
|
||||||
const duplicateFunctionNames = getDuplicates(functions
|
|
||||||
.map((func) => func.name.toLowerCase()));
|
|
||||||
if (duplicateFunctionNames.length) {
|
|
||||||
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
|
||||||
if (functions.some((func) => !func)) {
|
|
||||||
throw new Error(`some functions are undefined`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
|
|
||||||
const functionsWithParameters = functions
|
|
||||||
.filter((func) => func.parameters && func.parameters.length > 0);
|
|
||||||
for (const func of functionsWithParameters) {
|
|
||||||
const duplicateParameterNames = getDuplicates(func.parameters);
|
|
||||||
if (duplicateParameterNames.length) {
|
|
||||||
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
|
||||||
const duplicateCodes = getDuplicates(functions.map((func) => func.code));
|
|
||||||
if (duplicateCodes.length > 0) {
|
|
||||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
|
||||||
}
|
|
||||||
const duplicateRevertCodes = getDuplicates(functions
|
|
||||||
.filter((func) => func.revertCode)
|
|
||||||
.map((func) => func.revertCode));
|
|
||||||
if (duplicateRevertCodes.length > 0) {
|
|
||||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
|
||||||
if (!functions || functions.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ensureNoUndefinedItem(functions);
|
|
||||||
ensureNoDuplicatesInFunctionNames(functions);
|
|
||||||
ensureNoDuplicatesInParameterNames(functions);
|
|
||||||
ensureNoDuplicateCode(functions);
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendLine(code: ICompiledCode): ICompiledCode {
|
|
||||||
const appendLineIfNotEmpty = (str: string) => str ? `${str}\n` : str;
|
|
||||||
return {
|
|
||||||
code: appendLineIfNotEmpty(code.code),
|
|
||||||
revertCode: appendLineIfNotEmpty(code.revertCode),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function merge(codes: readonly ICompiledCode[]): ICompiledCode {
|
|
||||||
return {
|
|
||||||
code: codes.map((code) => code.code).join(''),
|
|
||||||
revertCode: codes.map((code) => code.revertCode).join(''),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function compileCode(func: FunctionData, parameters: FunctionCallParametersData): ICompiledCode {
|
|
||||||
return {
|
|
||||||
code: compileExpressions(func.code, parameters),
|
|
||||||
revertCode: compileExpressions(func.revertCode, parameters),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function compileExpressions(code: string, parameters: FunctionCallParametersData): string {
|
|
||||||
let intermediateCode = generateIlCode(code);
|
|
||||||
intermediateCode = substituteParameters(intermediateCode, parameters);
|
|
||||||
return intermediateCode.compile();
|
|
||||||
}
|
|
||||||
|
|
||||||
function substituteParameters(intermediateCode: IILCode, parameters: FunctionCallParametersData): IILCode {
|
|
||||||
const parameterNames = intermediateCode.getUniqueParameterNames();
|
|
||||||
if (parameterNames.length && !parameters) {
|
|
||||||
throw new Error(`no parameters defined, expected: ${printList(parameterNames)}`);
|
|
||||||
}
|
|
||||||
for (const parameterName of parameterNames) {
|
|
||||||
const parameterValue = parameters[parameterName];
|
|
||||||
if (!parameterValue) {
|
|
||||||
throw Error(`parameter value is not provided for "${parameterName}" in function call`);
|
|
||||||
}
|
|
||||||
intermediateCode = intermediateCode.substituteParameter(parameterName, parameterValue);
|
|
||||||
}
|
|
||||||
return intermediateCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureValidCall(call: FunctionCallData, scriptName: string) {
|
|
||||||
if (!call) {
|
|
||||||
throw new Error(`undefined function call in script "${scriptName}"`);
|
|
||||||
}
|
|
||||||
if (!call.function) {
|
|
||||||
throw new Error(`empty function name called in script "${scriptName}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCallSequence(call: ScriptFunctionCallData): FunctionCallData[] {
|
|
||||||
if (call instanceof Array) {
|
|
||||||
return call as FunctionCallData[];
|
|
||||||
}
|
|
||||||
return [ call as FunctionCallData ];
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function parseCode(script: ScriptData, context: ICategoryCollectionParseContext)
|
|||||||
if (context.compiler.canCompile(script)) {
|
if (context.compiler.canCompile(script)) {
|
||||||
return context.compiler.compile(script);
|
return context.compiler.compile(script);
|
||||||
}
|
}
|
||||||
return new ScriptCode(script.code, script.revertCode, script.name, context.syntax);
|
return new ScriptCode(script.code, script.revertCode, context.syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNotBothCallAndCode(script: ScriptData) {
|
function ensureNotBothCallAndCode(script: ScriptData) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { createEnumParser } from '../Common/Enum';
|
import { createEnumParser } from '../Common/Enum';
|
||||||
import { generateIlCode } from './Script/Compiler/ILCode';
|
import { generateIlCode } from './Script/Compiler/Expressions/ILCode';
|
||||||
|
|
||||||
export function parseScriptingDefinition(
|
export function parseScriptingDefinition(
|
||||||
definition: ScriptingDefinitionData,
|
definition: ScriptingDefinitionData,
|
||||||
|
|||||||
31
src/application/collections/collection.yaml.d.ts
vendored
31
src/application/collections/collection.yaml.d.ts
vendored
@@ -18,30 +18,33 @@ declare module 'js-yaml-loader!*' {
|
|||||||
readonly docs?: DocumentationUrlsData;
|
readonly docs?: DocumentationUrlsData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionData {
|
export interface InstructionHolder {
|
||||||
name: string;
|
readonly name: string;
|
||||||
code: string;
|
|
||||||
revertCode?: string;
|
readonly code?: string;
|
||||||
parameters?: readonly string[];
|
readonly revertCode?: string;
|
||||||
|
|
||||||
|
readonly call?: ScriptFunctionCallData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionData extends InstructionHolder {
|
||||||
|
readonly parameters?: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionCallParametersData {
|
export interface FunctionCallParametersData {
|
||||||
[index: string]: string;
|
readonly [index: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionCallData {
|
export interface FunctionCallData {
|
||||||
function: string;
|
readonly function: string;
|
||||||
parameters?: FunctionCallParametersData;
|
readonly parameters?: FunctionCallParametersData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScriptFunctionCallData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
export type ScriptFunctionCallData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
||||||
|
|
||||||
export interface ScriptData extends DocumentableData {
|
export interface ScriptData extends InstructionHolder, DocumentableData {
|
||||||
name: string;
|
readonly name: string;
|
||||||
code?: string;
|
readonly recommend?: string;
|
||||||
revertCode?: string;
|
|
||||||
call: ScriptFunctionCallData;
|
|
||||||
recommend?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptingDefinitionData {
|
export interface ScriptingDefinitionData {
|
||||||
|
|||||||
@@ -39,6 +39,248 @@ actions:
|
|||||||
sudo rm -rfv /var/spool/cups/c0*
|
sudo rm -rfv /var/spool/cups/c0*
|
||||||
sudo rm -rfv /var/spool/cups/tmp/*
|
sudo rm -rfv /var/spool/cups/tmp/*
|
||||||
sudo rm -rfv /var/spool/cups/cache/job.cache*
|
sudo rm -rfv /var/spool/cups/cache/job.cache*
|
||||||
|
-
|
||||||
|
name: Empty trash on all volumes
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
# on all mounted volumes
|
||||||
|
sudo rm -rfv /Volumes/*/.Trashes/* &>/dev/null
|
||||||
|
# on main HDD
|
||||||
|
sudo rm -rfv ~/.Trash/* &>/dev/null
|
||||||
|
-
|
||||||
|
name: Clear system cache files
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
sudo rm -rfv /Library/Caches/* &>/dev/null
|
||||||
|
sudo rm -rfv /System/Library/Caches/* &>/dev/null
|
||||||
|
sudo rm -rfv ~/Library/Caches/* &>/dev/null
|
||||||
|
-
|
||||||
|
name: Clear system log files
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
sudo rm -rfv /private/var/log/asl/*.asl &>/dev/null
|
||||||
|
sudo rm -rfv /Library/Logs/DiagnosticReports/* &>/dev/null
|
||||||
|
sudo rm -rfv /Library/Logs/Adobe/* &>/dev/null
|
||||||
|
rm -rfv ~/Library/Containers/com.apple.mail/Data/Library/Logs/Mail/* &>/dev/null
|
||||||
|
rm -rfv ~/Library/Logs/CoreSimulator/* &>/dev/null
|
||||||
|
sudo rm -rfv /var/log/*
|
||||||
|
-
|
||||||
|
category: Clear browser history
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
category: Clear Google Chrome history
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Clear Google Chrome browsing history
|
||||||
|
code: |-
|
||||||
|
rm -rfv ~/Library/Application\ Support/Google/Chrome/Default/History &>/dev/null
|
||||||
|
rm -rfv ~/Library/Application\ Support/Google/Chrome/Default/History-journal &>/dev/null
|
||||||
|
-
|
||||||
|
name: Google Chrome Cache Files
|
||||||
|
code: sudo rm -rfv ~/Library/Application\ Support/Google/Chrome/Default/Application\ Cache/* &>/dev/null
|
||||||
|
-
|
||||||
|
category: Clear Safari history
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Clear Safari browsing history
|
||||||
|
code: |-
|
||||||
|
rm -f ~/Library/Safari/History.plist
|
||||||
|
rm -f ~/Library/Safari/HistoryIndex.sk
|
||||||
|
-
|
||||||
|
name: Clear Safari downloads history
|
||||||
|
code: rm -f ~/Library/Safari/Downloads.plist
|
||||||
|
-
|
||||||
|
name: Clear Safari top sites
|
||||||
|
code: rm -f ~/Library/Safari/TopSites.plist
|
||||||
|
-
|
||||||
|
name: Clear Safari last session history
|
||||||
|
code: rm -f ~/Library/Safari/LastSession.plist
|
||||||
|
-
|
||||||
|
name: Clear Safari caches
|
||||||
|
code: |-
|
||||||
|
rm -f ~/Library/Caches/com.apple.Safari/Cache.db
|
||||||
|
rm -f ~/Library/Safari/WebpageIcons.db
|
||||||
|
rm -rf ~/Library/Caches/com.apple.Safari/Webpage Previews
|
||||||
|
-
|
||||||
|
name: Clear copy of the Safari history
|
||||||
|
code: rm -rf ~/Library/Caches/Metadata/Safari/History
|
||||||
|
-
|
||||||
|
name: Clear search history embedded in Safari preferences
|
||||||
|
code: defaults write ~/Library/Preferences/com.apple.Safari RecentSearchStrings '( )'
|
||||||
|
-
|
||||||
|
name: Clear Safari cookies
|
||||||
|
code: rm -f ~/Library/Cookies/Cookies.plists
|
||||||
|
-
|
||||||
|
name: Clear Safari zoom level preferences per site
|
||||||
|
code: rm -f ~/Library/Safari/PerSiteZoomPreferences.plists
|
||||||
|
-
|
||||||
|
name: Clear URLs that are allowed to display notifications in Safari
|
||||||
|
code: rm -f ~/Library/Safari/UserNotificationPreferences.plist
|
||||||
|
-
|
||||||
|
name: Clear Safari per-site preferences for Downloads, Geolocation, PopUps, and Autoplays
|
||||||
|
code: rm -f ~/Library/Safari/PerSitePreferences.db
|
||||||
|
-
|
||||||
|
category: Clear Firefox history
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Clear Firefox cache
|
||||||
|
code: |-
|
||||||
|
sudo rm -rf ~/Library/Caches/Mozilla/
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/netpredictions.sqlite
|
||||||
|
-
|
||||||
|
name: Delete Firefox form history
|
||||||
|
code: |-
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/formhistory.sqlite
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/formhistory.dat
|
||||||
|
-
|
||||||
|
name: Delete Firefox site preferences
|
||||||
|
code: rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/content-prefs.sqlite
|
||||||
|
-
|
||||||
|
name: Delete Firefox session restore data (loads after the browser closes or crashes)
|
||||||
|
code: |-
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionCheckpoints.json
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore*.js*
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore.bak*
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore-backups/previous.js*
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore-backups/recovery.js*
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore-backups/recovery.bak*
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore-backups/previous.bak*
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore-backups/upgrade.js*-20*
|
||||||
|
-
|
||||||
|
name: Delete Firefox passwords
|
||||||
|
docs: http://kb.mozillazine.org/Password_Manager
|
||||||
|
code: |-
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/signons.txt
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/signons2.txt
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/signons3.txt
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/signons.sqlite
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/logins.json
|
||||||
|
-
|
||||||
|
name: Delete Firefox HTML5 cookies
|
||||||
|
code: rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/webappsstore.sqlite
|
||||||
|
-
|
||||||
|
name: Delete Firefox crash reports
|
||||||
|
code: |-
|
||||||
|
rm -rfv ~/Library/Application\ Support/Firefox/Crash\ Reports/
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/minidumps/*.dmp
|
||||||
|
-
|
||||||
|
name: Delete Firefox backup files
|
||||||
|
code: |-
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/bookmarkbackups/*.json
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/bookmarkbackups/*.jsonlz4
|
||||||
|
-
|
||||||
|
name: Delete Firefox cookies
|
||||||
|
code: |-
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/cookies.txt
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/cookies.sqlite
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/cookies.sqlite-shm
|
||||||
|
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/cookies.sqlite-wal
|
||||||
|
rm -rfv ~/Library/Application\ Support/Firefox/Profiles/*/storage/default/http*
|
||||||
|
-
|
||||||
|
category: Clear third party application data
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Clear Adobe cache
|
||||||
|
recommend: standard
|
||||||
|
code: sudo rm -rfv ~/Library/Application\ Support/Adobe/Common/Media\ Cache\ Files/* &>/dev/null
|
||||||
|
-
|
||||||
|
name: Clear Gradle cache
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
if [ -d "/Users/${HOST}/.gradle/caches" ]; then
|
||||||
|
rm -rfv ~/.gradle/caches/ &> /dev/null
|
||||||
|
fi
|
||||||
|
-
|
||||||
|
name: Clear Dropbox cache
|
||||||
|
recommend: standard
|
||||||
|
code: |-
|
||||||
|
if [ -d "/Users/${HOST}/Dropbox" ]; then
|
||||||
|
sudo rm -rfv ~/Dropbox/.dropbox.cache/* &>/dev/null
|
||||||
|
fi
|
||||||
|
-
|
||||||
|
name: Clear Google Drive file stream cache
|
||||||
|
recommend: standard
|
||||||
|
code: |-
|
||||||
|
killall "Google Drive File Stream"
|
||||||
|
rm -rfv ~/Library/Application\ Support/Google/DriveFS/[0-9a-zA-Z]*/content_cache &>/dev/null
|
||||||
|
-
|
||||||
|
name: Clear Composer cache
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
if type "composer" &> /dev/null; then
|
||||||
|
composer clearcache &> /dev/null
|
||||||
|
fi
|
||||||
|
-
|
||||||
|
name: Clear Homebrew cache
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
if type "brew" &>/dev/null; then
|
||||||
|
brew cleanup -s &>/dev/null
|
||||||
|
rm -rfv $(brew --cache) &>/dev/null
|
||||||
|
brew tap --repair &>/dev/null
|
||||||
|
fi
|
||||||
|
-
|
||||||
|
name: Clear any old versions of Ruby gems
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
if type "gem" &> /dev/null; then
|
||||||
|
gem cleanup &>/dev/null
|
||||||
|
fi
|
||||||
|
-
|
||||||
|
name: Clear Docker
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
if type "docker" &> /dev/null; then
|
||||||
|
docker system prune -af
|
||||||
|
fi
|
||||||
|
-
|
||||||
|
name: Clear Pyenv-VirtualEnv cache
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
if [ "$PYENV_VIRTUALENV_CACHE_PATH" ]; then
|
||||||
|
rm -rfv $PYENV_VIRTUALENV_CACHE_PATH &>/dev/null
|
||||||
|
fi
|
||||||
|
-
|
||||||
|
name: Clear NPM cache
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
if type "npm" &> /dev/null; then
|
||||||
|
npm cache clean --force
|
||||||
|
fi
|
||||||
|
-
|
||||||
|
name: Clear Yarn cache
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
if type "yarn" &> /dev/null; then
|
||||||
|
echo 'Cleanup Yarn Cache...'
|
||||||
|
yarn cache clean --force
|
||||||
|
fi
|
||||||
|
-
|
||||||
|
category: iOS Cleanup
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Clear iOS applications
|
||||||
|
recommend: strict
|
||||||
|
code: rm -rfv ~/Music/iTunes/iTunes\ Media/Mobile\ Applications/* &>/dev/null
|
||||||
|
-
|
||||||
|
name: Clear iOS photo caches
|
||||||
|
recommend: standard
|
||||||
|
code: rm -rf ~/Pictures/iPhoto\ Library/iPod\ Photo\ Cache/*
|
||||||
|
-
|
||||||
|
name: Remove iOS Device Backups
|
||||||
|
recommend: strict
|
||||||
|
code: rm -rfv ~/Library/Application\ Support/MobileSync/Backup/* &>/dev/null
|
||||||
|
-
|
||||||
|
name: Clear iOS Simulators
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
if type "xcrun" &>/dev/null; then
|
||||||
|
osascript -e 'tell application "com.apple.CoreSimulator.CoreSimulatorService" to quit'
|
||||||
|
osascript -e 'tell application "iOS Simulator" to quit'
|
||||||
|
osascript -e 'tell application "Simulator" to quit'
|
||||||
|
xcrun simctl shutdown all
|
||||||
|
xcrun simctl erase all
|
||||||
|
fi
|
||||||
-
|
-
|
||||||
name: Clear the list of iOS devices connected
|
name: Clear the list of iOS devices connected
|
||||||
recommend: strict
|
recommend: strict
|
||||||
@@ -49,8 +291,64 @@ actions:
|
|||||||
sudo defaults delete /Library/Preferences/com.apple.iPod.plist Devices
|
sudo defaults delete /Library/Preferences/com.apple.iPod.plist Devices
|
||||||
sudo rm -rfv /var/db/lockdown/*
|
sudo rm -rfv /var/db/lockdown/*
|
||||||
-
|
-
|
||||||
name: Reset privacy database (remove all permissions)
|
name: Clear XCode Derived Data and Archives
|
||||||
code: sudo tccutil reset All
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
rm -rfv ~/Library/Developer/Xcode/DerivedData/* &>/dev/null
|
||||||
|
rm -rfv ~/Library/Developer/Xcode/Archives/* &>/dev/null
|
||||||
|
rm -rfv ~/Library/Developer/Xcode/iOS Device Logs/* &>/dev/null
|
||||||
|
-
|
||||||
|
name: Clear DNS cache
|
||||||
|
recommend: standard
|
||||||
|
code: |-
|
||||||
|
sudo dscacheutil -flushcache
|
||||||
|
sudo killall -HUP mDNSResponder
|
||||||
|
-
|
||||||
|
name: Purge inactive memory
|
||||||
|
recommend: standard
|
||||||
|
code: sudo purge
|
||||||
|
-
|
||||||
|
category: Reset privacy permissions for all applications
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Reset camera permissions
|
||||||
|
code: tccutil reset Camera
|
||||||
|
-
|
||||||
|
name: Reset microphone permissions
|
||||||
|
code: tccutil reset Microphone
|
||||||
|
-
|
||||||
|
name: Reset accessibility permissions
|
||||||
|
code: tccutil reset Accessibility
|
||||||
|
-
|
||||||
|
name: Reset screen capture permissions
|
||||||
|
code: tccutil reset ScreenCapture
|
||||||
|
-
|
||||||
|
name: Reset reminders permissions
|
||||||
|
code: tccutil reset Reminders
|
||||||
|
-
|
||||||
|
name: Reset photos permissions
|
||||||
|
code: tccutil reset Photos
|
||||||
|
-
|
||||||
|
name: Reset calendar permissions
|
||||||
|
code: tccutil reset Calendar
|
||||||
|
-
|
||||||
|
name: Reset full disk access permissions
|
||||||
|
code: tccutil reset SystemPolicyAllFiles
|
||||||
|
-
|
||||||
|
name: Reset contacts permissions
|
||||||
|
code: tccutil reset SystemPolicyAllFiles
|
||||||
|
-
|
||||||
|
name: Reset desktop folder permissions
|
||||||
|
code: tccutil reset SystemPolicyDesktopFolder
|
||||||
|
-
|
||||||
|
name: Reset documents folder permissions
|
||||||
|
code: tccutil reset SystemPolicyDocumentsFolder
|
||||||
|
-
|
||||||
|
name: Reset downloads permissions
|
||||||
|
code: tccutil reset SystemPolicyDownloadsFolder
|
||||||
|
-
|
||||||
|
name: Reset all app permissions
|
||||||
|
code: tccutil reset All
|
||||||
-
|
-
|
||||||
category: Configure programs
|
category: Configure programs
|
||||||
children:
|
children:
|
||||||
|
|||||||
@@ -476,26 +476,62 @@ actions:
|
|||||||
schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\KernelCeipTask" /ENABLE
|
schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\KernelCeipTask" /ENABLE
|
||||||
schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip" /ENABLE
|
schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip" /ENABLE
|
||||||
-
|
-
|
||||||
name: Disable Webcam Telemetry (devicecensus.exe)
|
category: Disable Webcam Telemetry (devicecensus.exe)
|
||||||
recommend: standard
|
docs:
|
||||||
docs: https://www.ghacks.net/2019/09/23/what-is-devicecensus-exe-on-windows-10-and-why-does-it-need-internet-connectivity/
|
- https://www.ghacks.net/2019/09/23/what-is-devicecensus-exe-on-windows-10-and-why-does-it-need-internet-connectivity/
|
||||||
code: schtasks /change /TN "Microsoft\Windows\Device Information\Device" /DISABLE
|
- https://answers.microsoft.com/en-us/windows/forum/windows_10-security/devicecensusexe-and-host-process-for-windows-task/520d42a2-45c1-402a-81de-e1116ecf2538
|
||||||
revertCode: schtasks /change /TN "Microsoft\Windows\Device Information\Device" /ENABLE
|
children:
|
||||||
-
|
-
|
||||||
name: Disable Application Experience (Compatibility Telemetry)
|
name: Disable devicecensus.exe (telemetry) task
|
||||||
recommend: standard
|
recommend: standard
|
||||||
code: |-
|
code: schtasks /change /TN "Microsoft\Windows\Device Information\Device" /disable
|
||||||
schtasks /change /TN "Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" /DISABLE
|
revertCode: schtasks /change /TN "Microsoft\Windows\Device Information\Device" /enable
|
||||||
schtasks /change /TN "Microsoft\Windows\Application Experience\ProgramDataUpdater" /DISABLE
|
-
|
||||||
schtasks /change /TN "Microsoft\Windows\Application Experience\StartupAppTask" /DISABLE
|
name: Disable devicecensus.exe (telemetry) process
|
||||||
schtasks /change /TN "Microsoft\Windows\Application Experience\AitAgent" /DISABLE
|
recommend: standard
|
||||||
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\CompatTelRunner.exe" /v "Debugger" /t REG_SZ /d "%windir%\System32\taskkill.exe" /f
|
call:
|
||||||
revertCode: |-
|
function: KillProcessWhenItStarts
|
||||||
schtasks /change /TN "Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" /ENABLE
|
parameters:
|
||||||
schtasks /change /TN "Microsoft\Windows\Application Experience\ProgramDataUpdater" /ENABLE
|
processName: DeviceCensus.exe
|
||||||
schtasks /change /TN "Microsoft\Windows\Application Experience\StartupAppTask" /ENABLE
|
-
|
||||||
schtasks /change /TN "Microsoft\Windows\Application Experience\AitAgent" /ENABLE
|
category: Disable Compatibility Telemetry (Application Experience)
|
||||||
reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\CompatTelRunner.exe" /v "Debugger" /f
|
children:
|
||||||
|
-
|
||||||
|
category: Disable Microsoft Compatibility Appraiser
|
||||||
|
docs: https://www.ghacks.net/2016/10/26/turn-off-the-windows-customer-experience-program/
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable Microsoft Compatibility Appraiser task
|
||||||
|
recommend: standard
|
||||||
|
code: schtasks /change /TN "Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" /disable
|
||||||
|
revertCode: schtasks /change /TN "Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" /enable
|
||||||
|
-
|
||||||
|
name: Disable CompatTelRunner.exe (Microsoft Compatibility Appraiser) process
|
||||||
|
recommend: standard
|
||||||
|
call:
|
||||||
|
function: KillProcessWhenItStarts
|
||||||
|
parameters:
|
||||||
|
processName: CompatTelRunner.exe
|
||||||
|
-
|
||||||
|
name: Disable sending information to Customer Experience Improvement Program
|
||||||
|
recommend: standard
|
||||||
|
docs:
|
||||||
|
- https://www.ghacks.net/2016/10/26/turn-off-the-windows-customer-experience-program/
|
||||||
|
- https://answers.microsoft.com/en-us/windows/forum/windows_10-performance/permanently-disabling-windows-compatibility/6bf71583-81b0-4a74-ae2e-8fd73305aad1
|
||||||
|
code: schtasks /change /TN "Microsoft\Windows\Application Experience\ProgramDataUpdater" /disable
|
||||||
|
revertCode: schtasks /change /TN "Microsoft\Windows\Application Experience\ProgramDataUpdater" /enable
|
||||||
|
-
|
||||||
|
name: Disable Application Impact Telemetry Agent task
|
||||||
|
recommend: standard
|
||||||
|
docs: https://www.shouldiblockit.com/aitagent.exe-6181.aspx
|
||||||
|
code: schtasks /change /TN "Microsoft\Windows\Application Experience\AitAgent" /disable
|
||||||
|
revertCode: schtasks /change /TN "Microsoft\Windows\Application Experience\AitAgent" /enable
|
||||||
|
-
|
||||||
|
name: Disable "Disable apps to improve performance" reminder
|
||||||
|
recommend: strict
|
||||||
|
docs: https://www.ghacks.net/2016/10/26/turn-off-the-windows-customer-experience-program/
|
||||||
|
code: schtasks /change /TN "Microsoft\Windows\Application Experience\StartupAppTask" /disable
|
||||||
|
revertCode: schtasks /change /TN "Microsoft\Windows\Application Experience\StartupAppTask" /enable
|
||||||
-
|
-
|
||||||
name: Disable telemetry in data collection policy
|
name: Disable telemetry in data collection policy
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -1001,6 +1037,34 @@ actions:
|
|||||||
recommend: standard
|
recommend: standard
|
||||||
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\bluetoothSync" /v "Value" /d "Deny" /t REG_SZ /f
|
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\bluetoothSync" /v "Value" /d "Deny" /t REG_SZ /f
|
||||||
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\bluetoothSync" /v "Value" /d "Allow" /t REG_SZ /f
|
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\bluetoothSync" /v "Value" /d "Allow" /t REG_SZ /f
|
||||||
|
-
|
||||||
|
category: Disable app access to voice activation
|
||||||
|
docs: https://www.tenforums.com/tutorials/130122-allow-deny-apps-access-use-voice-activation-windows-10-a.html
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable apps and Cortana to activate with voice
|
||||||
|
recommend: standard
|
||||||
|
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.AppPrivacy::LetAppsActivateWithVoice
|
||||||
|
code: |-
|
||||||
|
reg add "HKCU\Software\Microsoft\Speech_OneCore\Settings\VoiceActivation\UserPreferenceForAllApps" /v "AgentActivationEnabled" /t REG_DWORD /d 0 /f
|
||||||
|
:: Using GPO (re-activation through GUI is not possible)
|
||||||
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy" /v "LetAppsActivateWithVoice" /t REG_DWORD /d 2 /f
|
||||||
|
revertCode: |-
|
||||||
|
reg add "HKCU\Software\Microsoft\Speech_OneCore\Settings\VoiceActivation\UserPreferenceForAllApps" /v "AgentActivationEnabled" /t REG_DWORD /d 1 /f
|
||||||
|
:: Using GPO
|
||||||
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy" /v "LetAppsActivateWithVoice" /f
|
||||||
|
-
|
||||||
|
name: Disable apps and Cortana to activate with voice when sytem is locked
|
||||||
|
recommend: standard
|
||||||
|
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.AppPrivacy::LetAppsActivateWithVoiceAboveLock
|
||||||
|
code: |-
|
||||||
|
reg add "HKCU\Software\Microsoft\Speech_OneCore\Settings\VoiceActivation\UserPreferenceForAllApps" /v "AgentActivationOnLockScreenEnabled" /t REG_DWORD /d 0 /f
|
||||||
|
:: Using GPO (re-activation through GUI is not possible)
|
||||||
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy" /v "LetAppsActivateWithVoiceAboveLock" /t REG_DWORD /d 2 /f
|
||||||
|
revertCode: |-
|
||||||
|
reg add "HKCU\Software\Microsoft\Speech_OneCore\Settings\VoiceActivation\UserPreferenceForAllApps" /v "AgentActivationOnLockScreenEnabled" /t REG_DWORD /d 1 /f
|
||||||
|
:: Using GPO
|
||||||
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy" /v "LetAppsActivateWithVoiceAboveLock" /f
|
||||||
-
|
-
|
||||||
category: Disable location access
|
category: Disable location access
|
||||||
children:
|
children:
|
||||||
@@ -1081,11 +1145,61 @@ actions:
|
|||||||
revertCode: |-
|
revertCode: |-
|
||||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaEnabled" /t REG_DWORD /d 1 /f
|
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaEnabled" /t REG_DWORD /d 1 /f
|
||||||
reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaEnabled" /t REG_DWORD /d 1 /f
|
reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaEnabled" /t REG_DWORD /d 1 /f
|
||||||
|
-
|
||||||
|
category: Disable Cortana history
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Prevent Cortana from displaying history
|
||||||
|
recommend: standard
|
||||||
|
code: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "HistoryViewEnabled" /t REG_DWORD /d 0 /f
|
||||||
|
revertCode: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "HistoryViewEnabled" /f
|
||||||
|
-
|
||||||
|
name: Prevent Cortana from using device history
|
||||||
|
recommend: standard
|
||||||
|
code: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "DeviceHistoryEnabled" /t REG_DWORD /d 0 /f
|
||||||
|
revertCode: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "DeviceHistoryEnabled" /f
|
||||||
-
|
-
|
||||||
name: Remove the Cortana taskbar icon
|
name: Remove the Cortana taskbar icon
|
||||||
recommend: standard
|
recommend: standard
|
||||||
code: reg add HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced /v "ShowCortanaButton" /t REG_DWORD /d 0 /f
|
code: reg add HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced /v "ShowCortanaButton" /t REG_DWORD /d 0 /f
|
||||||
revertCode: reg delete HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced /v "ShowCortanaButton" /f
|
revertCode: reg delete HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced /v "ShowCortanaButton" /f
|
||||||
|
-
|
||||||
|
name: Disable Cortana in ambient mode
|
||||||
|
recommend: standard
|
||||||
|
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaInAmbientMode" /t REG_DWORD /d 0 /f
|
||||||
|
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaInAmbientMode" /t REG_DWORD /d 1 /f
|
||||||
|
-
|
||||||
|
category: Disable Cortana voice listening
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable "Hey Cortana" voice activation
|
||||||
|
recommend: standard
|
||||||
|
code: |-
|
||||||
|
reg add "HKCU\Software\Microsoft\Speech_OneCore\Preferences" /v "VoiceActivationOn" /t REG_DWORD /d 0 /f
|
||||||
|
reg add "HKLM\Software\Microsoft\Speech_OneCore\Preferences" /v "VoiceActivationDefaultOn" /t REG_DWORD /d 0 /f
|
||||||
|
revertCode: |-
|
||||||
|
reg add "HKCU\Software\Microsoft\Speech_OneCore\Preferences" /v "VoiceActivationOn" /t REG_DWORD /d 1 /f
|
||||||
|
reg add "HKLM\Software\Microsoft\Speech_OneCore\Preferences" /v "VoiceActivationDefaultOn" /t REG_DWORD /d 1 /f
|
||||||
|
-
|
||||||
|
name: Disable Cortana listening to commands on Windows key + C
|
||||||
|
recommend: standard
|
||||||
|
code: reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Search" /v "VoiceShortcut" /t REG_DWORD /d 0 /f
|
||||||
|
revertCode: reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Search" /v "VoiceShortcut" /t REG_DWORD /d 1 /f
|
||||||
|
-
|
||||||
|
name: Disable using Cortana even when device is locked
|
||||||
|
recommend: standard
|
||||||
|
code: reg add "HKCU\Software\Microsoft\Speech_OneCore\Preferences" /v "VoiceActivationEnableAboveLockscreen" /t REG_DWORD /d 0 /f
|
||||||
|
revertCode: reg add "HKCU\Software\Microsoft\Speech_OneCore\Preferences" /v "VoiceActivationEnableAboveLockscreen" /t REG_DWORD /d 1 /f
|
||||||
|
-
|
||||||
|
name: Disable automatic update of Speech Data
|
||||||
|
recommend: standard
|
||||||
|
code: reg add "HKCU\Software\Microsoft\Speech_OneCore\Preferences" /v "ModelDownloadAllowed" /t REG_DWORD /d 0 /f
|
||||||
|
revertCode: reg delete "HKCU\Software\Microsoft\Speech_OneCore\Preferences" /v "ModelDownloadAllowed" /f
|
||||||
|
-
|
||||||
|
name: Disable Cortana voice support during Windows setup
|
||||||
|
recommend: standard
|
||||||
|
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v "DisableVoice" /t REG_DWORD /d 1 /f
|
||||||
|
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v "DisableVoice" /f
|
||||||
-
|
-
|
||||||
category: Configure Windows search indexing
|
category: Configure Windows search indexing
|
||||||
children:
|
children:
|
||||||
@@ -2719,8 +2833,19 @@ actions:
|
|||||||
-
|
-
|
||||||
name: Disable NetBios for all interfaces
|
name: Disable NetBios for all interfaces
|
||||||
docs: https://10dsecurity.com/saying-goodbye-netbios/
|
docs: https://10dsecurity.com/saying-goodbye-netbios/
|
||||||
code: Powershell -Command "$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces'; Get-ChildItem $key | foreach { Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 2 -Verbose}"
|
call:
|
||||||
revertCode: Powershell -Command "$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces'; Get-ChildItem $key | foreach { Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 0 -Verbose}"
|
function: RunPowerShell
|
||||||
|
parameters:
|
||||||
|
code:
|
||||||
|
$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces';
|
||||||
|
Get-ChildItem $key | foreach {
|
||||||
|
Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 2 -Verbose
|
||||||
|
}
|
||||||
|
revertCode:
|
||||||
|
$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces';
|
||||||
|
Get-ChildItem $key | foreach {
|
||||||
|
Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 0 -Verbose
|
||||||
|
}
|
||||||
-
|
-
|
||||||
category: Remove bloatware
|
category: Remove bloatware
|
||||||
children:
|
children:
|
||||||
@@ -3486,12 +3611,13 @@ actions:
|
|||||||
function: UninstallSystemApp
|
function: UninstallSystemApp
|
||||||
parameters:
|
parameters:
|
||||||
packageName: Microsoft.Windows.SecureAssessmentBrowser
|
packageName: Microsoft.Windows.SecureAssessmentBrowser
|
||||||
-
|
# -
|
||||||
name: Start app
|
# # Not a bloatware, required for different setting windows such as WiFi and battery panes in action bar
|
||||||
call:
|
# name: Start app
|
||||||
function: UninstallSystemApp
|
# call:
|
||||||
parameters:
|
# function: UninstallSystemApp
|
||||||
packageName: Microsoft.Windows.ShellExperienceHost
|
# parameters:
|
||||||
|
# packageName: Microsoft.Windows.ShellExperienceHost
|
||||||
-
|
-
|
||||||
category: Windows Feedback
|
category: Windows Feedback
|
||||||
children:
|
children:
|
||||||
@@ -3523,12 +3649,13 @@ actions:
|
|||||||
function: UninstallSystemApp
|
function: UninstallSystemApp
|
||||||
parameters:
|
parameters:
|
||||||
packageName: Windows.ContactSupport
|
packageName: Windows.ContactSupport
|
||||||
-
|
# -
|
||||||
name: Settings app
|
# # Not a bloatware, required for core OS functinoality
|
||||||
call:
|
# name: Settings app
|
||||||
function: UninstallSystemApp
|
# call:
|
||||||
parameters:
|
# function: UninstallSystemApp
|
||||||
packageName: Windows.immersivecontrolpanel
|
# parameters:
|
||||||
|
# packageName: Windows.immersivecontrolpanel
|
||||||
-
|
-
|
||||||
name: Windows Print 3D app
|
name: Windows Print 3D app
|
||||||
call:
|
call:
|
||||||
@@ -4165,26 +4292,36 @@ actions:
|
|||||||
copy "%~dpnx0" "%AppData%\Microsoft\Windows\Start Menu\Programs\Startup\privacy-cleanup.bat"
|
copy "%~dpnx0" "%AppData%\Microsoft\Windows\Start Menu\Programs\Startup\privacy-cleanup.bat"
|
||||||
revertCode: del /f /q %AppData%\Microsoft\Windows\Start Menu\Programs\Startup\privacy-cleanup.bat
|
revertCode: del /f /q %AppData%\Microsoft\Windows\Start Menu\Programs\Startup\privacy-cleanup.bat
|
||||||
functions:
|
functions:
|
||||||
|
-
|
||||||
|
name: KillProcessWhenItStarts
|
||||||
|
parameters: [ processName ]
|
||||||
|
# https://docs.microsoft.com/en-us/previous-versions/windows/desktop/xperf/image-file-execution-options
|
||||||
|
code: reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\'{{ $processName }}'" /v "Debugger" /t REG_SZ /d "%windir%\System32\taskkill.exe" /f
|
||||||
|
revertCode: reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\'{{ $processName }}'" /v "Debugger" /f
|
||||||
-
|
-
|
||||||
name: UninstallStoreApp
|
name: UninstallStoreApp
|
||||||
parameters: [ packageName ]
|
parameters: [ packageName ]
|
||||||
code: PowerShell -Command "Get-AppxPackage '{{ $packageName }}' | Remove-AppxPackage"
|
call:
|
||||||
|
function: RunPowerShell
|
||||||
|
parameters:
|
||||||
|
code: Get-AppxPackage '{{ $packageName }}' | Remove-AppxPackage
|
||||||
revertCode:
|
revertCode:
|
||||||
PowerShell -ExecutionPolicy Unrestricted -Command "
|
|
||||||
$package = Get-AppxPackage -AllUsers '{{ $packageName }}';
|
$package = Get-AppxPackage -AllUsers '{{ $packageName }}';
|
||||||
if (!$package) {
|
if (!$package) {
|
||||||
Write-Error \"Cannot reinstall '{{ $packageName }}'\" -ErrorAction Stop
|
Write-Error \"Cannot reinstall '{{ $packageName }}'\" -ErrorAction Stop
|
||||||
}
|
}
|
||||||
$manifest = $package.InstallLocation + '\AppxManifest.xml';
|
$manifest = $package.InstallLocation + '\AppxManifest.xml';
|
||||||
Add-AppxPackage -DisableDevelopmentMode -Register \"$manifest\" "
|
Add-AppxPackage -DisableDevelopmentMode -Register \"$manifest\"
|
||||||
-
|
-
|
||||||
name: UninstallSystemApp
|
name: UninstallSystemApp
|
||||||
parameters: [ packageName ]
|
parameters: [ packageName ]
|
||||||
# It simply renames files
|
# It simply renames files
|
||||||
# Because system apps are non removable (check: (Get-AppxPackage -AllUsers 'Windows.CBSPreview').NonRemovable)
|
# Because system apps are non removable (check: (Get-AppxPackage -AllUsers 'Windows.CBSPreview').NonRemovable)
|
||||||
# Otherwise they throw 0x80070032 when trying to uninstall them
|
# Otherwise they throw 0x80070032 when trying to uninstall them
|
||||||
|
call:
|
||||||
|
function: RunPowerShell
|
||||||
|
parameters:
|
||||||
code:
|
code:
|
||||||
PowerShell -Command "
|
|
||||||
$package = (Get-AppxPackage -AllUsers '{{ $packageName }}');
|
$package = (Get-AppxPackage -AllUsers '{{ $packageName }}');
|
||||||
if (!$package) {
|
if (!$package) {
|
||||||
Write-Host 'Not installed';
|
Write-Host 'Not installed';
|
||||||
@@ -4202,9 +4339,8 @@ functions:
|
|||||||
Write-Host \"Rename '$($file.FullName)' to '$newName'\";
|
Write-Host \"Rename '$($file.FullName)' to '$newName'\";
|
||||||
Move-Item -LiteralPath \"$($file.FullName)\" -Destination \"$newName\" -Force;
|
Move-Item -LiteralPath \"$($file.FullName)\" -Destination \"$newName\" -Force;
|
||||||
}
|
}
|
||||||
};"
|
}
|
||||||
revertCode:
|
revertCode:
|
||||||
PowerShell -Command "
|
|
||||||
$package = (Get-AppxPackage -AllUsers '{{ $packageName }}');
|
$package = (Get-AppxPackage -AllUsers '{{ $packageName }}');
|
||||||
if (!$package) {
|
if (!$package) {
|
||||||
Write-Error 'App could not be found' -ErrorAction Stop;
|
Write-Error 'App could not be found' -ErrorAction Stop;
|
||||||
@@ -4220,12 +4356,17 @@ functions:
|
|||||||
Write-Host \"Rename '$($file.FullName)' to '$newName'\";
|
Write-Host \"Rename '$($file.FullName)' to '$newName'\";
|
||||||
Move-Item -LiteralPath \"$($file.FullName)\" -Destination \"$newName\" -Force;
|
Move-Item -LiteralPath \"$($file.FullName)\" -Destination \"$newName\" -Force;
|
||||||
}
|
}
|
||||||
};"
|
}
|
||||||
-
|
-
|
||||||
name: UninstallCapability
|
name: UninstallCapability
|
||||||
parameters: [ capabilityName ]
|
parameters: [ capabilityName ]
|
||||||
code: PowerShell -Command "Get-WindowsCapability -Online -Name '{{ $capabilityName }}*' | Remove-WindowsCapability -Online"
|
call:
|
||||||
revertCode: PowerShell -Command "$capability = Get-WindowsCapability -Online -Name '{{ $capabilityName }}*'; Add-WindowsCapability -Name \"$capability.Name\" -Online"
|
function: RunPowerShell
|
||||||
|
parameters:
|
||||||
|
code: Get-WindowsCapability -Online -Name '{{ $capabilityName }}*' | Remove-WindowsCapability -Online
|
||||||
|
revertCode:
|
||||||
|
$capability = Get-WindowsCapability -Online -Name '{{ $capabilityName }}*';
|
||||||
|
Add-WindowsCapability -Name \"$capability.Name\" -Online
|
||||||
-
|
-
|
||||||
name: RenameSystemFile
|
name: RenameSystemFile
|
||||||
parameters: [ filePath ]
|
parameters: [ filePath ]
|
||||||
@@ -4250,15 +4391,28 @@ functions:
|
|||||||
-
|
-
|
||||||
name: SetVsCodeSetting
|
name: SetVsCodeSetting
|
||||||
parameters: [ setting, powerShellValue ]
|
parameters: [ setting, powerShellValue ]
|
||||||
|
call:
|
||||||
|
function: RunPowerShell
|
||||||
|
parameters:
|
||||||
code:
|
code:
|
||||||
Powershell -Command "
|
|
||||||
$jsonfile = \"$env:APPDATA\Code\User\settings.json\";
|
$jsonfile = \"$env:APPDATA\Code\User\settings.json\";
|
||||||
|
if (!(Test-Path $jsonfile -PathType Leaf)) {
|
||||||
|
Write-Host \"No updates. Settings file was not at $jsonfile\";
|
||||||
|
exit 0;
|
||||||
|
}
|
||||||
$json = Get-Content $jsonfile | Out-String | ConvertFrom-Json;
|
$json = Get-Content $jsonfile | Out-String | ConvertFrom-Json;
|
||||||
$json | Add-Member -Type NoteProperty -Name '{{ $setting }}' -Value {{ $powerShellValue }} -Force;
|
$json | Add-Member -Type NoteProperty -Name '{{ $setting }}' -Value {{ $powerShellValue }} -Force;
|
||||||
$json | ConvertTo-Json | Set-Content $jsonfile;"
|
$json | ConvertTo-Json | Set-Content $jsonfile;
|
||||||
revertCode:
|
revertCode:
|
||||||
Powershell -Command "
|
|
||||||
$jsonfile = \"$env:APPDATA\Code\User\settings.json\";
|
$jsonfile = \"$env:APPDATA\Code\User\settings.json\";
|
||||||
|
if (!(Test-Path $jsonfile -PathType Leaf)) {
|
||||||
|
Write-Error \"Settings file could not be found at $jsonfile\" -ErrorAction Stop;
|
||||||
|
}
|
||||||
$json = Get-Content $jsonfile | ConvertFrom-Json;
|
$json = Get-Content $jsonfile | ConvertFrom-Json;
|
||||||
$json.PSObject.Properties.Remove('{{ $setting }}');
|
$json.PSObject.Properties.Remove('{{ $setting }}');
|
||||||
$json | ConvertTo-Json | Set-Content $jsonfile;"
|
$json | ConvertTo-Json | Set-Content $jsonfile;
|
||||||
|
-
|
||||||
|
name: RunPowerShell
|
||||||
|
parameters: [ code, revertCode ]
|
||||||
|
code: PowerShell -ExecutionPolicy Unrestricted -Command "{{ $code }}"
|
||||||
|
revertCode: PowerShell -ExecutionPolicy Unrestricted -Command "{{ $revertCode }}"
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ export class ScriptCode implements IScriptCode {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly execute: string,
|
public readonly execute: string,
|
||||||
public readonly revert: string,
|
public readonly revert: string,
|
||||||
scriptName: string,
|
|
||||||
syntax: ILanguageSyntax) {
|
syntax: ILanguageSyntax) {
|
||||||
if (!scriptName) { throw new Error('script name is undefined'); }
|
if (!syntax) { throw new Error('undefined syntax'); }
|
||||||
if (!syntax) { throw new Error('syntax is undefined'); }
|
validateCode(execute, syntax);
|
||||||
validateCode(scriptName, execute, syntax);
|
|
||||||
if (revert) {
|
if (revert) {
|
||||||
scriptName = `${scriptName} (revert)`;
|
try {
|
||||||
validateCode(scriptName, revert, syntax);
|
validateCode(revert, syntax);
|
||||||
if (execute === revert) {
|
if (execute === revert) {
|
||||||
throw new Error(`${scriptName}: Code itself and its reverting code cannot be the same`);
|
throw new Error(`Code itself and its reverting code cannot be the same`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw Error(`(revert): ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,21 +25,21 @@ export interface ILanguageSyntax {
|
|||||||
readonly commonCodeParts: string[];
|
readonly commonCodeParts: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(name: string, code: string, syntax: ILanguageSyntax): void {
|
function validateCode(code: string, syntax: ILanguageSyntax): void {
|
||||||
if (!code || code.length === 0) {
|
if (!code || code.length === 0) {
|
||||||
throw new Error(`code of ${name} is empty or undefined`);
|
throw new Error(`code is empty or undefined`);
|
||||||
}
|
}
|
||||||
ensureNoEmptyLines(name, code);
|
ensureNoEmptyLines(code);
|
||||||
ensureCodeHasUniqueLines(name, code, syntax);
|
ensureCodeHasUniqueLines(code, syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNoEmptyLines(name: string, code: string): void {
|
function ensureNoEmptyLines(code: string): void {
|
||||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
||||||
throw Error(`script has empty lines "${name}"`);
|
throw Error(`script has empty lines`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureCodeHasUniqueLines(name: string, code: string, syntax: ILanguageSyntax): void {
|
function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
|
||||||
const lines = code.split('\n')
|
const lines = code.split('\n')
|
||||||
.filter((line) => !shouldIgnoreLine(line, syntax));
|
.filter((line) => !shouldIgnoreLine(line, syntax));
|
||||||
if (lines.length === 0) {
|
if (lines.length === 0) {
|
||||||
@@ -46,7 +47,7 @@ function ensureCodeHasUniqueLines(name: string, code: string, syntax: ILanguageS
|
|||||||
}
|
}
|
||||||
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||||
if (duplicateLines.length !== 0) {
|
if (duplicateLines.length !== 0) {
|
||||||
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
throw Error(`Duplicates detected in script :\n ${duplicateLines.join('\n')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export class Clipboard {
|
export class Clipboard {
|
||||||
public static copyText(text: string): void {
|
public static copyText(text: string): void {
|
||||||
const el = document.createElement('textarea');
|
const el = document.createElement('textarea');
|
||||||
|
|||||||
69
src/infrastructure/CodeRunner.ts
Normal file
69
src/infrastructure/CodeRunner.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import child_process from 'child_process';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
|
export async function runCodeAsync(
|
||||||
|
code: string, folderName: string, fileExtension: string,
|
||||||
|
node = getNodeJs(), environment = Environment.CurrentEnvironment): Promise<void> {
|
||||||
|
const dir = node.path.join(node.os.tmpdir(), folderName);
|
||||||
|
await node.fs.promises.mkdir(dir, {recursive: true});
|
||||||
|
const filePath = node.path.join(dir, `run.${fileExtension}`);
|
||||||
|
await node.fs.promises.writeFile(filePath, code);
|
||||||
|
await node.fs.promises.chmod(filePath, '755');
|
||||||
|
const command = getExecuteCommand(filePath, environment);
|
||||||
|
node.child_process.exec(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
||||||
|
switch (environment.os) {
|
||||||
|
case OperatingSystem.macOS:
|
||||||
|
return `open -a Terminal.app ${scriptPath}`;
|
||||||
|
// Another option with graphical sudo would be
|
||||||
|
// `osascript -e "do shell script \\"${scriptPath}\\" with administrator privileges"`
|
||||||
|
// However it runs in background
|
||||||
|
case OperatingSystem.Windows:
|
||||||
|
return scriptPath;
|
||||||
|
default:
|
||||||
|
throw Error('undefined os');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeJs(): INodeJs {
|
||||||
|
return { os, path, fs, child_process };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeJs {
|
||||||
|
os: INodeOs;
|
||||||
|
path: INodePath;
|
||||||
|
fs: INodeFs;
|
||||||
|
child_process: INodeChildProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeOs {
|
||||||
|
tmpdir(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodePath {
|
||||||
|
join(...paths: string[]): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeChildProcess {
|
||||||
|
exec(command: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeFs {
|
||||||
|
readonly promises: INodeFsPromises;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface INodeFsPromisesMakeDirectoryOptions {
|
||||||
|
recursive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface INodeFsPromises { // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v13/fs.d.ts
|
||||||
|
chmod(path: string, mode: string | number): Promise<void>;
|
||||||
|
mkdir(path: string, options: INodeFsPromisesMakeDirectoryOptions): Promise<string>;
|
||||||
|
writeFile(path: string, data: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -7,8 +7,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
|||||||
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
||||||
/** SOLID ICONS (PREFIX: fas (default)) */
|
/** SOLID ICONS (PREFIX: fas (default)) */
|
||||||
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop,
|
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop,
|
||||||
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf } from '@fortawesome/free-solid-svg-icons';
|
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf, faPlay, faArrowsAltH } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
|
||||||
export class IconBootstrapper implements IVueBootstrapper {
|
export class IconBootstrapper implements IVueBootstrapper {
|
||||||
public bootstrap(vue: VueConstructor): void {
|
public bootstrap(vue: VueConstructor): void {
|
||||||
@@ -24,9 +23,12 @@ export class IconBootstrapper implements IVueBootstrapper {
|
|||||||
faTimes,
|
faTimes,
|
||||||
faFileDownload, faSave,
|
faFileDownload, faSave,
|
||||||
faCopy,
|
faCopy,
|
||||||
|
faPlay,
|
||||||
faSearch,
|
faSearch,
|
||||||
faBatteryFull, faBatteryHalf,
|
faBatteryFull, faBatteryHalf,
|
||||||
faInfoCircle);
|
faInfoCircle,
|
||||||
|
faArrowsAltH,
|
||||||
|
);
|
||||||
vue.component('font-awesome-icon', FontAwesomeIcon);
|
vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="instructions">
|
<div class="instructions">
|
||||||
<!-- <p>
|
<p>
|
||||||
Since you're using online version of {{ this.appName }}, you will need to do additional steps after downloading the file to execute your script on macOS:
|
Since you're using online version of {{ this.appName }}, you will need to do additional steps after downloading the file to execute your script on macOS:
|
||||||
</p> -->
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
@@ -73,9 +73,9 @@
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</p>
|
</p>
|
||||||
<!-- <p>
|
<p>
|
||||||
Or download the <a :href="this.macOsDownloadUrl">offline version</a> to run your scripts directly to skip these steps.
|
Or download the <a :href="this.macOsDownloadUrl">offline version</a> to run your scripts directly to skip these steps.
|
||||||
</p> -->
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container" v-if="hasCode">
|
<div class="container" v-if="hasCode">
|
||||||
|
<IconButton
|
||||||
|
v-if="this.canRun"
|
||||||
|
text="Run"
|
||||||
|
v-on:click="executeCodeAsync"
|
||||||
|
icon-prefix="fas" icon-name="play">
|
||||||
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
:text="this.isDesktopVersion ? 'Save' : 'Download'"
|
:text="this.isDesktopVersion ? 'Save' : 'Download'"
|
||||||
v-on:click="saveCodeAsync"
|
v-on:click="saveCodeAsync"
|
||||||
@@ -38,6 +44,8 @@ import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|||||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { runCodeAsync } from '@/infrastructure/CodeRunner';
|
||||||
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -48,8 +56,9 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
|||||||
export default class TheCodeButtons extends StatefulVue {
|
export default class TheCodeButtons extends StatefulVue {
|
||||||
public readonly macOsModalName = 'macos-instructions';
|
public readonly macOsModalName = 'macos-instructions';
|
||||||
|
|
||||||
|
public readonly isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
||||||
|
public canRun = false;
|
||||||
public hasCode = false;
|
public hasCode = false;
|
||||||
public isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
|
||||||
public isMacOsCollection = false;
|
public isMacOsCollection = false;
|
||||||
public fileName = '';
|
public fileName = '';
|
||||||
|
|
||||||
@@ -64,8 +73,13 @@ export default class TheCodeButtons extends StatefulVue {
|
|||||||
this.$modal.show(this.macOsModalName);
|
this.$modal.show(this.macOsModalName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public async executeCodeAsync() {
|
||||||
|
const context = await this.getCurrentContextAsync();
|
||||||
|
await executeCodeAsync(context);
|
||||||
|
}
|
||||||
|
|
||||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||||
|
this.canRun = this.isDesktopVersion && newState.collection.os === Environment.CurrentEnvironment.os;
|
||||||
this.isMacOsCollection = newState.collection.os === OperatingSystem.macOS;
|
this.isMacOsCollection = newState.collection.os === OperatingSystem.macOS;
|
||||||
this.fileName = buildFileName(newState.collection.scripting);
|
this.fileName = buildFileName(newState.collection.scripting);
|
||||||
this.react(newState.code);
|
this.react(newState.code);
|
||||||
@@ -108,6 +122,15 @@ function buildFileName(scripting: IScriptingDefinition) {
|
|||||||
}
|
}
|
||||||
return fileName;
|
return fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function executeCodeAsync(context: IApplicationContext) {
|
||||||
|
await runCodeAsync(
|
||||||
|
/*code*/ context.state.code.current,
|
||||||
|
/*appName*/ context.app.info.name,
|
||||||
|
/*fileExtension*/ context.state.collection.scripting.fileExtension,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :id="editorId" class="code-area" ></div>
|
<Responsive v-on:sizeChanged="sizeChanged()">
|
||||||
|
<div
|
||||||
|
:id="editorId"
|
||||||
|
class="code-area"
|
||||||
|
></div>
|
||||||
|
</Responsive>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop } from 'vue-property-decorator';
|
import { Component, Prop } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from './StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import ace from 'ace-builds';
|
import ace from 'ace-builds';
|
||||||
import 'ace-builds/webpack-resolver';
|
import 'ace-builds/webpack-resolver';
|
||||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||||
@@ -12,8 +17,13 @@ import { IScript } from '@/domain/IScript';
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
|
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
|
||||||
|
import Responsive from '@/presentation/Responsive.vue';
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
components: {
|
||||||
|
Responsive,
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class TheCodeArea extends StatefulVue {
|
export default class TheCodeArea extends StatefulVue {
|
||||||
public readonly editorId = 'codeEditor';
|
public readonly editorId = 'codeEditor';
|
||||||
|
|
||||||
@@ -25,6 +35,11 @@ export default class TheCodeArea extends StatefulVue {
|
|||||||
public destroyed() {
|
public destroyed() {
|
||||||
this.destroyEditor();
|
this.destroyEditor();
|
||||||
}
|
}
|
||||||
|
public sizeChanged() {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||||
this.destroyEditor();
|
this.destroyEditor();
|
||||||
@@ -44,7 +59,6 @@ export default class TheCodeArea extends StatefulVue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.editor.setValue(event.code, 1);
|
this.editor.setValue(event.code, 1);
|
||||||
|
|
||||||
if (event.addedScripts && event.addedScripts.length) {
|
if (event.addedScripts && event.addedScripts.length) {
|
||||||
this.reactToChanges(event, event.addedScripts);
|
this.reactToChanges(event, event.addedScripts);
|
||||||
} else if (event.changedScripts && event.changedScripts.length) {
|
} else if (event.changedScripts && event.changedScripts.length) {
|
||||||
@@ -83,6 +97,7 @@ export default class TheCodeArea extends StatefulVue {
|
|||||||
private destroyEditor() {
|
private destroyEditor() {
|
||||||
if (this.editor) {
|
if (this.editor) {
|
||||||
this.editor.destroy();
|
this.editor.destroy();
|
||||||
|
this.editor = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,6 +110,7 @@ function initializeEditor(theme: string, editorId: string, language: ScriptingLa
|
|||||||
editor.setTheme(`ace/theme/${theme}`);
|
editor.setTheme(`ace/theme/${theme}`);
|
||||||
editor.setReadOnly(true);
|
editor.setReadOnly(true);
|
||||||
editor.setAutoScrollEditorIntoView(true);
|
editor.setAutoScrollEditorIntoView(true);
|
||||||
|
editor.setShowPrintMargin(false); // hides vertical line
|
||||||
editor.getSession().setUseWrapMode(true); // So code is readable on mobile
|
editor.getSession().setUseWrapMode(true); // So code is readable on mobile
|
||||||
return editor;
|
return editor;
|
||||||
}
|
}
|
||||||
@@ -129,12 +145,12 @@ function getDefaultCode(language: ScriptingLanguage): string {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/presentation/styles/colors.scss";
|
@import "@/presentation/styles/colors.scss";
|
||||||
.code-area {
|
::v-deep .code-area {
|
||||||
width: 100%;
|
|
||||||
max-height: 1000px;
|
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
&__highlight {
|
&__highlight {
|
||||||
background-color: $accent;
|
background-color: $accent;
|
||||||
75
src/presentation/Responsive.vue
Normal file
75
src/presentation/Responsive.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="containerElement" class="container">
|
||||||
|
<slot ref="containerElement"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Emit } from 'vue-property-decorator';
|
||||||
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
|
import { throttle } from './Throttle';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Responsive extends Vue {
|
||||||
|
private width: number;
|
||||||
|
private height: number;
|
||||||
|
private observer: ResizeObserver;
|
||||||
|
private get container(): HTMLElement { return this.$refs.containerElement as HTMLElement; }
|
||||||
|
|
||||||
|
public mounted() {
|
||||||
|
this.width = this.container.offsetWidth;
|
||||||
|
this.height = this.container.offsetHeight;
|
||||||
|
const resizeCallback = throttle(() => this.updateSize(), 200);
|
||||||
|
this.observer = new ResizeObserver(resizeCallback);
|
||||||
|
this.observer.observe(this.container);
|
||||||
|
this.fireChangeEvents();
|
||||||
|
}
|
||||||
|
public updateSize() {
|
||||||
|
let sizeChanged = false;
|
||||||
|
if (this.isWidthChanged()) {
|
||||||
|
this.updateWidth(this.container.offsetWidth);
|
||||||
|
sizeChanged = true;
|
||||||
|
}
|
||||||
|
if (this.isHeightChanged()) {
|
||||||
|
this.updateHeight(this.container.offsetHeight);
|
||||||
|
sizeChanged = true;
|
||||||
|
}
|
||||||
|
if (sizeChanged) {
|
||||||
|
this.$emit('sizeChanged');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Emit('widthChanged') public updateWidth(width: number) {
|
||||||
|
this.width = width;
|
||||||
|
}
|
||||||
|
@Emit('heightChanged') public updateHeight(height: number) {
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
public destroyed() {
|
||||||
|
if (this.observer) {
|
||||||
|
this.observer.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fireChangeEvents() {
|
||||||
|
this.updateWidth(this.container.offsetWidth);
|
||||||
|
this.updateHeight(this.container.offsetHeight);
|
||||||
|
this.$emit('sizeChanged');
|
||||||
|
}
|
||||||
|
private isWidthChanged(): boolean {
|
||||||
|
return this.width !== this.container.offsetWidth;
|
||||||
|
}
|
||||||
|
private isHeightChanged(): boolean {
|
||||||
|
return this.height !== this.container.offsetHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block; // if inline then it has no height or weight
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<Responsive v-on:widthChanged="width = $event">
|
||||||
|
<!-- <div id="responsivity-debug">
|
||||||
|
Width: {{ width || 'undefined' }}
|
||||||
|
Size: <span v-if="width <= 500">small</span><span v-if="width > 500 && width < 750">medium</span><span v-if="width >= 750">big</span>
|
||||||
|
</div> -->
|
||||||
<div v-if="categoryIds != null && categoryIds.length > 0" class="cards">
|
<div v-if="categoryIds != null && categoryIds.length > 0" class="cards">
|
||||||
<CardListItem
|
<CardListItem
|
||||||
class="card"
|
class="card"
|
||||||
|
v-bind:class="{
|
||||||
|
'small-screen': width <= 500,
|
||||||
|
'medium-screen': width > 500 && width < 750,
|
||||||
|
'big-screen': width >= 750
|
||||||
|
}"
|
||||||
v-for="categoryId of categoryIds"
|
v-for="categoryId of categoryIds"
|
||||||
:data-category="categoryId"
|
:data-category="categoryId"
|
||||||
v-bind:key="categoryId"
|
v-bind:key="categoryId"
|
||||||
@@ -12,12 +21,13 @@
|
|||||||
</CardListItem>
|
</CardListItem>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="error">Something went bad 😢</div>
|
<div v-else class="error">Something went bad 😢</div>
|
||||||
</div>
|
</Responsive>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component } from 'vue-property-decorator';
|
|
||||||
import CardListItem from './CardListItem.vue';
|
import CardListItem from './CardListItem.vue';
|
||||||
|
import Responsive from '@/presentation/Responsive.vue';
|
||||||
|
import { Component } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { hasDirective } from './NonCollapsingDirective';
|
import { hasDirective } from './NonCollapsingDirective';
|
||||||
@@ -26,9 +36,11 @@ import { ICategoryCollectionState } from '@/application/Context/State/ICategoryC
|
|||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
CardListItem,
|
CardListItem,
|
||||||
|
Responsive,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class CardList extends StatefulVue {
|
export default class CardList extends StatefulVue {
|
||||||
|
public width: number = 0;
|
||||||
public categoryIds: number[] = [];
|
public categoryIds: number[] = [];
|
||||||
public activeCategoryId?: number = null;
|
public activeCategoryId?: number = null;
|
||||||
|
|
||||||
@@ -75,6 +87,7 @@ export default class CardList extends StatefulVue {
|
|||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
font-family: $main-font;
|
font-family: $main-font;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -106,12 +106,7 @@ $expanded-margin-top: 30px;
|
|||||||
|
|
||||||
.card {
|
.card {
|
||||||
margin: 15px;
|
margin: 15px;
|
||||||
width: calc((100% / 3) - #{$card-line-break-width});
|
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
// Media queries for stacking cards
|
|
||||||
@media screen and (max-width: $big-screen-width) { width: calc((100% / 2) - #{$card-line-break-width}); }
|
|
||||||
@media screen and (max-width: $medium-screen-width) { width: 100%; }
|
|
||||||
@media screen and (max-width: $small-screen-width) { width: 90%; }
|
|
||||||
|
|
||||||
&__inner {
|
&__inner {
|
||||||
padding: $card-padding $card-padding 0 $card-padding;
|
padding: $card-padding $card-padding 0 $card-padding;
|
||||||
@@ -241,31 +236,32 @@ $expanded-margin-top: 30px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@mixin adaptive-card($cards-in-row) {
|
||||||
@media screen and (min-width: $big-screen-width) { // when 3 cards in a row
|
&.card {
|
||||||
.card:nth-of-type(3n+2) .card__expander {
|
width: calc((100% / #{$cards-in-row}) - #{$card-line-break-width});
|
||||||
margin-left: calc(-100% - #{$card-line-break-width});
|
@for $nth-card from 2 through $cards-in-row {
|
||||||
|
&:nth-of-type(#{$cards-in-row}n+#{$nth-card}) {
|
||||||
|
.card__expander {
|
||||||
|
$card-left: -100% * ($nth-card - 1);
|
||||||
|
$additional-space: $card-line-break-width * ($nth-card - 1);
|
||||||
|
margin-left: calc(#{$card-left} - #{$additional-space});
|
||||||
}
|
}
|
||||||
.card:nth-of-type(3n+3) .card__expander {
|
|
||||||
margin-left: calc(-200% - (#{$card-line-break-width} * 2));
|
|
||||||
}
|
}
|
||||||
.card:nth-of-type(3n+4) {
|
}
|
||||||
|
// Ensure new line after last row
|
||||||
|
$card-after-last: $cards-in-row + 1;
|
||||||
|
&:nth-of-type(#{$cards-in-row}n+#{$card-after-last}) {
|
||||||
clear: left;
|
clear: left;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.card__expander {
|
.card__expander {
|
||||||
width: calc(300% + (#{$card-line-break-width} * 2));
|
$all-cards-width: 100% * $cards-in-row;
|
||||||
|
$card-padding: $card-line-break-width * ($cards-in-row - 1);
|
||||||
|
width: calc(#{$all-cards-width} + #{$card-padding});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: $medium-screen-width) and (max-width: $big-screen-width) { // when 2 cards in a row
|
.big-screen { @include adaptive-card(3); }
|
||||||
.card:nth-of-type(2n+2) .card__expander {
|
.medium-screen { @include adaptive-card(2); }
|
||||||
margin-left: calc(-100% - #{$card-line-break-width});
|
.small-screen { @include adaptive-card(1); }
|
||||||
}
|
|
||||||
.card:nth-of-type(2n+3) {
|
|
||||||
clear: left;
|
|
||||||
}
|
|
||||||
.card__expander {
|
|
||||||
width: calc(200% + #{$card-line-break-width});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="part select">Select:</div>
|
<div class="part">Select:</div>
|
||||||
<div class="part">
|
<div class="part">
|
||||||
<div class="part">
|
<div class="part">
|
||||||
<SelectableOption
|
<SelectableOption
|
||||||
@@ -173,5 +173,4 @@ function areAllSelected(
|
|||||||
}
|
}
|
||||||
font-family: $normal-font;
|
font-family: $normal-font;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<!-- <div>OS:</div> -->
|
||||||
|
<div class="os-list">
|
||||||
<div v-for="os in this.allOses" :key="os.name">
|
<div v-for="os in this.allOses" :key="os.name">
|
||||||
<span
|
<span
|
||||||
class="name"
|
class="os-name"
|
||||||
v-bind:class="{ 'current': currentOs === os.os }"
|
v-bind:class="{ 'current': currentOs === os.os }"
|
||||||
v-on:click="changeOsAsync(os.os)">
|
v-on:click="changeOsAsync(os.os)">
|
||||||
{{ os.name }}
|
{{ os.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -42,7 +45,7 @@ export default class TheOsChanger extends StatefulVue {
|
|||||||
function renderOsName(os: OperatingSystem): string {
|
function renderOsName(os: OperatingSystem): string {
|
||||||
switch (os) {
|
switch (os) {
|
||||||
case OperatingSystem.Windows: return 'Windows';
|
case OperatingSystem.Windows: return 'Windows';
|
||||||
case OperatingSystem.macOS: return 'macOS (preview)';
|
case OperatingSystem.macOS: return 'macOS';
|
||||||
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
|
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,11 +58,14 @@ function renderOsName(os: OperatingSystem): string {
|
|||||||
font-family: $normal-font;
|
font-family: $normal-font;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
.os-list {
|
||||||
|
display: flex;
|
||||||
|
margin-left: 0.25rem;
|
||||||
div + div::before {
|
div + div::before {
|
||||||
content: "|";
|
content: "|";
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
.name {
|
.os-name {
|
||||||
&:not(.current) {
|
&:not(.current) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -72,4 +78,5 @@ function renderOsName(os: OperatingSystem): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
77
src/presentation/Scripts/Menu/TheScriptsMenu.vue
Normal file
77
src/presentation/Scripts/Menu/TheScriptsMenu.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div id="container">
|
||||||
|
<TheSelector class="item" />
|
||||||
|
<TheOsChanger class="item" />
|
||||||
|
<TheGrouper
|
||||||
|
class="item"
|
||||||
|
v-on:groupingChanged="$emit('groupingChanged', $event)"
|
||||||
|
v-if="!this.isSearching" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component } from 'vue-property-decorator';
|
||||||
|
import TheOsChanger from './TheOsChanger.vue';
|
||||||
|
import TheSelector from './Selector/TheSelector.vue';
|
||||||
|
import TheGrouper from './Grouping/TheGrouper.vue';
|
||||||
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
TheSelector,
|
||||||
|
TheOsChanger,
|
||||||
|
TheGrouper,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class TheScriptsMenu extends StatefulVue {
|
||||||
|
public isSearching = false;
|
||||||
|
|
||||||
|
private listeners = new Array<IEventSubscription>();
|
||||||
|
|
||||||
|
public destroyed() {
|
||||||
|
this.unsubscribeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initialize(): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||||
|
this.subscribe(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribe(state: ICategoryCollectionState) {
|
||||||
|
this.listeners.push(state.filter.filterRemoved.on(() => {
|
||||||
|
this.isSearching = false;
|
||||||
|
}));
|
||||||
|
state.filter.filtered.on(() => {
|
||||||
|
this.isSearching = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private unsubscribeAll() {
|
||||||
|
this.listeners.forEach((listener) => listener.unsubscribe());
|
||||||
|
this.listeners.splice(0, this.listeners.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
#container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.item {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 5px 0 5px;
|
||||||
|
&:first-child {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
src/presentation/Scripts/Slider/Handle.vue
Normal file
71
src/presentation/Scripts/Slider/Handle.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="handle"
|
||||||
|
:style="{ cursor: cursorCssValue }"
|
||||||
|
@mousedown="startResize">
|
||||||
|
<div class="line"></div>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="image"
|
||||||
|
:icon="['fas', 'arrows-alt-h']"
|
||||||
|
/> <!-- exchange-alt arrows-alt-h-->
|
||||||
|
<div class="line"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Handle extends Vue {
|
||||||
|
public readonly cursorCssValue = 'ew-resize';
|
||||||
|
private initialX: number = undefined;
|
||||||
|
|
||||||
|
public startResize(event: MouseEvent): void {
|
||||||
|
this.initialX = event.clientX;
|
||||||
|
document.body.style.setProperty('cursor', this.cursorCssValue);
|
||||||
|
document.addEventListener('mousemove', this.resize);
|
||||||
|
window.addEventListener('mouseup', this.stopResize);
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
public resize(event: MouseEvent): void {
|
||||||
|
const displacementX = event.clientX - this.initialX;
|
||||||
|
this.$emit('resized', displacementX);
|
||||||
|
this.initialX = event.clientX;
|
||||||
|
}
|
||||||
|
public stopResize(): void {
|
||||||
|
document.body.style.removeProperty('cursor');
|
||||||
|
document.removeEventListener('mousemove', this.resize);
|
||||||
|
window.removeEventListener('mouseup', this.stopResize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
&:hover {
|
||||||
|
.line {
|
||||||
|
background: $gray;
|
||||||
|
}
|
||||||
|
.image {
|
||||||
|
color: $gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
flex: 1;
|
||||||
|
background: $dark-gray;
|
||||||
|
width: 3px;
|
||||||
|
}
|
||||||
|
.image {
|
||||||
|
color: $dark-gray;
|
||||||
|
}
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
53
src/presentation/Scripts/Slider/HorizontalResizeSlider.vue
Normal file
53
src/presentation/Scripts/Slider/HorizontalResizeSlider.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="slider">
|
||||||
|
<div class="left" ref="leftElement">
|
||||||
|
<slot name="left"></slot>
|
||||||
|
</div>
|
||||||
|
<Handle class="handle" @resized="onResize($event)" />
|
||||||
|
<div class="right">
|
||||||
|
<slot name="right"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
import Handle from './Handle.vue';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
Handle,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class HorizontalResizeSlider extends Vue {
|
||||||
|
private get left(): HTMLElement { return this.$refs.leftElement as HTMLElement; }
|
||||||
|
|
||||||
|
public onResize(displacementX: number): void {
|
||||||
|
const leftWidth = this.left.offsetWidth + displacementX;
|
||||||
|
this.left.style.width = `${leftWidth}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@/presentation/styles/media.scss";
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
.right {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (max-width: $vertical-view-breakpoint) {
|
||||||
|
.slider {
|
||||||
|
flex-direction: column;
|
||||||
|
.left {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
.handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
src/presentation/Scripts/TheScriptArea.vue
Normal file
49
src/presentation/Scripts/TheScriptArea.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div class="scripts">
|
||||||
|
<TheScriptsMenu v-on:groupingChanged="grouping = $event" />
|
||||||
|
<HorizontalResizeSlider class="row">
|
||||||
|
<template v-slot:left>
|
||||||
|
<TheScriptsList :grouping="grouping" />
|
||||||
|
</template>
|
||||||
|
<template v-slot:right>
|
||||||
|
<TheCodeArea theme="xcode" />
|
||||||
|
</template>
|
||||||
|
</HorizontalResizeSlider>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
import TheCodeArea from '@/presentation/Code/TheCodeArea.vue';
|
||||||
|
import TheScriptsList from '@/presentation/Scripts/TheScriptsList.vue';
|
||||||
|
import TheScriptsMenu from '@/presentation/Scripts/Menu/TheScriptsMenu.vue';
|
||||||
|
import HorizontalResizeSlider from '@/presentation/Scripts/Slider/HorizontalResizeSlider.vue';
|
||||||
|
import { Grouping } from '@/presentation/Scripts/Menu/Grouping/Grouping';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
TheCodeArea,
|
||||||
|
TheScriptsList,
|
||||||
|
TheScriptsMenu,
|
||||||
|
HorizontalResizeSlider,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class TheScriptArea extends Vue {
|
||||||
|
public grouping = Grouping.Cards;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.scripts {
|
||||||
|
> * + * {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::v-deep .left {
|
||||||
|
width: 55%; // initial width
|
||||||
|
min-width: 20%;
|
||||||
|
}
|
||||||
|
::v-deep .right {
|
||||||
|
min-width: 20%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,17 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
|
||||||
<div class="heading">
|
|
||||||
<TheSelector class="item"/>
|
|
||||||
<TheOsChanger class="item"/>
|
|
||||||
<TheGrouper
|
|
||||||
class="item"
|
|
||||||
v-on:groupingChanged="onGroupingChanged($event)"
|
|
||||||
v-if="!this.isSearching" />
|
|
||||||
</div>
|
|
||||||
<div class="scripts">
|
<div class="scripts">
|
||||||
<div v-if="!isSearching">
|
<div v-if="!isSearching">
|
||||||
<CardList v-if="currentGrouping === Grouping.Cards"/>
|
<CardList v-if="grouping === Grouping.Cards"/>
|
||||||
<div class="tree" v-if="currentGrouping === Grouping.None">
|
<div class="tree" v-if="grouping === Grouping.None">
|
||||||
<ScriptsTree />
|
<ScriptsTree />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,18 +26,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TheGrouper from '@/presentation/Scripts/Grouping/TheGrouper.vue';
|
import TheGrouper from '@/presentation/Scripts/Menu/Grouping/TheGrouper.vue';
|
||||||
import TheOsChanger from '@/presentation/Scripts/TheOsChanger.vue';
|
|
||||||
import TheSelector from '@/presentation/Scripts/Selector/TheSelector.vue';
|
|
||||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||||
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
|
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
|
||||||
import { Component } from 'vue-property-decorator';
|
import { Component, Prop } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import { Grouping } from './Grouping/Grouping';
|
import { Grouping } from '@/presentation/Scripts/Menu/Grouping/Grouping';
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||||
@@ -55,10 +43,8 @@ import { ApplicationFactory } from '@/application/ApplicationFactory';
|
|||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
TheGrouper,
|
TheGrouper,
|
||||||
TheSelector,
|
|
||||||
ScriptsTree,
|
ScriptsTree,
|
||||||
CardList,
|
CardList,
|
||||||
TheOsChanger,
|
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
threeDotsTrim(query: string) {
|
threeDotsTrim(query: string) {
|
||||||
@@ -70,10 +56,11 @@ filters: {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class TheScripts extends StatefulVue {
|
export default class TheScriptsList extends StatefulVue {
|
||||||
|
@Prop() public grouping: Grouping;
|
||||||
|
|
||||||
public repositoryUrl = '';
|
public repositoryUrl = '';
|
||||||
public Grouping = Grouping; // Make it accessible from view
|
public Grouping = Grouping; // Make it accessible from the view
|
||||||
public currentGrouping = Grouping.Cards;
|
|
||||||
public searchQuery = '';
|
public searchQuery = '';
|
||||||
public isSearching = false;
|
public isSearching = false;
|
||||||
public searchHasMatches = false;
|
public searchHasMatches = false;
|
||||||
@@ -87,9 +74,6 @@ export default class TheScripts extends StatefulVue {
|
|||||||
const filter = context.state.filter;
|
const filter = context.state.filter;
|
||||||
filter.removeFilter();
|
filter.removeFilter();
|
||||||
}
|
}
|
||||||
public onGroupingChanged(group: Grouping) {
|
|
||||||
this.currentGrouping = group;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||||
this.events.unsubscribeAll();
|
this.events.unsubscribeAll();
|
||||||
@@ -114,11 +98,16 @@ export default class TheScripts extends StatefulVue {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/presentation/styles/colors.scss";
|
@import "@/presentation/styles/colors.scss";
|
||||||
@import "@/presentation/styles/fonts.scss";
|
@import "@/presentation/styles/fonts.scss";
|
||||||
|
@import "@/presentation/styles/media.scss";
|
||||||
|
|
||||||
$inner-margin: 4px;
|
$inner-margin: 4px;
|
||||||
|
|
||||||
.scripts {
|
.scripts {
|
||||||
margin-top: $inner-margin;
|
margin-top: $inner-margin;
|
||||||
|
@media screen and (min-width: $vertical-view-breakpoint) { // so the current code is always visible
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
.tree {
|
.tree {
|
||||||
padding-left: 3%;
|
padding-left: 3%;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
@@ -167,22 +156,4 @@ $inner-margin: 4px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
|
||||||
margin-top: $inner-margin;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
.item {
|
|
||||||
flex: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 5px 0 5px;
|
|
||||||
&:first-child {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -99,13 +99,13 @@ export default class TheFooter extends Vue {
|
|||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@media (max-width: $big-screen-width) {
|
@media screen and (max-width: $big-screen-width) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
&__section {
|
&__section {
|
||||||
display: flex;
|
display: flex;
|
||||||
@media (max-width: $big-screen-width) {
|
@media screen and (max-width: $big-screen-width) {
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
width:100%;
|
width:100%;
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
@@ -129,7 +129,7 @@ export default class TheFooter extends Vue {
|
|||||||
content: "|";
|
content: "|";
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
}
|
}
|
||||||
@media (max-width: $big-screen-width) {
|
@media screen and (max-width: $big-screen-width) {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|||||||
30
src/presentation/Throttle.ts
Normal file
30
src/presentation/Throttle.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export function throttle<T extends []>(
|
||||||
|
callback: (..._: T) => void, wait: number,
|
||||||
|
timer: ITimer = NodeTimer): (..._: T) => void {
|
||||||
|
let queuedToRun: ReturnType<typeof setTimeout>;
|
||||||
|
let previouslyRun: number;
|
||||||
|
return function invokeFn(...args: T) {
|
||||||
|
const now = timer.dateNow();
|
||||||
|
if (queuedToRun) {
|
||||||
|
queuedToRun = timer.clearTimeout(queuedToRun) as undefined;
|
||||||
|
}
|
||||||
|
if (!previouslyRun || (now - previouslyRun >= wait)) {
|
||||||
|
callback(...args);
|
||||||
|
previouslyRun = now;
|
||||||
|
} else {
|
||||||
|
queuedToRun = timer.setTimeout(invokeFn.bind(null, ...args), wait - (now - previouslyRun));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITimer {
|
||||||
|
setTimeout: (callback: () => void, ms: number) => ReturnType<typeof setTimeout>;
|
||||||
|
clearTimeout: (timeoutId: ReturnType<typeof setTimeout>) => void;
|
||||||
|
dateNow(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeTimer: ITimer = {
|
||||||
|
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
||||||
|
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
|
||||||
|
dateNow: () => Date.now(),
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
$big-screen-width: 992px;
|
$big-screen-width: 992px;
|
||||||
$medium-screen-width: 768px;
|
$medium-screen-width: 768px;
|
||||||
$small-screen-width: 380px;
|
$small-screen-width: 380px;
|
||||||
|
|
||||||
|
$vertical-view-breakpoint: 992px;
|
||||||
@@ -23,15 +23,37 @@ describe('BatchBuilder', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('writeStandardOut', () => {
|
describe('writeStandardOut', () => {
|
||||||
it('prepends expected', () => {
|
const testData = [
|
||||||
|
{
|
||||||
|
name: 'plain text',
|
||||||
|
text: 'test',
|
||||||
|
expected: 'echo test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'text with ampersand',
|
||||||
|
text: 'a & b',
|
||||||
|
expected: 'echo a ^& b',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'text with percent sign',
|
||||||
|
text: '90%',
|
||||||
|
expected: 'echo 90%%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'text with multiple ampersands and percent signs',
|
||||||
|
text: 'Me&you in % ? You & me = 0%',
|
||||||
|
expected: 'echo Me^&you in %% ? You ^& me = 0%%',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const test of testData) {
|
||||||
|
it(test.name, () => {
|
||||||
// arrange
|
// arrange
|
||||||
const text = 'test';
|
|
||||||
const expected = `echo ${text}`;
|
|
||||||
const sut = new BatchBuilderRevealer();
|
const sut = new BatchBuilderRevealer();
|
||||||
// act
|
// act
|
||||||
const actual = sut.writeStandardOut(text);
|
const actual = sut.writeStandardOut(test.text);
|
||||||
// assert
|
// assert
|
||||||
expect(expected).to.equal(actual);
|
expect(test.expected).to.equal(actual);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,15 +23,32 @@ describe('ShellBuilder', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('writeStandardOut', () => {
|
describe('writeStandardOut', () => {
|
||||||
it('prepends expected', () => {
|
const testData = [
|
||||||
|
{
|
||||||
|
name: 'plain text',
|
||||||
|
text: 'test',
|
||||||
|
expected: 'echo \'test\'',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'text with single quote',
|
||||||
|
text: 'I\'m not who you think I am',
|
||||||
|
expected: 'echo \'I\'\\\'\'m not who you think I am\'',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'text with multiple single quotes',
|
||||||
|
text: 'I\'m what you\'re',
|
||||||
|
expected: 'echo \'I\'\\\'\'m what you\'\\\'\'re\'',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const test of testData) {
|
||||||
|
it(test.name, () => {
|
||||||
// arrange
|
// arrange
|
||||||
const text = 'test';
|
|
||||||
const expected = `echo '${text}'`;
|
|
||||||
const sut = new ShellBuilderRevealer();
|
const sut = new ShellBuilderRevealer();
|
||||||
// act
|
// act
|
||||||
const actual = sut.writeStandardOut(text);
|
const actual = sut.writeStandardOut(test.text);
|
||||||
// assert
|
// assert
|
||||||
expect(expected).to.equal(actual);
|
expect(test.expected).to.equal(actual);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ describe('CategoryCollectionParser', () => {
|
|||||||
const scriptName = 'script-name';
|
const scriptName = 'script-name';
|
||||||
const script = ScriptDataStub.createWithCall({ function: functionName })
|
const script = ScriptDataStub.createWithCall({ function: functionName })
|
||||||
.withName(scriptName);
|
.withName(scriptName);
|
||||||
const func = new FunctionDataStub()
|
const func = FunctionDataStub.createWithCode()
|
||||||
.withName(functionName)
|
.withName(functionName)
|
||||||
.withCode(expectedCode);
|
.withCode(expectedCode);
|
||||||
const category = new CategoryDataStub()
|
const category = new CategoryDataStub()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ describe('CategoryCollectionParseContext', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'undefined scripting';
|
const expectedError = 'undefined scripting';
|
||||||
const scripting = undefined;
|
const scripting = undefined;
|
||||||
const functionsData = [ new FunctionDataStub() ];
|
const functionsData = [ FunctionDataStub.createWithCode() ];
|
||||||
// act
|
// act
|
||||||
const act = () => new CategoryCollectionParseContext(functionsData, scripting);
|
const act = () => new CategoryCollectionParseContext(functionsData, scripting);
|
||||||
// assert
|
// assert
|
||||||
@@ -39,7 +39,7 @@ describe('CategoryCollectionParseContext', () => {
|
|||||||
describe('compiler', () => {
|
describe('compiler', () => {
|
||||||
it('constructed as expected', () => {
|
it('constructed as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const functionsData = [ new FunctionDataStub() ];
|
const functionsData = [ FunctionDataStub.createWithCode() ];
|
||||||
const syntax = new LanguageSyntaxStub();
|
const syntax = new LanguageSyntaxStub();
|
||||||
const expected = new ScriptCompiler(functionsData, syntax);
|
const expected = new ScriptCompiler(functionsData, syntax);
|
||||||
const language = ScriptingLanguage.shellscript;
|
const language = ScriptingLanguage.shellscript;
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||||
|
|
||||||
|
describe('ExpressionsCompiler', () => {
|
||||||
|
describe('parameter substitution', () => {
|
||||||
|
describe('substitutes as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const testCases = [ {
|
||||||
|
name: 'with different parameters',
|
||||||
|
code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
|
||||||
|
parameters: {
|
||||||
|
firstParameter: 'llo',
|
||||||
|
secondParameter: 'world',
|
||||||
|
},
|
||||||
|
expected: 'Hello world!',
|
||||||
|
}, {
|
||||||
|
name: 'with single parameter',
|
||||||
|
code: '{{ $parameter }}!',
|
||||||
|
parameters: {
|
||||||
|
parameter: 'Hodor',
|
||||||
|
},
|
||||||
|
expected: 'Hodor!',
|
||||||
|
|
||||||
|
}];
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
it(testCase.name, () => {
|
||||||
|
const sut = new MockableExpressionsCompiler();
|
||||||
|
// act
|
||||||
|
const actual = sut.compileExpressions(testCase.code, testCase.parameters);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(testCase.expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
describe('throws when expected value is not provided', () => {
|
||||||
|
// arrange
|
||||||
|
const noParameterTestCases = [
|
||||||
|
{
|
||||||
|
name: 'empty parameters',
|
||||||
|
code: '{{ $parameter }}!',
|
||||||
|
parameters: {},
|
||||||
|
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'undefined parameters',
|
||||||
|
code: '{{ $parameter }}!',
|
||||||
|
parameters: undefined,
|
||||||
|
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'unnecessary parameter provided',
|
||||||
|
code: '{{ $parameter }}!',
|
||||||
|
parameters: {
|
||||||
|
unnecessaryParameter: 'unnecessaryValue',
|
||||||
|
},
|
||||||
|
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'undefined value',
|
||||||
|
code: '{{ $parameter }}!',
|
||||||
|
parameters: {
|
||||||
|
parameter: undefined,
|
||||||
|
},
|
||||||
|
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'multiple values are not',
|
||||||
|
code: '{{ $parameter1 }}, {{ $parameter2 }}, {{ $parameter3 }}',
|
||||||
|
parameters: {},
|
||||||
|
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2", "parameter3"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'some values are provided',
|
||||||
|
code: '{{ $parameter1 }}, {{ $parameter2 }}, {{ $parameter3 }}',
|
||||||
|
parameters: {
|
||||||
|
parameter2: 'value',
|
||||||
|
},
|
||||||
|
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3"',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const testCase of noParameterTestCases) {
|
||||||
|
it(testCase.name, () => {
|
||||||
|
const sut = new MockableExpressionsCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileExpressions(testCase.code, testCase.parameters);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(testCase.expectedError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class MockableExpressionsCompiler extends ExpressionsCompiler {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { generateIlCode } from '@/application/Parser/Script/Compiler/ILCode';
|
import { generateIlCode } from '@/application/Parser/Script/Compiler/Expressions/ILCode';
|
||||||
|
|
||||||
describe('ILCode', () => {
|
describe('ILCode', () => {
|
||||||
describe('getUniqueParameterNames', () => {
|
describe('getUniqueParameterNames', () => {
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
|
import { FunctionData } from 'js-yaml-loader!*';
|
||||||
|
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler';
|
||||||
|
import { FunctionCompiler } from '@/application/Parser/Script/Compiler/Function/FunctionCompiler';
|
||||||
|
import { FunctionCallCompilerStub } from '../../../../../stubs/FunctionCallCompilerStub';
|
||||||
|
import { FunctionDataStub } from '../../../../../stubs/FunctionDataStub';
|
||||||
|
|
||||||
|
describe('FunctionsCompiler', () => {
|
||||||
|
describe('compileFunctions', () => {
|
||||||
|
describe('validates functions', () => {
|
||||||
|
it('throws if one of the functions is undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = `some functions are undefined`;
|
||||||
|
const functions = [ FunctionDataStub.createWithCode(), undefined ];
|
||||||
|
const sut = new MockableFunctionCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileFunctions(functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws when functions have same names', () => {
|
||||||
|
// arrange
|
||||||
|
const name = 'same-func-name';
|
||||||
|
const expectedError = `duplicate function name: "${name}"`;
|
||||||
|
const functions = [
|
||||||
|
FunctionDataStub.createWithCode().withName(name),
|
||||||
|
FunctionDataStub.createWithCode().withName(name),
|
||||||
|
];
|
||||||
|
const sut = new MockableFunctionCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileFunctions(functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws when function parameters have same names', () => {
|
||||||
|
// arrange
|
||||||
|
const parameterName = 'duplicate-parameter';
|
||||||
|
const func = FunctionDataStub.createWithCall()
|
||||||
|
.withParameters(parameterName, parameterName);
|
||||||
|
const expectedError = `"${func.name}": duplicate parameter name: "${parameterName}"`;
|
||||||
|
const sut = new MockableFunctionCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileFunctions([ func ]);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
describe('throws when when function have duplicate code', () => {
|
||||||
|
it('code', () => {
|
||||||
|
// arrange
|
||||||
|
const code = 'duplicate-code';
|
||||||
|
const expectedError = `duplicate "code" in functions: "${code}"`;
|
||||||
|
const functions = [
|
||||||
|
FunctionDataStub.createWithoutCallOrCodes().withName('func-1').withCode(code),
|
||||||
|
FunctionDataStub.createWithoutCallOrCodes().withName('func-2').withCode(code),
|
||||||
|
];
|
||||||
|
const sut = new MockableFunctionCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileFunctions(functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('revertCode', () => {
|
||||||
|
// arrange
|
||||||
|
const revertCode = 'duplicate-revert-code';
|
||||||
|
const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`;
|
||||||
|
const functions = [
|
||||||
|
FunctionDataStub.createWithoutCallOrCodes()
|
||||||
|
.withName('func-1').withCode('code-1').withRevertCode(revertCode),
|
||||||
|
FunctionDataStub.createWithoutCallOrCodes()
|
||||||
|
.withName('func-2').withCode('code-2').withRevertCode(revertCode),
|
||||||
|
];
|
||||||
|
const sut = new MockableFunctionCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileFunctions(functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('both code and call are defined', () => {
|
||||||
|
// arrange
|
||||||
|
const functionName = 'invalid-function';
|
||||||
|
const expectedError = `both "code" and "call" are defined in "${functionName}"`;
|
||||||
|
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
|
||||||
|
.withName(functionName)
|
||||||
|
.withCode('code')
|
||||||
|
.withMockCall();
|
||||||
|
const sut = new MockableFunctionCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileFunctions([ invalidFunction ]);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('neither code and call is defined', () => {
|
||||||
|
// arrange
|
||||||
|
const functionName = 'invalid-function';
|
||||||
|
const expectedError = `neither "code" or "call" is defined in "${functionName}"`;
|
||||||
|
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
|
||||||
|
.withName(functionName);
|
||||||
|
const sut = new MockableFunctionCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileFunctions([ invalidFunction ]);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('returns empty with empty functions', () => {
|
||||||
|
// arrange
|
||||||
|
const emptyValues = [ [], undefined ];
|
||||||
|
const sut = new MockableFunctionCompiler();
|
||||||
|
for (const emptyFunctions of emptyValues) {
|
||||||
|
// act
|
||||||
|
const actual = sut.compileFunctions(emptyFunctions);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.not.equal(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('parses single function with code as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const name = 'function-name';
|
||||||
|
const expected = FunctionDataStub
|
||||||
|
.createWithoutCallOrCodes()
|
||||||
|
.withName(name)
|
||||||
|
.withCode('expected-code')
|
||||||
|
.withRevertCode('expected-revert-code')
|
||||||
|
.withParameters('expected-parameter-1', 'expected-parameter-2');
|
||||||
|
const sut = new MockableFunctionCompiler();
|
||||||
|
// act
|
||||||
|
const collection = sut.compileFunctions([ expected ]);
|
||||||
|
// expect
|
||||||
|
const actual = collection.getFunctionByName(name);
|
||||||
|
expectEqualFunctions(expected, actual);
|
||||||
|
});
|
||||||
|
it('parses function with call as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const calleeName = 'callee-function';
|
||||||
|
const caller = FunctionDataStub.createWithoutCallOrCodes()
|
||||||
|
.withName('caller-function')
|
||||||
|
.withCall({ function: calleeName });
|
||||||
|
const callee = FunctionDataStub.createWithoutCallOrCodes()
|
||||||
|
.withName(calleeName)
|
||||||
|
.withCode('expected-code')
|
||||||
|
.withRevertCode('expected-revert-code');
|
||||||
|
const sut = new MockableFunctionCompiler();
|
||||||
|
// act
|
||||||
|
const collection = sut.compileFunctions([ caller, callee ]);
|
||||||
|
// expect
|
||||||
|
const actual = collection.getFunctionByName(caller.name);
|
||||||
|
expectEqualFunctionCode(callee, actual);
|
||||||
|
});
|
||||||
|
it('parses multiple functions with call as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const calleeName = 'callee-function';
|
||||||
|
const caller1 = FunctionDataStub.createWithoutCallOrCodes()
|
||||||
|
.withName('caller-function')
|
||||||
|
.withCall({ function: calleeName });
|
||||||
|
const caller2 = FunctionDataStub.createWithoutCallOrCodes()
|
||||||
|
.withName('caller-function-2')
|
||||||
|
.withCall({ function: calleeName });
|
||||||
|
const callee = FunctionDataStub.createWithoutCallOrCodes()
|
||||||
|
.withName(calleeName)
|
||||||
|
.withCode('expected-code')
|
||||||
|
.withRevertCode('expected-revert-code');
|
||||||
|
const sut = new MockableFunctionCompiler();
|
||||||
|
// act
|
||||||
|
const collection = sut.compileFunctions([ caller1, caller2, callee ]);
|
||||||
|
// expect
|
||||||
|
const compiledCaller1 = collection.getFunctionByName(caller1.name);
|
||||||
|
const compiledCaller2 = collection.getFunctionByName(caller2.name);
|
||||||
|
expectEqualFunctionCode(callee, compiledCaller1);
|
||||||
|
expectEqualFunctionCode(callee, compiledCaller2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function expectEqualFunctions(expected: FunctionData, actual: ISharedFunction) {
|
||||||
|
expect(actual.name).to.equal(expected.name);
|
||||||
|
expect(actual.parameters).to.deep.equal(expected.parameters);
|
||||||
|
expectEqualFunctionCode(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectEqualFunctionCode(expected: FunctionData, actual: ISharedFunction) {
|
||||||
|
expect(actual.code).to.equal(expected.code);
|
||||||
|
expect(actual.revertCode).to.equal(expected.revertCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockableFunctionCompiler extends FunctionCompiler {
|
||||||
|
constructor(functionCallCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub()) {
|
||||||
|
super(functionCallCompiler);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { SharedFunction } from '@/application/Parser/Script/Compiler/Function/SharedFunction';
|
||||||
|
|
||||||
|
describe('SharedFunction', () => {
|
||||||
|
describe('name', () => {
|
||||||
|
it('sets as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'expected-function-name';
|
||||||
|
// act
|
||||||
|
const sut = new SharedFunctionBuilder()
|
||||||
|
.withName(expected)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(sut.name).equal(expected);
|
||||||
|
});
|
||||||
|
it('throws if empty or undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'undefined function name';
|
||||||
|
const invalidValues = [ undefined, '' ];
|
||||||
|
for (const invalidValue of invalidValues) {
|
||||||
|
// act
|
||||||
|
const act = () => new SharedFunctionBuilder()
|
||||||
|
.withName(invalidValue)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('parameters', () => {
|
||||||
|
it('sets as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = [ 'expected-parameter' ];
|
||||||
|
// act
|
||||||
|
const sut = new SharedFunctionBuilder()
|
||||||
|
.withParameters(expected)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(sut.parameters).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
it('returns empty array if undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = [ ];
|
||||||
|
const value = undefined;
|
||||||
|
// act
|
||||||
|
const sut = new SharedFunctionBuilder()
|
||||||
|
.withParameters(value)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(sut.parameters).to.not.equal(undefined);
|
||||||
|
expect(sut.parameters).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('code', () => {
|
||||||
|
it('sets as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'expected-code';
|
||||||
|
// act
|
||||||
|
const sut = new SharedFunctionBuilder()
|
||||||
|
.withCode(expected)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(sut.code).equal(expected);
|
||||||
|
});
|
||||||
|
it('throws if empty or undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const functionName = 'expected-function-name';
|
||||||
|
const expectedError = `undefined function ("${functionName}") code`;
|
||||||
|
const invalidValues = [ undefined, '' ];
|
||||||
|
for (const invalidValue of invalidValues) {
|
||||||
|
// act
|
||||||
|
const act = () => new SharedFunctionBuilder()
|
||||||
|
.withName(functionName)
|
||||||
|
.withCode(invalidValue)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('revertCode', () => {
|
||||||
|
it('sets as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const testData = [ 'expected-revert-code', undefined, '' ];
|
||||||
|
for (const data of testData) {
|
||||||
|
// act
|
||||||
|
const sut = new SharedFunctionBuilder()
|
||||||
|
.withRevertCode(data)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(sut.revertCode).equal(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class SharedFunctionBuilder {
|
||||||
|
private name = 'name';
|
||||||
|
private parameters: readonly string[] = [ 'parameter' ];
|
||||||
|
private code = 'code';
|
||||||
|
private revertCode = 'revert-code';
|
||||||
|
|
||||||
|
public build(): SharedFunction {
|
||||||
|
return new SharedFunction(
|
||||||
|
this.name,
|
||||||
|
this.parameters,
|
||||||
|
this.code,
|
||||||
|
this.revertCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
public withName(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withParameters(parameters: readonly string[]) {
|
||||||
|
this.parameters = parameters;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withCode(code: string) {
|
||||||
|
this.code = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withRevertCode(revertCode: string) {
|
||||||
|
this.revertCode = revertCode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { SharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/SharedFunctionCollection';
|
||||||
|
import { SharedFunctionStub } from '../../../../../stubs/SharedFunctionStub';
|
||||||
|
|
||||||
|
describe('SharedFunctionCollection', () => {
|
||||||
|
describe('addFunction', () => {
|
||||||
|
it('throws if function is undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'undefined function';
|
||||||
|
const func = undefined;
|
||||||
|
const sut = new SharedFunctionCollection();
|
||||||
|
// act
|
||||||
|
const act = () => sut.addFunction(func);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws if function with same name already exists', () => {
|
||||||
|
// arrange
|
||||||
|
const functionName = 'duplicate-function';
|
||||||
|
const expectedError = `function with name ${functionName} already exists`;
|
||||||
|
const func = new SharedFunctionStub()
|
||||||
|
.withName('duplicate-function');
|
||||||
|
const sut = new SharedFunctionCollection();
|
||||||
|
sut.addFunction(func);
|
||||||
|
// act
|
||||||
|
const act = () => sut.addFunction(func);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('getFunctionByName', () => {
|
||||||
|
it('throws if name is undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'undefined function name';
|
||||||
|
const invalidValues = [ undefined, '' ];
|
||||||
|
const sut = new SharedFunctionCollection();
|
||||||
|
for (const invalidValue of invalidValues) {
|
||||||
|
const name = invalidValue;
|
||||||
|
// act
|
||||||
|
const act = () => sut.getFunctionByName(name);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('throws if function does not exist', () => {
|
||||||
|
// arrange
|
||||||
|
const name = 'unique-name';
|
||||||
|
const expectedError = `called function is not defined "${name}"`;
|
||||||
|
const func = new SharedFunctionStub()
|
||||||
|
.withName('unexpected-name');
|
||||||
|
const sut = new SharedFunctionCollection();
|
||||||
|
sut.addFunction(func);
|
||||||
|
// act
|
||||||
|
const act = () => sut.getFunctionByName(name);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('returns existing function', () => {
|
||||||
|
// arrange
|
||||||
|
const name = 'expected-function-name';
|
||||||
|
const expected = new SharedFunctionStub()
|
||||||
|
.withName(name);
|
||||||
|
const sut = new SharedFunctionCollection();
|
||||||
|
sut.addFunction(new SharedFunctionStub().withName('another-function-name'));
|
||||||
|
sut.addFunction(expected);
|
||||||
|
// act
|
||||||
|
const actual = sut.getFunctionByName(name);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { FunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!*';
|
||||||
|
import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/FunctionCallCompiler';
|
||||||
|
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
|
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||||
|
import { ExpressionsCompilerStub } from '../../../../../stubs/ExpressionsCompilerStub';
|
||||||
|
import { SharedFunctionCollectionStub } from '../../../../../stubs/SharedFunctionCollectionStub';
|
||||||
|
import { SharedFunctionStub } from '../../../../../stubs/SharedFunctionStub';
|
||||||
|
|
||||||
|
describe('FunctionCallCompiler', () => {
|
||||||
|
describe('compileCall', () => {
|
||||||
|
describe('call', () => {
|
||||||
|
it('throws with undefined call', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'undefined call';
|
||||||
|
const call = undefined;
|
||||||
|
const functions = new SharedFunctionCollectionStub();
|
||||||
|
const sut = new MockableFunctionCallCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileCall(call, functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws if call is not an object', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'called function(s) must be an object';
|
||||||
|
const invalidCalls: readonly any[] = ['string', 33];
|
||||||
|
const sut = new MockableFunctionCallCompiler();
|
||||||
|
const functions = new SharedFunctionCollectionStub();
|
||||||
|
invalidCalls.forEach((invalidCall) => {
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileCall(invalidCall, functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('throws if call sequence has undefined call', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'undefined function call';
|
||||||
|
const call: FunctionCallData[] = [
|
||||||
|
{ function: 'function-name' },
|
||||||
|
undefined,
|
||||||
|
];
|
||||||
|
const functions = new SharedFunctionCollectionStub();
|
||||||
|
const sut = new MockableFunctionCallCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileCall(call, functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws if call sequence has undefined function name', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'empty function name called';
|
||||||
|
const call: FunctionCallData[] = [
|
||||||
|
{ function: 'function-name' },
|
||||||
|
{ function: undefined },
|
||||||
|
];
|
||||||
|
const functions = new SharedFunctionCollectionStub();
|
||||||
|
const sut = new MockableFunctionCallCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileCall(call, functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws if call parameters does not match function parameters', () => {
|
||||||
|
// arrange
|
||||||
|
const unexpectedCallParameterName = 'unexpected-parameter-name';
|
||||||
|
const func = new SharedFunctionStub()
|
||||||
|
.withName('test-function-name')
|
||||||
|
.withParameters('another-parameter');
|
||||||
|
const expectedError = `function "${func.name}" has unexpected parameter(s) provided: "${unexpectedCallParameterName}"`;
|
||||||
|
const sut = new MockableFunctionCallCompiler();
|
||||||
|
const params: FunctionCallParametersData = {
|
||||||
|
[`${unexpectedCallParameterName}`]: 'unexpected-parameter-value',
|
||||||
|
};
|
||||||
|
const call: FunctionCallData = { function: func.name, parameters: params };
|
||||||
|
const functions = new SharedFunctionCollectionStub().withFunction(func);
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileCall(call, functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('functions', () => {
|
||||||
|
it('throws with undefined functions', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'undefined functions';
|
||||||
|
const call: FunctionCallData = { function: 'function-call' };
|
||||||
|
const functions = undefined;
|
||||||
|
const sut = new MockableFunctionCallCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileCall(call, functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws if function does not exist', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'function does not exist';
|
||||||
|
const call: FunctionCallData = { function: 'function-call' };
|
||||||
|
const functions: ISharedFunctionCollection = {
|
||||||
|
getFunctionByName: () => { throw new Error(expectedError); },
|
||||||
|
};
|
||||||
|
const sut = new MockableFunctionCallCompiler();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compileCall(call, functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('builds code as expected', () => {
|
||||||
|
describe('builds single call as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const parametersTestCases = [
|
||||||
|
{
|
||||||
|
name: 'undefined parameters',
|
||||||
|
parameters: undefined,
|
||||||
|
parameterValues: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'empty parameters',
|
||||||
|
parameters: [],
|
||||||
|
parameterValues: { },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-empty parameters',
|
||||||
|
parameters: [ 'param1', 'param2' ],
|
||||||
|
parameterValues: { param1: 'value1', param2: 'value2' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const testCase of parametersTestCases) {
|
||||||
|
it(testCase.name, () => {
|
||||||
|
const expectedExecute = `expected-execute`;
|
||||||
|
const expectedRevert = `expected-revert`;
|
||||||
|
const func = new SharedFunctionStub().withParameters(...testCase.parameters);
|
||||||
|
const functions = new SharedFunctionCollectionStub().withFunction(func);
|
||||||
|
const call: FunctionCallData = { function: func.name, parameters: testCase.parameterValues };
|
||||||
|
const expressionsCompilerMock = new ExpressionsCompilerStub()
|
||||||
|
.setup(func.code, testCase.parameterValues, expectedExecute)
|
||||||
|
.setup(func.revertCode, testCase.parameterValues, expectedRevert);
|
||||||
|
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
|
||||||
|
// act
|
||||||
|
const actual = sut.compileCall(call, functions);
|
||||||
|
// assert
|
||||||
|
expect(actual.code).to.equal(expectedExecute);
|
||||||
|
expect(actual.revertCode).to.equal(expectedRevert);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('builds call sequence as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const firstFunction = new SharedFunctionStub()
|
||||||
|
.withName('first-function-name')
|
||||||
|
.withCode('first-function-code')
|
||||||
|
.withRevertCode('first-function-revert-code');
|
||||||
|
const secondFunction = new SharedFunctionStub()
|
||||||
|
.withName('second-function-name')
|
||||||
|
.withParameters('testParameter')
|
||||||
|
.withCode('second-function-code')
|
||||||
|
.withRevertCode('second-function-revert-code');
|
||||||
|
const secondCallArguments = { testParameter: 'testValue' };
|
||||||
|
const call: FunctionCallData[] = [
|
||||||
|
{ function: firstFunction.name },
|
||||||
|
{ function: secondFunction.name, parameters: secondCallArguments },
|
||||||
|
];
|
||||||
|
const expressionsCompilerMock = new ExpressionsCompilerStub()
|
||||||
|
.setup(firstFunction.code, {}, firstFunction.code)
|
||||||
|
.setup(firstFunction.revertCode, {}, firstFunction.revertCode)
|
||||||
|
.setup(secondFunction.code, secondCallArguments, secondFunction.code)
|
||||||
|
.setup(secondFunction.revertCode, secondCallArguments, secondFunction.revertCode);
|
||||||
|
const expectedExecute = `${firstFunction.code}\n${secondFunction.code}`;
|
||||||
|
const expectedRevert = `${firstFunction.revertCode}\n${secondFunction.revertCode}`;
|
||||||
|
const functions = new SharedFunctionCollectionStub()
|
||||||
|
.withFunction(firstFunction)
|
||||||
|
.withFunction(secondFunction);
|
||||||
|
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
|
||||||
|
// act
|
||||||
|
const actual = sut.compileCall(call, functions);
|
||||||
|
// assert
|
||||||
|
expect(actual.code).to.equal(expectedExecute);
|
||||||
|
expect(actual.revertCode).to.equal(expectedRevert);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class MockableFunctionCallCompiler extends FunctionCallCompiler {
|
||||||
|
constructor(expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub()) {
|
||||||
|
super(expressionsCompiler);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
|
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
|
||||||
import { FunctionData, ScriptData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
import { FunctionData } from 'js-yaml-loader!@/*';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
import { IScriptCompiler } from '@/application/Parser/Script/Compiler/IScriptCompiler';
|
import { IFunctionCompiler } from '@/application/Parser/Script/Compiler/Function/IFunctionCompiler';
|
||||||
|
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler';
|
||||||
|
import { ICompiledCode } from '@/application/Parser/Script/Compiler/FunctionCall/ICompiledCode';
|
||||||
import { LanguageSyntaxStub } from '../../../../stubs/LanguageSyntaxStub';
|
import { LanguageSyntaxStub } from '../../../../stubs/LanguageSyntaxStub';
|
||||||
import { ScriptDataStub } from '../../../../stubs/ScriptDataStub';
|
import { ScriptDataStub } from '../../../../stubs/ScriptDataStub';
|
||||||
import { FunctionDataStub } from '../../../../stubs/FunctionDataStub';
|
import { FunctionDataStub } from '../../../../stubs/FunctionDataStub';
|
||||||
|
import { FunctionCallCompilerStub } from '../../../../stubs/FunctionCallCompilerStub';
|
||||||
|
import { FunctionCompilerStub } from '../../../../stubs/FunctionCompilerStub';
|
||||||
|
import { SharedFunctionCollectionStub } from '../../../../stubs/SharedFunctionCollectionStub';
|
||||||
|
|
||||||
describe('ScriptCompiler', () => {
|
describe('ScriptCompiler', () => {
|
||||||
describe('ctor', () => {
|
describe('ctor', () => {
|
||||||
@@ -22,88 +26,20 @@ describe('ScriptCompiler', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
it('throws if one of the functions is undefined', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = `some functions are undefined`;
|
|
||||||
const functions = [ new FunctionDataStub(), undefined ];
|
|
||||||
// act
|
|
||||||
const act = () => new ScriptCompilerBuilder()
|
|
||||||
.withFunctions(...functions)
|
|
||||||
.build();
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
it('throws when functions have same names', () => {
|
|
||||||
// arrange
|
|
||||||
const name = 'same-func-name';
|
|
||||||
const expectedError = `duplicate function name: "${name}"`;
|
|
||||||
const functions = [
|
|
||||||
new FunctionDataStub().withName(name),
|
|
||||||
new FunctionDataStub().withName(name),
|
|
||||||
];
|
|
||||||
// act
|
|
||||||
const act = () => new ScriptCompilerBuilder()
|
|
||||||
.withFunctions(...functions)
|
|
||||||
.build();
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
it('throws when function parameters have same names', () => {
|
|
||||||
// arrange
|
|
||||||
const parameterName = 'duplicate-parameter';
|
|
||||||
const func = new FunctionDataStub()
|
|
||||||
.withParameters(parameterName, parameterName);
|
|
||||||
const expectedError = `"${func.name}": duplicate parameter name: "${parameterName}"`;
|
|
||||||
// act
|
|
||||||
const act = () => new ScriptCompilerBuilder()
|
|
||||||
.withFunctions(func)
|
|
||||||
.build();
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
describe('throws when when function have duplicate code', () => {
|
|
||||||
it('code', () => {
|
|
||||||
// arrange
|
|
||||||
const code = 'duplicate-code';
|
|
||||||
const expectedError = `duplicate "code" in functions: "${code}"`;
|
|
||||||
const functions = [
|
|
||||||
new FunctionDataStub().withName('func-1').withCode(code),
|
|
||||||
new FunctionDataStub().withName('func-2').withCode(code),
|
|
||||||
];
|
|
||||||
// act
|
|
||||||
const act = () => new ScriptCompilerBuilder()
|
|
||||||
.withFunctions(...functions)
|
|
||||||
.build();
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
it('revertCode', () => {
|
|
||||||
// arrange
|
|
||||||
const revertCode = 'duplicate-revert-code';
|
|
||||||
const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`;
|
|
||||||
const functions = [
|
|
||||||
new FunctionDataStub().withName('func-1').withCode('code-1').withRevertCode(revertCode),
|
|
||||||
new FunctionDataStub().withName('func-2').withCode('code-2').withRevertCode(revertCode),
|
|
||||||
];
|
|
||||||
// act
|
|
||||||
const act = () => new ScriptCompilerBuilder()
|
|
||||||
.withFunctions(...functions)
|
|
||||||
.build();
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('can construct with empty functions', () => {
|
|
||||||
// arrange
|
|
||||||
const builder = new ScriptCompilerBuilder()
|
|
||||||
.withEmptyFunctions();
|
|
||||||
// act
|
|
||||||
const act = () => builder.build();
|
|
||||||
// assert
|
|
||||||
expect(act).to.not.throw();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('canCompile', () => {
|
describe('canCompile', () => {
|
||||||
|
it('throws if script is undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'undefined script';
|
||||||
|
const argument = undefined;
|
||||||
|
const builder = new ScriptCompilerBuilder()
|
||||||
|
.withEmptyFunctions()
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const act = () => builder.canCompile(argument);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
it('returns true if "call" is defined', () => {
|
it('returns true if "call" is defined', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new ScriptCompilerBuilder()
|
const sut = new ScriptCompilerBuilder()
|
||||||
@@ -128,274 +64,97 @@ describe('ScriptCompiler', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('compile', () => {
|
describe('compile', () => {
|
||||||
describe('invalid state', () => {
|
it('throws if script is undefined', () => {
|
||||||
it('throws if functions are empty', () => {
|
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'cannot compile without shared functions';
|
const expectedError = 'undefined script';
|
||||||
const sut = new ScriptCompilerBuilder()
|
const argument = undefined;
|
||||||
|
const builder = new ScriptCompilerBuilder()
|
||||||
.withEmptyFunctions()
|
.withEmptyFunctions()
|
||||||
.build();
|
.build();
|
||||||
|
// act
|
||||||
|
const act = () => builder.compile(argument);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('returns code as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected: ICompiledCode = {
|
||||||
|
code: 'expected-code',
|
||||||
|
revertCode: 'expected-revert-code',
|
||||||
|
};
|
||||||
const script = ScriptDataStub.createWithCall();
|
const script = ScriptDataStub.createWithCall();
|
||||||
|
const functions = [ FunctionDataStub.createWithCode().withName('existing-func') ];
|
||||||
|
const compiledFunctions = new SharedFunctionCollectionStub();
|
||||||
|
const compilerMock = new FunctionCompilerStub();
|
||||||
|
compilerMock.setup(functions, compiledFunctions);
|
||||||
|
const callCompilerMock = new FunctionCallCompilerStub();
|
||||||
|
callCompilerMock.setup(script.call, compiledFunctions, expected);
|
||||||
|
const sut = new ScriptCompilerBuilder()
|
||||||
|
.withFunctions(...functions)
|
||||||
|
.withFunctionCompiler(compilerMock)
|
||||||
|
.withFunctionCallCompiler(callCompilerMock)
|
||||||
|
.build();
|
||||||
// act
|
// act
|
||||||
const act = () => sut.compile(script);
|
const code = sut.compile(script);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(code.execute).to.equal(expected.code);
|
||||||
|
expect(code.revert).to.equal(expected.revertCode);
|
||||||
});
|
});
|
||||||
it('throws if call is not an object', () => {
|
it('creates with expected syntax', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'called function(s) must be an object';
|
let isUsed = false;
|
||||||
const invalidValues = [undefined, 'string', 33];
|
const syntax: ILanguageSyntax = {
|
||||||
|
get commentDelimiters() {
|
||||||
|
isUsed = true;
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
get commonCodeParts() {
|
||||||
|
isUsed = true;
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
||||||
const sut = new ScriptCompilerBuilder()
|
const sut = new ScriptCompilerBuilder()
|
||||||
.withSomeFunctions()
|
.withSomeFunctions()
|
||||||
.build();
|
|
||||||
invalidValues.forEach((invalidValue) => {
|
|
||||||
const script = ScriptDataStub.createWithoutCallOrCodes() // because call ctor overwrites "undefined"
|
|
||||||
.withCall(invalidValue as any);
|
|
||||||
// act
|
|
||||||
const act = () => sut.compile(script);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('invalid function reference', () => {
|
|
||||||
it('throws if function does not exist', () => {
|
|
||||||
// arrange
|
|
||||||
const sut = new ScriptCompilerBuilder()
|
|
||||||
.withSomeFunctions()
|
|
||||||
.build();
|
|
||||||
const nonExistingFunctionName = 'non-existing-func';
|
|
||||||
const expectedError = `called function is not defined "${nonExistingFunctionName}"`;
|
|
||||||
const call: ScriptFunctionCallData = { function: nonExistingFunctionName };
|
|
||||||
const script = ScriptDataStub.createWithCall(call);
|
|
||||||
// act
|
|
||||||
const act = () => sut.compile(script);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
it('throws if function is undefined', () => {
|
|
||||||
// arrange
|
|
||||||
const existingFunctionName = 'existing-func';
|
|
||||||
const sut = new ScriptCompilerBuilder()
|
|
||||||
.withFunctionNames(existingFunctionName)
|
|
||||||
.build();
|
|
||||||
const call: ScriptFunctionCallData = [
|
|
||||||
{ function: existingFunctionName },
|
|
||||||
undefined,
|
|
||||||
];
|
|
||||||
const script = ScriptDataStub.createWithCall(call);
|
|
||||||
const expectedError = `undefined function call in script "${script.name}"`;
|
|
||||||
// act
|
|
||||||
const act = () => sut.compile(script);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
it('throws if function name is not given', () => {
|
|
||||||
// arrange
|
|
||||||
const existingFunctionName = 'existing-func';
|
|
||||||
const sut = new ScriptCompilerBuilder()
|
|
||||||
.withFunctionNames(existingFunctionName)
|
|
||||||
.build();
|
|
||||||
const call: FunctionCallData[] = [
|
|
||||||
{ function: existingFunctionName },
|
|
||||||
{ function: undefined }];
|
|
||||||
const script = ScriptDataStub.createWithCall(call);
|
|
||||||
const expectedError = `empty function name called in script "${script.name}"`;
|
|
||||||
// act
|
|
||||||
const act = () => sut.compile(script);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('throws if provided parameters does not match given ones', () => {
|
|
||||||
// arrange
|
|
||||||
const unexpectedParameterName = 'unexpected-parameter-name';
|
|
||||||
const functionName = 'test-function-name';
|
|
||||||
const expectedError = `function "${functionName}" has unexpected parameter(s) provided: "${unexpectedParameterName}"`;
|
|
||||||
const sut = new ScriptCompilerBuilder()
|
|
||||||
.withFunctions(
|
|
||||||
new FunctionDataStub()
|
|
||||||
.withName(functionName)
|
|
||||||
.withParameters('another-parameter'))
|
|
||||||
.build();
|
|
||||||
const params: FunctionCallParametersData = {};
|
|
||||||
params[unexpectedParameterName] = 'unexpected-parameter-value';
|
|
||||||
const call: ScriptFunctionCallData = { function: functionName, parameters: params };
|
|
||||||
const script = ScriptDataStub.createWithCall(call);
|
|
||||||
// act
|
|
||||||
const act = () => sut.compile(script);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('builds code as expected', () => {
|
|
||||||
it('creates code with expected syntax', () => { // test through script validation logic
|
|
||||||
// act
|
|
||||||
const commentDelimiter = 'should not throw';
|
|
||||||
const syntax = new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter);
|
|
||||||
const func = new FunctionDataStub()
|
|
||||||
.withCode(`${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`);
|
|
||||||
const sut = new ScriptCompilerBuilder()
|
|
||||||
.withFunctions(func)
|
|
||||||
.withSyntax(syntax)
|
.withSyntax(syntax)
|
||||||
.build();
|
.build();
|
||||||
const call: FunctionCallData = { function: func.name };
|
const scriptData = ScriptDataStub.createWithCall();
|
||||||
const script = ScriptDataStub.createWithCall(call);
|
|
||||||
// act
|
// act
|
||||||
const act = () => sut.compile(script);
|
sut.compile(scriptData);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.not.throw();
|
expect(isUsed).to.equal(true);
|
||||||
});
|
});
|
||||||
it('builds single call as expected', () => {
|
it('rethrows error from ScriptCode with script name', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const functionName = 'testSharedFunction';
|
const scriptName = 'scriptName'; // // arrange
|
||||||
const expectedExecute = `expected-execute`;
|
const innerError = 'innerError';
|
||||||
const expectedRevert = `expected-revert`;
|
const expectedError = `Script "${scriptName}" ${innerError}`;
|
||||||
const func = new FunctionDataStub()
|
const callCompiler: IFunctionCallCompiler = {
|
||||||
.withName(functionName)
|
compileCall: () => { throw new Error(innerError); },
|
||||||
.withCode(expectedExecute)
|
};
|
||||||
.withRevertCode(expectedRevert);
|
const scriptData = ScriptDataStub.createWithCall()
|
||||||
|
.withName(scriptName);
|
||||||
const sut = new ScriptCompilerBuilder()
|
const sut = new ScriptCompilerBuilder()
|
||||||
.withFunctions(func)
|
.withSomeFunctions()
|
||||||
|
.withFunctionCallCompiler(callCompiler)
|
||||||
.build();
|
.build();
|
||||||
const call: FunctionCallData = { function: functionName };
|
|
||||||
const script = ScriptDataStub.createWithCall(call);
|
|
||||||
// act
|
// act
|
||||||
const actual = sut.compile(script);
|
const act = () => sut.compile(scriptData);
|
||||||
// assert
|
|
||||||
expect(actual.execute).to.equal(expectedExecute);
|
|
||||||
expect(actual.revert).to.equal(expectedRevert);
|
|
||||||
});
|
|
||||||
it('builds call sequence as expected', () => {
|
|
||||||
// arrange
|
|
||||||
const firstFunction = new FunctionDataStub()
|
|
||||||
.withName('first-function-name')
|
|
||||||
.withCode('first-function-code')
|
|
||||||
.withRevertCode('first-function-revert-code');
|
|
||||||
const secondFunction = new FunctionDataStub()
|
|
||||||
.withName('second-function-name')
|
|
||||||
.withCode('second-function-code')
|
|
||||||
.withRevertCode('second-function-revert-code');
|
|
||||||
const expectedExecute = `${firstFunction.code}\n${secondFunction.code}`;
|
|
||||||
const expectedRevert = `${firstFunction.revertCode}\n${secondFunction.revertCode}`;
|
|
||||||
const sut = new ScriptCompilerBuilder()
|
|
||||||
.withFunctions(firstFunction, secondFunction)
|
|
||||||
.build();
|
|
||||||
const call: FunctionCallData[] = [
|
|
||||||
{ function: firstFunction.name },
|
|
||||||
{ function: secondFunction.name },
|
|
||||||
];
|
|
||||||
const script = ScriptDataStub.createWithCall(call);
|
|
||||||
// act
|
|
||||||
const actual = sut.compile(script);
|
|
||||||
// assert
|
|
||||||
expect(actual.execute).to.equal(expectedExecute);
|
|
||||||
expect(actual.revert).to.equal(expectedRevert);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('parameter substitution', () => {
|
|
||||||
describe('substitutes as expected', () => {
|
|
||||||
it('with different parameters', () => {
|
|
||||||
// arrange
|
|
||||||
const env = new TestEnvironment({
|
|
||||||
code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
|
|
||||||
parameters: {
|
|
||||||
firstParameter: 'llo',
|
|
||||||
secondParameter: 'world',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const expected = env.expect('Hello world!');
|
|
||||||
// act
|
|
||||||
const actual = env.sut.compile(env.script);
|
|
||||||
// assert
|
|
||||||
expect(actual).to.deep.equal(expected);
|
|
||||||
});
|
|
||||||
it('with single parameter', () => {
|
|
||||||
// arrange
|
|
||||||
const env = new TestEnvironment({
|
|
||||||
code: '{{ $parameter }}!',
|
|
||||||
parameters: {
|
|
||||||
parameter: 'Hodor',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const expected = env.expect('Hodor!');
|
|
||||||
// act
|
|
||||||
const actual = env.sut.compile(env.script);
|
|
||||||
// assert
|
|
||||||
expect(actual).to.deep.equal(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('throws when parameters are undefined', () => {
|
|
||||||
// arrange
|
|
||||||
const env = new TestEnvironment({
|
|
||||||
code: '{{ $parameter }} {{ $parameter }}!',
|
|
||||||
});
|
|
||||||
const expectedError = 'no parameters defined, expected: "parameter"';
|
|
||||||
// act
|
|
||||||
const act = () => env.sut.compile(env.script);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
it('throws when parameter value is not provided', () => {
|
|
||||||
// arrange
|
|
||||||
const env = new TestEnvironment({
|
|
||||||
code: '{{ $parameter }} {{ $parameter }}!',
|
|
||||||
parameters: {
|
|
||||||
parameter: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const expectedError = 'parameter value is not provided for "parameter" in function call';
|
|
||||||
// act
|
|
||||||
const act = () => env.sut.compile(env.script);
|
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
interface ITestCase {
|
|
||||||
code: string;
|
|
||||||
parameters?: FunctionCallParametersData;
|
|
||||||
}
|
|
||||||
class TestEnvironment {
|
|
||||||
public readonly sut: IScriptCompiler;
|
|
||||||
public readonly script: ScriptData;
|
|
||||||
constructor(testCase: ITestCase) {
|
|
||||||
const functionName = 'testFunction';
|
|
||||||
const parameters = testCase.parameters ? Object.keys(testCase.parameters) : [];
|
|
||||||
const func = new FunctionDataStub()
|
|
||||||
.withName(functionName)
|
|
||||||
.withParameters(...parameters)
|
|
||||||
.withCode(this.getCode(testCase.code, 'execute'))
|
|
||||||
.withRevertCode(this.getCode(testCase.code, 'revert'));
|
|
||||||
const syntax = new LanguageSyntaxStub();
|
|
||||||
this.sut = new ScriptCompiler([func], syntax);
|
|
||||||
const call: FunctionCallData = {
|
|
||||||
function: functionName,
|
|
||||||
parameters: testCase.parameters,
|
|
||||||
};
|
|
||||||
this.script = ScriptDataStub.createWithCall(call);
|
|
||||||
}
|
|
||||||
public expect(code: string): IScriptCode {
|
|
||||||
return {
|
|
||||||
execute: this.getCode(code, 'execute'),
|
|
||||||
revert: this.getCode(code, 'revert'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
private getCode(text: string, type: 'execute' | 'revert'): string {
|
|
||||||
return `${text} (${type})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// tslint:disable-next-line:max-classes-per-file
|
|
||||||
class ScriptCompilerBuilder {
|
class ScriptCompilerBuilder {
|
||||||
private static createFunctions(...names: string[]): FunctionData[] {
|
private static createFunctions(...names: string[]): FunctionData[] {
|
||||||
return names.map((functionName) => {
|
return names.map((functionName) => {
|
||||||
return new FunctionDataStub().withName(functionName);
|
return FunctionDataStub.createWithCode().withName(functionName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
private functions: FunctionData[];
|
private functions: FunctionData[];
|
||||||
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
||||||
|
private functionCompiler: IFunctionCompiler = new FunctionCompilerStub();
|
||||||
|
private callCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub();
|
||||||
public withFunctions(...functions: FunctionData[]): ScriptCompilerBuilder {
|
public withFunctions(...functions: FunctionData[]): ScriptCompilerBuilder {
|
||||||
this.functions = functions;
|
this.functions = functions;
|
||||||
return this;
|
return this;
|
||||||
@@ -416,10 +175,18 @@ class ScriptCompilerBuilder {
|
|||||||
this.syntax = syntax;
|
this.syntax = syntax;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
public withFunctionCompiler(functionCompiler: IFunctionCompiler): ScriptCompilerBuilder {
|
||||||
|
this.functionCompiler = functionCompiler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withFunctionCallCompiler(callCompiler: IFunctionCallCompiler): ScriptCompilerBuilder {
|
||||||
|
this.callCompiler = callCompiler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
public build(): ScriptCompiler {
|
public build(): ScriptCompiler {
|
||||||
if (!this.functions) {
|
if (!this.functions) {
|
||||||
throw new Error('Function behavior not defined');
|
throw new Error('Function behavior not defined');
|
||||||
}
|
}
|
||||||
return new ScriptCompiler(this.functions, this.syntax);
|
return new ScriptCompiler(this.functions, this.syntax, this.functionCompiler, this.callCompiler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ describe('ScriptParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('code', () => {
|
describe('code', () => {
|
||||||
it('parses code as expected', () => {
|
it('parses "execute" as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = 'expected-code';
|
const expected = 'expected-code';
|
||||||
const script = ScriptDataStub
|
const script = ScriptDataStub
|
||||||
@@ -125,7 +125,7 @@ describe('ScriptParser', () => {
|
|||||||
const actual = parsed.code.execute;
|
const actual = parsed.code.execute;
|
||||||
expect(actual).to.equal(expected);
|
expect(actual).to.equal(expected);
|
||||||
});
|
});
|
||||||
it('parses revertCode as expected', () => {
|
it('parses "revert" as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = 'expected-revert-code';
|
const expected = 'expected-revert-code';
|
||||||
const script = ScriptDataStub
|
const script = ScriptDataStub
|
||||||
|
|||||||
@@ -6,23 +6,9 @@ import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|||||||
import { LanguageSyntaxStub } from '../stubs/LanguageSyntaxStub';
|
import { LanguageSyntaxStub } from '../stubs/LanguageSyntaxStub';
|
||||||
|
|
||||||
describe('ScriptCode', () => {
|
describe('ScriptCode', () => {
|
||||||
describe('scriptName', () => {
|
|
||||||
it('throws if undefined', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = 'name is undefined';
|
|
||||||
const name = undefined;
|
|
||||||
// act
|
|
||||||
const act = () => new ScriptCodeBuilder()
|
|
||||||
.withName(name)
|
|
||||||
.build();
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('code', () => {
|
describe('code', () => {
|
||||||
describe('throws with invalid code', () => {
|
describe('throws with invalid code', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const scriptName = 'test-script';
|
|
||||||
const testCases = [
|
const testCases = [
|
||||||
{
|
{
|
||||||
name: 'throws when "execute" and "revert" are same',
|
name: 'throws when "execute" and "revert" are same',
|
||||||
@@ -30,7 +16,7 @@ describe('ScriptCode', () => {
|
|||||||
execute: 'same',
|
execute: 'same',
|
||||||
revert: 'same',
|
revert: 'same',
|
||||||
},
|
},
|
||||||
expectedError: `${scriptName} (revert): Code itself and its reverting code cannot be the same`,
|
expectedError: `(revert): Code itself and its reverting code cannot be the same`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'cannot construct with undefined "execute"',
|
name: 'cannot construct with undefined "execute"',
|
||||||
@@ -38,7 +24,7 @@ describe('ScriptCode', () => {
|
|||||||
execute: undefined,
|
execute: undefined,
|
||||||
revert: 'code',
|
revert: 'code',
|
||||||
},
|
},
|
||||||
expectedError: `code of ${scriptName} is empty or undefined`,
|
expectedError: `code is empty or undefined`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'cannot construct with empty "execute"',
|
name: 'cannot construct with empty "execute"',
|
||||||
@@ -46,14 +32,13 @@ describe('ScriptCode', () => {
|
|||||||
execute: '',
|
execute: '',
|
||||||
revert: 'code',
|
revert: 'code',
|
||||||
},
|
},
|
||||||
expectedError: `code of ${scriptName} is empty or undefined`,
|
expectedError: `code is empty or undefined`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
it(testCase.name, () => {
|
it(testCase.name, () => {
|
||||||
// act
|
// act
|
||||||
const act = () => new ScriptCodeBuilder()
|
const act = () => new ScriptCodeBuilder()
|
||||||
.withName(scriptName)
|
|
||||||
.withExecute( testCase.code.execute)
|
.withExecute( testCase.code.execute)
|
||||||
.withRevert(testCase.code.revert)
|
.withRevert(testCase.code.revert)
|
||||||
.build();
|
.build();
|
||||||
@@ -64,39 +49,35 @@ describe('ScriptCode', () => {
|
|||||||
});
|
});
|
||||||
describe('throws with invalid code in both "execute" or "revert"', () => {
|
describe('throws with invalid code in both "execute" or "revert"', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const scriptName = 'script-name';
|
|
||||||
const testCases = [
|
const testCases = [
|
||||||
{
|
{
|
||||||
testName: 'cannot construct with duplicate lines',
|
testName: 'cannot construct with duplicate lines',
|
||||||
code: 'duplicate\nduplicate\ntest\nduplicate',
|
code: 'duplicate\nduplicate\ntest\nduplicate',
|
||||||
expectedMessage: 'Duplicates detected in script "$scriptName":\n duplicate\nduplicate',
|
expectedMessage: 'Duplicates detected in script :\n duplicate\nduplicate',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testName: 'cannot construct with empty lines',
|
testName: 'cannot construct with empty lines',
|
||||||
code: 'line1\n\n\nline2',
|
code: 'line1\n\n\nline2',
|
||||||
expectedMessage: 'script has empty lines "$scriptName"',
|
expectedMessage: 'script has empty lines',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
// act
|
// act
|
||||||
const actions = [];
|
const actions = [];
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
const substituteScriptName = (name: string) => testCase.expectedMessage.replace('$scriptName', name);
|
|
||||||
actions.push(...[
|
actions.push(...[
|
||||||
{
|
{
|
||||||
act: () => new ScriptCodeBuilder()
|
act: () => new ScriptCodeBuilder()
|
||||||
.withName(scriptName)
|
|
||||||
.withExecute(testCase.code)
|
.withExecute(testCase.code)
|
||||||
.build(),
|
.build(),
|
||||||
testName: `execute: ${testCase.testName}`,
|
testName: `execute: ${testCase.testName}`,
|
||||||
expectedMessage: substituteScriptName(scriptName),
|
expectedMessage: testCase.expectedMessage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
act: () => new ScriptCodeBuilder()
|
act: () => new ScriptCodeBuilder()
|
||||||
.withName(scriptName)
|
|
||||||
.withRevert(testCase.code)
|
.withRevert(testCase.code)
|
||||||
.build(),
|
.build(),
|
||||||
testName: `revert: ${testCase.testName}`,
|
testName: `revert: ${testCase.testName}`,
|
||||||
expectedMessage: substituteScriptName(`${scriptName} (revert)`),
|
expectedMessage: `(revert): ${testCase.expectedMessage}`,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -168,18 +149,26 @@ describe('ScriptCode', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('syntax', () => {
|
||||||
|
it('throws if undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'undefined syntax';
|
||||||
|
const syntax = undefined;
|
||||||
|
// act
|
||||||
|
const act = () => new ScriptCodeBuilder()
|
||||||
|
.withSyntax(syntax)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
class ScriptCodeBuilder {
|
class ScriptCodeBuilder {
|
||||||
public execute = 'default-execute-code';
|
public execute = 'default-execute-code';
|
||||||
public revert = '';
|
public revert = '';
|
||||||
public scriptName = 'default-script-name';
|
|
||||||
public syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
public syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
||||||
|
|
||||||
public withName(name: string) {
|
|
||||||
this.scriptName = name;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
public withExecute(execute: string) {
|
public withExecute(execute: string) {
|
||||||
this.execute = execute;
|
this.execute = execute;
|
||||||
return this;
|
return this;
|
||||||
@@ -197,7 +186,6 @@ class ScriptCodeBuilder {
|
|||||||
return new ScriptCode(
|
return new ScriptCode(
|
||||||
this.execute,
|
this.execute,
|
||||||
this.revert,
|
this.revert,
|
||||||
this.scriptName,
|
|
||||||
this.syntax);
|
this.syntax);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
249
tests/unit/infrastructure/CodeRunner.spec.ts
Normal file
249
tests/unit/infrastructure/CodeRunner.spec.ts
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import { EnvironmentStub } from './../stubs/EnvironmentStub';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { runCodeAsync } from '@/infrastructure/CodeRunner';
|
||||||
|
|
||||||
|
describe('CodeRunner', () => {
|
||||||
|
describe('runCodeAsync', () => {
|
||||||
|
it('creates temporary directory recursively', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedDir = 'expected-dir';
|
||||||
|
const folderName = 'privacy.sexy';
|
||||||
|
const context = new TestContext();
|
||||||
|
context.mocks.os.setupTmpdir('tmp');
|
||||||
|
context.mocks.path.setupJoin(expectedDir, 'tmp', folderName);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await context
|
||||||
|
.withFolderName(folderName)
|
||||||
|
.runCodeAsync();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(context.mocks.fs.mkdirHistory.length).to.equal(1);
|
||||||
|
expect(context.mocks.fs.mkdirHistory[0].isRecursive).to.equal(true);
|
||||||
|
expect(context.mocks.fs.mkdirHistory[0].path).to.equal(expectedDir);
|
||||||
|
});
|
||||||
|
it('creates a file with expected code and path', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCode = 'expected-code';
|
||||||
|
const expectedFilePath = 'expected-file-path';
|
||||||
|
|
||||||
|
const extension = '.sh';
|
||||||
|
const expectedName = `run.${extension}`;
|
||||||
|
const folderName = 'privacy.sexy';
|
||||||
|
const context = new TestContext();
|
||||||
|
context.mocks.os.setupTmpdir('tmp');
|
||||||
|
context.mocks.path.setupJoin('folder', 'tmp', folderName);
|
||||||
|
context.mocks.path.setupJoin(expectedFilePath, 'folder', expectedName);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await context
|
||||||
|
.withCode(expectedCode)
|
||||||
|
.withFolderName(folderName)
|
||||||
|
.withExtension(extension)
|
||||||
|
.runCodeAsync();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(context.mocks.fs.writeFileHistory.length).to.equal(1);
|
||||||
|
expect(context.mocks.fs.writeFileHistory[0].data).to.equal(expectedCode);
|
||||||
|
expect(context.mocks.fs.writeFileHistory[0].path).to.equal(expectedFilePath);
|
||||||
|
});
|
||||||
|
it('set file permissions as expected', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedMode = '755';
|
||||||
|
const expectedFilePath = 'expected-file-path';
|
||||||
|
|
||||||
|
const extension = '.sh';
|
||||||
|
const expectedName = `run.${extension}`;
|
||||||
|
const folderName = 'privacy.sexy';
|
||||||
|
const context = new TestContext();
|
||||||
|
context.mocks.os.setupTmpdir('tmp');
|
||||||
|
context.mocks.path.setupJoin('folder', 'tmp', folderName);
|
||||||
|
context.mocks.path.setupJoin(expectedFilePath, 'folder', expectedName);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await context
|
||||||
|
.withFolderName(folderName)
|
||||||
|
.withExtension(extension)
|
||||||
|
.runCodeAsync();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(context.mocks.fs.chmodCallHistory.length).to.equal(1);
|
||||||
|
expect(context.mocks.fs.chmodCallHistory[0].mode).to.equal(expectedMode);
|
||||||
|
expect(context.mocks.fs.chmodCallHistory[0].path).to.equal(expectedFilePath);
|
||||||
|
});
|
||||||
|
describe('executes as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const filePath = 'expected-file-path';
|
||||||
|
const testData = [ {
|
||||||
|
os: OperatingSystem.Windows,
|
||||||
|
expected: filePath,
|
||||||
|
}, {
|
||||||
|
os: OperatingSystem.macOS,
|
||||||
|
expected: `open -a Terminal.app ${filePath}`,
|
||||||
|
}];
|
||||||
|
for (const data of testData) {
|
||||||
|
it(`returns ${data.expected} on ${OperatingSystem[data.os]}`, async () => {
|
||||||
|
const context = new TestContext();
|
||||||
|
context.mocks.os.setupTmpdir('non-important-temp-dir-name');
|
||||||
|
context.mocks.path.setupJoinSequence('non-important-folder-name', filePath);
|
||||||
|
context.withOs(data.os);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await context
|
||||||
|
.withOs(data.os)
|
||||||
|
.runCodeAsync();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(context.mocks.child_process.executionHistory.length).to.equal(1);
|
||||||
|
expect(context.mocks.child_process.executionHistory[0]).to.equal(data.expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('runs in expected order', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedOrder = [ NodeJsCommand.mkdir, NodeJsCommand.writeFile, NodeJsCommand.chmod ];
|
||||||
|
const context = new TestContext();
|
||||||
|
context.mocks.os.setupTmpdir('non-important-temp-dir-name');
|
||||||
|
context.mocks.path.setupJoinSequence('non-important-folder-name1', 'non-important-folder-name2');
|
||||||
|
|
||||||
|
// act
|
||||||
|
await context.runCodeAsync();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const actualOrder = context.mocks.commandHistory.filter((command) => expectedOrder.includes(command));
|
||||||
|
expect(expectedOrder).to.deep.equal(actualOrder);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class TestContext {
|
||||||
|
public mocks = getNodeJsMocks();
|
||||||
|
|
||||||
|
private code: string = 'code';
|
||||||
|
private folderName: string = 'folderName';
|
||||||
|
private fileExtension: string = 'fileExtension';
|
||||||
|
private env = mockEnvironment(OperatingSystem.Windows);
|
||||||
|
|
||||||
|
public async runCodeAsync(): Promise<void> {
|
||||||
|
await runCodeAsync(this.code, this.folderName, this.fileExtension, this.mocks, this.env);
|
||||||
|
}
|
||||||
|
public withOs(os: OperatingSystem) {
|
||||||
|
this.env = mockEnvironment(os);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withFolderName(folderName: string) {
|
||||||
|
this.folderName = folderName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withCode(code: string) {
|
||||||
|
this.code = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withExtension(fileExtension: string) {
|
||||||
|
this.fileExtension = fileExtension;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockEnvironment(os: OperatingSystem) {
|
||||||
|
return new EnvironmentStub().withOs(os);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum NodeJsCommand { tmpdir, join, exec, mkdir, writeFile, chmod }
|
||||||
|
|
||||||
|
function getNodeJsMocks() {
|
||||||
|
const commandHistory = new Array<NodeJsCommand>();
|
||||||
|
return {
|
||||||
|
os: mockOs(commandHistory),
|
||||||
|
path: mockPath(commandHistory),
|
||||||
|
fs: mockNodeFs(commandHistory),
|
||||||
|
child_process: mockChildProcess(commandHistory),
|
||||||
|
commandHistory,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockOs(commandHistory: NodeJsCommand[]) {
|
||||||
|
let tmpDir: string;
|
||||||
|
return {
|
||||||
|
setupTmpdir: (value: string): void => {
|
||||||
|
tmpDir = value;
|
||||||
|
},
|
||||||
|
tmpdir: (): string => {
|
||||||
|
if (!tmpDir) {
|
||||||
|
throw new Error('tmpdir not set up');
|
||||||
|
}
|
||||||
|
commandHistory.push(NodeJsCommand.tmpdir);
|
||||||
|
return tmpDir;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockPath(commandHistory: NodeJsCommand[]) {
|
||||||
|
const sequence = new Array<string>();
|
||||||
|
const scenarios = new Map<string, string>();
|
||||||
|
const getScenarioKey = (paths: string[]) => paths.join('|');
|
||||||
|
return {
|
||||||
|
setupJoin: (returnValue: string, ...paths: string[]): void => {
|
||||||
|
scenarios.set(getScenarioKey(paths), returnValue);
|
||||||
|
},
|
||||||
|
setupJoinSequence: (...valuesToReturn: string[]): void => {
|
||||||
|
sequence.push(...valuesToReturn);
|
||||||
|
sequence.reverse();
|
||||||
|
},
|
||||||
|
join: (...paths: string[]): string => {
|
||||||
|
commandHistory.push(NodeJsCommand.join);
|
||||||
|
if (sequence.length > 0) {
|
||||||
|
return sequence.pop();
|
||||||
|
}
|
||||||
|
const key = getScenarioKey(paths);
|
||||||
|
if (!scenarios.has(key)) {
|
||||||
|
return paths.join('/');
|
||||||
|
}
|
||||||
|
return scenarios.get(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockChildProcess(commandHistory: NodeJsCommand[]) {
|
||||||
|
const executionHistory = new Array<string>();
|
||||||
|
return {
|
||||||
|
exec: (command: string): void => {
|
||||||
|
commandHistory.push(NodeJsCommand.exec);
|
||||||
|
executionHistory.push(command);
|
||||||
|
},
|
||||||
|
executionHistory,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockNodeFs(commandHistory: NodeJsCommand[]) {
|
||||||
|
interface IMkdirCall { path: string; isRecursive: boolean; }
|
||||||
|
interface IWriteFileCall { path: string; data: string; }
|
||||||
|
interface IChmodCall { path: string; mode: string | number; }
|
||||||
|
const mkdirHistory = new Array<IMkdirCall>();
|
||||||
|
const writeFileHistory = new Array<IWriteFileCall>();
|
||||||
|
const chmodCallHistory = new Array<IChmodCall>();
|
||||||
|
return {
|
||||||
|
promises: {
|
||||||
|
mkdir: (path, options) => {
|
||||||
|
commandHistory.push(NodeJsCommand.mkdir);
|
||||||
|
mkdirHistory.push({ path, isRecursive: options && options.recursive });
|
||||||
|
return Promise.resolve(path);
|
||||||
|
},
|
||||||
|
writeFile: (path, data) => {
|
||||||
|
commandHistory.push(NodeJsCommand.writeFile);
|
||||||
|
writeFileHistory.push({ path, data });
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
chmod: (path, mode) => {
|
||||||
|
commandHistory.push(NodeJsCommand.chmod);
|
||||||
|
chmodCallHistory.push({ path, mode });
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mkdirHistory,
|
||||||
|
writeFileHistory,
|
||||||
|
chmodCallHistory,
|
||||||
|
};
|
||||||
|
}
|
||||||
76
tests/unit/presentation/Throttle.spec.ts
Normal file
76
tests/unit/presentation/Throttle.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { throttle, ITimer } from '@/presentation/Throttle';
|
||||||
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||||
|
|
||||||
|
describe('throttle', () => {
|
||||||
|
it('should call the callback immediately', () => {
|
||||||
|
// arrange
|
||||||
|
const timer = new TimerMock();
|
||||||
|
let totalRuns = 0;
|
||||||
|
const callback = () => totalRuns++;
|
||||||
|
const throttleFunc = throttle(callback, 500, timer);
|
||||||
|
// act
|
||||||
|
throttleFunc();
|
||||||
|
// assert
|
||||||
|
expect(totalRuns).to.equal(1);
|
||||||
|
});
|
||||||
|
it('should call the callback again after the timeout', () => {
|
||||||
|
// arrange
|
||||||
|
const timer = new TimerMock();
|
||||||
|
let totalRuns = 0;
|
||||||
|
const callback = () => totalRuns++;
|
||||||
|
const throttleFunc = throttle(callback, 500, timer);
|
||||||
|
// act
|
||||||
|
throttleFunc();
|
||||||
|
totalRuns--;
|
||||||
|
throttleFunc();
|
||||||
|
timer.tick(500);
|
||||||
|
// assert
|
||||||
|
expect(totalRuns).to.equal(1);
|
||||||
|
});
|
||||||
|
it('calls the callback at most once at given time', () => {
|
||||||
|
// arrange
|
||||||
|
const timer = new TimerMock();
|
||||||
|
let totalRuns = 0;
|
||||||
|
const callback = () => totalRuns++;
|
||||||
|
const waitInMs = 500;
|
||||||
|
const totalCalls = 10;
|
||||||
|
const throttleFunc = throttle(callback, waitInMs, timer);
|
||||||
|
// act
|
||||||
|
for (let i = 0; i < totalCalls; i++) {
|
||||||
|
timer.tick(waitInMs / totalCalls * i);
|
||||||
|
throttleFunc();
|
||||||
|
}
|
||||||
|
// assert
|
||||||
|
expect(totalRuns).to.equal(2); // initial and at the end
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class TimerMock implements ITimer {
|
||||||
|
private timeChanged = new EventSource<number>();
|
||||||
|
private subscriptions = new Array<IEventSubscription>();
|
||||||
|
private currentTime = 0;
|
||||||
|
public setTimeout(callback: () => void, ms: number): NodeJS.Timeout {
|
||||||
|
const runTime = this.currentTime + ms;
|
||||||
|
const subscription = this.timeChanged.on((time) => {
|
||||||
|
if (time >= runTime) {
|
||||||
|
callback();
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.subscriptions.push(subscription);
|
||||||
|
return (this.subscriptions.length - 1) as any;
|
||||||
|
}
|
||||||
|
public clearTimeout(timeoutId: NodeJS.Timeout): void {
|
||||||
|
this.subscriptions[timeoutId as any].unsubscribe();
|
||||||
|
}
|
||||||
|
public dateNow(): number {
|
||||||
|
return this.currentTime;
|
||||||
|
}
|
||||||
|
public tick(ms: number): void {
|
||||||
|
this.currentTime = ms;
|
||||||
|
this.timeChanged.notify(this.currentTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
tests/unit/stubs/ExpressionsCompilerStub.ts
Normal file
28
tests/unit/stubs/ExpressionsCompilerStub.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { IExpressionsCompiler, ParameterValueDictionary } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||||
|
|
||||||
|
interface Scenario { code: string; parameters: ParameterValueDictionary; result: string; }
|
||||||
|
|
||||||
|
export class ExpressionsCompilerStub implements IExpressionsCompiler {
|
||||||
|
private readonly scenarios = new Array<Scenario>();
|
||||||
|
public setup(code: string, parameters: ParameterValueDictionary, result: string) {
|
||||||
|
this.scenarios.push({ code, parameters, result });
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string {
|
||||||
|
const scenario = this.scenarios.find((s) => s.code === code && deepEqual(s.parameters, parameters));
|
||||||
|
if (scenario) {
|
||||||
|
return scenario.result;
|
||||||
|
}
|
||||||
|
return `[ExpressionsCompilerStub] code: "${code}"` +
|
||||||
|
`| parameters: ${Object.keys(parameters || {}).map((p) => p + '=' + parameters[p]).join(',')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepEqual(dict1: ParameterValueDictionary, dict2: ParameterValueDictionary) {
|
||||||
|
const dict1Keys = Object.keys(dict1 || {});
|
||||||
|
const dict2Keys = Object.keys(dict2 || {});
|
||||||
|
if (dict1Keys.length !== dict2Keys.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return dict1Keys.every((key) => dict2.hasOwnProperty(key) && dict2[key] === dict1[key]);
|
||||||
|
}
|
||||||
26
tests/unit/stubs/FunctionCallCompilerStub.ts
Normal file
26
tests/unit/stubs/FunctionCallCompilerStub.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
|
import { ICompiledCode } from '@/application/Parser/Script/Compiler/FunctionCall/ICompiledCode';
|
||||||
|
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler';
|
||||||
|
import { FunctionCallData, ScriptFunctionCallData } from 'js-yaml-loader!*';
|
||||||
|
|
||||||
|
interface Scenario { call: ScriptFunctionCallData; functions: ISharedFunctionCollection; result: ICompiledCode; }
|
||||||
|
|
||||||
|
export class FunctionCallCompilerStub implements IFunctionCallCompiler {
|
||||||
|
public scenarios = new Array<Scenario>();
|
||||||
|
public setup(call: ScriptFunctionCallData, functions: ISharedFunctionCollection, result: ICompiledCode) {
|
||||||
|
this.scenarios.push({ call, functions, result });
|
||||||
|
}
|
||||||
|
public compileCall(
|
||||||
|
call: ScriptFunctionCallData,
|
||||||
|
functions: ISharedFunctionCollection): ICompiledCode {
|
||||||
|
const predefined = this.scenarios.find((s) => s.call === call && s.functions === functions);
|
||||||
|
if (predefined) {
|
||||||
|
return predefined.result;
|
||||||
|
}
|
||||||
|
const callee = functions.getFunctionByName((call as FunctionCallData).function);
|
||||||
|
return {
|
||||||
|
code: callee.code,
|
||||||
|
revertCode: callee.revertCode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
41
tests/unit/stubs/FunctionCompilerStub.ts
Normal file
41
tests/unit/stubs/FunctionCompilerStub.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { IFunctionCompiler } from '@/application/Parser/Script/Compiler/Function/IFunctionCompiler';
|
||||||
|
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
|
import { FunctionData } from 'js-yaml-loader!*';
|
||||||
|
import { SharedFunctionCollectionStub } from './SharedFunctionCollectionStub';
|
||||||
|
|
||||||
|
export class FunctionCompilerStub implements IFunctionCompiler {
|
||||||
|
private setupResults = new Array<{
|
||||||
|
functions: readonly FunctionData[],
|
||||||
|
result: ISharedFunctionCollection,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
public setup(functions: readonly FunctionData[], result: ISharedFunctionCollection) {
|
||||||
|
this.setupResults.push( { functions, result });
|
||||||
|
}
|
||||||
|
|
||||||
|
public compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection {
|
||||||
|
const result = this.findResult(functions);
|
||||||
|
return result || new SharedFunctionCollectionStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
private findResult(functions: readonly FunctionData[]): ISharedFunctionCollection {
|
||||||
|
for (const result of this.setupResults) {
|
||||||
|
if (sequenceEqual(result.functions, functions)) {
|
||||||
|
return result.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
|
if (array1.length !== array2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const sortedArray1 = sort(array1);
|
||||||
|
const sortedArray2 = sort(array2);
|
||||||
|
return sortedArray1.every((val, index) => val === sortedArray2[index]);
|
||||||
|
function sort(array: readonly T[]) {
|
||||||
|
return array.slice().sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,31 @@
|
|||||||
import { FunctionData } from 'js-yaml-loader!*';
|
import { FunctionData, ScriptFunctionCallData } from 'js-yaml-loader!*';
|
||||||
|
|
||||||
export class FunctionDataStub implements FunctionData {
|
export class FunctionDataStub implements FunctionData {
|
||||||
|
public static createWithCode() {
|
||||||
|
return new FunctionDataStub()
|
||||||
|
.withCode('stub-code')
|
||||||
|
.withRevertCode('stub-revert-code');
|
||||||
|
}
|
||||||
|
public static createWithCall(call?: ScriptFunctionCallData) {
|
||||||
|
let instance = new FunctionDataStub();
|
||||||
|
if (call) {
|
||||||
|
instance = instance.withCall(call);
|
||||||
|
} else {
|
||||||
|
instance = instance.withMockCall();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
public static createWithoutCallOrCodes() {
|
||||||
|
return new FunctionDataStub();
|
||||||
|
}
|
||||||
|
|
||||||
public name = 'function data stub';
|
public name = 'function data stub';
|
||||||
public code = 'function data stub code';
|
public code: string;
|
||||||
public revertCode = 'function data stub revertCode';
|
public revertCode: string;
|
||||||
public parameters?: readonly string[];
|
public parameters?: readonly string[];
|
||||||
|
public call?: ScriptFunctionCallData;
|
||||||
|
|
||||||
|
private constructor() { }
|
||||||
|
|
||||||
public withName(name: string) {
|
public withName(name: string) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -22,4 +43,12 @@ export class FunctionDataStub implements FunctionData {
|
|||||||
this.revertCode = revertCode;
|
this.revertCode = revertCode;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
public withCall(call: ScriptFunctionCallData) {
|
||||||
|
this.call = call;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withMockCall() {
|
||||||
|
this.call = { function: 'func' };
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export class ScriptDataStub implements ScriptData {
|
|||||||
public recommend = RecommendationLevel[RecommendationLevel.Standard].toLowerCase();
|
public recommend = RecommendationLevel[RecommendationLevel.Standard].toLowerCase();
|
||||||
public docs = ['hello.com'];
|
public docs = ['hello.com'];
|
||||||
|
|
||||||
|
private constructor() { }
|
||||||
|
|
||||||
public withName(name: string): ScriptDataStub {
|
public withName(name: string): ScriptDataStub {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
21
tests/unit/stubs/SharedFunctionCollectionStub.ts
Normal file
21
tests/unit/stubs/SharedFunctionCollectionStub.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
|
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
|
|
||||||
|
export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
|
||||||
|
private readonly functions = new Map<string, ISharedFunction>();
|
||||||
|
public withFunction(func: ISharedFunction) {
|
||||||
|
this.functions.set(func.name, func);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public getFunctionByName(name: string): ISharedFunction {
|
||||||
|
if (this.functions.has(name)) {
|
||||||
|
return this.functions.get(name);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
parameters: [],
|
||||||
|
code: 'code by SharedFunctionCollectionStub',
|
||||||
|
revertCode: 'revert-code by SharedFunctionCollectionStub',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
27
tests/unit/stubs/SharedFunctionStub.ts
Normal file
27
tests/unit/stubs/SharedFunctionStub.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
|
|
||||||
|
export class SharedFunctionStub implements ISharedFunction {
|
||||||
|
public name = 'shared-function-stub-name';
|
||||||
|
public parameters?: readonly string[] = [
|
||||||
|
'shared-function-stub-parameter',
|
||||||
|
];
|
||||||
|
public code = 'shared-function-stub-code';
|
||||||
|
public revertCode = 'shared-function-stub-revert-code';
|
||||||
|
|
||||||
|
public withName(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withCode(code: string) {
|
||||||
|
this.code = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withRevertCode(revertCode: string) {
|
||||||
|
this.revertCode = revertCode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withParameters(...params: string[]) {
|
||||||
|
this.parameters = params;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user