as-test 1.1.10 → 1.3.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,5 @@
1
- import { existsSync, readFileSync } from "fs";
1
+ import { existsSync, mkdirSync, readFileSync } from "fs";
2
+ import { INTERNAL_FEATURE_NAMES, normalizeFeatureName } from "../types.js";
2
3
  import { glob } from "glob";
3
4
  import chalk from "chalk";
4
5
  import { spawn } from "child_process";
@@ -11,6 +12,8 @@ import {
11
12
  applyMode,
12
13
  getPkgRunner,
13
14
  loadConfig,
15
+ resolveArtifactPath,
16
+ resolveSpecRelativePath,
14
17
  tokenizeCommand,
15
18
  resolveProjectModule,
16
19
  } from "../util.js";
@@ -64,11 +67,8 @@ export async function build(
64
67
  const inputFiles = (await glob(inputPatterns)).sort((a, b) =>
65
68
  a.localeCompare(b),
66
69
  );
67
- // Disambiguation must consider the entire configured input set, not just the
68
- // selector-filtered subset, otherwise running `ast test <one-spec>` writes
69
- // an artifact name the runner won't look up (it always globs full input).
70
- const duplicateSpecBasenames =
71
- await resolveAllConfiguredDuplicateBasenames(sourceInputPatterns);
70
+ await assertNoArtifactCollisions(sourceInputPatterns);
71
+ warnOnUnknownModeReferences(inputFiles, loadedConfig.modes ?? {});
72
72
  const coverageEnabled = resolveCoverageEnabled(
73
73
  config.coverage,
74
74
  featureToggles.coverage,
@@ -77,6 +77,7 @@ export async function build(
77
77
  ...mode.env,
78
78
  ...config.buildOptions.env,
79
79
  AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
80
+ AS_TEST_MODE_NAME: modeName ?? "default",
80
81
  };
81
82
  if (
82
83
  !resolvedConfig &&
@@ -85,7 +86,10 @@ export async function build(
85
86
  ) {
86
87
  const pool = getSerialBuildWorkerPool();
87
88
  for (const file of inputFiles) {
88
- const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
89
+ const outFile = path.join(
90
+ config.outDir,
91
+ resolveArtifactPath(file, sourceInputPatterns),
92
+ );
89
93
  const invocation = getBuildCommand(
90
94
  config,
91
95
  pkgRunner,
@@ -106,7 +110,11 @@ export async function build(
106
110
  return;
107
111
  }
108
112
  for (const file of inputFiles) {
109
- const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
113
+ const outFile = path.join(
114
+ config.outDir,
115
+ resolveArtifactPath(file, sourceInputPatterns),
116
+ );
117
+ mkdirSync(path.dirname(outFile), { recursive: true });
110
118
  const invocation = getBuildCommand(
111
119
  config,
112
120
  pkgRunner,
@@ -127,6 +135,10 @@ export async function build(
127
135
  kind,
128
136
  stage: "build",
129
137
  file,
138
+ entryKey: resolveSpecRelativePath(file, sourceInputPatterns).replace(
139
+ /\.ts$/i,
140
+ "",
141
+ ),
130
142
  mode: modeLabel,
131
143
  cwd: process.cwd(),
132
144
  buildCommand,
@@ -189,9 +201,10 @@ export async function getBuildInvocationPreview(
189
201
  }
190
202
  const sourceInputPatterns =
191
203
  overrides.kind === "fuzz" ? config.fuzz.input : config.input;
192
- const duplicateSpecBasenames =
193
- await resolveAllConfiguredDuplicateBasenames(sourceInputPatterns);
194
- const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
204
+ const outFile = path.join(
205
+ config.outDir,
206
+ resolveArtifactPath(file, sourceInputPatterns),
207
+ );
195
208
  return getBuildCommand(
196
209
  config,
197
210
  getPkgRunner(),
@@ -229,9 +242,10 @@ export async function getBuildReuseInfo(
229
242
  }
230
243
  const sourceInputPatterns =
231
244
  overrides.kind === "fuzz" ? config.fuzz.input : config.input;
232
- const duplicateSpecBasenames =
233
- await resolveAllConfiguredDuplicateBasenames(sourceInputPatterns);
234
- const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
245
+ const outFile = path.join(
246
+ config.outDir,
247
+ resolveArtifactPath(file, sourceInputPatterns),
248
+ );
235
249
  const invocation = getBuildCommand(
236
250
  config,
237
251
  getPkgRunner(),
@@ -248,6 +262,7 @@ export async function getBuildReuseInfo(
248
262
  ...mode.env,
249
263
  ...config.buildOptions.env,
250
264
  AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
265
+ AS_TEST_MODE_NAME: modeName ?? "default",
251
266
  };
252
267
  return {
253
268
  signature: JSON.stringify({
@@ -262,6 +277,129 @@ export async function getBuildReuseInfo(
262
277
  function hasCustomBuildCommand(config) {
263
278
  return !!config.buildOptions.cmd.trim().length;
264
279
  }
280
+ // Scans input spec files for `mode([...], fn)` calls whose entries reference
281
+ // mode names not present in the configured set. Collects all (file, name)
282
+ // hits and prints a single formatted block to stdout. The implicit "default"
283
+ // name is only valid when no configured mode has `default: true` — otherwise
284
+ // a named mode always runs and `AS_TEST_MODE_NAME` is never literal "default".
285
+ const MODE_CALL_RE = /\bmode\s*\(\s*\[([^\]]*)\]/g;
286
+ const MODE_STRING_RE = /["']([^"']*)["']/g;
287
+ const STRIP_COMMENTS_RE = /\/\*[\s\S]*?\*\/|\/\/.*$/gm;
288
+ const reportedModeWarnings = new Set();
289
+ const pendingModeWarningsByFile = new Map();
290
+ // Scans input spec files for `mode([...], fn)` calls whose entries reference
291
+ // mode names not present in the configured set, and buffers them for later
292
+ // printing via flushModeWarnings(). Called as early as possible (before the
293
+ // reporter starts streaming progress). De-duplicates across invocations.
294
+ export function warnOnUnknownModeReferences(files, configuredModes) {
295
+ const modeEntries = Object.entries(configuredModes ?? {});
296
+ const fallsBackToImplicitDefault =
297
+ modeEntries.length === 0 ||
298
+ modeEntries.every(([, mode]) => mode?.default === false);
299
+ const knownModes = new Set(modeEntries.map(([name]) => name));
300
+ if (fallsBackToImplicitDefault) knownModes.add("default");
301
+ const knownList = [...knownModes].sort();
302
+ for (const file of files) {
303
+ let text;
304
+ try {
305
+ text = readFileSync(file, "utf8");
306
+ } catch {
307
+ continue;
308
+ }
309
+ text = text.replace(STRIP_COMMENTS_RE, "");
310
+ for (const callMatch of text.matchAll(MODE_CALL_RE)) {
311
+ const arrayContents = callMatch[1] ?? "";
312
+ for (const strMatch of arrayContents.matchAll(MODE_STRING_RE)) {
313
+ let value = strMatch[1] ?? "";
314
+ if (value.length === 0) continue;
315
+ if (value.charCodeAt(0) === 33 /* '!' */) value = value.slice(1);
316
+ if (value.length === 0) continue;
317
+ if (knownModes.has(value)) continue;
318
+ const key = `${file}\x1f${value}`;
319
+ if (reportedModeWarnings.has(key)) continue;
320
+ reportedModeWarnings.add(key);
321
+ const warning = {
322
+ name: value,
323
+ suggestion: closestKnownMode(value, knownList),
324
+ };
325
+ const list = pendingModeWarningsByFile.get(file);
326
+ if (list) list.push(warning);
327
+ else pendingModeWarningsByFile.set(file, [warning]);
328
+ }
329
+ }
330
+ }
331
+ }
332
+ // Drains buffered mode warnings. When `showAll` is true, prints the full
333
+ // per-warning block; otherwise prints a one-line summary that tells the user
334
+ // to re-run with `--show-warnings`. No-op when there are no warnings.
335
+ export function flushModeWarnings(showAll) {
336
+ if (pendingModeWarningsByFile.size === 0) return;
337
+ const hits = [];
338
+ for (const [file, list] of pendingModeWarningsByFile) {
339
+ for (const w of list) {
340
+ hits.push({ file, name: w.name, suggestion: w.suggestion });
341
+ }
342
+ }
343
+ pendingModeWarningsByFile.clear();
344
+ if (hits.length === 0) return;
345
+ if (!showAll) {
346
+ const count = hits.length;
347
+ const noun = count === 1 ? "warning" : "warnings";
348
+ process.stdout.write(
349
+ `\nFound ${chalk.yellow.bold(count)} ${noun}. Run with ${chalk.dim("--show-warnings")} to view.\n`,
350
+ );
351
+ return;
352
+ }
353
+ const lines = [chalk.yellow.bold("WARNINGS:")];
354
+ for (const hit of hits) {
355
+ let line = ` - unknown mode reference ${chalk.bold(`"${hit.name}"`)} in ${chalk.dim(hit.file)}`;
356
+ if (hit.suggestion) {
357
+ line += ` - did you mean ${chalk.cyan(`"${hit.suggestion}"`)}?`;
358
+ }
359
+ lines.push(line);
360
+ }
361
+ process.stdout.write("\n" + lines.join("\n") + "\n");
362
+ }
363
+ // Returns the configured mode whose Levenshtein distance to `name` is below
364
+ // a small threshold (proportional to the longer string's length). Returns
365
+ // null when nothing's close enough — avoids suggesting wildly different names.
366
+ function closestKnownMode(name, candidates) {
367
+ let best = null;
368
+ let bestDist = Infinity;
369
+ for (const candidate of candidates) {
370
+ const d = levenshtein(name, candidate);
371
+ const threshold = Math.max(
372
+ 2,
373
+ Math.floor(Math.max(name.length, candidate.length) * 0.4),
374
+ );
375
+ if (d < bestDist && d <= threshold) {
376
+ bestDist = d;
377
+ best = candidate;
378
+ }
379
+ }
380
+ return best;
381
+ }
382
+ function levenshtein(a, b) {
383
+ const m = a.length;
384
+ const n = b.length;
385
+ if (m === 0) return n;
386
+ if (n === 0) return m;
387
+ let prev = new Array(n + 1);
388
+ let curr = new Array(n + 1);
389
+ for (let j = 0; j <= n; j++) prev[j] = j;
390
+ for (let i = 1; i <= m; i++) {
391
+ curr[0] = i;
392
+ for (let j = 1; j <= n; j++) {
393
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
394
+ const del = prev[j] + 1;
395
+ const ins = curr[j - 1] + 1;
396
+ const sub = prev[j - 1] + cost;
397
+ curr[j] = Math.min(del, ins, sub);
398
+ }
399
+ [prev, curr] = [curr, prev];
400
+ }
401
+ return prev[n];
402
+ }
265
403
  function getBuildCommand(
266
404
  config,
267
405
  pkgRunner,
@@ -356,54 +494,23 @@ function expandBuildCommand(template, file, outFile, target, modeName) {
356
494
  .replace(/<target>/g, target)
357
495
  .replace(/<mode>/g, modeName ?? "");
358
496
  }
359
- function resolveArtifactFileName(
360
- file,
361
- target,
362
- modeName,
363
- duplicateSpecBasenames = new Set(),
364
- ) {
365
- const base = path
366
- .basename(file)
367
- .replace(/\.spec\.ts$/, "")
368
- .replace(/\.ts$/, "");
369
- const legacy = !modeName
370
- ? `${path.basename(file).replace(".ts", ".wasm")}`
371
- : `${base}.${modeName}.${target}.wasm`;
372
- if (!duplicateSpecBasenames.has(path.basename(file))) {
373
- return legacy;
374
- }
375
- const disambiguator = resolveDisambiguator(file);
376
- if (!disambiguator.length) {
377
- return legacy;
378
- }
379
- const ext = path.extname(legacy);
380
- const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
381
- return `${stem}.${disambiguator}${ext}`;
382
- }
383
- async function resolveAllConfiguredDuplicateBasenames(configured) {
497
+ async function assertNoArtifactCollisions(configured) {
384
498
  const patterns = Array.isArray(configured) ? configured : [configured];
385
499
  const files = await glob(patterns);
386
- return resolveDuplicateBasenames(files);
387
- }
388
- function resolveDuplicateBasenames(files) {
389
- const counts = new Map();
500
+ const seen = new Map();
390
501
  for (const file of files) {
391
- const base = path.basename(file);
392
- counts.set(base, (counts.get(base) ?? 0) + 1);
393
- }
394
- const duplicates = new Set();
395
- for (const [base, count] of counts) {
396
- if (count > 1) duplicates.add(base);
502
+ const artifact = resolveArtifactPath(file, patterns);
503
+ const prev = seen.get(artifact);
504
+ if (prev != null && prev !== file) {
505
+ throw new Error(
506
+ `Two input files resolve to the same artifact path "${artifact}":\n` +
507
+ ` - ${prev}\n` +
508
+ ` - ${file}\n` +
509
+ `Rename one of them or narrow the input patterns to disambiguate.`,
510
+ );
511
+ }
512
+ seen.set(artifact, file);
397
513
  }
398
- return duplicates;
399
- }
400
- function resolveDisambiguator(file) {
401
- const relDir = path.dirname(path.relative(process.cwd(), file));
402
- if (!relDir.length || relDir == ".") return "";
403
- return relDir
404
- .replace(/[\\/]+/g, "__")
405
- .replace(/[^A-Za-z0-9._-]/g, "_")
406
- .replace(/^_+|_+$/g, "");
407
514
  }
408
515
  function resolveInputPatterns(configured, selectors) {
409
516
  const configuredInputs = Array.isArray(configured)
@@ -590,7 +697,8 @@ function getBuildStdout(error) {
590
697
  }
591
698
  function getDefaultBuildArgs(config, featureToggles) {
592
699
  const buildArgs = [];
593
- const tryAsEnabled = resolveTryAsEnabled(featureToggles.tryAs);
700
+ const effectiveFeatures = resolveEffectiveFeatures(config, featureToggles);
701
+ const tryAsEnabled = resolveTryAsEnabled(effectiveFeatures.has("try-as"));
594
702
  buildArgs.push("--transform", "as-test/transform");
595
703
  if (
596
704
  resolveProjectModule("json-as/transform") &&
@@ -607,6 +715,10 @@ function getDefaultBuildArgs(config, featureToggles) {
607
715
  if (tryAsEnabled) {
608
716
  buildArgs.push("--use", "AS_TEST_TRY_AS=1");
609
717
  }
718
+ for (const feature of effectiveFeatures) {
719
+ if (INTERNAL_FEATURE_NAMES.has(feature)) continue;
720
+ buildArgs.push("--enable", feature);
721
+ }
610
722
  // Should also strip any bindings-enabling from asconfig
611
723
  if (
612
724
  config.buildOptions.target == "bindings" ||
@@ -705,16 +817,29 @@ function transformsContainJsonAs(value) {
705
817
  }
706
818
  return false;
707
819
  }
708
- function resolveTryAsEnabled(override) {
709
- const installed = hasTryAsRuntime();
710
- if (override === false) return false;
711
- if (override === true && !installed) {
820
+ function resolveEffectiveFeatures(config, featureToggles) {
821
+ const effective = new Set();
822
+ for (const name of config.features) {
823
+ effective.add(normalizeFeatureName(name));
824
+ }
825
+ const overrides = featureToggles.featureOverrides ?? {};
826
+ for (const [name, enabled] of Object.entries(overrides)) {
827
+ const key = normalizeFeatureName(name);
828
+ if (!key.length) continue;
829
+ if (enabled) effective.add(key);
830
+ else effective.delete(key);
831
+ }
832
+ effective.delete("");
833
+ return effective;
834
+ }
835
+ function resolveTryAsEnabled(enabled) {
836
+ if (!enabled) return false;
837
+ if (!hasTryAsRuntime()) {
712
838
  throw new Error(
713
839
  'try-as feature was enabled, but package "try-as" is not installed',
714
840
  );
715
841
  }
716
- if (override === true) return true;
717
- return false;
842
+ return true;
718
843
  }
719
844
  function resolveCoverageEnabled(rawCoverage, override) {
720
845
  if (override != undefined) return override;
@@ -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) {
@@ -3,7 +3,12 @@ import * as path from "path";
3
3
  import { pathToFileURL } from "url";
4
4
  import { glob } from "glob";
5
5
  import { build } from "./build-core.js";
6
- import { applyMode, loadConfig } from "../util.js";
6
+ import {
7
+ applyMode,
8
+ loadConfig,
9
+ resolveArtifactPath,
10
+ resolveSpecRelativePath,
11
+ } from "../util.js";
7
12
  import { persistCrashRecord } from "../crash-store.js";
8
13
  const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
9
14
  const MAGIC = Buffer.from("WIPC");
@@ -29,13 +34,6 @@ export async function fuzz(
29
34
  `No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`,
30
35
  );
31
36
  }
32
- // Disambiguation must consider the full configured fuzz input set, not the
33
- // selector-filtered subset, so artifact names stay consistent across runs.
34
- const fullPatterns = Array.isArray(config.input)
35
- ? config.input
36
- : [config.input];
37
- const allFuzzFiles = await glob(fullPatterns);
38
- const duplicateBasenames = resolveDuplicateBasenames(allFuzzFiles);
39
37
  const results = [];
40
38
  for (const file of inputFiles) {
41
39
  const buildStartedAt = Date.now();
@@ -53,7 +51,6 @@ export async function fuzz(
53
51
  await runFuzzTarget(
54
52
  file,
55
53
  activeConfig.outDir,
56
- duplicateBasenames,
57
54
  config,
58
55
  fuzzerSelectors,
59
56
  buildStartedAt,
@@ -111,7 +108,6 @@ function encodeRunsOverrideKind(kind) {
111
108
  async function runFuzzTarget(
112
109
  file,
113
110
  outDir,
114
- duplicateBasenames,
115
111
  config,
116
112
  fuzzerSelectors,
117
113
  buildStartedAt,
@@ -120,7 +116,7 @@ async function runFuzzTarget(
120
116
  modeName,
121
117
  ) {
122
118
  const startedAt = Date.now();
123
- const artifact = resolveArtifactFileName(file, duplicateBasenames, modeName);
119
+ const artifact = resolveArtifactPath(file, config.input);
124
120
  const wasmPath = path.resolve(process.cwd(), outDir, artifact);
125
121
  const jsPath = resolveBindingsHelperPath(wasmPath);
126
122
  const helper = await import(pathToFileURL(jsPath).href + `?t=${Date.now()}`);
@@ -185,7 +181,11 @@ async function runFuzzTarget(
185
181
  const crash = persistCrashRecord(config.crashDir, {
186
182
  kind: "fuzz",
187
183
  file,
188
- entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
184
+ entryKey: buildFuzzCrashEntryKey(
185
+ file,
186
+ config.input,
187
+ modeName ?? "default",
188
+ ),
189
189
  mode: modeName ?? "default",
190
190
  seed: config.seed,
191
191
  error: crashMessage,
@@ -232,7 +232,11 @@ async function runFuzzTarget(
232
232
  const crash = persistCrashRecord(config.crashDir, {
233
233
  kind: "fuzz",
234
234
  file,
235
- entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
235
+ entryKey: buildFuzzCrashEntryKey(
236
+ file,
237
+ config.input,
238
+ modeName ?? "default",
239
+ ),
236
240
  mode: modeName ?? "default",
237
241
  seed: config.seed,
238
242
  error: `${reportParseError ? `invalid fuzz report payload: ${reportParseError}` : `missing fuzz report payload from ${path.basename(file)}`} (${diagnostics})`,
@@ -269,6 +273,7 @@ async function runFuzzTarget(
269
273
  file,
270
274
  entryKey: buildFuzzFailureEntryKey(
271
275
  file,
276
+ config.input,
272
277
  fuzzer.name,
273
278
  modeName ?? "default",
274
279
  ),
@@ -341,11 +346,19 @@ function buildFuzzReproCommand(file, seed, modeName, fuzzer, runs) {
341
346
  const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
342
347
  return `ast fuzz ${file}${modeArg}${fuzzerArg} --seed ${seed}${runsArg}`;
343
348
  }
344
- function buildFuzzFailureEntryKey(file, name, modeName) {
345
- return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}.${sanitizeEntryName(name)}`;
349
+ function buildFuzzFailureEntryKey(file, inputPatterns, name, modeName) {
350
+ const stem = resolveSpecRelativePath(file, inputPatterns).replace(
351
+ /\.ts$/i,
352
+ "",
353
+ );
354
+ return `${stem}.${sanitizeEntryName(modeName)}.${sanitizeEntryName(name)}`;
346
355
  }
347
- function buildFuzzCrashEntryKey(file, modeName) {
348
- return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}`;
356
+ function buildFuzzCrashEntryKey(file, inputPatterns, modeName) {
357
+ const stem = resolveSpecRelativePath(file, inputPatterns).replace(
358
+ /\.ts$/i,
359
+ "",
360
+ );
361
+ return `${stem}.${sanitizeEntryName(modeName)}`;
349
362
  }
350
363
  function sanitizeEntryName(name) {
351
364
  return (
@@ -458,45 +471,6 @@ function resolveFuzzInputPatterns(configured, selectors) {
458
471
  }
459
472
  return [...patterns];
460
473
  }
461
- function resolveArtifactFileName(file, duplicateBasenames, modeName) {
462
- const base = path
463
- .basename(file)
464
- .replace(/\.spec\.ts$/, "")
465
- .replace(/\.ts$/, "");
466
- const legacy = !modeName
467
- ? `${path.basename(file).replace(".ts", ".wasm")}`
468
- : `${base}.${modeName}.bindings.wasm`;
469
- if (!duplicateBasenames.has(path.basename(file))) {
470
- return legacy;
471
- }
472
- const disambiguator = resolveDisambiguator(file);
473
- if (!disambiguator.length) {
474
- return legacy;
475
- }
476
- const ext = path.extname(legacy);
477
- const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
478
- return `${stem}.${disambiguator}${ext}`;
479
- }
480
- function resolveDuplicateBasenames(files) {
481
- const counts = new Map();
482
- for (const file of files) {
483
- const base = path.basename(file);
484
- counts.set(base, (counts.get(base) ?? 0) + 1);
485
- }
486
- const duplicates = new Set();
487
- for (const [base, count] of counts) {
488
- if (count > 1) duplicates.add(base);
489
- }
490
- return duplicates;
491
- }
492
- function resolveDisambiguator(file) {
493
- const relDir = path.dirname(path.relative(process.cwd(), file));
494
- if (!relDir.length || relDir == ".") return "";
495
- return relDir
496
- .replace(/[\\/]+/g, "__")
497
- .replace(/[^A-Za-z0-9._-]/g, "_")
498
- .replace(/^_+|_+$/g, "");
499
- }
500
474
  function resolveBindingsHelperPath(wasmPath) {
501
475
  const bindingsPath = wasmPath.replace(/\.wasm$/, ".bindings.js");
502
476
  if (existsSync(bindingsPath)) return bindingsPath;