harmonyc 0.18.0 → 0.18.1
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 +32 -0
- package/code_generator/VitestGenerator.js +221 -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 +9 -0
- package/compiler/compile.js +32 -0
- package/compiler/compiler.d.ts +10 -0
- package/compiler/compiler.js +53 -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 +217 -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/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 +9 -0
- package/vitest/index.js +84 -0
package/cli/cli.d.ts
ADDED
package/cli/cli.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { compileFiles } from "../compiler/compiler.js";
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { watchFiles } from "./watch.js";
|
|
5
|
+
import { run, runWatch } from "./run.js";
|
|
6
|
+
const args = parseArgs({
|
|
7
|
+
options: {
|
|
8
|
+
help: { type: 'boolean' },
|
|
9
|
+
watch: { type: 'boolean', short: 'w' },
|
|
10
|
+
run: { type: 'boolean', short: 'r' },
|
|
11
|
+
},
|
|
12
|
+
allowPositionals: true,
|
|
13
|
+
});
|
|
14
|
+
if (args.positionals.length === 0 || args.values.help) {
|
|
15
|
+
console.error('Usage: harmonyc <input files glob pattern>');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
;
|
|
19
|
+
(async () => {
|
|
20
|
+
if (args.values.watch) {
|
|
21
|
+
const outFns = await watchFiles(args.positionals);
|
|
22
|
+
if (args.values.run) {
|
|
23
|
+
runWatch(outFns);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const { outFns } = await compileFiles(args.positionals);
|
|
28
|
+
if (args.values.run) {
|
|
29
|
+
run(outFns);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})();
|
package/cli/run.d.ts
ADDED
package/cli/run.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { exec, spawn } from 'child_process';
|
|
2
|
+
function runCommand(patterns) {
|
|
3
|
+
return `npx vitest run ${args(patterns)}`;
|
|
4
|
+
}
|
|
5
|
+
function runWatchCommand(patterns) {
|
|
6
|
+
return `npx vitest ${args(patterns)}`;
|
|
7
|
+
}
|
|
8
|
+
function args(patterns) {
|
|
9
|
+
return patterns.map((s) => JSON.stringify(s)).join(' ');
|
|
10
|
+
}
|
|
11
|
+
export function run(patterns) {
|
|
12
|
+
var _a, _b;
|
|
13
|
+
const cmd = runCommand(patterns);
|
|
14
|
+
const p = exec(cmd, { cwd: process.cwd() });
|
|
15
|
+
(_a = p.stdout) === null || _a === void 0 ? void 0 : _a.pipe(process.stdout);
|
|
16
|
+
(_b = p.stderr) === null || _b === void 0 ? void 0 : _b.pipe(process.stderr);
|
|
17
|
+
}
|
|
18
|
+
export function runWatch(patterns) {
|
|
19
|
+
const cmd = runWatchCommand(patterns);
|
|
20
|
+
spawn(cmd, { cwd: process.cwd(), stdio: 'inherit', shell: true });
|
|
21
|
+
}
|
package/cli/watch.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function watchFiles(patterns: string[]): Promise<string[]>;
|
package/cli/watch.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Watcher from 'watcher';
|
|
2
|
+
import { compileFile, compileFiles } from "../compiler/compiler.js";
|
|
3
|
+
export async function watchFiles(patterns) {
|
|
4
|
+
const { fns, outFns } = await compileFiles(patterns);
|
|
5
|
+
for (const file of fns) {
|
|
6
|
+
const watcher = new Watcher(file, { debounce: 20, ignoreInitial: true });
|
|
7
|
+
watcher.on('all', async () => {
|
|
8
|
+
var _a;
|
|
9
|
+
try {
|
|
10
|
+
await compileFile(file);
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
process.stdout.write(`\n`);
|
|
14
|
+
console.log((_a = e.message) !== null && _a !== void 0 ? _a : e);
|
|
15
|
+
process.stdout.write(`\n`);
|
|
16
|
+
}
|
|
17
|
+
logger.log(`Compiled ${file}`);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return outFns;
|
|
21
|
+
}
|
|
22
|
+
const logger = {
|
|
23
|
+
last: '',
|
|
24
|
+
n: 0,
|
|
25
|
+
log(msg) {
|
|
26
|
+
if (msg === this.last) {
|
|
27
|
+
process.stdout.write(`\r${msg} ${++this.n}x`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
process.stdout.write(`\r${msg}`);
|
|
31
|
+
this.last = msg;
|
|
32
|
+
this.n = 1;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Action, CodeGenerator, ErrorResponse, Feature, Phrase, Response, SaveToVariable, SetVariable, Test, TestGroup } from '../model/model.ts';
|
|
2
|
+
import { OutFile } from './outFile.ts';
|
|
3
|
+
export declare class VitestGenerator implements CodeGenerator {
|
|
4
|
+
private tf;
|
|
5
|
+
private sf;
|
|
6
|
+
private _sourceFileName;
|
|
7
|
+
static error(message: string, stack: string): string;
|
|
8
|
+
framework: string;
|
|
9
|
+
phraseFns: Map<string, Phrase>;
|
|
10
|
+
currentFeatureName: string;
|
|
11
|
+
constructor(tf: OutFile, sf: OutFile, _sourceFileName: string);
|
|
12
|
+
feature(feature: Feature): void;
|
|
13
|
+
testGroup(g: TestGroup): void;
|
|
14
|
+
featureVars: Map<string, string>;
|
|
15
|
+
resultCount: number;
|
|
16
|
+
test(t: Test): void;
|
|
17
|
+
errorStep(action: Action, errorResponse: ErrorResponse): void;
|
|
18
|
+
extraArgs: string[];
|
|
19
|
+
step(action: Action, responses: Response[]): void;
|
|
20
|
+
private declareFeatureVariables;
|
|
21
|
+
phrase(p: Phrase): void;
|
|
22
|
+
setVariable(action: SetVariable): void;
|
|
23
|
+
saveToVariable(s: SaveToVariable, what?: string): void;
|
|
24
|
+
stringLiteral(text: string, { withVariables }: {
|
|
25
|
+
withVariables: boolean;
|
|
26
|
+
}): string;
|
|
27
|
+
codeLiteral(src: string): string;
|
|
28
|
+
private paramName;
|
|
29
|
+
stringParamDeclaration(index: number): string;
|
|
30
|
+
variantParamDeclaration(index: number): string;
|
|
31
|
+
}
|
|
32
|
+
export declare function functionName(phrase: Phrase): string;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
import { Arg, Response, Word, } from "../model/model.js";
|
|
3
|
+
export class VitestGenerator {
|
|
4
|
+
static error(message, stack) {
|
|
5
|
+
return `const e = new SyntaxError(${str(message)});
|
|
6
|
+
e.stack = undefined;
|
|
7
|
+
throw e;
|
|
8
|
+
${stack ? `/* ${stack} */` : ''}`;
|
|
9
|
+
}
|
|
10
|
+
constructor(tf, sf, _sourceFileName) {
|
|
11
|
+
this.tf = tf;
|
|
12
|
+
this.sf = sf;
|
|
13
|
+
this._sourceFileName = _sourceFileName;
|
|
14
|
+
this.framework = 'vitest';
|
|
15
|
+
this.phraseFns = new Map();
|
|
16
|
+
this.currentFeatureName = '';
|
|
17
|
+
this.resultCount = 0;
|
|
18
|
+
this.extraArgs = [];
|
|
19
|
+
}
|
|
20
|
+
feature(feature) {
|
|
21
|
+
const phrasesModule = './' + basename(this.sf.name.replace(/.(js|ts)$/, ''));
|
|
22
|
+
const fn = (this.currentFeatureName = pascalCase(feature.name));
|
|
23
|
+
this.phraseFns = new Map();
|
|
24
|
+
// test file
|
|
25
|
+
if (this.framework === 'vitest') {
|
|
26
|
+
this.tf.print(`import { describe, test, expect } from "vitest";`);
|
|
27
|
+
}
|
|
28
|
+
if (feature.tests.length === 0) {
|
|
29
|
+
this.tf.print('');
|
|
30
|
+
this.tf.print(`describe.todo(${str(feature.name)});`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.tf.print(`import ${fn}Phrases from ${str(phrasesModule)};`);
|
|
34
|
+
this.tf.print(``);
|
|
35
|
+
for (const item of feature.testGroups) {
|
|
36
|
+
item.toCode(this);
|
|
37
|
+
}
|
|
38
|
+
this.tf.print(``);
|
|
39
|
+
// phrases file
|
|
40
|
+
this.sf.print(`export default class ${pascalCase(feature.name)}Phrases {`);
|
|
41
|
+
this.sf.indent(() => {
|
|
42
|
+
for (const ph of this.phraseFns.keys()) {
|
|
43
|
+
const p = this.phraseFns.get(ph);
|
|
44
|
+
const params = p.args.map((a, i) => a.toDeclaration(this, i)).join(', ');
|
|
45
|
+
this.sf.print(`async ${ph}(${params}) {`);
|
|
46
|
+
this.sf.indent(() => {
|
|
47
|
+
this.sf.print(`throw new Error(${str(`Pending: ${ph}`)});`);
|
|
48
|
+
});
|
|
49
|
+
this.sf.print(`}`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
this.sf.print(`};`);
|
|
53
|
+
}
|
|
54
|
+
testGroup(g) {
|
|
55
|
+
this.tf.print(`describe(${str(g.label.text)}, () => {`, g.label.start, g.label.text);
|
|
56
|
+
this.tf.indent(() => {
|
|
57
|
+
for (const item of g.items) {
|
|
58
|
+
item.toCode(this);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
this.tf.print('});');
|
|
62
|
+
}
|
|
63
|
+
test(t) {
|
|
64
|
+
var _a;
|
|
65
|
+
this.resultCount = 0;
|
|
66
|
+
this.featureVars = new Map();
|
|
67
|
+
// avoid shadowing this import name
|
|
68
|
+
this.featureVars.set(new Object(), this.currentFeatureName);
|
|
69
|
+
this.tf.print(`test(${str(t.name)}, async (context) => {`, (_a = t.lastStrain) === null || _a === void 0 ? void 0 : _a.start, t.testNumber);
|
|
70
|
+
this.tf.indent(() => {
|
|
71
|
+
this.tf.print(`context.task.meta.phrases ??= [];`);
|
|
72
|
+
for (const step of t.steps) {
|
|
73
|
+
step.toCode(this);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
this.tf.print('});');
|
|
77
|
+
}
|
|
78
|
+
errorStep(action, errorResponse) {
|
|
79
|
+
this.declareFeatureVariables([action]);
|
|
80
|
+
this.tf.print(`await expect(async () => {`);
|
|
81
|
+
this.tf.indent(() => {
|
|
82
|
+
action.toCode(this);
|
|
83
|
+
this.tf.print(`context.task.meta.phrases.push(${str(errorResponse.toSingleLineString())});`);
|
|
84
|
+
});
|
|
85
|
+
this.tf.print(`}).rejects.toThrow(${(errorResponse === null || errorResponse === void 0 ? void 0 : errorResponse.message) !== undefined
|
|
86
|
+
? str(errorResponse.message.text)
|
|
87
|
+
: ''});`);
|
|
88
|
+
}
|
|
89
|
+
step(action, responses) {
|
|
90
|
+
this.declareFeatureVariables([action, ...responses]);
|
|
91
|
+
if (responses.length === 0) {
|
|
92
|
+
action.toCode(this);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (action.isEmpty) {
|
|
96
|
+
for (const response of responses) {
|
|
97
|
+
response.toCode(this);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const res = `r${this.resultCount++ || ''}`;
|
|
102
|
+
this.tf.print(`const ${res} =`);
|
|
103
|
+
this.tf.indent(() => {
|
|
104
|
+
action.toCode(this);
|
|
105
|
+
try {
|
|
106
|
+
this.extraArgs = [res];
|
|
107
|
+
for (const response of responses) {
|
|
108
|
+
response.toCode(this);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
this.extraArgs = [];
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
declareFeatureVariables(phrases) {
|
|
117
|
+
for (const p of phrases) {
|
|
118
|
+
const feature = p.feature.name;
|
|
119
|
+
let f = this.featureVars.get(feature);
|
|
120
|
+
if (!f) {
|
|
121
|
+
f = toId(feature, abbrev, this.featureVars);
|
|
122
|
+
this.tf.print(`const ${f} = new ${pascalCase(feature)}Phrases(context);`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
phrase(p) {
|
|
127
|
+
const phrasefn = functionName(p);
|
|
128
|
+
if (!this.phraseFns.has(phrasefn))
|
|
129
|
+
this.phraseFns.set(phrasefn, p);
|
|
130
|
+
const f = this.featureVars.get(p.feature.name);
|
|
131
|
+
const args = p.args.map((a) => a.toCode(this));
|
|
132
|
+
args.push(...this.extraArgs);
|
|
133
|
+
if (p instanceof Response && p.parts.length === 1 && p.saveToVariable) {
|
|
134
|
+
return this.saveToVariable(p.saveToVariable);
|
|
135
|
+
}
|
|
136
|
+
const name = p.toSingleLineString();
|
|
137
|
+
this.tf.print(`(context.task.meta.phrases.push(${str(p.toString())}),`);
|
|
138
|
+
if (p instanceof Response && p.saveToVariable) {
|
|
139
|
+
this.saveToVariable(p.saveToVariable, '');
|
|
140
|
+
}
|
|
141
|
+
this.tf.printn(`await ${f}.`);
|
|
142
|
+
this.tf.write(`${functionName(p)}(${args.join(', ')})`, p.start, name);
|
|
143
|
+
this.tf.write(`);`);
|
|
144
|
+
this.tf.nl();
|
|
145
|
+
}
|
|
146
|
+
setVariable(action) {
|
|
147
|
+
this.tf.print(`(context.task.meta.variables ??= {})[${str(action.variableName)}] = ${action.value.toCode(this)};`);
|
|
148
|
+
}
|
|
149
|
+
saveToVariable(s, what = this.extraArgs[0] + ';') {
|
|
150
|
+
this.tf.print(`(context.task.meta.variables ??= {})[${str(s.variableName)}] = ${what}`.trimEnd());
|
|
151
|
+
}
|
|
152
|
+
stringLiteral(text, { withVariables }) {
|
|
153
|
+
if (withVariables && text.match(/\$\{/)) {
|
|
154
|
+
return templateStr(text).replace(/\\\$\{([^\s}]+)\}/g, (_, x) => `\${context.task.meta.variables?.[${str(x)}]}`);
|
|
155
|
+
}
|
|
156
|
+
return str(text);
|
|
157
|
+
}
|
|
158
|
+
codeLiteral(src) {
|
|
159
|
+
return src.replace(/\$\{([^\s}]+)\}/g, (_, x) => `context.task.meta.variables?.[${str(x)}]`);
|
|
160
|
+
}
|
|
161
|
+
paramName(index) {
|
|
162
|
+
return 'xyz'.charAt(index) || `a${index + 1}`;
|
|
163
|
+
}
|
|
164
|
+
stringParamDeclaration(index) {
|
|
165
|
+
return `${this.paramName(index)}: string`;
|
|
166
|
+
}
|
|
167
|
+
variantParamDeclaration(index) {
|
|
168
|
+
return `${this.paramName(index)}: any`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function str(s) {
|
|
172
|
+
if (s.includes('\n'))
|
|
173
|
+
return '\n' + templateStr(s);
|
|
174
|
+
let r = JSON.stringify(s);
|
|
175
|
+
return r;
|
|
176
|
+
}
|
|
177
|
+
function templateStr(s) {
|
|
178
|
+
return '`' + s.replace(/([`$\\])/g, '\\$1') + '`';
|
|
179
|
+
}
|
|
180
|
+
function capitalize(s) {
|
|
181
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
182
|
+
}
|
|
183
|
+
function toId(s, transform, previous) {
|
|
184
|
+
if (previous.has(s))
|
|
185
|
+
return previous.get(s);
|
|
186
|
+
let base = transform(s);
|
|
187
|
+
let id = base;
|
|
188
|
+
if ([...previous.values()].includes(id)) {
|
|
189
|
+
let i = 1;
|
|
190
|
+
while ([...previous.values()].includes(id + i))
|
|
191
|
+
i++;
|
|
192
|
+
id = base + i;
|
|
193
|
+
}
|
|
194
|
+
previous.set(s, id);
|
|
195
|
+
return id;
|
|
196
|
+
}
|
|
197
|
+
function words(s) {
|
|
198
|
+
return s.split(/[^0-9\p{L}]+/gu);
|
|
199
|
+
}
|
|
200
|
+
function pascalCase(s) {
|
|
201
|
+
return words(s).map(capitalize).join('');
|
|
202
|
+
}
|
|
203
|
+
function underscore(s) {
|
|
204
|
+
return words(s).join('_');
|
|
205
|
+
}
|
|
206
|
+
function abbrev(s) {
|
|
207
|
+
return words(s)
|
|
208
|
+
.map((x) => x.charAt(0).toUpperCase())
|
|
209
|
+
.join('');
|
|
210
|
+
}
|
|
211
|
+
export function functionName(phrase) {
|
|
212
|
+
const { kind } = phrase;
|
|
213
|
+
return ((kind === 'response' ? 'Then_' : 'When_') +
|
|
214
|
+
(phrase.parts
|
|
215
|
+
.flatMap((c) => c instanceof Word
|
|
216
|
+
? words(c.text).filter((x) => x)
|
|
217
|
+
: c instanceof Arg
|
|
218
|
+
? ['']
|
|
219
|
+
: [])
|
|
220
|
+
.join('_') || ''));
|
|
221
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SourceNode } from 'source-map-js';
|
|
2
|
+
import { Location } from '../model/model.ts';
|
|
3
|
+
export declare class OutFile {
|
|
4
|
+
name: string;
|
|
5
|
+
sourceFile: string;
|
|
6
|
+
level: number;
|
|
7
|
+
sm: SourceNode;
|
|
8
|
+
indentSpaces: number;
|
|
9
|
+
constructor(name: string, sourceFile: string);
|
|
10
|
+
indent(fn: () => void): void;
|
|
11
|
+
private get currentIndent();
|
|
12
|
+
clear(): void;
|
|
13
|
+
printn(line: string, start?: Location, name?: string, end?: Location): void;
|
|
14
|
+
print(line: string, start?: Location, name?: string, end?: Location): void;
|
|
15
|
+
write(line: string, start?: Location, name?: string, end?: Location): void;
|
|
16
|
+
nl(): void;
|
|
17
|
+
get sourceMap(): import("source-map-js").RawSourceMap;
|
|
18
|
+
get valueWithoutSourceMap(): string;
|
|
19
|
+
get value(): string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { SourceNode } from 'source-map-js';
|
|
2
|
+
export class OutFile {
|
|
3
|
+
constructor(name, sourceFile) {
|
|
4
|
+
this.name = name;
|
|
5
|
+
this.sourceFile = sourceFile;
|
|
6
|
+
this.level = 0;
|
|
7
|
+
this.indentSpaces = 2;
|
|
8
|
+
this.sm = new SourceNode(0, 0, sourceFile);
|
|
9
|
+
}
|
|
10
|
+
indent(fn) {
|
|
11
|
+
this.level++;
|
|
12
|
+
try {
|
|
13
|
+
fn();
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
this.level--;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
get currentIndent() {
|
|
20
|
+
return ' '.repeat(this.level * this.indentSpaces);
|
|
21
|
+
}
|
|
22
|
+
clear() {
|
|
23
|
+
this.sm = new SourceNode(0, 0, this.sourceFile);
|
|
24
|
+
}
|
|
25
|
+
printn(line, start, name, end) {
|
|
26
|
+
this.write(this.currentIndent + line, start, name, end);
|
|
27
|
+
}
|
|
28
|
+
print(line, start, name, end) {
|
|
29
|
+
this.write(this.currentIndent + line + '\n', start, name, end);
|
|
30
|
+
}
|
|
31
|
+
write(line, start, name, end) {
|
|
32
|
+
const chunk = line;
|
|
33
|
+
if (start) {
|
|
34
|
+
this.sm.add(new SourceNode(start.line, start.column, this.sourceFile, chunk, name));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
this.sm.add(new SourceNode(null, null, null, chunk));
|
|
38
|
+
}
|
|
39
|
+
if (end) {
|
|
40
|
+
this.sm.add(new SourceNode(end.line, end.column, this.sourceFile));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
nl() {
|
|
44
|
+
this.write('\n');
|
|
45
|
+
}
|
|
46
|
+
get sourceMap() {
|
|
47
|
+
return this.sm.toStringWithSourceMap({ file: this.name }).map.toJSON();
|
|
48
|
+
}
|
|
49
|
+
get valueWithoutSourceMap() {
|
|
50
|
+
return this.sm.toString();
|
|
51
|
+
}
|
|
52
|
+
get value() {
|
|
53
|
+
const { code, map } = this.sm.toStringWithSourceMap({ file: this.name });
|
|
54
|
+
let res = code;
|
|
55
|
+
res +=
|
|
56
|
+
`\n//# sour` + // not for this file ;)
|
|
57
|
+
`ceMappingURL=data:application/json,${encodeURIComponent(map.toString())}`;
|
|
58
|
+
return res;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class TestPhrases {
|
|
2
|
+
private context;
|
|
3
|
+
constructor(context: any);
|
|
4
|
+
When_goodbye(): void;
|
|
5
|
+
When_hello(): string;
|
|
6
|
+
When_greet_(name: string): void;
|
|
7
|
+
Then__is_(x: string, y: string): Promise<void>;
|
|
8
|
+
Then_last_char(s: string): string;
|
|
9
|
+
Then_last_char_of_greeting(): any;
|
|
10
|
+
Then_(s: string, r: string): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { expect } from 'vitest';
|
|
2
|
+
export class TestPhrases {
|
|
3
|
+
constructor(context) {
|
|
4
|
+
this.context = context;
|
|
5
|
+
}
|
|
6
|
+
When_goodbye() {
|
|
7
|
+
throw new Error('Goodbye, World!');
|
|
8
|
+
}
|
|
9
|
+
When_hello() {
|
|
10
|
+
return (this.context.task.meta.greeting = 'Hello!');
|
|
11
|
+
}
|
|
12
|
+
When_greet_(name) {
|
|
13
|
+
this.context.task.meta.greeting = `Hello, ${name}!`;
|
|
14
|
+
}
|
|
15
|
+
async Then__is_(x, y) {
|
|
16
|
+
expect(x).toBe(y);
|
|
17
|
+
}
|
|
18
|
+
Then_last_char(s) {
|
|
19
|
+
return s.slice(-1);
|
|
20
|
+
}
|
|
21
|
+
Then_last_char_of_greeting() {
|
|
22
|
+
return this.context.task.meta.greeting.slice(-1);
|
|
23
|
+
}
|
|
24
|
+
Then_(s, r) {
|
|
25
|
+
expect(s).toBe(r);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { OutFile } from '../code_generator/outFile.ts';
|
|
2
|
+
export interface CompiledFeature {
|
|
3
|
+
name: string;
|
|
4
|
+
code: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
export declare function compileFeature(fileName: string, src: string): {
|
|
7
|
+
outFile: OutFile;
|
|
8
|
+
phrasesFile: OutFile;
|
|
9
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { VitestGenerator } from "../code_generator/VitestGenerator.js";
|
|
3
|
+
import { OutFile } from "../code_generator/outFile.js";
|
|
4
|
+
import { base, phrasesFileName, testFileName } from "../filenames/filenames.js";
|
|
5
|
+
import { Feature } from "../model/model.js";
|
|
6
|
+
import { autoLabel } from "../optimizations/autoLabel/autoLabel.js";
|
|
7
|
+
import { parse } from "../parser/parser.js";
|
|
8
|
+
export function compileFeature(fileName, src) {
|
|
9
|
+
const feature = new Feature(basename(base(fileName)));
|
|
10
|
+
try {
|
|
11
|
+
feature.root = parse(src);
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
if (e.pos && e.errorMessage) {
|
|
15
|
+
e.message =
|
|
16
|
+
e.stack = `Error in ${fileName}:${e.pos.rowBegin}:${e.pos.columnBegin}: ${e.errorMessage}`;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
e.stack = `Error in ${fileName}: ${e.stack}`;
|
|
20
|
+
}
|
|
21
|
+
throw e;
|
|
22
|
+
}
|
|
23
|
+
feature.root.setFeature(feature);
|
|
24
|
+
autoLabel(feature.root);
|
|
25
|
+
const testFn = testFileName(fileName);
|
|
26
|
+
const testFile = new OutFile(testFn, fileName);
|
|
27
|
+
const phrasesFn = phrasesFileName(fileName);
|
|
28
|
+
const phrasesFile = new OutFile(phrasesFn, fileName);
|
|
29
|
+
const cg = new VitestGenerator(testFile, phrasesFile, fileName);
|
|
30
|
+
feature.toCode(cg);
|
|
31
|
+
return { outFile: testFile, phrasesFile };
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function compileFiles(pattern: string | string[]): Promise<{
|
|
2
|
+
fns: string[];
|
|
3
|
+
outFns: string[];
|
|
4
|
+
}>;
|
|
5
|
+
export declare function compileFile(fn: string): Promise<{
|
|
6
|
+
phrasesFileAction: string;
|
|
7
|
+
outFile: import("../code_generator/outFile.ts").OutFile;
|
|
8
|
+
phrasesFile: import("../code_generator/outFile.ts").OutFile;
|
|
9
|
+
} | undefined>;
|
|
10
|
+
export declare function preprocess(src: string): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import glob from 'fast-glob';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { VitestGenerator } from "../code_generator/VitestGenerator.js";
|
|
5
|
+
import { testFileName } from "../filenames/filenames.js";
|
|
6
|
+
import { compileFeature } from "./compile.js";
|
|
7
|
+
export async function compileFiles(pattern) {
|
|
8
|
+
var _a;
|
|
9
|
+
const fns = await glob(pattern);
|
|
10
|
+
if (!fns.length)
|
|
11
|
+
throw new Error(`No files found for pattern: ${String(pattern)}`);
|
|
12
|
+
const results = await Promise.allSettled(fns.map((fn) => compileFile(fn)));
|
|
13
|
+
const compiled = results.flatMap((r) => r.status === 'fulfilled' ? [r.value] : []);
|
|
14
|
+
const errored = results.flatMap((r) => r.status === 'rejected' && r.reason ? [r.reason] : []);
|
|
15
|
+
for (const error of errored) {
|
|
16
|
+
console.error((_a = error.message) !== null && _a !== void 0 ? _a : error);
|
|
17
|
+
}
|
|
18
|
+
console.log(`Compiled ${compiled.length} file${compiled.length === 1 ? '' : 's'}.`);
|
|
19
|
+
const features = compiled.filter((f) => f !== undefined);
|
|
20
|
+
const generated = features.filter((f) => f.phrasesFileAction === 'generated');
|
|
21
|
+
if (generated.length) {
|
|
22
|
+
console.log(`Generated ${generated.length} phrases file${generated.length === 1 ? '' : 's'}.`);
|
|
23
|
+
}
|
|
24
|
+
return { fns, outFns: features.map((f) => f.outFile.name) };
|
|
25
|
+
}
|
|
26
|
+
export async function compileFile(fn) {
|
|
27
|
+
var _a;
|
|
28
|
+
fn = resolve(fn);
|
|
29
|
+
const src = preprocess(readFileSync(fn, 'utf8').toString());
|
|
30
|
+
try {
|
|
31
|
+
const { outFile, phrasesFile } = compileFeature(fn, src);
|
|
32
|
+
writeFileSync(outFile.name, outFile.value);
|
|
33
|
+
let phrasesFileAction = 'ignored';
|
|
34
|
+
if (!existsSync(phrasesFile.name)) {
|
|
35
|
+
phrasesFileAction = 'generated';
|
|
36
|
+
writeFileSync(phrasesFile.name, phrasesFile.value);
|
|
37
|
+
}
|
|
38
|
+
return { phrasesFileAction, outFile, phrasesFile };
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
const outFileName = testFileName(fn);
|
|
42
|
+
writeFileSync(outFileName, VitestGenerator.error((_a = e.message) !== null && _a !== void 0 ? _a : `${e}`, e.stack));
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function preprocess(src) {
|
|
47
|
+
// strip BOM
|
|
48
|
+
if (src.charCodeAt(0) === 0xfeff) {
|
|
49
|
+
src = src.slice(1);
|
|
50
|
+
}
|
|
51
|
+
src = src.replace(/\r\n?/g, '\n');
|
|
52
|
+
return src;
|
|
53
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import glob from 'fast-glob';
|
|
2
|
+
const { globSync, convertPathToPattern } = glob;
|
|
3
|
+
export function base(fn) {
|
|
4
|
+
return fn.replace(/\.harmony(\.\w+)?$/i, '');
|
|
5
|
+
}
|
|
6
|
+
export function testFileName(fn) {
|
|
7
|
+
return base(fn) + '.test.mjs';
|
|
8
|
+
}
|
|
9
|
+
export function phrasesFileName(fn) {
|
|
10
|
+
const baseFn = base(fn);
|
|
11
|
+
const pattern = convertPathToPattern(baseFn);
|
|
12
|
+
const existing = globSync(`${pattern}.phrases.{tsx,jsx,ts,js}`);
|
|
13
|
+
if (existing.length) {
|
|
14
|
+
return existing.sort().at(-1);
|
|
15
|
+
}
|
|
16
|
+
return `${baseFn}.phrases.ts`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Branch } from './model.ts';
|
|
2
|
+
export declare class Router<N> {
|
|
3
|
+
outs: N[];
|
|
4
|
+
index: number;
|
|
5
|
+
random: () => number;
|
|
6
|
+
started: Set<N>;
|
|
7
|
+
covered: Set<N>;
|
|
8
|
+
constructor(outs: N[], seed?: string);
|
|
9
|
+
next(): N;
|
|
10
|
+
get incompleteCount(): number;
|
|
11
|
+
}
|
|
12
|
+
type N = Branch;
|
|
13
|
+
export declare class Routers {
|
|
14
|
+
private root;
|
|
15
|
+
routers: Map<Branch, Router<Branch>>;
|
|
16
|
+
constructor(root: N);
|
|
17
|
+
discover(branch: N): void;
|
|
18
|
+
get(branch: N): Router<Branch>;
|
|
19
|
+
nextWalk(): Branch[];
|
|
20
|
+
getIncompleteCount(): number;
|
|
21
|
+
}
|
|
22
|
+
export {};
|