harmonyc 0.18.1 → 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/code_generator/VitestGenerator.d.ts +9 -5
- package/code_generator/VitestGenerator.js +51 -32
- package/code_generator/test_phrases.d.ts +3 -3
- package/code_generator/test_phrases.js +3 -3
- package/compiler/compile.d.ts +9 -2
- package/compiler/compile.js +24 -6
- package/compiler/compiler.d.ts +0 -2
- package/compiler/compiler.js +3 -17
- package/model/model.d.ts +8 -0
- package/package.json +1 -1
- package/phrases_assistant/phrases_assistant.d.ts +13 -0
- package/phrases_assistant/phrases_assistant.js +146 -0
- package/vitest/index.d.ts +4 -2
- package/vitest/index.js +30 -13
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CompilerOptions } from '../compiler/compile.ts';
|
|
2
|
+
import { Action, CodeGenerator, ErrorResponse, Feature, Phrase, PhraseMethod, Response, SaveToVariable, SetVariable, Test, TestGroup } from '../model/model.ts';
|
|
2
3
|
import { OutFile } from './outFile.ts';
|
|
3
4
|
export declare class VitestGenerator implements CodeGenerator {
|
|
4
5
|
private tf;
|
|
5
|
-
private
|
|
6
|
-
private
|
|
6
|
+
private sourceFileName;
|
|
7
|
+
private opts;
|
|
7
8
|
static error(message: string, stack: string): string;
|
|
8
9
|
framework: string;
|
|
9
10
|
phraseFns: Map<string, Phrase>;
|
|
10
11
|
currentFeatureName: string;
|
|
11
|
-
|
|
12
|
+
featureClassName: string;
|
|
13
|
+
phraseMethods: PhraseMethod[];
|
|
14
|
+
constructor(tf: OutFile, sourceFileName: string, opts: CompilerOptions);
|
|
12
15
|
feature(feature: Feature): void;
|
|
13
16
|
testGroup(g: TestGroup): void;
|
|
14
17
|
featureVars: Map<string, string>;
|
|
@@ -28,5 +31,6 @@ export declare class VitestGenerator implements CodeGenerator {
|
|
|
28
31
|
private paramName;
|
|
29
32
|
stringParamDeclaration(index: number): string;
|
|
30
33
|
variantParamDeclaration(index: number): string;
|
|
34
|
+
functionName(phrase: Phrase): string;
|
|
35
|
+
argPlaceholder(i: number): string;
|
|
31
36
|
}
|
|
32
|
-
export declare function functionName(phrase: Phrase): string;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { basename } from 'path';
|
|
2
|
+
import { xyzab } from "../compiler/compile.js";
|
|
2
3
|
import { Arg, Response, Word, } from "../model/model.js";
|
|
4
|
+
const X = 'X'.codePointAt(0);
|
|
5
|
+
const A = 'A'.codePointAt(0);
|
|
3
6
|
export class VitestGenerator {
|
|
4
7
|
static error(message, stack) {
|
|
5
8
|
return `const e = new SyntaxError(${str(message)});
|
|
@@ -7,19 +10,22 @@ export class VitestGenerator {
|
|
|
7
10
|
throw e;
|
|
8
11
|
${stack ? `/* ${stack} */` : ''}`;
|
|
9
12
|
}
|
|
10
|
-
constructor(tf,
|
|
13
|
+
constructor(tf, sourceFileName, opts) {
|
|
11
14
|
this.tf = tf;
|
|
12
|
-
this.
|
|
13
|
-
this.
|
|
15
|
+
this.sourceFileName = sourceFileName;
|
|
16
|
+
this.opts = opts;
|
|
14
17
|
this.framework = 'vitest';
|
|
15
18
|
this.phraseFns = new Map();
|
|
16
19
|
this.currentFeatureName = '';
|
|
20
|
+
this.phraseMethods = [];
|
|
17
21
|
this.resultCount = 0;
|
|
18
22
|
this.extraArgs = [];
|
|
19
23
|
}
|
|
20
24
|
feature(feature) {
|
|
21
|
-
const phrasesModule = './' + basename(this.
|
|
22
|
-
const fn = (this.
|
|
25
|
+
const phrasesModule = './' + basename(this.sourceFileName.replace(/\.harmony$/, '.phrases.js'));
|
|
26
|
+
const fn = (this.featureClassName =
|
|
27
|
+
this.currentFeatureName =
|
|
28
|
+
pascalCase(feature.name) + 'Phrases');
|
|
23
29
|
this.phraseFns = new Map();
|
|
24
30
|
// test file
|
|
25
31
|
if (this.framework === 'vitest') {
|
|
@@ -30,26 +36,33 @@ export class VitestGenerator {
|
|
|
30
36
|
this.tf.print(`describe.todo(${str(feature.name)});`);
|
|
31
37
|
return;
|
|
32
38
|
}
|
|
33
|
-
this.tf.print(`import ${fn}
|
|
39
|
+
this.tf.print(`import ${fn} from ${str(phrasesModule)};`);
|
|
34
40
|
this.tf.print(``);
|
|
35
41
|
for (const item of feature.testGroups) {
|
|
36
42
|
item.toCode(this);
|
|
37
43
|
}
|
|
38
44
|
this.tf.print(``);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
for (const ph of this.phraseFns.keys()) {
|
|
46
|
+
const p = this.phraseFns.get(ph);
|
|
47
|
+
const parameters = p.args.map((a, i) => {
|
|
48
|
+
const declaration = a.toDeclaration(this, i);
|
|
49
|
+
const parts = declaration.split(': ');
|
|
50
|
+
return {
|
|
51
|
+
name: parts[0],
|
|
52
|
+
type: parts[1] || 'any',
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
if (p instanceof Response) {
|
|
56
|
+
parameters.push({
|
|
57
|
+
name: 'res',
|
|
58
|
+
type: 'any',
|
|
48
59
|
});
|
|
49
|
-
this.sf.print(`}`);
|
|
50
60
|
}
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
this.phraseMethods.push({
|
|
62
|
+
name: ph,
|
|
63
|
+
parameters,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
53
66
|
}
|
|
54
67
|
testGroup(g) {
|
|
55
68
|
this.tf.print(`describe(${str(g.label.text)}, () => {`, g.label.start, g.label.text);
|
|
@@ -124,7 +137,7 @@ export class VitestGenerator {
|
|
|
124
137
|
}
|
|
125
138
|
}
|
|
126
139
|
phrase(p) {
|
|
127
|
-
const phrasefn = functionName(p);
|
|
140
|
+
const phrasefn = this.functionName(p);
|
|
128
141
|
if (!this.phraseFns.has(phrasefn))
|
|
129
142
|
this.phraseFns.set(phrasefn, p);
|
|
130
143
|
const f = this.featureVars.get(p.feature.name);
|
|
@@ -139,7 +152,7 @@ export class VitestGenerator {
|
|
|
139
152
|
this.saveToVariable(p.saveToVariable, '');
|
|
140
153
|
}
|
|
141
154
|
this.tf.printn(`await ${f}.`);
|
|
142
|
-
this.tf.write(`${
|
|
155
|
+
this.tf.write(`${phrasefn}(${args.join(', ')})`, p.start, name);
|
|
143
156
|
this.tf.write(`);`);
|
|
144
157
|
this.tf.nl();
|
|
145
158
|
}
|
|
@@ -159,7 +172,7 @@ export class VitestGenerator {
|
|
|
159
172
|
return src.replace(/\$\{([^\s}]+)\}/g, (_, x) => `context.task.meta.variables?.[${str(x)}]`);
|
|
160
173
|
}
|
|
161
174
|
paramName(index) {
|
|
162
|
-
return
|
|
175
|
+
return xyzab(index);
|
|
163
176
|
}
|
|
164
177
|
stringParamDeclaration(index) {
|
|
165
178
|
return `${this.paramName(index)}: string`;
|
|
@@ -167,6 +180,23 @@ export class VitestGenerator {
|
|
|
167
180
|
variantParamDeclaration(index) {
|
|
168
181
|
return `${this.paramName(index)}: any`;
|
|
169
182
|
}
|
|
183
|
+
functionName(phrase) {
|
|
184
|
+
const { kind } = phrase;
|
|
185
|
+
let argIndex = -1;
|
|
186
|
+
return ((kind === 'response' ? 'Then_' : 'When_') +
|
|
187
|
+
(phrase.parts
|
|
188
|
+
.flatMap((c) => c instanceof Word
|
|
189
|
+
? words(c.text).filter((x) => x)
|
|
190
|
+
: c instanceof Arg
|
|
191
|
+
? [this.argPlaceholder(++argIndex)]
|
|
192
|
+
: [])
|
|
193
|
+
.join('_') || ''));
|
|
194
|
+
}
|
|
195
|
+
argPlaceholder(i) {
|
|
196
|
+
return typeof this.opts.argumentPlaceholder === 'function'
|
|
197
|
+
? this.opts.argumentPlaceholder(i)
|
|
198
|
+
: this.opts.argumentPlaceholder;
|
|
199
|
+
}
|
|
170
200
|
}
|
|
171
201
|
function str(s) {
|
|
172
202
|
if (s.includes('\n'))
|
|
@@ -208,14 +238,3 @@ function abbrev(s) {
|
|
|
208
238
|
.map((x) => x.charAt(0).toUpperCase())
|
|
209
239
|
.join('');
|
|
210
240
|
}
|
|
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
|
-
}
|
|
@@ -3,9 +3,9 @@ export declare class TestPhrases {
|
|
|
3
3
|
constructor(context: any);
|
|
4
4
|
When_goodbye(): void;
|
|
5
5
|
When_hello(): string;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
When_greet_X(name: string): void;
|
|
7
|
+
Then_X_is_Y(x: string, y: string): Promise<void>;
|
|
8
8
|
Then_last_char(s: string): string;
|
|
9
9
|
Then_last_char_of_greeting(): any;
|
|
10
|
-
|
|
10
|
+
Then_X(s: string, r: string): void;
|
|
11
11
|
}
|
|
@@ -9,10 +9,10 @@ export class TestPhrases {
|
|
|
9
9
|
When_hello() {
|
|
10
10
|
return (this.context.task.meta.greeting = 'Hello!');
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
When_greet_X(name) {
|
|
13
13
|
this.context.task.meta.greeting = `Hello, ${name}!`;
|
|
14
14
|
}
|
|
15
|
-
async
|
|
15
|
+
async Then_X_is_Y(x, y) {
|
|
16
16
|
expect(x).toBe(y);
|
|
17
17
|
}
|
|
18
18
|
Then_last_char(s) {
|
|
@@ -21,7 +21,7 @@ export class TestPhrases {
|
|
|
21
21
|
Then_last_char_of_greeting() {
|
|
22
22
|
return this.context.task.meta.greeting.slice(-1);
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
Then_X(s, r) {
|
|
25
25
|
expect(s).toBe(r);
|
|
26
26
|
}
|
|
27
27
|
}
|
package/compiler/compile.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { OutFile } from '../code_generator/outFile.ts';
|
|
2
|
+
export interface CompilerOptions {
|
|
3
|
+
argumentPlaceholder: string | ((index: number) => string);
|
|
4
|
+
}
|
|
2
5
|
export interface CompiledFeature {
|
|
3
6
|
name: string;
|
|
4
7
|
code: Record<string, string>;
|
|
5
8
|
}
|
|
6
|
-
export declare function
|
|
9
|
+
export declare function XYZAB(index: number): string;
|
|
10
|
+
export declare function xyzab(index: number): string;
|
|
11
|
+
export declare const DEFAULT_COMPILER_OPTIONS: CompilerOptions;
|
|
12
|
+
export declare function compileFeature(fileName: string, src: string, opts?: Partial<CompilerOptions>): {
|
|
7
13
|
outFile: OutFile;
|
|
8
|
-
|
|
14
|
+
phraseMethods: import("../model/model.ts").PhraseMethod[];
|
|
15
|
+
featureClassName: string;
|
|
9
16
|
};
|
package/compiler/compile.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { VitestGenerator } from "../code_generator/VitestGenerator.js";
|
|
3
3
|
import { OutFile } from "../code_generator/outFile.js";
|
|
4
|
-
import { base,
|
|
4
|
+
import { base, testFileName } from "../filenames/filenames.js";
|
|
5
5
|
import { Feature } from "../model/model.js";
|
|
6
6
|
import { autoLabel } from "../optimizations/autoLabel/autoLabel.js";
|
|
7
7
|
import { parse } from "../parser/parser.js";
|
|
8
|
-
|
|
8
|
+
const X = 'X'.codePointAt(0);
|
|
9
|
+
const A = 'A'.codePointAt(0);
|
|
10
|
+
const x = 'x'.codePointAt(0);
|
|
11
|
+
const a = 'a'.codePointAt(0);
|
|
12
|
+
export function XYZAB(index) {
|
|
13
|
+
return String.fromCodePoint(A + ((X - A + index) % 26));
|
|
14
|
+
}
|
|
15
|
+
export function xyzab(index) {
|
|
16
|
+
return String.fromCodePoint(a + ((x - a + index) % 26));
|
|
17
|
+
}
|
|
18
|
+
export const DEFAULT_COMPILER_OPTIONS = {
|
|
19
|
+
argumentPlaceholder: XYZAB,
|
|
20
|
+
};
|
|
21
|
+
export function compileFeature(fileName, src, opts = {}) {
|
|
9
22
|
const feature = new Feature(basename(base(fileName)));
|
|
10
23
|
try {
|
|
11
24
|
feature.root = parse(src);
|
|
@@ -24,9 +37,14 @@ export function compileFeature(fileName, src) {
|
|
|
24
37
|
autoLabel(feature.root);
|
|
25
38
|
const testFn = testFileName(fileName);
|
|
26
39
|
const testFile = new OutFile(testFn, fileName);
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
const cg = new VitestGenerator(testFile, fileName, {
|
|
41
|
+
...DEFAULT_COMPILER_OPTIONS,
|
|
42
|
+
...opts,
|
|
43
|
+
});
|
|
30
44
|
feature.toCode(cg);
|
|
31
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
outFile: testFile,
|
|
47
|
+
phraseMethods: cg.phraseMethods,
|
|
48
|
+
featureClassName: cg.featureClassName,
|
|
49
|
+
};
|
|
32
50
|
}
|
package/compiler/compiler.d.ts
CHANGED
|
@@ -3,8 +3,6 @@ export declare function compileFiles(pattern: string | string[]): Promise<{
|
|
|
3
3
|
outFns: string[];
|
|
4
4
|
}>;
|
|
5
5
|
export declare function compileFile(fn: string): Promise<{
|
|
6
|
-
phrasesFileAction: string;
|
|
7
6
|
outFile: import("../code_generator/outFile.ts").OutFile;
|
|
8
|
-
phrasesFile: import("../code_generator/outFile.ts").OutFile;
|
|
9
7
|
} | undefined>;
|
|
10
8
|
export declare function preprocess(src: string): string;
|
package/compiler/compiler.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import glob from 'fast-glob';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { resolve } from 'path';
|
|
4
|
-
import { VitestGenerator } from "../code_generator/VitestGenerator.js";
|
|
5
|
-
import { testFileName } from "../filenames/filenames.js";
|
|
6
4
|
import { compileFeature } from "./compile.js";
|
|
7
5
|
export async function compileFiles(pattern) {
|
|
8
6
|
var _a;
|
|
@@ -17,29 +15,17 @@ export async function compileFiles(pattern) {
|
|
|
17
15
|
}
|
|
18
16
|
console.log(`Compiled ${compiled.length} file${compiled.length === 1 ? '' : 's'}.`);
|
|
19
17
|
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
18
|
return { fns, outFns: features.map((f) => f.outFile.name) };
|
|
25
19
|
}
|
|
26
20
|
export async function compileFile(fn) {
|
|
27
|
-
var _a;
|
|
28
21
|
fn = resolve(fn);
|
|
29
22
|
const src = preprocess(readFileSync(fn, 'utf8').toString());
|
|
30
23
|
try {
|
|
31
|
-
const { outFile
|
|
24
|
+
const { outFile } = compileFeature(fn, src);
|
|
32
25
|
writeFileSync(outFile.name, outFile.value);
|
|
33
|
-
|
|
34
|
-
if (!existsSync(phrasesFile.name)) {
|
|
35
|
-
phrasesFileAction = 'generated';
|
|
36
|
-
writeFileSync(phrasesFile.name, phrasesFile.value);
|
|
37
|
-
}
|
|
38
|
-
return { phrasesFileAction, outFile, phrasesFile };
|
|
26
|
+
return { outFile };
|
|
39
27
|
}
|
|
40
28
|
catch (e) {
|
|
41
|
-
const outFileName = testFileName(fn);
|
|
42
|
-
writeFileSync(outFileName, VitestGenerator.error((_a = e.message) !== null && _a !== void 0 ? _a : `${e}`, e.stack));
|
|
43
29
|
return undefined;
|
|
44
30
|
}
|
|
45
31
|
}
|
package/model/model.d.ts
CHANGED
|
@@ -14,6 +14,14 @@ export interface CodeGenerator {
|
|
|
14
14
|
codeLiteral(src: string): string;
|
|
15
15
|
stringParamDeclaration(index: number): string;
|
|
16
16
|
variantParamDeclaration(index: number): string;
|
|
17
|
+
phraseMethods: PhraseMethod[];
|
|
18
|
+
}
|
|
19
|
+
export interface PhraseMethod {
|
|
20
|
+
name: string;
|
|
21
|
+
parameters: {
|
|
22
|
+
name: string;
|
|
23
|
+
type: string;
|
|
24
|
+
}[];
|
|
17
25
|
}
|
|
18
26
|
export interface Location {
|
|
19
27
|
line: number;
|
package/package.json
CHANGED
|
@@ -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/vitest/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
-
|
|
2
|
+
import { CompilerOptions } from '../compiler/compile.ts';
|
|
3
|
+
export interface HarmonyPluginOptions extends Partial<CompilerOptions> {
|
|
4
|
+
autoEditPhrases?: boolean;
|
|
3
5
|
}
|
|
4
|
-
export default function harmonyPlugin(
|
|
6
|
+
export default function harmonyPlugin(opts?: HarmonyPluginOptions): Plugin;
|
|
5
7
|
declare module 'vitest' {
|
|
6
8
|
interface TaskMeta {
|
|
7
9
|
phrases?: string[];
|
package/vitest/index.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
1
2
|
import c from 'tinyrainbow';
|
|
2
3
|
import { compileFeature } from "../compiler/compile.js";
|
|
3
4
|
import { preprocess } from "../compiler/compiler.js";
|
|
4
|
-
|
|
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 };
|
|
5
11
|
return {
|
|
6
12
|
name: 'harmony',
|
|
7
13
|
resolveId(id) {
|
|
@@ -9,11 +15,14 @@ export default function harmonyPlugin({} = {}) {
|
|
|
9
15
|
return id;
|
|
10
16
|
}
|
|
11
17
|
},
|
|
12
|
-
transform(code, id
|
|
18
|
+
transform(code, id) {
|
|
13
19
|
if (!id.endsWith('.harmony'))
|
|
14
20
|
return null;
|
|
15
21
|
code = preprocess(code);
|
|
16
|
-
const { outFile } = compileFeature(id, code);
|
|
22
|
+
const { outFile, phraseMethods, featureClassName } = compileFeature(id, code, opts);
|
|
23
|
+
if (options.autoEditPhrases) {
|
|
24
|
+
void updatePhrasesFile(id, phraseMethods, featureClassName);
|
|
25
|
+
}
|
|
17
26
|
return {
|
|
18
27
|
code: outFile.valueWithoutSourceMap,
|
|
19
28
|
map: outFile.sourceMap,
|
|
@@ -29,16 +38,6 @@ export default function harmonyPlugin({} = {}) {
|
|
|
29
38
|
}
|
|
30
39
|
config.test.reporters.splice(0, 0, new HarmonyReporter());
|
|
31
40
|
},
|
|
32
|
-
// This has been removed in favor of using transform, so no need to generate an actual file
|
|
33
|
-
// async configureServer(server) {
|
|
34
|
-
// const isWatchMode = server.config.server.watch !== null
|
|
35
|
-
// const patterns = [`${watchDir}/**/*.harmony`]
|
|
36
|
-
// if (isWatchMode) {
|
|
37
|
-
// await watchFiles(patterns)
|
|
38
|
-
// } else {
|
|
39
|
-
// await compileFiles(patterns)
|
|
40
|
-
// }
|
|
41
|
-
// },
|
|
42
41
|
};
|
|
43
42
|
}
|
|
44
43
|
class HarmonyReporter {
|
|
@@ -82,3 +81,21 @@ function addPhrases(task, depth = 2) {
|
|
|
82
81
|
delete task.meta.phrases; // to make sure not to add them again
|
|
83
82
|
}
|
|
84
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
|
+
}
|