@systemfsoftware/stryker-js-typescript-checker 5.0.4 → 6.0.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/index.mjs CHANGED
@@ -1,22 +1,4 @@
1
- import { Checker, CheckerFailed, Mutant, RunConfiguration, StrykerOptionsSchema, declarePlugin, errorToString, normalizeFileName } from "@systemfsoftware/stryker-js";
2
- import * as Effect from "effect/Effect";
3
- import * as FileSystem from "effect/FileSystem";
4
- import * as Layer from "effect/Layer";
5
- import * as Path from "effect/Path";
6
1
  import * as S from "effect/Schema";
7
- import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
8
- import { Predicate, Result, Schema } from "effect";
9
- import * as HashMap from "effect/HashMap";
10
- import * as Match from "effect/Match";
11
- import * as Option from "effect/Option";
12
- import { API, DiagnosticCategory } from "typescript/unstable/sync";
13
- import * as Arr from "effect/Array";
14
- import * as Result$1 from "effect/Result";
15
- import * as Context from "effect/Context";
16
- import * as MutableHashMap from "effect/MutableHashMap";
17
- import * as MutableHashSet from "effect/MutableHashSet";
18
- import * as Ref from "effect/Ref";
19
- import { SyntaxKind } from "typescript/unstable/ast";
20
2
  //#region schema/typescript-checker-options.json
21
3
  var typescript_checker_options_default = {
22
4
  $schema: "http://json-schema.org/draft-07/schema",
@@ -37,1245 +19,12 @@ var typescript_checker_options_default = {
37
19
  } }
38
20
  };
39
21
  //#endregion
40
- //#region src/CheckMutants.schema.ts
41
- /** A file name the node map can be keyed by: non-empty, and naming an extension. */
42
- const SourceFileSchema = S.NonEmptyString.pipe(S.check(S.isPattern(/\.[^./\\]+$/)));
43
- const DiagnosticSchema = S.Struct({
44
- fileName: S.optional(SourceFileSchema),
45
- text: S.String
46
- });
47
- const TSFileNodeSchema = S.suspend(() => S.Struct({
48
- fileName: SourceFileSchema,
49
- parents: S.Array(TSFileNodeSchema),
50
- children: S.Array(TSFileNodeSchema)
51
- }));
52
- var CheckMutantsInput = class extends S.TaggedClass()("CheckMutantsInput", {
53
- mutants: S.Array(Mutant),
54
- diagnostics: S.Array(DiagnosticSchema),
55
- nodes: S.Record(SourceFileSchema, TSFileNodeSchema)
56
- }) {};
57
- //#endregion
58
- //#region src/check-mutants.workflow.ts
59
- var DiagnosticWithoutFileError = class extends S.TaggedError()("DiagnosticWithoutFileError", { text: S.String }) {};
60
- var DiagnosticInUnrelatedFileError = class extends S.TaggedError()("DiagnosticInUnrelatedFileError", {
61
- text: S.String,
62
- fileName: S.String
63
- }) {};
64
- const CheckMutantsTypeId = Symbol.for("@systemfsoftware/stryker-js-typescript-checker/CheckMutants");
65
- const MutantCheckStatusSchema = S.Union([S.Struct({ status: S.Literal("passed") }), S.Struct({
66
- status: S.Literal("compileError"),
67
- reason: S.String
68
- })]);
69
- var CheckFinished = class extends S.TaggedClass()("CheckFinished", { results: S.Record(S.String, MutantCheckStatusSchema) }) {
70
- [CheckMutantsTypeId] = CheckMutantsTypeId;
71
- };
72
- var RetestRequired = class extends S.TaggedClass()("RetestRequired", {
73
- results: S.Record(S.String, MutantCheckStatusSchema),
74
- needsRetest: S.Array(Mutant)
75
- }) {
76
- [CheckMutantsTypeId] = CheckMutantsTypeId;
77
- };
78
- const normalizeFileName$3 = (fileName) => fileName.replace(/\\/g, "/");
79
- const nodeAt = (fileName, nodes) => Option.filter(Option.fromUndefinedOr(nodes[fileName]), () => Object.hasOwn(nodes, fileName));
80
- const nodeOf = (diagnostic, nodes) => Option.match(Option.filter(Option.fromUndefinedOr(diagnostic.fileName), (fileName) => fileName !== ""), {
81
- onNone: () => Result$1.fail(new DiagnosticWithoutFileError({ text: diagnostic.text })),
82
- onSome: (fileName) => Option.match(nodeAt(fileName, nodes), {
83
- onNone: () => Result$1.fail(new DiagnosticInUnrelatedFileError({
84
- text: diagnostic.text,
85
- fileName
86
- })),
87
- onSome: (node) => Result$1.succeed(node)
88
- })
89
- });
90
- const walk = (node, mutants, visited) => Option.match(Option.filter(Option.some(node), (current) => !visited.includes(current.fileName)), {
91
- onNone: () => [],
92
- onSome: (current) => [...mutants.filter((mutant) => normalizeFileName$3(mutant.fileName) === current.fileName), ...current.children.flatMap((child) => walk(child, mutants, [...visited, current.fileName]))]
93
- });
94
- const emptyAccumulator = () => ({
95
- definitive: HashMap.empty(),
96
- needsRetest: HashMap.empty()
97
- });
98
- const addAll = (into, mutants) => mutants.reduce((accumulated, mutant) => HashMap.set(accumulated, mutant.id, mutant), into);
99
- const appendDiagnostic = (into, mutantId, diagnostic) => HashMap.set(into, mutantId, [...Option.getOrElse(HashMap.get(into, mutantId), () => []), diagnostic]);
100
- const classifyOne = (state, diagnostic, mutants, nodes) => Result$1.flatMap(nodeOf(diagnostic, nodes), (node) => {
101
- const related = walk(node, mutants, []);
102
- return Result$1.succeed(Option.match(Option.filter(Arr.head(related), () => related.length === 1), {
103
- onSome: (only) => ({
104
- definitive: appendDiagnostic(state.definitive, only.id, diagnostic),
105
- needsRetest: state.needsRetest
106
- }),
107
- onNone: () => Option.match(Arr.head(related), {
108
- onNone: () => ({
109
- definitive: state.definitive,
110
- needsRetest: addAll(state.needsRetest, mutants)
111
- }),
112
- onSome: () => ({
113
- definitive: state.definitive,
114
- needsRetest: addAll(state.needsRetest, related)
115
- })
116
- })
117
- }));
118
- });
119
- const classifyDiagnostics = (diagnostics, mutants, nodes) => Option.match(Option.filter(Option.filter(Arr.head(mutants), () => mutants.length === 1), () => diagnostics.length > 0), {
120
- onSome: (only) => Result$1.succeed({
121
- definitive: HashMap.set(HashMap.empty(), only.id, [...diagnostics]),
122
- needsRetest: []
123
- }),
124
- onNone: () => Result$1.map(diagnostics.reduce((accumulated, diagnostic) => Result$1.flatMap(accumulated, (state) => classifyOne(state, diagnostic, mutants, nodes)), Result$1.succeed(emptyAccumulator())), (state) => ({
125
- definitive: state.definitive,
126
- needsRetest: HashMap.toValues(state.needsRetest).filter((mutant) => !HashMap.has(state.definitive, mutant.id))
127
- }))
128
- });
129
- const passedResults = (mutants) => mutants.map((mutant) => [mutant.id, { status: "passed" }]);
130
- const checkResults = (mutants, classification) => {
131
- const retestIds = classification.needsRetest.reduce((accumulated, mutant) => HashMap.set(accumulated, mutant.id, true), HashMap.empty());
132
- return mutants.flatMap((mutant) => Option.match(HashMap.get(classification.definitive, mutant.id), {
133
- onSome: (diagnostics) => [[mutant.id, {
134
- status: "compileError",
135
- reason: diagnostics.map((entry) => entry.text).join("\n")
136
- }]],
137
- onNone: () => Option.match(Option.filter(Option.some({ status: "passed" }), () => !HashMap.has(retestIds, mutant.id)), {
138
- onSome: (status) => [[mutant.id, status]],
139
- onNone: () => []
140
- })
141
- }));
142
- };
143
- const verdictOf = (mutants, classification) => {
144
- const results = Object.fromEntries(checkResults(mutants, classification));
145
- return Option.match(Arr.head(classification.needsRetest), {
146
- onNone: () => CheckFinished.make({ results }),
147
- onSome: () => RetestRequired.make({
148
- results,
149
- needsRetest: [...classification.needsRetest]
150
- })
151
- });
152
- };
153
- const classify = (input) => Result$1.map(classifyDiagnostics(input.diagnostics, input.mutants, input.nodes), (classification) => verdictOf(input.mutants, classification));
154
- const withoutDisambiguation = (input) => Option.match(Arr.head(input.mutants), {
155
- onNone: () => Option.some(CheckFinished.make({ results: {} })),
156
- onSome: (first) => Option.map(Option.filter(Option.some(first), () => !Object.hasOwn(input.nodes, normalizeFileName$3(first.fileName))), () => CheckFinished.make({ results: Object.fromEntries(passedResults(input.mutants)) }))
157
- });
158
- const verdict = (input) => Option.match(withoutDisambiguation(input), {
159
- onNone: () => classify(input),
160
- onSome: (decision) => Result$1.succeed(decision)
161
- });
162
- const checkMutants = Workflow.make(CheckMutantsInput, verdict);
163
- //#endregion
164
- //#region src/Checker.schema.ts
165
- /**
166
- * Checker — declarations for the TypeScript checker.
167
- *
168
- * Houses the wire types and error variants shared by the capability and its
169
- * workflow. Decoded at the checker boundary; no I/O.
170
- */
171
- const TypescriptCheckerOptionsSchema = S.Struct({ typescriptChecker: S.optional(S.Struct({ prioritizePerformanceOverAccuracy: S.optional(S.Boolean) })) });
172
- var CheckMutantsCommand = class extends S.TaggedClass()("CheckMutantsCommand", { mutants: S.Array(Mutant) }) {};
173
- /**
174
- * Every way the TypeScript compiler can fail while serving a check.
175
- * One tagged error — callers branch only on failure itself; `reason` keeps
176
- * cases distinguishable in reports.
177
- */
178
- var CompilerFailed = class extends S.TaggedError()("CompilerFailed", {
179
- reason: S.Literals([
180
- "not-initialized",
181
- "no-projects",
182
- "unknown-file-node",
183
- "file-not-in-project"
184
- ]),
185
- subject: S.optional(S.String)
186
- }) {
187
- get message() {
188
- return Match.value(this.reason).pipe(Match.when("not-initialized", () => "The TypeScript compiler was used before it was initialized"), Match.when("no-projects", () => `No projects were found for ${this.subject ?? "the tsconfig"}`), Match.when("unknown-file-node", () => `The file graph has no node for '${this.subject ?? "a file"}', which should not happen`), Match.when("file-not-in-project", () => `'${this.subject ?? "a file"}' is part of your TypeScript project but could not be found on disk`), Match.exhaustive);
189
- }
190
- };
191
- //#endregion
192
- //#region src/Compiler.schema.ts
193
- /**
194
- * Compiler — declarations for the TypeScript compiler and version guard.
195
- */
196
- /** The installed TypeScript version is below the supported floor. */
197
- var UnsupportedTypeScriptVersionError = class extends Schema.TaggedError()("UnsupportedTypeScriptVersionError", { version: Schema.String }) {
198
- get message() {
199
- return `@systemfsoftware/stryker-js-typescript-checker only supports typescript@7.0.0 or higher. Found typescript@${this.version}`;
200
- }
201
- };
202
- /** Requested file is not present in the hybrid in-memory file map. */
203
- var HybridFileNotFoundError = class extends Schema.TaggedError()("HybridFileNotFoundError", { fileName: Schema.String }) {};
204
- //#endregion
205
- //#region ../../../node_modules/.pnpm/@jsr+std__jsonc@1.0.2/node_modules/@jsr/std__jsonc/parse.js
206
- /**
207
- * Converts a JSON with Comments (JSONC) string into an object.
208
- *
209
- * @example Usage
210
- * ```ts
211
- * import { parse } from "@std/jsonc";
212
- * import { assertEquals } from "@std/assert";
213
- *
214
- * assertEquals(parse('{"foo": "bar"}'), { foo: "bar" });
215
- * assertEquals(parse('{"foo": "bar", }'), { foo: "bar" });
216
- * assertEquals(parse('{"foo": "bar", } /* comment *\/'), { foo: "bar" });
217
- * ```
218
- *
219
- * @throws {SyntaxError} If the JSONC string is invalid.
220
- * @param text A valid JSONC string.
221
- * @returns The parsed JsonValue from the JSONC string.
222
- */ function parse(text) {
223
- if (new.target) throw new TypeError("Cannot create an instance: parse is not a constructor");
224
- return new JsoncParser(text).parse();
225
- }
226
- var JsoncParser = class {
227
- #whitespace = /* @__PURE__ */ new Set(" \r\n");
228
- #numberEndToken = /* @__PURE__ */ new Set([..."[]{}:,/", ...this.#whitespace]);
229
- #text;
230
- #length;
231
- #tokenized;
232
- constructor(text) {
233
- this.#text = `${text}`;
234
- this.#length = this.#text.length;
235
- this.#tokenized = this.#tokenize();
236
- }
237
- parse() {
238
- const token = this.#getNext();
239
- const res = this.#parseJsonValue(token);
240
- const { done, value } = this.#tokenized.next();
241
- if (!done) throw new SyntaxError(buildErrorMessage(value));
242
- return res;
243
- }
244
- /** Read the next token. If the token is read to the end, it throws a SyntaxError. */ #getNext() {
245
- const { done, value } = this.#tokenized.next();
246
- if (done) throw new SyntaxError("Cannot parse JSONC: unexpected end of JSONC input");
247
- return value;
248
- }
249
- /** Split the JSONC string into token units. Whitespace and comments are skipped. */ *#tokenize() {
250
- for (let i = 0; i < this.#length; i++) {
251
- if (this.#whitespace.has(this.#text[i])) continue;
252
- if (this.#text[i] === "/" && this.#text[i + 1] === "*") {
253
- i += 2;
254
- let hasEndOfComment = false;
255
- for (; i < this.#length; i++) if (this.#text[i] === "*" && this.#text[i + 1] === "/") {
256
- hasEndOfComment = true;
257
- break;
258
- }
259
- if (!hasEndOfComment) throw new SyntaxError("Cannot parse JSONC: unexpected end of JSONC input");
260
- i++;
261
- continue;
262
- }
263
- if (this.#text[i] === "/" && this.#text[i + 1] === "/") {
264
- i += 2;
265
- for (; i < this.#length; i++) if (this.#text[i] === "\n" || this.#text[i] === "\r") break;
266
- continue;
267
- }
268
- switch (this.#text[i]) {
269
- case "{":
270
- yield {
271
- type: "BeginObject",
272
- position: i
273
- };
274
- break;
275
- case "}":
276
- yield {
277
- type: "EndObject",
278
- position: i
279
- };
280
- break;
281
- case "[":
282
- yield {
283
- type: "BeginArray",
284
- position: i
285
- };
286
- break;
287
- case "]":
288
- yield {
289
- type: "EndArray",
290
- position: i
291
- };
292
- break;
293
- case ":":
294
- yield {
295
- type: "NameSeparator",
296
- position: i
297
- };
298
- break;
299
- case ",":
300
- yield {
301
- type: "ValueSeparator",
302
- position: i
303
- };
304
- break;
305
- case "\"": {
306
- const startIndex = i;
307
- let shouldEscapeNext = false;
308
- i++;
309
- for (; i < this.#length; i++) {
310
- if (this.#text[i] === "\"" && !shouldEscapeNext) break;
311
- shouldEscapeNext = this.#text[i] === "\\" && !shouldEscapeNext;
312
- }
313
- yield {
314
- type: "String",
315
- sourceText: this.#text.substring(startIndex, i + 1),
316
- position: startIndex
317
- };
318
- break;
319
- }
320
- default: {
321
- const startIndex = i;
322
- for (; i < this.#length; i++) if (this.#numberEndToken.has(this.#text[i])) break;
323
- i--;
324
- yield {
325
- type: "NullOrTrueOrFalseOrNumber",
326
- sourceText: this.#text.substring(startIndex, i + 1),
327
- position: startIndex
328
- };
329
- }
330
- }
331
- }
332
- }
333
- #parseJsonValue(value) {
334
- switch (value.type) {
335
- case "BeginObject": return this.#parseObject();
336
- case "BeginArray": return this.#parseArray();
337
- case "NullOrTrueOrFalseOrNumber": return this.#parseNullOrTrueOrFalseOrNumber(value);
338
- case "String": return this.#parseString(value);
339
- default: throw new SyntaxError(buildErrorMessage(value));
340
- }
341
- }
342
- #parseObject() {
343
- const target = {};
344
- while (true) {
345
- const token1 = this.#getNext();
346
- if (token1.type === "EndObject") return target;
347
- if (token1.type !== "String") throw new SyntaxError(buildErrorMessage(token1));
348
- const key = this.#parseString(token1);
349
- const token2 = this.#getNext();
350
- if (token2.type !== "NameSeparator") throw new SyntaxError(buildErrorMessage(token2));
351
- const token3 = this.#getNext();
352
- Object.defineProperty(target, key, {
353
- value: this.#parseJsonValue(token3),
354
- writable: true,
355
- enumerable: true,
356
- configurable: true
357
- });
358
- const token4 = this.#getNext();
359
- if (token4.type === "EndObject") return target;
360
- if (token4.type !== "ValueSeparator") throw new SyntaxError(buildErrorMessage(token4));
361
- }
362
- }
363
- #parseArray() {
364
- const target = [];
365
- while (true) {
366
- const token1 = this.#getNext();
367
- if (token1.type === "EndArray") return target;
368
- target.push(this.#parseJsonValue(token1));
369
- const token2 = this.#getNext();
370
- if (token2.type === "EndArray") return target;
371
- if (token2.type !== "ValueSeparator") throw new SyntaxError(buildErrorMessage(token2));
372
- }
373
- }
374
- #parseString(value) {
375
- let parsed;
376
- try {
377
- parsed = JSON.parse(value.sourceText);
378
- } catch {
379
- throw new SyntaxError(buildErrorMessage(value));
380
- }
381
- if (typeof parsed !== "string") throw new TypeError(`Parsed value is not a string: ${parsed}`);
382
- return parsed;
383
- }
384
- #parseNullOrTrueOrFalseOrNumber(value) {
385
- if (value.sourceText === "null") return null;
386
- if (value.sourceText === "true") return true;
387
- if (value.sourceText === "false") return false;
388
- let parsed;
389
- try {
390
- parsed = JSON.parse(value.sourceText);
391
- } catch {
392
- throw new SyntaxError(buildErrorMessage(value));
393
- }
394
- if (typeof parsed !== "number") throw new TypeError(`Parsed value is not a number: ${parsed}`);
395
- return parsed;
396
- }
397
- };
398
- function buildErrorMessage({ type, sourceText, position }) {
399
- let token = "";
400
- switch (type) {
401
- case "BeginObject":
402
- token = "{";
403
- break;
404
- case "EndObject":
405
- token = "}";
406
- break;
407
- case "BeginArray":
408
- token = "[";
409
- break;
410
- case "EndArray":
411
- token = "]";
412
- break;
413
- case "NameSeparator":
414
- token = ":";
415
- break;
416
- case "ValueSeparator":
417
- token = ",";
418
- break;
419
- case "NullOrTrueOrFalseOrNumber":
420
- case "String": token = 30 < sourceText.length ? `${sourceText.slice(0, 30)}...` : sourceText;
421
- }
422
- return `Cannot parse JSONC: unexpected token "${token}" in JSONC at position ${position}`;
423
- }
424
- //#endregion
425
- //#region src/Tsconfig.schema.ts
426
- /**
427
- * Tsconfig — declarations for the TypeScript configuration consumed by the checker.
428
- *
429
- * Typed by Effect Schema and decoded at the boundary; the compiler capability
430
- * consumes only validated shapes.
431
- */
432
- /** The configured tsconfig failed to parse or is not a shape this package can consume. */
433
- var TsConfigParseError = class extends Schema.TaggedError()("TsConfigParseError", {
434
- file: Schema.String,
435
- reason: Schema.String
436
- }) {};
437
- /** The configured tsconfig file could not be read. */
438
- var TsConfigNotFoundError = class extends Schema.TaggedError()("TsConfigNotFoundError", { file: Schema.String }) {
439
- get message() {
440
- return `The tsconfig file does not exist at: "${this.file}". Please configure the tsconfig file in your stryker.conf file using "tsconfigFile"`;
441
- }
442
- };
443
- const TsConfigSchema = Schema.Struct({
444
- references: Schema.optional(Schema.Array(Schema.Struct({ path: Schema.String }))),
445
- compilerOptions: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
446
- });
447
- //#endregion
448
- //#region src/Tsconfig.ts
449
- /**
450
- * Tsconfig — capability for reading and normalizing TypeScript project configs.
451
- *
452
- * Normalizes via Effect Schema and tightens compilation options for mutation
453
- * checking (disabling quality checks, toggling emit for build-mode vs
454
- * single-project).
455
- */
456
- const normalizeFileName$2 = (fileName) => fileName.replace(/\\/g, "/");
457
- const COMPILER_OPTIONS_OVERRIDES = Object.freeze({
458
- allowUnreachableCode: true,
459
- noUnusedLocals: false,
460
- noUnusedParameters: false,
461
- skipLibCheck: true
462
- });
463
- const NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT = Object.freeze({
464
- noEmit: true,
465
- incremental: false,
466
- tsBuildInfoFile: void 0,
467
- composite: false
468
- });
469
- const LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES = Object.freeze({
470
- emitDeclarationOnly: true,
471
- noEmit: false,
472
- declarationMap: true,
473
- declaration: true,
474
- composite: true
475
- });
476
- const reasonOfThrown = (error) => Match.value(error).pipe(Match.when(Predicate.isError, (thrown) => thrown.message), Match.when(Predicate.isString, (thrown) => thrown), Match.orElse((thrown) => Option.match(Option.filter(Option.fromUndefinedOr(JSON.stringify(thrown)), (text) => text.length > 0), {
477
- onNone: () => "a non-Error value was thrown",
478
- onSome: (text) => text
479
- })));
480
- /**
481
- * Parses the raw text of a tsconfig file into a typed config, rejecting shapes this package cannot consume.
482
- * @param fileName The tsconfig file name, used for error reporting
483
- * @param jsonText The raw tsconfig content
484
- */
485
- function parseTsConfig(fileName, jsonText) {
486
- try {
487
- const value = parse(jsonText.replace(/^\uFEFF/, ""));
488
- return Result.mapError(Schema.decodeUnknownResult(TsConfigSchema)(value), (error) => new TsConfigParseError({
489
- file: fileName,
490
- reason: error.message
491
- }));
492
- } catch (error) {
493
- return Result.fail(new TsConfigParseError({
494
- file: fileName,
495
- reason: reasonOfThrown(error)
496
- }));
497
- }
498
- }
499
- /** Whether `--build` mode should be enabled based on `references` in the tsconfig. */
500
- const determineBuildModeEnabled = (tsconfigFileName, fsService) => Effect.gen(function* () {
501
- const parsed = parseTsConfig(tsconfigFileName, yield* fsService.readFileString(tsconfigFileName));
502
- return Result.match(parsed, {
503
- onFailure: () => false,
504
- onSuccess: (config) => config.references !== void 0
505
- });
506
- });
507
- const withCompilerOverrides = (config, extraOptions) => ({
508
- ...config.compilerOptions,
509
- ...COMPILER_OPTIONS_OVERRIDES,
510
- ...extraOptions
511
- });
512
- const projectReferencesJson = (config) => {
513
- const compilerOptions = withCompilerOverrides(config, LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES);
514
- delete compilerOptions["inlineSourceMap"];
515
- delete compilerOptions["inlineSources"];
516
- delete compilerOptions["mapRoute"];
517
- delete compilerOptions["sourceRoot"];
518
- delete compilerOptions["outFile"];
519
- return JSON.stringify({
520
- ...config,
521
- compilerOptions
522
- });
523
- };
524
- const singleProjectJson = (config) => {
525
- const compilerOptions = withCompilerOverrides(config, NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT);
526
- if (compilerOptions["declarationDir"] !== null) delete compilerOptions["declarationDir"];
527
- const { references: _references, ...withoutReferences } = config;
528
- return JSON.stringify({
529
- ...withoutReferences,
530
- compilerOptions
531
- });
532
- };
533
- /**
534
- * Overrides compiler options to speed up compilation and disable code quality
535
- * checks irrelevant during mutation testing.
536
- */
537
- function overrideOptions(config, useBuildMode) {
538
- if (useBuildMode) return projectReferencesJson(config);
539
- return singleProjectJson(config);
540
- }
541
- /**
542
- * Retrieves the referenced config files based on parsed configuration.
543
- */
544
- function retrieveReferencedProjects(config, fromDirName, pathService) {
545
- return (config.references ?? []).map((reference) => {
546
- let resolved = pathService.resolve(fromDirName, reference.path);
547
- if (!pathService.basename(resolved).endsWith(".json")) resolved = pathService.join(resolved, "tsconfig.json");
548
- return normalizeFileName$2(resolved);
549
- });
550
- }
551
- //#endregion
552
- //#region src/Compiler.ts
553
- const normalizeFileName$1 = (fileName) => fileName.replace(/\\/g, "/");
554
- const findSourceMapRegex = /\/\/# sourceMappingURL=(.+)$/m;
555
- /** A specifier that resolves relative to the importing file. */
556
- const relativeSpecifierPattern = /^\.\.?\//;
557
- /** A file that never belongs to the dependency graph: declarations and dependencies. */
558
- const ignoredGraphFileNamePattern = /\.d\.ts$|node_modules/;
559
- function getSourceMappingURL(content) {
560
- return findSourceMapRegex.exec(content)?.[1];
561
- }
562
- let cachedTSVersion;
563
- const isString = (value) => typeof value === "string";
564
- const isNonEmptyString = (value) => value !== void 0 && value !== "";
565
- const versionFieldOf = (raw) => {
566
- if (Predicate.hasProperty(raw, "version")) return Option.some(raw.version);
567
- return Option.none();
568
- };
569
- const readTypescriptPackageVersion = (fsService, pathService) => Effect.gen(function* () {
570
- const urlString = import.meta.resolve("typescript/package.json");
571
- const pkgPath = yield* pathService.fromFileUrl(new URL(urlString));
572
- const text = yield* fsService.readFileString(pkgPath);
573
- const raw = JSON.parse(text);
574
- return Option.getOrElse(Option.flatMap(versionFieldOf(raw), (version) => Option.liftPredicate(version, isString)), () => "");
575
- });
576
- const getTSVersion = (fsService, pathService) => Effect.gen(function* () {
577
- if (cachedTSVersion !== void 0) return cachedTSVersion;
578
- const version = yield* readTypescriptPackageVersion(fsService, pathService);
579
- cachedTSVersion = version;
580
- return version;
581
- });
582
- const minimumSupportedTypeScriptVersion = {
583
- major: 7,
584
- minor: 0,
585
- patch: 0
586
- };
587
- const versionComponent = (parts, index) => {
588
- const part = parts[index];
589
- if (part === void 0) return 0;
590
- return Number.parseInt(part, 10);
591
- };
592
- /** Drops any pre-release (`-`) or build (`+`) suffix, keeping the numeric base. */
593
- const parseTypeScriptVersion = (version) => {
594
- const parts = version.replace(/[-+][\s\S]*$/, "").split(".");
595
- return {
596
- major: versionComponent(parts, 0),
597
- minor: versionComponent(parts, 1),
598
- patch: versionComponent(parts, 2)
599
- };
600
- };
601
- const compareVersionNumbers = (left, right) => {
602
- return [
603
- left.major - right.major,
604
- left.minor - right.minor,
605
- left.patch - right.patch
606
- ].find((difference) => difference !== 0) ?? 0;
607
- };
608
- /**
609
- * Whether a TypeScript version satisfies `>=7.0.0`. Pre-release suffixes are
610
- * stripped so `7.0.0-beta` compares as `7.0.0`.
611
- */
612
- function isSupportedTypescriptVersion(version) {
613
- const parsed = parseTypeScriptVersion(version);
614
- if (![
615
- parsed.major,
616
- parsed.minor,
617
- parsed.patch
618
- ].every((part) => !Number.isNaN(part))) return false;
619
- return compareVersionNumbers(parsed, minimumSupportedTypeScriptVersion) >= 0;
620
- }
621
- const guardTSVersion = (fsService, pathService) => Effect.gen(function* () {
622
- const version = yield* getTSVersion(fsService, pathService);
623
- if (!isSupportedTypescriptVersion(version)) return yield* new UnsupportedTypeScriptVersionError({ version });
624
- });
625
- function makeScriptFile(content, fileName, modifiedTime = /* @__PURE__ */ new Date()) {
626
- return {
627
- content,
628
- fileName,
629
- originalContent: content,
630
- modifiedTime
631
- };
632
- }
633
- function withContent(file, content) {
634
- return {
635
- ...file,
636
- content,
637
- modifiedTime: /* @__PURE__ */ new Date()
638
- };
639
- }
640
- function mutateScriptFile(file, mutant) {
641
- const start = getOffset(file, mutant.location.start);
642
- const end = getOffset(file, mutant.location.end);
643
- const content = `${file.originalContent.slice(0, start)}${mutant.replacement}${file.originalContent.slice(end)}`;
644
- return {
645
- ...file,
646
- content,
647
- modifiedTime: /* @__PURE__ */ new Date()
648
- };
649
- }
650
- function resetScriptFile(file) {
651
- return {
652
- ...file,
653
- content: file.originalContent,
654
- modifiedTime: /* @__PURE__ */ new Date()
655
- };
656
- }
657
- function getOffset(file, pos) {
658
- const lines = file.originalContent.split("\n");
659
- const lineCount = Math.min(pos.line, lines.length);
660
- let offset = pos.column;
661
- lines.forEach((line, index) => {
662
- if (index < lineCount) offset += line.length + 1;
663
- });
664
- return offset;
665
- }
666
- const makeEmptyFilesMap = () => MutableHashMap.empty();
667
- const makeEmptyOverridesMap = () => MutableHashMap.empty();
668
- const setInPlace = (map, key, value) => {
669
- MutableHashMap.set(map, key, value);
670
- return map;
671
- };
672
- const makeHybridFileSystem = (fsService) => Effect.gen(function* () {
673
- const filesRef = yield* Ref.make(makeEmptyFilesMap());
674
- const overridesRef = yield* Ref.make(makeEmptyOverridesMap());
675
- const memoryContent = (file) => {
676
- if (file === void 0) return null;
677
- return file.content;
678
- };
679
- const readFromSources = (files, overrides, fileName) => {
680
- const override = MutableHashMap.get(overrides, fileName);
681
- if (Option.isSome(override)) return override.value;
682
- return Option.match(MutableHashMap.get(files, fileName), {
683
- onNone: () => void 0,
684
- onSome: memoryContent
685
- });
686
- };
687
- const existsInSources = (files, overrides, fileName) => {
688
- const override = MutableHashMap.get(overrides, fileName);
689
- if (Option.isSome(override)) return true;
690
- return Option.match(MutableHashMap.get(files, fileName), {
691
- onNone: () => void 0,
692
- onSome: (file) => file !== void 0
693
- });
694
- };
695
- const fileSystem = {
696
- readFile: (fileName) => {
697
- const normalized = normalizeFileName$1(fileName);
698
- if (normalized.endsWith(".tsbuildinfo")) return null;
699
- return readFromSources(filesRef.ref.current, overridesRef.ref.current, normalized);
700
- },
701
- fileExists: (fileName) => {
702
- const normalized = normalizeFileName$1(fileName);
703
- if (normalized.endsWith(".tsbuildinfo")) return false;
704
- return existsInSources(filesRef.ref.current, overridesRef.ref.current, normalized);
705
- },
706
- directoryExists: () => void 0,
707
- getAccessibleEntries: () => void 0,
708
- realpath: () => void 0
709
- };
710
- const readFileFromDisk = (fileName) => Effect.gen(function* () {
711
- const content = yield* fsService.readFileString(fileName).pipe(Effect.orElseSucceed(() => void 0));
712
- const file = Option.getOrUndefined(Option.map(Option.fromUndefinedOr(content), (text) => makeScriptFile(text, fileName)));
713
- yield* Ref.update(filesRef, (m) => setInPlace(m, fileName, file));
714
- return file;
715
- });
716
- const getFile = (fileName) => Effect.gen(function* () {
717
- const normalized = normalizeFileName$1(fileName);
718
- const files = yield* Ref.get(filesRef);
719
- const cached = MutableHashMap.get(files, normalized);
720
- if (Option.isSome(cached)) return cached.value;
721
- return yield* readFileFromDisk(normalized);
722
- });
723
- const fileForWrite = (existing, data, fileName) => {
724
- if (existing === void 0) return makeScriptFile(data, fileName);
725
- return withContent(existing, data);
726
- };
727
- const writeFile = (fileName, data) => Effect.gen(function* () {
728
- const normalized = normalizeFileName$1(fileName);
729
- const files = yield* Ref.get(filesRef);
730
- const existing = Option.getOrUndefined(MutableHashMap.get(files, normalized));
731
- yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, fileForWrite(existing, data, normalized)));
732
- });
733
- const mutateFile = (fileName, mutant) => Effect.gen(function* () {
734
- const file = yield* getFile(fileName);
735
- if (file === void 0) return yield* new HybridFileNotFoundError({ fileName });
736
- const next = mutateScriptFile(file, mutant);
737
- const normalized = normalizeFileName$1(fileName);
738
- yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, next));
739
- });
740
- const resetFile = (fileName) => Effect.gen(function* () {
741
- const normalized = normalizeFileName$1(fileName);
742
- const files = yield* Ref.get(filesRef);
743
- const file = Option.getOrUndefined(MutableHashMap.get(files, normalized));
744
- if (file === void 0) return;
745
- yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, resetScriptFile(file)));
746
- });
747
- const existsInMemory = (fileName) => Effect.gen(function* () {
748
- const files = yield* Ref.get(filesRef);
749
- return Option.getOrUndefined(MutableHashMap.get(files, normalizeFileName$1(fileName))) !== void 0;
750
- });
751
- const setTsConfigOverrides = (overrides) => Ref.set(overridesRef, overrides);
752
- return {
753
- fileSystem,
754
- getFile,
755
- writeFile,
756
- mutateFile,
757
- resetFile,
758
- existsInMemory,
759
- setTsConfigOverrides
760
- };
761
- });
762
- function makeTSFileNode(fileName) {
763
- return {
764
- fileName,
765
- parents: [],
766
- children: []
767
- };
768
- }
769
- function getAllParentReferencesIncludingSelf(node, allParentReferences = MutableHashSet.empty()) {
770
- MutableHashSet.add(allParentReferences, node);
771
- node.parents.forEach((parent) => collectParentReference(parent, allParentReferences));
772
- return allParentReferences;
773
- }
774
- function collectParentReference(parent, allParentReferences) {
775
- if (MutableHashSet.has(allParentReferences, parent)) return;
776
- getAllParentReferencesIncludingSelf(parent, allParentReferences);
777
- }
778
- function addRangeOfNodesToSet(nodes, nodesToAdd) {
779
- for (const node of nodesToAdd) MutableHashSet.add(nodes, node);
780
- }
781
- function findNode(fileName, nodes) {
782
- const node = Option.firstSomeOf([MutableHashMap.get(nodes, normalizeFileName$1(fileName)), MutableHashMap.get(nodes, fileName)]);
783
- if (Option.isNone(node)) throw new Error(`Node not in graph: ${fileName}`);
784
- return node.value;
785
- }
786
- function parentsHaveOverlapWith(currentNode, groupNodes) {
787
- return Array.from(getAllParentReferencesIncludingSelf(currentNode)).some((parentNode) => MutableHashSet.has(groupNodes, parentNode));
788
- }
789
- function mutantCanJoinGroup(currentNode, group) {
790
- if (MutableHashSet.has(group.ignoredNodes, currentNode)) return false;
791
- return !parentsHaveOverlapWith(currentNode, group.nodes);
792
- }
793
- function addMutantToGroup(currentMutant, mutantsToGroup, group, nodes) {
794
- const currentNode = findNode(currentMutant.fileName, nodes);
795
- if (!mutantCanJoinGroup(currentNode, group)) return;
796
- group.mutantIds.push(currentMutant.id);
797
- MutableHashSet.add(group.nodes, currentNode);
798
- MutableHashSet.remove(mutantsToGroup, currentMutant);
799
- addRangeOfNodesToSet(group.ignoredNodes, getAllParentReferencesIncludingSelf(currentNode));
800
- }
801
- function takeGroup(mutantsToGroup, nodes) {
802
- const group = {
803
- mutantIds: [],
804
- nodes: MutableHashSet.empty(),
805
- ignoredNodes: MutableHashSet.empty()
806
- };
807
- for (const currentMutant of mutantsToGroup) addMutantToGroup(currentMutant, mutantsToGroup, group, nodes);
808
- return group.mutantIds;
809
- }
810
- function createGroups(mutants, nodes) {
811
- const groups = [];
812
- const mutantsToGroup = MutableHashSet.fromIterable(mutants);
813
- while (MutableHashSet.size(mutantsToGroup) > 0) groups.push(takeGroup(mutantsToGroup, nodes));
814
- return groups;
815
- }
816
- var TypeScriptCompiler = class extends Context.Service()("@systemfsoftware/stryker-js-typescript-checker/TypeScriptCompiler") {};
817
- const makeDummy = Effect.gen(function* () {
818
- const stateRef = yield* Ref.make({
819
- api: void 0,
820
- snapshot: void 0,
821
- sourceFiles: MutableHashMap.empty(),
822
- nodes: MutableHashMap.empty(),
823
- lastMutants: [],
824
- lastMutatedFileNames: [],
825
- allTSConfigFiles: MutableHashSet.fromIterable(["tsconfig.json"]),
826
- tsconfigFile: "tsconfig.json"
827
- });
828
- yield* Effect.addFinalizer(() => Effect.gen(function* () {
829
- const s = yield* Ref.get(stateRef);
830
- yield* Effect.sync(() => s.snapshot?.dispose());
831
- yield* Effect.sync(() => s.api?.close());
832
- }));
833
- return {
834
- init: Effect.succeed([]),
835
- check: () => Effect.succeed([]),
836
- nodes: Ref.get(stateRef).pipe(Effect.map((s) => s.nodes)),
837
- close: Effect.gen(function* () {
838
- const s = yield* Ref.get(stateRef);
839
- yield* Effect.sync(() => s.snapshot?.dispose());
840
- yield* Effect.sync(() => s.api?.close());
841
- yield* Ref.update(stateRef, (prev) => ({
842
- ...prev,
843
- snapshot: void 0,
844
- api: void 0
845
- }));
846
- }),
847
- getLineAndCharacterOfPosition: () => Effect.succeed(void 0)
848
- };
849
- });
850
- Layer.effect(TypeScriptCompiler)(makeDummy);
851
- function makeTypescriptCompiler(options, fs, fsService, pathService) {
852
- if (!S.is(StrykerOptionsSchema)(options)) throw new Error("Invalid StrykerOptions");
853
- const rawTsconfigFile = normalizeFileName$1(options.tsconfigFile);
854
- const initialState = {
855
- api: void 0,
856
- snapshot: void 0,
857
- sourceFiles: MutableHashMap.empty(),
858
- nodes: MutableHashMap.empty(),
859
- lastMutants: [],
860
- lastMutatedFileNames: [],
861
- allTSConfigFiles: MutableHashSet.fromIterable([rawTsconfigFile]),
862
- tsconfigFile: rawTsconfigFile
863
- };
864
- const stateRef = Ref.makeUnsafe(initialState);
865
- const snapshotOf = (state) => {
866
- if (state.snapshot === void 0) return Effect.fail(new CompilerFailed({ reason: "not-initialized" }));
867
- return Effect.succeed(state.snapshot);
868
- };
869
- const programsOf = (snapshot, tsconfigFile) => {
870
- const projects = snapshot.getProjects();
871
- if (projects.length === 0) return Effect.fail(new CompilerFailed({
872
- reason: "no-projects",
873
- subject: tsconfigFile
874
- }));
875
- return Effect.succeed(projects.map((project) => project.program));
876
- };
877
- const getProgramsEffect = () => Effect.gen(function* () {
878
- const state = yield* Ref.get(stateRef);
879
- const snapshot = yield* snapshotOf(state);
880
- return yield* programsOf(snapshot, state.tsconfigFile);
881
- });
882
- const guardTSConfigFileExistsEffect = Effect.gen(function* () {
883
- const s = yield* Ref.get(stateRef);
884
- yield* fsService.readFileString(s.tsconfigFile).pipe(Effect.mapError(() => new TsConfigNotFoundError({ file: s.tsconfigFile })));
885
- });
886
- const isBlankTsConfigPath = (current) => current === void 0 || current === "";
887
- const isUnprocessedTsConfigPath = (current, processed) => !isBlankTsConfigPath(current) && !MutableHashSet.has(processed, current);
888
- const recordParsedTsConfig = (current, config, traversal) => {
889
- MutableHashMap.set(traversal.overrides, current, overrideOptions(config, traversal.buildModeEnabled));
890
- for (const referenced of retrieveReferencedProjects(config, pathService.dirname(current), pathService)) {
891
- MutableHashSet.add(traversal.allTsConfigFiles, normalizeFileName$1(referenced));
892
- traversal.pending.push(referenced);
893
- }
894
- };
895
- const recordTsConfig = (current, content, parsed, traversal) => {
896
- if (Result.isFailure(parsed)) {
897
- MutableHashMap.set(traversal.overrides, current, content);
898
- return;
899
- }
900
- recordParsedTsConfig(current, parsed.success, traversal);
901
- };
902
- const processNextTsConfig = (traversal) => Effect.gen(function* () {
903
- const current = traversal.pending.pop();
904
- if (!isUnprocessedTsConfigPath(current, traversal.processed)) return;
905
- MutableHashSet.add(traversal.processed, current);
906
- const content = yield* fsService.readFileString(current);
907
- recordTsConfig(current, content, parseTsConfig(current, content), traversal);
908
- });
909
- const collectAllTSConfigFiles = (buildModeEnabled) => Effect.gen(function* () {
910
- const state = yield* Ref.get(stateRef);
911
- const traversal = {
912
- overrides: MutableHashMap.empty(),
913
- pending: [state.tsconfigFile],
914
- processed: MutableHashSet.empty(),
915
- allTsConfigFiles: state.allTSConfigFiles,
916
- buildModeEnabled
917
- };
918
- while (traversal.pending.length > 0) yield* processNextTsConfig(traversal);
919
- yield* fs.setTsConfigOverrides(traversal.overrides);
920
- yield* Ref.update(stateRef, (prev) => ({
921
- ...prev,
922
- allTSConfigFiles: MutableHashSet.fromIterable(traversal.allTsConfigFiles)
923
- }));
924
- });
925
- const importDeclarationSpecifierOf = (statement, sourceFile) => {
926
- if (statement.kind !== SyntaxKind.ImportDeclaration) return Option.none();
927
- let specifier;
928
- statement.forEachChild((child) => {
929
- if (child.kind === SyntaxKind.StringLiteral) specifier = child;
930
- });
931
- return Option.map(Option.fromUndefinedOr(specifier), (found) => found.getText(sourceFile));
932
- };
933
- const collectImportEqualsSpecifiers = (statement, sourceFile, into) => {
934
- if (statement.kind !== SyntaxKind.ImportEqualsDeclaration) return;
935
- statement.forEachChild((child) => {
936
- if (child.kind === SyntaxKind.ExternalModuleReference) child.forEachChild((refChild) => {
937
- if (refChild.kind === SyntaxKind.StringLiteral) into.push(refChild.getText(sourceFile));
938
- });
939
- });
940
- };
941
- const extractImports = (sourceFile) => {
942
- const result = [];
943
- sourceFile.statements.forEach((statement) => {
944
- const specifier = importDeclarationSpecifierOf(statement, sourceFile);
945
- if (Option.isSome(specifier)) result.push(specifier.value);
946
- collectImportEqualsSpecifiers(statement, sourceFile, result);
947
- });
948
- sourceFile.referencedFiles.forEach((ref) => result.push(ref.fileName));
949
- sourceFile.typeReferenceDirectives.forEach((ref) => result.push(ref.fileName));
950
- return result;
951
- };
952
- const getResolutionCandidates = (resolved, pathService) => {
953
- const extension = pathService.extname(resolved);
954
- if (extension) {
955
- const withoutExt = resolved.slice(0, -extension.length);
956
- return [
957
- resolved,
958
- `${withoutExt}.ts`,
959
- `${withoutExt}.tsx`,
960
- `${withoutExt}.d.ts`,
961
- `${withoutExt}.js`,
962
- `${withoutExt}.jsx`,
963
- `${withoutExt}.mjs`,
964
- `${withoutExt}.cjs`
965
- ];
966
- }
967
- return [
968
- resolved,
969
- `${resolved}.ts`,
970
- `${resolved}.tsx`,
971
- `${resolved}.d.ts`,
972
- `${resolved}/index.ts`,
973
- `${resolved}/index.tsx`,
974
- `${resolved}/index.d.ts`,
975
- `${resolved}.js`,
976
- `${resolved}.jsx`,
977
- `${resolved}.mjs`,
978
- `${resolved}.cjs`,
979
- `${resolved}/index.js`,
980
- `${resolved}/index.jsx`,
981
- `${resolved}/index.mjs`,
982
- `${resolved}/index.cjs`
983
- ];
984
- };
985
- const resolveModuleSpecifier = (sourceFileName, specifier, sourceFiles, pathService) => {
986
- const cleaned = specifier.replace(/^['"]|['"]$/g, "");
987
- if (!relativeSpecifierPattern.test(cleaned)) return;
988
- const baseDir = pathService.dirname(sourceFileName);
989
- const resolved = normalizeFileName$1(pathService.resolve(baseDir, cleaned));
990
- return getResolutionCandidates(resolved, pathService).find((candidate) => MutableHashMap.has(sourceFiles, candidate));
991
- };
992
- const readFileText = (fileName) => Option.liftPredicate(fs.fileSystem.readFile?.(fileName), isString);
993
- const sourcesFieldOf = (rawMap) => {
994
- if (!Predicate.hasProperty(rawMap, "sources")) return Option.none();
995
- return Option.liftPredicate(rawMap.sources, Array.isArray);
996
- };
997
- const onlySourceOf = (sources) => {
998
- const names = sources.filter(isString);
999
- if (names.length !== 1) return Option.none();
1000
- return Option.fromUndefinedOr(names[0]);
1001
- };
1002
- const sourcePathFromMap = (declarationFileName, reference, pathService) => {
1003
- const sourceMapFileName = normalizeFileName$1(pathService.resolve(pathService.dirname(declarationFileName), reference));
1004
- return Option.flatMap(Option.flatMap(readFileText(sourceMapFileName), (content) => Option.flatMap(sourcesFieldOf(JSON.parse(content)), onlySourceOf)), (source) => Option.some(normalizeFileName$1(pathService.resolve(pathService.dirname(sourceMapFileName), source))));
1005
- };
1006
- const sourceMappedFileName = (declarationFileName, pathService) => Option.flatMap(Option.flatMap(readFileText(declarationFileName), (content) => Option.liftPredicate(getSourceMappingURL(content), isNonEmptyString)), (reference) => sourcePathFromMap(declarationFileName, reference, pathService));
1007
- const resolveTSInputFile = (dependencyFileName, pathService) => {
1008
- if (!dependencyFileName.endsWith(".d.ts")) return dependencyFileName;
1009
- return Option.getOrElse(sourceMappedFileName(dependencyFileName, pathService), () => dependencyFileName);
1010
- };
1011
- const registerGraphFile = (fileName, sourceFiles) => {
1012
- if (ignoredGraphFileNamePattern.test(fileName)) return;
1013
- const normalized = normalizeFileName$1(fileName);
1014
- MutableHashMap.set(sourceFiles, normalized, {
1015
- fileName: normalized,
1016
- imports: MutableHashSet.empty()
1017
- });
1018
- };
1019
- const registerSourceFiles = (programs, sourceFiles) => {
1020
- for (const program of programs) program.getSourceFileNames().forEach((fileName) => registerGraphFile(fileName, sourceFiles));
1021
- };
1022
- const isUsableResolution = (resolved) => resolved !== void 0 && resolved !== "";
1023
- const addImportEdge = (fileName, importedFileName, sourceFiles) => {
1024
- if (!MutableHashMap.has(sourceFiles, importedFileName)) return;
1025
- Option.match(MutableHashMap.get(sourceFiles, fileName), {
1026
- onNone: () => void 0,
1027
- onSome: (entry) => MutableHashSet.add(entry.imports, importedFileName)
1028
- });
1029
- };
1030
- const linkImport = (fileName, specifier, sourceFiles) => {
1031
- const resolved = resolveModuleSpecifier(fileName, specifier, sourceFiles, pathService);
1032
- if (!isUsableResolution(resolved)) return;
1033
- addImportEdge(fileName, resolveTSInputFile(resolved, pathService), sourceFiles);
1034
- };
1035
- const linkFileImports = (fileName, programs, sourceFiles) => {
1036
- const sourceFile = programs.map((program) => program.getSourceFile(fileName)).find((candidate) => candidate != null);
1037
- if (sourceFile === void 0) return;
1038
- extractImports(sourceFile).forEach((specifier) => linkImport(fileName, specifier, sourceFiles));
1039
- };
1040
- const buildDependencyGraph = (programs) => Effect.gen(function* () {
1041
- const state = yield* Ref.get(stateRef);
1042
- registerSourceFiles(programs, state.sourceFiles);
1043
- for (const [fileName] of state.sourceFiles) linkFileImports(fileName, programs, state.sourceFiles);
1044
- yield* Ref.update(stateRef, (prev) => ({
1045
- ...prev,
1046
- sourceFiles: MutableHashMap.fromIterable(state.sourceFiles)
1047
- }));
1048
- });
1049
- const createEmptyNodes = (state) => {
1050
- for (const [fileName] of state.sourceFiles) MutableHashMap.set(state.nodes, fileName, makeTSFileNode(fileName));
1051
- };
1052
- const childNodeOf = (state, fileName, imports) => Effect.gen(function* () {
1053
- const node = MutableHashMap.get(state.nodes, fileName);
1054
- if (Option.isNone(node)) return yield* new CompilerFailed({
1055
- reason: "unknown-file-node",
1056
- subject: fileName
1057
- });
1058
- const children = Array.from(imports).map((importName) => Option.getOrUndefined(MutableHashMap.get(state.nodes, importName))).filter((child) => child !== void 0);
1059
- return {
1060
- ...node.value,
1061
- children,
1062
- parents: []
1063
- };
1064
- });
1065
- const collectChildNodes = (state, withChildren) => Effect.gen(function* () {
1066
- for (const [fileName, file] of state.sourceFiles) {
1067
- const node = yield* childNodeOf(state, fileName, file.imports);
1068
- MutableHashMap.set(withChildren, fileName, node);
1069
- }
1070
- });
1071
- const replaceMapContents = (target, source) => {
1072
- MutableHashMap.clear(target);
1073
- for (const [key, value] of source) MutableHashMap.set(target, key, value);
1074
- };
1075
- const parentNodesOf = (node, nodes) => {
1076
- const parents = [];
1077
- MutableHashMap.forEach(nodes, (candidate) => {
1078
- if (candidate.children.includes(node)) parents.push(candidate);
1079
- });
1080
- return parents;
1081
- };
1082
- const linkParentReferences = (state) => {
1083
- const withParents = MutableHashMap.empty();
1084
- for (const [fileName, node] of state.nodes) MutableHashMap.set(withParents, fileName, {
1085
- ...node,
1086
- parents: parentNodesOf(node, state.nodes)
1087
- });
1088
- replaceMapContents(state.nodes, withParents);
1089
- };
1090
- const buildFileNodes = (state) => Effect.gen(function* () {
1091
- createEmptyNodes(state);
1092
- const withChildren = MutableHashMap.empty();
1093
- yield* collectChildNodes(state, withChildren);
1094
- replaceMapContents(state.nodes, withChildren);
1095
- linkParentReferences(state);
1096
- yield* Ref.update(stateRef, (prev) => ({
1097
- ...prev,
1098
- nodes: MutableHashMap.fromIterable(state.nodes)
1099
- }));
1100
- });
1101
- const getNodesEffect = Effect.gen(function* () {
1102
- const state = yield* Ref.get(stateRef);
1103
- if (MutableHashMap.size(state.nodes) > 0) return state.nodes;
1104
- yield* buildFileNodes(state);
1105
- return state.nodes;
1106
- });
1107
- const resetMutatedFiles = (mutants) => Effect.gen(function* () {
1108
- for (const mutant of mutants) yield* fs.resetFile(mutant.fileName);
1109
- });
1110
- const applyMutant = (mutant) => Effect.gen(function* () {
1111
- if ((yield* fs.getFile(mutant.fileName)) === void 0) return yield* new CompilerFailed({
1112
- reason: "file-not-in-project",
1113
- subject: mutant.fileName
1114
- });
1115
- yield* fs.mutateFile(mutant.fileName, mutant);
1116
- });
1117
- const applyMutants = (mutants) => Effect.gen(function* () {
1118
- for (const mutant of mutants) yield* applyMutant(mutant);
1119
- });
1120
- const hasOpenSnapshot = (state) => state.api !== void 0 && state.snapshot !== void 0;
1121
- const updateSnapshot = (state, changedFiles) => Effect.gen(function* () {
1122
- const previous = state.snapshot;
1123
- const next = state.api.updateSnapshot({
1124
- openProjects: Array.from(state.allTSConfigFiles),
1125
- fileChanges: { changed: changedFiles }
1126
- });
1127
- yield* Effect.sync(() => previous.dispose());
1128
- yield* Ref.update(stateRef, (prev) => ({
1129
- ...prev,
1130
- snapshot: next
1131
- }));
1132
- });
1133
- const check = (mutants) => Effect.gen(function* () {
1134
- const state = yield* Ref.get(stateRef);
1135
- yield* resetMutatedFiles(state.lastMutants);
1136
- yield* applyMutants(mutants);
1137
- const mutatedFileNames = Array.from(MutableHashSet.fromIterable(mutants.map((mutant) => normalizeFileName$1(mutant.fileName))));
1138
- const changedFiles = Array.from(MutableHashSet.fromIterable([...state.lastMutatedFileNames, ...mutatedFileNames]));
1139
- const current = yield* Ref.get(stateRef);
1140
- if (hasOpenSnapshot(current)) yield* updateSnapshot(current, changedFiles);
1141
- yield* Ref.update(stateRef, (prev) => ({
1142
- ...prev,
1143
- lastMutants: [...mutants],
1144
- lastMutatedFileNames: mutatedFileNames
1145
- }));
1146
- return (yield* getProgramsEffect()).flatMap((program) => [
1147
- ...program.getConfigFileParsingDiagnostics(),
1148
- ...program.getSemanticDiagnostics(),
1149
- ...program.getProgramDiagnostics()
1150
- ]).filter((diagnostic) => diagnostic.category === DiagnosticCategory.Error);
1151
- });
1152
- const init = Effect.gen(function* () {
1153
- yield* guardTSVersion(fsService, pathService);
1154
- const absoluteTsconfigFile = normalizeFileName$1(pathService.resolve(rawTsconfigFile));
1155
- yield* Ref.update(stateRef, (prev) => ({
1156
- ...prev,
1157
- tsconfigFile: absoluteTsconfigFile,
1158
- allTSConfigFiles: MutableHashSet.fromIterable([absoluteTsconfigFile])
1159
- }));
1160
- yield* guardTSConfigFileExistsEffect;
1161
- const buildModeEnabled = yield* determineBuildModeEnabled(absoluteTsconfigFile, fsService);
1162
- yield* collectAllTSConfigFiles(buildModeEnabled);
1163
- const s = yield* Ref.get(stateRef);
1164
- const api = new API({ fs: fs.fileSystem });
1165
- const snapshot = api.updateSnapshot({ openProjects: Array.from(s.allTSConfigFiles) });
1166
- yield* Ref.update(stateRef, (prev) => ({
1167
- ...prev,
1168
- api,
1169
- snapshot
1170
- }));
1171
- const programs = yield* getProgramsEffect();
1172
- yield* buildDependencyGraph(programs);
1173
- return yield* check([]);
1174
- });
1175
- const close = Effect.gen(function* () {
1176
- const s = yield* Ref.get(stateRef);
1177
- yield* Effect.sync(() => s.snapshot?.dispose());
1178
- yield* Effect.sync(() => s.api?.close());
1179
- yield* Ref.update(stateRef, (prev) => ({
1180
- ...prev,
1181
- snapshot: void 0,
1182
- api: void 0
1183
- }));
1184
- });
1185
- const getLineAndCharacterOfPosition = (fileName, position) => Effect.gen(function* () {
1186
- return (yield* getProgramsEffect()).map((program) => program.getSourceFile(fileName)).find((sourceFile) => sourceFile !== void 0)?.getLineAndCharacterOfPosition(position);
1187
- });
1188
- return {
1189
- init,
1190
- check,
1191
- nodes: getNodesEffect,
1192
- close,
1193
- getLineAndCharacterOfPosition
1194
- };
1195
- }
1196
- //#endregion
1197
- //#region src/mutant-groups.ts
1198
- const groupsWithStrangers = (inside, outside, nodes) => {
1199
- const groups = createGroups([...inside], nodes);
1200
- if (outside.length > 0) return [outside.map((mutant) => mutant.id), ...groups];
1201
- return groups;
1202
- };
1203
- const knownFileGroups = (mutants, nodes) => {
1204
- const inside = mutants.filter((mutant) => Option.isSome(MutableHashMap.get(nodes, normalizeFileName(mutant.fileName))));
1205
- const outside = mutants.filter((mutant) => Option.isNone(MutableHashMap.get(nodes, normalizeFileName(mutant.fileName))));
1206
- if (inside.length === 0) return mutants.map((mutant) => [mutant.id]);
1207
- return groupsWithStrangers(inside, outside, nodes);
1208
- };
1209
- const groupMutants = (mutants, nodes, prioritizePerformanceOverAccuracy) => {
1210
- if (prioritizePerformanceOverAccuracy) return knownFileGroups(mutants, nodes);
1211
- return [mutants.map((mutant) => mutant.id)];
1212
- };
1213
- //#endregion
1214
- //#region src/Checker.ts
1215
- function getPrioritize(options) {
1216
- const decoded = Schema.decodeUnknownOption(TypescriptCheckerOptionsSchema)(options);
1217
- return Option.getOrElse(Option.flatMap(decoded, (value) => Option.fromUndefinedOr(value.typescriptChecker?.prioritizePerformanceOverAccuracy)), () => false);
1218
- }
1219
- const refuse = (mutantIds, cause) => new CheckerFailed({
1220
- checkerName: "typescript",
1221
- mutantIds: [...mutantIds],
1222
- cause: errorToString(cause)
1223
- });
1224
- const severityOf = (category) => Match.value(category).pipe(Match.when(DiagnosticCategory.Error, () => "error"), Match.when(DiagnosticCategory.Warning, () => "warning"), Match.when(DiagnosticCategory.Suggestion, () => "suggestion"), Match.orElse(() => "message"));
1225
- const toCheckResult = (answer) => {
1226
- if (answer.status === "passed") return { status: "passed" };
1227
- return {
1228
- status: "compileError",
1229
- reason: answer.reason
1230
- };
1231
- };
1232
- const mergeAnswers = (runs) => runs.reduce((merged, answers) => Object.entries(answers).reduce((into, [id, answer]) => HashMap.set(into, id, toCheckResult(answer)), merged), HashMap.empty());
1233
- const checkCell = Cell.layer({
1234
- read: (command) => Effect.flatMap(TypeScriptCompiler, (compiler) => Effect.zipWith(compiler.nodes, compiler.check([...command.mutants]), (nodes, diagnostics) => new CheckMutantsInput({
1235
- mutants: [...command.mutants],
1236
- diagnostics: [...diagnostics],
1237
- nodes: Object.fromEntries(nodes)
1238
- }))).pipe(Effect.mapError((cause) => refuse(command.mutants.map((mutant) => mutant.id), cause))),
1239
- decide: checkMutants,
1240
- write: (outcome) => Result.match(outcome, {
1241
- onFailure: (failure) => Effect.fail(refuse([], failure)),
1242
- onSuccess: Effect.succeed
1243
- })
1244
- });
1245
- const makeCheckerService = ({ options, compiler }) => {
1246
- const verify = Cell.provide(checkCell, Layer.succeed(TypeScriptCompiler, compiler));
1247
- const positionOf = (error) => Option.match(Option.filter(Option.fromUndefinedOr(error.fileName), (fileName) => fileName !== ""), {
1248
- onNone: () => Effect.succeed(""),
1249
- onSome: (fileName) => compiler.getLineAndCharacterOfPosition(fileName, error.pos).pipe(Effect.orElseSucceed(() => void 0), Effect.map((at) => Option.match(Option.fromUndefinedOr(at), {
1250
- onNone: () => `${fileName}(1,1): `,
1251
- onSome: (position) => `${fileName}(${position.line + 1},${position.character + 1}): `
1252
- })))
1253
- });
1254
- const formatDiagnostic = (error) => positionOf(error).pipe(Effect.map((position) => `${position}${severityOf(error.category)} TS${error.code}: ${error.text}`));
1255
- const createErrorText = (errors) => Effect.map(Effect.forEach(errors, formatDiagnostic), (parts) => parts.join("\n"));
1256
- const soloRound = (mutant) => verify.run(new CheckMutantsCommand({ mutants: [mutant] })).pipe(Effect.map((decision) => decision.results));
1257
- const soloRounds = (decision) => Match.value(decision).pipe(Match.tag("CheckFinished", () => Effect.succeed([])), Match.tag("RetestRequired", (retest) => verify.run(new CheckMutantsCommand({ mutants: [] })).pipe(Effect.flatMap(() => Effect.forEach(retest.needsRetest, soloRound)))), Match.exhaustive);
1258
- return {
1259
- init: compiler.init.pipe(Effect.mapError((cause) => refuse([], cause)), Effect.flatMap((errors) => {
1260
- if (errors.length === 0) return Effect.void;
1261
- return createErrorText(errors).pipe(Effect.map((text) => refuse([], /* @__PURE__ */ new Error(`Typescript error(s) found in dry run compilation: ${text}`))), Effect.flatMap(Effect.fail));
1262
- })),
1263
- check: (mutants) => verify.run(new CheckMutantsCommand({ mutants: [...mutants] })).pipe(Effect.flatMap((first) => Effect.map(soloRounds(first), (rounds) => mergeAnswers([first.results, ...rounds])))),
1264
- group: (mutants) => compiler.nodes.pipe(Effect.map((nodes) => groupMutants(mutants, nodes, getPrioritize(options))), Effect.mapError((cause) => refuse(mutants.map((mutant) => mutant.id), cause)))
1265
- };
1266
- };
1267
- //#endregion
1268
22
  //#region src/index.ts
1269
- const strykerPlugins = [declarePlugin("Checker", "typescript", Layer.effect(Checker, Effect.gen(function* () {
1270
- const options = yield* RunConfiguration;
1271
- const fsService = yield* FileSystem.FileSystem;
1272
- const pathService = yield* Path.Path;
1273
- const compiler = makeTypescriptCompiler(options, yield* makeHybridFileSystem(fsService), fsService, pathService);
1274
- return makeCheckerService({
1275
- options,
1276
- compiler
1277
- });
1278
- })))];
23
+ const strykerPlugins = [{
24
+ kind: "Checker",
25
+ name: "typescript",
26
+ workerEntry: new URL("./main.mjs", import.meta.url).href
27
+ }];
1279
28
  const rawSchema = typescript_checker_options_default;
1280
29
  if (!S.is(S.Record(S.String, S.Unknown))(rawSchema)) throw new Error("Invalid typescript-checker schema file");
1281
30
  const strykerValidationSchema = rawSchema;