@systemfsoftware/stryker-js-mutation-run 1.2.5

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 (53) hide show
  1. package/LICENSE +212 -0
  2. package/README.md +38 -0
  3. package/dist/checker-C4tGiXtZ.mjs +588 -0
  4. package/dist/checker-resource-DTYEKXdg.d.mts +108 -0
  5. package/dist/checker-worker.d.mts +16 -0
  6. package/dist/checker-worker.mjs +2 -0
  7. package/dist/child-process-proxy-worker-lx1M5Mz3.mjs +567 -0
  8. package/dist/child-process-proxy-worker-main.d.mts +1 -0
  9. package/dist/child-process-proxy-worker-main.mjs +6 -0
  10. package/dist/child-process-test-runner-worker-BTWRYb8V.mjs +50 -0
  11. package/dist/child-process-test-runner-worker.d.mts +16 -0
  12. package/dist/child-process-test-runner-worker.mjs +2 -0
  13. package/dist/config/base.d.mts +21 -0
  14. package/dist/config/base.mjs +47 -0
  15. package/dist/config/config-resolution.d.mts +80 -0
  16. package/dist/config/config-resolution.mjs +2 -0
  17. package/dist/config/fork-schema.d.mts +5 -0
  18. package/dist/config/fork-schema.mjs +2 -0
  19. package/dist/errors-BkjxkY_S.d.mts +6 -0
  20. package/dist/errors.d.mts +2 -0
  21. package/dist/errors.mjs +10 -0
  22. package/dist/exit-classification.d.mts +28 -0
  23. package/dist/exit-classification.mjs +39 -0
  24. package/dist/fork-schema-BHuTbRBR.mjs +62 -0
  25. package/dist/incremental-differ-DDVwtXBn.d.mts +72 -0
  26. package/dist/incremental-differ-DY8AA09Q.mjs +579 -0
  27. package/dist/index-CyOSz6LC.d.mts +104 -0
  28. package/dist/index.d.mts +87 -0
  29. package/dist/index.mjs +1885 -0
  30. package/dist/mutants/incremental-differ.d.mts +2 -0
  31. package/dist/mutants/incremental-differ.mjs +2 -0
  32. package/dist/output-mode-CfGHDpi5.d.mts +17 -0
  33. package/dist/output-mode.d.mts +2 -0
  34. package/dist/output-mode.mjs +1 -0
  35. package/dist/plugins-Nx86IJQW.mjs +1618 -0
  36. package/dist/plugins.d.mts +2 -0
  37. package/dist/plugins.mjs +2 -0
  38. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  39. package/dist/run-event-CzRz9Gtk.d.mts +121 -0
  40. package/dist/run-event.d.mts +2 -0
  41. package/dist/run-event.mjs +1 -0
  42. package/dist/stryker-package-CTcsIZ4S.mjs +23 -0
  43. package/dist/stryker-package.d.mts +7 -0
  44. package/dist/stryker-package.mjs +2 -0
  45. package/dist/timer-BMrE1SQ3.d.mts +20 -0
  46. package/dist/timer.d.mts +2 -0
  47. package/dist/timer.mjs +48 -0
  48. package/dist/verdict-envelope-BWc61mHm.mjs +229 -0
  49. package/dist/verdict-envelope-CKLZPjxC.d.mts +94 -0
  50. package/dist/verdict-envelope.d.mts +2 -0
  51. package/dist/verdict-envelope.mjs +2 -0
  52. package/package.json +102 -0
  53. package/schema/stryker-schema.json +903 -0
@@ -0,0 +1,1618 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { ConfigError } from "./errors.mjs";
3
+ import { Timer } from "./timer.mjs";
4
+ import { n as REMOVED_OPTIONS } from "./fork-schema-BHuTbRBR.mjs";
5
+ import { INSTRUMENTER_CONSTANTS, StrykerOptionsSchema } from "@systemfsoftware/stryker-js-plugin-api/core";
6
+ import { commonTokens, tokens } from "@systemfsoftware/stryker-js-plugin-api/plugin";
7
+ import "typed-inject";
8
+ import { ERROR_CODES, StrykerError, deepFreeze, deepMerge, errorToString, findUnserializables, isErrnoException, normalizeFileName, normalizeWhitespaces, notEmpty, propertyPath, testFilesProvided } from "@stryker-mutator/util";
9
+ import fs from "fs";
10
+ import { isDeepStrictEqual } from "util";
11
+ import path from "path";
12
+ import { URL, fileURLToPath, pathToFileURL } from "url";
13
+ import { Match, Schema } from "effect";
14
+ import "@systemfsoftware/stryker-js-plugin-api/logging";
15
+ import { createRequire } from "module";
16
+ import os from "os";
17
+ import { Minimatch, minimatch } from "minimatch";
18
+ import * as Result$1 from "effect/Result";
19
+ import * as S from "effect/Schema";
20
+ import { Subject, mergeMap } from "rxjs";
21
+ import "@stryker-mutator/instrumenter";
22
+ import "mutation-testing-report-schema";
23
+ import * as Match$1 from "effect/Match";
24
+ import { exec } from "child_process";
25
+ import { DryRunStatus, TestStatus, toMutantRunResult } from "@systemfsoftware/stryker-js-plugin-api/test-runner";
26
+ import treeKill from "tree-kill";
27
+ //#region src/plugins/injection-tokens.ts
28
+ var injection_tokens_exports = /* @__PURE__ */ __exportAll({
29
+ checkerConcurrencyTokens: () => checkerConcurrencyTokens,
30
+ checkerFactory: () => checkerFactory,
31
+ checkerPool: () => checkerPool,
32
+ clearTextEnabled: () => clearTextEnabled,
33
+ concurrencyTokenProvider: () => concurrencyTokenProvider,
34
+ disableTypeChecksHelper: () => disableTypeChecksHelper,
35
+ dryRunResult: () => dryRunResult,
36
+ execa: () => execa,
37
+ execaSync: () => execaSync,
38
+ fs: () => "fs",
39
+ incrementalDiffer: () => incrementalDiffer,
40
+ loggerActiveLevel: () => loggerActiveLevel,
41
+ loggerConsoleOut: () => loggerConsoleOut,
42
+ loggerShowColors: () => loggerShowColors,
43
+ loggingServer: () => loggingServer,
44
+ loggingServerAddress: () => loggingServerAddress,
45
+ loggingSink: () => loggingSink,
46
+ mutantTestPlanner: () => mutantTestPlanner,
47
+ mutants: () => mutants,
48
+ mutationTestReportHelper: () => mutationTestReportHelper,
49
+ optionsValidator: () => optionsValidator,
50
+ pluginCreator: () => pluginCreator,
51
+ pluginModulePaths: () => pluginModulePaths,
52
+ pluginsByKind: () => pluginsByKind,
53
+ process: () => process$1,
54
+ progressEnabled: () => progressEnabled,
55
+ project: () => project,
56
+ reporter: () => reporter,
57
+ reporterOverride: () => reporterOverride,
58
+ reporterPluginModules: () => reporterPluginModules,
59
+ requireFromCwd: () => requireFromCwd,
60
+ resolveFromCwd: () => resolveFromCwd,
61
+ resolvedMode: () => resolvedMode,
62
+ runEventSink: () => runEventSink,
63
+ runId: () => runId,
64
+ runStartedAt: () => runStartedAt,
65
+ sandbox: () => sandbox,
66
+ temporaryDirectory: () => temporaryDirectory,
67
+ testCoverage: () => testCoverage,
68
+ testRunnerConcurrencyTokens: () => testRunnerConcurrencyTokens,
69
+ testRunnerFactory: () => testRunnerFactory,
70
+ testRunnerPool: () => testRunnerPool,
71
+ timeOverheadMS: () => timeOverheadMS,
72
+ timer: () => timer,
73
+ unexpectedExitRegistry: () => unexpectedExitRegistry,
74
+ validationSchema: () => validationSchema,
75
+ workerIdGenerator: () => workerIdGenerator
76
+ });
77
+ const checkerPool = "checkerPool";
78
+ const checkerFactory = "checkerFactory";
79
+ const checkerConcurrencyTokens = "checkerConcurrencyTokens";
80
+ const disableTypeChecksHelper = "disableTypeChecksHelper";
81
+ const execa = "execa";
82
+ const execaSync = "execaSync";
83
+ const dryRunResult = "dryRunResult";
84
+ const mutants = "mutants";
85
+ const mutantTestPlanner = "mutantTestPlanner";
86
+ const process$1 = "process";
87
+ const pluginModulePaths = "pluginModulePaths";
88
+ const temporaryDirectory = "temporaryDirectory";
89
+ const unexpectedExitRegistry = "unexpectedExitRegistry";
90
+ const timer = "timer";
91
+ const timeOverheadMS = "timeOverheadMS";
92
+ const loggingServerAddress = "loggingServerAddress";
93
+ const loggerActiveLevel = "loggerActiveLevel";
94
+ const loggerConsoleOut = "loggerConsoleOut";
95
+ const loggerShowColors = "loggerShowColors";
96
+ const loggingSink = "loggingSink";
97
+ const runEventSink = "runEventSink";
98
+ const runId = "runId";
99
+ const resolvedMode = "resolvedMode";
100
+ const progressEnabled = "progressEnabled";
101
+ const clearTextEnabled = "clearTextEnabled";
102
+ const runStartedAt = "runStartedAt";
103
+ const reporterPluginModules = "reporterPluginModules";
104
+ const loggingServer = "loggingServer";
105
+ const mutationTestReportHelper = "mutationTestReportHelper";
106
+ const sandbox = "sandbox";
107
+ const concurrencyTokenProvider = "concurrencyTokenProvider";
108
+ const testRunnerFactory = "testRunnerFactory";
109
+ const testRunnerPool = "testRunnerPool";
110
+ const testRunnerConcurrencyTokens = "testRunnerConcurrencyTokens";
111
+ const reporter = "reporter";
112
+ const reporterOverride = "reporterOverride";
113
+ const pluginCreator = "pluginCreator";
114
+ const pluginsByKind = "pluginsByKind";
115
+ const validationSchema = "validationSchema";
116
+ const optionsValidator = "optionsValidator";
117
+ const requireFromCwd = "requireFromCwd";
118
+ const resolveFromCwd = "resolveFromCwd";
119
+ const testCoverage = "testCoverage";
120
+ const incrementalDiffer = "incrementalDiffer";
121
+ const project = "project";
122
+ const workerIdGenerator = "worker-id-generator";
123
+ //#endregion
124
+ //#region src/plugins/plugin-creator.ts
125
+ var PluginCreator = class {
126
+ pluginsByKind;
127
+ injector;
128
+ static inject = tokens(pluginsByKind, commonTokens.injector);
129
+ constructor(pluginsByKind, injector) {
130
+ this.pluginsByKind = pluginsByKind;
131
+ this.injector = injector;
132
+ }
133
+ create(kind, name) {
134
+ const plugin = this.findPlugin(kind, name);
135
+ if (isFactoryPlugin(plugin)) return this.injector.injectFunction(plugin.factory);
136
+ else if (isClassPlugin(plugin)) return this.injector.injectClass(plugin.injectableClass);
137
+ else if (isValuePlugin(plugin)) return plugin.value;
138
+ throw new Error(`Plugin "${kind}:${name}" could not be created, missing "factory", "injectableClass" or "value" property.`);
139
+ }
140
+ findPlugin(kind, name) {
141
+ const plugins = this.pluginsByKind.get(kind);
142
+ if (plugins) {
143
+ const pluginFound = plugins.find((plugin) => plugin.name.toLowerCase() === name.toLowerCase());
144
+ if (pluginFound && isPluginOfKind(pluginFound, kind)) return pluginFound;
145
+ else throw new ConfigError(`Cannot find ${kind} plugin "${name}". Did you forget to install it? Loaded ${kind} plugins were: ${new Intl.ListFormat("en").format(plugins.map((p) => `"${p.name}"`))}`);
146
+ } else throw new ConfigError(`Cannot find ${kind} plugin "${name}". In fact, no ${kind} plugins were loaded. Did you forget to install it?`);
147
+ }
148
+ };
149
+ function isPluginOfKind(plugin, kind) {
150
+ return plugin.kind === kind;
151
+ }
152
+ function isFactoryPlugin(plugin) {
153
+ return "factory" in plugin;
154
+ }
155
+ function isClassPlugin(plugin) {
156
+ return "injectableClass" in plugin;
157
+ }
158
+ function isValuePlugin(plugin) {
159
+ return "value" in plugin;
160
+ }
161
+ //#endregion
162
+ //#region src/config/module-loader.ts
163
+ /**
164
+ * Wrapper around the 'import' expression (for testability)
165
+ * Resolves bare specifiers relative to cwd so plugins in the
166
+ * consumer's node_modules can be found even when the importing
167
+ * module lives at a different path in the monorepo.
168
+ */
169
+ function importModule(moduleName) {
170
+ if (moduleName.startsWith(".") || moduleName.startsWith("/") || moduleName.startsWith("file://")) return import(moduleName);
171
+ return import(createRequire(process.cwd() + "/noop.js").resolve(moduleName));
172
+ }
173
+ //#endregion
174
+ //#region src/project/file-system.ts
175
+ const MAX_CONCURRENT_FILE_IO = 256;
176
+ /**
177
+ * A wrapper around nodejs's 'fs' core module, for dependency injection purposes.
178
+ *
179
+ * Also has build-in buffering support with a concurrency limit (like "graceful-fs").
180
+ */
181
+ var FileSystem = class {
182
+ /**
183
+ * The buffered work queue: each pending call is a zero-argument thunk that
184
+ * performs the real `fs.promises` call. `mergeMap` with the concurrency
185
+ * limit is the "graceful-fs" hat.
186
+ */
187
+ todoSubject = new Subject();
188
+ subscription = this.todoSubject.pipe(mergeMap(async (work) => {
189
+ await work();
190
+ }, MAX_CONCURRENT_FILE_IO)).subscribe();
191
+ dispose() {
192
+ this.subscription.unsubscribe();
193
+ }
194
+ readFile(path, options) {
195
+ return this.queue(() => fs.promises.readFile(path, options));
196
+ }
197
+ copyFile(src, dest, mode) {
198
+ return this.queue(() => fs.promises.copyFile(src, dest, mode));
199
+ }
200
+ writeFile(path, data, options) {
201
+ return this.queue(() => fs.promises.writeFile(path, data, options));
202
+ }
203
+ mkdir(path, options) {
204
+ return this.queue(() => fs.promises.mkdir(path, options));
205
+ }
206
+ readdir(path, options) {
207
+ return this.queue(() => fs.promises.readdir(path, options));
208
+ }
209
+ /** Buffer one fs call behind the concurrency limit and await its outcome. */
210
+ queue(work) {
211
+ return new Promise((resolve, reject) => {
212
+ this.todoSubject.next(async () => {
213
+ try {
214
+ resolve(await work());
215
+ } catch (err) {
216
+ reject(err);
217
+ }
218
+ });
219
+ });
220
+ }
221
+ };
222
+ //#endregion
223
+ //#region src/project/project-file.ts
224
+ /**
225
+ * Represents a file inside the project under test.
226
+ * Has utility methods to help with copying it to the sandbox, or backing it up (when `--inPlace` is active)
227
+ *
228
+ * Assumes utf-8 as encoding when reading/writing content.
229
+ */
230
+ var ProjectFile = class {
231
+ fs;
232
+ name;
233
+ mutate;
234
+ #currentContent;
235
+ #originalContent;
236
+ #relativePath;
237
+ constructor(fs, name, mutate) {
238
+ this.fs = fs;
239
+ this.name = name;
240
+ this.mutate = mutate;
241
+ this.#relativePath = path.relative(process.cwd(), this.name);
242
+ }
243
+ async #writeTo(to) {
244
+ if (this.#currentContent === void 0) await this.fs.copyFile(this.name, to);
245
+ else await this.fs.writeFile(to, this.#currentContent, "utf-8");
246
+ }
247
+ setContent(content) {
248
+ this.#currentContent = content;
249
+ }
250
+ async toInstrumenterFile() {
251
+ return {
252
+ content: await this.readContent(),
253
+ mutate: this.mutate,
254
+ name: this.name
255
+ };
256
+ }
257
+ async readContent() {
258
+ if (this.#currentContent === void 0) this.#currentContent = await this.readOriginal();
259
+ return this.#currentContent;
260
+ }
261
+ async readOriginal() {
262
+ if (this.#originalContent === void 0) {
263
+ try {
264
+ this.#originalContent = await this.fs.readFile(this.name, "utf-8");
265
+ } catch (e) {
266
+ throw new StrykerError(`Could not read file "${this.name}"`, e);
267
+ }
268
+ if (this.#currentContent === void 0) this.#currentContent = this.#originalContent;
269
+ }
270
+ return this.#originalContent;
271
+ }
272
+ async writeInPlace() {
273
+ if (this.#currentContent !== void 0 && this.hasChanges) await this.fs.writeFile(this.name, this.#currentContent, "utf-8");
274
+ }
275
+ async writeToSandbox(sandboxDir) {
276
+ const folderName = path.join(sandboxDir, path.dirname(this.#relativePath));
277
+ const targetFileName = path.join(folderName, path.basename(this.#relativePath));
278
+ await this.fs.mkdir(path.dirname(targetFileName), { recursive: true });
279
+ await this.#writeTo(targetFileName);
280
+ return targetFileName;
281
+ }
282
+ async backupTo(backupDir) {
283
+ const backupFileName = path.join(backupDir, this.#relativePath);
284
+ await this.fs.mkdir(path.dirname(backupFileName), { recursive: true });
285
+ if (this.#originalContent === void 0) await this.fs.copyFile(this.name, backupFileName);
286
+ else await this.fs.writeFile(backupFileName, this.#originalContent);
287
+ return backupFileName;
288
+ }
289
+ get hasChanges() {
290
+ return this.#currentContent !== this.#originalContent;
291
+ }
292
+ };
293
+ //#endregion
294
+ //#region src/config/config-file-formats.ts
295
+ const combine = (prefixes, suffixes, extensions) => {
296
+ const fileNames = [];
297
+ for (const prefix of prefixes) for (const suffix of suffixes) for (const extension of extensions) fileNames.push(`${prefix}stryker${suffix}.${extension}`);
298
+ return fileNames;
299
+ };
300
+ const SUPPORTED_CONFIG_FILE_NAMES = Object.freeze(combine(["", "."], [".conf", ".config"], [
301
+ "json",
302
+ "js",
303
+ "mjs",
304
+ "cjs"
305
+ ]));
306
+ Object.freeze({
307
+ JSON: "stryker.config.json",
308
+ JAVASCRIPT: "stryker.config.mjs"
309
+ });
310
+ //#endregion
311
+ //#region src/config/config-document.schema.ts
312
+ /**
313
+ * A Stryker config document read from disk or imported from a JS module: any
314
+ * object record. Individual option keys and value shapes are enforced later
315
+ * by the options validator against the derived JSON schema, so this boundary
316
+ * decode only establishes that the document is an object — never a
317
+ * hand-written `isRecord` narrowing.
318
+ */
319
+ const ConfigDocumentSchema = S.Record(S.String, S.Unknown);
320
+ /**
321
+ * The shape of an imported JS config module as seen through the dynamic
322
+ * `import()` boundary: a module namespace whose `default` export is the
323
+ * config document. Only the default export is read here.
324
+ */
325
+ const ImportedModuleSchema = S.Struct({ default: S.optional(S.Unknown) });
326
+ //#endregion
327
+ //#region src/config/extends-step.ts
328
+ const initialExtendsStepState = {
329
+ visited: [],
330
+ documents: []
331
+ };
332
+ /**
333
+ * Merge a child config over a parent's resolved options.
334
+ * R2: scalars replace wholesale; objects merge one level deep.
335
+ * R3: a child key set to `null` deletes the inherited key.
336
+ * R4: the `plugins` array is the one exception to wholesale array replacement —
337
+ * the parent's plugin loaders stay inherited and the child's descriptors are
338
+ * appended, with the first occurrence of a descriptor winning.
339
+ *
340
+ * A copy of the precedence logic in `resolve-extends.ts`, moved here with the
341
+ * original staying put. Where the original re-decoded each object through
342
+ * `ConfigDocumentSchema`, this copy spreads the validated objects directly:
343
+ * they already passed that schema at the read boundary, so the re-decode was
344
+ * dead weight and its throw site does not move into this module.
345
+ */
346
+ function mergeConfigs(parent, child) {
347
+ const out = { ...parent };
348
+ for (const [key, value] of Object.entries(child)) {
349
+ if (value === null) {
350
+ delete out[key];
351
+ continue;
352
+ }
353
+ const parentValue = parent[key];
354
+ if (key === "plugins") {
355
+ const parentPlugins = Array.isArray(parentValue) ? parentValue : [];
356
+ const childPlugins = Array.isArray(value) ? value : [];
357
+ const merged = [...parentPlugins, ...childPlugins];
358
+ out[key] = merged.filter((descriptor, index) => typeof descriptor !== "string" || !merged.slice(0, index).includes(descriptor));
359
+ continue;
360
+ }
361
+ out[key] = parentValue !== null && parentValue !== void 0 && typeof parentValue === "object" && !Array.isArray(parentValue) && typeof value === "object" && !Array.isArray(value) ? {
362
+ ...parentValue,
363
+ ...value
364
+ } : value;
365
+ }
366
+ return out;
367
+ }
368
+ /**
369
+ * A bare package specifier (`pkg`, `@scope/pkg`, `@scope/pkg/sub`) versus a
370
+ * filesystem path. Everything not starting with `./`, `../`, `/` or `\` is
371
+ * treated as a specifier and routed through the Node resolver, so it honours
372
+ * `package.json#exports` the way `@systemfsoftware/tsconfig` does for
373
+ * `tsconfig.json`.
374
+ */
375
+ function isModuleSpecifier(value) {
376
+ return !(value.startsWith("./") || value.startsWith("../") || value.startsWith("/") || value.startsWith("\\"));
377
+ }
378
+ const stripExtends = (document) => {
379
+ const { extends: _ignored, ...rest } = document;
380
+ return rest;
381
+ };
382
+ /**
383
+ * Fold the accumulated chain bottom-up, exactly the order `resolveExtendsChain`
384
+ * merged in: the root document first, its child over it, and so on up to the
385
+ * document this step was asked to decide.
386
+ */
387
+ const mergeChainDocuments = (documents) => documents.reduceRight((merged, entry) => mergeConfigs(merged, stripExtends(entry.options)), {});
388
+ /**
389
+ * Decide the next act in an `extends` chain. Receives the accumulated state
390
+ * plus the last document read and the absolute path it was read from, and
391
+ * returns exactly one of:
392
+ *
393
+ * - `done` — no further parent: the whole chain is merged and carried.
394
+ * - `resolve` — a bare specifier the shell must resolve from `directory`.
395
+ * - `read` — a relative path, already resolved against the declaring
396
+ * document's directory (never the process working directory).
397
+ * - `refused` — a cycle (the path is already visited) or an `extends` that is
398
+ * not a string, naming the offending file.
399
+ *
400
+ * The request performs no read and no resolution; it is data the shell
401
+ * performs and feeds back.
402
+ */
403
+ const decideExtendsStep = (state, document, file) => {
404
+ if (state.visited.includes(file)) return {
405
+ _tag: "refused",
406
+ reason: "cycle",
407
+ file
408
+ };
409
+ const nextState = {
410
+ visited: [...state.visited, file],
411
+ documents: [...state.documents, {
412
+ path: file,
413
+ options: document
414
+ }]
415
+ };
416
+ return Match$1.value(document["extends"]).pipe(Match$1.when(void 0, () => ({
417
+ _tag: "done",
418
+ options: mergeChainDocuments(nextState.documents)
419
+ })), Match$1.when(null, () => ({
420
+ _tag: "done",
421
+ options: mergeChainDocuments(nextState.documents)
422
+ })), Match$1.when(Match$1.string, (extendValue) => Match$1.value(isModuleSpecifier(extendValue)).pipe(Match$1.when(true, () => ({
423
+ _tag: "resolve",
424
+ specifier: extendValue,
425
+ directory: path.dirname(file),
426
+ state: nextState
427
+ })), Match$1.when(false, () => ({
428
+ _tag: "read",
429
+ path: path.resolve(path.dirname(file), extendValue),
430
+ state: nextState
431
+ })), Match$1.exhaustive)), Match$1.orElse(() => ({
432
+ _tag: "refused",
433
+ reason: "non-string-extends",
434
+ file
435
+ })));
436
+ };
437
+ //#endregion
438
+ //#region src/config/resolve-extends.ts
439
+ async function readConfigFile(configFile) {
440
+ if (path.extname(configFile).toLowerCase() === ".json") {
441
+ let fileContent;
442
+ try {
443
+ fileContent = await fs.promises.readFile(configFile, "utf-8");
444
+ } catch (err) {
445
+ throw new ConfigError(`Cannot read config file "${configFile}"`, err);
446
+ }
447
+ let parsed;
448
+ try {
449
+ parsed = JSON.parse(fileContent);
450
+ } catch (err) {
451
+ throw new ConfigError(`Invalid config file "${configFile}". File contains invalid JSON`, err);
452
+ }
453
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new ConfigError(`Invalid config file "${configFile}". Config must be a JSON object`);
454
+ return S.decodeUnknownSync(ConfigDocumentSchema)(parsed);
455
+ }
456
+ let importedModule;
457
+ try {
458
+ importedModule = await import(pathToFileURL(path.resolve(configFile)).toString());
459
+ } catch (err) {
460
+ throw new ConfigError(`Invalid config file "${configFile}". Error during import`, err);
461
+ }
462
+ const exported = S.decodeUnknownSync(ImportedModuleSchema)(importedModule).default;
463
+ if (exported === void 0 || exported === null || typeof exported !== "object") throw new ConfigError(`Invalid config file "${configFile}". Default export of config file must be an object!`);
464
+ return S.decodeUnknownSync(ConfigDocumentSchema)(exported);
465
+ }
466
+ /**
467
+ * The resolve act, split out of the deleted `resolveExtendsTarget`. Its
468
+ * relative-path branch became the decision's `read` request; only this branch
469
+ * survives, converting a bare specifier to an absolute path through node's
470
+ * require resolver from the declaring config's directory — never the process
471
+ * working directory (R10). The error message is the pre-split one, verbatim.
472
+ */
473
+ function resolveExtendsSpecifier(specifier, configDir) {
474
+ const requireFrom = createRequire(path.join(configDir, "noop.js"));
475
+ try {
476
+ return requireFrom.resolve(specifier);
477
+ } catch (err) {
478
+ throw new ConfigError(`Cannot resolve extends target "${specifier}" from "${configDir}"`, err);
479
+ }
480
+ }
481
+ /**
482
+ * Interpret the `extends` decision at the shell boundary (R9, KTD2).
483
+ *
484
+ * Drives `decideExtendsStep` in a plain async loop. The entry document is
485
+ * already read by the caller; each returned request names the act the shell
486
+ * performs — read this absolute path, or resolve this specifier from the
487
+ * declaring directory — and the document the act yields is fed back together
488
+ * with the state the request carried. The shell holds no decision of its own:
489
+ * it only dispatches on the request it received and maps a refusal to the
490
+ * `ConfigError` the pre-split chain surfaced, byte for byte.
491
+ */
492
+ async function resolveExtends(configFile, document) {
493
+ const absolute = path.resolve(configFile);
494
+ let state = initialExtendsStepState;
495
+ let file = absolute;
496
+ let currentDocument = document;
497
+ for (;;) {
498
+ const outcome = await Match.value(decideExtendsStep(state, currentDocument, file)).pipe(Match.tag("done", (value) => ({
499
+ kind: "done",
500
+ options: value.options
501
+ })), Match.tag("read", (value) => readConfigFile(value.path).then((nextDocument) => ({
502
+ kind: "next",
503
+ state: value.state,
504
+ file: value.path,
505
+ document: nextDocument
506
+ }))), Match.tag("resolve", (value) => {
507
+ let resolvedPath;
508
+ try {
509
+ resolvedPath = resolveExtendsSpecifier(value.specifier, value.directory);
510
+ } catch (err) {
511
+ if (err instanceof ConfigError) throw err;
512
+ const reason = err instanceof Error ? `. ${err.message}` : "";
513
+ throw new ConfigError(`Cannot resolve extends target "${value.specifier}" from "${file}"${reason}`, err);
514
+ }
515
+ return readConfigFile(resolvedPath).then((nextDocument) => ({
516
+ kind: "next",
517
+ state: value.state,
518
+ file: resolvedPath,
519
+ document: nextDocument
520
+ }));
521
+ }), Match.tag("refused", (value) => {
522
+ const message = value.reason === "cycle" ? `Config inheritance cycle detected at "${value.file}"` : `Invalid config file "${value.file}". "extends" must be a string`;
523
+ throw new ConfigError(message);
524
+ }), Match.exhaustive);
525
+ if (outcome.kind === "done") return outcome.options;
526
+ state = outcome.state;
527
+ file = outcome.file;
528
+ currentDocument = outcome.document;
529
+ }
530
+ }
531
+ //#endregion
532
+ //#region src/config/config-reader.ts
533
+ const CONFIG_SYNTAX_HELP = `
534
+ Example of how a config file should look:
535
+ /**
536
+ * @type {import('@systemfsoftware/stryker-js-plugin-api/core').StrykerOptions}
537
+ */
538
+ export default {
539
+ // You're options here!
540
+ }
541
+
542
+ Or using commonjs:
543
+ /**
544
+ * @type {import('@systemfsoftware/stryker-js-plugin-api/core').StrykerOptions}
545
+ */
546
+ module.exports = {
547
+ // You're options here!
548
+ }
549
+
550
+ See https://stryker-mutator.io/docs/stryker-js/config-file for more information.`.trim();
551
+ async function exists(fileName) {
552
+ try {
553
+ await fs.promises.access(fileName);
554
+ return true;
555
+ } catch (err) {
556
+ if (isErrnoException(err) && err.code === "ENOENT") return false;
557
+ else throw err;
558
+ }
559
+ }
560
+ var ConfigReader = class {
561
+ log;
562
+ validator;
563
+ static inject = tokens(commonTokens.logger, optionsValidator);
564
+ constructor(log, validator) {
565
+ this.log = log;
566
+ this.validator = validator;
567
+ }
568
+ async readConfig(cliOptions) {
569
+ const options = await this.loadOptionsFromConfigFile(cliOptions);
570
+ deepMerge(options, cliOptions);
571
+ this.validator.validate(options);
572
+ if (this.log.isDebugEnabled()) this.log.debug(`Loaded config: ${JSON.stringify(options, null, 2)}`);
573
+ return options;
574
+ }
575
+ async loadOptionsFromConfigFile(cliOptions) {
576
+ const configFile = await this.findConfigFile(cliOptions["configFile"]);
577
+ if (!configFile) {
578
+ this.log.info("No config file specified. Running with command line arguments.");
579
+ this.log.info("Use `stryker init` command to generate your config file.");
580
+ return {};
581
+ }
582
+ this.log.debug(`Loading config from ${configFile}`);
583
+ const child = path.extname(configFile).toLocaleLowerCase() === ".json" ? await this.readJsonConfig(configFile) : await this.importJSConfig(configFile);
584
+ if (!("extends" in child)) return child;
585
+ return resolveExtends(configFile, child);
586
+ }
587
+ async findConfigFile(configFileName) {
588
+ if (typeof configFileName === "string") {
589
+ if (await exists(configFileName)) return configFileName;
590
+ else throw new ConfigReaderError("File does not exist!", configFileName);
591
+ }
592
+ for (const fileName of SUPPORTED_CONFIG_FILE_NAMES) if (await exists(fileName)) return fileName;
593
+ }
594
+ async readJsonConfig(configFile) {
595
+ const fileContent = await fs.promises.readFile(configFile, "utf-8");
596
+ try {
597
+ const parsed = JSON.parse(fileContent);
598
+ return S.decodeUnknownSync(S.Record(S.String, S.Unknown))(parsed);
599
+ } catch (err) {
600
+ throw new ConfigReaderError("File contains invalid JSON", configFile, err);
601
+ }
602
+ }
603
+ async importJSConfig(configFile) {
604
+ const importedModule = await this.importJSConfigModule(configFile);
605
+ if (this.hasDefaultExport(importedModule)) {
606
+ const maybeOptions = importedModule.default;
607
+ if (typeof maybeOptions !== "object") {
608
+ if (typeof maybeOptions === "function") this.log.fatal(`Invalid config file. Exporting a function is no longer supported. Please export an object with your configuration instead, or use a "stryker.conf.json" file.\n${CONFIG_SYNTAX_HELP}`);
609
+ else this.log.fatal(`Invalid config file. It must export an object, found a "${typeof maybeOptions}"!\n${CONFIG_SYNTAX_HELP}`);
610
+ throw new ConfigReaderError("Default export of config file must be an object!", configFile);
611
+ }
612
+ if (!maybeOptions || !Object.keys(maybeOptions).length) this.log.warn(`Stryker options were empty. Did you forget to export options from ${configFile}?`);
613
+ return { ...maybeOptions };
614
+ } else {
615
+ this.log.fatal(`Invalid config file. It is missing a default export. ${describeNamedExports()}\n${CONFIG_SYNTAX_HELP}`);
616
+ throw new ConfigReaderError("Config file must have a default export!", configFile);
617
+ function describeNamedExports() {
618
+ const namedExports = typeof importedModule === "object" && Object.keys(importedModule ?? {}) || [];
619
+ if (namedExports.length === 0) return "In fact, it didn't export anything.";
620
+ else return `Found named export(s): ${new Intl.ListFormat("en").format(namedExports.map((name) => `"${name}"`))}.`;
621
+ }
622
+ }
623
+ }
624
+ async importJSConfigModule(configFile) {
625
+ try {
626
+ return await importModule(pathToFileURL(path.resolve(configFile)).toString());
627
+ } catch (err) {
628
+ throw new ConfigReaderError("Error during import", configFile, err);
629
+ }
630
+ }
631
+ hasDefaultExport(importedModule) {
632
+ return importedModule && typeof importedModule === "object" && "default" in importedModule ? true : false;
633
+ }
634
+ };
635
+ var ConfigReaderError = class extends ConfigError {
636
+ constructor(message, configFileName, cause) {
637
+ super(`Invalid config file "${configFileName}". ${message}`, cause);
638
+ }
639
+ };
640
+ //#endregion
641
+ //#region src/config/file-matcher.ts
642
+ /**
643
+ * A helper class for matching files using the `disableTypeChecks` setting.
644
+ */
645
+ var FileMatcher = class {
646
+ allowHiddenFiles;
647
+ pattern;
648
+ constructor(pattern, allowHiddenFiles = true) {
649
+ this.allowHiddenFiles = allowHiddenFiles;
650
+ if (typeof pattern === "string") this.pattern = normalizeFileName(path.resolve(pattern));
651
+ else if (pattern) this.pattern = "**/*.{js,ts,jsx,tsx,html,vue,mjs,mts,cts,cjs}";
652
+ else this.pattern = pattern;
653
+ }
654
+ matches(fileName) {
655
+ if (typeof this.pattern === "string") return minimatch(normalizeFileName(path.resolve(fileName)), this.pattern, { dot: this.allowHiddenFiles });
656
+ else return this.pattern;
657
+ }
658
+ };
659
+ //#endregion
660
+ //#region src/config/meta-schema-builder.ts
661
+ var MetaSchemaBuilder = class {
662
+ schema;
663
+ log;
664
+ static inject = tokens(validationSchema, commonTokens.logger);
665
+ constructor(schema, log) {
666
+ this.schema = schema;
667
+ this.log = log;
668
+ }
669
+ buildMetaSchema(pluginSchemaContributions) {
670
+ this.log.debug("Contributing %s schemas from plugins to options validation.", pluginSchemaContributions.length);
671
+ return mergedSchema(this.schema, pluginSchemaContributions);
672
+ }
673
+ };
674
+ function mergedSchema(mainSchema, additionalSchemas) {
675
+ const schema = {
676
+ ...mainSchema,
677
+ properties: { ...mainSchema.properties },
678
+ definitions: { ...mainSchema.definitions }
679
+ };
680
+ Object.assign(schema.properties, ...additionalSchemas.map((s) => s.properties));
681
+ Object.assign(schema.definitions, ...additionalSchemas.map((s) => s.definitions));
682
+ return schema;
683
+ }
684
+ //#endregion
685
+ //#region src/project/incremental-report.schema.ts
686
+ const PositionSchema = S.Struct({
687
+ line: S.Finite,
688
+ column: S.Finite
689
+ });
690
+ const LocationSchema = S.Struct({
691
+ start: PositionSchema,
692
+ end: PositionSchema
693
+ });
694
+ const OpenEndLocationSchema = S.Struct({
695
+ start: PositionSchema,
696
+ end: S.optionalKey(PositionSchema)
697
+ });
698
+ /** A report mutant: the full named surface of `MutantResult`. */
699
+ const MutantResultLikeSchema = S.Struct({
700
+ id: S.String,
701
+ mutatorName: S.String,
702
+ replacement: S.String,
703
+ location: LocationSchema,
704
+ status: S.Literals([
705
+ "Killed",
706
+ "Survived",
707
+ "NoCoverage",
708
+ "Timeout",
709
+ "CompileError",
710
+ "RuntimeError",
711
+ "Ignored",
712
+ "Pending"
713
+ ]),
714
+ killedBy: S.optionalKey(S.mutable(S.Array(S.String))),
715
+ coveredBy: S.optionalKey(S.mutable(S.Array(S.String))),
716
+ static: S.optionalKey(S.Boolean),
717
+ statusReason: S.optionalKey(S.String),
718
+ testsCompleted: S.optionalKey(S.Finite),
719
+ description: S.optionalKey(S.String),
720
+ duration: S.optionalKey(S.Finite)
721
+ });
722
+ /** A report file entry. */
723
+ const FileResultLikeSchema = S.Struct({
724
+ language: S.String,
725
+ source: S.String,
726
+ mutants: S.mutable(S.Array(MutantResultLikeSchema))
727
+ });
728
+ /** A report test entry. */
729
+ const TestDefinitionLikeSchema = S.Struct({
730
+ id: S.String,
731
+ name: S.String,
732
+ location: S.optionalKey(OpenEndLocationSchema)
733
+ });
734
+ /** A report test file. */
735
+ const TestFileLikeSchema = S.Struct({
736
+ source: S.optionalKey(S.String),
737
+ tests: S.mutable(S.Array(TestDefinitionLikeSchema))
738
+ });
739
+ const ThresholdsLikeSchema = S.Struct({
740
+ high: S.Finite,
741
+ low: S.Finite
742
+ });
743
+ /**
744
+ * The prior-report document the incremental differ reuses. An open struct:
745
+ * every key the reader does not reshape is preserved verbatim. The report is
746
+ * data from disk, so it is decoded through this schema rather than trusted.
747
+ * `schemaVersion` and `thresholds` are declared — not just passed through the
748
+ * open rest — because the reshaped document is returned as a
749
+ * `MutationTestResult`, whose type names them.
750
+ */
751
+ const IncrementalReportSchema = S.StructWithRest(S.Struct({
752
+ schemaVersion: S.String,
753
+ thresholds: ThresholdsLikeSchema,
754
+ files: S.Record(S.String, FileResultLikeSchema),
755
+ testFiles: S.optionalKey(S.Record(S.String, TestFileLikeSchema))
756
+ }), [S.Record(S.String, S.Unknown)]);
757
+ //#endregion
758
+ //#region src/project/project.ts
759
+ /**
760
+ * Represents the project that is under test by Stryker users.
761
+ * This represents the files in the current working directory.
762
+ * Each file can be read into memory when needed (via `readContent`)
763
+ */
764
+ var Project = class {
765
+ fileDescriptions;
766
+ incrementalReport;
767
+ files = /* @__PURE__ */ new Map();
768
+ filesToMutate = /* @__PURE__ */ new Map();
769
+ testFiles;
770
+ constructor(fs, fileDescriptions, incrementalReport, testFiles = []) {
771
+ this.fileDescriptions = fileDescriptions;
772
+ this.incrementalReport = incrementalReport;
773
+ this.testFiles = testFiles;
774
+ Object.entries(fileDescriptions).forEach(([name, desc]) => {
775
+ const file = new ProjectFile(fs, name, desc.mutate);
776
+ this.files.set(name, file);
777
+ if (desc.mutate) this.filesToMutate.set(name, file);
778
+ });
779
+ }
780
+ get isEmpty() {
781
+ return this.files.size === 0;
782
+ }
783
+ logFiles(log, ignoreRules, force, mutatePatterns, testFilePatterns = []) {
784
+ if (this.isEmpty) log.warn(normalizeWhitespaces(`No files found in directory ${process.cwd()} using ignore rules: ${JSON.stringify(ignoreRules)}.
785
+ Make sure you run Stryker from the root directory of your project with the correct "${propertyPath()("ignorePatterns")}".`));
786
+ else {
787
+ if (this.filesToMutate.size) {
788
+ const incrementalInfo = this.incrementalReport ? ` using incremental report with ${Object.values(this.incrementalReport.files).reduce((total, { mutants }) => total + mutants.length, 0)} mutant(s), and ${Object.values(this.incrementalReport.testFiles ?? {}).reduce((total, { tests }) => total + tests.length, 0)} test(s)${force ? ". Force mode is activated, all mutants will be retested" : ""}` : "";
789
+ log.info(`Found ${this.filesToMutate.size} of ${this.files.size} file(s) to be mutated${incrementalInfo}.`);
790
+ } else {
791
+ const msg = normalizeWhitespaces(`Warning: No files found for mutation with the given glob expressions. As a result, a dry-run will be performed without actually modifying anything.
792
+ If you intended to mutate files, please check and adjust the configuration.
793
+ Current glob pattern(s) used:
794
+ ${new Intl.ListFormat("en").format(mutatePatterns.map((pattern) => `"${pattern}"`))}.
795
+
796
+ To enable file mutation, consider configuring the \`${propertyPath()("mutate")}\` property in your configuration file or using the --mutate option via the command line.`);
797
+ log.warn(msg);
798
+ }
799
+ if (this.testFiles.length > 0) log.info(`Found ${this.testFiles.length} test file(s) matching --testFiles patterns.`);
800
+ if (log.isDebugEnabled()) {
801
+ log.debug(`All input files: ${JSON.stringify([...this.files.keys()], null, 2)}`);
802
+ log.debug(`Files to mutate: ${JSON.stringify([...this.filesToMutate.keys()], null, 2)}`);
803
+ if (this.testFiles.length > 0) log.debug(`Test files: ${JSON.stringify(this.testFiles, null, 2)}`);
804
+ }
805
+ }
806
+ }
807
+ };
808
+ //#endregion
809
+ //#region src/project/project-reader.ts
810
+ const ALWAYS_IGNORE = Object.freeze([
811
+ "node_modules",
812
+ ".git",
813
+ "*.tsbuildinfo",
814
+ "/stryker.log",
815
+ ".next",
816
+ ".nuxt",
817
+ ".svelte-kit"
818
+ ]);
819
+ /**
820
+ * @see https://stryker-mutator.io/docs/stryker-js/configuration/#mutate-string
821
+ * @example
822
+ * * "src/app.js:1-11" will mutate lines 1 through 11 inside app.js.
823
+ * * "src/app.js:5:4-6:4" will mutate from line 5, column 4 through line 6 column 4 inside app.js (columns 4 are included).
824
+ * * "src/app.js:5-6:4" will mutate from line 5, column 0 through line 6 column 4 inside app.js (column 4 is included).
825
+ */
826
+ const MUTATION_RANGE_REGEX = /(.*?):((\d+)(?::(\d+))?-(\d+)(?::(\d+))?)$/;
827
+ var ProjectReader = class {
828
+ fs;
829
+ log;
830
+ mutatePatterns;
831
+ testFilePatterns;
832
+ ignoreRules;
833
+ incremental;
834
+ force;
835
+ incrementalFile;
836
+ static inject = tokens("fs", commonTokens.logger, commonTokens.options);
837
+ constructor(fs, log, { mutate, tempDirName, ignorePatterns, incremental, incrementalFile, force, htmlReporter, jsonReporter, testFiles }) {
838
+ this.fs = fs;
839
+ this.log = log;
840
+ this.mutatePatterns = mutate;
841
+ this.testFilePatterns = testFiles ?? [];
842
+ this.ignoreRules = [
843
+ ...ALWAYS_IGNORE,
844
+ tempDirName,
845
+ incrementalFile,
846
+ htmlReporter.fileName,
847
+ jsonReporter.fileName,
848
+ ...ignorePatterns
849
+ ];
850
+ this.incremental = incremental;
851
+ this.incrementalFile = incrementalFile;
852
+ this.force = force;
853
+ }
854
+ async read(targetMutatePatterns) {
855
+ const inputFileNames = await this.resolveInputFileNames();
856
+ const fileDescriptions = this.resolveFileDescriptions(inputFileNames, targetMutatePatterns);
857
+ const testFiles = this.resolveTestFiles(inputFileNames);
858
+ const project = new Project(this.fs, fileDescriptions, await this.readIncrementalReport(), testFiles);
859
+ project.logFiles(this.log, this.ignoreRules, this.force, this.mutatePatterns, this.testFilePatterns);
860
+ return project;
861
+ }
862
+ /**
863
+ * Resolves test file patterns to actual file paths.
864
+ * @param inputFileNames The list of all input file names to filter
865
+ * @returns Array of resolved test file paths
866
+ */
867
+ resolveTestFiles(inputFileNames) {
868
+ if (this.testFilePatterns.length === 0) return [];
869
+ const resolvedTestFiles = [];
870
+ for (const pattern of this.testFilePatterns) {
871
+ const matcher = new FileMatcher(pattern, false);
872
+ const matchedFiles = inputFileNames.filter((fileName) => matcher.matches(fileName));
873
+ if (matchedFiles.length === 0) this.log.warn(`Glob pattern "${pattern}" did not match any test files.`);
874
+ resolvedTestFiles.push(...matchedFiles);
875
+ }
876
+ return [...new Set(resolvedTestFiles)];
877
+ }
878
+ /**
879
+ * Takes the list of file names and creates file description object from it, containing logic about wether or not it needs to be mutated.
880
+ * If a mutate pattern starts with a `!`, it negates the pattern.
881
+ * @param inputFileNames the file names to filter
882
+ * @param targetMutatePatterns optional mutate patterns to limit the initial scope of files to mutate (with ranges)
883
+ */
884
+ resolveFileDescriptions(inputFileNames, targetMutatePatterns) {
885
+ const logAboutUselessPatterns = !isDeepStrictEqual(this.mutatePatterns, defaultOptions.mutate);
886
+ const mutateInputFileMap = /* @__PURE__ */ new Map();
887
+ inputFileNames.forEach((fileName) => mutateInputFileMap.set(fileName, { mutate: false }));
888
+ for (const pattern of this.mutatePatterns) if (pattern.startsWith("!")) {
889
+ const files = this.filterMutatePattern(mutateInputFileMap.keys(), pattern.substring(1));
890
+ if (logAboutUselessPatterns && files.size === 0) this.log.warn(`Glob pattern "${pattern}" did not exclude any files.`);
891
+ for (const fileName of files.keys()) mutateInputFileMap.set(fileName, { mutate: false });
892
+ } else {
893
+ const files = this.filterMutatePattern(inputFileNames, pattern);
894
+ if (logAboutUselessPatterns && files.size === 0) this.log.warn(`Glob pattern "${pattern}" did not result in any files.`);
895
+ for (const [fileName, file] of files) mutateInputFileMap.set(fileName, this.unionFileDescriptions(file, mutateInputFileMap.get(fileName)));
896
+ }
897
+ if (targetMutatePatterns) {
898
+ const seen = /* @__PURE__ */ new Map();
899
+ for (const pattern of targetMutatePatterns) {
900
+ const files = this.filterMutatePattern(mutateInputFileMap.keys(), pattern);
901
+ for (const [fileName, description] of files) {
902
+ const current = mutateInputFileMap.get(fileName);
903
+ if (current === void 0) continue;
904
+ const intersected = this.intersectFileDescriptions(current, description);
905
+ seen.set(fileName, this.unionFileDescriptions(intersected, seen.get(fileName)));
906
+ }
907
+ }
908
+ for (const fileName of mutateInputFileMap.keys()) {
909
+ const descriptionInSeen = seen.get(fileName);
910
+ if (descriptionInSeen) mutateInputFileMap.set(fileName, descriptionInSeen);
911
+ else mutateInputFileMap.set(fileName, { mutate: false });
912
+ }
913
+ }
914
+ return Object.fromEntries(mutateInputFileMap);
915
+ }
916
+ unionFileDescriptions(first, second) {
917
+ if (second) {
918
+ if (Array.isArray(first.mutate) && Array.isArray(second.mutate)) return { mutate: [...second.mutate, ...first.mutate] };
919
+ else if (second.mutate === true) return { mutate: true };
920
+ return { mutate: first.mutate || second.mutate };
921
+ }
922
+ return first;
923
+ }
924
+ intersectFileDescriptions(first, second) {
925
+ if (Array.isArray(first.mutate) && Array.isArray(second.mutate)) {
926
+ const secondMutate = second.mutate;
927
+ return { mutate: first.mutate.flatMap((firstRange) => secondMutate.map((secondRange) => {
928
+ const startLine = Math.max(firstRange.start.line, secondRange.start.line);
929
+ const endLine = Math.min(firstRange.end.line, secondRange.end.line);
930
+ if (startLine > endLine) return;
931
+ const startColumn = firstRange.start.line === startLine ? firstRange.start.column : secondRange.start.column;
932
+ const endColumn = firstRange.end.line === endLine ? firstRange.end.column : secondRange.end.column;
933
+ return {
934
+ start: {
935
+ line: startLine,
936
+ column: startColumn
937
+ },
938
+ end: {
939
+ line: endLine,
940
+ column: endColumn
941
+ }
942
+ };
943
+ })).filter(notEmpty) };
944
+ } else if (first.mutate === true) return second;
945
+ else if (second.mutate === true) return first;
946
+ return { mutate: false };
947
+ }
948
+ /**
949
+ * Filters a given list of file names given a mutate pattern.
950
+ * @param fileNames the file names to match to the pattern
951
+ * @param mutatePattern the pattern to match with
952
+ */
953
+ filterMutatePattern(fileNames, mutatePattern) {
954
+ const mutationRangeMatch = MUTATION_RANGE_REGEX.exec(mutatePattern);
955
+ let mutate = true;
956
+ if (mutationRangeMatch) {
957
+ const [_, newPattern, _mutationRange, startLine, startColumn = "0", endLine, endColumn = Number.MAX_SAFE_INTEGER.toString()] = mutationRangeMatch;
958
+ mutatePattern = newPattern ?? mutatePattern;
959
+ mutate = [{
960
+ start: {
961
+ line: parseInt(startLine ?? "1") - 1,
962
+ column: parseInt(startColumn)
963
+ },
964
+ end: {
965
+ line: parseInt(endLine ?? "1") - 1,
966
+ column: parseInt(endColumn)
967
+ }
968
+ }];
969
+ }
970
+ const matcher = new FileMatcher(mutatePattern, false);
971
+ const inputFiles = /* @__PURE__ */ new Map();
972
+ for (const fileName of fileNames) if (matcher.matches(fileName)) inputFiles.set(fileName, { mutate });
973
+ return inputFiles;
974
+ }
975
+ async resolveInputFileNames() {
976
+ const ignoreRules = this.ignoreRules.map((pattern) => new Minimatch(pattern, {
977
+ dot: true,
978
+ flipNegate: true,
979
+ nocase: true
980
+ }));
981
+ /**
982
+ * Rewrite of: https://github.com/npm/ignore-walk/blob/0e4f87adccb3e16f526d2e960ed04bdc77fd6cca/index.js#L213-L215
983
+ */
984
+ const matchesDirectoryPartially = (entryPath, rule) => {
985
+ return rule.match(`/${entryPath}`, true) || rule.match(entryPath, true);
986
+ };
987
+ const matchesDirectory = (entryName, entryPath, rule) => {
988
+ return matchesFile(entryName, entryPath, rule) || rule.match(`/${entryPath}/`) || rule.match(`${entryPath}/`) || rule.negate && matchesDirectoryPartially(entryPath, rule);
989
+ };
990
+ const matchesFile = (entryName, entryPath, rule) => {
991
+ return rule.match(entryName) || rule.match(entryPath) || rule.match(`/${entryPath}`);
992
+ };
993
+ const crawlDir = async (dir, rootDir = dir) => {
994
+ const dirEntries = await this.fs.readdir(dir, { withFileTypes: true });
995
+ const relativeName = path.relative(rootDir, dir);
996
+ return (await Promise.all(dirEntries.filter((dirEntry) => {
997
+ let included = true;
998
+ const entryPath = `${relativeName.length ? `${relativeName}/` : ""}${dirEntry.name}`;
999
+ ignoreRules.forEach((rule) => {
1000
+ if (rule.negate !== included) {
1001
+ if (dirEntry.isDirectory() ? matchesDirectory(dirEntry.name, entryPath, rule) : matchesFile(dirEntry.name, entryPath, rule)) included = rule.negate;
1002
+ }
1003
+ });
1004
+ return included;
1005
+ }).map(async (dirent) => {
1006
+ if (dirent.isDirectory()) return crawlDir(path.resolve(rootDir, relativeName, dirent.name), rootDir);
1007
+ else return path.resolve(rootDir, relativeName, dirent.name);
1008
+ }))).flat();
1009
+ };
1010
+ return await crawlDir(process.cwd());
1011
+ }
1012
+ async readIncrementalReport() {
1013
+ if (!this.incremental) return;
1014
+ try {
1015
+ const contents = await this.fs.readFile(this.incrementalFile, "utf-8");
1016
+ const result = JSON.parse(contents);
1017
+ const decoded = S.decodeUnknownSync(IncrementalReportSchema)(result);
1018
+ return {
1019
+ ...decoded,
1020
+ files: Object.fromEntries(Object.entries(decoded.files).map(([fileName, file]) => [fileName, {
1021
+ ...file,
1022
+ mutants: file.mutants.map((mutant) => ({
1023
+ ...mutant,
1024
+ location: reportLocationToStrykerLocation(mutant.location)
1025
+ }))
1026
+ }])),
1027
+ ...decoded.testFiles ? { testFiles: Object.fromEntries(Object.entries(decoded.testFiles).map(([fileName, file]) => [fileName, {
1028
+ ...file,
1029
+ tests: file.tests.map((test) => ({
1030
+ ...test,
1031
+ ...test.location ? { location: reportOpenEndLocationToStrykerLocation(test.location) } : {}
1032
+ }))
1033
+ }])) } : {}
1034
+ };
1035
+ } catch (err) {
1036
+ if (isErrnoException(err) && err.code === ERROR_CODES.NoSuchFileOrDirectory) {
1037
+ this.log.info("No incremental result file found at %s, a full mutation testing run will be performed.", this.incrementalFile);
1038
+ return;
1039
+ }
1040
+ throw err;
1041
+ }
1042
+ }
1043
+ };
1044
+ function reportOpenEndLocationToStrykerLocation({ start, end }) {
1045
+ return {
1046
+ start: reportPositionToStrykerPosition(start),
1047
+ ...end ? { end: reportPositionToStrykerPosition(end) } : {}
1048
+ };
1049
+ }
1050
+ function reportLocationToStrykerLocation({ start, end }) {
1051
+ return {
1052
+ start: reportPositionToStrykerPosition(start),
1053
+ end: reportPositionToStrykerPosition(end)
1054
+ };
1055
+ }
1056
+ function reportPositionToStrykerPosition({ line, column }) {
1057
+ return {
1058
+ line: line - 1,
1059
+ column: column - 1
1060
+ };
1061
+ }
1062
+ //#endregion
1063
+ //#region src/worker-pool/kill.ts
1064
+ function kill(pid) {
1065
+ const { promise, resolve, reject } = Promise.withResolvers();
1066
+ if (pid === void 0) {
1067
+ resolve();
1068
+ return promise;
1069
+ }
1070
+ treeKill(pid, "SIGKILL", (err) => {
1071
+ if (err && !canIgnore(err.code)) reject(err);
1072
+ else resolve();
1073
+ });
1074
+ function canIgnore(code) {
1075
+ return code === 255 || code === 128;
1076
+ }
1077
+ return promise;
1078
+ }
1079
+ //#endregion
1080
+ //#region src/test-runner/command-test-runner.ts
1081
+ /**
1082
+ * A test runner that uses a (bash or cmd) command to execute the tests.
1083
+ * Does not know hom many tests are executed or any code coverage results,
1084
+ * instead, it mimics a simple test result based on the exit code.
1085
+ * The command can be configured, but defaults to `npm test`.
1086
+ */
1087
+ var CommandTestRunner = class CommandTestRunner {
1088
+ workingDir;
1089
+ /**
1090
+ * "command"
1091
+ */
1092
+ static runnerName = CommandTestRunner.name.replace("TestRunner", "").toLowerCase();
1093
+ /**
1094
+ * Determines whether a given name is "command" (ignore case)
1095
+ * @param name Maybe "command", maybe not
1096
+ */
1097
+ static is(name) {
1098
+ return this.runnerName === name.toLowerCase();
1099
+ }
1100
+ settings;
1101
+ testFilesProvided;
1102
+ timeoutHandler;
1103
+ constructor(workingDir, options) {
1104
+ this.workingDir = workingDir;
1105
+ this.settings = options.commandRunner;
1106
+ this.testFilesProvided = testFilesProvided(options);
1107
+ }
1108
+ capabilities() {
1109
+ return { reloadEnvironment: true };
1110
+ }
1111
+ async init() {
1112
+ if (this.testFilesProvided) throw new Error(`The ${CommandTestRunner.runnerName} test runner does not support the --testFiles option.`);
1113
+ return Promise.resolve();
1114
+ }
1115
+ async dryRun(_options) {
1116
+ return this.run({});
1117
+ }
1118
+ async mutantRun({ activeMutant }) {
1119
+ const result = await this.run({ activeMutantId: activeMutant.id });
1120
+ return toMutantRunResult(result);
1121
+ }
1122
+ run({ activeMutantId }) {
1123
+ const timerInstance = new Timer();
1124
+ return new Promise((res, rej) => {
1125
+ const output = [];
1126
+ const env = activeMutantId === void 0 ? process.env : {
1127
+ ...process.env,
1128
+ [INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE]: activeMutantId
1129
+ };
1130
+ const childProcess = exec(this.settings.command, {
1131
+ cwd: this.workingDir,
1132
+ env
1133
+ });
1134
+ if (childProcess.stdout === null || childProcess.stderr === null) throw new Error(`Expected stdout and stderr streams for "${this.settings.command}", but one of them is null.`);
1135
+ const stdout = childProcess.stdout;
1136
+ const stderr = childProcess.stderr;
1137
+ childProcess.on("error", (error) => {
1138
+ kill(childProcess.pid).then(() => handleResolve(errorResult(error))).catch(rej);
1139
+ });
1140
+ childProcess.on("exit", (code) => {
1141
+ const result = completeResult(code, timerInstance);
1142
+ handleResolve(result);
1143
+ });
1144
+ stdout.on("data", (chunk) => {
1145
+ output.push(chunk);
1146
+ });
1147
+ stderr.on("data", (chunk) => {
1148
+ output.push(chunk);
1149
+ });
1150
+ this.timeoutHandler = async () => {
1151
+ handleResolve({ status: DryRunStatus.Timeout });
1152
+ await kill(childProcess.pid);
1153
+ };
1154
+ const handleResolve = (runResult) => {
1155
+ removeAllListeners();
1156
+ this.timeoutHandler = void 0;
1157
+ res(runResult);
1158
+ };
1159
+ function removeAllListeners() {
1160
+ stderr.removeAllListeners();
1161
+ stdout.removeAllListeners();
1162
+ childProcess.removeAllListeners();
1163
+ }
1164
+ function errorResult(error) {
1165
+ return {
1166
+ errorMessage: errorToString(error),
1167
+ status: DryRunStatus.Error
1168
+ };
1169
+ }
1170
+ function completeResult(exitCode, timer) {
1171
+ const duration = timer.elapsedMs();
1172
+ if (exitCode === 0) return {
1173
+ status: DryRunStatus.Complete,
1174
+ tests: [{
1175
+ id: "all",
1176
+ name: "All tests",
1177
+ status: TestStatus.Success,
1178
+ timeSpentMs: duration
1179
+ }]
1180
+ };
1181
+ else return {
1182
+ status: DryRunStatus.Complete,
1183
+ tests: [{
1184
+ id: "all",
1185
+ failureMessage: output.map((buf) => buf.toString()).join(os.EOL),
1186
+ name: "All tests",
1187
+ status: TestStatus.Failed,
1188
+ timeSpentMs: duration
1189
+ }]
1190
+ };
1191
+ }
1192
+ });
1193
+ }
1194
+ async dispose() {
1195
+ if (this.timeoutHandler) await this.timeoutHandler();
1196
+ }
1197
+ };
1198
+ //#endregion
1199
+ //#region src/config/is-warning-enabled.ts
1200
+ function isWarningEnabled(warningType, warningOptions) {
1201
+ if (typeof warningOptions === "boolean") return warningOptions;
1202
+ else return !!warningOptions[warningType];
1203
+ }
1204
+ //#endregion
1205
+ //#region src/config/options-path.ts
1206
+ /**
1207
+ * Print the name of (or path to) a stryker option
1208
+ */
1209
+ const optionsPath = propertyPath();
1210
+ //#endregion
1211
+ //#region src/config/validation-errors.ts
1212
+ /** `["mutator"]["name"]` — the path shape a rendered schema issue carries. */
1213
+ const PATH_LINE = /^at\s+(\[.*\])$/;
1214
+ const PATH_SEGMENT = /\["([^"]*)"\]|\[(\d+)\]/g;
1215
+ const EXPECTED_PATTERN = /^Expected a string matching the RegExp (.+)$/;
1216
+ const EXPECTED_TYPE = /^Expected (.+)$/;
1217
+ /** `["mutator"]["name"]` -> `mutator.name`; an index stays bracketed. */
1218
+ const dottedPath = (raw) => {
1219
+ let path = "";
1220
+ for (const [, key, index] of raw.matchAll(PATH_SEGMENT)) if (index !== void 0) path += `[${index}]`;
1221
+ else path += path.length > 0 ? `.${key ?? ""}` : key ?? "";
1222
+ return path;
1223
+ };
1224
+ /** The expectation, in the phrasing an operator reading the terminal already knows. */
1225
+ const phrase = (expectation) => {
1226
+ const pattern = EXPECTED_PATTERN.exec(expectation);
1227
+ if (pattern !== null) return `must match pattern "${pattern[1] ?? ""}"`;
1228
+ const type = EXPECTED_TYPE.exec(expectation);
1229
+ if (type !== null) return `should be ${type[1] ?? ""}`;
1230
+ return expectation;
1231
+ };
1232
+ /**
1233
+ * Renders a decode failure as one message per offending option.
1234
+ *
1235
+ * This replaced roughly two hundred lines that existed to make a JSON Schema
1236
+ * engine's error objects readable: they deduplicated errors shadowing each other
1237
+ * on one path, merged the several type errors a union produced for one option,
1238
+ * and rebuilt the option's path by hand. The schema issue already carries the
1239
+ * expectation and the path, so what remains is naming the option and keeping the
1240
+ * phrasing the CLI contract pins — an operator is told which option is wrong and
1241
+ * what it must be, not how the value was decoded.
1242
+ *
1243
+ * An issue is an expectation line followed by its indented `at [...]` path. If
1244
+ * that shape ever changes, each line arrives as its own message: the phrasing
1245
+ * degrades and no information is lost.
1246
+ */
1247
+ function describeErrors(error) {
1248
+ const messages = [];
1249
+ let expectation;
1250
+ for (const raw of error.message.split("\n")) {
1251
+ const line = raw.trim();
1252
+ if (line.length === 0) continue;
1253
+ const path = PATH_LINE.exec(line);
1254
+ if (path !== null && expectation !== void 0) {
1255
+ messages.push(`Config option "${dottedPath(path[1] ?? "")}" ${phrase(expectation)}.`);
1256
+ expectation = void 0;
1257
+ continue;
1258
+ }
1259
+ if (expectation !== void 0) messages.push(expectation);
1260
+ expectation = line;
1261
+ }
1262
+ if (expectation !== void 0) messages.push(expectation);
1263
+ return messages.length > 0 ? messages : [error.message];
1264
+ }
1265
+ //#endregion
1266
+ //#region src/config/options-validator.ts
1267
+ /**
1268
+ * Options are decoded by the schema that declares them. This replaced an ajv
1269
+ * instance run with `useDefaults`, which filled defaults from the *derived* JSON
1270
+ * Schema document and silently injected nothing wherever a `default` annotation
1271
+ * did not survive that derivation: `timeoutFactor`, `timeoutMS` and
1272
+ * `dryRunTimeoutMinutes` all arrived `undefined`, so
1273
+ * `dryRunTimeoutMinutes * 1000 * 60` was `NaN`, every dry run was given a 1 ms
1274
+ * budget, and each one reported "Initial test run timed out". Decoding through
1275
+ * the declaration cannot drift from it, and the open struct keeps the
1276
+ * plugin-contributed options a document-driven validator would have rejected.
1277
+ *
1278
+ * `errors: 'all'` keeps the previous behaviour of reporting every offending
1279
+ * option in one pass rather than stopping at the first.
1280
+ */
1281
+ const decodeOptions = S.decodeUnknownResult(StrykerOptionsSchema, { errors: "all" });
1282
+ var OptionsValidator = class OptionsValidator {
1283
+ schema;
1284
+ log;
1285
+ static inject = tokens(validationSchema, commonTokens.logger);
1286
+ constructor(schema, log) {
1287
+ this.schema = schema;
1288
+ this.log = log;
1289
+ }
1290
+ /**
1291
+ * Validates the provided options, throwing an error if something is wrong.
1292
+ * Optionally also warns about excess or unserializable options.
1293
+ * @param options The stryker options to validate
1294
+ * @param mark Wether or not to log warnings on unknown properties or unserializable properties
1295
+ */
1296
+ validate(options, mark = false) {
1297
+ this.removeDeprecatedOptions(options);
1298
+ this.validateRemovedSurface(options);
1299
+ this.schemaValidate(options);
1300
+ this.customValidation(options);
1301
+ if (mark) this.markOptions(options);
1302
+ }
1303
+ /**
1304
+ * Hard-fails on config names the rebuild removed (U2-U9): a removed
1305
+ * top-level key (`dashboard`, `eventReporter`) or a removed reporter name
1306
+ * inside `reporters` (`dots`, `event-recorder`, `progress-append-only`,
1307
+ * `dashboard`). Runs ahead of `schemaValidate` because a removed key is
1308
+ * absent from the AJV schema, so schema validation can never see it, and
1309
+ * the warning path (`markExcessOptions`) only fires when `mark` is set and
1310
+ * the `unknownOptions` warning is enabled — a removed key could otherwise
1311
+ * be silently accepted. Only the known-removed names are checked; an
1312
+ * unknown-but-not-removed key keeps its warning-only behaviour.
1313
+ */
1314
+ validateRemovedSurface(rawOptions) {
1315
+ const errors = [];
1316
+ for (const key of Object.keys(rawOptions)) if (Object.hasOwn(REMOVED_OPTIONS, key)) errors.push(`Config option "${key}" is no longer supported. ${REMOVED_OPTIONS[key]}`);
1317
+ const reporters = rawOptions["reporters"];
1318
+ if (Array.isArray(reporters)) {
1319
+ for (const name of reporters) if (typeof name === "string" && Object.hasOwn(REMOVED_OPTIONS, name)) errors.push(`Config option "reporters" contains removed reporter name "${name}". ${REMOVED_OPTIONS[name]}`);
1320
+ }
1321
+ errors.forEach((error) => this.log.error(error));
1322
+ this.throwErrorIfNeeded(errors);
1323
+ }
1324
+ removeDeprecatedOptions(rawOptions) {
1325
+ this.removeStringMutator(rawOptions);
1326
+ this.removeMutatorName(rawOptions);
1327
+ this.removeTestFramework(rawOptions);
1328
+ this.removeTranspilers(rawOptions);
1329
+ this.rewriteFiles(rawOptions);
1330
+ this.removeJestEnableBail(rawOptions);
1331
+ this.removeHtmlReporterBaseDir(rawOptions);
1332
+ this.migrateMaxConcurrentTestRunners(rawOptions);
1333
+ }
1334
+ static recordOf(value) {
1335
+ return S.decodeUnknownSync(S.Record(S.String, S.Unknown))(value);
1336
+ }
1337
+ removeStringMutator(rawOptions) {
1338
+ if (typeof rawOptions["mutator"] !== "string") return;
1339
+ this.log.warn("DEPRECATED. Use of \"mutator\" as string is no longer needed. You can remove it from your configuration. Stryker now supports mutating of JavaScript and friend files out of the box.");
1340
+ delete rawOptions["mutator"];
1341
+ }
1342
+ removeMutatorName(rawOptions) {
1343
+ const mutator = rawOptions["mutator"];
1344
+ if (typeof mutator !== "object" || mutator === null) return;
1345
+ const mutatorRecord = OptionsValidator.recordOf(mutator);
1346
+ if (!mutatorRecord["name"]) return;
1347
+ this.log.warn("DEPRECATED. Use of \"mutator.name\" is no longer needed. You can remove \"mutator.name\" from your configuration. Stryker now supports mutating of JavaScript and friend files out of the box.");
1348
+ delete mutatorRecord["name"];
1349
+ rawOptions["mutator"] = mutatorRecord;
1350
+ }
1351
+ removeTestFramework(rawOptions) {
1352
+ if (!Object.keys(rawOptions).includes("testFramework")) return;
1353
+ this.log.warn("DEPRECATED. Use of \"testFramework\" is no longer needed. You can remove it from your configuration. Your test runner plugin now handles its own test framework integration.");
1354
+ delete rawOptions["testFramework"];
1355
+ }
1356
+ removeTranspilers(rawOptions) {
1357
+ const transpilers = rawOptions["transpilers"];
1358
+ if (!Array.isArray(transpilers)) return;
1359
+ const example = transpilers.includes("babel") ? "babel src --out-dir lib" : transpilers.includes("typescript") ? "tsc -b" : transpilers.includes("webpack") ? "webpack --config webpack.config.js" : "npm run build";
1360
+ this.log.warn(`DEPRECATED. Support for "transpilers" is removed. You can now configure your own "${optionsPath("buildCommand")}". For example, ${example}.`);
1361
+ delete rawOptions["transpilers"];
1362
+ }
1363
+ rewriteFiles(rawOptions) {
1364
+ const files = rawOptions["files"];
1365
+ if (!Array.isArray(files)) return;
1366
+ const ignorePatternsName = optionsPath("ignorePatterns");
1367
+ const isString = (uncertain) => typeof uncertain === "string";
1368
+ const newIgnorePatterns = ["**", ...files.filter(isString).map((filePattern) => filePattern.startsWith("!") ? filePattern.substr(1) : `!${filePattern}`)];
1369
+ delete rawOptions["files"];
1370
+ this.log.warn(`DEPRECATED. Use of "files" is deprecated, please use "${ignorePatternsName}" instead (or remove "files" altogether will probably work as well). For now, rewriting them as ${JSON.stringify(newIgnorePatterns)}. See https://stryker-mutator.io/docs/stryker-js/configuration/#ignorepatterns-string`);
1371
+ const existingIgnorePatterns = Array.isArray(rawOptions[ignorePatternsName]) ? rawOptions[ignorePatternsName] : [];
1372
+ rawOptions[ignorePatternsName] = [...newIgnorePatterns, ...existingIgnorePatterns];
1373
+ }
1374
+ removeJestEnableBail(rawOptions) {
1375
+ const jestOptions = rawOptions["jest"];
1376
+ if (typeof jestOptions !== "object" || jestOptions === null) return;
1377
+ const jestRecord = OptionsValidator.recordOf(jestOptions);
1378
+ const enableBail = jestRecord["enableBail"];
1379
+ if (enableBail === void 0) return;
1380
+ this.log.warn("DEPRECATED. Use of \"jest.enableBail\" is deprecated, please use \"disableBail\" instead. See https://stryker-mutator.io/docs/stryker-js/configuration#disablebail-boolean");
1381
+ rawOptions["disableBail"] = !enableBail;
1382
+ delete jestRecord["enableBail"];
1383
+ rawOptions["jest"] = jestRecord;
1384
+ }
1385
+ removeHtmlReporterBaseDir(rawOptions) {
1386
+ const htmlReporter = rawOptions["htmlReporter"];
1387
+ if (typeof htmlReporter !== "object" || htmlReporter === null) return;
1388
+ const reporter = OptionsValidator.recordOf(htmlReporter);
1389
+ const baseDir = reporter["baseDir"];
1390
+ if (!baseDir) return;
1391
+ this.log.warn(`DEPRECATED. Use of "htmlReporter.baseDir" is deprecated, please use "${optionsPath("htmlReporter", "fileName")}" instead. See https://stryker-mutator.io/docs/stryker-js/configuration/#reporters-string`);
1392
+ const baseDirText = typeof baseDir === "string" ? baseDir : JSON.stringify(baseDir) ?? "";
1393
+ if (!reporter["fileName"]) reporter["fileName"] = path.join(baseDirText, "index.html");
1394
+ delete reporter["baseDir"];
1395
+ rawOptions["htmlReporter"] = reporter;
1396
+ }
1397
+ migrateMaxConcurrentTestRunners(rawOptions) {
1398
+ const maxConcurrent = rawOptions["maxConcurrentTestRunners"];
1399
+ if (typeof maxConcurrent !== "number" || maxConcurrent === Number.MAX_SAFE_INTEGER) return;
1400
+ this.log.warn("DEPRECATED. Use of \"maxConcurrentTestRunners\" is deprecated. Please use \"concurrency\" instead.");
1401
+ if (!rawOptions["concurrency"] && maxConcurrent < os.availableParallelism() - 1) rawOptions["concurrency"] = maxConcurrent;
1402
+ }
1403
+ customValidation(options) {
1404
+ const additionalErrors = [];
1405
+ if (options.thresholds.high < options.thresholds.low) additionalErrors.push("Config option \"thresholds.high\" should be higher than \"thresholds.low\".");
1406
+ if (CommandTestRunner.is(options.testRunner)) {
1407
+ if (options.testRunnerNodeArgs.length) this.log.warn("Using \"testRunnerNodeArgs\" together with the \"command\" test runner is not supported, these arguments will be ignored. You can add your custom arguments by setting the \"commandRunner.command\" option.");
1408
+ }
1409
+ if (options.ignoreStatic && options.coverageAnalysis !== "perTest") additionalErrors.push(`Config option "${optionsPath("ignoreStatic")}" is not supported with coverage analysis "${options.coverageAnalysis}". Either turn off "${optionsPath("ignoreStatic")}", or configure "${optionsPath("coverageAnalysis")}" to be "perTest".`);
1410
+ options.mutate.forEach((mutateString, index) => {
1411
+ const match = MUTATION_RANGE_REGEX.exec(mutateString);
1412
+ if (match) {
1413
+ if (new Minimatch(mutateString).hasMagic()) additionalErrors.push(`Config option "mutate[${index}]" is invalid. Cannot combine a glob expression with a mutation range in "${mutateString}".`);
1414
+ else {
1415
+ const [_, _fileName, mutationRange, startLine, _startColumn, endLine, _endColumn] = match;
1416
+ const start = parseInt(startLine ?? "", 10);
1417
+ const end = parseInt(endLine ?? "", 10);
1418
+ if (start < 1) additionalErrors.push(`Config option "mutate[${index}]" is invalid. Mutation range "${mutationRange}" is invalid, line ${start} does not exist (lines start at 1).`);
1419
+ if (start > end) additionalErrors.push(`Config option "mutate[${index}]" is invalid. Mutation range "${mutationRange}" is invalid. The "from" line number (${start}) should be less then the "to" line number (${end}).`);
1420
+ }
1421
+ }
1422
+ });
1423
+ additionalErrors.forEach((error) => this.log.error(error));
1424
+ this.throwErrorIfNeeded(additionalErrors);
1425
+ }
1426
+ /**
1427
+ * Decodes the options against their declaration, then writes the result back
1428
+ * onto the caller's object: the previous engine filled defaults in place and
1429
+ * every caller reads them off the object it passed, so the fill has to land
1430
+ * there rather than in a copy.
1431
+ */
1432
+ schemaValidate(options) {
1433
+ const decoded = decodeOptions(options);
1434
+ if (Result$1.isFailure(decoded)) {
1435
+ const describedErrors = describeErrors(decoded.failure);
1436
+ describedErrors.forEach((error) => this.log.error(error));
1437
+ this.throwErrorIfNeeded(describedErrors);
1438
+ return;
1439
+ }
1440
+ Object.assign(options, decoded.success);
1441
+ }
1442
+ throwErrorIfNeeded(errors) {
1443
+ if (errors.length > 0) {
1444
+ const headline = errors.length === 1 ? "Please correct this configuration error and try again." : "Please correct these configuration errors and try again.";
1445
+ throw new ConfigError(`${headline} ${errors.join(" ")}`);
1446
+ }
1447
+ }
1448
+ markOptions(options) {
1449
+ this.markExcessOptions(options);
1450
+ this.markUnserializableOptions(options);
1451
+ }
1452
+ markExcessOptions(options) {
1453
+ const OPTIONS_ADDED_BY_STRYKER = [
1454
+ "set",
1455
+ "configFile",
1456
+ "$schema"
1457
+ ];
1458
+ if (isWarningEnabled("unknownOptions", options.warnings)) {
1459
+ const schemaProperties = OptionsValidator.recordOf(this.schema["properties"] ?? {});
1460
+ const schemaKeys = Object.keys(schemaProperties);
1461
+ const excessPropertyNames = Object.keys(options).filter((key) => !key.endsWith("_comment")).filter((key) => !OPTIONS_ADDED_BY_STRYKER.includes(key)).filter((key) => !schemaKeys.includes(key));
1462
+ if (excessPropertyNames.length) {
1463
+ excessPropertyNames.forEach((excessPropertyName) => {
1464
+ this.log.warn(`Unknown stryker config option "${excessPropertyName}".`);
1465
+ });
1466
+ this.log.warn(`Possible causes:
1467
+ * Is it a typo on your end?
1468
+ * Did you only write this property as a comment? If so, please postfix it with "_comment".
1469
+ * You might be missing a plugin that is supposed to use it. Stryker loaded plugins from: ${JSON.stringify(options.plugins)}
1470
+ * The plugin that is using it did not contribute explicit validation.
1471
+ (disable "${optionsPath("warnings", "unknownOptions")}" to ignore this warning)`);
1472
+ }
1473
+ }
1474
+ }
1475
+ markUnserializableOptions(options) {
1476
+ if (isWarningEnabled("unserializableOptions", options.warnings)) {
1477
+ const unserializables = findUnserializables(options);
1478
+ if (unserializables) {
1479
+ unserializables.forEach((unserializable) => this.log.warn(`Config option "${unserializable.path.join(".")}" is not (fully) serializable. ${unserializable.reason}. Any test runner or checker worker processes might not receive this value as intended.`));
1480
+ this.log.warn(`(disable ${optionsPath("warnings", "unserializableOptions")} to ignore this warning)`);
1481
+ }
1482
+ }
1483
+ }
1484
+ };
1485
+ /**
1486
+ * The defaults are the declaration decoded against an empty object: every field
1487
+ * that declares a default supplies it, and the result is the complete option
1488
+ * set. It no longer routes through the validator, which used to be the only way
1489
+ * to obtain them and produced an object missing three numeric defaults.
1490
+ */
1491
+ function createDefaultOptions() {
1492
+ return Result$1.getOrThrow(decodeOptions({}));
1493
+ }
1494
+ const defaultOptions = deepFreeze(createDefaultOptions());
1495
+ //#endregion
1496
+ //#region src/plugins/plugin-loader.schema.ts
1497
+ /**
1498
+ * The shape of a module namespace that contributes Stryker plugins:
1499
+ * an exported `strykerPlugins` array. Elements are runtime plugin objects,
1500
+ * so only array-ness is checked here.
1501
+ */
1502
+ const PluginModuleSchema = Schema.Struct({ strykerPlugins: Schema.Array(Schema.Unknown) });
1503
+ /**
1504
+ * The shape of a module namespace that contributes a JSON schema
1505
+ * extension: an exported `strykerValidationSchema` record.
1506
+ */
1507
+ const SchemaValidationContributionSchema = Schema.Struct({ strykerValidationSchema: Schema.Record(Schema.String, Schema.Unknown) });
1508
+ //#endregion
1509
+ //#region src/plugins/plugin-loader.ts
1510
+ const IGNORED_PACKAGES = [
1511
+ "core",
1512
+ "api",
1513
+ "util",
1514
+ "instrumenter"
1515
+ ];
1516
+ /**
1517
+ * Can resolve modules and pull them into memory
1518
+ */
1519
+ var PluginLoader = class {
1520
+ log;
1521
+ static inject = tokens(commonTokens.logger);
1522
+ constructor(log) {
1523
+ this.log = log;
1524
+ }
1525
+ /**
1526
+ * Loads plugins based on configured plugin descriptors.
1527
+ * A plugin descriptor can be:
1528
+ * * A full url: "file:///home/nicojs/github/my-plugin.js"
1529
+ * * An absolute file path: "/home/nicojs/github/my-plugin.js"
1530
+ * * A relative path: "./my-plugin.js"
1531
+ * * A bare import expression: "@stryker-mutator/karma-runner"
1532
+ * * A simple glob expression (only wild cards are supported): "@stryker-mutator/*"
1533
+ */
1534
+ async load(pluginDescriptors) {
1535
+ const pluginModules = await this.resolvePluginModules(pluginDescriptors);
1536
+ const loadedPluginModules = (await Promise.all(pluginModules.map(async (moduleName) => {
1537
+ return {
1538
+ ...await this.loadPlugin(moduleName),
1539
+ moduleName
1540
+ };
1541
+ }))).filter(notEmpty);
1542
+ const result = {
1543
+ schemaContributions: [],
1544
+ pluginsByKind: /* @__PURE__ */ new Map(),
1545
+ pluginModulePaths: []
1546
+ };
1547
+ loadedPluginModules.forEach(({ plugins, schemaContribution, moduleName }) => {
1548
+ if (plugins) {
1549
+ result.pluginModulePaths.push(moduleName);
1550
+ plugins.forEach((plugin) => {
1551
+ const pluginsForKind = result.pluginsByKind.get(plugin.kind);
1552
+ if (pluginsForKind) pluginsForKind.push(plugin);
1553
+ else result.pluginsByKind.set(plugin.kind, [plugin]);
1554
+ });
1555
+ }
1556
+ if (schemaContribution) result.schemaContributions.push(schemaContribution);
1557
+ });
1558
+ return result;
1559
+ }
1560
+ async resolvePluginModules(pluginDescriptors) {
1561
+ return (await Promise.all(pluginDescriptors.map(async (pluginExpression) => {
1562
+ if (pluginExpression.includes("*")) return await this.globPluginModules(pluginExpression);
1563
+ else if (path.isAbsolute(pluginExpression) || pluginExpression.startsWith(".")) return pathToFileURL(path.resolve(pluginExpression)).toString();
1564
+ else return pluginExpression;
1565
+ }))).filter(notEmpty).flat();
1566
+ }
1567
+ async globPluginModules(pluginExpression) {
1568
+ const { org, pkg } = parsePluginExpression(pluginExpression);
1569
+ const pluginDirectory = path.resolve(fileURLToPath(new URL("../../../../../", import.meta.url)), org);
1570
+ const regexp = new RegExp("^" + pkg.replace("*", ".*"));
1571
+ this.log.debug("Loading %s from %s", pluginExpression, pluginDirectory);
1572
+ const plugins = (await fs.promises.readdir(pluginDirectory)).filter((pluginName) => !IGNORED_PACKAGES.includes(pluginName) && regexp.test(pluginName)).map((pluginName) => `${org.length ? `${org}/` : ""}${pluginName}`);
1573
+ if (plugins.length === 0 && !defaultOptions.plugins.includes(pluginExpression)) this.log.warn("Expression \"%s\" not resulted in plugins to load.", pluginExpression);
1574
+ plugins.forEach((plugin) => this.log.debug("Loading plugin \"%s\" (matched with expression %s)", plugin, pluginExpression));
1575
+ return plugins;
1576
+ }
1577
+ async loadPlugin(descriptor) {
1578
+ this.log.debug("Loading plugin %s", descriptor);
1579
+ try {
1580
+ const module = await importModule(descriptor);
1581
+ const plugins = isPluginModule(module) ? module.strykerPlugins : void 0;
1582
+ const schemaContribution = hasValidationSchemaContribution(module) ? module.strykerValidationSchema : void 0;
1583
+ if (plugins ?? schemaContribution) return {
1584
+ plugins,
1585
+ schemaContribution
1586
+ };
1587
+ else this.log.warn("Module \"%s\" did not contribute a StrykerJS plugin. It didn't export a \"%s\" or \"%s\".", descriptor, propertyPath()("strykerPlugins"), propertyPath()("strykerValidationSchema"));
1588
+ } catch (e) {
1589
+ if (isErrnoException(e) && e.code === "ERR_MODULE_NOT_FOUND" && e.message.indexOf(descriptor) !== -1) this.log.warn("Cannot find plugin \"%s\".\n Did you forget to install it ?", descriptor);
1590
+ else this.log.warn("Error during loading \"%s\" plugin:\n %s", descriptor, errorToString(e));
1591
+ }
1592
+ }
1593
+ };
1594
+ /**
1595
+ * Distills organization name from a package expression.
1596
+ * @example
1597
+ * '@stryker-mutator/core' => { org: '@stryker-mutator', 'core' }
1598
+ * 'glob' => { org: '', 'glob' }
1599
+ */
1600
+ function parsePluginExpression(pluginExpression) {
1601
+ const parts = pluginExpression.split("/");
1602
+ if (parts.length > 1) return {
1603
+ org: parts.slice(0, parts.length - 1).join("/"),
1604
+ pkg: parts[parts.length - 1] ?? ""
1605
+ };
1606
+ else return {
1607
+ org: "",
1608
+ pkg: parts[0] ?? ""
1609
+ };
1610
+ }
1611
+ function isPluginModule(module) {
1612
+ return Schema.is(PluginModuleSchema)(module);
1613
+ }
1614
+ function hasValidationSchemaContribution(module) {
1615
+ return Schema.is(SchemaValidationContributionSchema)(module);
1616
+ }
1617
+ //#endregion
1618
+ export { testRunnerPool as $, mutantTestPlanner as A, reporter as B, injection_tokens_exports as C, loggingServer as D, loggerShowColors as E, pluginModulePaths as F, runEventSink as G, reporterPluginModules as H, pluginsByKind as I, sandbox as J, runId as K, process$1 as L, mutationTestReportHelper as M, optionsValidator as N, loggingServerAddress as O, pluginCreator as P, testRunnerFactory as Q, progressEnabled as R, incrementalDiffer as S, loggerConsoleOut as T, requireFromCwd as U, reporterOverride as V, resolvedMode as W, testCoverage as X, temporaryDirectory as Y, testRunnerConcurrencyTokens as Z, clearTextEnabled as _, isWarningEnabled as a, dryRunResult as b, ProjectReader as c, ConfigReader as d, timeOverheadMS as et, FileSystem as f, checkerPool as g, checkerFactory as h, optionsPath as i, workerIdGenerator as it, mutants as j, loggingSink as k, MetaSchemaBuilder as l, checkerConcurrencyTokens as m, OptionsValidator as n, unexpectedExitRegistry as nt, CommandTestRunner as o, PluginCreator as p, runStartedAt as q, defaultOptions as r, validationSchema as rt, kill as s, PluginLoader as t, timer as tt, FileMatcher as u, concurrencyTokenProvider as v, loggerActiveLevel as w, execa as x, disableTypeChecksHelper as y, project as z };