as-test 1.1.6 → 1.1.7

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +4 -9
  3. package/assembly/index.ts +10 -15
  4. package/assembly/src/expectation.ts +11 -11
  5. package/assembly/src/fuzz.ts +11 -7
  6. package/assembly/src/log.ts +2 -2
  7. package/assembly/src/suite.ts +5 -5
  8. package/assembly/src/tests.ts +8 -8
  9. package/assembly/util/wipc.ts +5 -1
  10. package/bin/build-worker-pool.js +146 -142
  11. package/bin/build-worker.js +37 -34
  12. package/bin/commands/build-core.js +577 -465
  13. package/bin/commands/build.js +49 -29
  14. package/bin/commands/clean-core.js +120 -113
  15. package/bin/commands/clean.js +14 -8
  16. package/bin/commands/doctor-core.js +288 -289
  17. package/bin/commands/doctor.js +1 -1
  18. package/bin/commands/fuzz-core.js +467 -414
  19. package/bin/commands/fuzz.js +27 -10
  20. package/bin/commands/init-core.js +905 -794
  21. package/bin/commands/init.js +2 -2
  22. package/bin/commands/run-core.js +2675 -2344
  23. package/bin/commands/run.js +43 -25
  24. package/bin/commands/test.js +56 -32
  25. package/bin/commands/web-runner-source.js +1 -1
  26. package/bin/commands/web-session.js +516 -525
  27. package/bin/coverage-points.js +363 -341
  28. package/bin/crash-store.js +56 -66
  29. package/bin/index.js +4092 -3150
  30. package/bin/reporters/default.js +1090 -890
  31. package/bin/reporters/tap.js +319 -325
  32. package/bin/types.js +67 -67
  33. package/bin/util.js +1290 -1239
  34. package/bin/wipc.js +70 -73
  35. package/lib/build/index.d.ts +3 -1
  36. package/lib/build/index.js +1039 -1034
  37. package/lib/build/web-runner/client.js +1 -1
  38. package/lib/build/web-runner/html.js +1 -1
  39. package/lib/build/web-runner/worker.js +1 -1
  40. package/package.json +6 -3
  41. package/transform/lib/log.js +9 -5
  42. package/assembly/util/json.ts +0 -112
package/bin/util.js CHANGED
@@ -1,176 +1,213 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
- import { BuildOptions, Config, CoverageOptions, CoverageIgnoreOptions, FuzzConfig, ModeConfig, ReporterConfig, RunOptions, Runtime, } from "./types.js";
2
+ import {
3
+ BuildOptions,
4
+ Config,
5
+ CoverageOptions,
6
+ CoverageIgnoreOptions,
7
+ FuzzConfig,
8
+ ModeConfig,
9
+ ReporterConfig,
10
+ RunOptions,
11
+ Runtime,
12
+ } from "./types.js";
3
13
  import chalk from "chalk";
4
14
  import { createRequire } from "module";
5
- import { basename, delimiter, dirname, join, relative, resolve, sep, } from "path";
15
+ import {
16
+ basename,
17
+ delimiter,
18
+ dirname,
19
+ join,
20
+ relative,
21
+ resolve,
22
+ sep,
23
+ } from "path";
6
24
  import { fileURLToPath } from "url";
7
25
  const CONFIG_META = new WeakMap();
8
26
  export function formatTime(ms) {
9
- if (ms < 0) {
10
- throw new Error("Time should be a non-negative number.");
11
- }
12
- // Convert milliseconds to microseconds
13
- const us = ms * 1000;
14
- const units = [
15
- { name: "μs", divisor: 1 },
16
- { name: "ms", divisor: 1000 },
17
- { name: "s", divisor: 1000 * 1000 },
18
- { name: "m", divisor: 60 * 1000 * 1000 },
19
- { name: "h", divisor: 60 * 60 * 1000 * 1000 },
20
- { name: "d", divisor: 24 * 60 * 60 * 1000 * 1000 },
21
- ];
22
- for (let i = units.length - 1; i >= 0; i--) {
23
- const unit = units[i];
24
- if (us >= unit.divisor) {
25
- const value = Math.round((us / unit.divisor) * 1000) / 1000;
26
- return `${value}${unit.name}`;
27
- }
28
- }
29
- return `${us}us`;
27
+ if (ms < 0) {
28
+ throw new Error("Time should be a non-negative number.");
29
+ }
30
+ // Convert milliseconds to microseconds
31
+ const us = ms * 1000;
32
+ const units = [
33
+ { name: "μs", divisor: 1 },
34
+ { name: "ms", divisor: 1000 },
35
+ { name: "s", divisor: 1000 * 1000 },
36
+ { name: "m", divisor: 60 * 1000 * 1000 },
37
+ { name: "h", divisor: 60 * 60 * 1000 * 1000 },
38
+ { name: "d", divisor: 24 * 60 * 60 * 1000 * 1000 },
39
+ ];
40
+ for (let i = units.length - 1; i >= 0; i--) {
41
+ const unit = units[i];
42
+ if (us >= unit.divisor) {
43
+ const value = Math.round((us / unit.divisor) * 1000) / 1000;
44
+ return `${value}${unit.name}`;
45
+ }
46
+ }
47
+ return `${us}us`;
30
48
  }
31
49
  export function formatSpecDisplayPath(file) {
32
- const resolved = resolve(file);
33
- const parts = resolved.split(/[/\\]+/);
34
- const testsIndex = parts.lastIndexOf("__tests__");
35
- if (testsIndex >= 0 && testsIndex < parts.length - 1) {
36
- return parts.slice(testsIndex + 1).join("/");
37
- }
38
- const rel = relative(process.cwd(), resolved).split(sep).join("/");
39
- if (rel.length && rel != "." && rel != ".." && !rel.startsWith("../")) {
40
- return rel;
41
- }
42
- return basename(resolved);
50
+ const resolved = resolve(file);
51
+ const parts = resolved.split(/[/\\]+/);
52
+ const testsIndex = parts.lastIndexOf("__tests__");
53
+ if (testsIndex >= 0 && testsIndex < parts.length - 1) {
54
+ return parts.slice(testsIndex + 1).join("/");
55
+ }
56
+ const rel = relative(process.cwd(), resolved).split(sep).join("/");
57
+ if (rel.length && rel != "." && rel != ".." && !rel.startsWith("../")) {
58
+ return rel;
59
+ }
60
+ return basename(resolved);
43
61
  }
44
62
  export function loadConfig(CONFIG_PATH, warn = false) {
45
- const resolvedPath = resolve(CONFIG_PATH);
46
- const raw = readConfigRaw(resolvedPath, warn);
47
- return parseConfigRaw(raw, resolvedPath);
63
+ const resolvedPath = resolve(CONFIG_PATH);
64
+ const raw = readConfigRaw(resolvedPath, warn);
65
+ return parseConfigRaw(raw, resolvedPath);
48
66
  }
49
67
  function readConfigRaw(configPath, warn) {
50
- if (!existsSync(configPath)) {
51
- if (warn) {
52
- console.log(`${chalk.bgMagentaBright(" WARN ")}${chalk.dim(":")} Could not locate config file in the current directory! Continuing with default config.`);
53
- }
54
- return {};
55
- }
56
- const rawText = readFileSync(configPath, "utf8");
57
- let parsed;
58
- try {
59
- parsed = JSON.parse(rawText);
60
- }
61
- catch (error) {
62
- const message = error instanceof Error ? error.message : String(error);
63
- throw new Error(`invalid config JSON at ${configPath}\n${message}\nfix JSON syntax and rerun.`);
64
- }
65
- if (!parsed || typeof parsed != "object" || Array.isArray(parsed)) {
66
- throw new Error(`invalid config at ${configPath}\nroot value must be an object. Example: { "input": ["./assembly/__tests__/*.spec.ts"] }`);
67
- }
68
- const raw = parsed;
69
- validateConfig(raw, configPath);
70
- return raw;
68
+ if (!existsSync(configPath)) {
69
+ if (warn) {
70
+ console.log(
71
+ `${chalk.bgMagentaBright(" WARN ")}${chalk.dim(":")} Could not locate config file in the current directory! Continuing with default config.`,
72
+ );
73
+ }
74
+ return {};
75
+ }
76
+ const rawText = readFileSync(configPath, "utf8");
77
+ let parsed;
78
+ try {
79
+ parsed = JSON.parse(rawText);
80
+ } catch (error) {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ throw new Error(
83
+ `invalid config JSON at ${configPath}\n${message}\nfix JSON syntax and rerun.`,
84
+ );
85
+ }
86
+ if (!parsed || typeof parsed != "object" || Array.isArray(parsed)) {
87
+ throw new Error(
88
+ `invalid config at ${configPath}\nroot value must be an object. Example: { "input": ["./assembly/__tests__/*.spec.ts"] }`,
89
+ );
90
+ }
91
+ const raw = parsed;
92
+ validateConfig(raw, configPath);
93
+ return raw;
71
94
  }
72
95
  function parseConfigRaw(raw, configPath) {
73
- const configDir = dirname(configPath);
74
- const config = Object.assign(new Config(), raw);
75
- applyOutputConfig(raw.output, raw, config);
76
- config.env = parseEnvValue(raw.env, configDir, "$.env");
77
- const runOptionsRaw = raw.runOptions ?? {};
78
- config.buildOptions = Object.assign(new BuildOptions(), raw.buildOptions ?? {});
79
- config.buildOptions.cmd =
80
- typeof config.buildOptions.cmd == "string" ? config.buildOptions.cmd : "";
81
- config.buildOptions.args = Array.isArray(config.buildOptions.args)
82
- ? config.buildOptions.args.filter((item) => typeof item == "string")
83
- : [];
84
- config.buildOptions.env = parseEnvValue(raw.buildOptions?.env, configDir, "$.buildOptions.env");
85
- config.buildOptions.target =
86
- typeof config.buildOptions.target == "string" &&
87
- config.buildOptions.target.length
88
- ? config.buildOptions.target
89
- : "wasi";
90
- config.runOptions = Object.assign(new RunOptions(), runOptionsRaw);
91
- const reporterRaw = runOptionsRaw.reporter;
92
- if (typeof reporterRaw == "string") {
93
- config.runOptions.reporter = reporterRaw;
94
- }
95
- else if (reporterRaw && typeof reporterRaw == "object") {
96
- const reporterConfig = Object.assign(new ReporterConfig(), reporterRaw);
97
- reporterConfig.name =
98
- typeof reporterConfig.name == "string" ? reporterConfig.name : "";
99
- reporterConfig.options = Array.isArray(reporterConfig.options)
100
- ? reporterConfig.options.filter((value) => typeof value == "string")
101
- : [];
102
- reporterConfig.outDir =
103
- typeof reporterConfig.outDir == "string" ? reporterConfig.outDir : "";
104
- reporterConfig.outFile =
105
- typeof reporterConfig.outFile == "string" ? reporterConfig.outFile : "";
106
- config.runOptions.reporter = reporterConfig;
107
- }
108
- else {
109
- config.runOptions.reporter = "";
110
- }
111
- const runtimeRaw = runOptionsRaw.runtime;
112
- const runtime = new Runtime();
113
- const legacyRun = typeof runOptionsRaw.run == "string" && runOptionsRaw.run.length
114
- ? runOptionsRaw.run
115
- : "";
116
- const cmd = runtimeRaw && typeof runtimeRaw.cmd == "string" && runtimeRaw.cmd.length
117
- ? runtimeRaw.cmd
118
- : runtimeRaw && typeof runtimeRaw.run == "string" && runtimeRaw.run.length
119
- ? runtimeRaw.run
120
- : legacyRun
121
- ? legacyRun
122
- : runtime.cmd;
123
- runtime.cmd = cmd;
124
- runtime.browser =
125
- runtimeRaw && typeof runtimeRaw.browser == "string"
126
- ? runtimeRaw.browser
127
- : "";
128
- config.runOptions.runtime = runtime;
129
- config.runOptions.env = parseEnvValue(runOptionsRaw.env, configDir, "$.runOptions.env");
130
- const fuzzRaw = raw.fuzz ?? {};
131
- config.fuzz = Object.assign(new FuzzConfig(), fuzzRaw);
132
- config.fuzz.input = Array.isArray(config.fuzz.input)
133
- ? config.fuzz.input.filter((item) => typeof item == "string")
134
- : typeof fuzzRaw.input == "string"
135
- ? [fuzzRaw.input]
136
- : new FuzzConfig().input;
137
- config.fuzz.runs = normalizePositiveNumber(config.fuzz.runs, 1000);
138
- config.fuzz.seed = normalizeNonNegativeNumber(config.fuzz.seed, -1);
139
- config.fuzz.maxInputBytes = normalizePositiveNumber(config.fuzz.maxInputBytes, 4096);
140
- config.fuzz.target =
141
- typeof config.fuzz.target == "string" && config.fuzz.target.length
142
- ? config.fuzz.target
143
- : "bindings";
144
- config.fuzz.corpusDir =
145
- typeof config.fuzz.corpusDir == "string" && config.fuzz.corpusDir.length
146
- ? config.fuzz.corpusDir
147
- : "./.as-test/fuzz/corpus";
148
- config.fuzz.crashDir =
149
- typeof config.fuzz.crashDir == "string" && config.fuzz.crashDir.length
150
- ? config.fuzz.crashDir
151
- : "./.as-test/crashes";
152
- config.modes = parseModes(raw.modes, configDir);
153
- CONFIG_META.set(config, {
154
- sourcePath: configPath,
155
- raw,
156
- });
157
- return config;
96
+ const configDir = dirname(configPath);
97
+ const config = Object.assign(new Config(), raw);
98
+ applyOutputConfig(raw.output, raw, config);
99
+ config.env = parseEnvValue(raw.env, configDir, "$.env");
100
+ const runOptionsRaw = raw.runOptions ?? {};
101
+ config.buildOptions = Object.assign(
102
+ new BuildOptions(),
103
+ raw.buildOptions ?? {},
104
+ );
105
+ config.buildOptions.cmd =
106
+ typeof config.buildOptions.cmd == "string" ? config.buildOptions.cmd : "";
107
+ config.buildOptions.args = Array.isArray(config.buildOptions.args)
108
+ ? config.buildOptions.args.filter((item) => typeof item == "string")
109
+ : [];
110
+ config.buildOptions.env = parseEnvValue(
111
+ raw.buildOptions?.env,
112
+ configDir,
113
+ "$.buildOptions.env",
114
+ );
115
+ config.buildOptions.target =
116
+ typeof config.buildOptions.target == "string" &&
117
+ config.buildOptions.target.length
118
+ ? config.buildOptions.target
119
+ : "wasi";
120
+ config.runOptions = Object.assign(new RunOptions(), runOptionsRaw);
121
+ const reporterRaw = runOptionsRaw.reporter;
122
+ if (typeof reporterRaw == "string") {
123
+ config.runOptions.reporter = reporterRaw;
124
+ } else if (reporterRaw && typeof reporterRaw == "object") {
125
+ const reporterConfig = Object.assign(new ReporterConfig(), reporterRaw);
126
+ reporterConfig.name =
127
+ typeof reporterConfig.name == "string" ? reporterConfig.name : "";
128
+ reporterConfig.options = Array.isArray(reporterConfig.options)
129
+ ? reporterConfig.options.filter((value) => typeof value == "string")
130
+ : [];
131
+ reporterConfig.outDir =
132
+ typeof reporterConfig.outDir == "string" ? reporterConfig.outDir : "";
133
+ reporterConfig.outFile =
134
+ typeof reporterConfig.outFile == "string" ? reporterConfig.outFile : "";
135
+ config.runOptions.reporter = reporterConfig;
136
+ } else {
137
+ config.runOptions.reporter = "";
138
+ }
139
+ const runtimeRaw = runOptionsRaw.runtime;
140
+ const runtime = new Runtime();
141
+ const legacyRun =
142
+ typeof runOptionsRaw.run == "string" && runOptionsRaw.run.length
143
+ ? runOptionsRaw.run
144
+ : "";
145
+ const cmd =
146
+ runtimeRaw && typeof runtimeRaw.cmd == "string" && runtimeRaw.cmd.length
147
+ ? runtimeRaw.cmd
148
+ : runtimeRaw && typeof runtimeRaw.run == "string" && runtimeRaw.run.length
149
+ ? runtimeRaw.run
150
+ : legacyRun
151
+ ? legacyRun
152
+ : runtime.cmd;
153
+ runtime.cmd = cmd;
154
+ runtime.browser =
155
+ runtimeRaw && typeof runtimeRaw.browser == "string"
156
+ ? runtimeRaw.browser
157
+ : "";
158
+ config.runOptions.runtime = runtime;
159
+ config.runOptions.env = parseEnvValue(
160
+ runOptionsRaw.env,
161
+ configDir,
162
+ "$.runOptions.env",
163
+ );
164
+ const fuzzRaw = raw.fuzz ?? {};
165
+ config.fuzz = Object.assign(new FuzzConfig(), fuzzRaw);
166
+ config.fuzz.input = Array.isArray(config.fuzz.input)
167
+ ? config.fuzz.input.filter((item) => typeof item == "string")
168
+ : typeof fuzzRaw.input == "string"
169
+ ? [fuzzRaw.input]
170
+ : new FuzzConfig().input;
171
+ config.fuzz.runs = normalizePositiveNumber(config.fuzz.runs, 1000);
172
+ config.fuzz.seed = normalizeNonNegativeNumber(config.fuzz.seed, -1);
173
+ config.fuzz.maxInputBytes = normalizePositiveNumber(
174
+ config.fuzz.maxInputBytes,
175
+ 4096,
176
+ );
177
+ config.fuzz.target =
178
+ typeof config.fuzz.target == "string" && config.fuzz.target.length
179
+ ? config.fuzz.target
180
+ : "bindings";
181
+ config.fuzz.corpusDir =
182
+ typeof config.fuzz.corpusDir == "string" && config.fuzz.corpusDir.length
183
+ ? config.fuzz.corpusDir
184
+ : "./.as-test/fuzz/corpus";
185
+ config.fuzz.crashDir =
186
+ typeof config.fuzz.crashDir == "string" && config.fuzz.crashDir.length
187
+ ? config.fuzz.crashDir
188
+ : "./.as-test/crashes";
189
+ config.modes = parseModes(raw.modes, configDir);
190
+ CONFIG_META.set(config, {
191
+ sourcePath: configPath,
192
+ raw,
193
+ });
194
+ return config;
158
195
  }
159
196
  const TOP_LEVEL_KEYS = new Set([
160
- "$schema",
161
- "input",
162
- "output",
163
- "outDir",
164
- "logs",
165
- "coverageDir",
166
- "snapshotDir",
167
- "config",
168
- "coverage",
169
- "env",
170
- "buildOptions",
171
- "fuzz",
172
- "modes",
173
- "runOptions",
197
+ "$schema",
198
+ "input",
199
+ "output",
200
+ "outDir",
201
+ "logs",
202
+ "coverageDir",
203
+ "snapshotDir",
204
+ "config",
205
+ "coverage",
206
+ "env",
207
+ "buildOptions",
208
+ "fuzz",
209
+ "modes",
210
+ "runOptions",
174
211
  ]);
175
212
  const BUILD_OPTION_KEYS = new Set(["cmd", "args", "target", "env"]);
176
213
  const RUN_OPTION_KEYS = new Set(["runtime", "reporter", "run", "env"]); // includes legacy "run"
@@ -178,1223 +215,1237 @@ const RUNTIME_OPTION_KEYS = new Set(["cmd", "run", "browser"]); // includes lega
178
215
  const REPORTER_OPTION_KEYS = new Set(["name", "options", "outDir", "outFile"]);
179
216
  const OUTPUT_OPTION_KEYS = new Set(["build", "logs", "coverage", "snapshots"]);
180
217
  const FUZZ_OPTION_KEYS = new Set([
181
- "input",
182
- "runs",
183
- "seed",
184
- "maxInputBytes",
185
- "target",
186
- "corpusDir",
187
- "crashDir",
218
+ "input",
219
+ "runs",
220
+ "seed",
221
+ "maxInputBytes",
222
+ "target",
223
+ "corpusDir",
224
+ "crashDir",
188
225
  ]);
189
- const MODE_KEYS = new Set([...TOP_LEVEL_KEYS, "default"].filter((key) => key != "modes"));
226
+ const MODE_KEYS = new Set(
227
+ [...TOP_LEVEL_KEYS, "default"].filter((key) => key != "modes"),
228
+ );
190
229
  function validateConfig(raw, configPath) {
191
- const issues = [];
192
- validateUnknownKeys(raw, TOP_LEVEL_KEYS, "$", issues);
193
- validateStringField(raw, "$schema", "$", issues);
194
- validateInputField(raw, "input", "$", issues);
195
- validateOutputField(raw, "output", "$", issues);
196
- validateStringField(raw, "outDir", "$", issues);
197
- validateStringField(raw, "logs", "$", issues);
198
- validateStringField(raw, "coverageDir", "$", issues);
199
- validateStringField(raw, "snapshotDir", "$", issues);
200
- validateStringField(raw, "config", "$", issues);
201
- validateCoverageField(raw, "coverage", "$", issues);
202
- validateEnvField(raw, "env", "$", issues);
203
- validateBuildOptionsField(raw, "buildOptions", "$", issues);
204
- validateFuzzField(raw, "fuzz", "$", issues);
205
- validateRunOptionsField(raw, "runOptions", "$", issues);
206
- validateModesField(raw, "modes", "$", issues);
207
- if (!issues.length)
208
- return;
209
- const lines = issues.map((issue, index) => {
210
- const suffix = issue.fix ? `\n fix: ${issue.fix}` : "";
211
- return `${index + 1}. ${issue.path}: ${issue.message}${suffix}`;
212
- });
213
- throw new Error(`invalid config at ${configPath}\n${lines.join("\n")}\nrun "ast doctor" to check your setup.`);
230
+ const issues = [];
231
+ validateUnknownKeys(raw, TOP_LEVEL_KEYS, "$", issues);
232
+ validateStringField(raw, "$schema", "$", issues);
233
+ validateInputField(raw, "input", "$", issues);
234
+ validateOutputField(raw, "output", "$", issues);
235
+ validateStringField(raw, "outDir", "$", issues);
236
+ validateStringField(raw, "logs", "$", issues);
237
+ validateStringField(raw, "coverageDir", "$", issues);
238
+ validateStringField(raw, "snapshotDir", "$", issues);
239
+ validateStringField(raw, "config", "$", issues);
240
+ validateCoverageField(raw, "coverage", "$", issues);
241
+ validateEnvField(raw, "env", "$", issues);
242
+ validateBuildOptionsField(raw, "buildOptions", "$", issues);
243
+ validateFuzzField(raw, "fuzz", "$", issues);
244
+ validateRunOptionsField(raw, "runOptions", "$", issues);
245
+ validateModesField(raw, "modes", "$", issues);
246
+ if (!issues.length) return;
247
+ const lines = issues.map((issue, index) => {
248
+ const suffix = issue.fix ? `\n fix: ${issue.fix}` : "";
249
+ return `${index + 1}. ${issue.path}: ${issue.message}${suffix}`;
250
+ });
251
+ throw new Error(
252
+ `invalid config at ${configPath}\n${lines.join("\n")}\nrun "ast doctor" to check your setup.`,
253
+ );
214
254
  }
215
255
  function validateUnknownKeys(raw, allowed, pathPrefix, issues) {
216
- for (const key of Object.keys(raw)) {
217
- if (allowed.has(key))
218
- continue;
219
- const suggestion = resolveClosestKey(key, [...allowed]);
220
- issues.push({
221
- path: `${pathPrefix}.${key}`,
222
- message: "unknown property",
223
- fix: suggestion
224
- ? `use "${suggestion}" if that was intended, otherwise remove this property`
225
- : `remove this property`,
226
- });
227
- }
256
+ for (const key of Object.keys(raw)) {
257
+ if (allowed.has(key)) continue;
258
+ const suggestion = resolveClosestKey(key, [...allowed]);
259
+ issues.push({
260
+ path: `${pathPrefix}.${key}`,
261
+ message: "unknown property",
262
+ fix: suggestion
263
+ ? `use "${suggestion}" if that was intended, otherwise remove this property`
264
+ : `remove this property`,
265
+ });
266
+ }
228
267
  }
229
268
  function validateInputField(raw, key, pathPrefix, issues) {
230
- if (!(key in raw) || raw[key] == undefined)
231
- return;
232
- const value = raw[key];
233
- if (typeof value == "string") {
234
- if (!value.length) {
235
- issues.push({
236
- path: `${pathPrefix}.${key}`,
237
- message: "must not be an empty string",
238
- fix: "set to a glob pattern or remove it to use the default input patterns",
239
- });
240
- }
241
- return;
242
- }
243
- if (!Array.isArray(value)) {
244
- issues.push({
245
- path: `${pathPrefix}.${key}`,
246
- message: "must be a string or array of strings",
247
- fix: 'example: "input": ["./assembly/__tests__/*.spec.ts"]',
248
- });
249
- return;
250
- }
251
- for (let i = 0; i < value.length; i++) {
252
- if (typeof value[i] == "string" && value[i].length)
253
- continue;
254
- issues.push({
255
- path: `${pathPrefix}.${key}[${i}]`,
256
- message: "must be a non-empty string",
257
- fix: "remove invalid entries or replace them with valid glob strings",
258
- });
259
- }
269
+ if (!(key in raw) || raw[key] == undefined) return;
270
+ const value = raw[key];
271
+ if (typeof value == "string") {
272
+ if (!value.length) {
273
+ issues.push({
274
+ path: `${pathPrefix}.${key}`,
275
+ message: "must not be an empty string",
276
+ fix: "set to a glob pattern or remove it to use the default input patterns",
277
+ });
278
+ }
279
+ return;
280
+ }
281
+ if (!Array.isArray(value)) {
282
+ issues.push({
283
+ path: `${pathPrefix}.${key}`,
284
+ message: "must be a string or array of strings",
285
+ fix: 'example: "input": ["./assembly/__tests__/*.spec.ts"]',
286
+ });
287
+ return;
288
+ }
289
+ for (let i = 0; i < value.length; i++) {
290
+ if (typeof value[i] == "string" && value[i].length) continue;
291
+ issues.push({
292
+ path: `${pathPrefix}.${key}[${i}]`,
293
+ message: "must be a non-empty string",
294
+ fix: "remove invalid entries or replace them with valid glob strings",
295
+ });
296
+ }
260
297
  }
261
298
  function validateStringField(raw, key, pathPrefix, issues) {
262
- if (!(key in raw) || raw[key] == undefined)
263
- return;
264
- if (typeof raw[key] != "string") {
265
- issues.push({
266
- path: `${pathPrefix}.${key}`,
267
- message: "must be a string",
268
- fix: `set "${key}" to a string value`,
269
- });
270
- }
299
+ if (!(key in raw) || raw[key] == undefined) return;
300
+ if (typeof raw[key] != "string") {
301
+ issues.push({
302
+ path: `${pathPrefix}.${key}`,
303
+ message: "must be a string",
304
+ fix: `set "${key}" to a string value`,
305
+ });
306
+ }
271
307
  }
272
308
  function validateOutputField(raw, key, pathPrefix, issues) {
273
- if (!(key in raw) || raw[key] == undefined)
274
- return;
275
- const value = raw[key];
276
- if (typeof value == "string") {
277
- if (!value.length) {
278
- issues.push({
279
- path: `${pathPrefix}.${key}`,
280
- message: "must not be an empty string",
281
- fix: 'example: "output": "./.as-test/"',
282
- });
283
- }
284
- return;
285
- }
286
- if (!value || typeof value != "object" || Array.isArray(value)) {
287
- issues.push({
288
- path: `${pathPrefix}.${key}`,
289
- message: "must be a string or object",
290
- fix: 'example: "output": { "logs": "./.as-test/logs", "coverage": "./.as-test/coverage" }',
291
- });
292
- return;
293
- }
294
- const out = value;
295
- validateUnknownKeys(out, OUTPUT_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
296
- if ("build" in out && (typeof out.build != "string" || !out.build.length)) {
297
- issues.push({
298
- path: `${pathPrefix}.${key}.build`,
299
- message: "must be a non-empty string",
300
- });
301
- }
302
- if ("snapshots" in out &&
303
- (typeof out.snapshots != "string" || !out.snapshots.length)) {
304
- issues.push({
305
- path: `${pathPrefix}.${key}.snapshots`,
306
- message: "must be a non-empty string",
307
- });
308
- }
309
- if ("logs" in out && (typeof out.logs != "string" || !out.logs.length)) {
310
- issues.push({
311
- path: `${pathPrefix}.${key}.logs`,
312
- message: 'must be a non-empty string or "none"',
313
- });
314
- }
315
- if ("coverage" in out &&
316
- (typeof out.coverage != "string" || !out.coverage.length)) {
317
- issues.push({
318
- path: `${pathPrefix}.${key}.coverage`,
319
- message: 'must be a non-empty string or "none"',
320
- });
321
- }
309
+ if (!(key in raw) || raw[key] == undefined) return;
310
+ const value = raw[key];
311
+ if (typeof value == "string") {
312
+ if (!value.length) {
313
+ issues.push({
314
+ path: `${pathPrefix}.${key}`,
315
+ message: "must not be an empty string",
316
+ fix: 'example: "output": "./.as-test/"',
317
+ });
318
+ }
319
+ return;
320
+ }
321
+ if (!value || typeof value != "object" || Array.isArray(value)) {
322
+ issues.push({
323
+ path: `${pathPrefix}.${key}`,
324
+ message: "must be a string or object",
325
+ fix: 'example: "output": { "logs": "./.as-test/logs", "coverage": "./.as-test/coverage" }',
326
+ });
327
+ return;
328
+ }
329
+ const out = value;
330
+ validateUnknownKeys(out, OUTPUT_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
331
+ if ("build" in out && (typeof out.build != "string" || !out.build.length)) {
332
+ issues.push({
333
+ path: `${pathPrefix}.${key}.build`,
334
+ message: "must be a non-empty string",
335
+ });
336
+ }
337
+ if (
338
+ "snapshots" in out &&
339
+ (typeof out.snapshots != "string" || !out.snapshots.length)
340
+ ) {
341
+ issues.push({
342
+ path: `${pathPrefix}.${key}.snapshots`,
343
+ message: "must be a non-empty string",
344
+ });
345
+ }
346
+ if ("logs" in out && (typeof out.logs != "string" || !out.logs.length)) {
347
+ issues.push({
348
+ path: `${pathPrefix}.${key}.logs`,
349
+ message: 'must be a non-empty string or "none"',
350
+ });
351
+ }
352
+ if (
353
+ "coverage" in out &&
354
+ (typeof out.coverage != "string" || !out.coverage.length)
355
+ ) {
356
+ issues.push({
357
+ path: `${pathPrefix}.${key}.coverage`,
358
+ message: 'must be a non-empty string or "none"',
359
+ });
360
+ }
322
361
  }
323
362
  function validateCoverageField(raw, key, pathPrefix, issues) {
324
- if (!(key in raw) || raw[key] == undefined)
325
- return;
326
- validateCoverageValue(raw[key], `${pathPrefix}.${key}`, issues);
363
+ if (!(key in raw) || raw[key] == undefined) return;
364
+ validateCoverageValue(raw[key], `${pathPrefix}.${key}`, issues);
327
365
  }
328
366
  function validateCoverageValue(value, path, issues) {
329
- if (typeof value == "boolean")
330
- return;
331
- if (!value || typeof value != "object" || Array.isArray(value)) {
332
- issues.push({
333
- path,
334
- message: "must be a boolean or object",
335
- fix: 'use true/false or { "enabled": true, "mode": "project", "includeSpecs": false, "dependencies": ["json-as"], "include": ["assembly/**/*.ts"], "exclude": ["assembly/__tests__/**/*.spec.ts"] }',
336
- });
337
- return;
338
- }
339
- const obj = value;
340
- if ("enabled" in obj && typeof obj.enabled != "boolean") {
341
- issues.push({
342
- path: `${path}.enabled`,
343
- message: "must be a boolean",
344
- fix: "set to true or false",
345
- });
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
- }
363
- if ("includeSpecs" in obj && typeof obj.includeSpecs != "boolean") {
364
- issues.push({
365
- path: `${path}.includeSpecs`,
366
- message: "must be a boolean",
367
- fix: "set to true or false",
368
- });
369
- }
370
- validateStringArrayField(obj, "dependencies", path, issues);
371
- validateStringArrayField(obj, "include", path, issues);
372
- validateStringArrayField(obj, "exclude", path, issues);
373
- if ("ignore" in obj && obj.ignore != undefined) {
374
- if (!obj.ignore ||
375
- typeof obj.ignore != "object" ||
376
- Array.isArray(obj.ignore)) {
377
- issues.push({
378
- path: `${path}.ignore`,
379
- message: "must be an object",
380
- fix: 'set "ignore" to an object such as { "labels": ["Call"], "names": ["panic"] }',
381
- });
382
- }
383
- else {
384
- const ignore = obj.ignore;
385
- validateStringArrayField(ignore, "labels", `${path}.ignore`, issues);
386
- validateStringArrayField(ignore, "names", `${path}.ignore`, issues);
387
- validateStringArrayField(ignore, "locations", `${path}.ignore`, issues);
388
- validateStringArrayField(ignore, "snippets", `${path}.ignore`, issues);
389
- }
390
- }
367
+ if (typeof value == "boolean") return;
368
+ if (!value || typeof value != "object" || Array.isArray(value)) {
369
+ issues.push({
370
+ path,
371
+ message: "must be a boolean or object",
372
+ fix: 'use true/false or { "enabled": true, "mode": "project", "includeSpecs": false, "dependencies": ["json-as"], "include": ["assembly/**/*.ts"], "exclude": ["assembly/__tests__/**/*.spec.ts"] }',
373
+ });
374
+ return;
375
+ }
376
+ const obj = value;
377
+ if ("enabled" in obj && typeof obj.enabled != "boolean") {
378
+ issues.push({
379
+ path: `${path}.enabled`,
380
+ message: "must be a boolean",
381
+ fix: "set to true or false",
382
+ });
383
+ }
384
+ if ("mode" in obj && obj.mode != undefined) {
385
+ if (typeof obj.mode != "string") {
386
+ issues.push({
387
+ path: `${path}.mode`,
388
+ message: 'must be "project" or "all"',
389
+ fix: 'set "mode" to "project" or "all"',
390
+ });
391
+ } else if (obj.mode != "project" && obj.mode != "all") {
392
+ issues.push({
393
+ path: `${path}.mode`,
394
+ message: 'must be "project" or "all"',
395
+ fix: 'set "mode" to "project" or "all"',
396
+ });
397
+ }
398
+ }
399
+ if ("includeSpecs" in obj && typeof obj.includeSpecs != "boolean") {
400
+ issues.push({
401
+ path: `${path}.includeSpecs`,
402
+ message: "must be a boolean",
403
+ fix: "set to true or false",
404
+ });
405
+ }
406
+ validateStringArrayField(obj, "dependencies", path, issues);
407
+ validateStringArrayField(obj, "include", path, issues);
408
+ validateStringArrayField(obj, "exclude", path, issues);
409
+ if ("ignore" in obj && obj.ignore != undefined) {
410
+ if (
411
+ !obj.ignore ||
412
+ typeof obj.ignore != "object" ||
413
+ Array.isArray(obj.ignore)
414
+ ) {
415
+ issues.push({
416
+ path: `${path}.ignore`,
417
+ message: "must be an object",
418
+ fix: 'set "ignore" to an object such as { "labels": ["Call"], "names": ["panic"] }',
419
+ });
420
+ } else {
421
+ const ignore = obj.ignore;
422
+ validateStringArrayField(ignore, "labels", `${path}.ignore`, issues);
423
+ validateStringArrayField(ignore, "names", `${path}.ignore`, issues);
424
+ validateStringArrayField(ignore, "locations", `${path}.ignore`, issues);
425
+ validateStringArrayField(ignore, "snippets", `${path}.ignore`, issues);
426
+ }
427
+ }
391
428
  }
392
429
  function validateStringArrayField(raw, key, pathPrefix, issues) {
393
- if (!(key in raw) || raw[key] == undefined)
394
- return;
395
- const value = raw[key];
396
- if (!Array.isArray(value)) {
397
- issues.push({
398
- path: `${pathPrefix}.${key}`,
399
- message: "must be an array of strings",
400
- fix: `set "${key}" to an array of glob patterns`,
401
- });
402
- return;
403
- }
404
- for (let i = 0; i < value.length; i++) {
405
- if (typeof value[i] == "string" && value[i].length)
406
- continue;
407
- issues.push({
408
- path: `${pathPrefix}.${key}[${i}]`,
409
- message: "must be a non-empty string",
410
- fix: "remove invalid entries or replace them with valid glob strings",
411
- });
412
- }
430
+ if (!(key in raw) || raw[key] == undefined) return;
431
+ const value = raw[key];
432
+ if (!Array.isArray(value)) {
433
+ issues.push({
434
+ path: `${pathPrefix}.${key}`,
435
+ message: "must be an array of strings",
436
+ fix: `set "${key}" to an array of glob patterns`,
437
+ });
438
+ return;
439
+ }
440
+ for (let i = 0; i < value.length; i++) {
441
+ if (typeof value[i] == "string" && value[i].length) continue;
442
+ issues.push({
443
+ path: `${pathPrefix}.${key}[${i}]`,
444
+ message: "must be a non-empty string",
445
+ fix: "remove invalid entries or replace them with valid glob strings",
446
+ });
447
+ }
413
448
  }
414
449
  function validateEnvField(raw, key, pathPrefix, issues) {
415
- if (!(key in raw) || raw[key] == undefined)
416
- return;
417
- const value = raw[key];
418
- if (typeof value == "string") {
419
- if (!value.length) {
420
- issues.push({
421
- path: `${pathPrefix}.${key}`,
422
- message: "must not be an empty string",
423
- fix: 'use a .env file path like "./secrets/.env"',
424
- });
425
- }
426
- return;
427
- }
428
- if (Array.isArray(value)) {
429
- for (let i = 0; i < value.length; i++) {
430
- const item = value[i];
431
- if (typeof item != "string" || !item.length) {
432
- issues.push({
433
- path: `${pathPrefix}.${key}[${i}]`,
434
- message: "must be a non-empty string",
435
- fix: 'use entries like "FOO=1"',
436
- });
437
- continue;
438
- }
439
- const separator = item.indexOf("=");
440
- if (separator <= 0) {
441
- issues.push({
442
- path: `${pathPrefix}.${key}[${i}]`,
443
- message: 'must use "KEY=value" format',
444
- fix: 'example: "FOO=1"',
445
- });
446
- }
447
- }
448
- return;
449
- }
450
- if (!value || typeof value != "object") {
450
+ if (!(key in raw) || raw[key] == undefined) return;
451
+ const value = raw[key];
452
+ if (typeof value == "string") {
453
+ if (!value.length) {
454
+ issues.push({
455
+ path: `${pathPrefix}.${key}`,
456
+ message: "must not be an empty string",
457
+ fix: 'use a .env file path like "./secrets/.env"',
458
+ });
459
+ }
460
+ return;
461
+ }
462
+ if (Array.isArray(value)) {
463
+ for (let i = 0; i < value.length; i++) {
464
+ const item = value[i];
465
+ if (typeof item != "string" || !item.length) {
451
466
  issues.push({
452
- path: `${pathPrefix}.${key}`,
453
- message: "must be a .env file path, array of KEY=value strings, or object of string values",
454
- fix: 'example: "env": "./secrets/.env" or ["MY_FLAG=1"] or { "MY_FLAG": "1" }',
467
+ path: `${pathPrefix}.${key}[${i}]`,
468
+ message: "must be a non-empty string",
469
+ fix: 'use entries like "FOO=1"',
455
470
  });
456
- return;
457
- }
458
- for (const [name, item] of Object.entries(value)) {
459
- if (typeof item == "string")
460
- continue;
471
+ continue;
472
+ }
473
+ const separator = item.indexOf("=");
474
+ if (separator <= 0) {
461
475
  issues.push({
462
- path: `${pathPrefix}.${key}.${name}`,
463
- message: "must be a string",
464
- fix: "set environment values as strings",
476
+ path: `${pathPrefix}.${key}[${i}]`,
477
+ message: 'must use "KEY=value" format',
478
+ fix: 'example: "FOO=1"',
465
479
  });
466
- }
480
+ }
481
+ }
482
+ return;
483
+ }
484
+ if (!value || typeof value != "object") {
485
+ issues.push({
486
+ path: `${pathPrefix}.${key}`,
487
+ message:
488
+ "must be a .env file path, array of KEY=value strings, or object of string values",
489
+ fix: 'example: "env": "./secrets/.env" or ["MY_FLAG=1"] or { "MY_FLAG": "1" }',
490
+ });
491
+ return;
492
+ }
493
+ for (const [name, item] of Object.entries(value)) {
494
+ if (typeof item == "string") continue;
495
+ issues.push({
496
+ path: `${pathPrefix}.${key}.${name}`,
497
+ message: "must be a string",
498
+ fix: "set environment values as strings",
499
+ });
500
+ }
467
501
  }
468
502
  function validateBuildOptionsField(raw, key, pathPrefix, issues) {
469
- if (!(key in raw) || raw[key] == undefined)
470
- return;
471
- const value = raw[key];
472
- if (!value || typeof value != "object" || Array.isArray(value)) {
473
- issues.push({
474
- path: `${pathPrefix}.${key}`,
475
- message: "must be an object",
476
- fix: 'example: "buildOptions": { "cmd": "", "args": [], "target": "wasi" }',
477
- });
478
- return;
479
- }
480
- const obj = value;
481
- validateUnknownKeys(obj, BUILD_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
482
- if ("cmd" in obj && typeof obj.cmd != "string") {
483
- issues.push({
484
- path: `${pathPrefix}.${key}.cmd`,
485
- message: "must be a string",
486
- fix: "set to an empty string or a command template",
487
- });
488
- }
489
- if ("args" in obj && !isStringArray(obj.args)) {
490
- issues.push({
491
- path: `${pathPrefix}.${key}.args`,
492
- message: "must be an array of strings",
493
- fix: 'example: "args": ["--optimize"]',
494
- });
495
- }
496
- if ("target" in obj) {
497
- if (typeof obj.target != "string") {
498
- issues.push({
499
- path: `${pathPrefix}.${key}.target`,
500
- message: "must be a string",
501
- fix: 'set to "wasi", "bindings", or "web"',
502
- });
503
- }
504
- else if (obj.target != "wasi" &&
505
- obj.target != "bindings" &&
506
- obj.target != "web") {
507
- issues.push({
508
- path: `${pathPrefix}.${key}.target`,
509
- message: `must be "wasi", "bindings", or "web"`,
510
- fix: `received "${obj.target}"`,
511
- });
512
- }
513
- }
514
- validateEnvField(obj, "env", `${pathPrefix}.${key}`, issues);
503
+ if (!(key in raw) || raw[key] == undefined) return;
504
+ const value = raw[key];
505
+ if (!value || typeof value != "object" || Array.isArray(value)) {
506
+ issues.push({
507
+ path: `${pathPrefix}.${key}`,
508
+ message: "must be an object",
509
+ fix: 'example: "buildOptions": { "cmd": "", "args": [], "target": "wasi" }',
510
+ });
511
+ return;
512
+ }
513
+ const obj = value;
514
+ validateUnknownKeys(obj, BUILD_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
515
+ if ("cmd" in obj && typeof obj.cmd != "string") {
516
+ issues.push({
517
+ path: `${pathPrefix}.${key}.cmd`,
518
+ message: "must be a string",
519
+ fix: "set to an empty string or a command template",
520
+ });
521
+ }
522
+ if ("args" in obj && !isStringArray(obj.args)) {
523
+ issues.push({
524
+ path: `${pathPrefix}.${key}.args`,
525
+ message: "must be an array of strings",
526
+ fix: 'example: "args": ["--optimize"]',
527
+ });
528
+ }
529
+ if ("target" in obj) {
530
+ if (typeof obj.target != "string") {
531
+ issues.push({
532
+ path: `${pathPrefix}.${key}.target`,
533
+ message: "must be a string",
534
+ fix: 'set to "wasi", "bindings", or "web"',
535
+ });
536
+ } else if (
537
+ obj.target != "wasi" &&
538
+ obj.target != "bindings" &&
539
+ obj.target != "web"
540
+ ) {
541
+ issues.push({
542
+ path: `${pathPrefix}.${key}.target`,
543
+ message: `must be "wasi", "bindings", or "web"`,
544
+ fix: `received "${obj.target}"`,
545
+ });
546
+ }
547
+ }
548
+ validateEnvField(obj, "env", `${pathPrefix}.${key}`, issues);
515
549
  }
516
550
  function validateRunOptionsField(raw, key, pathPrefix, issues) {
517
- if (!(key in raw) || raw[key] == undefined)
518
- return;
519
- const value = raw[key];
520
- if (!value || typeof value != "object" || Array.isArray(value)) {
521
- issues.push({
522
- path: `${pathPrefix}.${key}`,
523
- message: "must be an object",
524
- fix: 'example: "runOptions": { "runtime": { "cmd": "node ... <file>" } }',
525
- });
526
- return;
527
- }
528
- const obj = value;
529
- validateUnknownKeys(obj, RUN_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
530
- if ("run" in obj && typeof obj.run != "string") {
551
+ if (!(key in raw) || raw[key] == undefined) return;
552
+ const value = raw[key];
553
+ if (!value || typeof value != "object" || Array.isArray(value)) {
554
+ issues.push({
555
+ path: `${pathPrefix}.${key}`,
556
+ message: "must be an object",
557
+ fix: 'example: "runOptions": { "runtime": { "cmd": "node ... <file>" } }',
558
+ });
559
+ return;
560
+ }
561
+ const obj = value;
562
+ validateUnknownKeys(obj, RUN_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
563
+ if ("run" in obj && typeof obj.run != "string") {
564
+ issues.push({
565
+ path: `${pathPrefix}.${key}.run`,
566
+ message: "must be a string",
567
+ fix: 'prefer "runtime.cmd"; legacy "run" must still be string',
568
+ });
569
+ }
570
+ if ("runtime" in obj && obj.runtime != undefined) {
571
+ const runtime = obj.runtime;
572
+ if (!runtime || typeof runtime != "object" || Array.isArray(runtime)) {
573
+ issues.push({
574
+ path: `${pathPrefix}.${key}.runtime`,
575
+ message: "must be an object",
576
+ fix: 'example: "runtime": { "cmd": "node ./.as-test/runners/default.wasi.js <file>" }',
577
+ });
578
+ } else {
579
+ const runtimeObj = runtime;
580
+ validateUnknownKeys(
581
+ runtimeObj,
582
+ RUNTIME_OPTION_KEYS,
583
+ `${pathPrefix}.${key}.runtime`,
584
+ issues,
585
+ );
586
+ if ("cmd" in runtimeObj && typeof runtimeObj.cmd != "string") {
531
587
  issues.push({
532
- path: `${pathPrefix}.${key}.run`,
533
- message: "must be a string",
534
- fix: 'prefer "runtime.cmd"; legacy "run" must still be string',
588
+ path: `${pathPrefix}.${key}.runtime.cmd`,
589
+ message: "must be a string",
590
+ fix: 'set to a runtime command including "<file>"',
535
591
  });
536
- }
537
- if ("runtime" in obj && obj.runtime != undefined) {
538
- const runtime = obj.runtime;
539
- if (!runtime || typeof runtime != "object" || Array.isArray(runtime)) {
540
- issues.push({
541
- path: `${pathPrefix}.${key}.runtime`,
542
- message: "must be an object",
543
- fix: 'example: "runtime": { "cmd": "node ./.as-test/runners/default.wasi.js <file>" }',
544
- });
545
- }
546
- else {
547
- const runtimeObj = runtime;
548
- validateUnknownKeys(runtimeObj, RUNTIME_OPTION_KEYS, `${pathPrefix}.${key}.runtime`, issues);
549
- if ("cmd" in runtimeObj && typeof runtimeObj.cmd != "string") {
550
- issues.push({
551
- path: `${pathPrefix}.${key}.runtime.cmd`,
552
- message: "must be a string",
553
- fix: 'set to a runtime command including "<file>"',
554
- });
555
- }
556
- if ("run" in runtimeObj && typeof runtimeObj.run != "string") {
557
- issues.push({
558
- path: `${pathPrefix}.${key}.runtime.run`,
559
- message: "must be a string",
560
- fix: 'legacy "run" should be a command string',
561
- });
562
- }
563
- if ("browser" in runtimeObj && typeof runtimeObj.browser != "string") {
564
- issues.push({
565
- path: `${pathPrefix}.${key}.runtime.browser`,
566
- message: "must be a string",
567
- fix: 'set to "chrome", "chromium", "firefox", "webkit", or an executable path',
568
- });
569
- }
570
- }
571
- }
572
- if ("reporter" in obj && obj.reporter != undefined) {
573
- const reporter = obj.reporter;
574
- if (typeof reporter == "string")
575
- return;
576
- if (!reporter || typeof reporter != "object" || Array.isArray(reporter)) {
577
- issues.push({
578
- path: `${pathPrefix}.${key}.reporter`,
579
- message: "must be a string or object",
580
- fix: 'use "default", "tap", or { "name": "...", ... }',
581
- });
582
- return;
583
- }
584
- const reporterObj = reporter;
585
- validateUnknownKeys(reporterObj, REPORTER_OPTION_KEYS, `${pathPrefix}.${key}.reporter`, issues);
586
- if ("name" in reporterObj && typeof reporterObj.name != "string") {
587
- issues.push({
588
- path: `${pathPrefix}.${key}.reporter.name`,
589
- message: "must be a string",
590
- fix: 'set to "default", "tap", or module path',
591
- });
592
- }
593
- if (!("name" in reporterObj)) {
594
- issues.push({
595
- path: `${pathPrefix}.${key}.reporter`,
596
- message: 'object reporter config requires "name"',
597
- fix: 'example: { "name": "tap", "outDir": "./.as-test/reports" }',
598
- });
599
- }
600
- if ("options" in reporterObj && !isStringArray(reporterObj.options)) {
601
- issues.push({
602
- path: `${pathPrefix}.${key}.reporter.options`,
603
- message: "must be an array of strings",
604
- fix: 'example: "options": ["single-file"]',
605
- });
606
- }
607
- if ("outDir" in reporterObj && typeof reporterObj.outDir != "string") {
608
- issues.push({
609
- path: `${pathPrefix}.${key}.reporter.outDir`,
610
- message: "must be a string",
611
- });
612
- }
613
- if ("outFile" in reporterObj && typeof reporterObj.outFile != "string") {
614
- issues.push({
615
- path: `${pathPrefix}.${key}.reporter.outFile`,
616
- message: "must be a string",
617
- });
618
- }
619
- }
620
- validateEnvField(obj, "env", `${pathPrefix}.${key}`, issues);
621
- }
622
- function validateFuzzField(raw, key, pathPrefix, issues) {
623
- if (!(key in raw) || raw[key] == undefined)
624
- return;
625
- const value = raw[key];
626
- if (!value || typeof value != "object" || Array.isArray(value)) {
592
+ }
593
+ if ("run" in runtimeObj && typeof runtimeObj.run != "string") {
627
594
  issues.push({
628
- path: `${pathPrefix}.${key}`,
629
- message: "must be an object",
630
- fix: 'example: "fuzz": { "input": ["./assembly/__fuzz__/*.fuzz.ts"], "runs": 1000 }',
595
+ path: `${pathPrefix}.${key}.runtime.run`,
596
+ message: "must be a string",
597
+ fix: 'legacy "run" should be a command string',
631
598
  });
632
- return;
633
- }
634
- const obj = value;
635
- validateUnknownKeys(obj, FUZZ_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
636
- validateInputField(obj, "input", `${pathPrefix}.${key}`, issues);
637
- validateStringField(obj, "target", `${pathPrefix}.${key}`, issues);
638
- validateStringField(obj, "corpusDir", `${pathPrefix}.${key}`, issues);
639
- validateStringField(obj, "crashDir", `${pathPrefix}.${key}`, issues);
640
- validateNumberField(obj, "runs", `${pathPrefix}.${key}`, issues, true);
641
- validateNumberField(obj, "seed", `${pathPrefix}.${key}`, issues, false);
642
- validateNumberField(obj, "maxInputBytes", `${pathPrefix}.${key}`, issues, true);
643
- if ("target" in obj && obj.target != "bindings") {
599
+ }
600
+ if ("browser" in runtimeObj && typeof runtimeObj.browser != "string") {
644
601
  issues.push({
645
- path: `${pathPrefix}.${key}.target`,
646
- message: 'must be "bindings"',
647
- fix: 'set to "bindings"',
602
+ path: `${pathPrefix}.${key}.runtime.browser`,
603
+ message: "must be a string",
604
+ fix: 'set to "chrome", "chromium", "firefox", "webkit", or an executable path',
648
605
  });
649
- }
606
+ }
607
+ }
608
+ }
609
+ if ("reporter" in obj && obj.reporter != undefined) {
610
+ const reporter = obj.reporter;
611
+ if (typeof reporter == "string") return;
612
+ if (!reporter || typeof reporter != "object" || Array.isArray(reporter)) {
613
+ issues.push({
614
+ path: `${pathPrefix}.${key}.reporter`,
615
+ message: "must be a string or object",
616
+ fix: 'use "default", "tap", or { "name": "...", ... }',
617
+ });
618
+ return;
619
+ }
620
+ const reporterObj = reporter;
621
+ validateUnknownKeys(
622
+ reporterObj,
623
+ REPORTER_OPTION_KEYS,
624
+ `${pathPrefix}.${key}.reporter`,
625
+ issues,
626
+ );
627
+ if ("name" in reporterObj && typeof reporterObj.name != "string") {
628
+ issues.push({
629
+ path: `${pathPrefix}.${key}.reporter.name`,
630
+ message: "must be a string",
631
+ fix: 'set to "default", "tap", or module path',
632
+ });
633
+ }
634
+ if (!("name" in reporterObj)) {
635
+ issues.push({
636
+ path: `${pathPrefix}.${key}.reporter`,
637
+ message: 'object reporter config requires "name"',
638
+ fix: 'example: { "name": "tap", "outDir": "./.as-test/reports" }',
639
+ });
640
+ }
641
+ if ("options" in reporterObj && !isStringArray(reporterObj.options)) {
642
+ issues.push({
643
+ path: `${pathPrefix}.${key}.reporter.options`,
644
+ message: "must be an array of strings",
645
+ fix: 'example: "options": ["single-file"]',
646
+ });
647
+ }
648
+ if ("outDir" in reporterObj && typeof reporterObj.outDir != "string") {
649
+ issues.push({
650
+ path: `${pathPrefix}.${key}.reporter.outDir`,
651
+ message: "must be a string",
652
+ });
653
+ }
654
+ if ("outFile" in reporterObj && typeof reporterObj.outFile != "string") {
655
+ issues.push({
656
+ path: `${pathPrefix}.${key}.reporter.outFile`,
657
+ message: "must be a string",
658
+ });
659
+ }
660
+ }
661
+ validateEnvField(obj, "env", `${pathPrefix}.${key}`, issues);
662
+ }
663
+ function validateFuzzField(raw, key, pathPrefix, issues) {
664
+ if (!(key in raw) || raw[key] == undefined) return;
665
+ const value = raw[key];
666
+ if (!value || typeof value != "object" || Array.isArray(value)) {
667
+ issues.push({
668
+ path: `${pathPrefix}.${key}`,
669
+ message: "must be an object",
670
+ fix: 'example: "fuzz": { "input": ["./assembly/__fuzz__/*.fuzz.ts"], "runs": 1000 }',
671
+ });
672
+ return;
673
+ }
674
+ const obj = value;
675
+ validateUnknownKeys(obj, FUZZ_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
676
+ validateInputField(obj, "input", `${pathPrefix}.${key}`, issues);
677
+ validateStringField(obj, "target", `${pathPrefix}.${key}`, issues);
678
+ validateStringField(obj, "corpusDir", `${pathPrefix}.${key}`, issues);
679
+ validateStringField(obj, "crashDir", `${pathPrefix}.${key}`, issues);
680
+ validateNumberField(obj, "runs", `${pathPrefix}.${key}`, issues, true);
681
+ validateNumberField(obj, "seed", `${pathPrefix}.${key}`, issues, false);
682
+ validateNumberField(
683
+ obj,
684
+ "maxInputBytes",
685
+ `${pathPrefix}.${key}`,
686
+ issues,
687
+ true,
688
+ );
689
+ if ("target" in obj && obj.target != "bindings") {
690
+ issues.push({
691
+ path: `${pathPrefix}.${key}.target`,
692
+ message: 'must be "bindings"',
693
+ fix: 'set to "bindings"',
694
+ });
695
+ }
650
696
  }
651
697
  function validateModesField(raw, key, pathPrefix, issues) {
652
- if (!(key in raw) || raw[key] == undefined)
653
- return;
654
- const value = raw[key];
655
- if (!value || typeof value != "object" || Array.isArray(value)) {
698
+ if (!(key in raw) || raw[key] == undefined) return;
699
+ const value = raw[key];
700
+ if (!value || typeof value != "object" || Array.isArray(value)) {
701
+ issues.push({
702
+ path: `${pathPrefix}.${key}`,
703
+ message: "must be an object",
704
+ fix: 'example: "modes": { "wasi": { "buildOptions": { "target": "wasi" } } }',
705
+ });
706
+ return;
707
+ }
708
+ for (const [modeName, modeRaw] of Object.entries(value)) {
709
+ if (typeof modeRaw == "string") {
710
+ if (!modeRaw.length) {
656
711
  issues.push({
657
- path: `${pathPrefix}.${key}`,
658
- message: "must be an object",
659
- fix: 'example: "modes": { "wasi": { "buildOptions": { "target": "wasi" } } }',
712
+ path: `${pathPrefix}.${key}.${modeName}`,
713
+ message: "must not be an empty string",
714
+ fix: 'set to a config file path like "./as-test.config.simd.json"',
660
715
  });
661
- return;
662
- }
663
- for (const [modeName, modeRaw] of Object.entries(value)) {
664
- if (typeof modeRaw == "string") {
665
- if (!modeRaw.length) {
666
- issues.push({
667
- path: `${pathPrefix}.${key}.${modeName}`,
668
- message: "must not be an empty string",
669
- fix: 'set to a config file path like "./as-test.config.simd.json"',
670
- });
671
- }
672
- continue;
673
- }
674
- if (!modeRaw || typeof modeRaw != "object" || Array.isArray(modeRaw)) {
675
- issues.push({
676
- path: `${pathPrefix}.${key}.${modeName}`,
677
- message: "must be a config object or config file path string",
678
- });
679
- continue;
680
- }
681
- const modeObj = modeRaw;
682
- const modePath = `${pathPrefix}.${key}.${modeName}`;
683
- validateUnknownKeys(modeObj, MODE_KEYS, modePath, issues);
684
- validateStringField(modeObj, "$schema", modePath, issues);
685
- validateInputField(modeObj, "input", modePath, issues);
686
- if ("default" in modeObj && typeof modeObj.default != "boolean") {
687
- issues.push({
688
- path: `${modePath}.default`,
689
- message: "must be a boolean",
690
- fix: 'set "default" to true or false',
691
- });
692
- }
693
- validateOutputField(modeObj, "output", modePath, issues);
694
- validateStringField(modeObj, "outDir", modePath, issues);
695
- validateStringField(modeObj, "logs", modePath, issues);
696
- validateStringField(modeObj, "coverageDir", modePath, issues);
697
- validateStringField(modeObj, "snapshotDir", modePath, issues);
698
- validateStringField(modeObj, "config", modePath, issues);
699
- validateCoverageField(modeObj, "coverage", modePath, issues);
700
- validateFuzzField(modeObj, "fuzz", modePath, issues);
701
- validateEnvField(modeObj, "env", modePath, issues);
702
- validateBuildOptionsField(modeObj, "buildOptions", modePath, issues);
703
- validateRunOptionsField(modeObj, "runOptions", modePath, issues);
704
- }
716
+ }
717
+ continue;
718
+ }
719
+ if (!modeRaw || typeof modeRaw != "object" || Array.isArray(modeRaw)) {
720
+ issues.push({
721
+ path: `${pathPrefix}.${key}.${modeName}`,
722
+ message: "must be a config object or config file path string",
723
+ });
724
+ continue;
725
+ }
726
+ const modeObj = modeRaw;
727
+ const modePath = `${pathPrefix}.${key}.${modeName}`;
728
+ validateUnknownKeys(modeObj, MODE_KEYS, modePath, issues);
729
+ validateStringField(modeObj, "$schema", modePath, issues);
730
+ validateInputField(modeObj, "input", modePath, issues);
731
+ if ("default" in modeObj && typeof modeObj.default != "boolean") {
732
+ issues.push({
733
+ path: `${modePath}.default`,
734
+ message: "must be a boolean",
735
+ fix: 'set "default" to true or false',
736
+ });
737
+ }
738
+ validateOutputField(modeObj, "output", modePath, issues);
739
+ validateStringField(modeObj, "outDir", modePath, issues);
740
+ validateStringField(modeObj, "logs", modePath, issues);
741
+ validateStringField(modeObj, "coverageDir", modePath, issues);
742
+ validateStringField(modeObj, "snapshotDir", modePath, issues);
743
+ validateStringField(modeObj, "config", modePath, issues);
744
+ validateCoverageField(modeObj, "coverage", modePath, issues);
745
+ validateFuzzField(modeObj, "fuzz", modePath, issues);
746
+ validateEnvField(modeObj, "env", modePath, issues);
747
+ validateBuildOptionsField(modeObj, "buildOptions", modePath, issues);
748
+ validateRunOptionsField(modeObj, "runOptions", modePath, issues);
749
+ }
705
750
  }
706
751
  function isStringArray(value) {
707
- return Array.isArray(value) && value.every((item) => typeof item == "string");
752
+ return Array.isArray(value) && value.every((item) => typeof item == "string");
708
753
  }
709
754
  function validateNumberField(raw, key, pathPrefix, issues, positiveOnly) {
710
- if (!(key in raw) || raw[key] == undefined)
711
- return;
712
- if (typeof raw[key] != "number" || !Number.isFinite(raw[key])) {
713
- issues.push({
714
- path: `${pathPrefix}.${key}`,
715
- message: "must be a finite number",
716
- fix: `set "${key}" to a numeric value`,
717
- });
718
- return;
719
- }
720
- if (positiveOnly && Number(raw[key]) <= 0) {
721
- issues.push({
722
- path: `${pathPrefix}.${key}`,
723
- message: "must be greater than zero",
724
- fix: `set "${key}" to a positive integer`,
725
- });
726
- return;
727
- }
728
- if (!positiveOnly && Number(raw[key]) < 0) {
729
- issues.push({
730
- path: `${pathPrefix}.${key}`,
731
- message: "must be zero or greater",
732
- fix: `set "${key}" to a non-negative integer`,
733
- });
734
- }
755
+ if (!(key in raw) || raw[key] == undefined) return;
756
+ if (typeof raw[key] != "number" || !Number.isFinite(raw[key])) {
757
+ issues.push({
758
+ path: `${pathPrefix}.${key}`,
759
+ message: "must be a finite number",
760
+ fix: `set "${key}" to a numeric value`,
761
+ });
762
+ return;
763
+ }
764
+ if (positiveOnly && Number(raw[key]) <= 0) {
765
+ issues.push({
766
+ path: `${pathPrefix}.${key}`,
767
+ message: "must be greater than zero",
768
+ fix: `set "${key}" to a positive integer`,
769
+ });
770
+ return;
771
+ }
772
+ if (!positiveOnly && Number(raw[key]) < 0) {
773
+ issues.push({
774
+ path: `${pathPrefix}.${key}`,
775
+ message: "must be zero or greater",
776
+ fix: `set "${key}" to a non-negative integer`,
777
+ });
778
+ }
735
779
  }
736
780
  function resolveClosestKey(value, keys) {
737
- let best = null;
738
- let bestDistance = Number.POSITIVE_INFINITY;
739
- for (const key of keys) {
740
- const distance = levenshteinDistance(value, key);
741
- if (distance < bestDistance) {
742
- bestDistance = distance;
743
- best = key;
744
- }
745
- }
746
- if (best && bestDistance <= 3)
747
- return best;
748
- return null;
781
+ let best = null;
782
+ let bestDistance = Number.POSITIVE_INFINITY;
783
+ for (const key of keys) {
784
+ const distance = levenshteinDistance(value, key);
785
+ if (distance < bestDistance) {
786
+ bestDistance = distance;
787
+ best = key;
788
+ }
789
+ }
790
+ if (best && bestDistance <= 3) return best;
791
+ return null;
749
792
  }
750
793
  function levenshteinDistance(left, right) {
751
- if (left == right)
752
- return 0;
753
- if (!left.length)
754
- return right.length;
755
- if (!right.length)
756
- return left.length;
757
- const matrix = [];
758
- for (let i = 0; i <= left.length; i++) {
759
- matrix[i] = [i];
760
- }
761
- for (let j = 0; j <= right.length; j++) {
762
- matrix[0][j] = j;
763
- }
764
- for (let i = 1; i <= left.length; i++) {
765
- for (let j = 1; j <= right.length; j++) {
766
- const cost = left[i - 1] == right[j - 1] ? 0 : 1;
767
- matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
768
- }
769
- }
770
- return matrix[left.length][right.length];
794
+ if (left == right) return 0;
795
+ if (!left.length) return right.length;
796
+ if (!right.length) return left.length;
797
+ const matrix = [];
798
+ for (let i = 0; i <= left.length; i++) {
799
+ matrix[i] = [i];
800
+ }
801
+ for (let j = 0; j <= right.length; j++) {
802
+ matrix[0][j] = j;
803
+ }
804
+ for (let i = 1; i <= left.length; i++) {
805
+ for (let j = 1; j <= right.length; j++) {
806
+ const cost = left[i - 1] == right[j - 1] ? 0 : 1;
807
+ matrix[i][j] = Math.min(
808
+ matrix[i - 1][j] + 1,
809
+ matrix[i][j - 1] + 1,
810
+ matrix[i - 1][j - 1] + cost,
811
+ );
812
+ }
813
+ }
814
+ return matrix[left.length][right.length];
771
815
  }
772
816
  function applyOutputConfig(rawOutput, rawConfig, config) {
773
- if (rawOutput == undefined)
774
- return;
775
- if (typeof rawOutput == "string") {
776
- applyOutputRoot(rawOutput, rawConfig, config);
777
- return;
778
- }
779
- if (!rawOutput || typeof rawOutput != "object" || Array.isArray(rawOutput)) {
780
- return;
781
- }
782
- const output = rawOutput;
783
- if ("build" in output &&
784
- typeof output.build == "string" &&
785
- output.build.length &&
786
- !("outDir" in rawConfig)) {
787
- config.outDir = output.build;
788
- }
789
- if ("logs" in output &&
790
- typeof output.logs == "string" &&
791
- output.logs.length &&
792
- !("logs" in rawConfig)) {
793
- config.logs = output.logs;
794
- }
795
- if ("coverage" in output &&
796
- typeof output.coverage == "string" &&
797
- output.coverage.length &&
798
- !("coverageDir" in rawConfig)) {
799
- config.coverageDir = output.coverage;
800
- }
801
- if ("snapshots" in output &&
802
- typeof output.snapshots == "string" &&
803
- output.snapshots.length &&
804
- !("snapshotDir" in rawConfig)) {
805
- config.snapshotDir = output.snapshots;
806
- }
817
+ if (rawOutput == undefined) return;
818
+ if (typeof rawOutput == "string") {
819
+ applyOutputRoot(rawOutput, rawConfig, config);
820
+ return;
821
+ }
822
+ if (!rawOutput || typeof rawOutput != "object" || Array.isArray(rawOutput)) {
823
+ return;
824
+ }
825
+ const output = rawOutput;
826
+ if (
827
+ "build" in output &&
828
+ typeof output.build == "string" &&
829
+ output.build.length &&
830
+ !("outDir" in rawConfig)
831
+ ) {
832
+ config.outDir = output.build;
833
+ }
834
+ if (
835
+ "logs" in output &&
836
+ typeof output.logs == "string" &&
837
+ output.logs.length &&
838
+ !("logs" in rawConfig)
839
+ ) {
840
+ config.logs = output.logs;
841
+ }
842
+ if (
843
+ "coverage" in output &&
844
+ typeof output.coverage == "string" &&
845
+ output.coverage.length &&
846
+ !("coverageDir" in rawConfig)
847
+ ) {
848
+ config.coverageDir = output.coverage;
849
+ }
850
+ if (
851
+ "snapshots" in output &&
852
+ typeof output.snapshots == "string" &&
853
+ output.snapshots.length &&
854
+ !("snapshotDir" in rawConfig)
855
+ ) {
856
+ config.snapshotDir = output.snapshots;
857
+ }
807
858
  }
808
859
  function applyOutputRoot(root, rawConfig, config) {
809
- if (!root.length)
810
- return;
811
- if (!("outDir" in rawConfig)) {
812
- config.outDir = join(root, "build");
813
- }
814
- if (!("logs" in rawConfig)) {
815
- config.logs = join(root, "logs");
816
- }
817
- if (!("coverageDir" in rawConfig)) {
818
- config.coverageDir = join(root, "coverage");
819
- }
820
- if (!("snapshotDir" in rawConfig)) {
821
- config.snapshotDir = join(root, "snapshots");
822
- }
860
+ if (!root.length) return;
861
+ if (!("outDir" in rawConfig)) {
862
+ config.outDir = join(root, "build");
863
+ }
864
+ if (!("logs" in rawConfig)) {
865
+ config.logs = join(root, "logs");
866
+ }
867
+ if (!("coverageDir" in rawConfig)) {
868
+ config.coverageDir = join(root, "coverage");
869
+ }
870
+ if (!("snapshotDir" in rawConfig)) {
871
+ config.snapshotDir = join(root, "snapshots");
872
+ }
823
873
  }
824
874
  function parseModes(raw, configDir) {
825
- if (!raw || typeof raw != "object" || Array.isArray(raw))
826
- return {};
827
- const out = {};
828
- const entries = Object.entries(raw);
829
- for (const [name, value] of entries) {
830
- const mode = new ModeConfig();
831
- if (typeof value == "string") {
832
- mode.path = resolve(configDir, value);
833
- mode.config = parseConfigRaw({}, join(configDir, `__mode__.${name}.json`));
834
- out[name] = mode;
835
- continue;
836
- }
837
- if (!value || typeof value != "object" || Array.isArray(value))
838
- continue;
839
- mode.default =
840
- !("default" in value) ||
841
- Boolean(value.default);
842
- mode.config = parseConfigRaw(value, join(configDir, `__mode__.${name}.json`));
843
- out[name] = mode;
844
- }
845
- return out;
875
+ if (!raw || typeof raw != "object" || Array.isArray(raw)) return {};
876
+ const out = {};
877
+ const entries = Object.entries(raw);
878
+ for (const [name, value] of entries) {
879
+ const mode = new ModeConfig();
880
+ if (typeof value == "string") {
881
+ mode.path = resolve(configDir, value);
882
+ mode.config = parseConfigRaw(
883
+ {},
884
+ join(configDir, `__mode__.${name}.json`),
885
+ );
886
+ out[name] = mode;
887
+ continue;
888
+ }
889
+ if (!value || typeof value != "object" || Array.isArray(value)) continue;
890
+ mode.default = !("default" in value) || Boolean(value.default);
891
+ mode.config = parseConfigRaw(
892
+ value,
893
+ join(configDir, `__mode__.${name}.json`),
894
+ );
895
+ out[name] = mode;
896
+ }
897
+ return out;
846
898
  }
847
899
  function parseEnvMap(raw) {
848
- if (!raw || typeof raw != "object" || Array.isArray(raw))
849
- return {};
850
- const env = {};
851
- for (const [key, val] of Object.entries(raw)) {
852
- if (typeof val == "string")
853
- env[key] = val;
854
- }
855
- return env;
900
+ if (!raw || typeof raw != "object" || Array.isArray(raw)) return {};
901
+ const env = {};
902
+ for (const [key, val] of Object.entries(raw)) {
903
+ if (typeof val == "string") env[key] = val;
904
+ }
905
+ return env;
856
906
  }
857
907
  function parseEnvValue(raw, configDir, pathLabel) {
858
- if (raw == undefined)
859
- return {};
860
- if (typeof raw == "string") {
861
- return parseEnvFile(resolve(configDir, raw), pathLabel);
862
- }
863
- if (Array.isArray(raw)) {
864
- return parseInlineEnvEntries(raw, pathLabel);
865
- }
866
- return parseEnvMap(raw);
908
+ if (raw == undefined) return {};
909
+ if (typeof raw == "string") {
910
+ return parseEnvFile(resolve(configDir, raw), pathLabel);
911
+ }
912
+ if (Array.isArray(raw)) {
913
+ return parseInlineEnvEntries(raw, pathLabel);
914
+ }
915
+ return parseEnvMap(raw);
867
916
  }
868
917
  function parseInlineEnvEntries(values, pathLabel) {
869
- const env = {};
870
- for (let i = 0; i < values.length; i++) {
871
- const item = values[i];
872
- if (typeof item != "string")
873
- continue;
874
- const separator = item.indexOf("=");
875
- if (separator <= 0) {
876
- throw new Error(`invalid config at ${pathLabel}\nenv entry at index ${i} must use KEY=value format`);
877
- }
878
- const key = item.slice(0, separator).trim();
879
- const value = item.slice(separator + 1);
880
- if (!key.length) {
881
- throw new Error(`invalid config at ${pathLabel}\nenv entry at index ${i} must use a non-empty key`);
882
- }
883
- env[key] = value;
884
- }
885
- return env;
918
+ const env = {};
919
+ for (let i = 0; i < values.length; i++) {
920
+ const item = values[i];
921
+ if (typeof item != "string") continue;
922
+ const separator = item.indexOf("=");
923
+ if (separator <= 0) {
924
+ throw new Error(
925
+ `invalid config at ${pathLabel}\nenv entry at index ${i} must use KEY=value format`,
926
+ );
927
+ }
928
+ const key = item.slice(0, separator).trim();
929
+ const value = item.slice(separator + 1);
930
+ if (!key.length) {
931
+ throw new Error(
932
+ `invalid config at ${pathLabel}\nenv entry at index ${i} must use a non-empty key`,
933
+ );
934
+ }
935
+ env[key] = value;
936
+ }
937
+ return env;
886
938
  }
887
939
  function parseEnvFile(envPath, pathLabel) {
888
- if (!existsSync(envPath)) {
889
- throw new Error(`invalid config at ${pathLabel}\nenv file not found: ${envPath}`);
890
- }
891
- const env = {};
892
- const lines = readFileSync(envPath, "utf8").split(/\r?\n/);
893
- for (let i = 0; i < lines.length; i++) {
894
- const rawLine = lines[i];
895
- const line = rawLine.trim();
896
- if (!line.length || line.startsWith("#"))
897
- continue;
898
- const normalized = line.startsWith("export ") ? line.slice(7).trim() : line;
899
- const separator = normalized.indexOf("=");
900
- if (separator <= 0) {
901
- throw new Error(`invalid config at ${pathLabel}\ninvalid env line ${i + 1} in ${envPath}: expected KEY=value`);
902
- }
903
- const key = normalized.slice(0, separator).trim();
904
- const value = normalized.slice(separator + 1).trim();
905
- env[key] = unquoteEnvValue(value);
906
- }
907
- return env;
940
+ if (!existsSync(envPath)) {
941
+ throw new Error(
942
+ `invalid config at ${pathLabel}\nenv file not found: ${envPath}`,
943
+ );
944
+ }
945
+ const env = {};
946
+ const lines = readFileSync(envPath, "utf8").split(/\r?\n/);
947
+ for (let i = 0; i < lines.length; i++) {
948
+ const rawLine = lines[i];
949
+ const line = rawLine.trim();
950
+ if (!line.length || line.startsWith("#")) continue;
951
+ const normalized = line.startsWith("export ") ? line.slice(7).trim() : line;
952
+ const separator = normalized.indexOf("=");
953
+ if (separator <= 0) {
954
+ throw new Error(
955
+ `invalid config at ${pathLabel}\ninvalid env line ${i + 1} in ${envPath}: expected KEY=value`,
956
+ );
957
+ }
958
+ const key = normalized.slice(0, separator).trim();
959
+ const value = normalized.slice(separator + 1).trim();
960
+ env[key] = unquoteEnvValue(value);
961
+ }
962
+ return env;
908
963
  }
909
964
  function unquoteEnvValue(value) {
910
- if (value.length < 2)
911
- return value;
912
- const quote = value[0];
913
- if ((quote != '"' && quote != "'") || value[value.length - 1] != quote) {
914
- return value;
915
- }
916
- const inner = value.slice(1, -1);
917
- if (quote == "'")
918
- return inner;
919
- return inner
920
- .replace(/\\n/g, "\n")
921
- .replace(/\\r/g, "\r")
922
- .replace(/\\t/g, "\t")
923
- .replace(/\\"/g, '"')
924
- .replace(/\\\\/g, "\\");
965
+ if (value.length < 2) return value;
966
+ const quote = value[0];
967
+ if ((quote != '"' && quote != "'") || value[value.length - 1] != quote) {
968
+ return value;
969
+ }
970
+ const inner = value.slice(1, -1);
971
+ if (quote == "'") return inner;
972
+ return inner
973
+ .replace(/\\n/g, "\n")
974
+ .replace(/\\r/g, "\r")
975
+ .replace(/\\t/g, "\t")
976
+ .replace(/\\"/g, '"')
977
+ .replace(/\\\\/g, "\\");
925
978
  }
926
979
  function normalizePositiveNumber(value, fallback) {
927
- if (typeof value != "number" || !Number.isFinite(value) || value <= 0) {
928
- return fallback;
929
- }
930
- return Math.floor(value);
980
+ if (typeof value != "number" || !Number.isFinite(value) || value <= 0) {
981
+ return fallback;
982
+ }
983
+ return Math.floor(value);
931
984
  }
932
985
  function normalizeNonNegativeNumber(value, fallback) {
933
- if (typeof value != "number" || !Number.isFinite(value) || value < 0) {
934
- return fallback;
935
- }
936
- return Math.floor(value);
986
+ if (typeof value != "number" || !Number.isFinite(value) || value < 0) {
987
+ return fallback;
988
+ }
989
+ return Math.floor(value);
937
990
  }
938
991
  function getConfigMeta(config) {
939
- const meta = CONFIG_META.get(config);
940
- if (!meta) {
941
- throw new Error("missing config metadata");
942
- }
943
- return meta;
992
+ const meta = CONFIG_META.get(config);
993
+ if (!meta) {
994
+ throw new Error("missing config metadata");
995
+ }
996
+ return meta;
944
997
  }
945
998
  function cloneCoverageOptions(coverage) {
946
- if (typeof coverage == "boolean")
947
- return coverage;
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 ?? [])];
952
- cloned.include = [...(coverage.include ?? [])];
953
- cloned.exclude = [...(coverage.exclude ?? [])];
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 ?? [])];
959
- return cloned;
999
+ if (typeof coverage == "boolean") return coverage;
1000
+ const cloned = Object.assign(new CoverageOptions(), coverage);
1001
+ const ignore = coverage.ignore ?? new CoverageIgnoreOptions();
1002
+ cloned.mode = coverage.mode ?? "project";
1003
+ cloned.dependencies = [...(coverage.dependencies ?? [])];
1004
+ cloned.include = [...(coverage.include ?? [])];
1005
+ cloned.exclude = [...(coverage.exclude ?? [])];
1006
+ cloned.ignore = Object.assign(new CoverageIgnoreOptions(), ignore);
1007
+ cloned.ignore.labels = [...(ignore.labels ?? [])];
1008
+ cloned.ignore.names = [...(ignore.names ?? [])];
1009
+ cloned.ignore.locations = [...(ignore.locations ?? [])];
1010
+ cloned.ignore.snippets = [...(ignore.snippets ?? [])];
1011
+ return cloned;
960
1012
  }
961
1013
  function cloneBuildOptions(options) {
962
- const cloned = Object.assign(new BuildOptions(), options);
963
- cloned.args = [...options.args];
964
- cloned.env = { ...options.env };
965
- return cloned;
1014
+ const cloned = Object.assign(new BuildOptions(), options);
1015
+ cloned.args = [...options.args];
1016
+ cloned.env = { ...options.env };
1017
+ return cloned;
966
1018
  }
967
1019
  function cloneRuntime(runtime) {
968
- return Object.assign(new Runtime(), runtime);
1020
+ return Object.assign(new Runtime(), runtime);
969
1021
  }
970
1022
  function cloneReporterConfig(reporter) {
971
- if (typeof reporter == "string")
972
- return reporter;
973
- const cloned = Object.assign(new ReporterConfig(), reporter);
974
- cloned.options = [...(reporter.options ?? [])];
975
- return cloned;
1023
+ if (typeof reporter == "string") return reporter;
1024
+ const cloned = Object.assign(new ReporterConfig(), reporter);
1025
+ cloned.options = [...(reporter.options ?? [])];
1026
+ return cloned;
976
1027
  }
977
1028
  function cloneRunOptions(options) {
978
- const cloned = Object.assign(new RunOptions(), options);
979
- cloned.runtime = cloneRuntime(options.runtime);
980
- cloned.reporter = cloneReporterConfig(options.reporter);
981
- cloned.env = { ...options.env };
982
- return cloned;
1029
+ const cloned = Object.assign(new RunOptions(), options);
1030
+ cloned.runtime = cloneRuntime(options.runtime);
1031
+ cloned.reporter = cloneReporterConfig(options.reporter);
1032
+ cloned.env = { ...options.env };
1033
+ return cloned;
983
1034
  }
984
1035
  function cloneFuzzConfig(config) {
985
- const cloned = Object.assign(new FuzzConfig(), config);
986
- cloned.input = [...config.input];
987
- return cloned;
1036
+ const cloned = Object.assign(new FuzzConfig(), config);
1037
+ cloned.input = [...config.input];
1038
+ return cloned;
988
1039
  }
989
1040
  function cloneModeConfig(config) {
990
- const cloned = new ModeConfig();
991
- cloned.path = config.path;
992
- cloned.config = cloneConfig(config.config);
993
- return cloned;
1041
+ const cloned = new ModeConfig();
1042
+ cloned.path = config.path;
1043
+ cloned.config = cloneConfig(config.config);
1044
+ return cloned;
994
1045
  }
995
1046
  function cloneConfig(config) {
996
- const cloned = Object.assign(new Config(), config);
997
- cloned.input = [...config.input];
998
- cloned.env = { ...config.env };
999
- cloned.buildOptions = cloneBuildOptions(config.buildOptions);
1000
- cloned.runOptions = cloneRunOptions(config.runOptions);
1001
- cloned.fuzz = cloneFuzzConfig(config.fuzz);
1002
- cloned.coverage = cloneCoverageOptions(config.coverage);
1003
- cloned.modes = Object.fromEntries(Object.entries(config.modes).map(([name, mode]) => [
1004
- name,
1005
- cloneModeConfig(mode),
1006
- ]));
1007
- CONFIG_META.set(cloned, getConfigMeta(config));
1008
- return cloned;
1047
+ const cloned = Object.assign(new Config(), config);
1048
+ cloned.input = [...config.input];
1049
+ cloned.env = { ...config.env };
1050
+ cloned.buildOptions = cloneBuildOptions(config.buildOptions);
1051
+ cloned.runOptions = cloneRunOptions(config.runOptions);
1052
+ cloned.fuzz = cloneFuzzConfig(config.fuzz);
1053
+ cloned.coverage = cloneCoverageOptions(config.coverage);
1054
+ cloned.modes = Object.fromEntries(
1055
+ Object.entries(config.modes).map(([name, mode]) => [
1056
+ name,
1057
+ cloneModeConfig(mode),
1058
+ ]),
1059
+ );
1060
+ CONFIG_META.set(cloned, getConfigMeta(config));
1061
+ return cloned;
1009
1062
  }
1010
1063
  function outputOverridesField(raw, field) {
1011
- if (field in raw)
1012
- return true;
1013
- if (!raw.output ||
1014
- typeof raw.output != "object" ||
1015
- Array.isArray(raw.output)) {
1016
- return false;
1017
- }
1018
- const output = raw.output;
1019
- if (field == "outDir")
1020
- return typeof output.build == "string" && output.build.length > 0;
1021
- if (field == "logs")
1022
- return typeof output.logs == "string" && output.logs.length > 0;
1023
- if (field == "coverageDir") {
1024
- return typeof output.coverage == "string" && output.coverage.length > 0;
1025
- }
1026
- return typeof output.snapshots == "string" && output.snapshots.length > 0;
1064
+ if (field in raw) return true;
1065
+ if (
1066
+ !raw.output ||
1067
+ typeof raw.output != "object" ||
1068
+ Array.isArray(raw.output)
1069
+ ) {
1070
+ return false;
1071
+ }
1072
+ const output = raw.output;
1073
+ if (field == "outDir")
1074
+ return typeof output.build == "string" && output.build.length > 0;
1075
+ if (field == "logs")
1076
+ return typeof output.logs == "string" && output.logs.length > 0;
1077
+ if (field == "coverageDir") {
1078
+ return typeof output.coverage == "string" && output.coverage.length > 0;
1079
+ }
1080
+ return typeof output.snapshots == "string" && output.snapshots.length > 0;
1027
1081
  }
1028
1082
  function mergeCoverageIgnoreOptions(base, override, raw) {
1029
- const merged = Object.assign(new CoverageIgnoreOptions(), base);
1030
- merged.labels = [...base.labels];
1031
- merged.names = [...base.names];
1032
- merged.locations = [...base.locations];
1033
- merged.snippets = [...base.snippets];
1034
- if ("labels" in raw)
1035
- merged.labels = [...override.labels];
1036
- if ("names" in raw)
1037
- merged.names = [...override.names];
1038
- if ("locations" in raw)
1039
- merged.locations = [...override.locations];
1040
- if ("snippets" in raw)
1041
- merged.snippets = [...override.snippets];
1042
- return merged;
1083
+ const merged = Object.assign(new CoverageIgnoreOptions(), base);
1084
+ merged.labels = [...base.labels];
1085
+ merged.names = [...base.names];
1086
+ merged.locations = [...base.locations];
1087
+ merged.snippets = [...base.snippets];
1088
+ if ("labels" in raw) merged.labels = [...override.labels];
1089
+ if ("names" in raw) merged.names = [...override.names];
1090
+ if ("locations" in raw) merged.locations = [...override.locations];
1091
+ if ("snippets" in raw) merged.snippets = [...override.snippets];
1092
+ return merged;
1043
1093
  }
1044
1094
  function mergeCoverageConfig(base, override, raw) {
1045
- if (typeof raw == "boolean")
1046
- return override;
1047
- if (!raw || typeof raw != "object" || Array.isArray(raw))
1048
- return cloneCoverageOptions(base);
1049
- const mergedBase = typeof base == "boolean"
1050
- ? Object.assign(new CoverageOptions(), { enabled: base })
1051
- : cloneCoverageOptions(base);
1052
- const overrideOptions = typeof override == "boolean"
1053
- ? Object.assign(new CoverageOptions(), { enabled: override })
1054
- : cloneCoverageOptions(override);
1055
- const rawObject = raw;
1056
- if ("enabled" in rawObject)
1057
- mergedBase.enabled = overrideOptions.enabled;
1058
- if ("mode" in rawObject)
1059
- mergedBase.mode = overrideOptions.mode;
1060
- if ("includeSpecs" in rawObject)
1061
- mergedBase.includeSpecs = overrideOptions.includeSpecs;
1062
- if ("dependencies" in rawObject)
1063
- mergedBase.dependencies = [...overrideOptions.dependencies];
1064
- if ("include" in rawObject)
1065
- mergedBase.include = [...overrideOptions.include];
1066
- if ("exclude" in rawObject)
1067
- mergedBase.exclude = [...overrideOptions.exclude];
1068
- if (rawObject.ignore &&
1069
- typeof rawObject.ignore == "object" &&
1070
- !Array.isArray(rawObject.ignore)) {
1071
- mergedBase.ignore = mergeCoverageIgnoreOptions(mergedBase.ignore, overrideOptions.ignore, rawObject.ignore);
1072
- }
1073
- return mergedBase;
1095
+ if (typeof raw == "boolean") return override;
1096
+ if (!raw || typeof raw != "object" || Array.isArray(raw))
1097
+ return cloneCoverageOptions(base);
1098
+ const mergedBase =
1099
+ typeof base == "boolean"
1100
+ ? Object.assign(new CoverageOptions(), { enabled: base })
1101
+ : cloneCoverageOptions(base);
1102
+ const overrideOptions =
1103
+ typeof override == "boolean"
1104
+ ? Object.assign(new CoverageOptions(), { enabled: override })
1105
+ : cloneCoverageOptions(override);
1106
+ const rawObject = raw;
1107
+ if ("enabled" in rawObject) mergedBase.enabled = overrideOptions.enabled;
1108
+ if ("mode" in rawObject) mergedBase.mode = overrideOptions.mode;
1109
+ if ("includeSpecs" in rawObject)
1110
+ mergedBase.includeSpecs = overrideOptions.includeSpecs;
1111
+ if ("dependencies" in rawObject)
1112
+ mergedBase.dependencies = [...overrideOptions.dependencies];
1113
+ if ("include" in rawObject) mergedBase.include = [...overrideOptions.include];
1114
+ if ("exclude" in rawObject) mergedBase.exclude = [...overrideOptions.exclude];
1115
+ if (
1116
+ rawObject.ignore &&
1117
+ typeof rawObject.ignore == "object" &&
1118
+ !Array.isArray(rawObject.ignore)
1119
+ ) {
1120
+ mergedBase.ignore = mergeCoverageIgnoreOptions(
1121
+ mergedBase.ignore,
1122
+ overrideOptions.ignore,
1123
+ rawObject.ignore,
1124
+ );
1125
+ }
1126
+ return mergedBase;
1074
1127
  }
1075
1128
  function mergeReporterConfigByRaw(base, override, raw) {
1076
- if (typeof raw == "string")
1077
- return override;
1078
- if (!raw || typeof raw != "object" || Array.isArray(raw)) {
1079
- return cloneReporterConfig(base);
1080
- }
1081
- const mergedBase = typeof base == "string"
1082
- ? new ReporterConfig()
1083
- : cloneReporterConfig(base);
1084
- const overrideConfig = typeof override == "string"
1085
- ? new ReporterConfig()
1086
- : cloneReporterConfig(override);
1087
- const rawObject = raw;
1088
- if ("name" in rawObject)
1089
- mergedBase.name = overrideConfig.name;
1090
- if ("options" in rawObject)
1091
- mergedBase.options = [...overrideConfig.options];
1092
- if ("outDir" in rawObject)
1093
- mergedBase.outDir = overrideConfig.outDir;
1094
- if ("outFile" in rawObject)
1095
- mergedBase.outFile = overrideConfig.outFile;
1096
- return mergedBase;
1129
+ if (typeof raw == "string") return override;
1130
+ if (!raw || typeof raw != "object" || Array.isArray(raw)) {
1131
+ return cloneReporterConfig(base);
1132
+ }
1133
+ const mergedBase =
1134
+ typeof base == "string" ? new ReporterConfig() : cloneReporterConfig(base);
1135
+ const overrideConfig =
1136
+ typeof override == "string"
1137
+ ? new ReporterConfig()
1138
+ : cloneReporterConfig(override);
1139
+ const rawObject = raw;
1140
+ if ("name" in rawObject) mergedBase.name = overrideConfig.name;
1141
+ if ("options" in rawObject) mergedBase.options = [...overrideConfig.options];
1142
+ if ("outDir" in rawObject) mergedBase.outDir = overrideConfig.outDir;
1143
+ if ("outFile" in rawObject) mergedBase.outFile = overrideConfig.outFile;
1144
+ return mergedBase;
1097
1145
  }
1098
1146
  function mergeBuildOptions(base, override, raw) {
1099
- const merged = cloneBuildOptions(base);
1100
- if ("cmd" in raw)
1101
- merged.cmd = override.cmd;
1102
- if ("args" in raw)
1103
- merged.args = [...override.args];
1104
- if ("target" in raw)
1105
- merged.target = override.target;
1106
- if ("env" in raw) {
1107
- merged.env = { ...override.env };
1108
- }
1109
- return merged;
1147
+ const merged = cloneBuildOptions(base);
1148
+ if ("cmd" in raw) merged.cmd = override.cmd;
1149
+ if ("args" in raw) merged.args = [...override.args];
1150
+ if ("target" in raw) merged.target = override.target;
1151
+ if ("env" in raw) {
1152
+ merged.env = { ...override.env };
1153
+ }
1154
+ return merged;
1110
1155
  }
1111
1156
  function mergeRunOptions(base, override, raw) {
1112
- const merged = cloneRunOptions(base);
1113
- if ("runtime" in raw || "run" in raw) {
1114
- const runtimeRaw = raw.runtime;
1115
- if ("run" in raw ||
1116
- (runtimeRaw && ("cmd" in runtimeRaw || "run" in runtimeRaw))) {
1117
- merged.runtime.cmd = override.runtime.cmd;
1118
- }
1119
- if (runtimeRaw && "browser" in runtimeRaw) {
1120
- merged.runtime.browser = override.runtime.browser;
1121
- }
1122
- }
1123
- if ("reporter" in raw) {
1124
- merged.reporter = mergeReporterConfigByRaw(merged.reporter, override.reporter, raw.reporter);
1125
- }
1126
- if ("env" in raw) {
1127
- merged.env = { ...override.env };
1128
- }
1129
- return merged;
1157
+ const merged = cloneRunOptions(base);
1158
+ if ("runtime" in raw || "run" in raw) {
1159
+ const runtimeRaw = raw.runtime;
1160
+ if (
1161
+ "run" in raw ||
1162
+ (runtimeRaw && ("cmd" in runtimeRaw || "run" in runtimeRaw))
1163
+ ) {
1164
+ merged.runtime.cmd = override.runtime.cmd;
1165
+ }
1166
+ if (runtimeRaw && "browser" in runtimeRaw) {
1167
+ merged.runtime.browser = override.runtime.browser;
1168
+ }
1169
+ }
1170
+ if ("reporter" in raw) {
1171
+ merged.reporter = mergeReporterConfigByRaw(
1172
+ merged.reporter,
1173
+ override.reporter,
1174
+ raw.reporter,
1175
+ );
1176
+ }
1177
+ if ("env" in raw) {
1178
+ merged.env = { ...override.env };
1179
+ }
1180
+ return merged;
1130
1181
  }
1131
1182
  function mergeFuzzConfig(base, override, raw) {
1132
- const merged = cloneFuzzConfig(base);
1133
- if ("input" in raw)
1134
- merged.input = [...override.input];
1135
- if ("runs" in raw)
1136
- merged.runs = override.runs;
1137
- if ("seed" in raw)
1138
- merged.seed = override.seed;
1139
- if ("maxInputBytes" in raw)
1140
- merged.maxInputBytes = override.maxInputBytes;
1141
- if ("target" in raw)
1142
- merged.target = override.target;
1143
- if ("corpusDir" in raw)
1144
- merged.corpusDir = override.corpusDir;
1145
- if ("crashDir" in raw)
1146
- merged.crashDir = override.crashDir;
1147
- return merged;
1183
+ const merged = cloneFuzzConfig(base);
1184
+ if ("input" in raw) merged.input = [...override.input];
1185
+ if ("runs" in raw) merged.runs = override.runs;
1186
+ if ("seed" in raw) merged.seed = override.seed;
1187
+ if ("maxInputBytes" in raw) merged.maxInputBytes = override.maxInputBytes;
1188
+ if ("target" in raw) merged.target = override.target;
1189
+ if ("corpusDir" in raw) merged.corpusDir = override.corpusDir;
1190
+ if ("crashDir" in raw) merged.crashDir = override.crashDir;
1191
+ return merged;
1148
1192
  }
1149
1193
  function mergeRootConfig(base, override) {
1150
- const merged = cloneConfig(base);
1151
- const raw = getConfigMeta(override).raw;
1152
- if ("$schema" in raw)
1153
- merged.$schema = override.$schema;
1154
- if ("input" in raw)
1155
- merged.input = [...override.input];
1156
- if (outputOverridesField(raw, "outDir"))
1157
- merged.outDir = override.outDir;
1158
- if (outputOverridesField(raw, "logs"))
1159
- merged.logs = override.logs;
1160
- if (outputOverridesField(raw, "coverageDir")) {
1161
- merged.coverageDir = override.coverageDir;
1162
- }
1163
- if (outputOverridesField(raw, "snapshotDir")) {
1164
- merged.snapshotDir = override.snapshotDir;
1165
- }
1166
- if ("config" in raw)
1167
- merged.config = override.config;
1168
- if ("coverage" in raw) {
1169
- merged.coverage = mergeCoverageConfig(merged.coverage, override.coverage, raw.coverage);
1170
- }
1171
- if ("env" in raw) {
1172
- merged.env = { ...override.env };
1173
- }
1174
- if (raw.buildOptions &&
1175
- typeof raw.buildOptions == "object" &&
1176
- !Array.isArray(raw.buildOptions)) {
1177
- merged.buildOptions = mergeBuildOptions(merged.buildOptions, override.buildOptions, raw.buildOptions);
1178
- }
1179
- if (raw.runOptions &&
1180
- typeof raw.runOptions == "object" &&
1181
- !Array.isArray(raw.runOptions)) {
1182
- merged.runOptions = mergeRunOptions(merged.runOptions, override.runOptions, raw.runOptions);
1183
- }
1184
- if (raw.fuzz && typeof raw.fuzz == "object" && !Array.isArray(raw.fuzz)) {
1185
- merged.fuzz = mergeFuzzConfig(merged.fuzz, override.fuzz, raw.fuzz);
1186
- }
1187
- CONFIG_META.set(merged, getConfigMeta(override));
1188
- return merged;
1194
+ const merged = cloneConfig(base);
1195
+ const raw = getConfigMeta(override).raw;
1196
+ if ("$schema" in raw) merged.$schema = override.$schema;
1197
+ if ("input" in raw) merged.input = [...override.input];
1198
+ if (outputOverridesField(raw, "outDir")) merged.outDir = override.outDir;
1199
+ if (outputOverridesField(raw, "logs")) merged.logs = override.logs;
1200
+ if (outputOverridesField(raw, "coverageDir")) {
1201
+ merged.coverageDir = override.coverageDir;
1202
+ }
1203
+ if (outputOverridesField(raw, "snapshotDir")) {
1204
+ merged.snapshotDir = override.snapshotDir;
1205
+ }
1206
+ if ("config" in raw) merged.config = override.config;
1207
+ if ("coverage" in raw) {
1208
+ merged.coverage = mergeCoverageConfig(
1209
+ merged.coverage,
1210
+ override.coverage,
1211
+ raw.coverage,
1212
+ );
1213
+ }
1214
+ if ("env" in raw) {
1215
+ merged.env = { ...override.env };
1216
+ }
1217
+ if (
1218
+ raw.buildOptions &&
1219
+ typeof raw.buildOptions == "object" &&
1220
+ !Array.isArray(raw.buildOptions)
1221
+ ) {
1222
+ merged.buildOptions = mergeBuildOptions(
1223
+ merged.buildOptions,
1224
+ override.buildOptions,
1225
+ raw.buildOptions,
1226
+ );
1227
+ }
1228
+ if (
1229
+ raw.runOptions &&
1230
+ typeof raw.runOptions == "object" &&
1231
+ !Array.isArray(raw.runOptions)
1232
+ ) {
1233
+ merged.runOptions = mergeRunOptions(
1234
+ merged.runOptions,
1235
+ override.runOptions,
1236
+ raw.runOptions,
1237
+ );
1238
+ }
1239
+ if (raw.fuzz && typeof raw.fuzz == "object" && !Array.isArray(raw.fuzz)) {
1240
+ merged.fuzz = mergeFuzzConfig(merged.fuzz, override.fuzz, raw.fuzz);
1241
+ }
1242
+ CONFIG_META.set(merged, getConfigMeta(override));
1243
+ return merged;
1189
1244
  }
1190
1245
  function applyPerModeOutputDefaults(base, merged, override, modeName) {
1191
- const raw = getConfigMeta(override).raw;
1192
- if (!outputOverridesField(raw, "outDir")) {
1193
- merged.outDir = appendPathSegment(base.outDir, modeName);
1194
- }
1195
- if (!outputOverridesField(raw, "logs") && base.logs != "none") {
1196
- merged.logs = appendPathSegment(base.logs, modeName);
1197
- }
1198
- if (!outputOverridesField(raw, "coverageDir") && base.coverageDir != "none") {
1199
- merged.coverageDir = appendPathSegment(base.coverageDir, modeName);
1200
- }
1246
+ const raw = getConfigMeta(override).raw;
1247
+ if (!outputOverridesField(raw, "outDir")) {
1248
+ merged.outDir = appendPathSegment(base.outDir, modeName);
1249
+ }
1250
+ if (!outputOverridesField(raw, "logs") && base.logs != "none") {
1251
+ merged.logs = appendPathSegment(base.logs, modeName);
1252
+ }
1253
+ if (!outputOverridesField(raw, "coverageDir") && base.coverageDir != "none") {
1254
+ merged.coverageDir = appendPathSegment(base.coverageDir, modeName);
1255
+ }
1201
1256
  }
1202
1257
  function resolveModeOverrideConfig(root, modeName) {
1203
- const mode = root.modes[modeName];
1204
- if (!mode) {
1205
- throw new Error(`unknown mode "${modeName}"`);
1206
- }
1207
- if (mode.path) {
1208
- const override = loadConfig(mode.path, false);
1209
- if (Object.keys(override.modes).length) {
1210
- throw new Error(`mode "${modeName}" config file cannot declare nested modes`);
1211
- }
1212
- return override;
1213
- }
1214
- return cloneConfig(mode.config);
1258
+ const mode = root.modes[modeName];
1259
+ if (!mode) {
1260
+ throw new Error(`unknown mode "${modeName}"`);
1261
+ }
1262
+ if (mode.path) {
1263
+ const override = loadConfig(mode.path, false);
1264
+ if (Object.keys(override.modes).length) {
1265
+ throw new Error(
1266
+ `mode "${modeName}" config file cannot declare nested modes`,
1267
+ );
1268
+ }
1269
+ return override;
1270
+ }
1271
+ return cloneConfig(mode.config);
1215
1272
  }
1216
1273
  export function resolveModeNames(rawArgs) {
1217
- const names = [];
1218
- for (let i = 0; i < rawArgs.length; i++) {
1219
- const arg = rawArgs[i];
1220
- if (arg == "--mode") {
1221
- const next = rawArgs[i + 1];
1222
- if (!next || next.startsWith("-"))
1223
- continue;
1224
- i++;
1225
- appendModeTokens(names, next);
1226
- continue;
1227
- }
1228
- if (arg.startsWith("--mode=")) {
1229
- appendModeTokens(names, arg.slice("--mode=".length));
1230
- }
1231
- }
1232
- return [...new Set(names)];
1274
+ const names = [];
1275
+ for (let i = 0; i < rawArgs.length; i++) {
1276
+ const arg = rawArgs[i];
1277
+ if (arg == "--mode") {
1278
+ const next = rawArgs[i + 1];
1279
+ if (!next || next.startsWith("-")) continue;
1280
+ i++;
1281
+ appendModeTokens(names, next);
1282
+ continue;
1283
+ }
1284
+ if (arg.startsWith("--mode=")) {
1285
+ appendModeTokens(names, arg.slice("--mode=".length));
1286
+ }
1287
+ }
1288
+ return [...new Set(names)];
1233
1289
  }
1234
1290
  export function getDefaultModeNames(config) {
1235
- return Object.entries(config.modes)
1236
- .filter(([, mode]) => mode.default !== false)
1237
- .map(([name]) => name);
1291
+ return Object.entries(config.modes)
1292
+ .filter(([, mode]) => mode.default !== false)
1293
+ .map(([name]) => name);
1238
1294
  }
1239
1295
  function appendModeTokens(out, value) {
1240
- for (const token of value.split(",")) {
1241
- const mode = token.trim();
1242
- if (!mode.length)
1243
- continue;
1244
- out.push(mode);
1245
- }
1296
+ for (const token of value.split(",")) {
1297
+ const mode = token.trim();
1298
+ if (!mode.length) continue;
1299
+ out.push(mode);
1300
+ }
1246
1301
  }
1247
1302
  export function applyMode(config, modeName) {
1248
- if (!modeName) {
1249
- const merged = cloneConfig(config);
1250
- merged.outDir = appendPathSegment(config.outDir, "default");
1251
- if (config.logs != "none") {
1252
- merged.logs = appendPathSegment(config.logs, "default");
1253
- }
1254
- if (config.coverageDir != "none") {
1255
- merged.coverageDir = appendPathSegment(config.coverageDir, "default");
1256
- }
1257
- merged.fuzz.crashDir = appendPathSegment(config.fuzz.crashDir, "default");
1258
- merged.fuzz.corpusDir = appendPathSegment(config.fuzz.corpusDir, "default");
1259
- const env = {
1260
- ...process.env,
1261
- ...config.env,
1262
- };
1263
- if (merged.runOptions.runtime.browser.length) {
1264
- env.BROWSER = merged.runOptions.runtime.browser;
1265
- }
1266
- return {
1267
- config: merged,
1268
- env,
1269
- };
1303
+ if (!modeName) {
1304
+ const merged = cloneConfig(config);
1305
+ merged.outDir = appendPathSegment(config.outDir, "default");
1306
+ if (config.logs != "none") {
1307
+ merged.logs = appendPathSegment(config.logs, "default");
1270
1308
  }
1271
- if (!config.modes[modeName]) {
1272
- const known = Object.keys(config.modes);
1273
- const available = known.length ? known.join(", ") : "(none)";
1274
- throw new Error(`unknown mode "${modeName}". Available modes: ${available}`);
1309
+ if (config.coverageDir != "none") {
1310
+ merged.coverageDir = appendPathSegment(config.coverageDir, "default");
1275
1311
  }
1276
- const modeOverride = resolveModeOverrideConfig(config, modeName);
1277
- const merged = mergeRootConfig(config, modeOverride);
1278
- applyPerModeOutputDefaults(config, merged, modeOverride, modeName);
1312
+ merged.fuzz.crashDir = appendPathSegment(config.fuzz.crashDir, "default");
1313
+ merged.fuzz.corpusDir = appendPathSegment(config.fuzz.corpusDir, "default");
1279
1314
  const env = {
1280
- ...process.env,
1281
- ...merged.env,
1315
+ ...process.env,
1316
+ ...config.env,
1282
1317
  };
1283
1318
  if (merged.runOptions.runtime.browser.length) {
1284
- env.BROWSER = merged.runOptions.runtime.browser;
1319
+ env.BROWSER = merged.runOptions.runtime.browser;
1285
1320
  }
1286
1321
  return {
1287
- config: merged,
1288
- env,
1289
- modeName,
1322
+ config: merged,
1323
+ env,
1290
1324
  };
1325
+ }
1326
+ if (!config.modes[modeName]) {
1327
+ const known = Object.keys(config.modes);
1328
+ const available = known.length ? known.join(", ") : "(none)";
1329
+ throw new Error(
1330
+ `unknown mode "${modeName}". Available modes: ${available}`,
1331
+ );
1332
+ }
1333
+ const modeOverride = resolveModeOverrideConfig(config, modeName);
1334
+ const merged = mergeRootConfig(config, modeOverride);
1335
+ applyPerModeOutputDefaults(config, merged, modeOverride, modeName);
1336
+ const env = {
1337
+ ...process.env,
1338
+ ...merged.env,
1339
+ };
1340
+ if (merged.runOptions.runtime.browser.length) {
1341
+ env.BROWSER = merged.runOptions.runtime.browser;
1342
+ }
1343
+ return {
1344
+ config: merged,
1345
+ env,
1346
+ modeName,
1347
+ };
1291
1348
  }
1292
1349
  function appendPathSegment(basePath, segment) {
1293
- return join(basePath, segment);
1350
+ return join(basePath, segment);
1294
1351
  }
1295
1352
  export function getCliVersion() {
1296
- const candidates = [
1297
- join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
1298
- join(process.cwd(), "package.json"),
1299
- ];
1300
- for (const pkgPath of candidates) {
1301
- if (!existsSync(pkgPath))
1302
- continue;
1303
- try {
1304
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1305
- if (pkg.version)
1306
- return pkg.version;
1307
- }
1308
- catch {
1309
- // ignore invalid package metadata and continue to fallback candidate
1310
- }
1353
+ const candidates = [
1354
+ join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
1355
+ join(process.cwd(), "package.json"),
1356
+ ];
1357
+ for (const pkgPath of candidates) {
1358
+ if (!existsSync(pkgPath)) continue;
1359
+ try {
1360
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1361
+ if (pkg.version) return pkg.version;
1362
+ } catch {
1363
+ // ignore invalid package metadata and continue to fallback candidate
1311
1364
  }
1312
- return "0.0.0";
1365
+ }
1366
+ return "0.0.0";
1313
1367
  }
1314
1368
  export function getPkgRunner() {
1315
- const userAgent = process.env.npm_config_user_agent ?? "";
1316
- if (userAgent.startsWith("pnpm"))
1317
- return "pnpx";
1318
- if (userAgent.startsWith("yarn"))
1319
- return "yarn";
1320
- if (userAgent.startsWith("bun"))
1321
- return "bunx";
1322
- return "npx";
1369
+ const userAgent = process.env.npm_config_user_agent ?? "";
1370
+ if (userAgent.startsWith("pnpm")) return "pnpx";
1371
+ if (userAgent.startsWith("yarn")) return "yarn";
1372
+ if (userAgent.startsWith("bun")) return "bunx";
1373
+ return "npx";
1323
1374
  }
1324
1375
  export function getExec(exec) {
1325
- const PATH = (process.env.PATH ?? "").split(delimiter);
1326
- for (const pathDir of PATH) {
1327
- const fullPath = join(pathDir, exec + (process.platform === "win32" ? ".exe" : ""));
1328
- if (existsSync(fullPath)) {
1329
- return fullPath;
1330
- }
1331
- }
1332
- return null;
1376
+ const PATH = (process.env.PATH ?? "").split(delimiter);
1377
+ for (const pathDir of PATH) {
1378
+ const fullPath = join(
1379
+ pathDir,
1380
+ exec + (process.platform === "win32" ? ".exe" : ""),
1381
+ );
1382
+ if (existsSync(fullPath)) {
1383
+ return fullPath;
1384
+ }
1385
+ }
1386
+ return null;
1333
1387
  }
1334
1388
  export function tokenizeCommand(command) {
1335
- const out = [];
1336
- let current = "";
1337
- let quote = null;
1338
- let escaping = false;
1339
- for (let i = 0; i < command.length; i++) {
1340
- const ch = command[i];
1341
- if (escaping) {
1342
- current += ch;
1343
- escaping = false;
1344
- continue;
1345
- }
1346
- if (ch == "\\") {
1347
- if (quote == "'") {
1348
- current += ch;
1349
- }
1350
- else {
1351
- escaping = true;
1352
- }
1353
- continue;
1354
- }
1355
- if (quote) {
1356
- if (ch == quote) {
1357
- quote = null;
1358
- }
1359
- else {
1360
- current += ch;
1361
- }
1362
- continue;
1363
- }
1364
- if (ch == '"' || ch == "'") {
1365
- quote = ch;
1366
- continue;
1367
- }
1368
- if (/\s/.test(ch)) {
1369
- if (current.length) {
1370
- out.push(current);
1371
- current = "";
1372
- }
1373
- continue;
1374
- }
1375
- current += ch;
1376
- }
1389
+ const out = [];
1390
+ let current = "";
1391
+ let quote = null;
1392
+ let escaping = false;
1393
+ for (let i = 0; i < command.length; i++) {
1394
+ const ch = command[i];
1377
1395
  if (escaping) {
1378
- current += "\\";
1396
+ current += ch;
1397
+ escaping = false;
1398
+ continue;
1399
+ }
1400
+ if (ch == "\\") {
1401
+ if (quote == "'") {
1402
+ current += ch;
1403
+ } else {
1404
+ escaping = true;
1405
+ }
1406
+ continue;
1379
1407
  }
1380
1408
  if (quote) {
1381
- throw new Error(`unterminated quote in command: ${command}`);
1409
+ if (ch == quote) {
1410
+ quote = null;
1411
+ } else {
1412
+ current += ch;
1413
+ }
1414
+ continue;
1382
1415
  }
1383
- if (current.length) {
1384
- out.push(current);
1416
+ if (ch == '"' || ch == "'") {
1417
+ quote = ch;
1418
+ continue;
1385
1419
  }
1386
- return out;
1420
+ if (/\s/.test(ch)) {
1421
+ if (current.length) {
1422
+ out.push(current);
1423
+ current = "";
1424
+ }
1425
+ continue;
1426
+ }
1427
+ current += ch;
1428
+ }
1429
+ if (escaping) {
1430
+ current += "\\";
1431
+ }
1432
+ if (quote) {
1433
+ throw new Error(`unterminated quote in command: ${command}`);
1434
+ }
1435
+ if (current.length) {
1436
+ out.push(current);
1437
+ }
1438
+ return out;
1387
1439
  }
1388
1440
  export function resolveProjectModule(specifier) {
1389
- const cwdRequire = createRequire(join(process.cwd(), "package.json"));
1390
- const localRequire = createRequire(import.meta.url);
1391
- for (const req of [cwdRequire, localRequire]) {
1392
- try {
1393
- return req.resolve(specifier);
1394
- }
1395
- catch {
1396
- // try next resolver
1397
- }
1441
+ const cwdRequire = createRequire(join(process.cwd(), "package.json"));
1442
+ const localRequire = createRequire(import.meta.url);
1443
+ for (const req of [cwdRequire, localRequire]) {
1444
+ try {
1445
+ return req.resolve(specifier);
1446
+ } catch {
1447
+ // try next resolver
1398
1448
  }
1399
- return null;
1449
+ }
1450
+ return null;
1400
1451
  }