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.
- package/cli/cli.d.ts +2 -0
- package/cli/cli.js +32 -0
- package/cli/run.d.ts +2 -0
- package/cli/run.js +21 -0
- package/cli/watch.d.ts +1 -0
- package/cli/watch.js +35 -0
- package/code_generator/VitestGenerator.d.ts +36 -0
- package/code_generator/VitestGenerator.js +240 -0
- package/code_generator/outFile.d.ts +20 -0
- package/code_generator/outFile.js +60 -0
- package/code_generator/test_phrases.d.ts +11 -0
- package/code_generator/test_phrases.js +27 -0
- package/compiler/compile.d.ts +16 -0
- package/compiler/compile.js +50 -0
- package/compiler/compiler.d.ts +8 -0
- package/compiler/compiler.js +39 -0
- package/filenames/filenames.d.ts +3 -0
- package/filenames/filenames.js +17 -0
- package/model/Router.d.ts +22 -0
- package/model/Router.js +54 -0
- package/model/model.d.ts +225 -0
- package/model/model.js +526 -0
- package/optimizations/autoLabel/autoLabel.d.ts +2 -0
- package/optimizations/autoLabel/autoLabel.js +19 -0
- package/package.json +1 -4
- package/parser/lexer.d.ts +21 -0
- package/parser/lexer.js +123 -0
- package/parser/lexer_rules.d.ts +33 -0
- package/parser/lexer_rules.js +74 -0
- package/parser/parser.d.ts +18 -0
- package/parser/parser.js +76 -0
- package/phrases_assistant/phrases_assistant.d.ts +13 -0
- package/phrases_assistant/phrases_assistant.js +146 -0
- package/util/indent.d.ts +1 -0
- package/util/indent.js +5 -0
- package/util/iterators.d.ts +1 -0
- package/util/iterators.js +5 -0
- package/util/xmur3.d.ts +1 -0
- package/util/xmur3.js +12 -0
- package/vitest/index.d.ts +11 -0
- 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>;
|
package/parser/parser.js
ADDED
|
@@ -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
|
+
}
|
package/util/indent.d.ts
ADDED
|
@@ -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 @@
|
|
|
1
|
+
export declare function flatMap<T, R>(iterable: Iterable<T>, callback: (value: T) => Iterable<R>): Generator<R, void, undefined>;
|
package/util/xmur3.d.ts
ADDED
|
@@ -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
|
+
}
|
package/vitest/index.js
ADDED
|
@@ -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
|
+
}
|