as-test 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +1 -4
- package/as-test.config.schema.json +15 -0
- package/assembly/coverage.ts +22 -26
- package/assembly/index.ts +68 -47
- package/assembly/src/expectation.ts +154 -123
- package/assembly/src/fuzz.ts +10 -10
- package/assembly/src/log.ts +3 -3
- package/assembly/src/mode.ts +55 -0
- package/assembly/src/reflect.ts +122 -0
- package/assembly/src/stringify.ts +240 -0
- package/assembly/src/suite.ts +48 -27
- package/assembly/src/tests.ts +7 -7
- package/assembly/util/wipc.ts +2 -2
- package/bin/build-worker-pool.js +9 -0
- package/bin/build-worker.js +27 -3
- package/bin/commands/build-core.js +293 -86
- package/bin/commands/build.js +3 -1
- package/bin/commands/init-core.js +253 -8
- package/bin/commands/run-core.js +165 -41
- package/bin/commands/run.js +2 -1
- package/bin/commands/test.js +3 -2
- package/bin/dependency-graph.js +0 -0
- package/bin/index.js +592 -97
- package/bin/reporters/default.js +34 -0
- package/bin/types.js +7 -0
- package/bin/util.js +52 -0
- package/package.json +4 -8
- package/transform/lib/equals.js +388 -0
- package/transform/lib/index.js +28 -0
- package/transform/lib/log.js +3 -7
- package/transform/lib/types.js +4 -2
- package/transform/lib/transform.js +0 -502
package/assembly/src/tests.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { escape, stringify } from "./stringify";
|
|
2
2
|
|
|
3
3
|
export class Tests {
|
|
4
4
|
public order: i32 = 0;
|
|
@@ -10,24 +10,24 @@ export class Tests {
|
|
|
10
10
|
public message: string = "";
|
|
11
11
|
public location: string = "";
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
toJSON(): string {
|
|
14
14
|
return (
|
|
15
15
|
'{"order":' +
|
|
16
16
|
this.order.toString() +
|
|
17
17
|
',"type":' +
|
|
18
|
-
|
|
18
|
+
escape(this.type) +
|
|
19
19
|
',"verdict":' +
|
|
20
|
-
|
|
20
|
+
escape(this.verdict) +
|
|
21
21
|
',"left":' +
|
|
22
22
|
(this.left.length ? this.left : "null") +
|
|
23
23
|
',"right":' +
|
|
24
24
|
(this.right.length ? this.right : "null") +
|
|
25
25
|
',"instr":' +
|
|
26
|
-
|
|
26
|
+
escape(this.instr) +
|
|
27
27
|
',"message":' +
|
|
28
|
-
|
|
28
|
+
escape(this.message) +
|
|
29
29
|
',"location":' +
|
|
30
|
-
|
|
30
|
+
escape(this.location) +
|
|
31
31
|
"}"
|
|
32
32
|
);
|
|
33
33
|
}
|
package/assembly/util/wipc.ts
CHANGED
package/bin/build-worker-pool.js
CHANGED
|
@@ -24,6 +24,7 @@ export class BuildWorkerPool {
|
|
|
24
24
|
buildCommand: args.buildCommand,
|
|
25
25
|
featureToggles,
|
|
26
26
|
overrides,
|
|
27
|
+
onReads: args.onReads,
|
|
27
28
|
resolve,
|
|
28
29
|
reject,
|
|
29
30
|
});
|
|
@@ -113,6 +114,13 @@ export class BuildWorkerPool {
|
|
|
113
114
|
worker.busy = false;
|
|
114
115
|
worker.task = null;
|
|
115
116
|
if (message.type == "done") {
|
|
117
|
+
if (task.onReads && message.reads?.length) {
|
|
118
|
+
try {
|
|
119
|
+
task.onReads(message.reads);
|
|
120
|
+
} catch {
|
|
121
|
+
// a misbehaving sink shouldn't poison the build pipeline.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
116
124
|
task.resolve();
|
|
117
125
|
} else {
|
|
118
126
|
task.reject(deserializeError(message.error));
|
|
@@ -134,6 +142,7 @@ export class BuildWorkerPool {
|
|
|
134
142
|
modeName: task.modeName,
|
|
135
143
|
featureToggles: task.featureToggles,
|
|
136
144
|
overrides: task.overrides,
|
|
145
|
+
recordReads: !!task.onReads,
|
|
137
146
|
});
|
|
138
147
|
}
|
|
139
148
|
}
|
package/bin/build-worker.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { build } from "./commands/build-core.js";
|
|
2
|
-
process.env.AS_TEST_BUILD_API = "1";
|
|
1
|
+
import { build, buildRecorderStorage } from "./commands/build-core.js";
|
|
3
2
|
process.on("message", async (message) => {
|
|
4
3
|
if (!message || message.type != "build-file") return;
|
|
5
|
-
|
|
4
|
+
// Force the in-process API build path inside this worker so the readFile
|
|
5
|
+
// hook is reachable. We do it on first message rather than at module load
|
|
6
|
+
// so importing this file from a test doesn't mutate the parent env.
|
|
7
|
+
process.env.AS_TEST_BUILD_API = "1";
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
const collected = [];
|
|
10
|
+
const runBuild = async () => {
|
|
6
11
|
await build(
|
|
7
12
|
message.configPath,
|
|
8
13
|
[message.file],
|
|
@@ -10,9 +15,28 @@ process.on("message", async (message) => {
|
|
|
10
15
|
message.featureToggles,
|
|
11
16
|
message.overrides,
|
|
12
17
|
);
|
|
18
|
+
};
|
|
19
|
+
try {
|
|
20
|
+
if (message.recordReads) {
|
|
21
|
+
const store = {
|
|
22
|
+
// asc commonly resolves the same source twice during a build (entry
|
|
23
|
+
// lookups, transform passes). Dedupe at record time so IPC payloads
|
|
24
|
+
// stay bounded — `(mode, spec)` is constant for the worker's lifetime
|
|
25
|
+
// of this task, so a file-keyed set is sufficient.
|
|
26
|
+
record: (mode, spec, file) => {
|
|
27
|
+
if (seen.has(file)) return;
|
|
28
|
+
seen.add(file);
|
|
29
|
+
collected.push({ mode, spec, file });
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
await buildRecorderStorage.run(store, runBuild);
|
|
33
|
+
} else {
|
|
34
|
+
await runBuild();
|
|
35
|
+
}
|
|
13
36
|
send({
|
|
14
37
|
type: "done",
|
|
15
38
|
id: message.id,
|
|
39
|
+
reads: message.recordReads ? collected : undefined,
|
|
16
40
|
});
|
|
17
41
|
} catch (error) {
|
|
18
42
|
send({
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
2
|
+
import { promises as fsPromises } from "fs";
|
|
3
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
4
|
+
import { INTERNAL_FEATURE_NAMES, normalizeFeatureName } from "../types.js";
|
|
2
5
|
import { glob } from "glob";
|
|
3
6
|
import chalk from "chalk";
|
|
4
7
|
import { spawn } from "child_process";
|
|
5
8
|
import * as path from "path";
|
|
6
9
|
import {
|
|
7
10
|
createMemoryStream,
|
|
11
|
+
libraryFiles as ascLibraryFiles,
|
|
8
12
|
main as ascMain,
|
|
9
13
|
} from "assemblyscript/dist/asc.js";
|
|
10
14
|
import {
|
|
@@ -19,6 +23,7 @@ import {
|
|
|
19
23
|
import { persistCrashRecord } from "../crash-store.js";
|
|
20
24
|
import { BuildWorkerPool } from "../build-worker-pool.js";
|
|
21
25
|
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
26
|
+
export const buildRecorderStorage = new AsyncLocalStorage();
|
|
22
27
|
export class BuildFailureError extends Error {
|
|
23
28
|
constructor(args) {
|
|
24
29
|
super(args.message);
|
|
@@ -67,6 +72,7 @@ export async function build(
|
|
|
67
72
|
a.localeCompare(b),
|
|
68
73
|
);
|
|
69
74
|
await assertNoArtifactCollisions(sourceInputPatterns);
|
|
75
|
+
warnOnUnknownModeReferences(inputFiles, loadedConfig.modes ?? {});
|
|
70
76
|
const coverageEnabled = resolveCoverageEnabled(
|
|
71
77
|
config.coverage,
|
|
72
78
|
featureToggles.coverage,
|
|
@@ -75,11 +81,13 @@ export async function build(
|
|
|
75
81
|
...mode.env,
|
|
76
82
|
...config.buildOptions.env,
|
|
77
83
|
AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
|
|
84
|
+
AS_TEST_MODE_NAME: modeName ?? "default",
|
|
78
85
|
};
|
|
79
86
|
if (
|
|
80
87
|
!resolvedConfig &&
|
|
81
88
|
!process.env.AS_TEST_BUILD_API &&
|
|
82
|
-
!hasCustomBuildCommand(config)
|
|
89
|
+
!hasCustomBuildCommand(config) &&
|
|
90
|
+
!buildRecorderStorage.getStore()
|
|
83
91
|
) {
|
|
84
92
|
const pool = getSerialBuildWorkerPool();
|
|
85
93
|
for (const file of inputFiles) {
|
|
@@ -259,6 +267,7 @@ export async function getBuildReuseInfo(
|
|
|
259
267
|
...mode.env,
|
|
260
268
|
...config.buildOptions.env,
|
|
261
269
|
AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
|
|
270
|
+
AS_TEST_MODE_NAME: modeName ?? "default",
|
|
262
271
|
};
|
|
263
272
|
return {
|
|
264
273
|
signature: JSON.stringify({
|
|
@@ -273,6 +282,129 @@ export async function getBuildReuseInfo(
|
|
|
273
282
|
function hasCustomBuildCommand(config) {
|
|
274
283
|
return !!config.buildOptions.cmd.trim().length;
|
|
275
284
|
}
|
|
285
|
+
// Scans input spec files for `mode([...], fn)` calls whose entries reference
|
|
286
|
+
// mode names not present in the configured set. Collects all (file, name)
|
|
287
|
+
// hits and prints a single formatted block to stdout. The implicit "default"
|
|
288
|
+
// name is only valid when no configured mode has `default: true` — otherwise
|
|
289
|
+
// a named mode always runs and `AS_TEST_MODE_NAME` is never literal "default".
|
|
290
|
+
const MODE_CALL_RE = /\bmode\s*\(\s*\[([^\]]*)\]/g;
|
|
291
|
+
const MODE_STRING_RE = /["']([^"']*)["']/g;
|
|
292
|
+
const STRIP_COMMENTS_RE = /\/\*[\s\S]*?\*\/|\/\/.*$/gm;
|
|
293
|
+
const reportedModeWarnings = new Set();
|
|
294
|
+
const pendingModeWarningsByFile = new Map();
|
|
295
|
+
// Scans input spec files for `mode([...], fn)` calls whose entries reference
|
|
296
|
+
// mode names not present in the configured set, and buffers them for later
|
|
297
|
+
// printing via flushModeWarnings(). Called as early as possible (before the
|
|
298
|
+
// reporter starts streaming progress). De-duplicates across invocations.
|
|
299
|
+
export function warnOnUnknownModeReferences(files, configuredModes) {
|
|
300
|
+
const modeEntries = Object.entries(configuredModes ?? {});
|
|
301
|
+
const fallsBackToImplicitDefault =
|
|
302
|
+
modeEntries.length === 0 ||
|
|
303
|
+
modeEntries.every(([, mode]) => mode?.default === false);
|
|
304
|
+
const knownModes = new Set(modeEntries.map(([name]) => name));
|
|
305
|
+
if (fallsBackToImplicitDefault) knownModes.add("default");
|
|
306
|
+
const knownList = [...knownModes].sort();
|
|
307
|
+
for (const file of files) {
|
|
308
|
+
let text;
|
|
309
|
+
try {
|
|
310
|
+
text = readFileSync(file, "utf8");
|
|
311
|
+
} catch {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
text = text.replace(STRIP_COMMENTS_RE, "");
|
|
315
|
+
for (const callMatch of text.matchAll(MODE_CALL_RE)) {
|
|
316
|
+
const arrayContents = callMatch[1] ?? "";
|
|
317
|
+
for (const strMatch of arrayContents.matchAll(MODE_STRING_RE)) {
|
|
318
|
+
let value = strMatch[1] ?? "";
|
|
319
|
+
if (value.length === 0) continue;
|
|
320
|
+
if (value.charCodeAt(0) === 33 /* '!' */) value = value.slice(1);
|
|
321
|
+
if (value.length === 0) continue;
|
|
322
|
+
if (knownModes.has(value)) continue;
|
|
323
|
+
const key = `${file}\x1f${value}`;
|
|
324
|
+
if (reportedModeWarnings.has(key)) continue;
|
|
325
|
+
reportedModeWarnings.add(key);
|
|
326
|
+
const warning = {
|
|
327
|
+
name: value,
|
|
328
|
+
suggestion: closestKnownMode(value, knownList),
|
|
329
|
+
};
|
|
330
|
+
const list = pendingModeWarningsByFile.get(file);
|
|
331
|
+
if (list) list.push(warning);
|
|
332
|
+
else pendingModeWarningsByFile.set(file, [warning]);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Drains buffered mode warnings. When `showAll` is true, prints the full
|
|
338
|
+
// per-warning block; otherwise prints a one-line summary that tells the user
|
|
339
|
+
// to re-run with `--show-warnings`. No-op when there are no warnings.
|
|
340
|
+
export function flushModeWarnings(showAll) {
|
|
341
|
+
if (pendingModeWarningsByFile.size === 0) return;
|
|
342
|
+
const hits = [];
|
|
343
|
+
for (const [file, list] of pendingModeWarningsByFile) {
|
|
344
|
+
for (const w of list) {
|
|
345
|
+
hits.push({ file, name: w.name, suggestion: w.suggestion });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
pendingModeWarningsByFile.clear();
|
|
349
|
+
if (hits.length === 0) return;
|
|
350
|
+
if (!showAll) {
|
|
351
|
+
const count = hits.length;
|
|
352
|
+
const noun = count === 1 ? "warning" : "warnings";
|
|
353
|
+
process.stdout.write(
|
|
354
|
+
`\nFound ${chalk.yellow.bold(count)} ${noun}. Run with ${chalk.dim("--show-warnings")} to view.\n`,
|
|
355
|
+
);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const lines = [chalk.yellow.bold("WARNINGS:")];
|
|
359
|
+
for (const hit of hits) {
|
|
360
|
+
let line = ` - unknown mode reference ${chalk.bold(`"${hit.name}"`)} in ${chalk.dim(hit.file)}`;
|
|
361
|
+
if (hit.suggestion) {
|
|
362
|
+
line += ` - did you mean ${chalk.cyan(`"${hit.suggestion}"`)}?`;
|
|
363
|
+
}
|
|
364
|
+
lines.push(line);
|
|
365
|
+
}
|
|
366
|
+
process.stdout.write("\n" + lines.join("\n") + "\n");
|
|
367
|
+
}
|
|
368
|
+
// Returns the configured mode whose Levenshtein distance to `name` is below
|
|
369
|
+
// a small threshold (proportional to the longer string's length). Returns
|
|
370
|
+
// null when nothing's close enough — avoids suggesting wildly different names.
|
|
371
|
+
function closestKnownMode(name, candidates) {
|
|
372
|
+
let best = null;
|
|
373
|
+
let bestDist = Infinity;
|
|
374
|
+
for (const candidate of candidates) {
|
|
375
|
+
const d = levenshtein(name, candidate);
|
|
376
|
+
const threshold = Math.max(
|
|
377
|
+
2,
|
|
378
|
+
Math.floor(Math.max(name.length, candidate.length) * 0.4),
|
|
379
|
+
);
|
|
380
|
+
if (d < bestDist && d <= threshold) {
|
|
381
|
+
bestDist = d;
|
|
382
|
+
best = candidate;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return best;
|
|
386
|
+
}
|
|
387
|
+
function levenshtein(a, b) {
|
|
388
|
+
const m = a.length;
|
|
389
|
+
const n = b.length;
|
|
390
|
+
if (m === 0) return n;
|
|
391
|
+
if (n === 0) return m;
|
|
392
|
+
let prev = new Array(n + 1);
|
|
393
|
+
let curr = new Array(n + 1);
|
|
394
|
+
for (let j = 0; j <= n; j++) prev[j] = j;
|
|
395
|
+
for (let i = 1; i <= m; i++) {
|
|
396
|
+
curr[0] = i;
|
|
397
|
+
for (let j = 1; j <= n; j++) {
|
|
398
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
399
|
+
const del = prev[j] + 1;
|
|
400
|
+
const ins = curr[j - 1] + 1;
|
|
401
|
+
const sub = prev[j - 1] + cost;
|
|
402
|
+
curr[j] = Math.min(del, ins, sub);
|
|
403
|
+
}
|
|
404
|
+
[prev, curr] = [curr, prev];
|
|
405
|
+
}
|
|
406
|
+
return prev[n];
|
|
407
|
+
}
|
|
276
408
|
function getBuildCommand(
|
|
277
409
|
config,
|
|
278
410
|
pkgRunner,
|
|
@@ -300,9 +432,25 @@ function getBuildCommand(
|
|
|
300
432
|
args: [...tokens.slice(1), ...userArgs],
|
|
301
433
|
};
|
|
302
434
|
}
|
|
303
|
-
const
|
|
435
|
+
const tryAsAlreadyConfigured =
|
|
436
|
+
argsDeclareTryAs(userArgs) || asconfigDeclaresTryAs(config.config);
|
|
437
|
+
const defaultArgs = getDefaultBuildArgs(
|
|
438
|
+
config,
|
|
439
|
+
featureToggles,
|
|
440
|
+
tryAsAlreadyConfigured,
|
|
441
|
+
);
|
|
304
442
|
const ascInvocation = resolveAscInvocation(pkgRunner);
|
|
305
|
-
|
|
443
|
+
// as-test's own transform goes first so CoverageTransform sees the
|
|
444
|
+
// unmodified user AST. User-supplied `--transform` flags follow it,
|
|
445
|
+
// then the rest of as-test's default args (config, features, etc.).
|
|
446
|
+
const args = [
|
|
447
|
+
...ascInvocation.args,
|
|
448
|
+
file,
|
|
449
|
+
"--transform",
|
|
450
|
+
"as-test/transform",
|
|
451
|
+
...userArgs,
|
|
452
|
+
...defaultArgs,
|
|
453
|
+
];
|
|
306
454
|
if (config.outDir.length) {
|
|
307
455
|
args.push("-o", outFile);
|
|
308
456
|
}
|
|
@@ -451,7 +599,12 @@ function ensureDeps(config) {
|
|
|
451
599
|
}
|
|
452
600
|
}
|
|
453
601
|
async function buildFile(invocation, env) {
|
|
454
|
-
|
|
602
|
+
// The readFile hook only works through the API path. If the watch recorder
|
|
603
|
+
// is active but env wasn't already set, force the API path so we can
|
|
604
|
+
// deliver the read stream.
|
|
605
|
+
const recorderActive = !!buildRecorderStorage.getStore();
|
|
606
|
+
const wantsApi = recorderActive || process.env.AS_TEST_BUILD_API == "1";
|
|
607
|
+
if (wantsApi && invocation.apiArgs?.length) {
|
|
455
608
|
await buildFileViaApi(invocation.apiArgs, env);
|
|
456
609
|
return;
|
|
457
610
|
}
|
|
@@ -472,8 +625,28 @@ async function buildFileViaApi(args, env) {
|
|
|
472
625
|
});
|
|
473
626
|
const previousEnv = snapshotEnv();
|
|
474
627
|
applyEnv(env);
|
|
628
|
+
// asc's `libraryFiles` is a module-global dict that `--lib` flags mutate
|
|
629
|
+
// by inserting new entries (e.g. wasi-shim files when targeting wasi).
|
|
630
|
+
// When we call ascMain in-process across multiple modes (which the watch
|
|
631
|
+
// loop does), those entries leak into later compiles and try to resolve
|
|
632
|
+
// imports that the next mode's lib path doesn't satisfy. Snapshot the
|
|
633
|
+
// keys before each call and drop anything new after, so each ascMain sees
|
|
634
|
+
// the same baseline stdlib.
|
|
635
|
+
const baselineLibraryKeys = new Set(Object.keys(ascLibraryFiles));
|
|
475
636
|
try {
|
|
476
|
-
const
|
|
637
|
+
const ascOptions = { stdout, stderr };
|
|
638
|
+
const recorder = buildRecorderStorage.getStore();
|
|
639
|
+
if (recorder) {
|
|
640
|
+
const specFile = args[0] ? path.resolve(args[0]) : "";
|
|
641
|
+
const modeName = process.env.AS_TEST_MODE_NAME;
|
|
642
|
+
const mode = modeName && modeName !== "default" ? modeName : undefined;
|
|
643
|
+
if (specFile) {
|
|
644
|
+
ascOptions.readFile = makeRecordingReadFile((abs) => {
|
|
645
|
+
recorder.record(mode, specFile, abs);
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const result = await ascMain(args, ascOptions);
|
|
477
650
|
if (result.error) {
|
|
478
651
|
const error = result.error;
|
|
479
652
|
error.stderr = stderrChunks.join("").trim();
|
|
@@ -482,8 +655,27 @@ async function buildFileViaApi(args, env) {
|
|
|
482
655
|
}
|
|
483
656
|
} finally {
|
|
484
657
|
restoreEnv(previousEnv);
|
|
658
|
+
for (const key of Object.keys(ascLibraryFiles)) {
|
|
659
|
+
if (!baselineLibraryKeys.has(key)) {
|
|
660
|
+
delete ascLibraryFiles[key];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
485
663
|
}
|
|
486
664
|
}
|
|
665
|
+
// Mirrors asc's own default readFile (path.resolve(baseDir, filename),
|
|
666
|
+
// readFile utf-8, return null on ENOENT) and records each successful read.
|
|
667
|
+
function makeRecordingReadFile(onFileRead) {
|
|
668
|
+
return async (filename, baseDir) => {
|
|
669
|
+
const resolved = path.resolve(baseDir, filename);
|
|
670
|
+
try {
|
|
671
|
+
const content = await fsPromises.readFile(resolved, "utf8");
|
|
672
|
+
onFileRead(resolved);
|
|
673
|
+
return content;
|
|
674
|
+
} catch {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
487
679
|
async function buildFileViaSpawn(invocation, env) {
|
|
488
680
|
await new Promise((resolve, reject) => {
|
|
489
681
|
const child = spawn(invocation.command, invocation.args, {
|
|
@@ -548,6 +740,7 @@ function formatInvocation(invocation) {
|
|
|
548
740
|
.join(" ");
|
|
549
741
|
}
|
|
550
742
|
export { getBuildCommand, formatInvocation };
|
|
743
|
+
export { argsDeclareTryAs, asconfigDeclaresTryAs };
|
|
551
744
|
function getBuildStderr(error) {
|
|
552
745
|
const err = error;
|
|
553
746
|
const stderr = err?.stderr;
|
|
@@ -568,17 +761,24 @@ function getBuildStdout(error) {
|
|
|
568
761
|
if (stdout instanceof Buffer) return stdout.toString("utf8").trim();
|
|
569
762
|
return "";
|
|
570
763
|
}
|
|
571
|
-
function getDefaultBuildArgs(
|
|
764
|
+
function getDefaultBuildArgs(
|
|
765
|
+
config,
|
|
766
|
+
featureToggles,
|
|
767
|
+
tryAsAlreadyConfigured = false,
|
|
768
|
+
) {
|
|
572
769
|
const buildArgs = [];
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
770
|
+
const effectiveFeatures = resolveEffectiveFeatures(config, featureToggles);
|
|
771
|
+
const tryAsEnabled = resolveTryAsEnabled(effectiveFeatures.has("try-as"));
|
|
772
|
+
// `--transform as-test/transform` is appended by `getBuildCommand` at
|
|
773
|
+
// the front of the user-supplied transforms so coverage instruments
|
|
774
|
+
// the unmodified user AST.
|
|
775
|
+
// Auto-inject `--transform try-as/transform` when the `try-as`
|
|
776
|
+
// feature is on. Unlike json-as, try-as is tightly coupled to as-test
|
|
777
|
+
// (it powers `toThrow()`), only meaningful when the user explicitly
|
|
778
|
+
// opts into the feature, and doesn't rewrite arbitrary user code in
|
|
779
|
+
// ways that surprise consumers — auto-injection keeps the feature
|
|
780
|
+
// ergonomics intact without the conflict surface json-as exposed.
|
|
781
|
+
if (tryAsEnabled && !tryAsAlreadyConfigured) {
|
|
582
782
|
buildArgs.push("--transform", "try-as/transform");
|
|
583
783
|
}
|
|
584
784
|
if (config.config && config.config !== "none") {
|
|
@@ -587,6 +787,10 @@ function getDefaultBuildArgs(config, featureToggles) {
|
|
|
587
787
|
if (tryAsEnabled) {
|
|
588
788
|
buildArgs.push("--use", "AS_TEST_TRY_AS=1");
|
|
589
789
|
}
|
|
790
|
+
for (const feature of effectiveFeatures) {
|
|
791
|
+
if (INTERNAL_FEATURE_NAMES.has(feature)) continue;
|
|
792
|
+
buildArgs.push("--enable", feature);
|
|
793
|
+
}
|
|
590
794
|
// Should also strip any bindings-enabling from asconfig
|
|
591
795
|
if (
|
|
592
796
|
config.buildOptions.target == "bindings" ||
|
|
@@ -617,84 +821,29 @@ function getDefaultBuildArgs(config, featureToggles) {
|
|
|
617
821
|
}
|
|
618
822
|
return buildArgs;
|
|
619
823
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
function userSuppliesJsonAsTransform(config) {
|
|
625
|
-
for (const raw of config.buildOptions.args) {
|
|
626
|
-
if (!raw.length) continue;
|
|
627
|
-
const tokens = tokenizeCommand(raw);
|
|
628
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
629
|
-
const token = tokens[i];
|
|
630
|
-
if (token == "--transform") {
|
|
631
|
-
const value = tokens[i + 1];
|
|
632
|
-
if (value && JSON_AS_TRANSFORM_PATTERN.test(value)) return true;
|
|
633
|
-
} else if (token.startsWith("--transform=")) {
|
|
634
|
-
const value = token.slice("--transform=".length);
|
|
635
|
-
if (value && JSON_AS_TRANSFORM_PATTERN.test(value)) return true;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
824
|
+
function resolveEffectiveFeatures(config, featureToggles) {
|
|
825
|
+
const effective = new Set();
|
|
826
|
+
for (const name of config.features) {
|
|
827
|
+
effective.add(normalizeFeatureName(name));
|
|
638
828
|
}
|
|
639
|
-
|
|
640
|
-
|
|
829
|
+
const overrides = featureToggles.featureOverrides ?? {};
|
|
830
|
+
for (const [name, enabled] of Object.entries(overrides)) {
|
|
831
|
+
const key = normalizeFeatureName(name);
|
|
832
|
+
if (!key.length) continue;
|
|
833
|
+
if (enabled) effective.add(key);
|
|
834
|
+
else effective.delete(key);
|
|
641
835
|
}
|
|
642
|
-
|
|
836
|
+
effective.delete("");
|
|
837
|
+
return effective;
|
|
643
838
|
}
|
|
644
|
-
function
|
|
645
|
-
|
|
646
|
-
if (
|
|
647
|
-
seen.add(resolved);
|
|
648
|
-
let parsed;
|
|
649
|
-
try {
|
|
650
|
-
parsed = JSON.parse(readFileSync(resolved, "utf8"));
|
|
651
|
-
} catch {
|
|
652
|
-
return false;
|
|
653
|
-
}
|
|
654
|
-
if (!parsed || typeof parsed != "object") return false;
|
|
655
|
-
const obj = parsed;
|
|
656
|
-
if (transformsContainJsonAs(obj.options)) return true;
|
|
657
|
-
if (transformsContainJsonAs(obj)) return true;
|
|
658
|
-
const targets = obj.targets;
|
|
659
|
-
if (targets && typeof targets == "object" && !Array.isArray(targets)) {
|
|
660
|
-
for (const value of Object.values(targets)) {
|
|
661
|
-
if (transformsContainJsonAs(value)) return true;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
const extendsValue = obj.extends;
|
|
665
|
-
if (typeof extendsValue == "string" && extendsValue.length) {
|
|
666
|
-
const parentPath = path.resolve(path.dirname(resolved), extendsValue);
|
|
667
|
-
if (existsSync(parentPath) && asconfigDeclaresJsonAs(parentPath, seen)) {
|
|
668
|
-
return true;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
return false;
|
|
672
|
-
}
|
|
673
|
-
function transformsContainJsonAs(value) {
|
|
674
|
-
if (!value || typeof value != "object") return false;
|
|
675
|
-
const transform = value.transform;
|
|
676
|
-
if (typeof transform == "string") {
|
|
677
|
-
return JSON_AS_TRANSFORM_PATTERN.test(transform);
|
|
678
|
-
}
|
|
679
|
-
if (Array.isArray(transform)) {
|
|
680
|
-
for (const item of transform) {
|
|
681
|
-
if (typeof item == "string" && JSON_AS_TRANSFORM_PATTERN.test(item)) {
|
|
682
|
-
return true;
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
|
-
function resolveTryAsEnabled(override) {
|
|
689
|
-
const installed = hasTryAsRuntime();
|
|
690
|
-
if (override === false) return false;
|
|
691
|
-
if (override === true && !installed) {
|
|
839
|
+
function resolveTryAsEnabled(enabled) {
|
|
840
|
+
if (!enabled) return false;
|
|
841
|
+
if (!hasTryAsRuntime()) {
|
|
692
842
|
throw new Error(
|
|
693
843
|
'try-as feature was enabled, but package "try-as" is not installed',
|
|
694
844
|
);
|
|
695
845
|
}
|
|
696
|
-
|
|
697
|
-
return false;
|
|
846
|
+
return true;
|
|
698
847
|
}
|
|
699
848
|
function resolveCoverageEnabled(rawCoverage, override) {
|
|
700
849
|
if (override != undefined) return override;
|
|
@@ -708,6 +857,64 @@ function resolveCoverageEnabled(rawCoverage, override) {
|
|
|
708
857
|
function hasTryAsRuntime() {
|
|
709
858
|
return resolveProjectModule("try-as/package.json") != null;
|
|
710
859
|
}
|
|
860
|
+
const TRY_AS_TRANSFORM_RE = /(?:^|[\\/])try-as(?:[\\/]|$)/;
|
|
861
|
+
function isTryAsTransformSpec(value) {
|
|
862
|
+
if (typeof value !== "string") return false;
|
|
863
|
+
if (value === "try-as") return true;
|
|
864
|
+
return TRY_AS_TRANSFORM_RE.test(value);
|
|
865
|
+
}
|
|
866
|
+
function argsDeclareTryAs(args) {
|
|
867
|
+
for (let i = 0; i < args.length; i++) {
|
|
868
|
+
const arg = args[i];
|
|
869
|
+
if (arg === "--transform" || arg === "-t") {
|
|
870
|
+
const next = args[i + 1];
|
|
871
|
+
if (isTryAsTransformSpec(next)) return true;
|
|
872
|
+
} else if (arg.startsWith("--transform=")) {
|
|
873
|
+
if (isTryAsTransformSpec(arg.slice("--transform=".length))) return true;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
function asconfigDeclaresTryAs(configPath, seen = new Set()) {
|
|
879
|
+
if (!configPath || configPath === "none") return false;
|
|
880
|
+
const resolved = path.isAbsolute(configPath)
|
|
881
|
+
? configPath
|
|
882
|
+
: path.resolve(process.cwd(), configPath);
|
|
883
|
+
if (seen.has(resolved)) return false;
|
|
884
|
+
seen.add(resolved);
|
|
885
|
+
if (!existsSync(resolved)) return false;
|
|
886
|
+
let parsed;
|
|
887
|
+
try {
|
|
888
|
+
parsed = JSON.parse(readFileSync(resolved, "utf8"));
|
|
889
|
+
} catch {
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
893
|
+
const obj = parsed;
|
|
894
|
+
const options = obj.options;
|
|
895
|
+
if (options && typeof options === "object") {
|
|
896
|
+
const transform = options.transform;
|
|
897
|
+
if (Array.isArray(transform)) {
|
|
898
|
+
for (const t of transform) {
|
|
899
|
+
if (isTryAsTransformSpec(t)) return true;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const extendsField = obj.extends;
|
|
904
|
+
const extendsList = Array.isArray(extendsField)
|
|
905
|
+
? extendsField
|
|
906
|
+
: typeof extendsField === "string"
|
|
907
|
+
? [extendsField]
|
|
908
|
+
: [];
|
|
909
|
+
for (const ext of extendsList) {
|
|
910
|
+
if (typeof ext !== "string") continue;
|
|
911
|
+
const extPath = path.isAbsolute(ext)
|
|
912
|
+
? ext
|
|
913
|
+
: path.resolve(path.dirname(resolved), ext);
|
|
914
|
+
if (asconfigDeclaresTryAs(extPath, seen)) return true;
|
|
915
|
+
}
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
711
918
|
function resolveWasiShim() {
|
|
712
919
|
const resolved = resolveProjectModule(
|
|
713
920
|
"@assemblyscript/wasi-shim/asconfig.json",
|
package/bin/commands/build.js
CHANGED
|
@@ -5,6 +5,8 @@ export {
|
|
|
5
5
|
formatInvocation,
|
|
6
6
|
getBuildInvocationPreview,
|
|
7
7
|
getBuildReuseInfo,
|
|
8
|
+
warnOnUnknownModeReferences,
|
|
9
|
+
flushModeWarnings,
|
|
8
10
|
} from "./build-core.js";
|
|
9
11
|
export async function executeBuildCommand(
|
|
10
12
|
rawArgs,
|
|
@@ -17,8 +19,8 @@ export async function executeBuildCommand(
|
|
|
17
19
|
const featureToggles = deps.resolveFeatureToggles(rawArgs, "build");
|
|
18
20
|
const parallel = deps.resolveBuildParallelJobs(rawArgs);
|
|
19
21
|
const buildFeatureToggles = {
|
|
20
|
-
tryAs: featureToggles.tryAs,
|
|
21
22
|
coverage: featureToggles.coverage,
|
|
23
|
+
featureOverrides: featureToggles.featureOverrides,
|
|
22
24
|
};
|
|
23
25
|
const modeTargets = deps.resolveExecutionModes(configPath, selectedModes);
|
|
24
26
|
if (listFlags.list || listFlags.listModes) {
|