harmonyc 0.6.0-9 → 0.7.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/LICENSE +21 -0
- package/README.md +62 -29
- package/{cli.js → cli/cli.js} +3 -3
- package/cli/watch.js +35 -0
- package/code_generator/JavaScript.js +169 -0
- package/compiler/compile.js +32 -0
- package/{compiler.js → compiler/compiler.js} +14 -3
- package/filenames/filenames.js +3 -3
- package/{Router.js → model/Router.js} +1 -1
- package/model/model.js +364 -0
- package/optimizations/autoLabel/autoLabel.js +15 -0
- package/package.json +4 -4
- package/parser/lexer.js +4 -0
- package/parser/lexer_rules.js +62 -0
- package/parser/parser.js +78 -0
- package/vitest/index.js +9 -0
- package/compile.js +0 -14
- package/config.js +0 -1
- package/frameworks/Gherkin.js +0 -37
- package/frameworks/NodeTest.js +0 -35
- package/js_api/js_api.js +0 -83
- package/languages/Gherkin.js +0 -33
- package/languages/JavaScript.js +0 -87
- package/model.js +0 -218
- package/syntax.js +0 -102
- package/watch.js +0 -11
- /package/{run.js → cli/run.js} +0 -0
- /package/{outFile.js → code_generator/outFile.js} +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2025 Bernát Kalló
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,57 +1,90 @@
|
|
|
1
1
|
# Harmony Code
|
|
2
2
|
|
|
3
|
-
A test design & BDD tool that helps you separate the _what_ to test from the _how_ to automate it. You write test cases in a simple
|
|
3
|
+
A test design & BDD tool that helps you separate the _what_ to test from the _how_ to automate it. You write test cases in a simple easy-to-read format, and then automate them with Vitest (and soon with other frameworks and languages).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Setup
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
You need to have Node.js installed. Then you can install Harmony Code in your project folder by:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
npx harmonyc --run --watch 'src/**/*.harmony.md'
|
|
13
|
-
```
|
|
9
|
+
```bash
|
|
10
|
+
npm install harmonyc
|
|
11
|
+
```
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
- => this will create a stub `*.steps.ts` file if it doesn't exist
|
|
13
|
+
Then add it to your `vitest.config.js` or `vite.config.js` file, and specify which folder to watch for `.harmony` files:
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
```js
|
|
16
|
+
import harmony from 'harmonyc/vitest'
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
export default {
|
|
19
|
+
plugins: [harmony({ watchDir: 'src' })],
|
|
20
|
+
}
|
|
21
|
+
```
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
# Products API
|
|
23
|
+
You can run it manually for all `.harmony` files in your `src` folder:
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- create product => error: unauthorized
|
|
28
|
-
- **Admin**:
|
|
29
|
-
1. authenticate admin
|
|
30
|
-
2. create product => product created
|
|
31
|
-
3. **Delete:**
|
|
32
|
-
- delete product => product deleted
|
|
25
|
+
```bash
|
|
26
|
+
harmonyc src/**/*.harmony
|
|
33
27
|
```
|
|
34
28
|
|
|
35
|
-
|
|
29
|
+
This will generate `.test.mjs` files next to the `.harmony` files, and generate empty definition files for you.
|
|
30
|
+
|
|
31
|
+
## Syntax
|
|
32
|
+
|
|
33
|
+
A `.harmony` file is a text file with a syntax that looks like this:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
+ Products API:
|
|
37
|
+
+ Create:
|
|
38
|
+
+ Anonymous:
|
|
39
|
+
- create product => !! "unauthorized"
|
|
40
|
+
+ Admin:
|
|
41
|
+
- authenticate with "admin" => product count `0`
|
|
42
|
+
- create product
|
|
43
|
+
=> product created
|
|
44
|
+
=> product count `1`
|
|
45
|
+
- Delete:
|
|
46
|
+
- delete product => product deleted => product count `0`
|
|
47
|
+
```
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
### Indentation
|
|
38
50
|
|
|
39
|
-
The
|
|
51
|
+
The lines of a file are nodes of a tree. The tree is specified with the indentation of the lines, which is n times 2 spaces and a `+` or `-` with one more space. The `+` or `-` sign is considered to be part of the indentation.
|
|
40
52
|
|
|
41
53
|
### Sequences and forks
|
|
42
54
|
|
|
43
|
-
|
|
55
|
+
`-` means a sequence: the node follows the previous sibling node and its descendants.
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
`+` means a fork: the node directly follows its parent node. All siblings with `+` are separate branches, they will generate separate scenarios.
|
|
58
|
+
|
|
59
|
+
### Actions and responses (phrases)
|
|
60
|
+
|
|
61
|
+
After the mark, every node can contain an action and zero or more responses. The action is the text before the `=>`, and the responses are the text after the `=>`.
|
|
62
|
+
|
|
63
|
+
Both actions and responses get compiled to simple function calls - in JavaScript, awaited function calls. The return value of the action is passed to the responses of the same step as the last argument.
|
|
64
|
+
|
|
65
|
+
### Arguments
|
|
66
|
+
|
|
67
|
+
Phrases can have arguments which are passed to the implementation function. There are two types of arguments: double-quoted strings are passed to the code as strings, and backtick-quoted strings are passed as is. You can use backticks to pass numbers, booleans, null, or objects.
|
|
46
68
|
|
|
47
69
|
### Labels
|
|
48
70
|
|
|
49
71
|
Label are nodes that end with `:`. You can use them to structure your test design.
|
|
50
72
|
They are not included in the test case, but the test case name is generated from the labels.
|
|
51
73
|
|
|
52
|
-
###
|
|
74
|
+
### Comments
|
|
75
|
+
|
|
76
|
+
Lines starting with `#` or `//` are comments and are ignored.
|
|
77
|
+
|
|
78
|
+
### Steps
|
|
79
|
+
|
|
80
|
+
All other lines are steps. A step consists of an action, and one or more responses denoted by `=>`.
|
|
81
|
+
Actions will become `When` steps, and responses will become `Then` steps.
|
|
82
|
+
|
|
83
|
+
### Error matching
|
|
84
|
+
|
|
85
|
+
You can use `!!` to denote an error response. This will verify that the action throws an error. You can specify the error message after the `!!`.
|
|
53
86
|
|
|
54
|
-
|
|
87
|
+
## Running the tests
|
|
55
88
|
|
|
56
89
|
## License
|
|
57
90
|
|
package/{cli.js → cli/cli.js}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { compileFiles } from
|
|
2
|
+
import { compileFiles } from "../compiler/compiler.js";
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
|
-
import { watchFiles } from
|
|
5
|
-
import { run, runWatch } from
|
|
4
|
+
import { watchFiles } from "./watch.js";
|
|
5
|
+
import { run, runWatch } from "./run.js";
|
|
6
6
|
const args = parseArgs({
|
|
7
7
|
options: {
|
|
8
8
|
help: { type: 'boolean' },
|
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,169 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
import { Arg, Word, } from "../model/model.js";
|
|
3
|
+
export class NodeTest {
|
|
4
|
+
constructor(tf, sf) {
|
|
5
|
+
this.tf = tf;
|
|
6
|
+
this.sf = sf;
|
|
7
|
+
this.framework = 'vitest';
|
|
8
|
+
this.phraseFns = new Map();
|
|
9
|
+
this.currentFeatureName = '';
|
|
10
|
+
this.resultCount = 0;
|
|
11
|
+
this.extraArgs = [];
|
|
12
|
+
}
|
|
13
|
+
feature(feature) {
|
|
14
|
+
const stepsModule = './' + basename(this.sf.name.replace(/.(js|ts)$/, ''));
|
|
15
|
+
const fn = (this.currentFeatureName = pascalCase(feature.name));
|
|
16
|
+
this.phraseFns = new Map();
|
|
17
|
+
if (this.framework === 'vitest') {
|
|
18
|
+
this.tf.print(`import { describe, test, expect } from 'vitest';`);
|
|
19
|
+
}
|
|
20
|
+
this.tf.print(`import ${fn}Steps from ${str(stepsModule)};`);
|
|
21
|
+
this.tf.print(``);
|
|
22
|
+
for (const item of feature.testGroups) {
|
|
23
|
+
item.toCode(this);
|
|
24
|
+
}
|
|
25
|
+
this.sf.print(`export default class ${pascalCase(feature.name)}Steps {`);
|
|
26
|
+
this.sf.indent(() => {
|
|
27
|
+
for (const ph of this.phraseFns.keys()) {
|
|
28
|
+
const p = this.phraseFns.get(ph);
|
|
29
|
+
const params = p.args.map((a, i) => a.toDeclaration(this, i)).join(', ');
|
|
30
|
+
this.sf.print(`async ${ph}(${params}) {`);
|
|
31
|
+
this.sf.indent(() => {
|
|
32
|
+
this.sf.print(`throw new Error(${str(`Pending: ${ph}`)});`);
|
|
33
|
+
});
|
|
34
|
+
this.sf.print(`}`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
this.sf.print(`};`);
|
|
38
|
+
}
|
|
39
|
+
testGroup(g) {
|
|
40
|
+
this.tf.print(`describe(${str(g.name)}, () => {`);
|
|
41
|
+
this.tf.indent(() => {
|
|
42
|
+
for (const item of g.items) {
|
|
43
|
+
item.toCode(this);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
this.tf.print('});');
|
|
47
|
+
}
|
|
48
|
+
test(t) {
|
|
49
|
+
this.resultCount = 0;
|
|
50
|
+
this.featureVars = new Map();
|
|
51
|
+
// avoid shadowing this import name
|
|
52
|
+
this.featureVars.set(new Object(), this.currentFeatureName);
|
|
53
|
+
this.tf.print(`test(${str(t.name)}, async () => {`);
|
|
54
|
+
this.tf.indent(() => {
|
|
55
|
+
for (const step of t.steps) {
|
|
56
|
+
step.toCode(this);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
this.tf.print('});');
|
|
60
|
+
}
|
|
61
|
+
errorStep(action, errorMessage) {
|
|
62
|
+
var _a;
|
|
63
|
+
this.tf.print(`expect(async () => {`);
|
|
64
|
+
this.tf.indent(() => {
|
|
65
|
+
action.toCode(this);
|
|
66
|
+
});
|
|
67
|
+
this.tf.print(`}).rejects.toThrow(${(_a = errorMessage === null || errorMessage === void 0 ? void 0 : errorMessage.toCode(this)) !== null && _a !== void 0 ? _a : ''});`);
|
|
68
|
+
}
|
|
69
|
+
step(action, responses) {
|
|
70
|
+
for (const p of [action, ...responses]) {
|
|
71
|
+
const feature = p.feature.name;
|
|
72
|
+
let f = this.featureVars.get(feature);
|
|
73
|
+
if (!f) {
|
|
74
|
+
f = toId(feature, abbrev, this.featureVars);
|
|
75
|
+
this.tf.print(`const ${f} = new ${pascalCase(feature)}Steps();`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (responses.length === 0) {
|
|
79
|
+
action.toCode(this);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const res = `r${this.resultCount++ || ''}`;
|
|
83
|
+
this.tf.print(`const ${res} =`);
|
|
84
|
+
this.tf.indent(() => {
|
|
85
|
+
action.toCode(this);
|
|
86
|
+
try {
|
|
87
|
+
this.extraArgs = [res];
|
|
88
|
+
for (const response of responses) {
|
|
89
|
+
response.toCode(this);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
this.extraArgs = [];
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
phrase(p) {
|
|
98
|
+
const phrasefn = this.functionName(p);
|
|
99
|
+
if (!this.phraseFns.has(phrasefn))
|
|
100
|
+
this.phraseFns.set(phrasefn, p);
|
|
101
|
+
const f = this.featureVars.get(p.feature.name);
|
|
102
|
+
const args = p.args.map((a) => a.toCode(this));
|
|
103
|
+
args.push(...this.extraArgs);
|
|
104
|
+
this.tf.print(`await ${f}.${this.functionName(p)}(${args.join(', ')});`);
|
|
105
|
+
}
|
|
106
|
+
stringLiteral(text) {
|
|
107
|
+
return str(text);
|
|
108
|
+
}
|
|
109
|
+
codeLiteral(src) {
|
|
110
|
+
return src;
|
|
111
|
+
}
|
|
112
|
+
paramName(index) {
|
|
113
|
+
return 'xyz'.charAt(index) || `a${index + 1}`;
|
|
114
|
+
}
|
|
115
|
+
stringParamDeclaration(index) {
|
|
116
|
+
return `${this.paramName(index)}: string`;
|
|
117
|
+
}
|
|
118
|
+
variantParamDeclaration(index) {
|
|
119
|
+
return `${this.paramName(index)}: any`;
|
|
120
|
+
}
|
|
121
|
+
functionName(phrase) {
|
|
122
|
+
const { kind } = phrase;
|
|
123
|
+
return ((kind === 'response' ? 'Then_' : 'When_') +
|
|
124
|
+
([...phrase.content, phrase.docstring ? [phrase.docstring] : []]
|
|
125
|
+
.map((c) => c instanceof Word ? underscore(c.text) : c instanceof Arg ? '_' : '')
|
|
126
|
+
.filter((x) => x)
|
|
127
|
+
.join('_') || '_'));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function str(s) {
|
|
131
|
+
if (s.includes('\n'))
|
|
132
|
+
return '\n' + templateStr(s);
|
|
133
|
+
let r = JSON.stringify(s);
|
|
134
|
+
return r;
|
|
135
|
+
}
|
|
136
|
+
function templateStr(s) {
|
|
137
|
+
return '`' + s.replace(/([`$\\])/g, '\\$1') + '`';
|
|
138
|
+
}
|
|
139
|
+
function capitalize(s) {
|
|
140
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
141
|
+
}
|
|
142
|
+
function toId(s, transform, previous) {
|
|
143
|
+
if (previous.has(s))
|
|
144
|
+
return previous.get(s);
|
|
145
|
+
let base = transform(s);
|
|
146
|
+
let id = base;
|
|
147
|
+
if ([...previous.values()].includes(id)) {
|
|
148
|
+
let i = 1;
|
|
149
|
+
while ([...previous.values()].includes(id + i))
|
|
150
|
+
i++;
|
|
151
|
+
id = base + i;
|
|
152
|
+
}
|
|
153
|
+
previous.set(s, id);
|
|
154
|
+
return id;
|
|
155
|
+
}
|
|
156
|
+
function words(s) {
|
|
157
|
+
return s.split(/[^0-9\p{L}]+/gu);
|
|
158
|
+
}
|
|
159
|
+
function pascalCase(s) {
|
|
160
|
+
return words(s).map(capitalize).join('');
|
|
161
|
+
}
|
|
162
|
+
function underscore(s) {
|
|
163
|
+
return words(s).join('_');
|
|
164
|
+
}
|
|
165
|
+
function abbrev(s) {
|
|
166
|
+
return words(s)
|
|
167
|
+
.map((x) => x.charAt(0).toUpperCase())
|
|
168
|
+
.join('');
|
|
169
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NodeTest } from "../code_generator/JavaScript.js";
|
|
2
|
+
import { OutFile } from "../code_generator/outFile.js";
|
|
3
|
+
import { parse } from "../parser/parser.js";
|
|
4
|
+
import { base, stepsFileName, testFileName } from "../filenames/filenames.js";
|
|
5
|
+
import { Feature } from "../model/model.js";
|
|
6
|
+
import { basename } from 'node:path';
|
|
7
|
+
import { autoLabel } from "../optimizations/autoLabel/autoLabel.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}\n${e.errorMessage}`;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
e.stack = `Error in ${fileName}\n${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);
|
|
27
|
+
const stepsFn = stepsFileName(fileName);
|
|
28
|
+
const stepsFile = new OutFile(stepsFn);
|
|
29
|
+
const cg = new NodeTest(testFile, stepsFile);
|
|
30
|
+
feature.toCode(cg);
|
|
31
|
+
return { outFile: testFile, stepsFile };
|
|
32
|
+
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import glob from 'fast-glob';
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
-
import {
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { compileFeature } from "./compile.js";
|
|
4
5
|
export async function compileFiles(pattern) {
|
|
6
|
+
var _a;
|
|
5
7
|
const fns = await glob(pattern);
|
|
6
8
|
if (!fns.length)
|
|
7
9
|
throw new Error(`No files found for pattern: ${String(pattern)}`);
|
|
8
|
-
const
|
|
10
|
+
const results = await Promise.allSettled(fns.map((fn) => compileFile(fn)));
|
|
11
|
+
const features = results.flatMap((r) => r.status === 'fulfilled' ? [r.value] : []);
|
|
12
|
+
const errors = results.flatMap((r) => r.status === 'rejected' ? [r.reason] : []);
|
|
13
|
+
for (const error of errors) {
|
|
14
|
+
console.log((_a = error.message) !== null && _a !== void 0 ? _a : error);
|
|
15
|
+
}
|
|
9
16
|
console.log(`Compiled ${fns.length} file${fns.length === 1 ? '' : 's'}.`);
|
|
10
17
|
const generated = features.filter((f) => f.stepsFileAction === 'generated');
|
|
11
18
|
if (generated.length) {
|
|
@@ -14,7 +21,11 @@ export async function compileFiles(pattern) {
|
|
|
14
21
|
return { fns, outFns: features.map((f) => f.outFile.name) };
|
|
15
22
|
}
|
|
16
23
|
export async function compileFile(fn) {
|
|
17
|
-
|
|
24
|
+
fn = resolve(fn);
|
|
25
|
+
const src = readFileSync(fn, 'utf8')
|
|
26
|
+
.toString()
|
|
27
|
+
.replace(/\r\n/g, '\n')
|
|
28
|
+
.replace(/\r/g, '\n');
|
|
18
29
|
const { outFile, stepsFile } = compileFeature(fn, src);
|
|
19
30
|
writeFileSync(outFile.name, outFile.value);
|
|
20
31
|
let stepsFileAction = 'ignored';
|
package/filenames/filenames.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import glob from 'fast-glob';
|
|
2
2
|
const { globSync, convertPathToPattern } = glob;
|
|
3
3
|
export function base(fn) {
|
|
4
|
-
return fn.replace(/\.harmony
|
|
4
|
+
return fn.replace(/\.harmony(\.\w+)?$/i, '');
|
|
5
5
|
}
|
|
6
6
|
export function testFileName(fn) {
|
|
7
7
|
return base(fn) + '.test.mjs';
|
|
@@ -9,9 +9,9 @@ export function testFileName(fn) {
|
|
|
9
9
|
export function stepsFileName(fn) {
|
|
10
10
|
const baseFn = base(fn);
|
|
11
11
|
const pattern = convertPathToPattern(baseFn);
|
|
12
|
-
const existing = globSync(`${pattern}.steps
|
|
12
|
+
const existing = globSync(`${pattern}.steps.{tsx,jsx,ts,js}`);
|
|
13
13
|
if (existing.length) {
|
|
14
|
-
return existing.sort()
|
|
14
|
+
return existing.sort().at(-1);
|
|
15
15
|
}
|
|
16
16
|
return `${baseFn}.steps.ts`;
|
|
17
17
|
}
|