as-test 0.5.2 → 0.5.4
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/CHANGELOG.md +54 -0
- package/README.md +98 -8
- package/as-test.config.schema.json +49 -0
- package/assembly/coverage.ts +20 -0
- package/assembly/src/expectation.ts +32 -9
- package/assembly/util/json.ts +2 -3
- package/bin/commands/build-core.js +268 -0
- package/bin/commands/build.js +16 -0
- package/bin/commands/doctor-core.js +335 -0
- package/bin/commands/doctor.js +5 -0
- package/bin/commands/init-core.js +991 -0
- package/bin/commands/init.js +6 -0
- package/bin/{run.js → commands/run-core.js} +147 -29
- package/bin/commands/run.js +20 -0
- package/bin/commands/test.js +23 -0
- package/bin/commands/types.js +1 -0
- package/bin/index.js +848 -59
- package/bin/reporters/default.js +26 -9
- package/bin/types.js +2 -0
- package/bin/util.js +594 -10
- package/package.json +42 -42
- package/transform/lib/coverage.js +130 -124
- package/transform/lib/index.js +58 -23
- package/bin/build.js +0 -136
- package/bin/init.js +0 -496
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 2026-03-04
|
|
4
|
+
|
|
5
|
+
### CLI & Config Validation
|
|
6
|
+
|
|
7
|
+
- feat: validate config shape and field types before applying defaults for all CLI entry points.
|
|
8
|
+
- feat: reject unknown config keys with nearest-key suggestions.
|
|
9
|
+
- feat: show structured validation diagnostics with JSON paths and fix hints.
|
|
10
|
+
- feat: fail fast on invalid config JSON with parser error details.
|
|
11
|
+
|
|
12
|
+
### Docs
|
|
13
|
+
|
|
14
|
+
- docs: add README guidance for strict config validation behavior and example error output.
|
|
15
|
+
|
|
16
|
+
## 2026-02-25 - v0.5.3
|
|
17
|
+
|
|
18
|
+
### CLI, Modes & Matrix
|
|
19
|
+
|
|
20
|
+
- feat: support mode fan-out behavior consistently across `ast build`, `ast run`, and `ast test`.
|
|
21
|
+
- feat: when no `--mode` is provided and modes are configured, run using configured modes.
|
|
22
|
+
- feat: add matrix-style per-file output with mode-aware timing:
|
|
23
|
+
- non-verbose: average time
|
|
24
|
+
- verbose: per-mode times.
|
|
25
|
+
- feat: add real-time matrix line updates in the default reporter and normalize timing precision to one decimal.
|
|
26
|
+
- feat: support comma-separated bare selectors (for example `ast test box,custom,generics,string`) across build/run/test selectors.
|
|
27
|
+
|
|
28
|
+
### Config Merge & Env Behavior
|
|
29
|
+
|
|
30
|
+
- fix: apply mode config as field-level merge over base config instead of replacing entire sections.
|
|
31
|
+
- fix: merge `buildOptions.args` between base config and mode config.
|
|
32
|
+
- fix: pass config env variables to both build and run processes for mode execution.
|
|
33
|
+
|
|
34
|
+
### Build Pipeline & Feature Flags
|
|
35
|
+
|
|
36
|
+
- feat: include the exact build command in build failure output.
|
|
37
|
+
- feat: allow `buildOptions.cmd` to override default command generation while still appending user build args.
|
|
38
|
+
- feat: support CLI feature toggles:
|
|
39
|
+
- `--enable coverage` / `--disable coverage`
|
|
40
|
+
- `--enable try-as` / `--disable try-as`.
|
|
41
|
+
|
|
42
|
+
### Reporter & Summaries
|
|
43
|
+
|
|
44
|
+
- fix: move mode summary rendering into the default reporter (via run-complete event payload).
|
|
45
|
+
- feat: include `modeSummary` for single-mode runs.
|
|
46
|
+
- feat: include mode and snapshot totals in final summary output.
|
|
47
|
+
- fix: `--clean` output now behaves as non-TTY in default reporter:
|
|
48
|
+
- no in-place line editing,
|
|
49
|
+
- no suite expand/collapse logs,
|
|
50
|
+
- final per-file verdict lines only.
|
|
51
|
+
|
|
52
|
+
### Coverage & Transform
|
|
53
|
+
|
|
54
|
+
- fix: ignore AssemblyScript builtin/compiler helper calls during coverage instrumentation (including `isString`, `changetype<T>`, `idof<T>`, `sizeof<T>`).
|
|
55
|
+
- fix: mock transform now collects mocked import targets across sources so WASI mock imports are rewritten reliably (resolves import-shape runtime failures).
|
|
56
|
+
|
|
3
57
|
## 2026-02-24 - v0.5.2
|
|
4
58
|
|
|
5
59
|
### Runtime & Serialization
|
package/README.md
CHANGED
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
- [Installation](#installation)
|
|
10
10
|
- [Examples](#examples)
|
|
11
11
|
- [Writing Tests](#writing-tests)
|
|
12
|
+
- [Setup Diagnostics](#setup-diagnostics)
|
|
12
13
|
- [Mocking](#mocking)
|
|
13
14
|
- [Snapshots](#snapshots)
|
|
14
15
|
- [Coverage](#coverage)
|
|
15
16
|
- [Custom Reporters](#custom-reporters)
|
|
16
17
|
- [Assertions](#assertions)
|
|
18
|
+
- [CLI Style Guide](#cli-style-guide)
|
|
17
19
|
- [License](#license)
|
|
18
20
|
- [Contact](#contact)
|
|
19
21
|
|
|
@@ -40,6 +42,11 @@ The installation script will set everything up for you:
|
|
|
40
42
|
npx as-test init --dir ./path-to-install
|
|
41
43
|
```
|
|
42
44
|
|
|
45
|
+
To scaffold and install dependencies in one step:
|
|
46
|
+
```bash
|
|
47
|
+
npx as-test init --dir ./path-to-install --install
|
|
48
|
+
```
|
|
49
|
+
|
|
43
50
|
Alternatively, you can install it manually:
|
|
44
51
|
```bash
|
|
45
52
|
npm install as-test --save-dev
|
|
@@ -74,7 +81,7 @@ describe("math", () => {
|
|
|
74
81
|
});
|
|
75
82
|
```
|
|
76
83
|
|
|
77
|
-
###
|
|
84
|
+
### File selection (`ast run`, `ast build`, `ast test`)
|
|
78
85
|
|
|
79
86
|
No selectors:
|
|
80
87
|
|
|
@@ -105,6 +112,14 @@ Multiple selectors:
|
|
|
105
112
|
ast test sleep array ./assembly/__tests__/snapshot.spec.ts
|
|
106
113
|
```
|
|
107
114
|
|
|
115
|
+
Comma-separated bare suite names:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
ast test box,custom,generics,string
|
|
119
|
+
ast run box,custom,generics,string
|
|
120
|
+
ast build box,custom,generics,string
|
|
121
|
+
```
|
|
122
|
+
|
|
108
123
|
If nothing matches, `ast test` exits non-zero with:
|
|
109
124
|
|
|
110
125
|
```text
|
|
@@ -114,11 +129,57 @@ No test files matched: ...
|
|
|
114
129
|
### Useful flags
|
|
115
130
|
|
|
116
131
|
- `--config <path>`: use another config file
|
|
117
|
-
- `--mode <name[,name...]>`: run one or multiple named config modes
|
|
132
|
+
- `--mode <name[,name...]>`: run one or multiple named config modes (if omitted and `modes` is configured, as-test runs all configured modes)
|
|
118
133
|
- `--update-snapshots`: write snapshot updates
|
|
119
134
|
- `--no-snapshot`: disable snapshot assertions for the run
|
|
120
135
|
- `--show-coverage`: print uncovered coverage points
|
|
136
|
+
- `--enable <feature>`: enable as-test feature (`coverage`, `try-as`)
|
|
137
|
+
- `--disable <feature>`: disable as-test feature (`coverage`, `try-as`)
|
|
121
138
|
- `--verbose`: keep expanded suite/test lines and update running `....` statuses in place
|
|
139
|
+
- `--clean`: disable in-place TTY updates and print only final per-file verdict lines. Useful for CI/CD.
|
|
140
|
+
- `--list`: show resolved files, per-mode artifacts, and runtime command without executing
|
|
141
|
+
- `--list-modes`: show configured and selected modes without executing
|
|
142
|
+
- `--help` / `-h`: show command-specific help (`ast test --help`, `ast init --help`, etc.)
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
ast build --enable try-as
|
|
148
|
+
ast test --disable coverage
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Preview execution plan:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
ast test --list
|
|
155
|
+
ast test --list-modes
|
|
156
|
+
ast run sleep --list --mode wasi
|
|
157
|
+
ast build --list --mode wasi,bindings
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Setup Diagnostics
|
|
161
|
+
|
|
162
|
+
Use `ast doctor` to validate local setup before running tests.
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
ast doctor
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
You can also target specific modes and config files:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
ast doctor --config ./as-test.config.json --mode wasi,bindings
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`doctor` checks:
|
|
175
|
+
|
|
176
|
+
- config file loading and mode resolution
|
|
177
|
+
- required dependencies (for example `assemblyscript`, `@assemblyscript/wasi-shim` for WASI targets)
|
|
178
|
+
- runtime command parsing and executable availability
|
|
179
|
+
- runtime script path existence (for script-host runtimes)
|
|
180
|
+
- test spec file discovery from configured input patterns
|
|
181
|
+
|
|
182
|
+
If any `ERROR` checks are found, `ast doctor` exits non-zero.
|
|
122
183
|
|
|
123
184
|
## Mocking
|
|
124
185
|
|
|
@@ -230,13 +291,12 @@ Example:
|
|
|
230
291
|
{
|
|
231
292
|
"$schema": "./as-test.config.schema.json",
|
|
232
293
|
"input": ["./assembly/__tests__/*.spec.ts"],
|
|
233
|
-
"
|
|
234
|
-
"logs": "./.as-test/logs",
|
|
235
|
-
"coverageDir": "./.as-test/coverage",
|
|
236
|
-
"snapshotDir": "./.as-test/snapshots",
|
|
294
|
+
"output": "./.as-test/",
|
|
237
295
|
"config": "none",
|
|
238
296
|
"coverage": true,
|
|
297
|
+
"env": {},
|
|
239
298
|
"buildOptions": {
|
|
299
|
+
"cmd": "",
|
|
240
300
|
"args": [],
|
|
241
301
|
"target": "wasi"
|
|
242
302
|
},
|
|
@@ -253,15 +313,38 @@ Example:
|
|
|
253
313
|
Key fields:
|
|
254
314
|
|
|
255
315
|
- `input`: glob list of spec files
|
|
316
|
+
- `output`: output alias. Use a root string (`"./.as-test/"`) or object (`{ "build": "...", "logs": "...", "coverage": "...", "snapshots": "..." }`)
|
|
256
317
|
- `outDir`: compiled wasm output dir
|
|
257
318
|
- `logs`: log output dir or `"none"`
|
|
258
319
|
- `coverageDir`: coverage output dir or `"none"`
|
|
259
320
|
- `snapshotDir`: snapshot storage dir
|
|
321
|
+
- `outDir`, `logs`, `coverageDir`, and `snapshotDir` still work; when both are set, these explicit fields override `output`
|
|
322
|
+
- `env`: environment variables injected into build and runtime processes
|
|
323
|
+
- `buildOptions.cmd`: optional custom build command template; when set it replaces default build command and flags. Supports `<file>`, `<name>`, `<outFile>`, `<target>`, `<mode>`
|
|
260
324
|
- `buildOptions.target`: `wasi` or `bindings`
|
|
261
|
-
- `modes`: named overrides for target/args/runtime/env/artifact directories (selected via `--mode`)
|
|
325
|
+
- `modes`: named overrides for command/target/args/runtime/env/artifact directories (selected via `--mode`); `mode.env` overrides top-level `env`
|
|
262
326
|
- `runOptions.runtime.cmd`: runtime command, supports `<file>` and `<name>`; if its script path is missing, as-test falls back to the default runner for the selected target
|
|
263
327
|
- `runOptions.reporter`: reporter selection as a string or object
|
|
264
328
|
|
|
329
|
+
Validation behavior:
|
|
330
|
+
|
|
331
|
+
- Config parsing is strict for `ast build`, `ast run`, `ast test`, and `ast doctor`.
|
|
332
|
+
- Invalid JSON fails early with parser details (`line`/`column` when provided by Node).
|
|
333
|
+
- Unknown properties are rejected and include a nearest-key suggestion when possible.
|
|
334
|
+
- Invalid property types are reported with their JSON path and a short fix hint.
|
|
335
|
+
- On validation failure, the command exits non-zero and prints `run "ast doctor" to check your setup.`
|
|
336
|
+
|
|
337
|
+
Example validation error:
|
|
338
|
+
|
|
339
|
+
```text
|
|
340
|
+
invalid config at ./as-test.config.json
|
|
341
|
+
1. $.inpoot: unknown property
|
|
342
|
+
fix: use "input" if that was intended, otherwise remove this property
|
|
343
|
+
2. $.runOptions.runtime.cmd: must be a string
|
|
344
|
+
fix: set to a runtime command including "<file>"
|
|
345
|
+
run "ast doctor" to check your setup.
|
|
346
|
+
```
|
|
347
|
+
|
|
265
348
|
Example multi-runtime matrix:
|
|
266
349
|
|
|
267
350
|
```json
|
|
@@ -309,6 +392,13 @@ Run all modes:
|
|
|
309
392
|
ast test --mode wasi-simd,wasi-nosimd,bindings-node-simd
|
|
310
393
|
```
|
|
311
394
|
|
|
395
|
+
Summary totals:
|
|
396
|
+
|
|
397
|
+
- `Modes` in the default reporter is config-scoped (`total` is all configured modes)
|
|
398
|
+
- when selecting fewer modes with `--mode`, unselected modes are counted as `skipped`
|
|
399
|
+
- `Files` in the default reporter is also config-scoped (`total` is all files from configured input patterns)
|
|
400
|
+
- when selecting fewer files, unselected files are counted as `skipped`
|
|
401
|
+
|
|
312
402
|
When using `--mode`, compiled artifacts are emitted as:
|
|
313
403
|
|
|
314
404
|
```text
|
|
@@ -472,7 +562,7 @@ Available matchers:
|
|
|
472
562
|
- `toStartWith(prefix)`
|
|
473
563
|
- `toEndWith(suffix)`
|
|
474
564
|
- `toHaveLength(length)`
|
|
475
|
-
- `toContain(
|
|
565
|
+
- `toContain(itemOrSubstring)` (`toContains` alias supported)
|
|
476
566
|
- `toThrow()` (with `try-as`)
|
|
477
567
|
- `toMatchSnapshot(name?)`
|
|
478
568
|
|
|
@@ -17,6 +17,37 @@
|
|
|
17
17
|
},
|
|
18
18
|
"default": ["./assembly/__tests__/*.spec.ts"]
|
|
19
19
|
},
|
|
20
|
+
"output": {
|
|
21
|
+
"description": "Output alias. Use a root string or a per-artifact object. Legacy fields (outDir/logs/coverageDir/snapshotDir) still work and override this alias when both are set.",
|
|
22
|
+
"oneOf": [
|
|
23
|
+
{
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Root output directory. Expands to build/logs/coverage/snapshots subdirectories."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "object",
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"properties": {
|
|
31
|
+
"build": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Build artifact output directory (maps to outDir)."
|
|
34
|
+
},
|
|
35
|
+
"logs": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Log artifact output directory (maps to logs). Use \"none\" to disable."
|
|
38
|
+
},
|
|
39
|
+
"coverage": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Coverage artifact output directory (maps to coverageDir). Use \"none\" to disable."
|
|
42
|
+
},
|
|
43
|
+
"snapshots": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Snapshot output directory (maps to snapshotDir)."
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
20
51
|
"outDir": {
|
|
21
52
|
"type": "string",
|
|
22
53
|
"description": "Directory where compiled artifacts are written.",
|
|
@@ -80,10 +111,23 @@
|
|
|
80
111
|
],
|
|
81
112
|
"default": true
|
|
82
113
|
},
|
|
114
|
+
"env": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"description": "Environment variables injected when building/running.",
|
|
117
|
+
"additionalProperties": {
|
|
118
|
+
"type": "string"
|
|
119
|
+
},
|
|
120
|
+
"default": {}
|
|
121
|
+
},
|
|
83
122
|
"buildOptions": {
|
|
84
123
|
"type": "object",
|
|
85
124
|
"additionalProperties": false,
|
|
86
125
|
"properties": {
|
|
126
|
+
"cmd": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"description": "Custom build command template. When set, this replaces the default asc command/flags/output handling. Supports placeholders: <file>, <name>, <outFile>, <target>, <mode>.",
|
|
129
|
+
"default": ""
|
|
130
|
+
},
|
|
87
131
|
"args": {
|
|
88
132
|
"type": "array",
|
|
89
133
|
"items": {
|
|
@@ -98,6 +142,7 @@
|
|
|
98
142
|
}
|
|
99
143
|
},
|
|
100
144
|
"default": {
|
|
145
|
+
"cmd": "",
|
|
101
146
|
"args": [],
|
|
102
147
|
"target": "wasi"
|
|
103
148
|
}
|
|
@@ -153,6 +198,10 @@
|
|
|
153
198
|
"type": "object",
|
|
154
199
|
"additionalProperties": false,
|
|
155
200
|
"properties": {
|
|
201
|
+
"cmd": {
|
|
202
|
+
"type": "string",
|
|
203
|
+
"description": "Mode-specific custom build command template."
|
|
204
|
+
},
|
|
156
205
|
"args": {
|
|
157
206
|
"type": "array",
|
|
158
207
|
"items": {
|
package/assembly/coverage.ts
CHANGED
|
@@ -23,6 +23,26 @@ export function __REGISTER(point: CoverPoint): void {
|
|
|
23
23
|
Coverage.SN.hashes.set(point.hash, point);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function __REGISTER_RAW(
|
|
27
|
+
file: string,
|
|
28
|
+
hash: string,
|
|
29
|
+
line: i32,
|
|
30
|
+
column: i32,
|
|
31
|
+
type: string,
|
|
32
|
+
): void {
|
|
33
|
+
if (Coverage.SN.allIndex.has(hash)) return;
|
|
34
|
+
const point = new CoverPoint();
|
|
35
|
+
point.file = file;
|
|
36
|
+
point.hash = hash;
|
|
37
|
+
point.line = line;
|
|
38
|
+
point.column = column;
|
|
39
|
+
point.type = type;
|
|
40
|
+
Coverage.SN.points++;
|
|
41
|
+
Coverage.SN.allIndex.set(hash, Coverage.SN.all.length);
|
|
42
|
+
Coverage.SN.all.push(point);
|
|
43
|
+
Coverage.SN.hashes.set(hash, point);
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
export function __COVER(hash: string): void {
|
|
27
47
|
if (Coverage.SN.allIndex.has(hash)) {
|
|
28
48
|
const index = Coverage.SN.allIndex.get(hash);
|
|
@@ -334,18 +334,41 @@ export class Expectation<T> extends Tests {
|
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
/**
|
|
337
|
-
* Tests if an array contains
|
|
337
|
+
* Tests if an array or string contains a value
|
|
338
338
|
*/
|
|
339
339
|
// @ts-ignore
|
|
340
340
|
toContain(value: valueof<T>): void {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
341
|
+
if (isString<T>()) {
|
|
342
|
+
// @ts-ignore
|
|
343
|
+
const left = this._left as string;
|
|
344
|
+
// @ts-ignore
|
|
345
|
+
const needle = value as string;
|
|
346
|
+
const passed = left.indexOf(needle) >= 0;
|
|
347
|
+
this._resolve(passed, "toContain", q(left), q(needle));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (isArray<T>()) {
|
|
352
|
+
// @ts-ignore
|
|
353
|
+
const passed = this._left.includes(value);
|
|
354
|
+
this._resolve(
|
|
355
|
+
passed,
|
|
356
|
+
"toContain",
|
|
357
|
+
stringifyValue<T>(this._left),
|
|
358
|
+
stringifyValue<valueof<T>>(value),
|
|
359
|
+
);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
ERROR("toContain() can only be used on string and array types!");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Alias for toContain().
|
|
368
|
+
*/
|
|
369
|
+
// @ts-ignore
|
|
370
|
+
toContains(value: valueof<T>): void {
|
|
371
|
+
this.toContain(value);
|
|
349
372
|
}
|
|
350
373
|
|
|
351
374
|
/**
|
package/assembly/util/json.ts
CHANGED
|
@@ -18,17 +18,16 @@ export function stringifyValue<T>(value: T): string {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
if (isInteger<T>() || isFloat<T>()) {
|
|
21
|
-
// @ts-
|
|
21
|
+
// @ts-expect-error: type
|
|
22
22
|
return value.toString();
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
if (isString<T>()) {
|
|
26
|
-
// @ts-ignore
|
|
27
26
|
return quote(value as string);
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
if (isArray<T>()) {
|
|
31
|
-
// @ts-
|
|
30
|
+
// @ts-expect-error: type
|
|
32
31
|
return stringifyArray<valueof<T>>(value as valueof<T>[]);
|
|
33
32
|
}
|
|
34
33
|
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { glob } from "glob";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { applyMode, getPkgRunner, loadConfig, tokenizeCommand, } from "../util.js";
|
|
7
|
+
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
8
|
+
export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], modeName, featureToggles = {}) {
|
|
9
|
+
const loadedConfig = loadConfig(configPath, false);
|
|
10
|
+
const mode = applyMode(loadedConfig, modeName);
|
|
11
|
+
const config = mode.config;
|
|
12
|
+
if (!hasCustomBuildCommand(config)) {
|
|
13
|
+
ensureDeps(config);
|
|
14
|
+
}
|
|
15
|
+
const pkgRunner = getPkgRunner();
|
|
16
|
+
const inputPatterns = resolveInputPatterns(config.input, selectors);
|
|
17
|
+
const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
|
|
18
|
+
const duplicateSpecBasenames = await resolveDuplicateSpecBasenames(config.input);
|
|
19
|
+
const coverageEnabled = resolveCoverageEnabled(config.coverage, featureToggles.coverage);
|
|
20
|
+
const buildEnv = {
|
|
21
|
+
...mode.env,
|
|
22
|
+
AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
|
|
23
|
+
};
|
|
24
|
+
for (const file of inputFiles) {
|
|
25
|
+
const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
|
|
26
|
+
const invocation = getBuildCommand(config, pkgRunner, file, outFile, modeName, featureToggles);
|
|
27
|
+
try {
|
|
28
|
+
buildFile(invocation, buildEnv);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const modeLabel = modeName ?? "default";
|
|
32
|
+
throw new Error(`Failed to build ${path.basename(file)} in mode ${modeLabel} with ${getBuildStderr(error)}\nBuild command: ${formatInvocation(invocation)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function hasCustomBuildCommand(config) {
|
|
37
|
+
return !!config.buildOptions.cmd.trim().length;
|
|
38
|
+
}
|
|
39
|
+
function getBuildCommand(config, pkgRunner, file, outFile, modeName, featureToggles = {}) {
|
|
40
|
+
const userArgs = getUserBuildArgs(config);
|
|
41
|
+
if (hasCustomBuildCommand(config)) {
|
|
42
|
+
const tokens = tokenizeCommand(expandBuildCommand(config.buildOptions.cmd, file, outFile, config.buildOptions.target, modeName));
|
|
43
|
+
if (!tokens.length) {
|
|
44
|
+
throw new Error("custom build command is empty");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
command: tokens[0],
|
|
48
|
+
args: [...tokens.slice(1), ...userArgs],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const defaultArgs = getDefaultBuildArgs(config, featureToggles);
|
|
52
|
+
const args = ["asc", file, ...userArgs, ...defaultArgs];
|
|
53
|
+
if (config.outDir.length) {
|
|
54
|
+
args.push("-o", outFile);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
command: pkgRunner,
|
|
58
|
+
args,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function getUserBuildArgs(config) {
|
|
62
|
+
return config.buildOptions.args.filter((value) => value.length > 0);
|
|
63
|
+
}
|
|
64
|
+
function expandBuildCommand(template, file, outFile, target, modeName) {
|
|
65
|
+
const name = path
|
|
66
|
+
.basename(file)
|
|
67
|
+
.replace(/\.spec\.ts$/, "")
|
|
68
|
+
.replace(/\.ts$/, "");
|
|
69
|
+
return template
|
|
70
|
+
.replace(/<file>/g, file)
|
|
71
|
+
.replace(/<name>/g, name)
|
|
72
|
+
.replace(/<outFile>/g, outFile)
|
|
73
|
+
.replace(/<target>/g, target)
|
|
74
|
+
.replace(/<mode>/g, modeName ?? "");
|
|
75
|
+
}
|
|
76
|
+
function resolveArtifactFileName(file, target, modeName, duplicateSpecBasenames = new Set()) {
|
|
77
|
+
const base = path
|
|
78
|
+
.basename(file)
|
|
79
|
+
.replace(/\.spec\.ts$/, "")
|
|
80
|
+
.replace(/\.ts$/, "");
|
|
81
|
+
const legacy = !modeName
|
|
82
|
+
? `${path.basename(file).replace(".ts", ".wasm")}`
|
|
83
|
+
: `${base}.${modeName}.${target}.wasm`;
|
|
84
|
+
if (!duplicateSpecBasenames.has(path.basename(file))) {
|
|
85
|
+
return legacy;
|
|
86
|
+
}
|
|
87
|
+
const disambiguator = resolveDisambiguator(file);
|
|
88
|
+
if (!disambiguator.length) {
|
|
89
|
+
return legacy;
|
|
90
|
+
}
|
|
91
|
+
const ext = path.extname(legacy);
|
|
92
|
+
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
93
|
+
return `${stem}.${disambiguator}${ext}`;
|
|
94
|
+
}
|
|
95
|
+
async function resolveDuplicateSpecBasenames(configured) {
|
|
96
|
+
const patterns = Array.isArray(configured) ? configured : [configured];
|
|
97
|
+
const files = await glob(patterns);
|
|
98
|
+
const counts = new Map();
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const base = path.basename(file);
|
|
101
|
+
counts.set(base, (counts.get(base) ?? 0) + 1);
|
|
102
|
+
}
|
|
103
|
+
const duplicates = new Set();
|
|
104
|
+
for (const [base, count] of counts) {
|
|
105
|
+
if (count > 1)
|
|
106
|
+
duplicates.add(base);
|
|
107
|
+
}
|
|
108
|
+
return duplicates;
|
|
109
|
+
}
|
|
110
|
+
function resolveDisambiguator(file) {
|
|
111
|
+
const relDir = path.dirname(path.relative(process.cwd(), file));
|
|
112
|
+
if (!relDir.length || relDir == ".")
|
|
113
|
+
return "";
|
|
114
|
+
return relDir
|
|
115
|
+
.replace(/[\\/]+/g, "__")
|
|
116
|
+
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
117
|
+
.replace(/^_+|_+$/g, "");
|
|
118
|
+
}
|
|
119
|
+
function resolveInputPatterns(configured, selectors) {
|
|
120
|
+
const configuredInputs = Array.isArray(configured)
|
|
121
|
+
? configured
|
|
122
|
+
: [configured];
|
|
123
|
+
if (!selectors.length)
|
|
124
|
+
return configuredInputs;
|
|
125
|
+
const patterns = new Set();
|
|
126
|
+
for (const selector of expandSelectors(selectors)) {
|
|
127
|
+
if (!selector)
|
|
128
|
+
continue;
|
|
129
|
+
if (isBareSuiteSelector(selector)) {
|
|
130
|
+
const base = stripSuiteSuffix(selector);
|
|
131
|
+
for (const configuredInput of configuredInputs) {
|
|
132
|
+
patterns.add(path.join(path.dirname(configuredInput), `${base}.spec.ts`));
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
patterns.add(selector);
|
|
137
|
+
}
|
|
138
|
+
return [...patterns];
|
|
139
|
+
}
|
|
140
|
+
function expandSelectors(selectors) {
|
|
141
|
+
const expanded = [];
|
|
142
|
+
for (const selector of selectors) {
|
|
143
|
+
if (!selector)
|
|
144
|
+
continue;
|
|
145
|
+
if (!shouldSplitSelector(selector)) {
|
|
146
|
+
expanded.push(selector);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
for (const token of selector.split(",")) {
|
|
150
|
+
const trimmed = token.trim();
|
|
151
|
+
if (!trimmed.length)
|
|
152
|
+
continue;
|
|
153
|
+
expanded.push(trimmed);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return expanded;
|
|
157
|
+
}
|
|
158
|
+
function shouldSplitSelector(selector) {
|
|
159
|
+
return (selector.includes(",") &&
|
|
160
|
+
!selector.includes("/") &&
|
|
161
|
+
!selector.includes("\\") &&
|
|
162
|
+
!/[*?[\]{}]/.test(selector));
|
|
163
|
+
}
|
|
164
|
+
function isBareSuiteSelector(selector) {
|
|
165
|
+
return (!selector.includes("/") &&
|
|
166
|
+
!selector.includes("\\") &&
|
|
167
|
+
!/[*?[\]{}]/.test(selector));
|
|
168
|
+
}
|
|
169
|
+
function stripSuiteSuffix(selector) {
|
|
170
|
+
return selector.replace(/\.spec\.ts$/, "").replace(/\.ts$/, "");
|
|
171
|
+
}
|
|
172
|
+
function ensureDeps(config) {
|
|
173
|
+
if (config.buildOptions.target == "wasi") {
|
|
174
|
+
if (!existsSync("./node_modules/@assemblyscript/wasi-shim/asconfig.json")) {
|
|
175
|
+
console.log(`${chalk.bgRed(" ERROR ")}${chalk.dim(":")} could not find @assemblyscript/wasi-shim! Add it to your dependencies to run with WASI!`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function buildFile(invocation, env) {
|
|
181
|
+
const result = spawnSync(invocation.command, invocation.args, {
|
|
182
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
183
|
+
encoding: "utf8",
|
|
184
|
+
env,
|
|
185
|
+
shell: false,
|
|
186
|
+
});
|
|
187
|
+
if (result.error)
|
|
188
|
+
throw result.error;
|
|
189
|
+
if (result.status !== 0) {
|
|
190
|
+
const error = new Error(result.stderr?.trim() ||
|
|
191
|
+
result.stdout?.trim() ||
|
|
192
|
+
`command exited with code ${result.status}`);
|
|
193
|
+
error.stderr = result.stderr ?? "";
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function formatInvocation(invocation) {
|
|
198
|
+
return [invocation.command, ...invocation.args]
|
|
199
|
+
.map((token) => (/\s/.test(token) ? JSON.stringify(token) : token))
|
|
200
|
+
.join(" ");
|
|
201
|
+
}
|
|
202
|
+
function getBuildStderr(error) {
|
|
203
|
+
const err = error;
|
|
204
|
+
const stderr = err?.stderr;
|
|
205
|
+
if (typeof stderr == "string") {
|
|
206
|
+
const trimmed = stderr.trim();
|
|
207
|
+
if (trimmed.length)
|
|
208
|
+
return trimmed;
|
|
209
|
+
}
|
|
210
|
+
else if (stderr instanceof Buffer) {
|
|
211
|
+
const trimmed = stderr.toString("utf8").trim();
|
|
212
|
+
if (trimmed.length)
|
|
213
|
+
return trimmed;
|
|
214
|
+
}
|
|
215
|
+
const message = typeof err?.message == "string" ? err.message.trim() : "";
|
|
216
|
+
return message || "unknown error";
|
|
217
|
+
}
|
|
218
|
+
function getDefaultBuildArgs(config, featureToggles) {
|
|
219
|
+
const buildArgs = [];
|
|
220
|
+
const tryAsEnabled = resolveTryAsEnabled(featureToggles.tryAs);
|
|
221
|
+
buildArgs.push("--transform", "as-test/transform");
|
|
222
|
+
if (tryAsEnabled) {
|
|
223
|
+
buildArgs.push("--transform", "try-as/transform");
|
|
224
|
+
}
|
|
225
|
+
if (config.config && config.config !== "none") {
|
|
226
|
+
buildArgs.push("--config", config.config);
|
|
227
|
+
}
|
|
228
|
+
if (tryAsEnabled) {
|
|
229
|
+
buildArgs.push("--use", "AS_TEST_TRY_AS=1");
|
|
230
|
+
}
|
|
231
|
+
// Should also strip any bindings-enabling from asconfig
|
|
232
|
+
if (config.buildOptions.target == "bindings") {
|
|
233
|
+
buildArgs.push("--use", "AS_TEST_BINDINGS=1", "--bindings", "raw", "--exportRuntime", "--exportStart", "_start");
|
|
234
|
+
}
|
|
235
|
+
else if (config.buildOptions.target == "wasi") {
|
|
236
|
+
buildArgs.push("--use", "AS_TEST_WASI=1", "--config", "./node_modules/@assemblyscript/wasi-shim/asconfig.json");
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
console.log(`${chalk.bgRed(" ERROR ")}${chalk.dim(":")} could not determine target in config! Set target to 'bindings' or 'wasi'`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
return buildArgs;
|
|
243
|
+
}
|
|
244
|
+
function resolveTryAsEnabled(override) {
|
|
245
|
+
const installed = hasTryAsRuntime();
|
|
246
|
+
if (override === false)
|
|
247
|
+
return false;
|
|
248
|
+
if (override === true && !installed) {
|
|
249
|
+
throw new Error('try-as feature was enabled, but package "try-as" is not installed');
|
|
250
|
+
}
|
|
251
|
+
return installed;
|
|
252
|
+
}
|
|
253
|
+
function resolveCoverageEnabled(rawCoverage, override) {
|
|
254
|
+
if (override != undefined)
|
|
255
|
+
return override;
|
|
256
|
+
if (typeof rawCoverage == "boolean")
|
|
257
|
+
return rawCoverage;
|
|
258
|
+
if (rawCoverage && typeof rawCoverage == "object") {
|
|
259
|
+
const enabled = rawCoverage.enabled;
|
|
260
|
+
if (typeof enabled == "boolean")
|
|
261
|
+
return enabled;
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
function hasTryAsRuntime() {
|
|
266
|
+
return (existsSync(path.join(process.cwd(), "node_modules/try-as")) ||
|
|
267
|
+
existsSync(path.join(process.cwd(), "node_modules/try-as/package.json")));
|
|
268
|
+
}
|