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.
@@ -1,4 +1,4 @@
1
- import { JSON } from "json-as/assembly";
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
- serialize(): string {
13
+ toJSON(): string {
14
14
  return (
15
15
  '{"order":' +
16
16
  this.order.toString() +
17
17
  ',"type":' +
18
- JSON.stringify<string>(this.type) +
18
+ escape(this.type) +
19
19
  ',"verdict":' +
20
- JSON.stringify<string>(this.verdict) +
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
- JSON.stringify<string>(this.instr) +
26
+ escape(this.instr) +
27
27
  ',"message":' +
28
- JSON.stringify<string>(this.message) +
28
+ escape(this.message) +
29
29
  ',"location":' +
30
- JSON.stringify<string>(this.location) +
30
+ escape(this.location) +
31
31
  "}"
32
32
  );
33
33
  }
@@ -1,7 +1,7 @@
1
- import { JSON } from "json-as/assembly";
1
+ import { escape } from "../src/stringify";
2
2
 
3
3
  function q(s: string): string {
4
- return JSON.stringify<string>(s);
4
+ return escape(s);
5
5
  }
6
6
 
7
7
  // @ts-ignore
@@ -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
  }
@@ -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
- try {
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 defaultArgs = getDefaultBuildArgs(config, featureToggles);
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
- const args = [...ascInvocation.args, file, ...userArgs, ...defaultArgs];
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
- if (process.env.AS_TEST_BUILD_API == "1" && invocation.apiArgs?.length) {
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 result = await ascMain(args, { stdout, stderr });
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(config, featureToggles) {
764
+ function getDefaultBuildArgs(
765
+ config,
766
+ featureToggles,
767
+ tryAsAlreadyConfigured = false,
768
+ ) {
572
769
  const buildArgs = [];
573
- const tryAsEnabled = resolveTryAsEnabled(featureToggles.tryAs);
574
- buildArgs.push("--transform", "as-test/transform");
575
- if (
576
- resolveProjectModule("json-as/transform") &&
577
- !userSuppliesJsonAsTransform(config)
578
- ) {
579
- buildArgs.push("--transform", "json-as/transform");
580
- }
581
- if (tryAsEnabled) {
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
- // Treats anything whose path contains a "json-as" path component as a
621
- // user-supplied json-as transform. Matches bare specifiers, subpath specifiers,
622
- // absolute paths, and ./node_modules paths.
623
- const JSON_AS_TRANSFORM_PATTERN = /(?:^|[\\/])json-as(?:[\\/@]|$)/;
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
- if (config.config && config.config !== "none" && existsSync(config.config)) {
640
- if (asconfigDeclaresJsonAs(config.config, new Set())) return true;
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
- return false;
836
+ effective.delete("");
837
+ return effective;
643
838
  }
644
- function asconfigDeclaresJsonAs(configFile, seen) {
645
- const resolved = path.resolve(configFile);
646
- if (seen.has(resolved)) return false;
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
- if (override === true) return true;
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",
@@ -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) {