diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..161f7f1 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', + 'plugin:prettier/recommended', + ], + "parserOptions": { + "project": ["./tsconfig.json"] + }, +}; \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..163e734 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "all", + "printWidth": 120, + "tabWidth": 4, + "semi": false, + "singleQuote": true, + "arrowParens": "avoid" +} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 6aa7a3a..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-check - -import eslint from '@eslint/js'; -import tseslint from 'typescript-eslint'; - -export default tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.recommended, -); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b450c76..49c45a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,9 @@ ], "devDependencies": { "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "3.2.5", "typescript": "^5.3.3", "typescript-eslint": "^7.0.1" } @@ -1378,6 +1381,18 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2572,6 +2587,48 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -2726,6 +2783,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -4516,6 +4579,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -4948,6 +5038,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -5194,6 +5300,12 @@ "node": ">=6" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6670,6 +6782,12 @@ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "optional": true }, + "@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -7537,6 +7655,23 @@ "text-table": "^0.2.0" } }, + "eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "requires": {} + }, + "eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + } + }, "eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -7642,6 +7777,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -8975,6 +9116,21 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, "pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -9267,6 +9423,16 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "requires": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -9440,6 +9606,12 @@ } } }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index cc6e8c8..508a00a 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,16 @@ "scripts": { "compile": "npm run compile --workspaces", "test": "npm run test --workspaces --if-present", - "generate": "npm run generate --workspaces --if-present" + "generate": "npm run generate --workspaces --if-present", + "lint": "npm run lint --workspaces" }, "author": "Omer van Kloeten", "license": "ISC", "devDependencies": { "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "3.2.5", "typescript": "^5.3.3", "typescript-eslint": "^7.0.1" }, diff --git a/packages/design/package.json b/packages/design/package.json index d666762..e49dfa5 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -24,7 +24,8 @@ "scripts": { "compile": "tsc-silent --project tsconfig.json --suppress 1344@src/diagram/parser.js --compiler ../../node_modules/typescript/lib/typescript.js", "test": "jest", - "generate": "jison src/diagram/stateDiagram.jison --outfile src/diagram/parser.js --module-type commonjs" + "generate": "jison src/diagram/stateDiagram.jison --outfile src/diagram/parser.js --module-type commonjs", + "lint": "eslint **/*.ts --fix" }, "author": "Omer van Kloeten", "license": "ISC" diff --git a/packages/design/src/scaffold.ts b/packages/design/src/scaffold.ts index 055fde1..2f73d41 100644 --- a/packages/design/src/scaffold.ts +++ b/packages/design/src/scaffold.ts @@ -1,217 +1,253 @@ -import {parser, Statement, StateStatement} from "./diagram/parser"; -import _ from "lodash"; -import * as path from "path"; +import { parser, Statement, StateStatement } from './diagram/parser' +import _ from 'lodash' +import * as path from 'path' const pascalCase = (identifier: string) => _.camelCase(identifier).replace(/^(\w)/, s => s.toUpperCase()) parser.yy = { setDirection: () => { - console.log("test"); + // Intentionally left empty }, trimColon: (text: string) => { - return (/^\s*:\s*(.+)/.exec(text) ?? ['', ''])[1].trim(); + return (/^\s*:\s*(.+)/.exec(text) ?? ['', ''])[1].trim() }, - setRootDoc: (x: any) => { - console.log("test"); + setRootDoc: () => { + // Intentionally left empty }, getDividerId: () => { - debugger; - return 'test'; + // Currently unused + throw new Error('Unexpected usage of getDividerId') }, - setAccDescription: (x: string) => { - console.log(x); - debugger; + setAccDescription: () => { + // Currently unused + throw new Error('Unexpected usage of setAccDescription') }, - setAccTitle: (x: string) => { - console.log(x); - debugger; - } -}; + setAccTitle: () => { + // Currently unused + throw new Error('Unexpected usage of setAccTitle') + }, +} -const NoOutgoingConnection = '???'; -const EndState = '[*]'; +const NoOutgoingConnection = '???' +const EndState = '[*]' type Step = { - name: string; - attempts: number; - backoffMs: number; - whatsNext: { type: 'step', name: string } | { - type: 'decision', - name: string - } | (typeof NoOutgoingConnection) | (typeof EndState); + name: string + attempts: number + backoffMs: number + whatsNext: + | { type: 'step'; name: string } + | { + type: 'decision' + name: string + } + | typeof NoOutgoingConnection + | typeof EndState } type Decision = { - name: string; - outcomes: { name: string, target: string }[]; + name: string + outcomes: { name: string; target: string }[] } -function splitFile(mermaidContents: string): { header: string, diagramBody: string } { - const split = /---(.+)---(.+)/s.exec(mermaidContents); +function splitFile(mermaidContents: string): { header: string; diagramBody: string } { + const split = /---(.+)---(.+)/s.exec(mermaidContents) if (split === null) { - throw new Error("Unable to parse Marmaid diagram - please check that you have both a header and a diagram."); + throw new Error('Unable to parse Mermaid diagram - please check that you have both a header and a diagram.') } return { header: split[1].trim(), diagramBody: split[2].trim(), - }; + } } function titleNameFromHeader(header: string): string { - const titleExec = /^title\s*:\s*(.+)$/m.exec(header); + const titleExec = /^title\s*:\s*(.+)$/m.exec(header) if (titleExec === null) { - throw new Error("Mermaid diagram must have a title. Please specify one in the header."); + throw new Error('Mermaid diagram must have a title. Please specify one in the header.') } - return pascalCase(titleExec[1]); + return pascalCase(titleExec[1]) } function inferStepsAndDecisions(diagramStatements: Statement[]): { - firstStep: string; - decisions: Decision[]; + firstStep: string + decisions: Decision[] steps: Step[] } { - const steps: { [name: string]: Step } = {}; - const decisions: { [name: string]: Decision } = {}; - let firstStep: string | null = null; + const steps: { [name: string]: Step } = {} + const decisions: { [name: string]: Decision } = {} + let firstStep: string | null = null const validDecisionNames = diagramStatements - .filter(s => s.stmt === "state" && s.type === "choice") + .filter(s => s.stmt === 'state' && s.type === 'choice') .map(s => (s as StateStatement).id) for (const statement of diagramStatements) { - if (statement.stmt === "state") { - if (statement.type === "choice") { + if (statement.stmt === 'state') { + if (statement.type === 'choice') { // Already covered before - continue; + continue } - console.log(`Ignoring unsupported statement: ${JSON.stringify(statement)}`); + console.log(`Ignoring unsupported statement: ${JSON.stringify(statement)}`) + } else if (statement.stmt === 'relation') { + const from = statement.state1.id + const to = statement.state2.id - } else if (statement.stmt === "relation") { - const from = statement.state1.id; - const to = statement.state2.id; - - const fromDecision = validDecisionNames.includes(from); - const toDecision = validDecisionNames.includes(to); + const fromDecision = validDecisionNames.includes(from) + const toDecision = validDecisionNames.includes(to) // TODO: Split to validation and walking // TODO: Validate names are valid identifiers, too if (from === '[*]') { // First step if (firstStep !== null) { - throw new Error(`Unable to have multiple first steps. Only one step may arrive from [*]. Found another at: ${JSON.stringify(statement)}`); + throw new Error( + `Unable to have multiple first steps. Only one step may arrive from [*]. Found another at: ${JSON.stringify(statement)}`, + ) } if (to === '[*]') { - throw new Error('The initial state [*] may not be directly connected to the end state [*].'); + throw new Error('The initial state [*] may not be directly connected to the end state [*].') } - firstStep = to; + firstStep = to } else { if (!fromDecision) { steps[from] = steps[from] ?? { name: from, attempts: 1, backoffMs: 1000, - whatsNext: NoOutgoingConnection - }; + whatsNext: NoOutgoingConnection, + } if (steps[from].whatsNext !== NoOutgoingConnection && from !== to) { - throw new Error(`Only one relation may leave a non-decision step. The step ${from} is related to both of the steps ${JSON.stringify(steps[from].whatsNext)} and ${to}.`); + throw new Error( + `Only one relation may leave a non-decision step. The step ${from} is related to both of the steps ${JSON.stringify(steps[from].whatsNext)} and ${to}.`, + ) } } - if (to === "[*]") { - steps[from].whatsNext = EndState; + if (to === '[*]') { + steps[from].whatsNext = EndState } else { if (!toDecision) { steps[to] = steps[to] ?? { name: to, attempts: 1, backoffMs: 1000, - whatsNext: NoOutgoingConnection - }; + whatsNext: NoOutgoingConnection, + } } if (!fromDecision && from !== to) { - steps[from].whatsNext = {name: to, type: toDecision ? 'decision' : 'step'}; + steps[from].whatsNext = { name: to, type: toDecision ? 'decision' : 'step' } } } } if (fromDecision) { if (toDecision) { - throw new Error(`A decision can not be directly connected to another decision. There is a connection between ${from} and ${to}.`) + throw new Error( + `A decision can not be directly connected to another decision. There is a connection between ${from} and ${to}.`, + ) } // This is a relation coming out of a decision if (from === to) { - throw new Error(`Decisions may not have self-arrows. The following relation has one: ${JSON.stringify(statement)}`); + throw new Error( + `Decisions may not have self-arrows. The following relation has one: ${JSON.stringify(statement)}`, + ) } if (statement.description === undefined) { - throw new Error(`All relations coming out of decisions must be named. The following relation is unnamed: ${JSON.stringify(statement)}`); + throw new Error( + `All relations coming out of decisions must be named. The following relation is unnamed: ${JSON.stringify(statement)}`, + ) } - decisions[from] = (decisions[from] ?? {name: from, outcomes: []}); - decisions[from].outcomes.push({name: statement.description, target: to}); + decisions[from] = decisions[from] ?? { name: from, outcomes: [] } + decisions[from].outcomes.push({ name: statement.description, target: to }) } if (from === to) { // This is a self-arrow if (statement.description === undefined) { - throw new Error(`Self-arrows must include information about retries. The following relation is unnamed: ${JSON.stringify(statement)}`); + throw new Error( + `Self-arrows must include information about retries. The following relation is unnamed: ${JSON.stringify(statement)}`, + ) } // Note: The parser escapes backslashes - const parameters = Object.fromEntries(statement.description.replace('\\n', '\n').split('\n').map(s => s.split('='))); + const parsedPairs: [string, string][] = statement.description + .replace('\\n', '\n') + .split('\n') + .map(s => s.split('=')) + .filter(arr => arr.length === 2) + .map(([k, v]) => [k, v]) + + const parameters: { [name: string]: string } = Object.fromEntries(parsedPairs) if (parameters.attempts) { - const attempts = Number.parseFloat(parameters.attempts); + const attempts = Number.parseFloat(parameters.attempts) if (attempts <= 0 || Math.floor(attempts) !== attempts) { - throw new Error(`Number of attempts for ${from} is ${attempts}. Only whole positive numbers are allowed.`) + throw new Error( + `Number of attempts for ${from} is ${attempts}. Only whole positive numbers are allowed.`, + ) } - steps[from].attempts = attempts; + steps[from].attempts = attempts } if (parameters.backoffMs) { - const backoffMs = Number.parseFloat(parameters.backoffMs); + const backoffMs = Number.parseFloat(parameters.backoffMs) if (backoffMs <= 0 || Math.floor(backoffMs) !== backoffMs) { - throw new Error(`backoffMs for ${from} is ${backoffMs}. Only whole positive numbers are allowed.`) + throw new Error( + `backoffMs for ${from} is ${backoffMs}. Only whole positive numbers are allowed.`, + ) } - steps[from].backoffMs = backoffMs; + steps[from].backoffMs = backoffMs } - - } else if (from !== '[*]') { } - } else { - console.log("Ignoring unsupported statement: " + JSON.stringify(statement)); + console.log('Ignoring unsupported statement: ' + JSON.stringify(statement)) } } if (firstStep === null) { - throw new Error("Unable to determine any step being the first, with a relation coming from [*]."); + throw new Error('Unable to determine any step being the first, with a relation coming from [*].') } - const stepsWithoutOutgoingConnections = Object.values(steps).filter(s => s.whatsNext === NoOutgoingConnection).map(s => s.name); + const stepsWithoutOutgoingConnections = Object.values(steps) + .filter(s => s.whatsNext === NoOutgoingConnection) + .map(s => s.name) if (stepsWithoutOutgoingConnections.length > 0) { - throw new Error(`The following steps have no outgoing connections and must be connected to the end state [*] explicitly: ${stepsWithoutOutgoingConnections.join(", ")}`) + throw new Error( + `The following steps have no outgoing connections and must be connected to the end state [*] explicitly: ${stepsWithoutOutgoingConnections.join(', ')}`, + ) } - return {firstStep, steps: Object.values(steps), decisions: Object.values(decisions)}; + return { firstStep, steps: Object.values(steps), decisions: Object.values(decisions) } } -function fillScaffoldTemplate(implementationRef: string, decisionTypes: string[], registerStepCalls: string[], functionDeclarationsCode: string[], firstTaskName: string, firstPayloadName: string, payloadNames: string[], workflowName: string): string { +function fillScaffoldTemplate( + implementationRef: string, + decisionTypes: string[], + registerStepCalls: string[], + functionDeclarationsCode: string[], + firstTaskName: string, + firstPayloadName: string, + payloadNames: string[], + workflowName: string, +): string { return ` /* This file is automatically generated. It gets overwritten on build */ import {CompleteWorkflow, WrongTimingError, WorkflowBase} from "@gamgee/run"; @@ -243,85 +279,91 @@ export abstract class ${workflowName}Base extends WorkflowBase { protected async _enqueue(taskName: string, payload: JSONValue, store: StateStore, taskId?: string): Promise { throw new WrongTimingError(); } -}`; -}; +}` +} function stepNameToPayloadName(stepName: string) { - return `${pascalCase(stepName)}Payload`; + return `${pascalCase(stepName)}Payload` } function decisionNameToTypeName(decisionName: string) { - return `${pascalCase(decisionName)}Decision`; + return `${pascalCase(decisionName)}Decision` } function escapeString(str: string) { - return str.replace("'", "\\'"); + return str.replace("'", "\\'") } function typeCodeFromDecision(decision: Decision) { - const outcomes = decision.outcomes.map(o => `{ + const outcomes = decision.outcomes.map( + o => `{ decision: '${escapeString(o.name)}', targetTaskName: '${escapeString(o.target)}', payload: ${stepNameToPayloadName(o.target)}, -}`); +}`, + ) - return `export type ${decisionNameToTypeName(decision.name)} = ${outcomes.join(' | ')};`; + return `export type ${decisionNameToTypeName(decision.name)} = ${outcomes.join(' | ')};` } function registerStepCallCodeFromStep(step: Step) { - return `super._registerStep({ name: '${escapeString(step.name)}', run: this.${stepToFunctionName(step)}, attempts: ${step.attempts}, backoffMs: ${step.backoffMs} });`; + return `super._registerStep({ name: '${escapeString(step.name)}', run: this.${stepToFunctionName(step)}, attempts: ${step.attempts}, backoffMs: ${step.backoffMs} });` } function stepToFunctionName(step: Step) { - return _.camelCase(step.name); + return _.camelCase(step.name) } function functionDeclarationCodeFromStep(step: Step) { - let returnType: string; + let returnType: string switch (step.whatsNext) { case NoOutgoingConnection: - throw new Error(`Step ${step.name} has no outgoing connections.`); + throw new Error(`Step ${step.name} has no outgoing connections.`) case EndState: - returnType = 'CompleteWorkflow'; - break; + returnType = 'CompleteWorkflow' + break default: switch (step.whatsNext.type) { - case "step": - returnType = `{ targetTaskName: '${_.camelCase(step.whatsNext.name)}', payload: ${stepNameToPayloadName(step.whatsNext.name)} }`; - break; + case 'step': + returnType = `{ targetTaskName: '${_.camelCase(step.whatsNext.name)}', payload: ${stepNameToPayloadName(step.whatsNext.name)} }` + break - case "decision": - returnType = decisionNameToTypeName(step.whatsNext.name); - break; + case 'decision': + returnType = decisionNameToTypeName(step.whatsNext.name) + break default: - throw new Error(`Internal error: Unknown next step type ${JSON.stringify(step.whatsNext)}`); + throw new Error(`Internal error: Unknown next step type ${JSON.stringify(step.whatsNext)}`) } } - return `abstract ${(stepToFunctionName(step))}(payload: ${stepNameToPayloadName(step.name)}): Promise<${returnType}>;`; + return `abstract ${stepToFunctionName(step)}(payload: ${stepNameToPayloadName(step.name)}): Promise<${returnType}>;` } // TODO: implementationRelativePath is currently useless -export function mermaidToScaffold(mermaidContents: string, diagramFilePath: string, implementationRelativePath: string) { - const {header, diagramBody} = splitFile(mermaidContents); +export function mermaidToScaffold( + mermaidContents: string, + diagramFilePath: string, + implementationRelativePath: string, +) { + const { header, diagramBody } = splitFile(mermaidContents) - const titleName = titleNameFromHeader(header); + const titleName = titleNameFromHeader(header) - const output = parser.parse(diagramBody); + const output = parser.parse(diagramBody) - const {firstStep, steps, decisions} = inferStepsAndDecisions(output); + const { firstStep, steps, decisions } = inferStepsAndDecisions(output) - const decisionTypesCode: string[] = decisions.map(d => typeCodeFromDecision(d)); - const registerStepCalls: string[] = steps.map(s => registerStepCallCodeFromStep(s)); - const functionDeclarationsCode: string[] = steps.map(s => functionDeclarationCodeFromStep(s)); + const decisionTypesCode: string[] = decisions.map(d => typeCodeFromDecision(d)) + const registerStepCalls: string[] = steps.map(s => registerStepCallCodeFromStep(s)) + const functionDeclarationsCode: string[] = steps.map(s => functionDeclarationCodeFromStep(s)) - const baseName = path.basename(diagramFilePath, path.extname(diagramFilePath)); - const generatedFilePath = path.join(path.dirname(diagramFilePath), baseName + '.generated.ts'); - const implementationRef = `${implementationRelativePath}/${baseName}`.replace('//', '/'); + const baseName = path.basename(diagramFilePath, path.extname(diagramFilePath)) + const generatedFilePath = path.join(path.dirname(diagramFilePath), baseName + '.generated.ts') + const implementationRef = `${implementationRelativePath}/${baseName}`.replace('//', '/') return { generatedFilePath, @@ -333,8 +375,7 @@ export function mermaidToScaffold(mermaidContents: string, diagramFilePath: stri firstStep, stepNameToPayloadName(firstStep), steps.map(s => stepNameToPayloadName(s.name)), - pascalCase(titleName) - ) - }; + pascalCase(titleName), + ), + } } - diff --git a/packages/design/test/test.spec.ts b/packages/design/test/test.spec.ts index 31324a6..2509029 100644 --- a/packages/design/test/test.spec.ts +++ b/packages/design/test/test.spec.ts @@ -1,4 +1,4 @@ -import {mermaidToScaffold} from "../src/scaffold"; +import { mermaidToScaffold } from '../src/scaffold' const fileText: string = ` --- @@ -18,12 +18,12 @@ stateDiagram-v2 POST_FIRST_TASK --> thirdTask: Value Isn't Null SecondTask --> thirdTask thirdTask --> [*] -`; +` describe('test', () => { it('test', () => { - const {generatedFilePath, contents} = mermaidToScaffold(fileText, "./my-file.ts", "."); - console.log(generatedFilePath); - console.log(contents); + const { generatedFilePath, contents } = mermaidToScaffold(fileText, './my-file.ts', '.') + console.log(generatedFilePath) + console.log(contents) }) -}); \ No newline at end of file +}) diff --git a/packages/interfaces/json.ts b/packages/interfaces/json.ts index feb11b8..4fdce5b 100644 --- a/packages/interfaces/json.ts +++ b/packages/interfaces/json.ts @@ -1,3 +1,3 @@ -export type JSONPrimitive = string | number | boolean | null; -export type JSONObject = { [member: string]: JSONValue }; -export type JSONValue = JSONPrimitive | JSONObject | Array; +export type JSONPrimitive = string | number | boolean | null +export type JSONObject = { [member: string]: JSONValue } +export type JSONValue = JSONPrimitive | JSONObject | Array diff --git a/packages/interfaces/package.json b/packages/interfaces/package.json index e7db2f9..9af48f9 100644 --- a/packages/interfaces/package.json +++ b/packages/interfaces/package.json @@ -4,7 +4,8 @@ "description": "Interfaces between the different modules of Gamgee", "devDependencies": {}, "scripts": { - "test": "jest" + "test": "jest", + "lint": "eslint **/*.ts --fix" }, "author": "Omer van Kloeten", "license": "ISC" diff --git a/packages/interfaces/store.ts b/packages/interfaces/store.ts index fc9457a..51cb325 100644 --- a/packages/interfaces/store.ts +++ b/packages/interfaces/store.ts @@ -1,17 +1,17 @@ -import {WorkflowTask} from "./task"; +import { WorkflowTask } from './task' export enum FetchStrategy { - Newest = "LIFO", - Oldest = "FIFO", - Drain = "Drain", // Pick up everything except for first tasks. TODO: How should this be implemented this time? + Newest = 'LIFO', + Oldest = 'FIFO', + Drain = 'Drain', // Pick up everything except for first tasks. TODO: How should this be implemented this time? } -export type Query = Partial<{ workflowType: string, taskName: string, strategy: FetchStrategy }>; +export type Query = Partial<{ workflowType: string; taskName: string; strategy: FetchStrategy }> export interface StateStore { - upsertTask(newTask: WorkflowTask): Promise; + upsertTask(newTask: WorkflowTask): Promise - tryFetchingTask(query: Query, timeoutMs: number): Promise; - - clearTask(id: WorkflowTask['id']): Promise; -} \ No newline at end of file + tryFetchingTask(query: Query, timeoutMs: number): Promise + + clearTask(id: WorkflowTask['id']): Promise +} diff --git a/packages/interfaces/task.ts b/packages/interfaces/task.ts index 390d468..acc301a 100644 --- a/packages/interfaces/task.ts +++ b/packages/interfaces/task.ts @@ -1,8 +1,7 @@ - // TODO: Attempt count? export type WorkflowTask = { - readonly id: string; - readonly typeId: string; - readonly taskName: string; - readonly serializedPayload: string; + readonly id: string + readonly typeId: string + readonly taskName: string + readonly serializedPayload: string } diff --git a/packages/run/package.json b/packages/run/package.json index ebea89d..b162a2f 100644 --- a/packages/run/package.json +++ b/packages/run/package.json @@ -17,7 +17,8 @@ "scripts": { "generate": "mm2ws test/**/*.mermaid", "compile": "tsc --build .", - "test": "jest" + "test": "jest", + "lint": "eslint **/*.ts --fix" }, "author": "Omer van Kloeten", "license": "ISC" diff --git a/packages/run/src/worker.ts b/packages/run/src/worker.ts index 600619d..674e0ab 100644 --- a/packages/run/src/worker.ts +++ b/packages/run/src/worker.ts @@ -1,41 +1,49 @@ -import {workflowFactory} from "./workflow-factory"; -import {Query, StateStore} from "@gamgee/interfaces/store"; -import {WorkflowTask} from "@gamgee/interfaces/task"; +import { workflowFactory } from './workflow-factory' +import { Query, StateStore } from '@gamgee/interfaces/store' +import { WorkflowTask } from '@gamgee/interfaces/task' // TODO: Task locking 🤔 export class WorkflowWorker { // Runs only a single task - async executeWaitingTask(store: StateStore, query: Query, fetchTimeoutMs: number): Promise { - const maybeTask = await store.tryFetchingTask(query, fetchTimeoutMs); - + async executeWaitingTask( + store: StateStore, + query: Query, + fetchTimeoutMs: number, + ): Promise { + const maybeTask = await store.tryFetchingTask(query, fetchTimeoutMs) + if (maybeTask === null) { - return "No Tasks Waiting"; + return 'No Tasks Waiting' } - - const workflow = workflowFactory.create(maybeTask.typeId); - return await workflow._runTask(maybeTask, store); + + const workflow = workflowFactory.create(maybeTask.typeId) + return await workflow._runTask(maybeTask, store) } - + // Runs a workflow to completion - async executeWaitingWorkflow(store: StateStore, query: Query, fetchTimeoutMs: number): Promise<"Workflow Completed" | "No Tasks Waiting"> { - const maybeTask = await store.tryFetchingTask(query, fetchTimeoutMs); + async executeWaitingWorkflow( + store: StateStore, + query: Query, + fetchTimeoutMs: number, + ): Promise<'Workflow Completed' | 'No Tasks Waiting'> { + const maybeTask = await store.tryFetchingTask(query, fetchTimeoutMs) if (maybeTask === null) { - return "No Tasks Waiting"; + return 'No Tasks Waiting' } - const workflow = workflowFactory.create(maybeTask.typeId); + const workflow = workflowFactory.create(maybeTask.typeId) - async function executeUntilComplete(task: WorkflowTask): Promise<"Workflow Completed" | "No Tasks Waiting"> { - const result = await workflow._runTask(task, store); - - if (result === "Workflow Completed") { - return "Workflow Completed"; + async function executeUntilComplete(task: WorkflowTask): Promise<'Workflow Completed' | 'No Tasks Waiting'> { + const result = await workflow._runTask(task, store) + + if (result === 'Workflow Completed') { + return 'Workflow Completed' } - - return await executeUntilComplete(result); + + return await executeUntilComplete(result) } - return await executeUntilComplete(maybeTask); + return await executeUntilComplete(maybeTask) } -} \ No newline at end of file +} diff --git a/packages/run/src/workflow-factory.ts b/packages/run/src/workflow-factory.ts index 82b206f..edeef2c 100644 --- a/packages/run/src/workflow-factory.ts +++ b/packages/run/src/workflow-factory.ts @@ -1,16 +1,16 @@ // TODO: This is very error prone in its current naive state -import {WorkflowBase} from "./workflow"; +import { WorkflowBase } from './workflow' class WorkflowFactory { - private readonly workflows: { [key: string]: () => WorkflowBase } = {}; + private readonly workflows: { [key: string]: () => WorkflowBase } = {} register(workflowType: string, create: () => WorkflowBase) { - this.workflows[workflowType] = create; + this.workflows[workflowType] = create } create(workflowType: string) { - return this.workflows[workflowType](); + return this.workflows[workflowType]() } } -export const workflowFactory = new WorkflowFactory(); +export const workflowFactory = new WorkflowFactory() diff --git a/packages/run/src/workflow.ts b/packages/run/src/workflow.ts index 71020e3..6a301e0 100644 --- a/packages/run/src/workflow.ts +++ b/packages/run/src/workflow.ts @@ -1,68 +1,73 @@ -import {JSONValue} from "@gamgee/interfaces/json"; -import {StateStore} from "@gamgee/interfaces/store"; -import {WorkflowTask} from "@gamgee/interfaces/task"; -import {workflowFactory} from "./workflow-factory"; +import { JSONValue } from '@gamgee/interfaces/json' +import { StateStore } from '@gamgee/interfaces/store' +import { WorkflowTask } from '@gamgee/interfaces/task' +import { workflowFactory } from './workflow-factory' export class WrongTimingError extends Error { constructor() { - super("Unable to do this at runtime. Please refer to your mermaid diagram."); - Object.setPrototypeOf(this, WrongTimingError.prototype); + super('Unable to do this at runtime. Please refer to your mermaid diagram.') + Object.setPrototypeOf(this, WrongTimingError.prototype) } } - -export const CompleteWorkflow = {targetTaskName: null, payload: null}; -export type CompleteWorkflow = typeof CompleteWorkflow; +export const CompleteWorkflow = { targetTaskName: null, payload: null } +export type CompleteWorkflow = typeof CompleteWorkflow type StepResult = { - targetTaskName: string | null, + targetTaskName: string | null payload: JSONValue -}; +} type StepDefinition = { - name: string, - run: (payload: T) => Promise, - attempts: number, - backoffMs: number, -}; + name: string + run: (payload: T) => Promise + attempts: number + backoffMs: number +} export abstract class WorkflowBase { - private readonly steps: { [name: string]: Omit, 'name'> } = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private readonly steps: { [name: string]: Omit, 'name'> } = {} protected constructor(readonly workflowType: string) { // TODO: When trying to register two distinct implementations with the same name, throw (e.g. use a hash in the generated file) - workflowFactory.register(workflowType, () => new (this.constructor as new () => this)()); // TODO: This probably should work? + workflowFactory.register(workflowType, () => new (this.constructor as new () => this)()) // TODO: This probably should work? } protected _registerStep(def: StepDefinition): void { - this.steps[def.name] = Object.assign({}, def, {run: def.run.bind(this)}); + this.steps[def.name] = Object.assign({}, def, { run: def.run.bind(this) }) } async _runTask(task: WorkflowTask, store: StateStore): Promise { // TODO: Validate stuff - const stepDef = this.steps[task.taskName]; + const stepDef = this.steps[task.taskName] // TODO: On failure, increment attempts counter and persist - const result = await stepDef.run(JSON.parse(task.serializedPayload)); + const result = await stepDef.run(JSON.parse(task.serializedPayload)) if (result.targetTaskName === null) { - await store.clearTask(task.id); - return 'Workflow Completed'; + await store.clearTask(task.id) + return 'Workflow Completed' } - return await this._enqueue(result.targetTaskName, result.payload, store, task.id); + return await this._enqueue(result.targetTaskName, result.payload, store, task.id) } - protected async _enqueue(taskName: string, payload: JSONValue, store: StateStore, taskId?: string): Promise { + protected async _enqueue( + taskName: string, + payload: JSONValue, + store: StateStore, + taskId?: string, + ): Promise { const newTask: WorkflowTask = { id: taskId ?? Math.random().toString(36).slice(2), typeId: this.workflowType, taskName, serializedPayload: JSON.stringify(payload), - }; + } - await store.upsertTask(newTask); + await store.upsertTask(newTask) - return newTask; + return newTask } } diff --git a/packages/test/package.json b/packages/test/package.json index 9527464..2dc0635 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -14,7 +14,8 @@ }, "scripts": { "compile": "tsc --build .", - "test": "jest" + "test": "jest", + "lint": "eslint **/*.ts --fix" }, "author": "Omer van Kloeten", "license": "ISC" diff --git a/packages/test/stateStores/in-memory.ts b/packages/test/stateStores/in-memory.ts index a0cd725..75d1d88 100644 --- a/packages/test/stateStores/in-memory.ts +++ b/packages/test/stateStores/in-memory.ts @@ -1,60 +1,64 @@ -import {FetchStrategy, StateStore} from "@gamgee/interfaces/store"; -import {WorkflowTask} from "@gamgee/interfaces/task"; +import { FetchStrategy, StateStore } from '@gamgee/interfaces/store' +import { WorkflowTask } from '@gamgee/interfaces/task' export default class InMemoryStateStore implements StateStore { - private clock: number = 0; - private readonly tasks: { [id: string]: WorkflowTask & { clock: number } } = {}; + private clock: number = 0 + private readonly tasks: { [id: string]: WorkflowTask & { clock: number } } = {} async upsertTask(newTask: WorkflowTask): Promise { - this.tasks[newTask.id] = Object.assign({ clock: this.clock }, newTask); - this.clock++; + this.tasks[newTask.id] = Object.assign({ clock: this.clock }, newTask) + this.clock++ + return Promise.resolve() } - async tryFetchingTask(query: Partial<{ - workflowType: string; - taskName: string; - strategy: FetchStrategy; - }>, timeoutMs: number): Promise { - const validTasks: (WorkflowTask & { clock: number })[] = Object.values(this.tasks) - .filter((task) => { - if (query.workflowType && task.typeId !== query.workflowType) { - return false; - } - - if (query.taskName && task.taskName !== query.taskName) { - return false; - } - - return true; - }) - + async tryFetchingTask( + query: Partial<{ + workflowType: string + taskName: string + strategy: FetchStrategy + }>, + ): Promise { + const validTasks: (WorkflowTask & { clock: number })[] = Object.values(this.tasks).filter(task => { + if (query.workflowType && task.typeId !== query.workflowType) { + return false + } + + if (query.taskName && task.taskName !== query.taskName) { + return false + } + + return true + }) + if (validTasks.length === 0) { - return null; + return Promise.resolve(null) } - + const sortedTasks = validTasks.sort((a, b) => a.clock - b.clock) - + switch (query.strategy) { - case undefined: case FetchStrategy.Oldest: - return sortedTasks[0]; - + case undefined: + case FetchStrategy.Oldest: + return Promise.resolve(sortedTasks[0]) + case FetchStrategy.Newest: - return sortedTasks[sortedTasks.length - 1]; - + return Promise.resolve(sortedTasks[sortedTasks.length - 1]) + default: - throw new Error("Not implemented... yet."); + throw new Error('Not implemented... yet.') } } async clearTask(id: WorkflowTask['id']): Promise { - delete this.tasks[id]; - this.clock++; + delete this.tasks[id] + this.clock++ + return Promise.resolve() } - + getStats(): { taskUpdatesSeen: number; tasksRemaining: number } { return { tasksRemaining: Object.values(this.tasks).length, taskUpdatesSeen: this.clock, } } -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index d939e0b..ff8d3f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + "incremental": true, + /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ @@ -11,7 +12,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", + "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */