as-test 1.1.3 → 1.1.5

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 CHANGED
@@ -1,19 +1,37 @@
1
1
  # Change Log
2
2
 
3
- ## 2026-05-13 - v1.1.2
3
+ ## 2026-05-15 - v1.1.5
4
+
5
+ - fix: coverage `mode` and `dependencies` filtering now correctly handles AssemblyScript-normalized `~lib/<pkg>/...` paths, which are the actual runtime paths emitted for `node_modules` imports.
6
+ - fix: `ENTRY_FILE` injected by the transform now uses the full relative path instead of the basename, preventing snapshot key collisions between specs with the same filename in different directories; snapshot lookup normalizes the file prefix to maintain backward compatibility with existing `.snap` files.
7
+ - fix: transform visitor and coverage instrumentation now resolve `NodeKind` values at runtime instead of relying on compile-time const enum inlining, so they remain correct across AssemblyScript versions.
8
+ - fix: add a no-op `TupleType` case to the transform visitor so files using tuple types no longer throw during instrumentation.
9
+
10
+ ## 2026-05-14 - v1.1.4
11
+
12
+ - feat: add `coverage.mode` (`project` or `all`) plus `coverage.dependencies` package allowlisting so dependency coverage can include normal or pnpm-installed packages without raw path globs.
13
+
14
+ ## 2026-05-13 - v1.1.3
15
+
16
+ ### Coverage
4
17
 
5
- - update build and run faliures to provide helpful error messages and reproduction commands and instructions
6
- - remove confirmation from ast clean
7
18
  - feat: make coverage gaps hierarchical and easier to scan, with parent-before-child grouping, tree-style connectors, collapsed nested gaps by default, and `--show-coverage=all` / `--verbose` expansion.
8
19
  - feat: add richer coverage point names including `DefaultValue`, `Ternary`, `IfBranch`, `Assignment`, `Loop`, `Return`, and `Throw` so uncovered points describe the actual construct instead of falling back to broad labels.
9
20
  - fix: make coverage snippets underline the emitted construct span instead of recomputing from the raw column, so cases like inline `if (...) ...` and assignments highlight the right text.
10
21
 
11
- ## 2026-05-12 -v1.1.1
22
+ ## 2026-05-13 - v1.1.2
23
+
24
+ ### Reporting & CLI
25
+
26
+ - fix: update build and run failures to provide clearer error messages plus reproduction commands and instructions.
27
+ - fix: remove the confirmation prompt from `ast clean`.
28
+
29
+ ## 2026-05-12 - v1.1.1
12
30
 
13
31
  - add `ast clean` command to remove build outputs, coverage outputs, crash reports, and logs.
14
32
  - remove deps
15
33
 
16
- ## 2026-05-12 -v1.1.0
34
+ ## 2026-05-12 - v1.1.0
17
35
 
18
36
  ### Upgrading to 1.1.0
19
37
 
@@ -81,7 +99,7 @@
81
99
  - feat: make integer `FuzzSeed` helpers default to the full range of their target type when no options are provided, instead of collapsing to `0`.
82
100
  - perf: add unchecked full-range fast paths for default integer seed generation while keeping explicit user-provided ranges validated.
83
101
 
84
- ## 2025-05-03 - v1.0.13
102
+ ## 2026-05-03 - v1.0.13
85
103
 
86
104
  - feat: add `--fuzzer` / `--fuzzers` filtering for `ast fuzz` and `ast test --fuzz`, accept `--suite` / `--suites` as fuzz aliases, and include target-specific repro commands in fuzz failure output.
87
105
  - feat: add `--suite` / `--suites` filtering for `ast run` and `ast test`, and print suite-specific repro commands on failing test assertions.
package/README.md CHANGED
@@ -16,12 +16,13 @@
16
16
  - [Why as-test](#why-as-test)
17
17
  - [Installation](#installation)
18
18
  - [Docs](#docs)
19
- - [Project Layout](#project-layout)
20
19
  - [Writing Tests](#writing-tests)
21
20
  - [Mocking](#mocking)
22
21
  - [Snapshots](#snapshots)
23
- - [Fuzzing](#fuzzing)
24
22
  - [Runtimes](#runtimes)
23
+ - [Modes](#modes)
24
+ - [Coverage](#coverage)
25
+ - [Fuzzing](#fuzzing)
25
26
  - [Examples](#examples)
26
27
  - [License](#license)
27
28
 
@@ -33,15 +34,6 @@ Most AssemblyScript testing tools are tied to a single runtime, usually Node.js.
33
34
  If you deploy to WASI, Wazero, or a custom runtime, you often end up mocking everything and maintaining parallel logic just for tests.
34
35
  as-test solves this by letting you run tests on your actual target runtime, while only mocking what’s necessary.
35
36
 
36
- Key benefits
37
-
38
- - Runtime-agnostic: test on WASI, bindings, or custom runners
39
- - Minimal mocking: keep real imports when possible
40
- - Production-like testing: catch runtime-specific issues early
41
- - Inline mocking and snapshots
42
- - Custom reporters and coverage
43
- - Integrated fuzzing support
44
-
45
37
  ## Installation
46
38
 
47
39
  The easiest way to start is with the project initializer:
@@ -64,50 +56,6 @@ Full documentation lives at:
64
56
 
65
57
  <https://docs.jairus.dev/as-test>
66
58
 
67
- ## Project Layout
68
-
69
- By default, `as-test` looks for:
70
-
71
- - tests in `assembly/__tests__`
72
- - fuzzers in `assembly/__fuzz__`
73
- - config in `as-test.config.json`
74
-
75
- Generated files go into `.as-test/`.
76
-
77
- Minimal `as-test.config.json`:
78
-
79
- ```json
80
- {
81
- "input": ["assembly/__tests__/*.spec.ts"],
82
- "output": ".as-test/",
83
- "buildOptions": {
84
- "target": "wasi"
85
- },
86
- "runOptions": {
87
- "runtime": {
88
- "cmd": "node .as-test/runners/default.wasi.js"
89
- }
90
- }
91
- }
92
- ```
93
-
94
- Coverage point filtering is configurable when you want to ignore known-noisy gaps:
95
-
96
- ```json
97
- {
98
- "coverage": {
99
- "enabled": true,
100
- "include": ["assembly/src/**/*.ts"],
101
- "ignore": {
102
- "labels": ["Call"],
103
- "names": ["panic", "serialize"],
104
- "locations": ["assembly/src/fuzz.ts:38:*"],
105
- "snippets": ["*message: string*"]
106
- }
107
- }
108
- }
109
- ```
110
-
111
59
  ## Writing Tests
112
60
 
113
61
  Tests usually live in `assembly/__tests__/*.spec.ts`.
@@ -119,7 +67,7 @@ import { describe, expect, test } from "as-test";
119
67
 
120
68
  describe("math", () => {
121
69
  test("adds numbers", () => {
122
- expect(1 + 2).toBe(3);
70
+ expect(1 + 2).toBe(3, "should add two numbers");
123
71
  });
124
72
  });
125
73
  ```
@@ -145,8 +93,8 @@ npx ast test math
145
93
  Re-run one suite inside a matching file:
146
94
 
147
95
  ```bash
148
- npx ast run math --suite array-check
149
- npx ast run math --suite array-manipulation/array-check
96
+ npx ast run math --suite math
97
+ npx ast run math --suite math/adds-numbers
150
98
  ```
151
99
 
152
100
  You do not need to learn every CLI flag to get started. Most projects can begin with `npx ast test`, then add more configuration only when they need it.
@@ -232,96 +180,6 @@ If an existing snapshot legitimately changed, overwrite it with:
232
180
  npx ast test --overwrite-snapshots
233
181
  ```
234
182
 
235
- ## Fuzzing
236
-
237
- Fuzzers usually live in `assembly/__fuzz__/*.fuzz.ts`.
238
-
239
- Example:
240
-
241
- ```ts
242
- import { expect, FuzzSeed, fuzz } from "as-test";
243
-
244
- fuzz("bounded integer addition", (left: i32, right: i32): bool => {
245
- const sum = left + right;
246
- expect(sum - right).toBe(left);
247
- return sum >= i32.MIN_VALUE;
248
- }).generate((seed: FuzzSeed, run: (left: i32, right: i32) => bool): void => {
249
- run(seed.i32({ min: -1000, max: 1000 }), seed.i32({ min: -1000, max: 1000 }));
250
- });
251
- ```
252
-
253
- Pass a third argument to override the operation count for one target without changing the global fuzz config:
254
-
255
- ```ts
256
- fuzz(
257
- "hot path stays stable",
258
- (): void => {
259
- expect(1 + 1).toBe(2);
260
- },
261
- 250,
262
- );
263
- ```
264
-
265
- Or pass it as the second argument to `.generate(...)`:
266
-
267
- ```ts
268
- fuzz(
269
- "ascii strings survive concatenation boundaries",
270
- (input: string): bool => {
271
- expect(input.length <= 40).toBe(true);
272
- return true;
273
- },
274
- ).generate((seed: FuzzSeed, run: (input: string) => bool): void => {
275
- run(seed.string({ charset: "ascii", min: 0, max: 40 }));
276
- }, 250);
277
- ```
278
-
279
- You can still override fuzz runs from the CLI when you want to force a different count for the current command:
280
-
281
- ```bash
282
- npx ast fuzz --runs 500
283
- npx ast fuzz --runs 1.5x
284
- npx ast fuzz --runs +10%
285
- npx ast fuzz --runs +100000
286
- ```
287
-
288
- If you used `npx ast init` with a fuzzer example, the config is already there. Otherwise, add a `fuzz` block to `as-test.config.json` so `npx ast fuzz` knows what to build:
289
-
290
- ```json
291
- {
292
- "fuzz": {
293
- "input": ["./assembly/__fuzz__/*.fuzz.ts"],
294
- "target": "bindings"
295
- }
296
- }
297
- ```
298
-
299
- `ast fuzz` runs fuzz files across the selected modes, reports one result per file, and keeps the final summary separate from the normal test totals. If you want one combined command, use `ast test --fuzz`.
300
-
301
- By default, each fuzz run campaign picks a new random base seed. Pin a seed with `--seed <n>` (or `--fuzz-seed <n>` on `ast test`) when you want deterministic replay.
302
-
303
- When a fuzzer fails, `as-test` now prints the exact failing seeds and one-run repro commands such as `ast fuzz ... --seed <seed+n> --runs 1`. Crash records in `.as-test/crashes` also include the captured inputs passed to `run(...)`, which helps when the generator itself has side effects.
304
-
305
- Run only fuzzers:
306
-
307
- ```bash
308
- npx ast fuzz
309
- ```
310
-
311
- Run one matching fuzz target:
312
-
313
- ```bash
314
- npx ast fuzz string --fuzzer ascii-strings-survive-concatenation-boundaries
315
- ```
316
-
317
- Run tests and fuzzers together:
318
-
319
- ```bash
320
- npx ast test --fuzz
321
- ```
322
-
323
- Fuzzing is there when you want broader input coverage, but it does not get in the way of the normal test flow. You can start with ordinary specs and add fuzzers later.
324
-
325
183
  ## Runtimes
326
184
 
327
185
  One of the main reasons to use `as-test` is that you are not locked into a single runtime.
@@ -350,7 +208,13 @@ Then run your tests normally:
350
208
  npx ast test
351
209
  ```
352
210
 
353
- If you want to keep more than one runtime around, use modes:
211
+ If you want to keep a single runtime, one config is enough. If you want to fan out across multiple runtimes, use modes.
212
+
213
+ ## Modes
214
+
215
+ Modes let one project keep more than one runtime or build target available at the same time.
216
+
217
+ For example:
354
218
 
355
219
  ```json
356
220
  {
@@ -389,8 +253,12 @@ Set `"default": false` on a mode when you want to keep it available for explicit
389
253
  "modes": {
390
254
  "web": {
391
255
  "default": false,
256
+ "buildOptions": {
257
+ "target": "web"
258
+ },
392
259
  "runOptions": {
393
260
  "runtime": {
261
+ "cmd": "node ./.as-test/runners/default.web.js",
394
262
  "browser": "chromium"
395
263
  }
396
264
  }
@@ -423,8 +291,12 @@ Modes can also be full config objects. That means a mode can override fuzzing, i
423
291
  "input": ["./assembly/__fuzz__/web/*.fuzz.ts"],
424
292
  "runs": 200
425
293
  },
294
+ "buildOptions": {
295
+ "target": "web"
296
+ },
426
297
  "runOptions": {
427
298
  "runtime": {
299
+ "cmd": "node ./.as-test/runners/default.web.js",
428
300
  "browser": "chromium"
429
301
  }
430
302
  }
@@ -455,6 +327,135 @@ or
455
327
  npx ast test --mode wasi,bindings
456
328
  ```
457
329
 
330
+ ## Coverage
331
+
332
+ Coverage is opt-in.
333
+
334
+ Enable it from the CLI:
335
+
336
+ ```bash
337
+ npx ast test --enable coverage
338
+ npx ast test --enable coverage --show-coverage
339
+ npx ast test --enable coverage --show-coverage=all
340
+ ```
341
+
342
+ Or from config:
343
+
344
+ ```json
345
+ {
346
+ "coverage": {
347
+ "enabled": true,
348
+ "mode": "project",
349
+ "dependencies": ["json-as"],
350
+ "includeSpecs": false
351
+ }
352
+ }
353
+ ```
354
+
355
+ Coverage modes:
356
+
357
+ - `project`
358
+ - covers project files only
359
+ - excludes dependency files by default
360
+ - `all`
361
+ - covers project files and dependency files
362
+ - still excludes AssemblyScript stdlib files
363
+
364
+ If you only want specific dependencies, keep `mode: "project"` and list package names in `dependencies`.
365
+ That works for both normal installs and `pnpm` layouts.
366
+
367
+ `--show-coverage` prints uncovered point details. `--show-coverage=all` and `--verbose` expand nested uncovered gaps instead of collapsing them.
368
+
369
+ ## Fuzzing
370
+
371
+ Fuzzers usually live in `assembly/__fuzz__/*.fuzz.ts`.
372
+
373
+ Example:
374
+
375
+ ```ts
376
+ import { expect, FuzzSeed, fuzz } from "as-test";
377
+
378
+ fuzz("bounded integer addition", (left: i32, right: i32): bool => {
379
+ const sum = left + right;
380
+ expect(sum - right).toBe(left);
381
+ return sum >= i32.MIN_VALUE;
382
+ }).generate((seed: FuzzSeed, run: (left: i32, right: i32) => bool): void => {
383
+ run(seed.i32({ min: -1000, max: 1000 }), seed.i32({ min: -1000, max: 1000 }));
384
+ });
385
+ ```
386
+
387
+ Pass a third argument to override the operation count for one target without changing the global fuzz config:
388
+
389
+ ```ts
390
+ fuzz(
391
+ "hot path stays stable",
392
+ (): void => {
393
+ expect(1 + 1).toBe(2);
394
+ },
395
+ 250,
396
+ );
397
+ ```
398
+
399
+ Or pass it as the second argument to `.generate(...)`:
400
+
401
+ ```ts
402
+ fuzz(
403
+ "ascii strings survive concatenation boundaries",
404
+ (input: string): bool => {
405
+ expect(input.length <= 40).toBe(true);
406
+ return true;
407
+ },
408
+ ).generate((seed: FuzzSeed, run: (input: string) => bool): void => {
409
+ run(seed.string({ charset: "ascii", min: 0, max: 40 }));
410
+ }, 250);
411
+ ```
412
+
413
+ You can still override fuzz runs from the CLI when you want to force a different count for the current command:
414
+
415
+ ```bash
416
+ npx ast fuzz --runs 500
417
+ npx ast fuzz --runs 1.5x
418
+ npx ast fuzz --runs +10%
419
+ npx ast fuzz --runs +100000
420
+ ```
421
+
422
+ If you used `npx ast init` with a fuzzer example, the config is already there. Otherwise, add a `fuzz` block to `as-test.config.json` so `npx ast fuzz` knows what to build:
423
+
424
+ ```json
425
+ {
426
+ "fuzz": {
427
+ "input": ["./assembly/__fuzz__/*.fuzz.ts"],
428
+ "target": "bindings"
429
+ }
430
+ }
431
+ ```
432
+
433
+ `ast fuzz` runs fuzz files across the selected modes, reports one result per file, and keeps the final summary separate from the normal test totals. If you want one combined command, use `ast test --fuzz`.
434
+
435
+ By default, each fuzz run campaign picks a new random base seed. Pin a seed with `--seed <n>` (or `--fuzz-seed <n>` on `ast test`) when you want deterministic replay.
436
+
437
+ When a fuzzer fails, `as-test` now prints the exact failing seeds and one-run repro commands such as `ast fuzz ... --seed <seed+n> --runs 1`. Crash records in `.as-test/crashes` also include the captured inputs passed to `run(...)`, which helps when the generator itself has side effects.
438
+
439
+ Run only fuzzers:
440
+
441
+ ```bash
442
+ npx ast fuzz
443
+ ```
444
+
445
+ Run one matching fuzz target:
446
+
447
+ ```bash
448
+ npx ast fuzz string --fuzzer ascii-strings-survive-concatenation-boundaries
449
+ ```
450
+
451
+ Run tests and fuzzers together:
452
+
453
+ ```bash
454
+ npx ast test --fuzz
455
+ ```
456
+
457
+ Fuzzing is there when you want broader input coverage, but it does not get in the way of the normal test flow. You can start with ordinary specs and add fuzzers later.
458
+
458
459
  This is the general idea throughout the project: write tests once, then choose the runtime that matches how your code actually runs.
459
460
 
460
461
  ## Examples
@@ -101,11 +101,25 @@
101
101
  "type": "boolean",
102
102
  "default": false
103
103
  },
104
+ "mode": {
105
+ "type": "string",
106
+ "enum": ["project", "all"],
107
+ "description": "Broad coverage eligibility. \"project\" covers project sources only. \"all\" also includes dependency sources.",
108
+ "default": "project"
109
+ },
104
110
  "includeSpecs": {
105
111
  "type": "boolean",
106
112
  "description": "Include *.spec.ts files in coverage collection.",
107
113
  "default": false
108
114
  },
115
+ "dependencies": {
116
+ "type": "array",
117
+ "description": "Package-name allowlist for dependency coverage, including pnpm-installed packages.",
118
+ "items": {
119
+ "type": "string"
120
+ },
121
+ "default": []
122
+ },
109
123
  "include": {
110
124
  "type": "array",
111
125
  "description": "Only include matching source files in coverage reports when this list is not empty.",
@@ -369,8 +383,18 @@
369
383
  "enabled": {
370
384
  "type": "boolean"
371
385
  },
386
+ "mode": {
387
+ "type": "string",
388
+ "enum": ["project", "all"]
389
+ },
372
390
  "includeSpecs": {
373
391
  "type": "boolean"
392
+ },
393
+ "dependencies": {
394
+ "type": "array",
395
+ "items": {
396
+ "type": "string"
397
+ }
374
398
  }
375
399
  }
376
400
  }
@@ -23,6 +23,7 @@ class SnapshotStore {
23
23
  this.matched = 0;
24
24
  this.failed = 0;
25
25
  this.warnedMissing = new Set();
26
+ this.specBasename = path.basename(specFile);
26
27
  const dir = path.join(process.cwd(), snapshotDir);
27
28
  const relative = resolveArtifactRelativePath(specFile, "__tests__").replace(/\.ts$/, ".snap");
28
29
  this.filePath = path.join(dir, relative);
@@ -37,6 +38,7 @@ class SnapshotStore {
37
38
  }
38
39
  assert(key, actual, allowSnapshot, createSnapshots, overwriteSnapshots) {
39
40
  key = canonicalizeSnapshotKey(key);
41
+ key = normalizeSnapshotKeyPrefix(key, this.specBasename);
40
42
  if (!allowSnapshot)
41
43
  return { ok: true, expected: actual, warnMissing: false };
42
44
  if (!(key in this.data)) {
@@ -260,6 +262,12 @@ function localizeSnapshotKey(specFile, key) {
260
262
  const prefix = `${path.basename(specFile)}::`;
261
263
  return key.startsWith(prefix) ? key.slice(prefix.length) : key;
262
264
  }
265
+ function normalizeSnapshotKeyPrefix(key, specBasename) {
266
+ const sep = key.indexOf("::");
267
+ if (sep < 0)
268
+ return key;
269
+ return `${specBasename}::${key.slice(sep + 2)}`;
270
+ }
263
271
  function qualifySnapshotKey(specFile, key) {
264
272
  return `${path.basename(specFile)}::${key}`;
265
273
  }
@@ -1313,12 +1321,18 @@ function isIgnoredCoverageFile(file, coverage) {
1313
1321
  const normalized = file.replace(/\\/g, "/");
1314
1322
  if (!isAllowedCoverageSourceFile(normalized))
1315
1323
  return true;
1316
- if (normalized.startsWith("node_modules/"))
1317
- return true;
1318
- if (normalized.includes("/node_modules/"))
1319
- return true;
1320
1324
  if (isAssemblyScriptStdlibFile(normalized))
1321
1325
  return true;
1326
+ const classification = classifyCoverageFile(normalized);
1327
+ if (classification.kind == "dependency") {
1328
+ if (coverage.mode != "all" && !coverage.dependencies.length)
1329
+ return true;
1330
+ if (coverage.dependencies.length &&
1331
+ (!classification.packageName ||
1332
+ !coverage.dependencies.includes(classification.packageName))) {
1333
+ return true;
1334
+ }
1335
+ }
1322
1336
  if (!coverage.includeSpecs && normalized.endsWith(".spec.ts"))
1323
1337
  return true;
1324
1338
  if (coverage.include.length &&
@@ -1376,9 +1390,52 @@ function isAllowedCoverageSourceFile(file) {
1376
1390
  const lower = file.toLowerCase();
1377
1391
  return lower.endsWith(".ts") || lower.endsWith(".as");
1378
1392
  }
1393
+ // AssemblyScript normalizes node_modules/<pkg>/... to ~lib/<pkg>/... in Source.normalizedPath.
1394
+ // This set contains the root names that are actual AS stdlib modules, so we can distinguish
1395
+ // real stdlib (~lib/array.ts) from third-party packages (~lib/json-as/assembly/index.ts).
1396
+ const AS_STDLIB_ROOT_NAMES = new Set([
1397
+ "array",
1398
+ "arraybuffer",
1399
+ "atomics",
1400
+ "bindings",
1401
+ "builtins",
1402
+ "compat",
1403
+ "console",
1404
+ "crypto",
1405
+ "dataview",
1406
+ "date",
1407
+ "diagnostics",
1408
+ "error",
1409
+ "function",
1410
+ "iterator",
1411
+ "map",
1412
+ "math",
1413
+ "memory",
1414
+ "number",
1415
+ "object",
1416
+ "polyfills",
1417
+ "process",
1418
+ "reference",
1419
+ "regexp",
1420
+ "rt",
1421
+ "set",
1422
+ "shared",
1423
+ "staticarray",
1424
+ "string",
1425
+ "symbol",
1426
+ "table",
1427
+ "typedarray",
1428
+ "uri",
1429
+ "util",
1430
+ "vector",
1431
+ ]);
1379
1432
  function isAssemblyScriptStdlibFile(file) {
1380
- if (file.startsWith("~lib/"))
1381
- return true;
1433
+ if (file.startsWith("~lib/")) {
1434
+ // Extract the first path segment after ~lib/ (strip any file extension)
1435
+ const after = file.slice("~lib/".length);
1436
+ const root = (after.split("/")[0] ?? "").replace(/\.[^.]+$/, "");
1437
+ return AS_STDLIB_ROOT_NAMES.has(root);
1438
+ }
1382
1439
  if (file.includes("/~lib/"))
1383
1440
  return true;
1384
1441
  if (file.startsWith("assemblyscript/std/"))
@@ -1387,11 +1444,57 @@ function isAssemblyScriptStdlibFile(file) {
1387
1444
  return true;
1388
1445
  return false;
1389
1446
  }
1447
+ function classifyCoverageFile(file) {
1448
+ const packageName = resolveCoverageDependencyPackage(file);
1449
+ if (packageName) {
1450
+ return { kind: "dependency", packageName };
1451
+ }
1452
+ return { kind: "project", packageName: null };
1453
+ }
1454
+ function resolveCoverageDependencyPackage(file) {
1455
+ const normalized = file.replace(/\\/g, "/");
1456
+ // AssemblyScript normalizes node_modules/<pkg>/... to ~lib/<pkg>/... at compile time.
1457
+ // Handle that path format so coverage.mode and coverage.dependencies work at runtime.
1458
+ if (normalized.startsWith("~lib/")) {
1459
+ const after = normalized.slice("~lib/".length);
1460
+ const segments = after.split("/").filter(Boolean);
1461
+ if (!segments.length)
1462
+ return null;
1463
+ if (segments[0].startsWith("@")) {
1464
+ if (segments.length < 2)
1465
+ return null;
1466
+ return `${segments[0]}/${segments[1]}`;
1467
+ }
1468
+ // Strip file extension for bare module entries like ~lib/json-as.ts (unusual but safe)
1469
+ return segments[0].replace(/\.[^.]+$/, "") || null;
1470
+ }
1471
+ const marker = "/node_modules/";
1472
+ const prefixed = normalized.startsWith("node_modules/")
1473
+ ? `/${normalized}`
1474
+ : normalized;
1475
+ const index = prefixed.lastIndexOf(marker);
1476
+ if (index == -1)
1477
+ return null;
1478
+ const after = prefixed.slice(index + marker.length);
1479
+ if (!after.length)
1480
+ return null;
1481
+ const segments = after.split("/").filter(Boolean);
1482
+ if (!segments.length)
1483
+ return null;
1484
+ if (segments[0].startsWith("@")) {
1485
+ if (segments.length < 2)
1486
+ return null;
1487
+ return `${segments[0]}/${segments[1]}`;
1488
+ }
1489
+ return segments[0];
1490
+ }
1390
1491
  function resolveCoverageOptions(raw) {
1391
1492
  if (typeof raw == "boolean") {
1392
1493
  return {
1393
1494
  enabled: raw,
1495
+ mode: "project",
1394
1496
  includeSpecs: false,
1497
+ dependencies: [],
1395
1498
  include: [],
1396
1499
  exclude: [],
1397
1500
  ignore: {
@@ -1409,7 +1512,11 @@ function resolveCoverageOptions(raw) {
1409
1512
  : null;
1410
1513
  return {
1411
1514
  enabled: obj.enabled == null ? false : Boolean(obj.enabled),
1515
+ mode: obj.mode == "all" ? "all" : "project",
1412
1516
  includeSpecs: Boolean(obj.includeSpecs),
1517
+ dependencies: Array.isArray(obj.dependencies)
1518
+ ? obj.dependencies.filter((item) => typeof item == "string")
1519
+ : [],
1413
1520
  include: Array.isArray(obj.include)
1414
1521
  ? obj.include.filter((item) => typeof item == "string")
1415
1522
  : [],
@@ -1434,7 +1541,9 @@ function resolveCoverageOptions(raw) {
1434
1541
  }
1435
1542
  return {
1436
1543
  enabled: false,
1544
+ mode: "project",
1437
1545
  includeSpecs: false,
1546
+ dependencies: [],
1438
1547
  include: [],
1439
1548
  exclude: [],
1440
1549
  ignore: {
@@ -2489,3 +2598,9 @@ function resolveReporterFactory(mod) {
2489
2598
  }
2490
2599
  throw new Error(`reporter module must export a factory as "createReporter" or default`);
2491
2600
  }
2601
+ export const __coverageInternals = {
2602
+ classifyCoverageFile,
2603
+ resolveCoverageDependencyPackage,
2604
+ isIgnoredCoverageFile,
2605
+ resolveCoverageOptions,
2606
+ };
package/bin/types.js CHANGED
@@ -18,7 +18,9 @@ export class Config {
18
18
  export class CoverageOptions {
19
19
  constructor() {
20
20
  this.enabled = false;
21
+ this.mode = "project";
21
22
  this.includeSpecs = false;
23
+ this.dependencies = [];
22
24
  this.include = [];
23
25
  this.exclude = [];
24
26
  this.ignore = new CoverageIgnoreOptions();
package/bin/util.js CHANGED
@@ -332,7 +332,7 @@ function validateCoverageValue(value, path, issues) {
332
332
  issues.push({
333
333
  path,
334
334
  message: "must be a boolean or object",
335
- fix: 'use true/false or { "enabled": true, "includeSpecs": false, "include": ["assembly/**/*.ts"], "exclude": ["assembly/__tests__/**/*.spec.ts"] }',
335
+ fix: 'use true/false or { "enabled": true, "mode": "project", "includeSpecs": false, "dependencies": ["json-as"], "include": ["assembly/**/*.ts"], "exclude": ["assembly/__tests__/**/*.spec.ts"] }',
336
336
  });
337
337
  return;
338
338
  }
@@ -344,6 +344,22 @@ function validateCoverageValue(value, path, issues) {
344
344
  fix: "set to true or false",
345
345
  });
346
346
  }
347
+ if ("mode" in obj && obj.mode != undefined) {
348
+ if (typeof obj.mode != "string") {
349
+ issues.push({
350
+ path: `${path}.mode`,
351
+ message: 'must be "project" or "all"',
352
+ fix: 'set "mode" to "project" or "all"',
353
+ });
354
+ }
355
+ else if (obj.mode != "project" && obj.mode != "all") {
356
+ issues.push({
357
+ path: `${path}.mode`,
358
+ message: 'must be "project" or "all"',
359
+ fix: 'set "mode" to "project" or "all"',
360
+ });
361
+ }
362
+ }
347
363
  if ("includeSpecs" in obj && typeof obj.includeSpecs != "boolean") {
348
364
  issues.push({
349
365
  path: `${path}.includeSpecs`,
@@ -351,6 +367,7 @@ function validateCoverageValue(value, path, issues) {
351
367
  fix: "set to true or false",
352
368
  });
353
369
  }
370
+ validateStringArrayField(obj, "dependencies", path, issues);
354
371
  validateStringArrayField(obj, "include", path, issues);
355
372
  validateStringArrayField(obj, "exclude", path, issues);
356
373
  if ("ignore" in obj && obj.ignore != undefined) {
@@ -929,13 +946,16 @@ function cloneCoverageOptions(coverage) {
929
946
  if (typeof coverage == "boolean")
930
947
  return coverage;
931
948
  const cloned = Object.assign(new CoverageOptions(), coverage);
949
+ const ignore = coverage.ignore ?? new CoverageIgnoreOptions();
950
+ cloned.mode = coverage.mode ?? "project";
951
+ cloned.dependencies = [...(coverage.dependencies ?? [])];
932
952
  cloned.include = [...(coverage.include ?? [])];
933
953
  cloned.exclude = [...(coverage.exclude ?? [])];
934
- cloned.ignore = Object.assign(new CoverageIgnoreOptions(), coverage.ignore);
935
- cloned.ignore.labels = [...(coverage.ignore.labels ?? [])];
936
- cloned.ignore.names = [...(coverage.ignore.names ?? [])];
937
- cloned.ignore.locations = [...(coverage.ignore.locations ?? [])];
938
- cloned.ignore.snippets = [...(coverage.ignore.snippets ?? [])];
954
+ cloned.ignore = Object.assign(new CoverageIgnoreOptions(), ignore);
955
+ cloned.ignore.labels = [...(ignore.labels ?? [])];
956
+ cloned.ignore.names = [...(ignore.names ?? [])];
957
+ cloned.ignore.locations = [...(ignore.locations ?? [])];
958
+ cloned.ignore.snippets = [...(ignore.snippets ?? [])];
939
959
  return cloned;
940
960
  }
941
961
  function cloneBuildOptions(options) {
@@ -1035,8 +1055,12 @@ function mergeCoverageConfig(base, override, raw) {
1035
1055
  const rawObject = raw;
1036
1056
  if ("enabled" in rawObject)
1037
1057
  mergedBase.enabled = overrideOptions.enabled;
1058
+ if ("mode" in rawObject)
1059
+ mergedBase.mode = overrideOptions.mode;
1038
1060
  if ("includeSpecs" in rawObject)
1039
1061
  mergedBase.includeSpecs = overrideOptions.includeSpecs;
1062
+ if ("dependencies" in rawObject)
1063
+ mergedBase.dependencies = [...overrideOptions.dependencies];
1040
1064
  if ("include" in rawObject)
1041
1065
  mergedBase.include = [...overrideOptions.include];
1042
1066
  if ("exclude" in rawObject)
@@ -1270,8 +1294,8 @@ function appendPathSegment(basePath, segment) {
1270
1294
  }
1271
1295
  export function getCliVersion() {
1272
1296
  const candidates = [
1273
- join(process.cwd(), "package.json"),
1274
1297
  join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
1298
+ join(process.cwd(), "package.json"),
1275
1299
  ];
1276
1300
  for (const pkgPath of candidates) {
1277
1301
  if (!existsSync(pkgPath))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "as-test",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "author": "Jairus Tanaka",
5
5
  "repository": {
6
6
  "type": "git",
@@ -94,7 +94,7 @@
94
94
  "docs:preview": "vitepress preview docs",
95
95
  "format": "prettier -w .",
96
96
  "release:check": "npm run build:cli && npm run build:lib && npm run build:transform && npm run test && npm run test:integration && npm run test:examples && npm pack --dry-run --cache /tmp/as-test-npm-cache",
97
- "prepublishOnly": "npm run build:cli && npm run build:lib && npm run build:transform && npm run test && npm run test:integration"
97
+ "prepublishOnly": "npm run build:cli && npm run build:lib && npm run build:transform && npm run format && npm run test && npm run test:integration"
98
98
  },
99
99
  "type": "module"
100
100
  }
@@ -2,6 +2,7 @@ import { BlockStatement, ExpressionStatement, Node, } from "assemblyscript/dist/
2
2
  import { RangeTransform } from "./range.js";
3
3
  import { isStdlib, SimpleParser } from "./util.js";
4
4
  import { Visitor } from "./visitor.js";
5
+ import { NodeKind } from "./types.js";
5
6
  const COVERAGE_IGNORED_CALLS = new Set([
6
7
  "beforeAll",
7
8
  "afterAll",
@@ -114,7 +115,7 @@ export class CoverageTransform extends Visitor {
114
115
  const coverStmt = createCoverStatement(point.hash, node);
115
116
  replacer.visit(coverStmt);
116
117
  this.globalStatements.push(registerStmt);
117
- if (node.kind == 31) {
118
+ if (node.kind == NodeKind.Block) {
118
119
  const block = node;
119
120
  block.statements.unshift(coverStmt);
120
121
  return block;
@@ -150,7 +151,7 @@ export class CoverageTransform extends Visitor {
150
151
  this.visit(node.expression, node);
151
152
  this.visit(node.typeArguments, node);
152
153
  for (const arg of node.args) {
153
- if (arg.kind == 15)
154
+ if (arg.kind == NodeKind.Function)
154
155
  continue;
155
156
  this.visit(arg, node);
156
157
  }
@@ -264,7 +265,7 @@ export class CoverageTransform extends Visitor {
264
265
  this.globalStatements.push(registerStmt);
265
266
  }
266
267
  }
267
- visitFunctionDeclaration(node, isDefault) {
268
+ visitFunctionDeclaration(node) {
268
269
  if (node.visited)
269
270
  return;
270
271
  node.visited = true;
@@ -278,7 +279,7 @@ export class CoverageTransform extends Visitor {
278
279
  const registerStmt = createRegisterStatement(point);
279
280
  replacer.visit(registerStmt);
280
281
  this.globalStatements.push(registerStmt);
281
- if (node.body.kind === 36) {
282
+ if (node.body.kind === NodeKind.Export) {
282
283
  const coverStmt = SimpleParser.parseStatement(`{
283
284
  __COVER("${point.hash}")
284
285
  return $$REPLACE_ME
@@ -327,14 +328,14 @@ export class CoverageTransform extends Visitor {
327
328
  const ifFalse = node.ifFalse;
328
329
  const path = node.range.source.normalizedPath;
329
330
  if (ifTrue &&
330
- ifTrue.kind !== 31 &&
331
+ ifTrue.kind !== NodeKind.Block &&
331
332
  !isBuiltinStatement(ifTrue)) {
332
333
  node.ifTrue = this.instrumentStatementBody(path, ifTrue, "IfBranch");
333
334
  visitIfTrue = true;
334
335
  visitIfFalse = !!ifFalse;
335
336
  }
336
337
  if (ifFalse &&
337
- ifFalse.kind !== 31 &&
338
+ ifFalse.kind !== NodeKind.Block &&
338
339
  !isBuiltinStatement(ifFalse)) {
339
340
  node.ifFalse = this.instrumentStatementBody(path, ifFalse, "IfBranch");
340
341
  visitIfTrue = true;
@@ -533,35 +534,35 @@ function getCallName(node) {
533
534
  return getExpressionName(node.expression);
534
535
  }
535
536
  function isBuiltinStatement(node) {
536
- if (node.kind !== 39)
537
+ if (node.kind !== NodeKind.Expression)
537
538
  return false;
538
539
  return isBuiltinCallExpression(node.expression);
539
540
  }
540
541
  function isBuiltinCallExpression(node) {
541
542
  const unwrapped = unwrapParenthesized(node);
542
- if (unwrapped.kind !== 10)
543
+ if (unwrapped.kind !== NodeKind.Call)
543
544
  return false;
544
545
  const call = unwrapped;
545
546
  const expression = unwrapParenthesized(call.expression);
546
- if (expression.kind !== 7)
547
+ if (expression.kind !== NodeKind.Identifier)
547
548
  return false;
548
549
  const name = expression.text;
549
550
  return COVERAGE_IGNORED_BUILTINS.has(name);
550
551
  }
551
552
  function unwrapParenthesized(node) {
552
553
  let current = node;
553
- while (current.kind === 21) {
554
+ while (current.kind === NodeKind.Parenthesized) {
554
555
  current = current.expression;
555
556
  }
556
557
  return current;
557
558
  }
558
559
  function getExpressionName(node) {
559
560
  switch (node.kind) {
560
- case 7:
561
+ case NodeKind.Identifier:
561
562
  return node.text;
562
- case 22:
563
+ case NodeKind.PropertyAccess:
563
564
  return node.property.text;
564
- case 21:
565
+ case NodeKind.Parenthesized:
565
566
  return getExpressionName(node.expression);
566
567
  default:
567
568
  return null;
@@ -27,7 +27,9 @@ export default class Transformer extends Transform {
27
27
  }
28
28
  });
29
29
  const entrySource = sources.find((v) => v.sourceKind == 1);
30
- const entryFile = entrySource ? entrySource.simplePath : "unknown";
30
+ const entryFile = entrySource
31
+ ? entrySource.normalizedPath.replace(/\.ts$/, "")
32
+ : "unknown";
31
33
  const mockedImportTargets = collectMockImportTargets(sources);
32
34
  for (const target of mockedImportTargets) {
33
35
  mock.importMocked.add(target);
@@ -1,3 +1,6 @@
1
+ import * as asc from "assemblyscript/dist/assemblyscript.js";
2
+ export const NodeKind = asc
3
+ .NodeKind;
1
4
  export var PropertyFlags;
2
5
  (function (PropertyFlags) {
3
6
  PropertyFlags[PropertyFlags["OmitNull"] = 0] = "OmitNull";
@@ -1,3 +1,4 @@
1
+ import { NodeKind } from "./types.js";
1
2
  export class Visitor {
2
3
  currentSource = null;
3
4
  visit(node, ref = null) {
@@ -14,216 +15,218 @@ export class Visitor {
14
15
  }
15
16
  _visit(node, ref) {
16
17
  switch (node.kind) {
17
- case 0:
18
+ case NodeKind.Source:
18
19
  this.visitSource(node, ref);
19
20
  break;
20
- case 1:
21
+ case NodeKind.NamedType:
21
22
  this.visitNamedTypeNode(node, ref);
22
23
  break;
23
- case 2:
24
+ case NodeKind.FunctionType:
24
25
  this.visitFunctionTypeNode(node, ref);
25
26
  break;
26
- case 4:
27
+ case NodeKind.TupleType:
28
+ break;
29
+ case NodeKind.TypeName:
27
30
  this.visitTypeName(node, ref);
28
31
  break;
29
- case 5:
32
+ case NodeKind.TypeParameter:
30
33
  this.visitTypeParameter(node, ref);
31
34
  break;
32
- case 7:
35
+ case NodeKind.Identifier:
33
36
  this.visitIdentifierExpression(node, ref);
34
37
  break;
35
- case 8:
38
+ case NodeKind.Assertion:
36
39
  this.visitAssertionExpression(node, ref);
37
40
  break;
38
- case 9:
41
+ case NodeKind.Binary:
39
42
  this.visitBinaryExpression(node, ref);
40
43
  break;
41
- case 10:
44
+ case NodeKind.Call:
42
45
  this.visitCallExpression(node, ref);
43
46
  break;
44
- case 11:
47
+ case NodeKind.Class:
45
48
  this.visitClassExpression(node, ref);
46
49
  break;
47
- case 12:
50
+ case NodeKind.Comma:
48
51
  this.visitCommaExpression(node, ref);
49
52
  break;
50
- case 13:
53
+ case NodeKind.ElementAccess:
51
54
  this.visitElementAccessExpression(node, ref);
52
55
  break;
53
- case 15:
56
+ case NodeKind.Function:
54
57
  this.visitFunctionExpression(node, ref);
55
58
  break;
56
- case 16:
59
+ case NodeKind.InstanceOf:
57
60
  this.visitInstanceOfExpression(node, ref);
58
61
  break;
59
- case 17:
62
+ case NodeKind.Literal:
60
63
  this.visitLiteralExpression(node, ref);
61
64
  break;
62
- case 18:
65
+ case NodeKind.New:
63
66
  this.visitNewExpression(node, ref);
64
67
  break;
65
- case 21:
68
+ case NodeKind.Parenthesized:
66
69
  this.visitParenthesizedExpression(node, ref);
67
70
  break;
68
- case 22:
71
+ case NodeKind.PropertyAccess:
69
72
  this.visitPropertyAccessExpression(node, ref);
70
73
  break;
71
- case 23:
74
+ case NodeKind.Ternary:
72
75
  this.visitTernaryExpression(node, ref);
73
76
  break;
74
- case 28:
77
+ case NodeKind.UnaryPostfix:
75
78
  this.visitUnaryPostfixExpression(node, ref);
76
79
  break;
77
- case 29:
80
+ case NodeKind.UnaryPrefix:
78
81
  this.visitUnaryPrefixExpression(node, ref);
79
82
  break;
80
- case 31:
83
+ case NodeKind.Block:
81
84
  this.visitBlockStatement(node, ref);
82
85
  break;
83
- case 32:
86
+ case NodeKind.Break:
84
87
  this.visitBreakStatement(node, ref);
85
88
  break;
86
- case 33:
89
+ case NodeKind.Continue:
87
90
  this.visitContinueStatement(node, ref);
88
91
  break;
89
- case 34:
92
+ case NodeKind.Do:
90
93
  this.visitDoStatement(node, ref);
91
94
  break;
92
- case 35:
95
+ case NodeKind.Empty:
93
96
  this.visitEmptyStatement(node, ref);
94
97
  break;
95
- case 36:
98
+ case NodeKind.Export:
96
99
  this.visitExportStatement(node, ref);
97
100
  break;
98
- case 37:
101
+ case NodeKind.ExportDefault:
99
102
  this.visitExportDefaultStatement(node, ref);
100
103
  break;
101
- case 38:
104
+ case NodeKind.ExportImport:
102
105
  this.visitExportImportStatement(node, ref);
103
106
  break;
104
- case 39:
107
+ case NodeKind.Expression:
105
108
  this.visitExpressionStatement(node, ref);
106
109
  break;
107
- case 40:
110
+ case NodeKind.For:
108
111
  this.visitForStatement(node, ref);
109
112
  break;
110
- case 42:
113
+ case NodeKind.If:
111
114
  this.visitIfStatement(node, ref);
112
115
  break;
113
- case 43:
116
+ case NodeKind.Import:
114
117
  this.visitImportStatement(node, ref);
115
118
  break;
116
- case 44:
119
+ case NodeKind.Return:
117
120
  this.visitReturnStatement(node, ref);
118
121
  break;
119
- case 45:
122
+ case NodeKind.Switch:
120
123
  this.visitSwitchStatement(node, ref);
121
124
  break;
122
- case 46:
125
+ case NodeKind.Throw:
123
126
  this.visitThrowStatement(node, ref);
124
127
  break;
125
- case 47:
128
+ case NodeKind.Try:
126
129
  this.visitTryStatement(node, ref);
127
130
  break;
128
- case 48:
131
+ case NodeKind.Variable:
129
132
  this.visitVariableStatement(node, ref);
130
133
  break;
131
- case 50:
134
+ case NodeKind.While:
132
135
  this.visitWhileStatement(node, ref);
133
136
  break;
134
- case 52:
137
+ case NodeKind.ClassDeclaration:
135
138
  this.visitClassDeclaration(node, false, ref);
136
139
  break;
137
- case 53:
140
+ case NodeKind.EnumDeclaration:
138
141
  this.visitEnumDeclaration(node, false, ref);
139
142
  break;
140
- case 54:
143
+ case NodeKind.EnumValueDeclaration:
141
144
  this.visitEnumValueDeclaration(node, ref);
142
145
  break;
143
- case 55:
146
+ case NodeKind.FieldDeclaration:
144
147
  this.visitFieldDeclaration(node, ref);
145
148
  break;
146
- case 56:
149
+ case NodeKind.FunctionDeclaration:
147
150
  this.visitFunctionDeclaration(node, false, ref);
148
151
  break;
149
- case 57:
152
+ case NodeKind.ImportDeclaration:
150
153
  this.visitImportDeclaration(node, ref);
151
154
  break;
152
- case 58:
155
+ case NodeKind.InterfaceDeclaration:
153
156
  this.visitInterfaceDeclaration(node, false, ref);
154
157
  break;
155
- case 59:
158
+ case NodeKind.MethodDeclaration:
156
159
  this.visitMethodDeclaration(node, ref);
157
160
  break;
158
- case 60:
161
+ case NodeKind.NamespaceDeclaration:
159
162
  this.visitNamespaceDeclaration(node, false, ref);
160
163
  break;
161
- case 61:
164
+ case NodeKind.TypeDeclaration:
162
165
  this.visitTypeDeclaration(node, ref);
163
166
  break;
164
- case 62:
167
+ case NodeKind.VariableDeclaration:
165
168
  this.visitVariableDeclaration(node, ref);
166
169
  break;
167
- case 63:
170
+ case NodeKind.Decorator:
168
171
  this.visitDecoratorNode(node, ref);
169
172
  break;
170
- case 64:
173
+ case NodeKind.ExportMember:
171
174
  this.visitExportMember(node, ref);
172
175
  break;
173
- case 65:
176
+ case NodeKind.SwitchCase:
174
177
  this.visitSwitchCase(node, ref);
175
178
  break;
176
- case 66:
179
+ case NodeKind.IndexSignature:
177
180
  this.visitIndexSignature(node, ref);
178
181
  break;
179
- case 19:
182
+ case NodeKind.Null:
180
183
  this.visitNullExpression(node, ref);
181
184
  break;
182
- case 26: {
185
+ case NodeKind.True: {
183
186
  this.visitTrueExpression(node, ref);
184
187
  break;
185
188
  }
186
- case 14: {
189
+ case NodeKind.False: {
187
190
  this.visitFalseExpression(node, ref);
188
191
  break;
189
192
  }
190
- case 30: {
193
+ case NodeKind.Compiled: {
191
194
  this.visitCompiledExpression(node, ref);
192
195
  break;
193
196
  }
194
- case 27: {
197
+ case NodeKind.Constructor: {
195
198
  this.visitConstructorExpression(node, ref);
196
199
  break;
197
200
  }
198
- case 67: {
201
+ case NodeKind.Comment: {
199
202
  this.visitComment(node, ref);
200
203
  break;
201
204
  }
202
- case 41: {
205
+ case NodeKind.ForOf: {
203
206
  this.visitForOfStatement(node, ref);
204
207
  break;
205
208
  }
206
- case 51: {
209
+ case NodeKind.Module: {
207
210
  this.visitModuleDeclaration(node, ref);
208
211
  break;
209
212
  }
210
- case 20: {
213
+ case NodeKind.Omitted: {
211
214
  this.visitOmittedExpression(node, ref);
212
215
  break;
213
216
  }
214
- case 6: {
217
+ case NodeKind.Parameter: {
215
218
  this.visitParameter(node, ref);
216
219
  break;
217
220
  }
218
- case 24: {
221
+ case NodeKind.Super: {
219
222
  this.visitSuperExpression(node, ref);
220
223
  break;
221
224
  }
222
- case 25: {
225
+ case NodeKind.This: {
223
226
  this.visitThisExpression(node, ref);
224
227
  break;
225
228
  }
226
- case 49: {
229
+ case NodeKind.Void: {
227
230
  this.visitVoidStatement(node, ref);
228
231
  break;
229
232
  }