harmonyc 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/cli/cli.d.ts +2 -0
  2. package/cli/cli.js +32 -0
  3. package/cli/run.d.ts +2 -0
  4. package/cli/run.js +21 -0
  5. package/cli/watch.d.ts +1 -0
  6. package/cli/watch.js +35 -0
  7. package/code_generator/VitestGenerator.d.ts +36 -0
  8. package/code_generator/VitestGenerator.js +240 -0
  9. package/code_generator/outFile.d.ts +20 -0
  10. package/code_generator/outFile.js +60 -0
  11. package/code_generator/test_phrases.d.ts +11 -0
  12. package/code_generator/test_phrases.js +27 -0
  13. package/compiler/compile.d.ts +16 -0
  14. package/compiler/compile.js +50 -0
  15. package/compiler/compiler.d.ts +8 -0
  16. package/compiler/compiler.js +39 -0
  17. package/filenames/filenames.d.ts +3 -0
  18. package/filenames/filenames.js +17 -0
  19. package/model/Router.d.ts +22 -0
  20. package/model/Router.js +54 -0
  21. package/model/model.d.ts +225 -0
  22. package/model/model.js +526 -0
  23. package/optimizations/autoLabel/autoLabel.d.ts +2 -0
  24. package/optimizations/autoLabel/autoLabel.js +19 -0
  25. package/package.json +1 -4
  26. package/parser/lexer.d.ts +21 -0
  27. package/parser/lexer.js +123 -0
  28. package/parser/lexer_rules.d.ts +33 -0
  29. package/parser/lexer_rules.js +74 -0
  30. package/parser/parser.d.ts +18 -0
  31. package/parser/parser.js +76 -0
  32. package/phrases_assistant/phrases_assistant.d.ts +13 -0
  33. package/phrases_assistant/phrases_assistant.js +146 -0
  34. package/util/indent.d.ts +1 -0
  35. package/util/indent.js +5 -0
  36. package/util/iterators.d.ts +1 -0
  37. package/util/iterators.js +5 -0
  38. package/util/xmur3.d.ts +1 -0
  39. package/util/xmur3.js +12 -0
  40. package/vitest/index.d.ts +11 -0
  41. package/vitest/index.js +101 -0
@@ -0,0 +1,33 @@
1
+ export declare enum T {
2
+ Newline = "newline",
3
+ Comment = "comment",
4
+ /** the => */
5
+ ResponseArrow = "=>",
6
+ ErrorMark = "!!",
7
+ Words = "words",
8
+ Minus = "-",
9
+ Plus = "+",
10
+ Colon = ":",
11
+ Space = "space",
12
+ OpeningBracket = "[",
13
+ ClosingBracket = "]",
14
+ OpeningBrace = "{",
15
+ ClosingBrace = "}",
16
+ And = "&",
17
+ Slash = "/",
18
+ Semicolon = ";",
19
+ DoubleQuoteString = "double-quote string",
20
+ UnclosedDoubleQuoteString = "unclosed double-quote string",
21
+ BacktickString = "backtick string",
22
+ UnclosedBacktickString = "unclosed backtick string",
23
+ InvalidEmptyBacktickString = "invalid empty backtick string",
24
+ InvalidWhitespace = "invalid whitespace",
25
+ InvalidTab = "invalid tab",
26
+ MultilineString = "multiline string",
27
+ InvalidMultilineStringMark = "invalid multiline string mark",
28
+ Variable = "variable",
29
+ InvalidEmptyVariable = "invalid empty variable",
30
+ UnclosedVariable = "unclosed variable"
31
+ }
32
+ declare const rules: [boolean, RegExp, T][];
33
+ export default rules;
@@ -0,0 +1,74 @@
1
+ export var T;
2
+ (function (T) {
3
+ T["Newline"] = "newline";
4
+ T["Comment"] = "comment";
5
+ /** the => */
6
+ T["ResponseArrow"] = "=>";
7
+ T["ErrorMark"] = "!!";
8
+ T["Words"] = "words";
9
+ T["Minus"] = "-";
10
+ T["Plus"] = "+";
11
+ T["Colon"] = ":";
12
+ T["Space"] = "space";
13
+ T["OpeningBracket"] = "[";
14
+ T["ClosingBracket"] = "]";
15
+ T["OpeningBrace"] = "{";
16
+ T["ClosingBrace"] = "}";
17
+ T["And"] = "&";
18
+ T["Slash"] = "/";
19
+ T["Semicolon"] = ";";
20
+ T["DoubleQuoteString"] = "double-quote string";
21
+ T["UnclosedDoubleQuoteString"] = "unclosed double-quote string";
22
+ T["BacktickString"] = "backtick string";
23
+ T["UnclosedBacktickString"] = "unclosed backtick string";
24
+ T["InvalidEmptyBacktickString"] = "invalid empty backtick string";
25
+ T["InvalidWhitespace"] = "invalid whitespace";
26
+ T["InvalidTab"] = "invalid tab";
27
+ T["MultilineString"] = "multiline string";
28
+ T["InvalidMultilineStringMark"] = "invalid multiline string mark";
29
+ T["Variable"] = "variable";
30
+ T["InvalidEmptyVariable"] = "invalid empty variable";
31
+ T["UnclosedVariable"] = "unclosed variable";
32
+ })(T || (T = {}));
33
+ const rules = [
34
+ // false = ignore token
35
+ // if multiple patterns match, the former wins
36
+ // patterns must be y (sticky)
37
+ [true, /\n/y, T.Newline],
38
+ [true, /\t/y, T.InvalidTab],
39
+ [true, /[\x00-\x1f]/y, T.InvalidWhitespace],
40
+ [true, /^( )*[+]( |$)/my, T.Plus],
41
+ [true, /^( )*[-]( |$)/my, T.Minus],
42
+ [false, / /y, T.Space],
43
+ [false, /(#|\/\/).*?(?=\n|$)/y, T.Comment],
44
+ [true, /:(?=\s*(?:\n|$))/y, T.Colon],
45
+ [true, /\[/y, T.OpeningBracket],
46
+ [true, /\]/y, T.ClosingBracket],
47
+ [true, /\{/y, T.OpeningBrace],
48
+ [true, /\}/y, T.ClosingBrace],
49
+ [true, /\//y, T.Slash],
50
+ [true, /&/y, T.And],
51
+ [true, /;/y, T.Semicolon],
52
+ [true, /!!/y, T.ErrorMark],
53
+ [true, /=>/y, T.ResponseArrow],
54
+ [
55
+ true,
56
+ /"(?:[^"\\\n]|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*"/y,
57
+ T.DoubleQuoteString,
58
+ ],
59
+ [
60
+ true,
61
+ /"(?:[^"\\\n]|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*/y,
62
+ T.UnclosedDoubleQuoteString,
63
+ ],
64
+ [true, /``/y, T.InvalidEmptyBacktickString],
65
+ [true, /`[^`]+`/y, T.BacktickString],
66
+ [true, /`[^`]*/y, T.UnclosedBacktickString],
67
+ [true, /\$\{[^}\n]+\}/y, T.Variable],
68
+ [true, /\$\{\}/y, T.InvalidEmptyVariable],
69
+ [true, /\$\{[^}\n]*/y, T.UnclosedVariable],
70
+ [true, /\|(?: .*|(?=\n|$))/y, T.MultilineString],
71
+ [true, /\|[^ \n]/y, T.InvalidMultilineStringMark],
72
+ [true, /.+?(?=[\[\]"`|#{}&/;]|\$\{|$|=>|!!|:\s*$)/my, T.Words],
73
+ ];
74
+ export default rules;
@@ -0,0 +1,18 @@
1
+ import type { Parser, ParserOutput, Token } from 'typescript-parsec';
2
+ import { Action, CodeLiteral, Docstring, ErrorResponse, Label, Response, Section, SetVariable, Step, StringLiteral, Switch, Word } from '../model/model.ts';
3
+ import { T } from './lexer.ts';
4
+ export declare function parse(input: string): Section;
5
+ export declare function parse<T>(input: string, production: Parser<any, T>): T;
6
+ export declare const NEWLINES: Parser<T, Token<T>[]>, WORDS: Parser<T, Word>, DOUBLE_QUOTE_STRING: Parser<T, StringLiteral>, BACKTICK_STRING: Parser<T, CodeLiteral>, DOCSTRING: Parser<T, Docstring>, ERROR_MARK: Parser<T, Token<T>>, VARIABLE: Parser<T, string>, SIMPLE_PART: Parser<T, Word | StringLiteral | Docstring | CodeLiteral>, SWITCH: Parser<T, Switch>, BRACES: Parser<T, Switch>, PART: Parser<T, Word | Switch | StringLiteral | Docstring | CodeLiteral>, PHRASE: Parser<T, (Word | Switch | StringLiteral | Docstring | CodeLiteral)[]>, ARG: Parser<T, StringLiteral | Docstring | CodeLiteral>, SET_VARIABLE: Parser<T, SetVariable>, ACTION: Parser<T, Action | SetVariable>, RESPONSE: Parser<T, Response>, ERROR_RESPONSE: Parser<T, ErrorResponse>, SAVE_TO_VARIABLE: Parser<T, Response>, ARROW: Parser<T, Token<T>>, RESPONSE_ITEM: Parser<T, Response | ErrorResponse>, STEP: Parser<T, Step>, LABEL: Parser<T, Label>, SECTION: Parser<T, Section>, BRANCH: Parser<T, Section | Step>, // section first, to make sure there is no colon after step
7
+ DENTS: Parser<T, {
8
+ dent: number;
9
+ isFork: boolean;
10
+ }>, NODE: Parser<T, {
11
+ dent: number;
12
+ branch: Section | Step;
13
+ }>, ANYTHING_BUT_NEWLINE: {
14
+ parse(token: Token<T> | undefined): ParserOutput<T, Token<T>>;
15
+ }, TEXT: Parser<T, undefined>, LINE: Parser<T, {
16
+ dent: number;
17
+ branch: Section | Step;
18
+ } | undefined>, TEST_DESIGN: Parser<T, Section>;
@@ -0,0 +1,76 @@
1
+ import { alt_sc, apply, expectEOF, expectSingleResult, fail, kleft, kmid, kright, list_sc, nil, opt_sc, rep_sc, seq, tok, unableToConsumeToken, } from 'typescript-parsec';
2
+ import { Action, CodeLiteral, Docstring, ErrorResponse, Label, Response, SaveToVariable, Section, SetVariable, Step, StringLiteral, Switch, Word, } from "../model/model.js";
3
+ import { T, lexer } from "./lexer.js";
4
+ export function parse(input, production = TEST_DESIGN) {
5
+ const tokens = lexer.parse(input);
6
+ return expectSingleResult(expectEOF(production.parse(tokens)));
7
+ }
8
+ function anythingBut(kind) {
9
+ return {
10
+ parse(token) {
11
+ if (token === undefined)
12
+ return { successful: false, error: unableToConsumeToken(token) };
13
+ if (token.kind === kind)
14
+ return { successful: false, error: unableToConsumeToken(token) };
15
+ return {
16
+ candidates: [
17
+ {
18
+ firstToken: token,
19
+ nextToken: token.next,
20
+ result: token,
21
+ },
22
+ ],
23
+ successful: true,
24
+ error: undefined,
25
+ };
26
+ },
27
+ };
28
+ }
29
+ export const NEWLINES = list_sc(tok(T.Newline), nil()), WORDS = apply(tok(T.Words), ({ text }) => new Word(text.trimEnd().split(/\s+/).join(' '))), DOUBLE_QUOTE_STRING = alt_sc(apply(tok(T.DoubleQuoteString), ({ text }) => new StringLiteral(JSON.parse(text))), seq(tok(T.UnclosedDoubleQuoteString), fail('unclosed double-quote string'))), BACKTICK_STRING = apply(tok(T.BacktickString), ({ text }) => new CodeLiteral(text.slice(1, -1))), DOCSTRING = kright(opt_sc(NEWLINES), apply(list_sc(tok(T.MultilineString), tok(T.Newline)), (lines) => new Docstring(lines.map(({ text }) => text.slice(2)).join('\n')))), ERROR_MARK = tok(T.ErrorMark), VARIABLE = apply(tok(T.Variable), ({ text }) => text.slice(2, -1)), SIMPLE_PART = alt_sc(WORDS, DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING),
30
+ //REPEATER = apply(
31
+ //list_sc(rep_sc(SIMPLE_PART), tok(T.And)),
32
+ //(cs) => new Repeater(cs)
33
+ //),
34
+ SWITCH = apply(list_sc(SIMPLE_PART, tok(T.Slash)), (cs) => new Switch(cs)),
35
+ //ROUTER = apply(list_sc(REPEATER, tok(T.Semicolon)), (cs) => new Router(cs)),
36
+ BRACES = kmid(tok(T.OpeningBrace), SWITCH, tok(T.ClosingBrace)), PART = alt_sc(SIMPLE_PART, BRACES), PHRASE = rep_sc(PART), ARG = alt_sc(DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING), SET_VARIABLE = apply(seq(VARIABLE, ARG), ([variable, value]) => new SetVariable(variable, value)), ACTION = alt_sc(SET_VARIABLE, apply(PHRASE, (parts, range) => new Action(parts).at(range))), RESPONSE = apply(seq(PHRASE, opt_sc(VARIABLE)), ([parts, variable], range) => new Response(parts, variable !== undefined ? new SaveToVariable(variable) : undefined).at(range)), ERROR_RESPONSE = apply(seq(ERROR_MARK, opt_sc(alt_sc(DOUBLE_QUOTE_STRING, DOCSTRING))), ([, parts]) => new ErrorResponse(parts)), SAVE_TO_VARIABLE = apply(VARIABLE, (variable) => new Response([], new SaveToVariable(variable))), ARROW = kright(opt_sc(NEWLINES), tok(T.ResponseArrow)), RESPONSE_ITEM = kright(ARROW, alt_sc(SAVE_TO_VARIABLE, ERROR_RESPONSE, RESPONSE)), STEP = apply(seq(ACTION, rep_sc(RESPONSE_ITEM)), ([action, responses]) => new Step(action, responses).setFork(true)), LABEL = apply(kleft(list_sc(PART, nil()), tok(T.Colon)), (words, range) => new Label(words.map((w) => w.toString()).join(' ')).at(range)), SECTION = apply(LABEL, (text) => new Section(text)), BRANCH = apply(alt_sc(SECTION, STEP), (branch, range) => branch.at(range)), // section first, to make sure there is no colon after step
37
+ DENTS = apply(alt_sc(tok(T.Plus), tok(T.Minus)), (seqOrFork) => {
38
+ return {
39
+ dent: (seqOrFork.text.length - 2) / 2,
40
+ isFork: seqOrFork.kind === T.Plus,
41
+ };
42
+ }), NODE = apply(seq(DENTS, BRANCH), ([{ dent, isFork }, branch], [start, end]) => ({
43
+ dent,
44
+ branch: branch.setFork(isFork),
45
+ })), ANYTHING_BUT_NEWLINE = anythingBut(T.Newline), TEXT = apply(seq(tok(T.Words), rep_sc(ANYTHING_BUT_NEWLINE)), () => undefined), LINE = alt_sc(NODE, TEXT), TEST_DESIGN = kmid(rep_sc(NEWLINES), apply(opt_sc(list_sc(apply(LINE, (line, [start, end]) => ({ line, start, end })), NEWLINES)), (lines) => {
46
+ let dent;
47
+ const root = new Section(new Label(''));
48
+ let parent = root;
49
+ for (const { line, start } of lines !== null && lines !== void 0 ? lines : []) {
50
+ if (line === undefined)
51
+ continue;
52
+ const { dent: d, branch } = line;
53
+ if (dent === undefined) {
54
+ if (d !== 0)
55
+ throw new Error(`invalid indent ${d} at line ${start.pos.rowBegin}: first step must not be indented`);
56
+ dent = 0;
57
+ }
58
+ else if (Math.round(d) !== d) {
59
+ throw new Error(`invalid odd indent of ${d * 2} at line ${start.pos.rowBegin}`);
60
+ }
61
+ else if (d > dent + 1) {
62
+ throw new Error(`invalid indent ${d} at line ${start.pos.rowBegin}`);
63
+ }
64
+ else if (d === dent + 1) {
65
+ parent = parent.children[parent.children.length - 1];
66
+ ++dent;
67
+ }
68
+ else
69
+ while (d < dent) {
70
+ parent = parent.parent;
71
+ --dent;
72
+ }
73
+ parent.addChild(branch);
74
+ }
75
+ return root;
76
+ }), rep_sc(NEWLINES));
@@ -0,0 +1,13 @@
1
+ import * as t from 'ts-morph';
2
+ import { PhraseMethod } from '../model/model';
3
+ export declare class PhrasesAssistant {
4
+ project: t.Project;
5
+ file: t.SourceFile;
6
+ clazz: t.ClassDeclaration;
7
+ constructor(content: string, className: string);
8
+ ensureMethods(methods: PhraseMethod[]): void;
9
+ ensureMethod(method: PhraseMethod): void;
10
+ addMethod(method: PhraseMethod): void;
11
+ sortMethods(): void;
12
+ toCode(): string;
13
+ }
@@ -0,0 +1,146 @@
1
+ import * as t from 'ts-morph';
2
+ export class PhrasesAssistant {
3
+ constructor(content, className) {
4
+ var _a;
5
+ this.project = new t.Project({
6
+ useInMemoryFileSystem: true,
7
+ });
8
+ this.file = this.project.createSourceFile('filename.ts', content, {
9
+ overwrite: true,
10
+ });
11
+ const clazz = this.file.getClass(className);
12
+ if (!clazz) {
13
+ const defaultExport = this.file.getDefaultExportSymbol();
14
+ if (defaultExport) {
15
+ const decl = defaultExport.getDeclarations()[0];
16
+ if (t.Node.isClassDeclaration(decl)) {
17
+ this.clazz = decl;
18
+ this.clazz.rename(className);
19
+ }
20
+ }
21
+ }
22
+ else {
23
+ this.clazz = clazz;
24
+ if (!this.clazz.isDefaultExport()) {
25
+ this.clazz.setIsDefaultExport(true);
26
+ }
27
+ }
28
+ (_a = this.clazz) !== null && _a !== void 0 ? _a : (this.clazz = this.file.addClass({
29
+ name: className,
30
+ isDefaultExport: true,
31
+ }));
32
+ }
33
+ ensureMethods(methods) {
34
+ for (const method of methods) {
35
+ this.ensureMethod(method);
36
+ }
37
+ this.clazz
38
+ .getMethods()
39
+ .filter((m) => !methods.find((md) => md.name === m.getName()))
40
+ .forEach((m) => {
41
+ if (m.getStatements().length === 1 &&
42
+ m
43
+ .getStatements()[0]
44
+ .getText()
45
+ .match(/throw new Error\(["']TODO /)) {
46
+ m.remove();
47
+ }
48
+ });
49
+ this.sortMethods();
50
+ }
51
+ ensureMethod(method) {
52
+ let existing = this.clazz.getMethod(method.name);
53
+ if (!existing) {
54
+ this.addMethod(method);
55
+ }
56
+ }
57
+ addMethod(method) {
58
+ const m = this.clazz.addMethod({
59
+ isAsync: true,
60
+ name: method.name,
61
+ parameters: method.parameters,
62
+ statements: [`throw new Error("TODO ${method.name}");`],
63
+ });
64
+ m.formatText({ indentSize: 2 });
65
+ }
66
+ sortMethods() {
67
+ const groups = ['When_', 'Then_'];
68
+ const members = this.clazz.getMembersWithComments();
69
+ const sorted = members.slice().sort((a, b) => {
70
+ const kindA = t.Node.isMethodDeclaration(a)
71
+ ? groups.findIndex((g) => a.getName().startsWith(g))
72
+ : -1;
73
+ const kindB = t.Node.isMethodDeclaration(b)
74
+ ? groups.findIndex((g) => b.getName().startsWith(g))
75
+ : -1;
76
+ if (kindA !== kindB) {
77
+ return kindA - kindB;
78
+ }
79
+ if (kindA === -1 && kindB === -1) {
80
+ return 0;
81
+ }
82
+ if (!t.Node.isMethodDeclaration(a) || !t.Node.isMethodDeclaration(b)) {
83
+ // never happens
84
+ return 0;
85
+ }
86
+ return a.getName() < b.getName() ? -1 : 1;
87
+ });
88
+ const moves = calculateMoves(members, sorted);
89
+ for (const move of moves) {
90
+ const method = members[move.fromIndex];
91
+ if (t.Node.isCommentClassElement(method)) {
92
+ // something went wrong
93
+ return;
94
+ }
95
+ method.setOrder(move.toIndex);
96
+ const [moved] = members.splice(move.fromIndex, 1);
97
+ members.splice(move.toIndex, 0, moved);
98
+ }
99
+ }
100
+ toCode() {
101
+ let s = this.file.getFullText();
102
+ // fix extra space
103
+ const closing = this.clazz.getEnd() - 1;
104
+ if (s.slice(closing - 2, closing + 1) === '\n }') {
105
+ s = s.slice(0, closing - 1) + s.slice(closing);
106
+ }
107
+ return s;
108
+ }
109
+ }
110
+ /**
111
+ * Calculates a set of move operations to transform one array into another.
112
+ * Both arrays must contain the same elements, just in different order.
113
+ *
114
+ * @param actual - The current array that needs to be reordered
115
+ * @param desired - The target array with the desired order
116
+ * @returns Array of move operations {fromIndex, toIndex} that transform actual into desired
117
+ */
118
+ function calculateMoves(actual, desired) {
119
+ if (actual.length !== desired.length) {
120
+ throw new Error('Arrays must have the same length');
121
+ }
122
+ // Create a working copy to track changes
123
+ const working = [...actual];
124
+ const moves = [];
125
+ // For each position in the desired array
126
+ for (let targetIndex = 0; targetIndex < desired.length; targetIndex++) {
127
+ const targetElement = desired[targetIndex];
128
+ // Find where this element currently is in our working array
129
+ const currentIndex = working.indexOf(targetElement);
130
+ if (currentIndex === -1) {
131
+ throw new Error('Arrays must contain the same elements');
132
+ }
133
+ // If it's not in the right position, move it
134
+ if (currentIndex !== targetIndex) {
135
+ // Record the move operation
136
+ moves.push({
137
+ fromIndex: currentIndex,
138
+ toIndex: targetIndex,
139
+ });
140
+ // Apply the move to our working array
141
+ const [movedElement] = working.splice(currentIndex, 1);
142
+ working.splice(targetIndex, 0, movedElement);
143
+ }
144
+ }
145
+ return moves;
146
+ }
@@ -0,0 +1 @@
1
+ export declare const Indent: (n: number) => (lines: Iterable<string>) => Generator<string, void, unknown>;
package/util/indent.js ADDED
@@ -0,0 +1,5 @@
1
+ export const Indent = (n) => function* indent(lines) {
2
+ for (const line of lines) {
3
+ yield ' '.repeat(n) + line;
4
+ }
5
+ };
@@ -0,0 +1 @@
1
+ export declare function flatMap<T, R>(iterable: Iterable<T>, callback: (value: T) => Iterable<R>): Generator<R, void, undefined>;
@@ -0,0 +1,5 @@
1
+ export function* flatMap(iterable, callback) {
2
+ for (const value of iterable) {
3
+ yield* callback(value);
4
+ }
5
+ }
@@ -0,0 +1 @@
1
+ export declare function xmur3(str: string): () => number;
package/util/xmur3.js ADDED
@@ -0,0 +1,12 @@
1
+ // https://stackoverflow.com/revisions/47593316/25 by bryc (github.com/bryc)
2
+ export function xmur3(str) {
3
+ let h = 1779033703 ^ str.length;
4
+ for (let i = 0; i < str.length; i++)
5
+ (h = Math.imul(h ^ str.charCodeAt(i), 3432918353)),
6
+ (h = (h << 13) | (h >>> 19));
7
+ return function () {
8
+ h = Math.imul(h ^ (h >>> 16), 2246822507);
9
+ h = Math.imul(h ^ (h >>> 13), 3266489909);
10
+ return (h ^= h >>> 16) >>> 0;
11
+ };
12
+ }
@@ -0,0 +1,11 @@
1
+ import type { Plugin } from 'vite';
2
+ import { CompilerOptions } from '../compiler/compile.ts';
3
+ export interface HarmonyPluginOptions extends Partial<CompilerOptions> {
4
+ autoEditPhrases?: boolean;
5
+ }
6
+ export default function harmonyPlugin(opts?: HarmonyPluginOptions): Plugin;
7
+ declare module 'vitest' {
8
+ interface TaskMeta {
9
+ phrases?: string[];
10
+ }
11
+ }
@@ -0,0 +1,101 @@
1
+ import { readFile, writeFile } from 'fs/promises';
2
+ import c from 'tinyrainbow';
3
+ import { compileFeature } from "../compiler/compile.js";
4
+ import { preprocess } from "../compiler/compiler.js";
5
+ import { PhrasesAssistant } from "../phrases_assistant/phrases_assistant.js";
6
+ const DEFAULT_OPTIONS = {
7
+ autoEditPhrases: true,
8
+ };
9
+ export default function harmonyPlugin(opts = {}) {
10
+ const options = { ...DEFAULT_OPTIONS, ...opts };
11
+ return {
12
+ name: 'harmony',
13
+ resolveId(id) {
14
+ if (id.endsWith('.harmony')) {
15
+ return id;
16
+ }
17
+ },
18
+ transform(code, id) {
19
+ if (!id.endsWith('.harmony'))
20
+ return null;
21
+ code = preprocess(code);
22
+ const { outFile, phraseMethods, featureClassName } = compileFeature(id, code, opts);
23
+ if (options.autoEditPhrases) {
24
+ void updatePhrasesFile(id, phraseMethods, featureClassName);
25
+ }
26
+ return {
27
+ code: outFile.valueWithoutSourceMap,
28
+ map: outFile.sourceMap,
29
+ };
30
+ },
31
+ config(config) {
32
+ var _a, _b;
33
+ var _c;
34
+ (_a = config.test) !== null && _a !== void 0 ? _a : (config.test = {});
35
+ (_b = (_c = config.test).reporters) !== null && _b !== void 0 ? _b : (_c.reporters = ['default']);
36
+ if (!Array.isArray(config.test.reporters)) {
37
+ config.test.reporters = [config.test.reporters];
38
+ }
39
+ config.test.reporters.splice(0, 0, new HarmonyReporter());
40
+ },
41
+ };
42
+ }
43
+ class HarmonyReporter {
44
+ onInit(ctx) {
45
+ this.ctx = ctx;
46
+ }
47
+ onCollected(files) {
48
+ this.files = files;
49
+ }
50
+ onTaskUpdate(packs) {
51
+ if (this.files)
52
+ for (const file of this.files)
53
+ addPhrases(file);
54
+ }
55
+ onFinished(files, errors) {
56
+ for (const file of files)
57
+ addPhrases(file);
58
+ }
59
+ }
60
+ function addPhrases(task, depth = 2) {
61
+ var _a, _b;
62
+ if ('tasks' in task) {
63
+ for (const child of task.tasks)
64
+ addPhrases(child, depth + 1);
65
+ }
66
+ else if (task.type === 'test' &&
67
+ ((_a = task.result) === null || _a === void 0 ? void 0 : _a.state) === 'fail' &&
68
+ ((_b = task.meta) === null || _b === void 0 ? void 0 : _b.hasOwnProperty('phrases')) &&
69
+ task.meta.phrases.length > 0) {
70
+ const x = task;
71
+ x.name +=
72
+ '\n' +
73
+ task.meta
74
+ .phrases.map((step, i, a) => {
75
+ const indent = ' '.repeat(depth);
76
+ const failed = i === a.length - 1;
77
+ const figure = failed ? c.red('×') : c.green('✓');
78
+ return `${indent}${figure} ${step}`;
79
+ })
80
+ .join('\n');
81
+ delete task.meta.phrases; // to make sure not to add them again
82
+ }
83
+ }
84
+ async function updatePhrasesFile(id, phraseMethods, featureClassName) {
85
+ try {
86
+ const phrasesFile = id.replace(/\.harmony$/, '.phrases.ts');
87
+ let phrasesFileContent = '';
88
+ try {
89
+ phrasesFileContent = await readFile(phrasesFile, 'utf-8');
90
+ }
91
+ catch {
92
+ // File doesn't exist
93
+ }
94
+ const pa = new PhrasesAssistant(phrasesFileContent, featureClassName);
95
+ pa.ensureMethods(phraseMethods);
96
+ await writeFile(phrasesFile, pa.toCode());
97
+ }
98
+ catch (e) {
99
+ console.error('Error updating phrases file:', e);
100
+ }
101
+ }