@systemfsoftware/stryker-js-typescript-checker 0.1.0 → 1.2.3

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.
@@ -1,15 +1,21 @@
1
1
  $ tsdown
2
- tsdown v0.22.7 powered by rolldown v1.1.5
3
- config file: /mnt/projects/God/systemfsoftware/packages/stryker-js/typescript-checker/package.json
4
- entry: src/index.ts
5
- tsconfig: tsconfig.json
6
- ℹ Build start
7
- Cleaning 2 files
8
-
9
- WARN TypeScript 7.0 does not yet have a stable API and is experimental. Some options will be unavailable.
10
-
11
- Emit types with typescript@7.0.2
12
- ℹ dist/index.mjs 23.95 kB │ gzip: 6.75 kB
13
- dist/index.d.mts 4.66 kB gzip: 1.52 kB
14
- 2 files, total: 28.61 kB
15
- Build complete in 1441ms
2
+ ℹ tsdown v0.22.9 powered by rolldown v1.2.0
3
+ ℹ config file: /home/runner/work/systemfsoftware/systemfsoftware/packages/stryker-js/typescript-checker/tsdown.config.ts
4
+ ℹ entry: ./src/index.ts
5
+ ℹ tsconfig: tsconfig.json
6
+
7
+  WARN  `noExternal` is deprecated. Use `deps.alwaysBundle` instead.
8
+
9
+ ℹ Build start
10
+
11
+  WARN  TypeScript 7.0 does not yet have a stable API and is experimental. Some options will be unavailable.
12
+
13
+ ℹ Emit types with typescript@7.0.2
14
+ ℹ Hint: consider adding deps.onlyBundle option to avoid unintended bundling of dependencies, or set deps.onlyBundle: false to disable this hint.
15
+ See more at https://tsdown.dev/options/dependencies#deps-onlybundle
16
+ Detected dependencies in bundle:
17
+ - @jsr/std__jsonc
18
+ ℹ dist/index.mjs 31.51 kB │ gzip: 8.65 kB
19
+ ℹ dist/index.d.mts  4.66 kB │ gzip: 1.52 kB
20
+ ℹ 2 files, total: 36.17 kB
21
+ ✔ Build complete in 635ms
package/AGENTS.md ADDED
@@ -0,0 +1,7 @@
1
+ # AGENTS.md — `@systemfsoftware/stryker-js-typescript-checker`
2
+
3
+ > **Location:** `packages/stryker-js/typescript-checker/` — TypeScript checker plugin for Stryker, TS7-native. Universal agent rules live in the root `AGENTS.md`; this file carries only `typescript-checker/`-specific deltas.
4
+
5
+ Extends `@stryker-mutator/api` checker protocol, compiling projects directly with `typescript` against their own tsconfig (parsed via `@std/jsonc`). Built via tsdown, single entrypoint.
6
+
7
+ 🛑 Don't depend on `typescript` from the host project — peer dep resolved at runtime.
package/dist/index.mjs CHANGED
@@ -6,8 +6,231 @@ import { split, strykerReportBugUrl } from "@stryker-mutator/util";
6
6
  import { API, DiagnosticCategory } from "typescript/unstable/sync";
7
7
  import { createRequire } from "module";
8
8
  import path from "path";
9
+ import { Data, Either, Schema } from "effect";
9
10
  import semver from "semver";
10
11
  import { SyntaxKind } from "typescript/unstable/ast";
12
+ //#region ../../../node_modules/.pnpm/@jsr+std__jsonc@1.0.2/node_modules/@jsr/std__jsonc/parse.js
13
+ /**
14
+ * Converts a JSON with Comments (JSONC) string into an object.
15
+ *
16
+ * @example Usage
17
+ * ```ts
18
+ * import { parse } from "@std/jsonc";
19
+ * import { assertEquals } from "@std/assert";
20
+ *
21
+ * assertEquals(parse('{"foo": "bar"}'), { foo: "bar" });
22
+ * assertEquals(parse('{"foo": "bar", }'), { foo: "bar" });
23
+ * assertEquals(parse('{"foo": "bar", } /* comment *\/'), { foo: "bar" });
24
+ * ```
25
+ *
26
+ * @throws {SyntaxError} If the JSONC string is invalid.
27
+ * @param text A valid JSONC string.
28
+ * @returns The parsed JsonValue from the JSONC string.
29
+ */ function parse(text) {
30
+ if (new.target) throw new TypeError("Cannot create an instance: parse is not a constructor");
31
+ return new JsoncParser(text).parse();
32
+ }
33
+ var JsoncParser = class {
34
+ #whitespace = /* @__PURE__ */ new Set(" \r\n");
35
+ #numberEndToken = /* @__PURE__ */ new Set([..."[]{}:,/", ...this.#whitespace]);
36
+ #text;
37
+ #length;
38
+ #tokenized;
39
+ constructor(text) {
40
+ this.#text = `${text}`;
41
+ this.#length = this.#text.length;
42
+ this.#tokenized = this.#tokenize();
43
+ }
44
+ parse() {
45
+ const token = this.#getNext();
46
+ const res = this.#parseJsonValue(token);
47
+ const { done, value } = this.#tokenized.next();
48
+ if (!done) throw new SyntaxError(buildErrorMessage(value));
49
+ return res;
50
+ }
51
+ /** Read the next token. If the token is read to the end, it throws a SyntaxError. */ #getNext() {
52
+ const { done, value } = this.#tokenized.next();
53
+ if (done) throw new SyntaxError("Cannot parse JSONC: unexpected end of JSONC input");
54
+ return value;
55
+ }
56
+ /** Split the JSONC string into token units. Whitespace and comments are skipped. */ *#tokenize() {
57
+ for (let i = 0; i < this.#length; i++) {
58
+ if (this.#whitespace.has(this.#text[i])) continue;
59
+ if (this.#text[i] === "/" && this.#text[i + 1] === "*") {
60
+ i += 2;
61
+ let hasEndOfComment = false;
62
+ for (; i < this.#length; i++) if (this.#text[i] === "*" && this.#text[i + 1] === "/") {
63
+ hasEndOfComment = true;
64
+ break;
65
+ }
66
+ if (!hasEndOfComment) throw new SyntaxError("Cannot parse JSONC: unexpected end of JSONC input");
67
+ i++;
68
+ continue;
69
+ }
70
+ if (this.#text[i] === "/" && this.#text[i + 1] === "/") {
71
+ i += 2;
72
+ for (; i < this.#length; i++) if (this.#text[i] === "\n" || this.#text[i] === "\r") break;
73
+ continue;
74
+ }
75
+ switch (this.#text[i]) {
76
+ case "{":
77
+ yield {
78
+ type: "BeginObject",
79
+ position: i
80
+ };
81
+ break;
82
+ case "}":
83
+ yield {
84
+ type: "EndObject",
85
+ position: i
86
+ };
87
+ break;
88
+ case "[":
89
+ yield {
90
+ type: "BeginArray",
91
+ position: i
92
+ };
93
+ break;
94
+ case "]":
95
+ yield {
96
+ type: "EndArray",
97
+ position: i
98
+ };
99
+ break;
100
+ case ":":
101
+ yield {
102
+ type: "NameSeparator",
103
+ position: i
104
+ };
105
+ break;
106
+ case ",":
107
+ yield {
108
+ type: "ValueSeparator",
109
+ position: i
110
+ };
111
+ break;
112
+ case "\"": {
113
+ const startIndex = i;
114
+ let shouldEscapeNext = false;
115
+ i++;
116
+ for (; i < this.#length; i++) {
117
+ if (this.#text[i] === "\"" && !shouldEscapeNext) break;
118
+ shouldEscapeNext = this.#text[i] === "\\" && !shouldEscapeNext;
119
+ }
120
+ yield {
121
+ type: "String",
122
+ sourceText: this.#text.substring(startIndex, i + 1),
123
+ position: startIndex
124
+ };
125
+ break;
126
+ }
127
+ default: {
128
+ const startIndex = i;
129
+ for (; i < this.#length; i++) if (this.#numberEndToken.has(this.#text[i])) break;
130
+ i--;
131
+ yield {
132
+ type: "NullOrTrueOrFalseOrNumber",
133
+ sourceText: this.#text.substring(startIndex, i + 1),
134
+ position: startIndex
135
+ };
136
+ }
137
+ }
138
+ }
139
+ }
140
+ #parseJsonValue(value) {
141
+ switch (value.type) {
142
+ case "BeginObject": return this.#parseObject();
143
+ case "BeginArray": return this.#parseArray();
144
+ case "NullOrTrueOrFalseOrNumber": return this.#parseNullOrTrueOrFalseOrNumber(value);
145
+ case "String": return this.#parseString(value);
146
+ default: throw new SyntaxError(buildErrorMessage(value));
147
+ }
148
+ }
149
+ #parseObject() {
150
+ const target = {};
151
+ while (true) {
152
+ const token1 = this.#getNext();
153
+ if (token1.type === "EndObject") return target;
154
+ if (token1.type !== "String") throw new SyntaxError(buildErrorMessage(token1));
155
+ const key = this.#parseString(token1);
156
+ const token2 = this.#getNext();
157
+ if (token2.type !== "NameSeparator") throw new SyntaxError(buildErrorMessage(token2));
158
+ const token3 = this.#getNext();
159
+ Object.defineProperty(target, key, {
160
+ value: this.#parseJsonValue(token3),
161
+ writable: true,
162
+ enumerable: true,
163
+ configurable: true
164
+ });
165
+ const token4 = this.#getNext();
166
+ if (token4.type === "EndObject") return target;
167
+ if (token4.type !== "ValueSeparator") throw new SyntaxError(buildErrorMessage(token4));
168
+ }
169
+ }
170
+ #parseArray() {
171
+ const target = [];
172
+ while (true) {
173
+ const token1 = this.#getNext();
174
+ if (token1.type === "EndArray") return target;
175
+ target.push(this.#parseJsonValue(token1));
176
+ const token2 = this.#getNext();
177
+ if (token2.type === "EndArray") return target;
178
+ if (token2.type !== "ValueSeparator") throw new SyntaxError(buildErrorMessage(token2));
179
+ }
180
+ }
181
+ #parseString(value) {
182
+ let parsed;
183
+ try {
184
+ parsed = JSON.parse(value.sourceText);
185
+ } catch {
186
+ throw new SyntaxError(buildErrorMessage(value));
187
+ }
188
+ if (typeof parsed !== "string") throw new TypeError(`Parsed value is not a string: ${parsed}`);
189
+ return parsed;
190
+ }
191
+ #parseNullOrTrueOrFalseOrNumber(value) {
192
+ if (value.sourceText === "null") return null;
193
+ if (value.sourceText === "true") return true;
194
+ if (value.sourceText === "false") return false;
195
+ let parsed;
196
+ try {
197
+ parsed = JSON.parse(value.sourceText);
198
+ } catch {
199
+ throw new SyntaxError(buildErrorMessage(value));
200
+ }
201
+ if (typeof parsed !== "number") throw new TypeError(`Parsed value is not a number: ${parsed}`);
202
+ return parsed;
203
+ }
204
+ };
205
+ function buildErrorMessage({ type, sourceText, position }) {
206
+ let token = "";
207
+ switch (type) {
208
+ case "BeginObject":
209
+ token = "{";
210
+ break;
211
+ case "EndObject":
212
+ token = "}";
213
+ break;
214
+ case "BeginArray":
215
+ token = "[";
216
+ break;
217
+ case "EndArray":
218
+ token = "]";
219
+ break;
220
+ case "NameSeparator":
221
+ token = ":";
222
+ break;
223
+ case "ValueSeparator":
224
+ token = ",";
225
+ break;
226
+ case "NullOrTrueOrFalseOrNumber":
227
+ case "String":
228
+ token = 30 < sourceText.length ? `${sourceText.slice(0, 30)}...` : sourceText;
229
+ break;
230
+ }
231
+ return `Cannot parse JSONC: unexpected token "${token}" in JSONC at position ${position}`;
232
+ }
233
+ //#endregion
11
234
  //#region src/tsconfig-helpers.ts
12
235
  const COMPILER_OPTIONS_OVERRIDES = Object.freeze({
13
236
  allowUnreachableCode: true,
@@ -37,35 +260,58 @@ function getTSVersion() {
37
260
  function guardTSVersion(version = getTSVersion()) {
38
261
  if (!semver.satisfies(version, ">=7.0.0", { includePrerelease: true })) throw new Error(`@systemfsoftware/stryker-js-typescript-checker only supports typescript@7.0.0 or higher. Found typescript@${version}`);
39
262
  }
40
- function stripJsonComments(json) {
41
- return json.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
42
- }
43
- function parseConfigFileTextToJson(fileName, jsonText) {
263
+ /**
264
+ * Error returned when a tsconfig file fails to parse or does not match the shape this package consumes.
265
+ */
266
+ var TsConfigParseError = class extends Data.TaggedError("TsConfigParseError") {};
267
+ const JsonRecord = Schema.Record({
268
+ key: Schema.String,
269
+ value: Schema.Unknown
270
+ });
271
+ const TsConfigSchema = Schema.Struct({
272
+ references: Schema.optional(Schema.Array(Schema.Struct({ path: Schema.String }, JsonRecord))),
273
+ compilerOptions: Schema.optional(JsonRecord)
274
+ }, JsonRecord);
275
+ /**
276
+ * Parses the raw text of a tsconfig file into a typed config, rejecting shapes this package cannot consume.
277
+ * @param fileName The tsconfig file name, used for error reporting
278
+ * @param jsonText The raw tsconfig content
279
+ */
280
+ function parseTsConfig(fileName, jsonText) {
44
281
  try {
45
- const stripped = stripJsonComments(jsonText);
46
- return { config: JSON.parse(stripped) };
282
+ const value = parse(jsonText.replace(/^\uFEFF/, ""));
283
+ return Either.mapLeft(Schema.decodeUnknownEither(TsConfigSchema)(value), (issue) => new TsConfigParseError({
284
+ file: fileName,
285
+ reason: issue.message
286
+ }));
47
287
  } catch (error) {
48
- return { error };
288
+ return Either.left(new TsConfigParseError({
289
+ file: fileName,
290
+ reason: error instanceof Error ? error.message : String(error)
291
+ }));
49
292
  }
50
293
  }
294
+ /**
295
+ * Determines whether or not to use `--build` mode based on "references" being there in the config file
296
+ * @param tsconfigFileName The tsconfig file to parse
297
+ */
51
298
  function determineBuildModeEnabled(tsconfigFileName) {
52
- const parsed = parseConfigFileTextToJson(tsconfigFileName, readFileSync(tsconfigFileName, "utf-8"));
53
- if (parsed.error) return false;
54
- return "references" in parsed.config;
299
+ const parsed = parseTsConfig(tsconfigFileName, readFileSync(tsconfigFileName, "utf-8"));
300
+ return Either.match(parsed, {
301
+ onLeft: () => false,
302
+ onRight: (config) => config.references !== void 0
303
+ });
55
304
  }
56
305
  /**
57
306
  * Overrides some options to speed up compilation and disable some code quality checks we don't want during mutation testing
58
- * @param parsedConfig The parsed config file
307
+ * @param config The parsed config file
59
308
  * @param useBuildMode whether or not `--build` mode is used
60
309
  */
61
- function overrideOptions(parsedConfig, useBuildMode) {
62
- const config = parsedConfig.config ?? {};
310
+ function overrideOptions(config, useBuildMode) {
63
311
  const compilerOptions = {
64
312
  ...config.compilerOptions,
65
313
  ...COMPILER_OPTIONS_OVERRIDES,
66
- ...useBuildMode ? LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES : NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT,
67
- target: "es2022",
68
- moduleResolution: "bundler"
314
+ ...useBuildMode ? LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES : NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT
69
315
  };
70
316
  if (!useBuildMode && compilerOptions["declarationDir"] !== void 0 && compilerOptions["declarationDir"] !== null) delete compilerOptions["declarationDir"];
71
317
  if (useBuildMode) {
@@ -82,17 +328,15 @@ function overrideOptions(parsedConfig, useBuildMode) {
82
328
  }
83
329
  /**
84
330
  * Retrieves the referenced config files based on parsed configuration
85
- * @param parsedConfig The parsed config file
331
+ * @param config The parsed config file
86
332
  * @param fromDirName The directory where to resolve from
87
333
  */
88
- function retrieveReferencedProjects(parsedConfig, fromDirName) {
89
- const config = parsedConfig.config;
90
- if (Array.isArray(config?.references)) return config.references.map((reference) => {
334
+ function retrieveReferencedProjects(config, fromDirName) {
335
+ return (config.references ?? []).map((reference) => {
91
336
  let resolved = path.resolve(fromDirName, reference.path);
92
337
  if (!path.basename(resolved).endsWith(".json")) resolved = path.join(resolved, "tsconfig.json");
93
338
  return toPosixFileName(resolved);
94
339
  });
95
- return [];
96
340
  }
97
341
  /**
98
342
  * Replaces backslashes with forward slashes (used by typescript)
@@ -393,13 +637,14 @@ var TypescriptCompiler = class {
393
637
  if (!current || processed.has(current)) continue;
394
638
  processed.add(current);
395
639
  const content = readFileSync(current, "utf-8");
396
- const parsed = parseConfigFileTextToJson(current, content);
397
- if (parsed.error) {
640
+ const parsed = parseTsConfig(current, content);
641
+ if (Either.isLeft(parsed)) {
642
+ this.log.warn(`Could not parse tsconfig file "%s": %s. Compiler-option overrides and project-reference walking were skipped for this file, so mutants may be misreported as compile errors.`, current, parsed.left.reason);
398
643
  tsConfigOverrides.set(current, content);
399
644
  continue;
400
645
  }
401
- tsConfigOverrides.set(current, overrideOptions(parsed, buildModeEnabled));
402
- for (const referenced of retrieveReferencedProjects(parsed, path.dirname(current))) {
646
+ tsConfigOverrides.set(current, overrideOptions(parsed.right, buildModeEnabled));
647
+ for (const referenced of retrieveReferencedProjects(parsed.right, path.dirname(current))) {
403
648
  this.allTSConfigFiles.add(referenced);
404
649
  toProcess.push(referenced);
405
650
  }
package/package.json CHANGED
@@ -1,31 +1,40 @@
1
1
  {
2
2
  "name": "@systemfsoftware/stryker-js-typescript-checker",
3
- "version": "0.1.0",
3
+ "version": "1.2.3",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/systemfsoftware/systemfsoftware.git",
7
+ "directory": "packages/stryker-js/typescript-checker"
8
+ },
9
+ "homepage": "https://github.com/systemfsoftware/systemfsoftware/tree/main/packages/stryker-js/typescript-checker#readme",
10
+ "bugs": "https://github.com/systemfsoftware/systemfsoftware/issues",
4
11
  "description": "TypeScript checker plugin for Stryker — TS7 native",
5
12
  "type": "module",
6
13
  "exports": {
7
- ".": {
8
- "@systemfsoftware/source": "./src/index.ts",
9
- "default": "./dist/index.mjs"
10
- },
14
+ ".": "./dist/index.mjs",
11
15
  "./package.json": "./package.json"
12
16
  },
13
17
  "license": "Apache-2.0",
14
18
  "dependencies": {
15
19
  "@stryker-mutator/api": "^9.6.1",
16
20
  "@stryker-mutator/util": "^9.6.1",
21
+ "effect": "^3.22.0",
17
22
  "semver": "^7.7.0",
18
23
  "tslib": "~2.8.0",
19
24
  "typescript": "^7"
20
25
  },
21
26
  "devDependencies": {
27
+ "@std/jsonc": "npm:@jsr/std__jsonc@^1.0.2",
22
28
  "@types/node": "^24",
23
29
  "@types/semver": "^7.5.8",
24
30
  "rimraf": "^6.1.3",
25
- "tsdown": "^0.22.7",
31
+ "tsdown": "^0.22.9",
26
32
  "vitest": "^4",
27
- "@systemfsoftware/tsconfig": "^1.0.0",
28
- "@systemfsoftware/vitest-config": "^0.1.0"
33
+ "@systemfsoftware/vitest-config": "^0.1.0",
34
+ "@systemfsoftware/tsconfig": "^1.3.1"
35
+ },
36
+ "inlinedDependencies": {
37
+ "@jsr/std__jsonc": "1.0.2"
29
38
  },
30
39
  "scripts": {
31
40
  "clean": "rimraf dist",
@@ -2,6 +2,8 @@ import { readFileSync } from 'fs'
2
2
  import { createRequire } from 'module'
3
3
  import path from 'path'
4
4
 
5
+ import { parse } from '@std/jsonc'
6
+ import { Data, Either, Schema as S } from 'effect'
5
7
  import semver from 'semver'
6
8
 
7
9
  // Override some compiler options that have to do with code quality. When mutating, we're not interested in the resulting code quality
@@ -50,53 +52,78 @@ export function guardTSVersion(version = getTSVersion()): void {
50
52
  }
51
53
 
52
54
  /**
53
- * Determines whether or not to use `--build` mode based on "references" being there in the config file
54
- * @param tsconfigFileName The tsconfig file to parse
55
+ * Error returned when a tsconfig file fails to parse or does not match the shape this package consumes.
55
56
  */
56
- export interface ParsedConfig {
57
- config?: unknown
58
- error?: Error
59
- }
60
-
61
- function stripJsonComments(json: string): string {
62
- return json
63
- .replace(/\/\*[\s\S]*?\*\//g, '')
64
- .replace(/\/\/.*$/gm, '')
65
- }
57
+ export class TsConfigParseError extends Data.TaggedError('TsConfigParseError')<{
58
+ readonly file: string
59
+ readonly reason: string
60
+ }> {}
61
+
62
+ const JsonRecord = S.Record({ key: S.String, value: S.Unknown })
63
+ const TsConfigSchema = S.Struct(
64
+ {
65
+ references: S.optional(S.Array(S.Struct({ path: S.String }, JsonRecord))),
66
+ compilerOptions: S.optional(JsonRecord),
67
+ },
68
+ JsonRecord,
69
+ )
70
+ type TsConfig = S.Schema.Type<typeof TsConfigSchema>
66
71
 
67
- export function parseConfigFileTextToJson(fileName: string, jsonText: string): ParsedConfig {
72
+ /**
73
+ * Parses the raw text of a tsconfig file into a typed config, rejecting shapes this package cannot consume.
74
+ * @param fileName The tsconfig file name, used for error reporting
75
+ * @param jsonText The raw tsconfig content
76
+ */
77
+ export function parseTsConfig(fileName: string, jsonText: string): Either.Either<TsConfig, TsConfigParseError> {
78
+ // `@std/jsonc`'s whitespace set excludes U+FEFF, so it rejects a leading BOM, while `tsc` tolerates one.
68
79
  try {
69
- const stripped = stripJsonComments(jsonText)
70
- return { config: JSON.parse(stripped) }
80
+ const value = parse(jsonText.replace(/^\uFEFF/, ''))
81
+ // Rebuilds the object, reordering keys (declared fields hoist). Safe here: this
82
+ // package only reads the config, and `overrideOptions` builds a fresh object anyway.
83
+ // The core sibling (`packages/stryker-js/core/src/sandbox/parse-config-helper.ts`)
84
+ // deliberately uses an `S.is` guard instead — it mutates the parsed config and writes
85
+ // it back with `JSON.stringify`, so a rebuild there would reorder the user's tsconfig
86
+ // on disk. Independent boundaries by KTD-2; this note keeps the divergence deliberate.
87
+ return Either.mapLeft(
88
+ S.decodeUnknownEither(TsConfigSchema)(value),
89
+ (issue) => new TsConfigParseError({ file: fileName, reason: issue.message }),
90
+ )
71
91
  } catch (error) {
72
- return { error: error as Error }
92
+ return Either.left(
93
+ new TsConfigParseError({
94
+ file: fileName,
95
+ reason: error instanceof Error ? error.message : String(error),
96
+ }),
97
+ )
73
98
  }
74
99
  }
75
100
 
101
+ /**
102
+ * Determines whether or not to use `--build` mode based on "references" being there in the config file
103
+ * @param tsconfigFileName The tsconfig file to parse
104
+ */
76
105
  export function determineBuildModeEnabled(tsconfigFileName: string): boolean {
77
106
  const tsconfigFile = readFileSync(tsconfigFileName, 'utf-8')
78
- const parsed = parseConfigFileTextToJson(tsconfigFileName, tsconfigFile)
79
- if (parsed.error) {
80
- return false
81
- }
82
- const useProjectReferences = 'references' in (parsed.config as { references?: unknown[] })
83
- return useProjectReferences
107
+ const parsed = parseTsConfig(tsconfigFileName, tsconfigFile)
108
+ return Either.match(parsed, {
109
+ onLeft: () => false,
110
+ onRight: (config) => config.references !== undefined,
111
+ })
84
112
  }
85
113
 
86
114
  /**
87
115
  * Overrides some options to speed up compilation and disable some code quality checks we don't want during mutation testing
88
- * @param parsedConfig The parsed config file
116
+ * @param config The parsed config file
89
117
  * @param useBuildMode whether or not `--build` mode is used
90
118
  */
91
- export function overrideOptions(parsedConfig: ParsedConfig, useBuildMode: boolean): string {
92
- const config = (parsedConfig.config ?? {}) as { compilerOptions?: Record<string, unknown> }
119
+ export function overrideOptions(config: TsConfig, useBuildMode: boolean): string {
120
+ // `target` and `moduleResolution` are deliberately absent: both belong to the consumer.
121
+ // Forcing `moduleResolution` contradicts `module: NodeNext`/`Node16` (TS5095 + TS5109),
122
+ // and forcing `target` hides lib features the consumer's own target allows (TS2550).
93
123
  const compilerOptions: Record<string, unknown> = {
94
124
  ...config.compilerOptions,
95
125
  ...COMPILER_OPTIONS_OVERRIDES,
96
126
  ...(useBuildMode ? LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES : NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT),
97
- // TypeScript 7 removed some legacy defaults that the upstream fixtures still use.
98
- target: 'es2022',
99
- moduleResolution: 'bundler',
100
127
  }
101
128
 
102
129
  if (
@@ -124,27 +151,19 @@ export function overrideOptions(parsedConfig: ParsedConfig, useBuildMode: boolea
124
151
  })
125
152
  }
126
153
 
127
- interface ProjectReference {
128
- path: string
129
- }
130
-
131
154
  /**
132
155
  * Retrieves the referenced config files based on parsed configuration
133
- * @param parsedConfig The parsed config file
156
+ * @param config The parsed config file
134
157
  * @param fromDirName The directory where to resolve from
135
158
  */
136
- export function retrieveReferencedProjects(parsedConfig: ParsedConfig, fromDirName: string): string[] {
137
- const config = parsedConfig.config as { references?: ProjectReference[] } | undefined
138
- if (Array.isArray(config?.references)) {
139
- return config!.references.map((reference) => {
140
- let resolved = path.resolve(fromDirName, reference.path)
141
- if (!path.basename(resolved).endsWith('.json')) {
142
- resolved = path.join(resolved, 'tsconfig.json')
143
- }
144
- return toPosixFileName(resolved)
145
- })
146
- }
147
- return []
159
+ export function retrieveReferencedProjects(config: TsConfig, fromDirName: string): string[] {
160
+ return (config.references ?? []).map((reference) => {
161
+ let resolved = path.resolve(fromDirName, reference.path)
162
+ if (!path.basename(resolved).endsWith('.json')) {
163
+ resolved = path.join(resolved, 'tsconfig.json')
164
+ }
165
+ return toPosixFileName(resolved)
166
+ })
148
167
  }
149
168
 
150
169
  /**
@@ -4,6 +4,7 @@ import path from 'path'
4
4
  import type { Mutant, StrykerOptions } from '@stryker-mutator/api/core'
5
5
  import type { Logger } from '@stryker-mutator/api/logging'
6
6
  import { commonTokens, tokens } from '@stryker-mutator/api/plugin'
7
+ import { Either } from 'effect'
7
8
  import { type SourceFile, SyntaxKind } from 'typescript/unstable/ast'
8
9
  import type { FileSystem } from 'typescript/unstable/fs'
9
10
  import { API, type Diagnostic, type DocumentIdentifier, type Program, type Snapshot } from 'typescript/unstable/sync'
@@ -16,7 +17,7 @@ import {
16
17
  getSourceMappingURL,
17
18
  guardTSVersion,
18
19
  overrideOptions,
19
- parseConfigFileTextToJson,
20
+ parseTsConfig,
20
21
  retrieveReferencedProjects,
21
22
  toPosixFileName,
22
23
  } from './tsconfig-helpers.js'
@@ -205,16 +206,21 @@ export class TypescriptCompiler implements ITypescriptCompiler, IFileRelationCre
205
206
  processed.add(current)
206
207
 
207
208
  const content = readFileSync(current, 'utf-8')
208
- const parsed = parseConfigFileTextToJson(current, content)
209
- if (parsed.error) {
209
+ const parsed = parseTsConfig(current, content)
210
+ if (Either.isLeft(parsed)) {
211
+ this.log.warn(
212
+ `Could not parse tsconfig file "%s": %s. Compiler-option overrides and project-reference walking were skipped for this file, so mutants may be misreported as compile errors.`,
213
+ current,
214
+ parsed.left.reason,
215
+ )
210
216
  tsConfigOverrides.set(current, content)
211
217
  continue
212
218
  }
213
- tsConfigOverrides.set(current, overrideOptions(parsed, buildModeEnabled))
219
+ tsConfigOverrides.set(current, overrideOptions(parsed.right, buildModeEnabled))
214
220
 
215
221
  for (
216
222
  const referenced of retrieveReferencedProjects(
217
- parsed,
223
+ parsed.right,
218
224
  path.dirname(current),
219
225
  )
220
226
  ) {
@@ -0,0 +1,101 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+
5
+ import { CheckStatus } from '@stryker-mutator/api/check'
6
+ import type { Location, Mutant, StrykerOptions } from '@stryker-mutator/api/core'
7
+ import type { Logger } from '@stryker-mutator/api/logging'
8
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
9
+
10
+ import { HybridFileSystem } from '../../src/fs/hybrid-file-system.js'
11
+ import { TypescriptChecker } from '../../src/typescript-checker.js'
12
+ import { TypescriptCompiler } from '../../src/typescript-compiler.js'
13
+
14
+ const resolveTestResource = path.resolve.bind(
15
+ path,
16
+ path.dirname(fileURLToPath(import.meta.url)),
17
+ '..',
18
+ '..',
19
+ 'testResources',
20
+ 'nodenext-project',
21
+ ) as unknown as typeof path.resolve
22
+
23
+ function createLogger(): Logger {
24
+ return {
25
+ isTraceEnabled: () => false,
26
+ isDebugEnabled: () => false,
27
+ isInfoEnabled: () => false,
28
+ isWarnEnabled: () => false,
29
+ isErrorEnabled: () => false,
30
+ isFatalEnabled: () => false,
31
+ trace: () => {},
32
+ debug: () => {},
33
+ info: () => {},
34
+ warn: () => {},
35
+ error: () => {},
36
+ fatal: () => {},
37
+ }
38
+ }
39
+
40
+ function createChecker(tsconfigFile: string): TypescriptChecker {
41
+ const options = {
42
+ tsconfigFile,
43
+ typescriptChecker: { prioritizePerformanceOverAccuracy: true },
44
+ } as unknown as StrykerOptions
45
+ const logger = createLogger()
46
+ const fileSystem = new HybridFileSystem()
47
+ const compiler = new TypescriptCompiler(logger, options, fileSystem)
48
+ return new TypescriptChecker(logger, options, compiler)
49
+ }
50
+
51
+ const utilSource = fs.readFileSync(resolveTestResource('src', 'util.ts'), 'utf8')
52
+
53
+ function createMutant(
54
+ findText: string,
55
+ replacement: string,
56
+ id: string,
57
+ ): Mutant {
58
+ const lines = utilSource.split('\n')
59
+ const lineNumber = lines.findIndex((line) => line.includes(findText))
60
+ if (lineNumber === -1) {
61
+ throw new Error(`Cannot find ${findText} in util.ts`)
62
+ }
63
+ const column = lines[lineNumber]!.indexOf(findText)
64
+ const location: Location = {
65
+ start: { line: lineNumber, column },
66
+ end: { line: lineNumber, column: column + findText.length },
67
+ }
68
+ return {
69
+ id,
70
+ fileName: resolveTestResource('src', 'util.ts'),
71
+ mutatorName: 'test-mutator',
72
+ location,
73
+ replacement,
74
+ }
75
+ }
76
+
77
+ describe('Typescript checker on a NodeNext project targeting es2024', () => {
78
+ let sut: TypescriptChecker
79
+
80
+ beforeEach(() => {
81
+ sut = createChecker(resolveTestResource('tsconfig.json'))
82
+ return sut.init()
83
+ })
84
+
85
+ afterEach(() => {
86
+ // @ts-expect-error private close method
87
+ sut.tsCompiler.close()
88
+ })
89
+
90
+ it('should validate a mutant that keeps the project compiling', async () => {
91
+ const mutant = createMutant('value % 2 === 0', 'value % 2 !== 0', 'passing')
92
+ const actual = await sut.check([mutant])
93
+ expect(actual).toEqual({ passing: { status: CheckStatus.Passed } })
94
+ })
95
+
96
+ it('should invalidate a mutant that violates the declared return type', async () => {
97
+ const mutant = createMutant("'even'", '42', 'breaking')
98
+ const actual = await sut.check([mutant])
99
+ expect(actual['breaking']!.status).toBe(CheckStatus.CompileError)
100
+ })
101
+ })
@@ -6,9 +6,11 @@ import { CheckStatus } from '@stryker-mutator/api/check'
6
6
  import type { FailedCheckResult } from '@stryker-mutator/api/check'
7
7
  import type { Location, Mutant, StrykerOptions } from '@stryker-mutator/api/core'
8
8
  import type { Logger } from '@stryker-mutator/api/logging'
9
+ import { Either } from 'effect'
9
10
  import { afterEach, beforeEach, describe, expect, it } from 'vitest'
10
11
 
11
12
  import { HybridFileSystem } from '../../src/fs/hybrid-file-system.js'
13
+ import { overrideOptions, parseTsConfig } from '../../src/tsconfig-helpers.js'
12
14
  import { TypescriptChecker } from '../../src/typescript-checker.js'
13
15
  import { TypescriptCompiler } from '../../src/typescript-compiler.js'
14
16
 
@@ -204,6 +206,39 @@ describe('Typescript checker on a single project', () => {
204
206
  'errorInFileAbove2Mutants/todo-counter.ts(7,7): error TS2322',
205
207
  )
206
208
  })
209
+
210
+ it('should compile the fixture source files discovered via the ** include glob', async () => {
211
+ // The fixture tsconfig selects its inputs via `include: ["src/**/*.ts"]`. If a comment
212
+ // stripper corrupted the glob (the `/**/` inside it looks like an empty block comment),
213
+ // TypeScript would find no input files and abort init with TS18003, so this assertion
214
+ // pins the end-to-end regression against the real checker path.
215
+ const mutant = createMutant(
216
+ 'errorInFileAbove2Mutants/todo.ts',
217
+ 'TodoList.allTodos.push(newItem)',
218
+ '"This should not be a string 🙄"',
219
+ 'glob-pin',
220
+ )
221
+ const actual = await sut.check([mutant])
222
+ expect(actual['glob-pin']?.status).toBe(CheckStatus.CompileError)
223
+ expect((actual['glob-pin'] as FailedCheckResult).reason).toContain(
224
+ 'todo.ts(15,9): error TS2322',
225
+ )
226
+ })
227
+
228
+ it('should preserve the fixture $schema URL and ** glob in the config produced by overrideOptions', () => {
229
+ // `$schema` holds a `//`-bearing string and `include` holds a `**` glob: the two shapes a
230
+ // naive comment stripper mangles. The compiler feeds the output of `overrideOptions` to
231
+ // TypeScript, so this asserts both survive byte-for-byte into that config.
232
+ const tsconfigFile = resolveTestResource('tsconfig.json')
233
+ const content = fs.readFileSync(tsconfigFile, 'utf8')
234
+ const parsed = parseTsConfig(tsconfigFile, content)
235
+ if (Either.isLeft(parsed)) {
236
+ throw new Error(`Expected fixture tsconfig to parse, got: ${parsed.left.reason}`)
237
+ }
238
+ const config = overrideOptions(parsed.right, false)
239
+ expect(config).toContain('"$schema":"https://json.schemastore.org/tsconfig"')
240
+ expect(config).toContain('"include":["src/**/*.ts"]')
241
+ })
207
242
  })
208
243
 
209
244
  const fileContents: Record<string, string> = Object.freeze({
@@ -0,0 +1,218 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'fs'
2
+ import { tmpdir } from 'os'
3
+ import path from 'path'
4
+
5
+ import { Either } from 'effect'
6
+ import { afterEach, describe, expect, it } from 'vitest'
7
+
8
+ import {
9
+ determineBuildModeEnabled,
10
+ overrideOptions,
11
+ parseTsConfig,
12
+ TsConfigParseError,
13
+ } from '../../src/tsconfig-helpers.js'
14
+
15
+ function expectRight<A, E>(either: Either.Either<A, E>): A {
16
+ if (Either.isLeft(either)) {
17
+ throw new Error(`Expected a Right result, got a Left: ${String(either.left)}`)
18
+ }
19
+ return either.right
20
+ }
21
+
22
+ describe('parseTsConfig', () => {
23
+ it('should preserve a glob pattern in include exactly', () => {
24
+ const config = expectRight(
25
+ parseTsConfig('tsconfig.json', '{"include":["src/**/*.workflow.ts"]}'),
26
+ )
27
+ expect(config).toEqual({ include: ['src/**/*.workflow.ts'] })
28
+ })
29
+
30
+ it('should round-trip a $schema URL unchanged', () => {
31
+ const config = expectRight(
32
+ parseTsConfig('tsconfig.json', '{"$schema":"https://json.schemastore.org/tsconfig"}'),
33
+ )
34
+ expect(config).toEqual({
35
+ $schema: 'https://json.schemastore.org/tsconfig',
36
+ })
37
+ })
38
+
39
+ it('should round-trip a Windows path and an escaped quote unchanged', () => {
40
+ const config = expectRight(
41
+ parseTsConfig(
42
+ 'tsconfig.json',
43
+ '{"paths":{"a":["C:\\\\x//y"]},"description":"he said \\"hi\\""}',
44
+ ),
45
+ )
46
+ expect(config).toEqual({
47
+ paths: { a: ['C:\\x//y'] },
48
+ description: 'he said "hi"',
49
+ })
50
+ })
51
+
52
+ it('should accept trailing commas in nested objects and arrays', () => {
53
+ const config = expectRight(
54
+ parseTsConfig(
55
+ 'tsconfig.json',
56
+ '{"compilerOptions":{"strict":true,},"include":["a.ts","b.ts",]}',
57
+ ),
58
+ )
59
+ expect(config).toEqual({
60
+ compilerOptions: { strict: true },
61
+ include: ['a.ts', 'b.ts'],
62
+ })
63
+ })
64
+
65
+ it('should accept content prefixed with a BOM', () => {
66
+ const withoutBom = expectRight(
67
+ parseTsConfig('tsconfig.json', '{"compilerOptions":{"strict":true}}'),
68
+ )
69
+ const withBom = expectRight(
70
+ parseTsConfig('tsconfig.json', '\uFEFF{"compilerOptions":{"strict":true}}'),
71
+ )
72
+ expect(withBom).toEqual(withoutBom)
73
+ })
74
+
75
+ it('should strip line and block comments', () => {
76
+ const config = expectRight(
77
+ parseTsConfig(
78
+ 'tsconfig.json',
79
+ '{\n// a line comment\n"a": 1,\n/* a block comment */\n"b": 2\n}',
80
+ ),
81
+ )
82
+ expect(config).toEqual({ a: 1, b: 2 })
83
+ })
84
+
85
+ it('should preserve unknown nested keys inside compilerOptions', () => {
86
+ const config = expectRight(
87
+ parseTsConfig(
88
+ 'tsconfig.json',
89
+ '{"compilerOptions":{"strict":true,"experimentalDecorators":true}}',
90
+ ),
91
+ )
92
+ expect(config.compilerOptions).toEqual({
93
+ strict: true,
94
+ experimentalDecorators: true,
95
+ })
96
+ })
97
+
98
+ it('should return a Left for a malformed document', () => {
99
+ const result = parseTsConfig('tsconfig.json', '{ "a": }')
100
+ expect(Either.isLeft(result)).toBe(true)
101
+ if (Either.isLeft(result)) {
102
+ expect(result.left).toBeInstanceOf(TsConfigParseError)
103
+ expect(result.left.file).toBe('tsconfig.json')
104
+ expect(result.left.reason.length).toBeGreaterThan(0)
105
+ }
106
+ })
107
+
108
+ it('should return a Left for truncated input', () => {
109
+ for (const truncated of ['{', '{"a":']) {
110
+ expect(Either.isLeft(parseTsConfig('tsconfig.json', truncated))).toBe(true)
111
+ }
112
+ })
113
+
114
+ it('should return a Left for an unterminated block comment', () => {
115
+ const result = parseTsConfig('tsconfig.json', '{"a":1,/* never closed')
116
+ expect(Either.isLeft(result)).toBe(true)
117
+ })
118
+
119
+ it('should return a Left for a bare string root (not a config object)', () => {
120
+ const result = parseTsConfig('tsconfig.json', '"just a string"')
121
+ expect(Either.isLeft(result)).toBe(true)
122
+ if (Either.isLeft(result)) {
123
+ expect(result.left).toBeInstanceOf(TsConfigParseError)
124
+ expect(result.left.file).toBe('tsconfig.json')
125
+ expect(result.left.reason.length).toBeGreaterThan(0)
126
+ }
127
+ })
128
+
129
+ it('should return a Left when references is not an array', () => {
130
+ const result = parseTsConfig('tsconfig.json', '{"references":"nope"}')
131
+ expect(Either.isLeft(result)).toBe(true)
132
+ if (Either.isLeft(result)) {
133
+ expect(result.left).toBeInstanceOf(TsConfigParseError)
134
+ expect(result.left.reason.length).toBeGreaterThan(0)
135
+ }
136
+ })
137
+ })
138
+
139
+ describe('overrideOptions', () => {
140
+ it('should apply the compiler-option overrides to an empty config', () => {
141
+ const parsed = JSON.parse(overrideOptions({}, false))
142
+ expect(parsed.compilerOptions).toMatchObject({
143
+ allowUnreachableCode: true,
144
+ noEmit: true,
145
+ })
146
+ })
147
+
148
+ it('should not inject a moduleResolution that contradicts the consumer module', () => {
149
+ const config = expectRight(
150
+ parseTsConfig('tsconfig.json', '{"compilerOptions":{"module":"NodeNext"}}'),
151
+ )
152
+ const output = JSON.parse(overrideOptions(config, false))
153
+ expect(output.compilerOptions.module).toBe('NodeNext')
154
+ expect(output.compilerOptions.moduleResolution).toBeUndefined()
155
+ })
156
+
157
+ it('should preserve the consumer target and moduleResolution verbatim', () => {
158
+ const config = expectRight(
159
+ parseTsConfig(
160
+ 'tsconfig.json',
161
+ '{"compilerOptions":{"target":"es2024","moduleResolution":"NodeNext"}}',
162
+ ),
163
+ )
164
+ const output = JSON.parse(overrideOptions(config, false))
165
+ expect(output.compilerOptions.target).toBe('es2024')
166
+ expect(output.compilerOptions.moduleResolution).toBe('NodeNext')
167
+ })
168
+
169
+ it('should preserve unknown top-level keys and unknown compilerOptions through to the output', () => {
170
+ const config = expectRight(
171
+ parseTsConfig(
172
+ 'tsconfig.json',
173
+ '{"include":["src/**/*.ts"],"compilerOptions":{"strict":true,"experimentalDecorators":true}}',
174
+ ),
175
+ )
176
+ const output = JSON.parse(overrideOptions(config, false))
177
+ expect(output.include).toEqual(['src/**/*.ts'])
178
+ expect(output.compilerOptions.strict).toBe(true)
179
+ expect(output.compilerOptions.experimentalDecorators).toBe(true)
180
+ })
181
+ })
182
+
183
+ describe('determineBuildModeEnabled', () => {
184
+ let tempDir: string | undefined
185
+
186
+ afterEach(() => {
187
+ if (tempDir !== undefined) {
188
+ rmSync(tempDir, { recursive: true, force: true })
189
+ }
190
+ })
191
+
192
+ function createTsconfig(content: string): string {
193
+ tempDir = mkdtempSync(path.join(tmpdir(), 'tsconfig-helpers-'))
194
+ const tsconfigPath = path.join(tempDir, 'tsconfig.json')
195
+ writeFileSync(tsconfigPath, content)
196
+ return tsconfigPath
197
+ }
198
+
199
+ it('should return true when the config has references', () => {
200
+ const tsconfigPath = createTsconfig('{"references":[{"path":"./a"}]}')
201
+ expect(determineBuildModeEnabled(tsconfigPath)).toBe(true)
202
+ })
203
+
204
+ it('should return false when the config has no references', () => {
205
+ const tsconfigPath = createTsconfig('{"compilerOptions":{"strict":true}}')
206
+ expect(determineBuildModeEnabled(tsconfigPath)).toBe(false)
207
+ })
208
+
209
+ it('should return false when the config is a bare string instead of throwing a TypeError', () => {
210
+ const tsconfigPath = createTsconfig('"just a string"')
211
+ expect(determineBuildModeEnabled(tsconfigPath)).toBe(false)
212
+ })
213
+
214
+ it('should return false when references is not an array', () => {
215
+ const tsconfigPath = createTsconfig('{"references":"nope"}')
216
+ expect(determineBuildModeEnabled(tsconfigPath)).toBe(false)
217
+ })
218
+ })
@@ -3,7 +3,7 @@ import { fileURLToPath } from 'url'
3
3
 
4
4
  import type { StrykerOptions } from '@stryker-mutator/api/core'
5
5
  import type { Logger } from '@stryker-mutator/api/logging'
6
- import { describe, expect, it } from 'vitest'
6
+ import { describe, expect, it, vi } from 'vitest'
7
7
 
8
8
  import { HybridFileSystem } from '../../src/fs/hybrid-file-system.js'
9
9
  import { TypescriptChecker } from '../../src/typescript-checker.js'
@@ -18,7 +18,7 @@ const resolveTestResource = path.resolve.bind(
18
18
  'errors',
19
19
  ) as unknown as typeof path.resolve
20
20
 
21
- function createLogger(): Logger {
21
+ function createLogger(warn: Logger['warn'] = () => {}): Logger {
22
22
  return {
23
23
  isTraceEnabled: () => false,
24
24
  isDebugEnabled: () => false,
@@ -29,18 +29,20 @@ function createLogger(): Logger {
29
29
  trace: () => {},
30
30
  debug: () => {},
31
31
  info: () => {},
32
- warn: () => {},
32
+ warn,
33
33
  error: () => {},
34
34
  fatal: () => {},
35
35
  }
36
36
  }
37
37
 
38
- function createChecker(tsconfigFile: string): TypescriptChecker {
38
+ function createChecker(
39
+ tsconfigFile: string,
40
+ logger: Logger = createLogger(),
41
+ ): TypescriptChecker {
39
42
  const options = {
40
43
  tsconfigFile,
41
44
  typescriptChecker: { prioritizePerformanceOverAccuracy: true },
42
45
  } as unknown as StrykerOptions
43
- const logger = createLogger()
44
46
  const fileSystem = new HybridFileSystem()
45
47
  const compiler = new TypescriptCompiler(logger, options, fileSystem)
46
48
  return new TypescriptChecker(logger, options, compiler)
@@ -57,7 +59,7 @@ describe('Typescript checker errors', () => {
57
59
  await expect(sut.init()).rejects.toThrow(
58
60
  'testResources/errors/compile-error/add.ts(2,3): error TS2322:',
59
61
  )
60
- })
62
+ }, 30_000)
61
63
 
62
64
  it('should reject initialization if tsconfig was invalid', async () => {
63
65
  const sut = createChecker(
@@ -71,6 +73,29 @@ describe('Typescript checker errors', () => {
71
73
  )
72
74
  })
73
75
 
76
+ it('should log a warning naming the tsconfig path and the skipped-overrides consequence when parsing falls back', async () => {
77
+ const warn = vi.fn()
78
+ const sut = createChecker(
79
+ resolveTestResource('invalid-tsconfig', 'tsconfig.json'),
80
+ createLogger(warn),
81
+ )
82
+
83
+ await expect(sut.init()).rejects.toThrow(
84
+ 'testResources/errors/invalid-tsconfig/tsconfig.json(1,1): error TS1005:',
85
+ )
86
+
87
+ expect(warn).toHaveBeenCalled()
88
+ const warnings = warn.mock.calls
89
+ .map((call) => call.join(' '))
90
+ .join('\n')
91
+ expect(warnings).toContain(
92
+ 'testResources/errors/invalid-tsconfig/tsconfig.json',
93
+ )
94
+ expect(warnings.toLowerCase()).toContain(
95
+ 'compiler-option overrides and project-reference walking were skipped',
96
+ )
97
+ })
98
+
74
99
  it("should reject when tsconfig file doesn't exist", async () => {
75
100
  const sut = createChecker(
76
101
  resolveTestResource('empty-dir', 'tsconfig.json'),
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "ES5",
4
3
  "types": []
5
4
  }
6
5
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "nodenext-project",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module"
6
+ }
@@ -0,0 +1,5 @@
1
+ import { evenOdd } from './util.js'
2
+
3
+ const numbers = [1, 2, 3, 4]
4
+
5
+ export const groupedWithEs2024ObjectGroupBy = Object.groupBy(numbers, evenOdd)
@@ -0,0 +1,3 @@
1
+ export function evenOdd(value: number): string {
2
+ return value % 2 === 0 ? 'even' : 'odd'
3
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "module": "NodeNext",
6
+
7
+ // `moduleResolution` is deliberately absent and `target` is deliberately newer
8
+ // than es2022: the checker must not supply either. See nodenext-project.it.spec.ts.
9
+ "target": "es2024",
10
+
11
+ "verbatimModuleSyntax": true,
12
+ "outDir": "dist",
13
+ "types": []
14
+ },
15
+ "include": ["src/**/*.ts"]
16
+ }
@@ -1,8 +1,6 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "strict": true,
4
- "target": "es5",
5
- "moduleResolution": "node",
6
4
  "module": "commonjs",
7
5
  "composite": true,
8
6
  "declaration": true,
@@ -1,8 +1,6 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "strict": true,
4
- "target": "es5",
5
- "moduleResolution": "node",
6
4
  "module": "commonjs",
7
5
  "outDir": "dist",
8
6
 
@@ -1,8 +1,7 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
2
3
  "compilerOptions": {
3
4
  "strict": true,
4
- "target": "es5",
5
- "moduleResolution": "node",
6
5
  "module": "commonjs",
7
6
  "outDir": "dist",
8
7
 
@@ -11,5 +10,6 @@
11
10
  "noUnusedParameters": true,
12
11
 
13
12
  "types": []
14
- }
13
+ },
14
+ "include": ["src/**/*.ts"]
15
15
  }
package/tsconfig.json CHANGED
@@ -3,8 +3,24 @@
3
3
  "compilerOptions": {
4
4
  "outDir": "dist",
5
5
  "rootDir": ".",
6
- "types": ["node"]
6
+ "types": [
7
+ "node"
8
+ ],
9
+ // Fork of @stryker-mutator/typescript-checker: the leaf AGENTS.md mandates a minimal upstream
10
+ // diff, so the constitution policy in @systemfsoftware/tsconfig/effect is deliberately NOT
11
+ // extended here. The Effect Language Service runs at its own defaults — correctness tier only.
12
+ "plugins": [
13
+ {
14
+ "name": "@effect/language-service"
15
+ }
16
+ ]
7
17
  },
8
- "include": ["src", "test"],
9
- "exclude": ["node_modules", "dist"]
18
+ "include": [
19
+ "src",
20
+ "test"
21
+ ],
22
+ "exclude": [
23
+ "node_modules",
24
+ "dist"
25
+ ]
10
26
  }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'tsdown'
2
+
3
+ export default defineConfig({
4
+ entry: {
5
+ index: './src/index.ts',
6
+ },
7
+ format: 'esm',
8
+ dts: true,
9
+ exports: { devExports: '@systemfsoftware/source' },
10
+ // `jsr:` specs publish as `npm:@jsr/std__jsonc@…`, which exists only on npm.jsr.io, so a
11
+ // default-registry consumer cannot install it. Inlining the parser (a leaf, no runtime
12
+ // imports) keeps the published artifact free of any `@jsr` dep. Dropping this line
13
+ // reintroduces an uninstallable release — verified by packing and installing the tarball.
14
+ noExternal: ['@std/jsonc'],
15
+ clean: false,
16
+ })
package/vitest.config.ts CHANGED
@@ -7,5 +7,7 @@ export default defineConfig({
7
7
  '**/.stryker-tmp/**',
8
8
  '**/testResources/**',
9
9
  ],
10
+ testTimeout: 30000,
11
+ hookTimeout: 30000,
10
12
  },
11
13
  })
@@ -1,3 +0,0 @@
1
- $ oxlint . --format=github
2
- Found 0 warnings and 0 errors.
3
- Finished in 103ms on 21 files with 94 rules using 12 threads.
@@ -1 +0,0 @@
1
- {"program":{"fileNames":["../../../../../node_modules/typescript/lib/lib.d.ts","../../../../../node_modules/typescript/lib/lib.es5.d.ts","../../../../../node_modules/typescript/lib/lib.dom.d.ts","../../../../../node_modules/typescript/lib/lib.webworker.importscripts.d.ts","../../../../../node_modules/typescript/lib/lib.scripthost.d.ts","../dist/utils/math.d.ts","./index.ts","../dist/utils/text.d.ts","./job.ts"],"fileInfos":["2dc8c927c9c162a773c6bb3cdc4f3286c23f10eedc67414028f9cb5951610f60",{"version":"f20c05dbfe50a208301d2a1da37b9931bce0466eb5a1f4fe240971b4ecc82b67","affectsGlobalScope":true},{"version":"9b087de7268e4efc5f215347a62656663933d63c0b1d7b624913240367b999ea","affectsGlobalScope":true},{"version":"7fac8cb5fc820bc2a59ae11ef1c5b38d3832c6d0dfaec5acdb5569137d09a481","affectsGlobalScope":true},{"version":"097a57355ded99c68e6df1b738990448e0bf170e606707df5a7c0481ff2427cd","affectsGlobalScope":true},"80dbf481ae698a44d6d4b60f3c36d84a94b2a5eb14927eae5347b82f33ec0277",{"version":"c9b6bdd48b8bdb8d8e7690c7cc18897a494b6ab17dc58083dacfaf14b846ab4f","signature":"40b6409b8d0dced1f6c3964012b7a7c1cd50e24c3242095d1c8cfc6cabe8bd31"},"cdf6a65d46d64de68df5d8a322621f74327b1ee02c3fde41f736e11d307fcfb1",{"version":"e4c28c497fe6cc6364b113c181c32ba58e70f02d824295e72b15d9570b403104","signature":"9be66c79f48b4876970daed5167e069d7f12f1a1ca616ecaa0ca8280946344ca"}],"options":{"composite":true,"declaration":true,"declarationMap":true,"module":1,"noUnusedLocals":true,"noUnusedParameters":true,"outDir":"../dist/src","strict":true,"target":1,"tsBuildInfoFile":"./src.tsbuildinfo"},"fileIdsList":[[6],[8]],"referencedMap":[[7,1],[9,2]],"exportedModulesMap":[],"semanticDiagnosticsPerFile":[1,3,2,5,4,6,8,7,9],"latestChangedDtsFile":"../dist/src/job.d.ts"},"version":"4.8.4"}
@@ -1 +0,0 @@
1
- {"program":{"fileNames":["../../../../../node_modules/typescript/lib/lib.d.ts","../../../../../node_modules/typescript/lib/lib.es5.d.ts","../../../../../node_modules/typescript/lib/lib.dom.d.ts","../../../../../node_modules/typescript/lib/lib.webworker.importscripts.d.ts","../../../../../node_modules/typescript/lib/lib.scripthost.d.ts","./math.ts","./text.ts"],"fileInfos":["2dc8c927c9c162a773c6bb3cdc4f3286c23f10eedc67414028f9cb5951610f60",{"version":"f20c05dbfe50a208301d2a1da37b9931bce0466eb5a1f4fe240971b4ecc82b67","affectsGlobalScope":true},{"version":"9b087de7268e4efc5f215347a62656663933d63c0b1d7b624913240367b999ea","affectsGlobalScope":true},{"version":"7fac8cb5fc820bc2a59ae11ef1c5b38d3832c6d0dfaec5acdb5569137d09a481","affectsGlobalScope":true},{"version":"097a57355ded99c68e6df1b738990448e0bf170e606707df5a7c0481ff2427cd","affectsGlobalScope":true},{"version":"6198e7d4a43aabb174a72ec9f0e8d2962912ad59ad90010aac3930868a8f62a4","signature":"0400cb85cef49e897c47df13e38b5cd199e0c900253f2d2ddf2e3491c27bc0a8"},{"version":"becd081df112726ab94c1ca1c05d6a59268fe0dabf7ad076d16ea851bf99e8fb","signature":"6039d94241358544e8d62a3a0ba90752a9973b3b2b422c187e2bcf7256fcda2e"}],"options":{"composite":true,"declaration":true,"declarationMap":true,"module":1,"noUnusedLocals":true,"noUnusedParameters":true,"outDir":"../dist/utils","strict":true,"target":1,"tsBuildInfoFile":"./utils.tsbuildinfo"},"referencedMap":[],"exportedModulesMap":[],"semanticDiagnosticsPerFile":[1,3,2,5,4,6,7],"latestChangedDtsFile":"../dist/utils/text.d.ts"},"version":"4.8.4"}
@@ -1 +0,0 @@
1
- {"program":{"fileNames":["../../../../node_modules/typescript/lib/lib.d.ts","../../../../node_modules/typescript/lib/lib.es5.d.ts","../../../../node_modules/typescript/lib/lib.dom.d.ts","../../../../node_modules/typescript/lib/lib.webworker.importscripts.d.ts","../../../../node_modules/typescript/lib/lib.scripthost.d.ts","./src/index.ts"],"fileInfos":["2dc8c927c9c162a773c6bb3cdc4f3286c23f10eedc67414028f9cb5951610f60",{"version":"f20c05dbfe50a208301d2a1da37b9931bce0466eb5a1f4fe240971b4ecc82b67","affectsGlobalScope":true},{"version":"9b087de7268e4efc5f215347a62656663933d63c0b1d7b624913240367b999ea","affectsGlobalScope":true},{"version":"7fac8cb5fc820bc2a59ae11ef1c5b38d3832c6d0dfaec5acdb5569137d09a481","affectsGlobalScope":true},{"version":"097a57355ded99c68e6df1b738990448e0bf170e606707df5a7c0481ff2427cd","affectsGlobalScope":true},"39441a0f0f37ba8f1a5ef3bf717953d53469cfd7a8450a8a2221959a67905497"],"options":{"module":1,"noUnusedLocals":true,"noUnusedParameters":true,"outDir":"./dist","strict":true,"target":1,"tsBuildInfoFile":"./do-not-delete.tsbuildinfo"},"referencedMap":[],"exportedModulesMap":[],"semanticDiagnosticsPerFile":[1,3,2,5,4,6]},"version":"4.8.4"}