as-test 1.0.13 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/util.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
- import { BuildOptions, Config, CoverageOptions, FuzzConfig, ModeConfig, ReporterConfig, RunOptions, Runtime, } from "./types.js";
2
+ import { BuildOptions, Config, CoverageOptions, CoverageIgnoreOptions, FuzzConfig, ModeConfig, ReporterConfig, RunOptions, Runtime, } from "./types.js";
3
3
  import chalk from "chalk";
4
4
  import { createRequire } from "module";
5
5
  import { delimiter, dirname, join, resolve } from "path";
6
6
  import { fileURLToPath } from "url";
7
+ const CONFIG_META = new WeakMap();
7
8
  export function formatTime(ms) {
8
9
  if (ms < 0) {
9
10
  throw new Error("Time should be a non-negative number.");
@@ -28,110 +29,121 @@ export function formatTime(ms) {
28
29
  return `${us}us`;
29
30
  }
30
31
  export function loadConfig(CONFIG_PATH, warn = false) {
31
- if (!existsSync(CONFIG_PATH)) {
32
- if (warn)
32
+ const resolvedPath = resolve(CONFIG_PATH);
33
+ const raw = readConfigRaw(resolvedPath, warn);
34
+ return parseConfigRaw(raw, resolvedPath);
35
+ }
36
+ function readConfigRaw(configPath, warn) {
37
+ if (!existsSync(configPath)) {
38
+ if (warn) {
33
39
  console.log(`${chalk.bgMagentaBright(" WARN ")}${chalk.dim(":")} Could not locate config file in the current directory! Continuing with default config.`);
34
- return new Config();
35
- }
36
- else {
37
- const rawText = readFileSync(CONFIG_PATH, "utf8");
38
- let parsed;
39
- try {
40
- parsed = JSON.parse(rawText);
41
40
  }
42
- catch (error) {
43
- const message = error instanceof Error ? error.message : String(error);
44
- throw new Error(`invalid config JSON at ${CONFIG_PATH}\n${message}\nfix JSON syntax and rerun.`);
45
- }
46
- if (!parsed || typeof parsed != "object" || Array.isArray(parsed)) {
47
- throw new Error(`invalid config at ${CONFIG_PATH}\nroot value must be an object. Example: { "input": ["./assembly/__tests__/*.spec.ts"] }`);
48
- }
49
- const raw = parsed;
50
- validateConfig(raw, CONFIG_PATH);
51
- const configDir = dirname(CONFIG_PATH);
52
- const config = Object.assign(new Config(), raw);
53
- applyOutputConfig(raw.output, raw, config);
54
- config.env = parseEnvValue(raw.env, configDir, "$.env");
55
- const runOptionsRaw = raw.runOptions ?? {};
56
- config.buildOptions = Object.assign(new BuildOptions(), raw.buildOptions ?? {});
57
- config.buildOptions.cmd =
58
- typeof config.buildOptions.cmd == "string" ? config.buildOptions.cmd : "";
59
- config.buildOptions.args = Array.isArray(config.buildOptions.args)
60
- ? config.buildOptions.args.filter((item) => typeof item == "string")
41
+ return {};
42
+ }
43
+ const rawText = readFileSync(configPath, "utf8");
44
+ let parsed;
45
+ try {
46
+ parsed = JSON.parse(rawText);
47
+ }
48
+ catch (error) {
49
+ const message = error instanceof Error ? error.message : String(error);
50
+ throw new Error(`invalid config JSON at ${configPath}\n${message}\nfix JSON syntax and rerun.`);
51
+ }
52
+ if (!parsed || typeof parsed != "object" || Array.isArray(parsed)) {
53
+ throw new Error(`invalid config at ${configPath}\nroot value must be an object. Example: { "input": ["./assembly/__tests__/*.spec.ts"] }`);
54
+ }
55
+ const raw = parsed;
56
+ validateConfig(raw, configPath);
57
+ return raw;
58
+ }
59
+ function parseConfigRaw(raw, configPath) {
60
+ const configDir = dirname(configPath);
61
+ const config = Object.assign(new Config(), raw);
62
+ applyOutputConfig(raw.output, raw, config);
63
+ config.env = parseEnvValue(raw.env, configDir, "$.env");
64
+ const runOptionsRaw = raw.runOptions ?? {};
65
+ config.buildOptions = Object.assign(new BuildOptions(), raw.buildOptions ?? {});
66
+ config.buildOptions.cmd =
67
+ typeof config.buildOptions.cmd == "string" ? config.buildOptions.cmd : "";
68
+ config.buildOptions.args = Array.isArray(config.buildOptions.args)
69
+ ? config.buildOptions.args.filter((item) => typeof item == "string")
70
+ : [];
71
+ config.buildOptions.env = parseEnvValue(raw.buildOptions?.env, configDir, "$.buildOptions.env");
72
+ config.buildOptions.target =
73
+ typeof config.buildOptions.target == "string" &&
74
+ config.buildOptions.target.length
75
+ ? config.buildOptions.target
76
+ : "wasi";
77
+ config.runOptions = Object.assign(new RunOptions(), runOptionsRaw);
78
+ const reporterRaw = runOptionsRaw.reporter;
79
+ if (typeof reporterRaw == "string") {
80
+ config.runOptions.reporter = reporterRaw;
81
+ }
82
+ else if (reporterRaw && typeof reporterRaw == "object") {
83
+ const reporterConfig = Object.assign(new ReporterConfig(), reporterRaw);
84
+ reporterConfig.name =
85
+ typeof reporterConfig.name == "string" ? reporterConfig.name : "";
86
+ reporterConfig.options = Array.isArray(reporterConfig.options)
87
+ ? reporterConfig.options.filter((value) => typeof value == "string")
61
88
  : [];
62
- config.buildOptions.env = parseEnvValue(raw.buildOptions?.env, configDir, "$.buildOptions.env");
63
- config.buildOptions.target =
64
- typeof config.buildOptions.target == "string" &&
65
- config.buildOptions.target.length
66
- ? config.buildOptions.target
67
- : "wasi";
68
- config.runOptions = Object.assign(new RunOptions(), runOptionsRaw);
69
- const reporterRaw = runOptionsRaw.reporter;
70
- if (typeof reporterRaw == "string") {
71
- config.runOptions.reporter = reporterRaw;
72
- }
73
- else if (reporterRaw && typeof reporterRaw == "object") {
74
- const reporterConfig = Object.assign(new ReporterConfig(), reporterRaw);
75
- reporterConfig.name =
76
- typeof reporterConfig.name == "string" ? reporterConfig.name : "";
77
- reporterConfig.options = Array.isArray(reporterConfig.options)
78
- ? reporterConfig.options.filter((value) => typeof value == "string")
79
- : [];
80
- reporterConfig.outDir =
81
- typeof reporterConfig.outDir == "string" ? reporterConfig.outDir : "";
82
- reporterConfig.outFile =
83
- typeof reporterConfig.outFile == "string" ? reporterConfig.outFile : "";
84
- config.runOptions.reporter = reporterConfig;
85
- }
86
- else {
87
- config.runOptions.reporter = "";
88
- }
89
- const runtimeRaw = runOptionsRaw.runtime;
90
- const runtime = new Runtime();
91
- const legacyRun = typeof runOptionsRaw.run == "string" && runOptionsRaw.run.length
92
- ? runOptionsRaw.run
93
- : "";
94
- const cmd = runtimeRaw && typeof runtimeRaw.cmd == "string" && runtimeRaw.cmd.length
95
- ? runtimeRaw.cmd
96
- : runtimeRaw &&
97
- typeof runtimeRaw.run == "string" &&
98
- runtimeRaw.run.length
99
- ? runtimeRaw.run
100
- : legacyRun
101
- ? legacyRun
102
- : runtime.cmd;
103
- runtime.cmd = cmd;
104
- runtime.browser =
105
- runtimeRaw && typeof runtimeRaw.browser == "string"
106
- ? runtimeRaw.browser
107
- : "";
108
- config.runOptions.runtime = runtime;
109
- config.runOptions.env = parseEnvValue(runOptionsRaw.env, configDir, "$.runOptions.env");
110
- const fuzzRaw = raw.fuzz ?? {};
111
- config.fuzz = Object.assign(new FuzzConfig(), fuzzRaw);
112
- config.fuzz.input = Array.isArray(config.fuzz.input)
113
- ? config.fuzz.input.filter((item) => typeof item == "string")
114
- : typeof fuzzRaw.input == "string"
115
- ? [fuzzRaw.input]
116
- : new FuzzConfig().input;
117
- config.fuzz.runs = normalizePositiveNumber(config.fuzz.runs, 1000);
118
- config.fuzz.seed = normalizeNonNegativeNumber(config.fuzz.seed, -1);
119
- config.fuzz.maxInputBytes = normalizePositiveNumber(config.fuzz.maxInputBytes, 4096);
120
- config.fuzz.target =
121
- typeof config.fuzz.target == "string" && config.fuzz.target.length
122
- ? config.fuzz.target
123
- : "bindings";
124
- config.fuzz.corpusDir =
125
- typeof config.fuzz.corpusDir == "string" && config.fuzz.corpusDir.length
126
- ? config.fuzz.corpusDir
127
- : "./.as-test/fuzz/corpus";
128
- config.fuzz.crashDir =
129
- typeof config.fuzz.crashDir == "string" && config.fuzz.crashDir.length
130
- ? config.fuzz.crashDir
131
- : "./.as-test/crashes";
132
- config.modes = parseModes(raw.modes, configDir);
133
- return config;
89
+ reporterConfig.outDir =
90
+ typeof reporterConfig.outDir == "string" ? reporterConfig.outDir : "";
91
+ reporterConfig.outFile =
92
+ typeof reporterConfig.outFile == "string" ? reporterConfig.outFile : "";
93
+ config.runOptions.reporter = reporterConfig;
134
94
  }
95
+ else {
96
+ config.runOptions.reporter = "";
97
+ }
98
+ const runtimeRaw = runOptionsRaw.runtime;
99
+ const runtime = new Runtime();
100
+ const legacyRun = typeof runOptionsRaw.run == "string" && runOptionsRaw.run.length
101
+ ? runOptionsRaw.run
102
+ : "";
103
+ const cmd = runtimeRaw && typeof runtimeRaw.cmd == "string" && runtimeRaw.cmd.length
104
+ ? runtimeRaw.cmd
105
+ : runtimeRaw &&
106
+ typeof runtimeRaw.run == "string" &&
107
+ runtimeRaw.run.length
108
+ ? runtimeRaw.run
109
+ : legacyRun
110
+ ? legacyRun
111
+ : runtime.cmd;
112
+ runtime.cmd = cmd;
113
+ runtime.browser =
114
+ runtimeRaw && typeof runtimeRaw.browser == "string"
115
+ ? runtimeRaw.browser
116
+ : "";
117
+ config.runOptions.runtime = runtime;
118
+ config.runOptions.env = parseEnvValue(runOptionsRaw.env, configDir, "$.runOptions.env");
119
+ const fuzzRaw = raw.fuzz ?? {};
120
+ config.fuzz = Object.assign(new FuzzConfig(), fuzzRaw);
121
+ config.fuzz.input = Array.isArray(config.fuzz.input)
122
+ ? config.fuzz.input.filter((item) => typeof item == "string")
123
+ : typeof fuzzRaw.input == "string"
124
+ ? [fuzzRaw.input]
125
+ : new FuzzConfig().input;
126
+ config.fuzz.runs = normalizePositiveNumber(config.fuzz.runs, 1000);
127
+ config.fuzz.seed = normalizeNonNegativeNumber(config.fuzz.seed, -1);
128
+ config.fuzz.maxInputBytes = normalizePositiveNumber(config.fuzz.maxInputBytes, 4096);
129
+ config.fuzz.target =
130
+ typeof config.fuzz.target == "string" && config.fuzz.target.length
131
+ ? config.fuzz.target
132
+ : "bindings";
133
+ config.fuzz.corpusDir =
134
+ typeof config.fuzz.corpusDir == "string" && config.fuzz.corpusDir.length
135
+ ? config.fuzz.corpusDir
136
+ : "./.as-test/fuzz/corpus";
137
+ config.fuzz.crashDir =
138
+ typeof config.fuzz.crashDir == "string" && config.fuzz.crashDir.length
139
+ ? config.fuzz.crashDir
140
+ : "./.as-test/crashes";
141
+ config.modes = parseModes(raw.modes, configDir);
142
+ CONFIG_META.set(config, {
143
+ sourcePath: configPath,
144
+ raw,
145
+ });
146
+ return config;
135
147
  }
136
148
  const TOP_LEVEL_KEYS = new Set([
137
149
  "$schema",
@@ -163,17 +175,7 @@ const FUZZ_OPTION_KEYS = new Set([
163
175
  "corpusDir",
164
176
  "crashDir",
165
177
  ]);
166
- const MODE_KEYS = new Set([
167
- "outDir",
168
- "logs",
169
- "coverageDir",
170
- "snapshotDir",
171
- "config",
172
- "coverage",
173
- "buildOptions",
174
- "runOptions",
175
- "env",
176
- ]);
178
+ const MODE_KEYS = new Set([...TOP_LEVEL_KEYS].filter((key) => key != "modes"));
177
179
  function validateConfig(raw, configPath) {
178
180
  const issues = [];
179
181
  validateUnknownKeys(raw, TOP_LEVEL_KEYS, "$", issues);
@@ -629,22 +631,36 @@ function validateModesField(raw, key, pathPrefix, issues) {
629
631
  return;
630
632
  }
631
633
  for (const [modeName, modeRaw] of Object.entries(value)) {
634
+ if (typeof modeRaw == "string") {
635
+ if (!modeRaw.length) {
636
+ issues.push({
637
+ path: `${pathPrefix}.${key}.${modeName}`,
638
+ message: "must not be an empty string",
639
+ fix: 'set to a config file path like "./as-test.config.simd.json"',
640
+ });
641
+ }
642
+ continue;
643
+ }
632
644
  if (!modeRaw || typeof modeRaw != "object" || Array.isArray(modeRaw)) {
633
645
  issues.push({
634
646
  path: `${pathPrefix}.${key}.${modeName}`,
635
- message: "must be an object",
647
+ message: "must be a config object or config file path string",
636
648
  });
637
649
  continue;
638
650
  }
639
651
  const modeObj = modeRaw;
640
652
  const modePath = `${pathPrefix}.${key}.${modeName}`;
641
653
  validateUnknownKeys(modeObj, MODE_KEYS, modePath, issues);
654
+ validateStringField(modeObj, "$schema", modePath, issues);
655
+ validateInputField(modeObj, "input", modePath, issues);
656
+ validateOutputField(modeObj, "output", modePath, issues);
642
657
  validateStringField(modeObj, "outDir", modePath, issues);
643
658
  validateStringField(modeObj, "logs", modePath, issues);
644
659
  validateStringField(modeObj, "coverageDir", modePath, issues);
645
660
  validateStringField(modeObj, "snapshotDir", modePath, issues);
646
661
  validateStringField(modeObj, "config", modePath, issues);
647
662
  validateCoverageField(modeObj, "coverage", modePath, issues);
663
+ validateFuzzField(modeObj, "fuzz", modePath, issues);
648
664
  validateEnvField(modeObj, "env", modePath, issues);
649
665
  validateBuildOptionsField(modeObj, "buildOptions", modePath, issues);
650
666
  validateRunOptionsField(modeObj, "runOptions", modePath, issues);
@@ -774,84 +790,16 @@ function parseModes(raw, configDir) {
774
790
  const out = {};
775
791
  const entries = Object.entries(raw);
776
792
  for (const [name, value] of entries) {
777
- if (!value || typeof value != "object" || Array.isArray(value))
778
- continue;
779
- const modeRaw = value;
780
793
  const mode = new ModeConfig();
781
- if (typeof modeRaw.outDir == "string" && modeRaw.outDir.length) {
782
- mode.outDir = modeRaw.outDir;
783
- }
784
- if (typeof modeRaw.logs == "string" && modeRaw.logs.length) {
785
- mode.logs = modeRaw.logs;
786
- }
787
- if (typeof modeRaw.coverageDir == "string" && modeRaw.coverageDir.length) {
788
- mode.coverageDir = modeRaw.coverageDir;
789
- }
790
- if (typeof modeRaw.snapshotDir == "string" && modeRaw.snapshotDir.length) {
791
- mode.snapshotDir = modeRaw.snapshotDir;
792
- }
793
- if (typeof modeRaw.config == "string" && modeRaw.config.length) {
794
- mode.config = modeRaw.config;
795
- }
796
- if (typeof modeRaw.coverage == "boolean") {
797
- mode.coverage = modeRaw.coverage;
798
- }
799
- else if (modeRaw.coverage && typeof modeRaw.coverage == "object") {
800
- mode.coverage = Object.assign(new CoverageOptions(), modeRaw.coverage);
801
- }
802
- if (modeRaw.buildOptions && typeof modeRaw.buildOptions == "object") {
803
- const buildRaw = modeRaw.buildOptions;
804
- const build = {};
805
- if (typeof buildRaw.cmd == "string") {
806
- build.cmd = buildRaw.cmd;
807
- }
808
- if (Array.isArray(buildRaw.args)) {
809
- build.args = buildRaw.args.filter((item) => typeof item == "string");
810
- }
811
- build.env = parseEnvValue(buildRaw.env, configDir, `$.modes.${name}.buildOptions.env`);
812
- if (typeof buildRaw.target == "string" && buildRaw.target.length) {
813
- build.target = buildRaw.target;
814
- }
815
- mode.buildOptions = build;
816
- }
817
- if (modeRaw.runOptions && typeof modeRaw.runOptions == "object") {
818
- const runRaw = modeRaw.runOptions;
819
- const run = {};
820
- if (runRaw.runtime && typeof runRaw.runtime == "object") {
821
- const runtimeRaw = runRaw.runtime;
822
- const runtime = new Runtime();
823
- if (typeof runtimeRaw.cmd == "string" && runtimeRaw.cmd.length) {
824
- runtime.cmd = runtimeRaw.cmd;
825
- }
826
- else if (typeof runtimeRaw.run == "string" && runtimeRaw.run.length) {
827
- runtime.cmd = runtimeRaw.run;
828
- }
829
- else {
830
- runtime.cmd = "";
831
- }
832
- runtime.browser =
833
- typeof runtimeRaw.browser == "string" ? runtimeRaw.browser : "";
834
- run.runtime = runtime;
835
- }
836
- if (typeof runRaw.reporter == "string") {
837
- run.reporter = runRaw.reporter;
838
- }
839
- else if (runRaw.reporter && typeof runRaw.reporter == "object") {
840
- const reporter = Object.assign(new ReporterConfig(), runRaw.reporter);
841
- reporter.name = typeof reporter.name == "string" ? reporter.name : "";
842
- reporter.options = Array.isArray(reporter.options)
843
- ? reporter.options.filter((item) => typeof item == "string")
844
- : [];
845
- reporter.outDir =
846
- typeof reporter.outDir == "string" ? reporter.outDir : "";
847
- reporter.outFile =
848
- typeof reporter.outFile == "string" ? reporter.outFile : "";
849
- run.reporter = reporter;
850
- }
851
- run.env = parseEnvValue(runRaw.env, configDir, `$.modes.${name}.runOptions.env`);
852
- mode.runOptions = run;
794
+ if (typeof value == "string") {
795
+ mode.path = resolve(configDir, value);
796
+ mode.config = parseConfigRaw({}, join(configDir, `__mode__.${name}.json`));
797
+ out[name] = mode;
798
+ continue;
853
799
  }
854
- mode.env = parseEnvValue(modeRaw.env, configDir, `$.modes.${name}.env`);
800
+ if (!value || typeof value != "object" || Array.isArray(value))
801
+ continue;
802
+ mode.config = parseConfigRaw(value, join(configDir, `__mode__.${name}.json`));
855
803
  out[name] = mode;
856
804
  }
857
805
  return out;
@@ -947,6 +895,209 @@ function normalizeNonNegativeNumber(value, fallback) {
947
895
  }
948
896
  return Math.floor(value);
949
897
  }
898
+ function getConfigMeta(config) {
899
+ const meta = CONFIG_META.get(config);
900
+ if (!meta) {
901
+ throw new Error("missing config metadata");
902
+ }
903
+ return meta;
904
+ }
905
+ function cloneCoverageOptions(coverage) {
906
+ if (typeof coverage == "boolean")
907
+ return coverage;
908
+ const cloned = Object.assign(new CoverageOptions(), coverage);
909
+ cloned.include = [...(coverage.include ?? [])];
910
+ cloned.exclude = [...(coverage.exclude ?? [])];
911
+ cloned.ignore = Object.assign(new CoverageIgnoreOptions(), coverage.ignore);
912
+ cloned.ignore.labels = [...(coverage.ignore.labels ?? [])];
913
+ cloned.ignore.names = [...(coverage.ignore.names ?? [])];
914
+ cloned.ignore.locations = [...(coverage.ignore.locations ?? [])];
915
+ cloned.ignore.snippets = [...(coverage.ignore.snippets ?? [])];
916
+ return cloned;
917
+ }
918
+ function cloneBuildOptions(options) {
919
+ const cloned = Object.assign(new BuildOptions(), options);
920
+ cloned.args = [...options.args];
921
+ cloned.env = { ...options.env };
922
+ return cloned;
923
+ }
924
+ function cloneRuntime(runtime) {
925
+ return Object.assign(new Runtime(), runtime);
926
+ }
927
+ function cloneReporterConfig(reporter) {
928
+ if (typeof reporter == "string")
929
+ return reporter;
930
+ const cloned = Object.assign(new ReporterConfig(), reporter);
931
+ cloned.options = [...(reporter.options ?? [])];
932
+ return cloned;
933
+ }
934
+ function cloneRunOptions(options) {
935
+ const cloned = Object.assign(new RunOptions(), options);
936
+ cloned.runtime = cloneRuntime(options.runtime);
937
+ cloned.reporter = cloneReporterConfig(options.reporter);
938
+ cloned.env = { ...options.env };
939
+ return cloned;
940
+ }
941
+ function cloneFuzzConfig(config) {
942
+ const cloned = Object.assign(new FuzzConfig(), config);
943
+ cloned.input = [...config.input];
944
+ return cloned;
945
+ }
946
+ function cloneModeConfig(config) {
947
+ const cloned = new ModeConfig();
948
+ cloned.path = config.path;
949
+ cloned.config = cloneConfig(config.config);
950
+ return cloned;
951
+ }
952
+ function cloneConfig(config) {
953
+ const cloned = Object.assign(new Config(), config);
954
+ cloned.input = [...config.input];
955
+ cloned.env = { ...config.env };
956
+ cloned.buildOptions = cloneBuildOptions(config.buildOptions);
957
+ cloned.runOptions = cloneRunOptions(config.runOptions);
958
+ cloned.fuzz = cloneFuzzConfig(config.fuzz);
959
+ cloned.coverage = cloneCoverageOptions(config.coverage);
960
+ cloned.modes = Object.fromEntries(Object.entries(config.modes).map(([name, mode]) => [name, cloneModeConfig(mode)]));
961
+ CONFIG_META.set(cloned, getConfigMeta(config));
962
+ return cloned;
963
+ }
964
+ function outputOverridesField(raw, field) {
965
+ if (field in raw)
966
+ return true;
967
+ if (!raw.output || typeof raw.output != "object" || Array.isArray(raw.output)) {
968
+ return false;
969
+ }
970
+ const output = raw.output;
971
+ if (field == "outDir")
972
+ return typeof output.build == "string" && output.build.length > 0;
973
+ if (field == "logs")
974
+ return typeof output.logs == "string" && output.logs.length > 0;
975
+ if (field == "coverageDir") {
976
+ return typeof output.coverage == "string" && output.coverage.length > 0;
977
+ }
978
+ return typeof output.snapshots == "string" && output.snapshots.length > 0;
979
+ }
980
+ function mergeBuildOptions(base, override, raw) {
981
+ const merged = cloneBuildOptions(base);
982
+ if ("cmd" in raw)
983
+ merged.cmd = override.cmd;
984
+ if ("args" in raw)
985
+ merged.args = [...override.args];
986
+ if ("target" in raw)
987
+ merged.target = override.target;
988
+ if ("env" in raw) {
989
+ merged.env = {
990
+ ...merged.env,
991
+ ...override.env,
992
+ };
993
+ }
994
+ return merged;
995
+ }
996
+ function mergeRunOptions(base, override, raw) {
997
+ const merged = cloneRunOptions(base);
998
+ if ("runtime" in raw || "run" in raw) {
999
+ const runtimeRaw = raw.runtime;
1000
+ if ("run" in raw || (runtimeRaw && ("cmd" in runtimeRaw || "run" in runtimeRaw))) {
1001
+ merged.runtime.cmd = override.runtime.cmd;
1002
+ }
1003
+ if (runtimeRaw && "browser" in runtimeRaw) {
1004
+ merged.runtime.browser = override.runtime.browser;
1005
+ }
1006
+ }
1007
+ if ("reporter" in raw) {
1008
+ merged.reporter = cloneReporterConfig(override.reporter);
1009
+ }
1010
+ if ("env" in raw) {
1011
+ merged.env = {
1012
+ ...merged.env,
1013
+ ...override.env,
1014
+ };
1015
+ }
1016
+ return merged;
1017
+ }
1018
+ function mergeFuzzConfig(base, override, raw) {
1019
+ const merged = cloneFuzzConfig(base);
1020
+ if ("input" in raw)
1021
+ merged.input = [...override.input];
1022
+ if ("runs" in raw)
1023
+ merged.runs = override.runs;
1024
+ if ("seed" in raw)
1025
+ merged.seed = override.seed;
1026
+ if ("maxInputBytes" in raw)
1027
+ merged.maxInputBytes = override.maxInputBytes;
1028
+ if ("target" in raw)
1029
+ merged.target = override.target;
1030
+ if ("corpusDir" in raw)
1031
+ merged.corpusDir = override.corpusDir;
1032
+ if ("crashDir" in raw)
1033
+ merged.crashDir = override.crashDir;
1034
+ return merged;
1035
+ }
1036
+ function mergeRootConfig(base, override) {
1037
+ const merged = cloneConfig(base);
1038
+ const raw = getConfigMeta(override).raw;
1039
+ if ("$schema" in raw)
1040
+ merged.$schema = override.$schema;
1041
+ if ("input" in raw)
1042
+ merged.input = [...override.input];
1043
+ if (outputOverridesField(raw, "outDir"))
1044
+ merged.outDir = override.outDir;
1045
+ if (outputOverridesField(raw, "logs"))
1046
+ merged.logs = override.logs;
1047
+ if (outputOverridesField(raw, "coverageDir")) {
1048
+ merged.coverageDir = override.coverageDir;
1049
+ }
1050
+ if (outputOverridesField(raw, "snapshotDir")) {
1051
+ merged.snapshotDir = override.snapshotDir;
1052
+ }
1053
+ if ("config" in raw)
1054
+ merged.config = override.config;
1055
+ if ("coverage" in raw)
1056
+ merged.coverage = cloneCoverageOptions(override.coverage);
1057
+ if ("env" in raw) {
1058
+ merged.env = {
1059
+ ...merged.env,
1060
+ ...override.env,
1061
+ };
1062
+ }
1063
+ if (raw.buildOptions && typeof raw.buildOptions == "object" && !Array.isArray(raw.buildOptions)) {
1064
+ merged.buildOptions = mergeBuildOptions(merged.buildOptions, override.buildOptions, raw.buildOptions);
1065
+ }
1066
+ if (raw.runOptions && typeof raw.runOptions == "object" && !Array.isArray(raw.runOptions)) {
1067
+ merged.runOptions = mergeRunOptions(merged.runOptions, override.runOptions, raw.runOptions);
1068
+ }
1069
+ if (raw.fuzz && typeof raw.fuzz == "object" && !Array.isArray(raw.fuzz)) {
1070
+ merged.fuzz = mergeFuzzConfig(merged.fuzz, override.fuzz, raw.fuzz);
1071
+ }
1072
+ CONFIG_META.set(merged, getConfigMeta(override));
1073
+ return merged;
1074
+ }
1075
+ function applyPerModeOutputDefaults(base, merged, override, modeName) {
1076
+ const raw = getConfigMeta(override).raw;
1077
+ if (!outputOverridesField(raw, "outDir")) {
1078
+ merged.outDir = appendPathSegment(base.outDir, modeName);
1079
+ }
1080
+ if (!outputOverridesField(raw, "logs") && base.logs != "none") {
1081
+ merged.logs = appendPathSegment(base.logs, modeName);
1082
+ }
1083
+ if (!outputOverridesField(raw, "coverageDir") && base.coverageDir != "none") {
1084
+ merged.coverageDir = appendPathSegment(base.coverageDir, modeName);
1085
+ }
1086
+ }
1087
+ function resolveModeOverrideConfig(root, modeName) {
1088
+ const mode = root.modes[modeName];
1089
+ if (!mode) {
1090
+ throw new Error(`unknown mode "${modeName}"`);
1091
+ }
1092
+ if (mode.path) {
1093
+ const override = loadConfig(mode.path, false);
1094
+ if (Object.keys(override.modes).length) {
1095
+ throw new Error(`mode "${modeName}" config file cannot declare nested modes`);
1096
+ }
1097
+ return override;
1098
+ }
1099
+ return cloneConfig(mode.config);
1100
+ }
950
1101
  export function resolveModeNames(rawArgs) {
951
1102
  const names = [];
952
1103
  for (let i = 0; i < rawArgs.length; i++) {
@@ -975,12 +1126,7 @@ function appendModeTokens(out, value) {
975
1126
  }
976
1127
  export function applyMode(config, modeName) {
977
1128
  if (!modeName) {
978
- const merged = Object.assign(new Config(), config);
979
- merged.buildOptions = Object.assign(new BuildOptions(), config.buildOptions);
980
- merged.runOptions = Object.assign(new RunOptions(), config.runOptions);
981
- merged.runOptions.runtime = Object.assign(new Runtime(), config.runOptions.runtime);
982
- merged.buildOptions.env = { ...config.buildOptions.env };
983
- merged.runOptions.env = { ...config.runOptions.env };
1129
+ const merged = cloneConfig(config);
984
1130
  merged.outDir = appendPathSegment(config.outDir, "default");
985
1131
  if (config.logs != "none") {
986
1132
  merged.logs = appendPathSegment(config.logs, "default");
@@ -988,7 +1134,6 @@ export function applyMode(config, modeName) {
988
1134
  if (config.coverageDir != "none") {
989
1135
  merged.coverageDir = appendPathSegment(config.coverageDir, "default");
990
1136
  }
991
- merged.fuzz = Object.assign(new FuzzConfig(), config.fuzz);
992
1137
  merged.fuzz.crashDir = appendPathSegment(config.fuzz.crashDir, "default");
993
1138
  merged.fuzz.corpusDir = appendPathSegment(config.fuzz.corpusDir, "default");
994
1139
  const env = {
@@ -1003,71 +1148,17 @@ export function applyMode(config, modeName) {
1003
1148
  env,
1004
1149
  };
1005
1150
  }
1006
- const mode = config.modes[modeName];
1007
- if (!mode) {
1151
+ if (!config.modes[modeName]) {
1008
1152
  const known = Object.keys(config.modes);
1009
1153
  const available = known.length ? known.join(", ") : "(none)";
1010
1154
  throw new Error(`unknown mode "${modeName}". Available modes: ${available}`);
1011
1155
  }
1012
- const merged = Object.assign(new Config(), config);
1013
- merged.buildOptions = Object.assign(new BuildOptions(), config.buildOptions);
1014
- merged.runOptions = Object.assign(new RunOptions(), config.runOptions);
1015
- merged.runOptions.runtime = Object.assign(new Runtime(), config.runOptions.runtime);
1016
- merged.buildOptions.env = { ...config.buildOptions.env };
1017
- merged.runOptions.env = { ...config.runOptions.env };
1018
- if (mode.outDir)
1019
- merged.outDir = mode.outDir;
1020
- else
1021
- merged.outDir = appendPathSegment(config.outDir, modeName);
1022
- if (mode.logs)
1023
- merged.logs = mode.logs;
1024
- else if (config.logs != "none")
1025
- merged.logs = appendPathSegment(config.logs, modeName);
1026
- if (mode.coverageDir)
1027
- merged.coverageDir = mode.coverageDir;
1028
- else if (config.coverageDir != "none")
1029
- merged.coverageDir = appendPathSegment(config.coverageDir, modeName);
1030
- if (mode.snapshotDir)
1031
- merged.snapshotDir = mode.snapshotDir;
1032
- if (mode.config)
1033
- merged.config = mode.config;
1034
- if (mode.coverage != undefined)
1035
- merged.coverage = mode.coverage;
1036
- if (mode.buildOptions.target)
1037
- merged.buildOptions.target = mode.buildOptions.target;
1038
- if (mode.buildOptions.cmd != undefined)
1039
- merged.buildOptions.cmd = mode.buildOptions.cmd;
1040
- if (mode.buildOptions.args) {
1041
- merged.buildOptions.args = [
1042
- ...merged.buildOptions.args,
1043
- ...mode.buildOptions.args,
1044
- ];
1045
- }
1046
- if (mode.buildOptions.env) {
1047
- merged.buildOptions.env = {
1048
- ...merged.buildOptions.env,
1049
- ...mode.buildOptions.env,
1050
- };
1051
- }
1052
- if (mode.runOptions.runtime?.cmd) {
1053
- merged.runOptions.runtime.cmd = mode.runOptions.runtime.cmd;
1054
- }
1055
- if (mode.runOptions.runtime?.browser != undefined) {
1056
- merged.runOptions.runtime.browser = mode.runOptions.runtime.browser;
1057
- }
1058
- if (mode.runOptions.reporter != undefined) {
1059
- merged.runOptions.reporter = mode.runOptions.reporter;
1060
- }
1061
- if (mode.runOptions.env) {
1062
- merged.runOptions.env = {
1063
- ...merged.runOptions.env,
1064
- ...mode.runOptions.env,
1065
- };
1066
- }
1156
+ const modeOverride = resolveModeOverrideConfig(config, modeName);
1157
+ const merged = mergeRootConfig(config, modeOverride);
1158
+ applyPerModeOutputDefaults(config, merged, modeOverride, modeName);
1067
1159
  const env = {
1068
1160
  ...process.env,
1069
- ...config.env,
1070
- ...mode.env,
1161
+ ...merged.env,
1071
1162
  };
1072
1163
  if (merged.runOptions.runtime.browser.length) {
1073
1164
  env.BROWSER = merged.runOptions.runtime.browser;