@t09tanaka/test-gates 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/jest.js ADDED
@@ -0,0 +1,39 @@
1
+ import {
2
+ FULL_COVERAGE,
3
+ resolveRootDir
4
+ } from "./chunk-4HAJIKDZ.js";
5
+ import {
6
+ NO_SPEC_PATTERN,
7
+ gateSpecs,
8
+ loadGates
9
+ } from "./chunk-XEKQKLKY.js";
10
+
11
+ // src/jest.ts
12
+ import path from "path";
13
+ function createJestGatesConfig(options = {}) {
14
+ const { rootDir: rootDirOption, ...overrides } = options;
15
+ const rootDir = resolveRootDir(rootDirOption);
16
+ const manifest = loadGates(rootDir);
17
+ const specs = gateSpecs(manifest);
18
+ return {
19
+ testEnvironment: "node",
20
+ coverageReporters: ["text"],
21
+ coverageDirectory: "<rootDir>/coverage/gates",
22
+ ...overrides,
23
+ rootDir,
24
+ // Never an empty array: Jest would fall back to its default pattern and run every test.
25
+ testMatch: (specs.length > 0 ? specs : [NO_SPEC_PATTERN]).map((spec) => `<rootDir>/${spec}`),
26
+ // With no spec left (`test-gates selfcheck` on a single-gate project) the run must fail on the
27
+ // missing coverage of the gate, not on "No tests found".
28
+ passWithNoTests: true,
29
+ collectCoverage: true,
30
+ coverageProvider: "babel",
31
+ collectCoverageFrom: manifest.gates.map((gate) => `<rootDir>/${gate.path}`),
32
+ coverageThreshold: Object.fromEntries(
33
+ manifest.gates.map((gate) => [path.join(rootDir, gate.path), { ...FULL_COVERAGE }])
34
+ )
35
+ };
36
+ }
37
+ export {
38
+ createJestGatesConfig
39
+ };
@@ -0,0 +1,634 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/stryker.ts
31
+ var stryker_exports = {};
32
+ __export(stryker_exports, {
33
+ createStrykerGatesConfig: () => createStrykerGatesConfig
34
+ });
35
+ module.exports = __toCommonJS(stryker_exports);
36
+
37
+ // src/load.ts
38
+ var import_node_fs2 = __toESM(require("fs"), 1);
39
+ var import_node_path2 = __toESM(require("path"), 1);
40
+
41
+ // src/core/manifest.ts
42
+ var MANIFEST_FILE = "test-gates.json";
43
+ var ManifestError = class extends Error {
44
+ };
45
+ function isRecord(value) {
46
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47
+ }
48
+ function isFilled(value) {
49
+ return typeof value === "string" && value.trim() !== "";
50
+ }
51
+ function allowanceProblems(entry) {
52
+ if (!isRecord(entry)) {
53
+ return [" must be an object"];
54
+ }
55
+ const problems = [];
56
+ if (!isFilled(entry.mutator)) {
57
+ problems.push(': "mutator" is missing');
58
+ }
59
+ if (!isFilled(entry.original)) {
60
+ problems.push(': "original" is missing');
61
+ }
62
+ if (typeof entry.replacement !== "string") {
63
+ problems.push(': "replacement" is missing');
64
+ }
65
+ if (!isFilled(entry.reason)) {
66
+ problems.push(': "reason" is missing (say why the mutant cannot be observed)');
67
+ }
68
+ if (entry.occurrence !== void 0 && !(Number.isInteger(entry.occurrence) && entry.occurrence >= 1)) {
69
+ problems.push(': "occurrence" must be an integer >= 1');
70
+ }
71
+ return problems;
72
+ }
73
+ function validateEquivalentMutants(gatePath, value, field = "equivalentMutants") {
74
+ if (value === void 0) {
75
+ return [];
76
+ }
77
+ if (!Array.isArray(value)) {
78
+ return [{ file: MANIFEST_FILE, message: `${gatePath}: ${field} must be an array` }];
79
+ }
80
+ return value.flatMap(
81
+ (entry, index) => allowanceProblems(entry).map((problem) => ({
82
+ file: MANIFEST_FILE,
83
+ message: `${gatePath}: ${field}[${index}]${problem}`
84
+ }))
85
+ );
86
+ }
87
+ function parseEntries(kind, entries, fields, violations) {
88
+ const usable = [];
89
+ const seen = /* @__PURE__ */ new Set();
90
+ entries.forEach((entry, index) => {
91
+ const at = `${kind}[${index}]`;
92
+ if (!isRecord(entry)) {
93
+ violations.push({ file: MANIFEST_FILE, message: `${at} must be an object` });
94
+ return;
95
+ }
96
+ if (!isFilled(entry.path)) {
97
+ violations.push({ file: MANIFEST_FILE, message: `${at}: "path" is missing` });
98
+ return;
99
+ }
100
+ for (const field of fields) {
101
+ if (!isFilled(entry[field])) {
102
+ violations.push({
103
+ file: MANIFEST_FILE,
104
+ message: `${entry.path}: "${field}" is missing in ${kind}`
105
+ });
106
+ }
107
+ }
108
+ if (seen.has(entry.path)) {
109
+ violations.push({ file: MANIFEST_FILE, message: `${entry.path}: listed twice in ${kind}` });
110
+ return;
111
+ }
112
+ seen.add(entry.path);
113
+ usable.push(entry);
114
+ });
115
+ return usable;
116
+ }
117
+ function parseManifest(text) {
118
+ let raw;
119
+ try {
120
+ raw = JSON.parse(text);
121
+ } catch (error) {
122
+ throw new ManifestError(`${MANIFEST_FILE} is not valid JSON: ${error.message}`);
123
+ }
124
+ if (!isRecord(raw)) {
125
+ throw new ManifestError(`${MANIFEST_FILE} must be a JSON object`);
126
+ }
127
+ if (!Array.isArray(raw.gates)) {
128
+ throw new ManifestError(`${MANIFEST_FILE}: "gates" must be an array`);
129
+ }
130
+ if (raw.candidates !== void 0 && !Array.isArray(raw.candidates)) {
131
+ throw new ManifestError(`${MANIFEST_FILE}: "candidates" must be an array`);
132
+ }
133
+ for (const key of Object.keys(raw)) {
134
+ if (!["$schema", "gates", "candidates", "settings"].includes(key)) {
135
+ throw new ManifestError(`${MANIFEST_FILE}: unknown top-level key ${JSON.stringify(key)}`);
136
+ }
137
+ }
138
+ const violations = [];
139
+ if (raw.gates.length === 0) {
140
+ violations.push({
141
+ file: MANIFEST_FILE,
142
+ message: "gates is empty. A project with nothing to gate should not have this file"
143
+ });
144
+ }
145
+ const gates = parseEntries("gates", raw.gates, ["decides", "impact"], violations);
146
+ const candidates = parseEntries(
147
+ "candidates",
148
+ raw.candidates ?? [],
149
+ ["decides", "blocker"],
150
+ violations
151
+ );
152
+ const gatePaths = new Set(gates.map((gate) => gate.path));
153
+ for (const candidate of candidates) {
154
+ if (gatePaths.has(candidate.path)) {
155
+ violations.push({
156
+ file: MANIFEST_FILE,
157
+ message: `${candidate.path}: listed in both gates and candidates`
158
+ });
159
+ }
160
+ }
161
+ for (const gate of gates) {
162
+ violations.push(...validateEquivalentMutants(gate.path, gate.equivalentMutants));
163
+ violations.push(
164
+ ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
165
+ );
166
+ }
167
+ return { gates, candidates, settings: raw.settings, violations };
168
+ }
169
+
170
+ // src/core/settings.ts
171
+ var DEFAULT_REPORT_FILE = "reports/mutation/mutation.json";
172
+ var DEFAULT_GATE_EXTENSIONS = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"];
173
+ var DEFAULT_IMPURE_IMPORTS = [
174
+ { pattern: "^@nestjs/", reason: "NestJS runtime (DI, decorators)" },
175
+ { pattern: "^class-(validator|transformer)$", reason: "decorator-based DTO library" },
176
+ {
177
+ pattern: "^(typeorm|sequelize|mongoose|knex|pg|mysql|mysql2|ioredis|redis)(/|$)",
178
+ reason: "database client"
179
+ },
180
+ { pattern: "^@prisma/adapter-", reason: "database client" },
181
+ { pattern: "^(react|react-dom)(/|$)", reason: "React runtime" },
182
+ { pattern: "^next(/|$)", reason: "Next.js runtime" },
183
+ { pattern: "^(server-only|client-only)$", reason: "Next.js execution environment" },
184
+ { pattern: "^(vue|vue-router|vue-i18n|pinia)(/|$)", reason: "Vue runtime" },
185
+ { pattern: "^nuxt(/|$)", reason: "Nuxt runtime" },
186
+ { pattern: "^#(app|imports)(/|$)", reason: "Nuxt runtime" },
187
+ { pattern: "\\.vue$", reason: "component" },
188
+ {
189
+ pattern: "^(express|express-jwt|fastify|koa|rxjs|passport)(/|$)",
190
+ reason: "HTTP framework runtime"
191
+ },
192
+ { pattern: "^passport-", reason: "HTTP framework runtime" },
193
+ {
194
+ pattern: "^(axios|openapi-fetch|stripe|nodemailer|firebase-admin)(/|$)",
195
+ reason: "HTTP client / external service SDK"
196
+ },
197
+ { pattern: "^@(aws-sdk|sentry|slack|stripe)/", reason: "external service SDK" },
198
+ {
199
+ pattern: "^(node:)?(fs|http|https|http2|net|tls|dgram|dns|child_process|worker_threads)(/|$)",
200
+ reason: "file system / network / process access"
201
+ }
202
+ ];
203
+ var DEFAULT_IMPURE_NAMED_IMPORTS = [
204
+ { module: "@prisma/client", names: ["PrismaClient"], reason: "connects to the database" }
205
+ ];
206
+ var DEFAULT_IMPURE_PATHS = [
207
+ { pattern: "\\.(vue|jsx|tsx)$", reason: "component file" },
208
+ { pattern: "\\.d\\.[cm]?ts$", reason: "type declaration file" }
209
+ ];
210
+ var DEFAULT_FORBIDDEN_SOURCE = [
211
+ {
212
+ pattern: `^\\s*(['"])use (client|server)\\1`,
213
+ reason: "'use client' / 'use server' directive (not a pure module)"
214
+ }
215
+ ];
216
+ var SettingsError = class extends Error {
217
+ };
218
+ function isRecord2(value) {
219
+ return typeof value === "object" && value !== null && !Array.isArray(value);
220
+ }
221
+ function compile(pattern, flags, where) {
222
+ try {
223
+ return new RegExp(pattern, flags);
224
+ } catch (error) {
225
+ throw new SettingsError(
226
+ `${where}: invalid regular expression ${JSON.stringify(pattern)} (${error.message})`
227
+ );
228
+ }
229
+ }
230
+ function stringArray(value, where) {
231
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item === "")) {
232
+ throw new SettingsError(`${where}: must be an array of non-empty strings`);
233
+ }
234
+ return value;
235
+ }
236
+ function optionalString(value, where) {
237
+ if (value === void 0) {
238
+ return null;
239
+ }
240
+ if (typeof value !== "string" || value === "") {
241
+ throw new SettingsError(`${where}: must be a non-empty string`);
242
+ }
243
+ return value;
244
+ }
245
+ function rejectUnknownKeys(value, known, where) {
246
+ for (const key of Object.keys(value)) {
247
+ if (!known.includes(key)) {
248
+ throw new SettingsError(`${where}: unknown key ${JSON.stringify(key)}`);
249
+ }
250
+ }
251
+ }
252
+ function section(value, known, where) {
253
+ if (value === void 0) {
254
+ return {};
255
+ }
256
+ if (!isRecord2(value)) {
257
+ throw new SettingsError(`${where}: must be an object`);
258
+ }
259
+ rejectUnknownKeys(value, known, where);
260
+ return value;
261
+ }
262
+ function compileRules(defaults, raw, where, extraKeys = []) {
263
+ const config = section(raw, ["defaults", "add", ...extraKeys], where);
264
+ if (config.defaults !== void 0 && typeof config.defaults !== "boolean") {
265
+ throw new SettingsError(`${where}.defaults: must be a boolean`);
266
+ }
267
+ const added = config.add ?? [];
268
+ if (!Array.isArray(added)) {
269
+ throw new SettingsError(`${where}.add: must be an array`);
270
+ }
271
+ const all = [];
272
+ if (config.defaults !== false) {
273
+ defaults.forEach((rule) => all.push({ rule, where: `${where} (default)` }));
274
+ }
275
+ added.forEach((item, index) => {
276
+ const at = `${where}.add[${index}]`;
277
+ if (typeof item === "string" && item !== "") {
278
+ all.push({ rule: { pattern: item }, where: at });
279
+ return;
280
+ }
281
+ if (!isRecord2(item) || typeof item.pattern !== "string" || item.pattern === "") {
282
+ throw new SettingsError(`${at}: must be a pattern string or { pattern, flags?, reason? }`);
283
+ }
284
+ rejectUnknownKeys(item, ["pattern", "flags", "reason"], at);
285
+ if (item.flags !== void 0 && typeof item.flags !== "string") {
286
+ throw new SettingsError(`${at}.flags: must be a string`);
287
+ }
288
+ if (item.reason !== void 0 && typeof item.reason !== "string") {
289
+ throw new SettingsError(`${at}.reason: must be a string`);
290
+ }
291
+ all.push({ rule: item, where: at });
292
+ });
293
+ return {
294
+ rules: all.map(({ rule, where: at }) => ({
295
+ regex: compile(rule.pattern, rule.flags, at),
296
+ reason: rule.reason ?? `matches ${rule.pattern}`
297
+ })),
298
+ section: config
299
+ };
300
+ }
301
+ function resolveSettings(raw) {
302
+ const settings = section(
303
+ raw,
304
+ [
305
+ "gateExtensions",
306
+ "spec",
307
+ "impureImports",
308
+ "impureNamedImports",
309
+ "impurePaths",
310
+ "forbiddenSource",
311
+ "importAliases",
312
+ "gateCommand",
313
+ "selfcheck",
314
+ "stryker",
315
+ "lcov",
316
+ "imports",
317
+ "mutation"
318
+ ],
319
+ "settings"
320
+ );
321
+ const gateExtensions = settings.gateExtensions === void 0 ? DEFAULT_GATE_EXTENSIONS : stringArray(settings.gateExtensions, "settings.gateExtensions");
322
+ if (gateExtensions.length === 0 || gateExtensions.some((ext) => !ext.startsWith("."))) {
323
+ throw new SettingsError(
324
+ 'settings.gateExtensions: must list at least one extension starting with "."'
325
+ );
326
+ }
327
+ const spec = section(settings.spec, ["suffixes", "rewrite"], "settings.spec");
328
+ const specSuffixes = spec.suffixes === void 0 ? null : stringArray(spec.suffixes, "settings.spec.suffixes");
329
+ const rewriteRaw = spec.rewrite ?? [];
330
+ if (!Array.isArray(rewriteRaw)) {
331
+ throw new SettingsError("settings.spec.rewrite: must be an array");
332
+ }
333
+ const specRewrite = rewriteRaw.map((item, index) => {
334
+ const at = `settings.spec.rewrite[${index}]`;
335
+ if (!isRecord2(item) || typeof item.from !== "string" || typeof item.to !== "string") {
336
+ throw new SettingsError(`${at}: must be { from, to }`);
337
+ }
338
+ rejectUnknownKeys(item, ["from", "to"], at);
339
+ return { regex: compile(item.from, void 0, at), to: item.to };
340
+ });
341
+ const imports = compileRules(
342
+ DEFAULT_IMPURE_IMPORTS,
343
+ settings.impureImports,
344
+ "settings.impureImports",
345
+ ["allow"]
346
+ );
347
+ const allowedImports = imports.section.allow === void 0 ? [] : stringArray(imports.section.allow, "settings.impureImports.allow").map(
348
+ (pattern, index) => compile(pattern, void 0, `settings.impureImports.allow[${index}]`)
349
+ );
350
+ const named = section(
351
+ settings.impureNamedImports,
352
+ ["defaults", "add"],
353
+ "settings.impureNamedImports"
354
+ );
355
+ if (named.defaults !== void 0 && typeof named.defaults !== "boolean") {
356
+ throw new SettingsError("settings.impureNamedImports.defaults: must be a boolean");
357
+ }
358
+ const namedAdd = named.add ?? [];
359
+ if (!Array.isArray(namedAdd)) {
360
+ throw new SettingsError("settings.impureNamedImports.add: must be an array");
361
+ }
362
+ const impureNamedImports = [
363
+ ...named.defaults === false ? [] : DEFAULT_IMPURE_NAMED_IMPORTS,
364
+ ...namedAdd.map((item, index) => {
365
+ const at = `settings.impureNamedImports.add[${index}]`;
366
+ if (!isRecord2(item) || typeof item.module !== "string" || item.module === "") {
367
+ throw new SettingsError(`${at}: must be { module, names, reason? }`);
368
+ }
369
+ rejectUnknownKeys(item, ["module", "names", "reason"], at);
370
+ const names = stringArray(item.names, `${at}.names`);
371
+ if (names.length === 0) {
372
+ throw new SettingsError(`${at}.names: must list at least one name`);
373
+ }
374
+ if (item.reason !== void 0 && typeof item.reason !== "string") {
375
+ throw new SettingsError(`${at}.reason: must be a string`);
376
+ }
377
+ return {
378
+ module: item.module,
379
+ names,
380
+ reason: item.reason ?? "not allowed in a pure module"
381
+ };
382
+ })
383
+ ];
384
+ const aliasesRaw = settings.importAliases ?? [];
385
+ if (!Array.isArray(aliasesRaw)) {
386
+ throw new SettingsError("settings.importAliases: must be an array");
387
+ }
388
+ const importAliases = aliasesRaw.map((item, index) => {
389
+ const at = `settings.importAliases[${index}]`;
390
+ if (!isRecord2(item) || typeof item.prefix !== "string" || item.prefix === "" || typeof item.target !== "string") {
391
+ throw new SettingsError(`${at}: must be { prefix, target }`);
392
+ }
393
+ rejectUnknownKeys(item, ["prefix", "target"], at);
394
+ return { prefix: item.prefix, target: item.target };
395
+ });
396
+ let gateCommand = null;
397
+ if (settings.gateCommand !== void 0) {
398
+ gateCommand = stringArray(settings.gateCommand, "settings.gateCommand");
399
+ if (gateCommand.length === 0) {
400
+ throw new SettingsError("settings.gateCommand: must not be empty");
401
+ }
402
+ }
403
+ const selfcheck = section(settings.selfcheck, ["mode", "failurePattern"], "settings.selfcheck");
404
+ if (selfcheck.mode !== void 0 && selfcheck.mode !== "all" && selfcheck.mode !== "first") {
405
+ throw new SettingsError('settings.selfcheck.mode: must be "all" or "first"');
406
+ }
407
+ const selfcheckFailurePattern = optionalString(
408
+ selfcheck.failurePattern,
409
+ "settings.selfcheck.failurePattern"
410
+ );
411
+ if (selfcheckFailurePattern !== null) {
412
+ compile(
413
+ selfcheckFailurePattern.split("{gate}").join("x"),
414
+ void 0,
415
+ "settings.selfcheck.failurePattern"
416
+ );
417
+ }
418
+ const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
419
+ if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
420
+ throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
421
+ }
422
+ const importAllowRaw = importsSection.allow ?? [];
423
+ if (!Array.isArray(importAllowRaw)) {
424
+ throw new SettingsError("settings.imports.allow: must be an array");
425
+ }
426
+ const importAllow = importAllowRaw.map((item, index) => {
427
+ const at = `settings.imports.allow[${index}]`;
428
+ if (typeof item === "string" && item !== "") {
429
+ return { module: item };
430
+ }
431
+ if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
432
+ rejectUnknownKeys(item, ["pattern", "flags"], at);
433
+ if (item.flags !== void 0 && typeof item.flags !== "string") {
434
+ throw new SettingsError(`${at}.flags: must be a string`);
435
+ }
436
+ return { regex: compile(item.pattern, item.flags, at) };
437
+ }
438
+ if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
439
+ rejectUnknownKeys(item, ["module", "names"], at);
440
+ const names = stringArray(item.names, `${at}.names`);
441
+ if (names.length === 0) {
442
+ throw new SettingsError(`${at}.names: must list at least one name`);
443
+ }
444
+ return { module: item.module, names };
445
+ }
446
+ throw new SettingsError(
447
+ `${at}: must be a package name, { pattern, flags? } or { module, names }`
448
+ );
449
+ });
450
+ const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
451
+ if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
452
+ throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
453
+ }
454
+ const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
455
+ const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
456
+ return {
457
+ gateExtensions,
458
+ specSuffixes,
459
+ specRewrite,
460
+ impureImports: imports.rules,
461
+ allowedImports,
462
+ impureNamedImports,
463
+ impurePaths: compileRules(DEFAULT_IMPURE_PATHS, settings.impurePaths, "settings.impurePaths").rules,
464
+ forbiddenSource: compileRules(
465
+ DEFAULT_FORBIDDEN_SOURCE,
466
+ settings.forbiddenSource,
467
+ "settings.forbiddenSource"
468
+ ).rules,
469
+ importAliases,
470
+ gateCommand,
471
+ selfcheckMode: selfcheck.mode ?? "all",
472
+ selfcheckFailurePattern,
473
+ strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
474
+ strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
475
+ importMode: importsSection.mode ?? "blocklist",
476
+ importAllow,
477
+ maxTimeouts: mutation.maxTimeouts ?? null,
478
+ lcovFile: optionalString(lcov.file, "settings.lcov.file"),
479
+ lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
480
+ (pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
481
+ )
482
+ };
483
+ }
484
+
485
+ // src/core/spec-path.ts
486
+ function gateExtensionOf(gatePath, gateExtensions) {
487
+ const matching = gateExtensions.filter((extension) => gatePath.endsWith(extension)).sort((a, b) => b.length - a.length);
488
+ return matching[0] ?? null;
489
+ }
490
+ function suffixesFor(extension, settings) {
491
+ return settings.specSuffixes ?? [`.spec${extension}`, `.test${extension}`];
492
+ }
493
+ function specCandidatesFor(gatePath, settings) {
494
+ const extension = gateExtensionOf(gatePath, settings.gateExtensions);
495
+ if (extension === null) {
496
+ return [];
497
+ }
498
+ let base = gatePath.slice(0, -extension.length);
499
+ for (const { regex, to } of settings.specRewrite) {
500
+ base = base.replace(regex, to);
501
+ }
502
+ return suffixesFor(extension, settings).map((suffix) => base + suffix);
503
+ }
504
+
505
+ // src/fs.ts
506
+ var import_node_fs = __toESM(require("fs"), 1);
507
+ var import_node_path = __toESM(require("path"), 1);
508
+ function existsExact(rootDir, relativePath, kind = "file") {
509
+ let current = rootDir;
510
+ for (const segment of relativePath.split("/")) {
511
+ if (segment === "" || segment === ".") {
512
+ continue;
513
+ }
514
+ let entries;
515
+ try {
516
+ entries = import_node_fs.default.readdirSync(current);
517
+ } catch {
518
+ return false;
519
+ }
520
+ if (!entries.includes(segment)) {
521
+ return false;
522
+ }
523
+ current = import_node_path.default.join(current, segment);
524
+ }
525
+ try {
526
+ return kind === "any" || import_node_fs.default.statSync(current).isFile();
527
+ } catch {
528
+ return false;
529
+ }
530
+ }
531
+
532
+ // src/load.ts
533
+ function readManifest(dir) {
534
+ const absoluteDir = import_node_path2.default.resolve(dir);
535
+ const manifestPath = import_node_path2.default.join(absoluteDir, MANIFEST_FILE);
536
+ let text;
537
+ try {
538
+ text = import_node_fs2.default.readFileSync(manifestPath, "utf8");
539
+ } catch (error) {
540
+ if (error.code === "ENOENT") {
541
+ throw new ManifestError(`${MANIFEST_FILE} not found in ${absoluteDir}`);
542
+ }
543
+ throw new ManifestError(`cannot read ${manifestPath}: ${error.message}`);
544
+ }
545
+ const parsed = parseManifest(text);
546
+ let settings;
547
+ try {
548
+ settings = resolveSettings(parsed.settings);
549
+ } catch (error) {
550
+ if (error instanceof SettingsError) {
551
+ throw new ManifestError(`${MANIFEST_FILE}: ${error.message}`);
552
+ }
553
+ throw error;
554
+ }
555
+ const gates = parsed.gates.map((gate) => {
556
+ const specCandidates = specCandidatesFor(gate.path, settings);
557
+ const existingSpecs = specCandidates.filter((candidate) => existsExact(absoluteDir, candidate));
558
+ return {
559
+ ...gate,
560
+ specCandidates,
561
+ existingSpecs,
562
+ spec: existingSpecs[0] ?? specCandidates[0] ?? null
563
+ };
564
+ });
565
+ return {
566
+ dir: absoluteDir,
567
+ gates,
568
+ candidates: parsed.candidates,
569
+ settings,
570
+ violations: parsed.violations
571
+ };
572
+ }
573
+ function loadGates(dir = process.cwd()) {
574
+ const manifest = readManifest(dir);
575
+ if (manifest.violations.length > 0) {
576
+ throw new ManifestError(
577
+ `${MANIFEST_FILE} has problems (run "test-gates check"):
578
+ ` + manifest.violations.map((violation) => ` - ${violation.message}`).join("\n")
579
+ );
580
+ }
581
+ return manifest;
582
+ }
583
+
584
+ // src/shared.ts
585
+ var import_node_path3 = __toESM(require("path"), 1);
586
+ var import_node_url = require("url");
587
+ function resolveRootDir(rootDir) {
588
+ if (rootDir === void 0) {
589
+ return process.cwd();
590
+ }
591
+ if (rootDir.startsWith("file:")) {
592
+ return import_node_path3.default.dirname((0, import_node_url.fileURLToPath)(rootDir));
593
+ }
594
+ return import_node_path3.default.resolve(rootDir);
595
+ }
596
+
597
+ // src/stryker.ts
598
+ var WEAKENING_OPTIONS = ["ignoreStatic", "ignorers"];
599
+ function createStrykerGatesConfig(options = {}) {
600
+ const { rootDir: rootDirOption, ...overrides } = options;
601
+ for (const option of WEAKENING_OPTIONS) {
602
+ if (overrides[option] !== void 0) {
603
+ throw new Error(
604
+ `test-gates: "${option}" takes mutants out of the evaluation. Allow equivalent mutants one by one in test-gates.json (equivalentMutants) instead`
605
+ );
606
+ }
607
+ }
608
+ const mutator = overrides.mutator;
609
+ if (Array.isArray(mutator?.excludedMutations) && mutator.excludedMutations.length > 0) {
610
+ throw new Error(
611
+ 'test-gates: "mutator.excludedMutations" takes mutants out of the evaluation. Allow equivalent mutants one by one in test-gates.json (equivalentMutants) instead'
612
+ );
613
+ }
614
+ const manifest = loadGates(resolveRootDir(rootDirOption));
615
+ const reporters = Array.isArray(overrides.reporters) ? overrides.reporters : ["clear-text", "progress"];
616
+ const thresholds = typeof overrides.thresholds === "object" && overrides.thresholds !== null ? overrides.thresholds : { high: 100, low: 100 };
617
+ const jsonReporter = typeof overrides.jsonReporter === "object" && overrides.jsonReporter !== null ? overrides.jsonReporter : {};
618
+ return {
619
+ coverageAnalysis: "perTest",
620
+ incremental: true,
621
+ incrementalFile: "reports/stryker-incremental.json",
622
+ tempDirName: ".stryker-tmp",
623
+ clearTextReporter: { logTests: false, reportTests: false },
624
+ ...overrides,
625
+ mutate: manifest.gates.map((gate) => gate.path),
626
+ thresholds: { ...thresholds, break: null },
627
+ reporters: reporters.includes("json") ? reporters : [...reporters, "json"],
628
+ jsonReporter: { ...jsonReporter, fileName: manifest.settings.strykerReportFile }
629
+ };
630
+ }
631
+ // Annotate the CommonJS export names for ESM import in node:
632
+ 0 && (module.exports = {
633
+ createStrykerGatesConfig
634
+ });
@@ -0,0 +1,16 @@
1
+ interface StrykerGatesOptions {
2
+ /** Directory of test-gates.json: `import.meta.url` of the config file, or a directory path. */
3
+ rootDir?: string;
4
+ /** Any other Stryker option: testRunner, jest / vitest, concurrency, ignorePatterns, … */
5
+ [option: string]: unknown;
6
+ }
7
+ /**
8
+ * Stryker config for the gates: mutate exactly the gates, never fail on the score
9
+ * (`thresholds.break: null`), and write the JSON report that `test-gates mutation` judges.
10
+ *
11
+ * `mutate`, `thresholds.break` and `jsonReporter.fileName` are always set here, and `json` is
12
+ * always among the reporters. Options that take mutants out of the evaluation are rejected.
13
+ */
14
+ declare function createStrykerGatesConfig(options?: StrykerGatesOptions): Record<string, unknown>;
15
+
16
+ export { type StrykerGatesOptions, createStrykerGatesConfig };