@systemfsoftware/stryker-js-typescript-checker 0.1.0 → 1.2.2
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/.turbo/turbo-build.log +20 -14
- package/AGENTS.md +7 -0
- package/dist/index.mjs +269 -22
- package/package.json +16 -7
- package/src/tsconfig-helpers.ts +61 -42
- package/src/typescript-compiler.ts +11 -5
- package/test/integration/single-project.it.spec.ts +35 -0
- package/test/integration/tsconfig-helpers.it.spec.ts +199 -0
- package/test/integration/typescript-checkers-errors.it.spec.ts +31 -6
- package/testResources/single-project/tsconfig.json +3 -1
- package/tsconfig.json +19 -3
- package/tsdown.config.ts +16 -0
- package/vitest.config.ts +2 -0
- package/.turbo/turbo-lint.log +0 -3
- package/testResources/project-references/src/src.tsbuildinfo +0 -1
- package/testResources/project-references/utils/utils.tsbuildinfo +0 -1
- package/testResources/project-with-ts-buildinfo/do-not-delete.tsbuildinfo +0 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
$ tsdown
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
[34mℹ[39m [34mtsdown v0.22.9[39m powered by [38;2;255;126;23mrolldown v1.2.0[39m
|
|
3
|
+
[34mℹ[39m config file: [4m/home/runner/work/systemfsoftware/systemfsoftware/packages/stryker-js/typescript-checker/tsdown.config.ts[24m
|
|
4
|
+
[34mℹ[39m entry: [34m./src/index.ts[39m
|
|
5
|
+
|
|
6
|
+
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
7
|
+
[34mℹ[39m Build start
|
|
8
|
+
[43m WARN [49m `noExternal` is deprecated. Use `deps.alwaysBundle` instead.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
[43m WARN [49m TypeScript 7.0 does not yet have a stable API and is experimental. Some options will be unavailable.
|
|
12
|
+
|
|
13
|
+
[34mℹ[39m Emit types with typescript@7.0.2
|
|
14
|
+
[34mℹ[39m Hint: consider adding [34mdeps.onlyBundle[39m option to avoid unintended bundling of dependencies, or set [34mdeps.onlyBundle: false[39m to disable this hint.
|
|
15
|
+
See more at [4mhttps://tsdown.dev/options/dependencies#deps-onlybundle[24m
|
|
16
|
+
Detected dependencies in bundle:
|
|
17
|
+
- [34m@jsr/std__jsonc[39m
|
|
18
|
+
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m31.56 kB[22m [2m│ gzip: 8.67 kB[22m
|
|
19
|
+
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 4.66 kB[22m [2m│ gzip: 1.52 kB[22m
|
|
20
|
+
[34mℹ[39m 2 files, total: 36.22 kB
|
|
21
|
+
[32m✔[39m Build complete in [32m561ms[39m
|
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,29 +260,54 @@ 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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
46
|
-
return
|
|
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 {
|
|
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 =
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
307
|
+
* @param config The parsed config file
|
|
59
308
|
* @param useBuildMode whether or not `--build` mode is used
|
|
60
309
|
*/
|
|
61
|
-
function overrideOptions(
|
|
62
|
-
const config = parsedConfig.config ?? {};
|
|
310
|
+
function overrideOptions(config, useBuildMode) {
|
|
63
311
|
const compilerOptions = {
|
|
64
312
|
...config.compilerOptions,
|
|
65
313
|
...COMPILER_OPTIONS_OVERRIDES,
|
|
@@ -82,17 +330,15 @@ function overrideOptions(parsedConfig, useBuildMode) {
|
|
|
82
330
|
}
|
|
83
331
|
/**
|
|
84
332
|
* Retrieves the referenced config files based on parsed configuration
|
|
85
|
-
* @param
|
|
333
|
+
* @param config The parsed config file
|
|
86
334
|
* @param fromDirName The directory where to resolve from
|
|
87
335
|
*/
|
|
88
|
-
function retrieveReferencedProjects(
|
|
89
|
-
|
|
90
|
-
if (Array.isArray(config?.references)) return config.references.map((reference) => {
|
|
336
|
+
function retrieveReferencedProjects(config, fromDirName) {
|
|
337
|
+
return (config.references ?? []).map((reference) => {
|
|
91
338
|
let resolved = path.resolve(fromDirName, reference.path);
|
|
92
339
|
if (!path.basename(resolved).endsWith(".json")) resolved = path.join(resolved, "tsconfig.json");
|
|
93
340
|
return toPosixFileName(resolved);
|
|
94
341
|
});
|
|
95
|
-
return [];
|
|
96
342
|
}
|
|
97
343
|
/**
|
|
98
344
|
* Replaces backslashes with forward slashes (used by typescript)
|
|
@@ -393,13 +639,14 @@ var TypescriptCompiler = class {
|
|
|
393
639
|
if (!current || processed.has(current)) continue;
|
|
394
640
|
processed.add(current);
|
|
395
641
|
const content = readFileSync(current, "utf-8");
|
|
396
|
-
const parsed =
|
|
397
|
-
if (parsed
|
|
642
|
+
const parsed = parseTsConfig(current, content);
|
|
643
|
+
if (Either.isLeft(parsed)) {
|
|
644
|
+
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
645
|
tsConfigOverrides.set(current, content);
|
|
399
646
|
continue;
|
|
400
647
|
}
|
|
401
|
-
tsConfigOverrides.set(current, overrideOptions(parsed, buildModeEnabled));
|
|
402
|
-
for (const referenced of retrieveReferencedProjects(parsed, path.dirname(current))) {
|
|
648
|
+
tsConfigOverrides.set(current, overrideOptions(parsed.right, buildModeEnabled));
|
|
649
|
+
for (const referenced of retrieveReferencedProjects(parsed.right, path.dirname(current))) {
|
|
403
650
|
this.allTSConfigFiles.add(referenced);
|
|
404
651
|
toProcess.push(referenced);
|
|
405
652
|
}
|
package/package.json
CHANGED
|
@@ -1,32 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@systemfsoftware/stryker-js-typescript-checker",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.2",
|
|
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.
|
|
31
|
+
"tsdown": "^0.22.9",
|
|
26
32
|
"vitest": "^4",
|
|
27
|
-
"@systemfsoftware/tsconfig": "^1.
|
|
33
|
+
"@systemfsoftware/tsconfig": "^1.3.0",
|
|
28
34
|
"@systemfsoftware/vitest-config": "^0.1.0"
|
|
29
35
|
},
|
|
36
|
+
"inlinedDependencies": {
|
|
37
|
+
"@jsr/std__jsonc": "1.0.2"
|
|
38
|
+
},
|
|
30
39
|
"scripts": {
|
|
31
40
|
"clean": "rimraf dist",
|
|
32
41
|
"build": "tsdown",
|
package/src/tsconfig-helpers.ts
CHANGED
|
@@ -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,46 +52,71 @@ export function guardTSVersion(version = getTSVersion()): void {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
|
-
*
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.
|
|
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
|
-
|
|
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
|
|
70
|
-
|
|
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
|
|
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 =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
116
|
+
* @param config The parsed config file
|
|
89
117
|
* @param useBuildMode whether or not `--build` mode is used
|
|
90
118
|
*/
|
|
91
|
-
export function overrideOptions(
|
|
92
|
-
const config = (parsedConfig.config ?? {}) as { compilerOptions?: Record<string, unknown> }
|
|
119
|
+
export function overrideOptions(config: TsConfig, useBuildMode: boolean): string {
|
|
93
120
|
const compilerOptions: Record<string, unknown> = {
|
|
94
121
|
...config.compilerOptions,
|
|
95
122
|
...COMPILER_OPTIONS_OVERRIDES,
|
|
@@ -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
|
|
156
|
+
* @param config The parsed config file
|
|
134
157
|
* @param fromDirName The directory where to resolve from
|
|
135
158
|
*/
|
|
136
|
-
export function retrieveReferencedProjects(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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 =
|
|
209
|
-
if (parsed
|
|
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
|
) {
|
|
@@ -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,199 @@
|
|
|
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
|
+
target: 'es2022',
|
|
146
|
+
moduleResolution: 'bundler',
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should preserve unknown top-level keys and unknown compilerOptions through to the output', () => {
|
|
151
|
+
const config = expectRight(
|
|
152
|
+
parseTsConfig(
|
|
153
|
+
'tsconfig.json',
|
|
154
|
+
'{"include":["src/**/*.ts"],"compilerOptions":{"strict":true,"experimentalDecorators":true}}',
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
const output = JSON.parse(overrideOptions(config, false))
|
|
158
|
+
expect(output.include).toEqual(['src/**/*.ts'])
|
|
159
|
+
expect(output.compilerOptions.strict).toBe(true)
|
|
160
|
+
expect(output.compilerOptions.experimentalDecorators).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('determineBuildModeEnabled', () => {
|
|
165
|
+
let tempDir: string | undefined
|
|
166
|
+
|
|
167
|
+
afterEach(() => {
|
|
168
|
+
if (tempDir !== undefined) {
|
|
169
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
function createTsconfig(content: string): string {
|
|
174
|
+
tempDir = mkdtempSync(path.join(tmpdir(), 'tsconfig-helpers-'))
|
|
175
|
+
const tsconfigPath = path.join(tempDir, 'tsconfig.json')
|
|
176
|
+
writeFileSync(tsconfigPath, content)
|
|
177
|
+
return tsconfigPath
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
it('should return true when the config has references', () => {
|
|
181
|
+
const tsconfigPath = createTsconfig('{"references":[{"path":"./a"}]}')
|
|
182
|
+
expect(determineBuildModeEnabled(tsconfigPath)).toBe(true)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should return false when the config has no references', () => {
|
|
186
|
+
const tsconfigPath = createTsconfig('{"compilerOptions":{"strict":true}}')
|
|
187
|
+
expect(determineBuildModeEnabled(tsconfigPath)).toBe(false)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should return false when the config is a bare string instead of throwing a TypeError', () => {
|
|
191
|
+
const tsconfigPath = createTsconfig('"just a string"')
|
|
192
|
+
expect(determineBuildModeEnabled(tsconfigPath)).toBe(false)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should return false when references is not an array', () => {
|
|
196
|
+
const tsconfigPath = createTsconfig('{"references":"nope"}')
|
|
197
|
+
expect(determineBuildModeEnabled(tsconfigPath)).toBe(false)
|
|
198
|
+
})
|
|
199
|
+
})
|
|
@@ -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(
|
|
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'),
|
package/tsconfig.json
CHANGED
|
@@ -3,8 +3,24 @@
|
|
|
3
3
|
"compilerOptions": {
|
|
4
4
|
"outDir": "dist",
|
|
5
5
|
"rootDir": ".",
|
|
6
|
-
"types": [
|
|
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": [
|
|
9
|
-
|
|
18
|
+
"include": [
|
|
19
|
+
"src",
|
|
20
|
+
"test"
|
|
21
|
+
],
|
|
22
|
+
"exclude": [
|
|
23
|
+
"node_modules",
|
|
24
|
+
"dist"
|
|
25
|
+
]
|
|
10
26
|
}
|
package/tsdown.config.ts
ADDED
|
@@ -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
package/.turbo/turbo-lint.log
DELETED
|
@@ -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"}
|