react-doctor 0.0.42 → 0.0.45
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/README.md +170 -42
- package/bin/react-doctor.js +13 -0
- package/dist/{process-browser-diagnostics-BHiLPUJT.js → browser-BOxs7MrK.js} +35 -21
- package/dist/{browser-DFbjNpPb.d.ts → browser-Dcq3yn-p.d.ts} +18 -3
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +1436 -499
- package/dist/index.d.ts +119 -12
- package/dist/index.js +1136 -327
- package/dist/react-doctor-plugin.js +2335 -127
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +2 -2
- package/package.json +36 -13
- package/dist/browser-DFbjNpPb.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/process-browser-diagnostics-BHiLPUJT.js.map +0 -1
- package/dist/react-doctor-plugin.d.ts.map +0 -1
- package/dist/react-doctor-plugin.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
+
import pc from "picocolors";
|
|
5
6
|
import { main } from "knip";
|
|
6
7
|
import { createOptions } from "knip/session";
|
|
7
8
|
import os from "node:os";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
//#region src/
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
//#region src/constants.ts
|
|
11
|
+
const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
|
|
12
|
+
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
13
|
+
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
14
|
+
const FETCH_TIMEOUT_MS = 1e4;
|
|
15
|
+
const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
16
|
+
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
17
|
+
const ERROR_RULE_PENALTY = 1.5;
|
|
18
|
+
const WARNING_RULE_PENALTY = .75;
|
|
19
|
+
const KNIP_CONFIG_LOCATIONS = [
|
|
20
|
+
"knip.json",
|
|
21
|
+
"knip.jsonc",
|
|
22
|
+
".knip.json",
|
|
23
|
+
".knip.jsonc",
|
|
24
|
+
"knip.ts",
|
|
25
|
+
"knip.js",
|
|
26
|
+
"knip.config.ts",
|
|
27
|
+
"knip.config.js"
|
|
28
|
+
];
|
|
29
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
30
|
+
"node_modules",
|
|
31
|
+
"dist",
|
|
32
|
+
"build",
|
|
33
|
+
"coverage"
|
|
34
|
+
]);
|
|
35
|
+
const PROXY_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
|
|
36
|
+
const buildNoReactDependencyError = (directory) => `No React dependency found in ${directory}/package.json. Add "react" to dependencies (or peerDependencies) and re-run.`;
|
|
16
37
|
//#endregion
|
|
17
38
|
//#region src/utils/match-glob-pattern.ts
|
|
18
39
|
const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
|
|
@@ -48,7 +69,11 @@ const toRelativePath = (filePath, rootDirectory) => {
|
|
|
48
69
|
if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
|
|
49
70
|
return normalizedFilePath.replace(/^\.\//, "");
|
|
50
71
|
};
|
|
51
|
-
const compileIgnoredFilePatterns = (userConfig) =>
|
|
72
|
+
const compileIgnoredFilePatterns = (userConfig) => {
|
|
73
|
+
const files = userConfig?.ignore?.files;
|
|
74
|
+
if (!Array.isArray(files)) return [];
|
|
75
|
+
return files.filter((entry) => typeof entry === "string").map(compileGlobPattern);
|
|
76
|
+
};
|
|
52
77
|
const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
|
|
53
78
|
if (patterns.length === 0) return false;
|
|
54
79
|
const relativePath = toRelativePath(filePath, rootDirectory);
|
|
@@ -89,9 +114,9 @@ const isRuleSuppressed = (commentRules, ruleId) => {
|
|
|
89
114
|
return commentRules.split(/[,\s]+/).some((rule) => rule.trim() === ruleId);
|
|
90
115
|
};
|
|
91
116
|
const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLinesSync) => {
|
|
92
|
-
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
|
|
117
|
+
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules.filter((rule) => typeof rule === "string") : []);
|
|
93
118
|
const ignoredFilePatterns = compileIgnoredFilePatterns(config);
|
|
94
|
-
const textComponentNames = new Set(Array.isArray(config.textComponents) ? config.textComponents : []);
|
|
119
|
+
const textComponentNames = new Set(Array.isArray(config.textComponents) ? config.textComponents.filter((name) => typeof name === "string") : []);
|
|
95
120
|
const hasTextComponents = textComponentNames.size > 0;
|
|
96
121
|
const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);
|
|
97
122
|
return diagnostics.filter((diagnostic) => {
|
|
@@ -144,32 +169,6 @@ const buildDiagnoseTimedResult = async (input) => {
|
|
|
144
169
|
};
|
|
145
170
|
};
|
|
146
171
|
//#endregion
|
|
147
|
-
//#region src/constants.ts
|
|
148
|
-
const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
|
|
149
|
-
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
150
|
-
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
151
|
-
const FETCH_TIMEOUT_MS = 1e4;
|
|
152
|
-
const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
153
|
-
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
154
|
-
const ERROR_RULE_PENALTY = 1.5;
|
|
155
|
-
const WARNING_RULE_PENALTY = .75;
|
|
156
|
-
const KNIP_CONFIG_LOCATIONS = [
|
|
157
|
-
"knip.json",
|
|
158
|
-
"knip.jsonc",
|
|
159
|
-
".knip.json",
|
|
160
|
-
".knip.jsonc",
|
|
161
|
-
"knip.ts",
|
|
162
|
-
"knip.js",
|
|
163
|
-
"knip.config.ts",
|
|
164
|
-
"knip.config.js"
|
|
165
|
-
];
|
|
166
|
-
const IGNORED_DIRECTORIES = new Set([
|
|
167
|
-
"node_modules",
|
|
168
|
-
"dist",
|
|
169
|
-
"build",
|
|
170
|
-
"coverage"
|
|
171
|
-
]);
|
|
172
|
-
//#endregion
|
|
173
172
|
//#region src/utils/jsx-include-paths.ts
|
|
174
173
|
const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
|
|
175
174
|
//#endregion
|
|
@@ -183,7 +182,7 @@ const diagnoseCore = async (deps, options = {}) => {
|
|
|
183
182
|
const userConfig = deps.loadUserConfig();
|
|
184
183
|
const effectiveLint = options.lint ?? userConfig?.lint ?? true;
|
|
185
184
|
const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;
|
|
186
|
-
if (!projectInfo.reactVersion) throw new Error(
|
|
185
|
+
if (!projectInfo.reactVersion) throw new Error(buildNoReactDependencyError(deps.rootDirectory));
|
|
187
186
|
const lintIncludePaths = options.lintIncludePaths !== void 0 ? options.lintIncludePaths : computeJsxIncludePaths(includePaths);
|
|
188
187
|
const { runLint, runDeadCode } = deps.createRunners({
|
|
189
188
|
resolvedDirectory,
|
|
@@ -201,7 +200,11 @@ const diagnoseCore = async (deps, options = {}) => {
|
|
|
201
200
|
console.error("Dead code analysis failed:", error);
|
|
202
201
|
return emptyDiagnostics;
|
|
203
202
|
}) : Promise.resolve(emptyDiagnostics);
|
|
204
|
-
const [
|
|
203
|
+
const [lintSettled, deadCodeSettled] = await Promise.allSettled([lintPromise, deadCodePromise]);
|
|
204
|
+
const lintDiagnostics = lintSettled.status === "fulfilled" ? lintSettled.value : emptyDiagnostics;
|
|
205
|
+
const deadCodeDiagnostics = deadCodeSettled.status === "fulfilled" ? deadCodeSettled.value : emptyDiagnostics;
|
|
206
|
+
if (lintSettled.status === "rejected") console.error("Lint rejected:", lintSettled.reason);
|
|
207
|
+
if (deadCodeSettled.status === "rejected") console.error("Dead code rejected:", deadCodeSettled.reason);
|
|
205
208
|
const environmentDiagnostics = deps.getExtraDiagnostics?.() ?? [];
|
|
206
209
|
const timed = await buildDiagnoseTimedResult({
|
|
207
210
|
mergedDiagnostics: [
|
|
@@ -215,12 +218,145 @@ const diagnoseCore = async (deps, options = {}) => {
|
|
|
215
218
|
startTime,
|
|
216
219
|
calculateDiagnosticsScore: deps.calculateDiagnosticsScore
|
|
217
220
|
});
|
|
218
|
-
return
|
|
221
|
+
return {
|
|
219
222
|
diagnostics: timed.diagnostics,
|
|
220
223
|
score: timed.score,
|
|
221
224
|
project: projectInfo,
|
|
222
225
|
elapsedMilliseconds: timed.elapsedMilliseconds
|
|
223
|
-
}
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/utils/summarize-diagnostics.ts
|
|
230
|
+
const summarizeDiagnostics = (diagnostics, worstScore = null, worstScoreLabel = null) => {
|
|
231
|
+
let errorCount = 0;
|
|
232
|
+
let warningCount = 0;
|
|
233
|
+
const affectedFiles = /* @__PURE__ */ new Set();
|
|
234
|
+
for (const diagnostic of diagnostics) {
|
|
235
|
+
if (diagnostic.severity === "error") errorCount++;
|
|
236
|
+
else warningCount++;
|
|
237
|
+
affectedFiles.add(diagnostic.filePath);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
errorCount,
|
|
241
|
+
warningCount,
|
|
242
|
+
affectedFileCount: affectedFiles.size,
|
|
243
|
+
totalDiagnosticCount: diagnostics.length,
|
|
244
|
+
score: worstScore,
|
|
245
|
+
scoreLabel: worstScoreLabel
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/utils/build-json-report.ts
|
|
250
|
+
const toJsonDiff = (diff) => {
|
|
251
|
+
if (!diff) return null;
|
|
252
|
+
return {
|
|
253
|
+
baseBranch: diff.baseBranch,
|
|
254
|
+
currentBranch: diff.currentBranch,
|
|
255
|
+
changedFileCount: diff.changedFiles.length,
|
|
256
|
+
isCurrentChanges: Boolean(diff.isCurrentChanges)
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
const findWorstScoredProject = (projects) => {
|
|
260
|
+
let worst = null;
|
|
261
|
+
let worstScore = Number.POSITIVE_INFINITY;
|
|
262
|
+
for (const project of projects) {
|
|
263
|
+
const score = project.score?.score;
|
|
264
|
+
if (typeof score !== "number") continue;
|
|
265
|
+
if (score < worstScore) {
|
|
266
|
+
worstScore = score;
|
|
267
|
+
worst = project;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return worst;
|
|
271
|
+
};
|
|
272
|
+
const buildJsonReport = (input) => {
|
|
273
|
+
const projects = input.scans.map(({ directory, result }) => ({
|
|
274
|
+
directory,
|
|
275
|
+
project: result.project,
|
|
276
|
+
diagnostics: result.diagnostics,
|
|
277
|
+
score: result.score,
|
|
278
|
+
skippedChecks: result.skippedChecks,
|
|
279
|
+
elapsedMilliseconds: result.elapsedMilliseconds
|
|
280
|
+
}));
|
|
281
|
+
const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
|
|
282
|
+
const worstScoredProject = findWorstScoredProject(projects);
|
|
283
|
+
const summary = summarizeDiagnostics(flattenedDiagnostics, worstScoredProject?.score?.score ?? null, worstScoredProject?.score?.label ?? null);
|
|
284
|
+
return {
|
|
285
|
+
schemaVersion: 1,
|
|
286
|
+
version: input.version,
|
|
287
|
+
ok: true,
|
|
288
|
+
directory: input.directory,
|
|
289
|
+
mode: input.mode,
|
|
290
|
+
diff: toJsonDiff(input.diff),
|
|
291
|
+
projects,
|
|
292
|
+
diagnostics: flattenedDiagnostics,
|
|
293
|
+
summary,
|
|
294
|
+
elapsedMilliseconds: input.totalElapsedMilliseconds,
|
|
295
|
+
error: null
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/utils/format-error-chain.ts
|
|
300
|
+
const collectErrorChain = (rootError) => {
|
|
301
|
+
const errorChain = [];
|
|
302
|
+
const visitedErrors = /* @__PURE__ */ new Set();
|
|
303
|
+
let currentError = rootError;
|
|
304
|
+
while (currentError !== void 0 && !visitedErrors.has(currentError)) {
|
|
305
|
+
visitedErrors.add(currentError);
|
|
306
|
+
errorChain.push(currentError);
|
|
307
|
+
currentError = currentError instanceof Error ? currentError.cause : void 0;
|
|
308
|
+
}
|
|
309
|
+
return errorChain;
|
|
310
|
+
};
|
|
311
|
+
const formatErrorMessage = (error) => error instanceof Error ? error.message || error.name : String(error);
|
|
312
|
+
const getErrorChainMessages = (rootError) => collectErrorChain(rootError).map(formatErrorMessage);
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/utils/build-json-report-error.ts
|
|
315
|
+
const safeStringify = (value) => {
|
|
316
|
+
try {
|
|
317
|
+
return String(value);
|
|
318
|
+
} catch {
|
|
319
|
+
return "Unrepresentable error";
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const safeGetErrorChain = (error) => {
|
|
323
|
+
try {
|
|
324
|
+
return getErrorChainMessages(error);
|
|
325
|
+
} catch {
|
|
326
|
+
return [safeStringify(error)];
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const buildJsonReportError = (input) => {
|
|
330
|
+
const chain = safeGetErrorChain(input.error);
|
|
331
|
+
const errorPayload = input.error instanceof Error ? {
|
|
332
|
+
message: input.error.message || input.error.name || "Error",
|
|
333
|
+
name: input.error.name || "Error",
|
|
334
|
+
chain
|
|
335
|
+
} : {
|
|
336
|
+
message: safeStringify(input.error),
|
|
337
|
+
name: "Error",
|
|
338
|
+
chain
|
|
339
|
+
};
|
|
340
|
+
return {
|
|
341
|
+
schemaVersion: 1,
|
|
342
|
+
version: input.version,
|
|
343
|
+
ok: false,
|
|
344
|
+
directory: input.directory,
|
|
345
|
+
mode: input.mode ?? "full",
|
|
346
|
+
diff: null,
|
|
347
|
+
projects: [],
|
|
348
|
+
diagnostics: [],
|
|
349
|
+
summary: {
|
|
350
|
+
errorCount: 0,
|
|
351
|
+
warningCount: 0,
|
|
352
|
+
affectedFileCount: 0,
|
|
353
|
+
totalDiagnosticCount: 0,
|
|
354
|
+
score: null,
|
|
355
|
+
scoreLabel: null
|
|
356
|
+
},
|
|
357
|
+
elapsedMilliseconds: input.elapsedMilliseconds,
|
|
358
|
+
error: errorPayload
|
|
359
|
+
};
|
|
224
360
|
};
|
|
225
361
|
//#endregion
|
|
226
362
|
//#region src/plugin/constants.ts
|
|
@@ -236,7 +372,11 @@ const isFile = (filePath) => {
|
|
|
236
372
|
};
|
|
237
373
|
//#endregion
|
|
238
374
|
//#region src/utils/read-package-json.ts
|
|
239
|
-
const
|
|
375
|
+
const cachedPackageJsons = /* @__PURE__ */ new Map();
|
|
376
|
+
const clearPackageJsonCache = () => {
|
|
377
|
+
cachedPackageJsons.clear();
|
|
378
|
+
};
|
|
379
|
+
const readPackageJsonUncached = (packageJsonPath) => {
|
|
240
380
|
try {
|
|
241
381
|
return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
242
382
|
} catch (error) {
|
|
@@ -248,10 +388,25 @@ const readPackageJson = (packageJsonPath) => {
|
|
|
248
388
|
throw error;
|
|
249
389
|
}
|
|
250
390
|
};
|
|
391
|
+
const readPackageJson = (packageJsonPath) => {
|
|
392
|
+
const absolutePath = path.resolve(packageJsonPath);
|
|
393
|
+
const cached = cachedPackageJsons.get(absolutePath);
|
|
394
|
+
if (cached !== void 0) return cached;
|
|
395
|
+
const result = readPackageJsonUncached(absolutePath);
|
|
396
|
+
cachedPackageJsons.set(absolutePath, result);
|
|
397
|
+
return result;
|
|
398
|
+
};
|
|
251
399
|
//#endregion
|
|
252
400
|
//#region src/utils/check-reduced-motion.ts
|
|
253
401
|
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
|
|
254
|
-
const REDUCED_MOTION_FILE_GLOBS =
|
|
402
|
+
const REDUCED_MOTION_FILE_GLOBS = [
|
|
403
|
+
"*.ts",
|
|
404
|
+
"*.tsx",
|
|
405
|
+
"*.js",
|
|
406
|
+
"*.jsx",
|
|
407
|
+
"*.css",
|
|
408
|
+
"*.scss"
|
|
409
|
+
];
|
|
255
410
|
const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
256
411
|
filePath: "package.json",
|
|
257
412
|
plugin: "react-doctor",
|
|
@@ -261,8 +416,7 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
|
261
416
|
help: "Add `useReducedMotion()` from your animation library, or a `@media (prefers-reduced-motion: reduce)` CSS query",
|
|
262
417
|
line: 0,
|
|
263
418
|
column: 0,
|
|
264
|
-
category: "Accessibility"
|
|
265
|
-
weight: 2
|
|
419
|
+
category: "Accessibility"
|
|
266
420
|
};
|
|
267
421
|
const checkReducedMotion = (rootDirectory) => {
|
|
268
422
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
@@ -279,17 +433,145 @@ const checkReducedMotion = (rootDirectory) => {
|
|
|
279
433
|
return [];
|
|
280
434
|
}
|
|
281
435
|
if (!hasMotionLibrary) return [];
|
|
436
|
+
const result = spawnSync("git", [
|
|
437
|
+
"grep",
|
|
438
|
+
"-ql",
|
|
439
|
+
"-E",
|
|
440
|
+
REDUCED_MOTION_GREP_PATTERN,
|
|
441
|
+
"--",
|
|
442
|
+
...REDUCED_MOTION_FILE_GLOBS
|
|
443
|
+
], {
|
|
444
|
+
cwd: rootDirectory,
|
|
445
|
+
stdio: [
|
|
446
|
+
"ignore",
|
|
447
|
+
"pipe",
|
|
448
|
+
"pipe"
|
|
449
|
+
]
|
|
450
|
+
});
|
|
451
|
+
if (result.error) return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
|
|
452
|
+
if (result.status === 0) return [];
|
|
453
|
+
return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
|
|
454
|
+
};
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/utils/parse-gitattributes-linguist.ts
|
|
457
|
+
const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
|
|
458
|
+
const FALSY_VALUES = new Set([
|
|
459
|
+
"false",
|
|
460
|
+
"0",
|
|
461
|
+
"off",
|
|
462
|
+
"no"
|
|
463
|
+
]);
|
|
464
|
+
const isTruthyLinguistAttribute = (token) => {
|
|
465
|
+
const match = LINGUIST_ATTRIBUTE_PATTERN.exec(token);
|
|
466
|
+
if (!match) return false;
|
|
467
|
+
if (match[1] === void 0) return true;
|
|
468
|
+
return !FALSY_VALUES.has(match[1].toLowerCase());
|
|
469
|
+
};
|
|
470
|
+
const parseGitattributesLinguistPaths = (filePath) => {
|
|
471
|
+
let content;
|
|
282
472
|
try {
|
|
283
|
-
|
|
284
|
-
cwd: rootDirectory,
|
|
285
|
-
stdio: "pipe"
|
|
286
|
-
});
|
|
287
|
-
return [];
|
|
473
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
288
474
|
} catch {
|
|
289
|
-
return [
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
const paths = [];
|
|
478
|
+
for (const rawLine of content.split("\n")) {
|
|
479
|
+
const line = rawLine.trim();
|
|
480
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
481
|
+
const tokens = line.split(/\s+/);
|
|
482
|
+
if (tokens.length < 2) continue;
|
|
483
|
+
const [pathSpec, ...attributes] = tokens;
|
|
484
|
+
if (attributes.some(isTruthyLinguistAttribute)) paths.push(pathSpec);
|
|
485
|
+
}
|
|
486
|
+
return paths;
|
|
487
|
+
};
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/utils/highlighter.ts
|
|
490
|
+
const highlighter = {
|
|
491
|
+
error: pc.red,
|
|
492
|
+
warn: pc.yellow,
|
|
493
|
+
info: pc.cyan,
|
|
494
|
+
success: pc.green,
|
|
495
|
+
dim: pc.dim
|
|
496
|
+
};
|
|
497
|
+
const logger = {
|
|
498
|
+
error(...args) {
|
|
499
|
+
console.error(highlighter.error(args.join(" ")));
|
|
500
|
+
},
|
|
501
|
+
warn(...args) {
|
|
502
|
+
console.warn(highlighter.warn(args.join(" ")));
|
|
503
|
+
},
|
|
504
|
+
info(...args) {
|
|
505
|
+
console.log(highlighter.info(args.join(" ")));
|
|
506
|
+
},
|
|
507
|
+
success(...args) {
|
|
508
|
+
console.log(highlighter.success(args.join(" ")));
|
|
509
|
+
},
|
|
510
|
+
dim(...args) {
|
|
511
|
+
console.log(highlighter.dim(args.join(" ")));
|
|
512
|
+
},
|
|
513
|
+
log(...args) {
|
|
514
|
+
console.log(args.join(" "));
|
|
515
|
+
},
|
|
516
|
+
break() {
|
|
517
|
+
console.log("");
|
|
290
518
|
}
|
|
291
519
|
};
|
|
292
520
|
//#endregion
|
|
521
|
+
//#region src/utils/read-ignore-file.ts
|
|
522
|
+
const stripGitignoreEscape = (pattern) => {
|
|
523
|
+
if (pattern.startsWith("\\#") || pattern.startsWith("\\!")) return pattern.slice(1);
|
|
524
|
+
return pattern;
|
|
525
|
+
};
|
|
526
|
+
const readIgnoreFile = (filePath) => {
|
|
527
|
+
let content;
|
|
528
|
+
try {
|
|
529
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
530
|
+
} catch (error) {
|
|
531
|
+
const errnoCode = error?.code;
|
|
532
|
+
if (errnoCode && errnoCode !== "ENOENT") logger.warn(`Could not read ignore file ${filePath}: ${errnoCode}`);
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
const patterns = [];
|
|
536
|
+
for (const line of content.split("\n")) {
|
|
537
|
+
const trimmed = line.trim();
|
|
538
|
+
if (trimmed.length === 0) continue;
|
|
539
|
+
if (trimmed.startsWith("#")) continue;
|
|
540
|
+
patterns.push(stripGitignoreEscape(trimmed));
|
|
541
|
+
}
|
|
542
|
+
return patterns;
|
|
543
|
+
};
|
|
544
|
+
//#endregion
|
|
545
|
+
//#region src/utils/collect-ignore-patterns.ts
|
|
546
|
+
const IGNORE_FILENAMES = [
|
|
547
|
+
".eslintignore",
|
|
548
|
+
".oxlintignore",
|
|
549
|
+
".prettierignore"
|
|
550
|
+
];
|
|
551
|
+
const cachedPatternsByRoot = /* @__PURE__ */ new Map();
|
|
552
|
+
const clearIgnorePatternsCache = () => {
|
|
553
|
+
cachedPatternsByRoot.clear();
|
|
554
|
+
};
|
|
555
|
+
const computeIgnorePatterns = (rootDirectory) => {
|
|
556
|
+
const seen = /* @__PURE__ */ new Set();
|
|
557
|
+
const patterns = [];
|
|
558
|
+
const addPattern = (pattern) => {
|
|
559
|
+
if (seen.has(pattern)) return;
|
|
560
|
+
seen.add(pattern);
|
|
561
|
+
patterns.push(pattern);
|
|
562
|
+
};
|
|
563
|
+
for (const filename of IGNORE_FILENAMES) for (const pattern of readIgnoreFile(path.join(rootDirectory, filename))) addPattern(pattern);
|
|
564
|
+
for (const linguistPath of parseGitattributesLinguistPaths(path.join(rootDirectory, ".gitattributes"))) addPattern(linguistPath);
|
|
565
|
+
return patterns;
|
|
566
|
+
};
|
|
567
|
+
const collectIgnorePatterns = (rootDirectory) => {
|
|
568
|
+
const cached = cachedPatternsByRoot.get(rootDirectory);
|
|
569
|
+
if (cached !== void 0) return cached;
|
|
570
|
+
const patterns = computeIgnorePatterns(rootDirectory);
|
|
571
|
+
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
572
|
+
return patterns;
|
|
573
|
+
};
|
|
574
|
+
//#endregion
|
|
293
575
|
//#region src/utils/find-monorepo-root.ts
|
|
294
576
|
const isMonorepoRoot = (directory) => {
|
|
295
577
|
if (isFile(path.join(directory, "pnpm-workspace.yaml"))) return true;
|
|
@@ -309,7 +591,11 @@ const findMonorepoRoot = (startDirectory) => {
|
|
|
309
591
|
};
|
|
310
592
|
//#endregion
|
|
311
593
|
//#region src/utils/is-plain-object.ts
|
|
312
|
-
const isPlainObject = (value) =>
|
|
594
|
+
const isPlainObject = (value) => {
|
|
595
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
596
|
+
const prototype = Object.getPrototypeOf(value);
|
|
597
|
+
return prototype === null || prototype === Object.prototype;
|
|
598
|
+
};
|
|
313
599
|
//#endregion
|
|
314
600
|
//#region src/utils/discover-project.ts
|
|
315
601
|
const REACT_COMPILER_PACKAGES = new Set([
|
|
@@ -317,6 +603,11 @@ const REACT_COMPILER_PACKAGES = new Set([
|
|
|
317
603
|
"react-compiler-runtime",
|
|
318
604
|
"eslint-plugin-react-compiler"
|
|
319
605
|
]);
|
|
606
|
+
const TANSTACK_QUERY_PACKAGES = new Set([
|
|
607
|
+
"@tanstack/react-query",
|
|
608
|
+
"@tanstack/query-core",
|
|
609
|
+
"react-query"
|
|
610
|
+
]);
|
|
320
611
|
const NEXT_CONFIG_FILENAMES = [
|
|
321
612
|
"next.config.js",
|
|
322
613
|
"next.config.mjs",
|
|
@@ -335,7 +626,11 @@ const VITE_CONFIG_FILENAMES = [
|
|
|
335
626
|
"vite.config.js",
|
|
336
627
|
"vite.config.ts",
|
|
337
628
|
"vite.config.mjs",
|
|
338
|
-
"vite.config.
|
|
629
|
+
"vite.config.mts",
|
|
630
|
+
"vite.config.cjs",
|
|
631
|
+
"vite.config.cts",
|
|
632
|
+
"vitest.config.ts",
|
|
633
|
+
"vitest.config.js"
|
|
339
634
|
];
|
|
340
635
|
const EXPO_APP_CONFIG_FILENAMES = [
|
|
341
636
|
"app.json",
|
|
@@ -343,7 +638,7 @@ const EXPO_APP_CONFIG_FILENAMES = [
|
|
|
343
638
|
"app.config.ts"
|
|
344
639
|
];
|
|
345
640
|
const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
|
|
346
|
-
const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*true\b/;
|
|
641
|
+
const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
|
|
347
642
|
const FRAMEWORK_PACKAGES = {
|
|
348
643
|
next: "nextjs",
|
|
349
644
|
"@tanstack/react-start": "tanstack-start",
|
|
@@ -373,6 +668,7 @@ const countSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
373
668
|
const countSourceFilesViaGit = (rootDirectory) => {
|
|
374
669
|
const result = spawnSync("git", [
|
|
375
670
|
"ls-files",
|
|
671
|
+
"-z",
|
|
376
672
|
"--cached",
|
|
377
673
|
"--others",
|
|
378
674
|
"--exclude-standard"
|
|
@@ -382,7 +678,7 @@ const countSourceFilesViaGit = (rootDirectory) => {
|
|
|
382
678
|
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
383
679
|
});
|
|
384
680
|
if (result.error || result.status !== 0) return null;
|
|
385
|
-
return result.stdout.split("\
|
|
681
|
+
return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;
|
|
386
682
|
};
|
|
387
683
|
const countSourceFiles = (rootDirectory) => countSourceFilesViaGit(rootDirectory) ?? countSourceFilesViaFilesystem(rootDirectory);
|
|
388
684
|
const collectAllDependencies = (packageJson) => ({
|
|
@@ -480,17 +776,17 @@ const resolveCatalogVersionFromCollection = (catalogs, packageName, catalogRefer
|
|
|
480
776
|
const resolveCatalogVersion = (packageJson, packageName, rootDirectory) => {
|
|
481
777
|
const rawVersion = collectAllDependencies(packageJson)[packageName];
|
|
482
778
|
const catalogName = rawVersion ? extractCatalogName(rawVersion) : null;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const version = resolveVersionFromCatalog(raw.catalog, packageName);
|
|
779
|
+
if (isPlainObject(packageJson.catalog)) {
|
|
780
|
+
const version = resolveVersionFromCatalog(packageJson.catalog, packageName);
|
|
486
781
|
if (version) return version;
|
|
487
782
|
}
|
|
488
|
-
if (isPlainObject(
|
|
489
|
-
|
|
490
|
-
|
|
783
|
+
if (isPlainObject(packageJson.catalogs)) {
|
|
784
|
+
const namedCatalog = catalogName ? packageJson.catalogs[catalogName] : void 0;
|
|
785
|
+
if (namedCatalog && isPlainObject(namedCatalog)) {
|
|
786
|
+
const version = resolveVersionFromCatalog(namedCatalog, packageName);
|
|
491
787
|
if (version) return version;
|
|
492
788
|
}
|
|
493
|
-
for (const catalogEntries of Object.values(
|
|
789
|
+
for (const catalogEntries of Object.values(packageJson.catalogs)) if (isPlainObject(catalogEntries)) {
|
|
494
790
|
const version = resolveVersionFromCatalog(catalogEntries, packageName);
|
|
495
791
|
if (version) return version;
|
|
496
792
|
}
|
|
@@ -531,11 +827,32 @@ const parsePnpmWorkspacePatterns = (rootDirectory) => {
|
|
|
531
827
|
}
|
|
532
828
|
return patterns;
|
|
533
829
|
};
|
|
830
|
+
const NX_PROJECT_DISCOVERY_DIRS = [
|
|
831
|
+
"apps",
|
|
832
|
+
"libs",
|
|
833
|
+
"packages"
|
|
834
|
+
];
|
|
835
|
+
const getNxWorkspaceDirectories = (rootDirectory) => {
|
|
836
|
+
if (!isFile(path.join(rootDirectory, "nx.json"))) return [];
|
|
837
|
+
const collected = [];
|
|
838
|
+
for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
|
|
839
|
+
const candidatePath = path.join(rootDirectory, candidate);
|
|
840
|
+
if (!fs.existsSync(candidatePath) || !fs.statSync(candidatePath).isDirectory()) continue;
|
|
841
|
+
for (const entry of fs.readdirSync(candidatePath, { withFileTypes: true })) {
|
|
842
|
+
if (!entry.isDirectory()) continue;
|
|
843
|
+
const projectDirectory = path.join(candidatePath, entry.name);
|
|
844
|
+
if (isFile(path.join(projectDirectory, "project.json")) || isFile(path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return collected;
|
|
848
|
+
};
|
|
534
849
|
const getWorkspacePatterns = (rootDirectory, packageJson) => {
|
|
535
850
|
const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
|
|
536
851
|
if (pnpmPatterns.length > 0) return pnpmPatterns;
|
|
537
852
|
if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
|
|
538
853
|
if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
|
|
854
|
+
const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
|
|
855
|
+
if (nxPatterns.length > 0) return nxPatterns;
|
|
539
856
|
return [];
|
|
540
857
|
};
|
|
541
858
|
const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
|
|
@@ -598,23 +915,35 @@ const hasCompilerInConfigFile = (filePath) => {
|
|
|
598
915
|
return REACT_COMPILER_ENABLED_FLAG_PATTERN.test(content) || REACT_COMPILER_PACKAGE_REFERENCE_PATTERN.test(content);
|
|
599
916
|
};
|
|
600
917
|
const hasCompilerInConfigFiles = (directory, filenames) => filenames.some((filename) => hasCompilerInConfigFile(path.join(directory, filename)));
|
|
918
|
+
const isProjectBoundary$1 = (directory) => {
|
|
919
|
+
if (fs.existsSync(path.join(directory, ".git"))) return true;
|
|
920
|
+
return isMonorepoRoot(directory);
|
|
921
|
+
};
|
|
601
922
|
const detectReactCompiler = (directory, packageJson) => {
|
|
602
923
|
if (hasCompilerPackage(packageJson)) return true;
|
|
603
924
|
if (hasCompilerInConfigFiles(directory, NEXT_CONFIG_FILENAMES)) return true;
|
|
604
925
|
if (hasCompilerInConfigFiles(directory, BABEL_CONFIG_FILENAMES)) return true;
|
|
605
926
|
if (hasCompilerInConfigFiles(directory, VITE_CONFIG_FILENAMES)) return true;
|
|
606
927
|
if (hasCompilerInConfigFiles(directory, EXPO_APP_CONFIG_FILENAMES)) return true;
|
|
928
|
+
if (isProjectBoundary$1(directory)) return false;
|
|
607
929
|
let ancestorDirectory = path.dirname(directory);
|
|
608
930
|
while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
|
|
609
931
|
const ancestorPackagePath = path.join(ancestorDirectory, "package.json");
|
|
610
932
|
if (isFile(ancestorPackagePath)) {
|
|
611
933
|
if (hasCompilerPackage(readPackageJson(ancestorPackagePath))) return true;
|
|
612
934
|
}
|
|
935
|
+
if (isProjectBoundary$1(ancestorDirectory)) return false;
|
|
613
936
|
ancestorDirectory = path.dirname(ancestorDirectory);
|
|
614
937
|
}
|
|
615
938
|
return false;
|
|
616
939
|
};
|
|
940
|
+
const cachedProjectInfos = /* @__PURE__ */ new Map();
|
|
941
|
+
const clearProjectCache = () => {
|
|
942
|
+
cachedProjectInfos.clear();
|
|
943
|
+
};
|
|
617
944
|
const discoverProject = (directory) => {
|
|
945
|
+
const cached = cachedProjectInfos.get(directory);
|
|
946
|
+
if (cached !== void 0) return cached;
|
|
618
947
|
const packageJsonPath = path.join(directory, "package.json");
|
|
619
948
|
if (!isFile(packageJsonPath)) throw new Error(`No package.json found in ${directory}`);
|
|
620
949
|
const packageJson = readPackageJson(packageJsonPath);
|
|
@@ -641,15 +970,56 @@ const discoverProject = (directory) => {
|
|
|
641
970
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
|
|
642
971
|
const sourceFileCount = countSourceFiles(directory);
|
|
643
972
|
const hasReactCompiler = detectReactCompiler(directory, packageJson);
|
|
644
|
-
|
|
973
|
+
const allDependencies = collectAllDependencies(packageJson);
|
|
974
|
+
const hasTanStackQuery = Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
|
|
975
|
+
const projectInfo = {
|
|
645
976
|
rootDirectory: directory,
|
|
646
977
|
projectName,
|
|
647
978
|
reactVersion,
|
|
648
979
|
framework,
|
|
649
980
|
hasTypeScript,
|
|
650
981
|
hasReactCompiler,
|
|
982
|
+
hasTanStackQuery,
|
|
651
983
|
sourceFileCount
|
|
652
984
|
};
|
|
985
|
+
cachedProjectInfos.set(directory, projectInfo);
|
|
986
|
+
return projectInfo;
|
|
987
|
+
};
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region src/utils/validate-config-types.ts
|
|
990
|
+
const BOOLEAN_FIELD_NAMES = [
|
|
991
|
+
"lint",
|
|
992
|
+
"deadCode",
|
|
993
|
+
"verbose",
|
|
994
|
+
"customRulesOnly",
|
|
995
|
+
"share",
|
|
996
|
+
"respectInlineDisables"
|
|
997
|
+
];
|
|
998
|
+
const warnConfigField = (message) => {
|
|
999
|
+
process.stderr.write(`[react-doctor] ${message}\n`);
|
|
1000
|
+
};
|
|
1001
|
+
const coerceMaybeBooleanString = (fieldName, value) => {
|
|
1002
|
+
if (typeof value === "boolean" || value === void 0) return value;
|
|
1003
|
+
if (value === "true") {
|
|
1004
|
+
warnConfigField(`config field "${fieldName}" is the string "true"; treating as boolean true.`);
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
if (value === "false") {
|
|
1008
|
+
warnConfigField(`config field "${fieldName}" is the string "false"; treating as boolean false.`);
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
warnConfigField(`config field "${fieldName}" must be a boolean (got ${typeof value}); ignoring this field.`);
|
|
1012
|
+
};
|
|
1013
|
+
const validateConfigTypes = (config) => {
|
|
1014
|
+
const validated = { ...config };
|
|
1015
|
+
for (const fieldName of BOOLEAN_FIELD_NAMES) {
|
|
1016
|
+
const original = config[fieldName];
|
|
1017
|
+
if (original === void 0) continue;
|
|
1018
|
+
const coerced = coerceMaybeBooleanString(fieldName, original);
|
|
1019
|
+
if (coerced === void 0) delete validated[fieldName];
|
|
1020
|
+
else validated[fieldName] = coerced;
|
|
1021
|
+
}
|
|
1022
|
+
return validated;
|
|
653
1023
|
};
|
|
654
1024
|
//#endregion
|
|
655
1025
|
//#region src/utils/load-config.ts
|
|
@@ -660,30 +1030,55 @@ const loadConfigFromDirectory = (directory) => {
|
|
|
660
1030
|
if (isFile(configFilePath)) try {
|
|
661
1031
|
const fileContent = fs.readFileSync(configFilePath, "utf-8");
|
|
662
1032
|
const parsed = JSON.parse(fileContent);
|
|
663
|
-
if (isPlainObject(parsed)) return parsed;
|
|
664
|
-
|
|
1033
|
+
if (isPlainObject(parsed)) return validateConfigTypes(parsed);
|
|
1034
|
+
logger.warn(`${CONFIG_FILENAME} must be a JSON object, ignoring.`);
|
|
665
1035
|
} catch (error) {
|
|
666
|
-
|
|
1036
|
+
logger.warn(`Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
|
|
667
1037
|
}
|
|
668
1038
|
const packageJsonPath = path.join(directory, "package.json");
|
|
669
1039
|
if (isFile(packageJsonPath)) try {
|
|
670
1040
|
const fileContent = fs.readFileSync(packageJsonPath, "utf-8");
|
|
671
|
-
const
|
|
672
|
-
if (isPlainObject(
|
|
1041
|
+
const packageJson = JSON.parse(fileContent);
|
|
1042
|
+
if (isPlainObject(packageJson)) {
|
|
1043
|
+
const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];
|
|
1044
|
+
if (isPlainObject(embeddedConfig)) return validateConfigTypes(embeddedConfig);
|
|
1045
|
+
}
|
|
673
1046
|
} catch {
|
|
674
1047
|
return null;
|
|
675
1048
|
}
|
|
676
1049
|
return null;
|
|
677
1050
|
};
|
|
1051
|
+
const isProjectBoundary = (directory) => fs.existsSync(path.join(directory, ".git")) || isMonorepoRoot(directory);
|
|
1052
|
+
const cachedConfigs = /* @__PURE__ */ new Map();
|
|
1053
|
+
const clearConfigCache = () => {
|
|
1054
|
+
cachedConfigs.clear();
|
|
1055
|
+
};
|
|
678
1056
|
const loadConfig = (rootDirectory) => {
|
|
1057
|
+
const cached = cachedConfigs.get(rootDirectory);
|
|
1058
|
+
if (cached !== void 0) return cached;
|
|
679
1059
|
const localConfig = loadConfigFromDirectory(rootDirectory);
|
|
680
|
-
if (localConfig)
|
|
1060
|
+
if (localConfig) {
|
|
1061
|
+
cachedConfigs.set(rootDirectory, localConfig);
|
|
1062
|
+
return localConfig;
|
|
1063
|
+
}
|
|
1064
|
+
if (isProjectBoundary(rootDirectory)) {
|
|
1065
|
+
cachedConfigs.set(rootDirectory, null);
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
681
1068
|
let ancestorDirectory = path.dirname(rootDirectory);
|
|
682
1069
|
while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
|
|
683
1070
|
const ancestorConfig = loadConfigFromDirectory(ancestorDirectory);
|
|
684
|
-
if (ancestorConfig)
|
|
1071
|
+
if (ancestorConfig) {
|
|
1072
|
+
cachedConfigs.set(rootDirectory, ancestorConfig);
|
|
1073
|
+
return ancestorConfig;
|
|
1074
|
+
}
|
|
1075
|
+
if (isProjectBoundary(ancestorDirectory)) {
|
|
1076
|
+
cachedConfigs.set(rootDirectory, null);
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
685
1079
|
ancestorDirectory = path.dirname(ancestorDirectory);
|
|
686
1080
|
}
|
|
1081
|
+
cachedConfigs.set(rootDirectory, null);
|
|
687
1082
|
return null;
|
|
688
1083
|
};
|
|
689
1084
|
//#endregion
|
|
@@ -703,16 +1098,18 @@ const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
|
703
1098
|
const listSourceFilesViaGit = (rootDirectory) => {
|
|
704
1099
|
const result = spawnSync("git", [
|
|
705
1100
|
"ls-files",
|
|
1101
|
+
"-z",
|
|
706
1102
|
"--cached",
|
|
707
1103
|
"--others",
|
|
708
|
-
"--exclude-standard"
|
|
1104
|
+
"--exclude-standard",
|
|
1105
|
+
"--recurse-submodules"
|
|
709
1106
|
], {
|
|
710
1107
|
cwd: rootDirectory,
|
|
711
1108
|
encoding: "utf-8",
|
|
712
1109
|
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
713
1110
|
});
|
|
714
1111
|
if (result.error || result.status !== 0) return null;
|
|
715
|
-
return result.stdout.split("\
|
|
1112
|
+
return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
|
|
716
1113
|
};
|
|
717
1114
|
const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
718
1115
|
const filePaths = [];
|
|
@@ -785,43 +1182,51 @@ const parseScoreResult = (value) => {
|
|
|
785
1182
|
label: labelValue
|
|
786
1183
|
};
|
|
787
1184
|
};
|
|
1185
|
+
const stripFilePaths = (diagnostics) => diagnostics.map(({ filePath: _filePath, ...rest }) => rest);
|
|
1186
|
+
const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
1187
|
+
const describeFailure = (error) => {
|
|
1188
|
+
if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
|
|
1189
|
+
if (error instanceof Error && error.message) return error.message;
|
|
1190
|
+
return String(error);
|
|
1191
|
+
};
|
|
788
1192
|
const tryScoreFromApi = async (diagnostics, fetchImplementation) => {
|
|
1193
|
+
if (typeof fetchImplementation !== "function") return null;
|
|
789
1194
|
const controller = new AbortController();
|
|
790
1195
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
791
1196
|
try {
|
|
792
1197
|
const response = await fetchImplementation(SCORE_API_URL, {
|
|
793
1198
|
method: "POST",
|
|
794
1199
|
headers: { "Content-Type": "application/json" },
|
|
795
|
-
body: JSON.stringify({ diagnostics }),
|
|
1200
|
+
body: JSON.stringify({ diagnostics: stripFilePaths(diagnostics) }),
|
|
796
1201
|
signal: controller.signal
|
|
797
1202
|
});
|
|
798
|
-
if (!response.ok)
|
|
1203
|
+
if (!response.ok) {
|
|
1204
|
+
console.warn(`[react-doctor] Score API returned ${response.status} ${response.statusText} — using local scoring`);
|
|
1205
|
+
return null;
|
|
1206
|
+
}
|
|
799
1207
|
return parseScoreResult(await response.json());
|
|
800
|
-
} catch {
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
console.warn(`[react-doctor] Score API unreachable (${describeFailure(error)}) — using local scoring`);
|
|
801
1210
|
return null;
|
|
802
1211
|
} finally {
|
|
803
1212
|
clearTimeout(timeoutId);
|
|
804
1213
|
}
|
|
805
1214
|
};
|
|
806
1215
|
//#endregion
|
|
1216
|
+
//#region src/utils/calculate-score-browser.ts
|
|
1217
|
+
const getGlobalFetch = () => typeof fetch === "function" ? fetch : void 0;
|
|
1218
|
+
const calculateScore$1 = async (diagnostics, fetchImplementation = getGlobalFetch()) => await tryScoreFromApi(diagnostics, fetchImplementation) ?? calculateScoreLocally(diagnostics);
|
|
1219
|
+
//#endregion
|
|
807
1220
|
//#region src/utils/proxy-fetch.ts
|
|
808
1221
|
const getGlobalProcess = () => {
|
|
809
1222
|
const candidate = globalThis.process;
|
|
810
1223
|
return candidate?.versions?.node ? candidate : void 0;
|
|
811
1224
|
};
|
|
812
|
-
const
|
|
1225
|
+
const getProxyUrl = () => {
|
|
813
1226
|
const proc = getGlobalProcess();
|
|
814
1227
|
if (!proc?.env) return void 0;
|
|
815
1228
|
return proc.env.HTTPS_PROXY ?? proc.env.https_proxy ?? proc.env.HTTP_PROXY ?? proc.env.http_proxy;
|
|
816
1229
|
};
|
|
817
|
-
let isProxyUrlResolved = false;
|
|
818
|
-
let resolvedProxyUrl;
|
|
819
|
-
const getProxyUrl = () => {
|
|
820
|
-
if (isProxyUrlResolved) return resolvedProxyUrl;
|
|
821
|
-
isProxyUrlResolved = true;
|
|
822
|
-
resolvedProxyUrl = readEnvProxy();
|
|
823
|
-
return resolvedProxyUrl;
|
|
824
|
-
};
|
|
825
1230
|
const createProxyDispatcher = async (proxyUrl) => {
|
|
826
1231
|
try {
|
|
827
1232
|
const { ProxyAgent } = await import("undici");
|
|
@@ -831,27 +1236,17 @@ const createProxyDispatcher = async (proxyUrl) => {
|
|
|
831
1236
|
}
|
|
832
1237
|
};
|
|
833
1238
|
const proxyFetch = async (url, init) => {
|
|
834
|
-
const
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
signal: controller.signal,
|
|
842
|
-
...dispatcher ? { dispatcher } : {}
|
|
843
|
-
});
|
|
844
|
-
} finally {
|
|
845
|
-
clearTimeout(timeoutId);
|
|
846
|
-
}
|
|
1239
|
+
const proxyUrl = getProxyUrl();
|
|
1240
|
+
const dispatcher = proxyUrl ? await createProxyDispatcher(proxyUrl) : null;
|
|
1241
|
+
const fetchInit = {
|
|
1242
|
+
...init,
|
|
1243
|
+
...dispatcher ? { dispatcher } : {}
|
|
1244
|
+
};
|
|
1245
|
+
return fetch(url, fetchInit);
|
|
847
1246
|
};
|
|
848
1247
|
//#endregion
|
|
849
1248
|
//#region src/utils/calculate-score-node.ts
|
|
850
|
-
const calculateScore =
|
|
851
|
-
const apiScore = await tryScoreFromApi(diagnostics, proxyFetch);
|
|
852
|
-
if (apiScore) return apiScore;
|
|
853
|
-
return calculateScoreLocally(diagnostics);
|
|
854
|
-
};
|
|
1249
|
+
const calculateScore = (diagnostics) => calculateScore$1(diagnostics, proxyFetch);
|
|
855
1250
|
//#endregion
|
|
856
1251
|
//#region src/utils/collect-unused-file-paths.ts
|
|
857
1252
|
const collectUnusedFilePaths = (filesIssues) => {
|
|
@@ -866,27 +1261,15 @@ const collectUnusedFilePaths = (filesIssues) => {
|
|
|
866
1261
|
return unusedFilePaths;
|
|
867
1262
|
};
|
|
868
1263
|
//#endregion
|
|
869
|
-
//#region src/utils/format-error-chain.ts
|
|
870
|
-
const collectErrorChain = (rootError) => {
|
|
871
|
-
const errorChain = [];
|
|
872
|
-
const visitedErrors = /* @__PURE__ */ new Set();
|
|
873
|
-
let currentError = rootError;
|
|
874
|
-
while (currentError !== void 0 && !visitedErrors.has(currentError)) {
|
|
875
|
-
visitedErrors.add(currentError);
|
|
876
|
-
errorChain.push(currentError);
|
|
877
|
-
currentError = currentError instanceof Error ? currentError.cause : void 0;
|
|
878
|
-
}
|
|
879
|
-
return errorChain;
|
|
880
|
-
};
|
|
881
|
-
const formatErrorMessage = (error) => error instanceof Error ? error.message || error.name : String(error);
|
|
882
|
-
const getErrorChainMessages = (rootError) => collectErrorChain(rootError).map(formatErrorMessage);
|
|
883
|
-
//#endregion
|
|
884
1264
|
//#region src/utils/extract-failed-plugin-name.ts
|
|
885
1265
|
const PLUGIN_CONFIG_PATTERN = /(?:^|[/\\\s])([a-z][a-z0-9-]*)\.config\./i;
|
|
1266
|
+
const RC_DOTFILE_PATTERN = /(?:^|[/\\])\.([a-z][a-z0-9-]*?)rc(?:\.[a-z]+)?(?:\b|$)/i;
|
|
886
1267
|
const extractFailedPluginName = (error) => {
|
|
887
1268
|
for (const errorMessage of getErrorChainMessages(error)) {
|
|
888
1269
|
const pluginNameMatch = errorMessage.match(PLUGIN_CONFIG_PATTERN);
|
|
889
1270
|
if (pluginNameMatch?.[1]) return pluginNameMatch[1].toLowerCase();
|
|
1271
|
+
const rcMatch = errorMessage.match(RC_DOTFILE_PATTERN);
|
|
1272
|
+
if (rcMatch?.[1]) return rcMatch[1].toLowerCase();
|
|
890
1273
|
}
|
|
891
1274
|
return null;
|
|
892
1275
|
};
|
|
@@ -895,37 +1278,46 @@ const extractFailedPluginName = (error) => {
|
|
|
895
1278
|
const hasKnipConfig = (directory) => KNIP_CONFIG_LOCATIONS.some((configFilename) => isFile(path.join(directory, configFilename)));
|
|
896
1279
|
//#endregion
|
|
897
1280
|
//#region src/utils/run-knip.ts
|
|
898
|
-
const
|
|
899
|
-
files:
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
duplicates:
|
|
1281
|
+
const KNIP_ISSUE_TYPE_DESCRIPTORS = {
|
|
1282
|
+
files: {
|
|
1283
|
+
category: "Dead Code",
|
|
1284
|
+
message: "Unused file",
|
|
1285
|
+
severity: "warning"
|
|
1286
|
+
},
|
|
1287
|
+
exports: {
|
|
1288
|
+
category: "Dead Code",
|
|
1289
|
+
message: "Unused export",
|
|
1290
|
+
severity: "warning"
|
|
1291
|
+
},
|
|
1292
|
+
types: {
|
|
1293
|
+
category: "Dead Code",
|
|
1294
|
+
message: "Unused type",
|
|
1295
|
+
severity: "warning"
|
|
1296
|
+
},
|
|
1297
|
+
duplicates: {
|
|
1298
|
+
category: "Dead Code",
|
|
1299
|
+
message: "Duplicate export",
|
|
1300
|
+
severity: "warning"
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
const FALLBACK_KNIP_DESCRIPTOR = {
|
|
1304
|
+
category: "Dead Code",
|
|
1305
|
+
message: "Issue",
|
|
1306
|
+
severity: "warning"
|
|
915
1307
|
};
|
|
916
1308
|
const collectIssueRecords = (records, issueType, rootDirectory) => {
|
|
1309
|
+
const descriptor = KNIP_ISSUE_TYPE_DESCRIPTORS[issueType] ?? FALLBACK_KNIP_DESCRIPTOR;
|
|
917
1310
|
const diagnostics = [];
|
|
918
1311
|
for (const issues of Object.values(records)) for (const issue of Object.values(issues)) diagnostics.push({
|
|
919
1312
|
filePath: path.relative(rootDirectory, issue.filePath),
|
|
920
1313
|
plugin: "knip",
|
|
921
1314
|
rule: issueType,
|
|
922
|
-
severity:
|
|
923
|
-
message: `${
|
|
1315
|
+
severity: descriptor.severity,
|
|
1316
|
+
message: `${descriptor.message}: ${issue.symbol}`,
|
|
924
1317
|
help: "",
|
|
925
1318
|
line: 0,
|
|
926
1319
|
column: 0,
|
|
927
|
-
category:
|
|
928
|
-
weight: 1
|
|
1320
|
+
category: descriptor.category
|
|
929
1321
|
});
|
|
930
1322
|
return diagnostics;
|
|
931
1323
|
};
|
|
@@ -934,10 +1326,11 @@ const silenced = async (fn) => {
|
|
|
934
1326
|
const originalInfo = console.info;
|
|
935
1327
|
const originalWarn = console.warn;
|
|
936
1328
|
const originalError = console.error;
|
|
937
|
-
|
|
938
|
-
console.
|
|
939
|
-
console.
|
|
940
|
-
console.
|
|
1329
|
+
const noop = () => {};
|
|
1330
|
+
console.log = noop;
|
|
1331
|
+
console.info = noop;
|
|
1332
|
+
console.warn = noop;
|
|
1333
|
+
console.error = noop;
|
|
941
1334
|
try {
|
|
942
1335
|
return await fn();
|
|
943
1336
|
} finally {
|
|
@@ -947,8 +1340,8 @@ const silenced = async (fn) => {
|
|
|
947
1340
|
console.error = originalError;
|
|
948
1341
|
}
|
|
949
1342
|
};
|
|
950
|
-
const TSCONFIG_FILENAMES = ["tsconfig.base.json", "tsconfig.json"];
|
|
951
|
-
const resolveTsConfigFile = (directory) => TSCONFIG_FILENAMES.find((filename) => fs.existsSync(path.join(directory, filename)));
|
|
1343
|
+
const TSCONFIG_FILENAMES$1 = ["tsconfig.base.json", "tsconfig.json"];
|
|
1344
|
+
const resolveTsConfigFile = (directory) => TSCONFIG_FILENAMES$1.find((filename) => fs.existsSync(path.join(directory, filename)));
|
|
952
1345
|
const tryDisableFailedPlugin = (error, parsedConfig, disabledPlugins) => {
|
|
953
1346
|
const failedPlugin = extractFailedPluginName(error);
|
|
954
1347
|
if (!failedPlugin || !(failedPlugin in parsedConfig) || disabledPlugins.has(failedPlugin)) return false;
|
|
@@ -967,7 +1360,7 @@ const runKnipWithOptions = async (knipCwd, workspaceName) => {
|
|
|
967
1360
|
const parsedConfig = options.parsedConfig;
|
|
968
1361
|
const disabledPlugins = /* @__PURE__ */ new Set();
|
|
969
1362
|
let lastKnipError;
|
|
970
|
-
for (let attempt = 0; attempt
|
|
1363
|
+
for (let attempt = 0; attempt < 6; attempt++) try {
|
|
971
1364
|
return await silenced(() => main(options));
|
|
972
1365
|
} catch (error) {
|
|
973
1366
|
lastKnipError = error;
|
|
@@ -996,17 +1389,17 @@ const runKnip = async (rootDirectory) => {
|
|
|
996
1389
|
if (!(hasNodeModules(rootDirectory) || monorepoRoot !== null && hasNodeModules(monorepoRoot))) return [];
|
|
997
1390
|
const { issues } = await runKnipForProject(rootDirectory, monorepoRoot);
|
|
998
1391
|
const diagnostics = [];
|
|
1392
|
+
const filesDescriptor = KNIP_ISSUE_TYPE_DESCRIPTORS.files;
|
|
999
1393
|
for (const unusedFilePath of collectUnusedFilePaths(issues.files)) diagnostics.push({
|
|
1000
1394
|
filePath: path.relative(rootDirectory, unusedFilePath),
|
|
1001
1395
|
plugin: "knip",
|
|
1002
1396
|
rule: "files",
|
|
1003
|
-
severity:
|
|
1004
|
-
message:
|
|
1397
|
+
severity: filesDescriptor.severity,
|
|
1398
|
+
message: filesDescriptor.message,
|
|
1005
1399
|
help: "This file is not imported by any other file in the project.",
|
|
1006
1400
|
line: 0,
|
|
1007
1401
|
column: 0,
|
|
1008
|
-
category:
|
|
1009
|
-
weight: 1
|
|
1402
|
+
category: filesDescriptor.category
|
|
1010
1403
|
});
|
|
1011
1404
|
for (const issueType of [
|
|
1012
1405
|
"exports",
|
|
@@ -1016,6 +1409,29 @@ const runKnip = async (rootDirectory) => {
|
|
|
1016
1409
|
return diagnostics;
|
|
1017
1410
|
};
|
|
1018
1411
|
//#endregion
|
|
1412
|
+
//#region src/utils/batch-include-paths.ts
|
|
1413
|
+
const estimateArgsLength = (args) => args.reduce((total, argument) => total + argument.length + 1, 0);
|
|
1414
|
+
const batchIncludePaths = (baseArgs, includePaths) => {
|
|
1415
|
+
const baseArgsLength = estimateArgsLength(baseArgs);
|
|
1416
|
+
const batches = [];
|
|
1417
|
+
let currentBatch = [];
|
|
1418
|
+
let currentBatchLength = baseArgsLength;
|
|
1419
|
+
for (const filePath of includePaths) {
|
|
1420
|
+
const entryLength = filePath.length + 1;
|
|
1421
|
+
const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
|
|
1422
|
+
const exceedsFileCount = currentBatch.length >= 500;
|
|
1423
|
+
if (exceedsArgLength || exceedsFileCount) {
|
|
1424
|
+
batches.push(currentBatch);
|
|
1425
|
+
currentBatch = [];
|
|
1426
|
+
currentBatchLength = baseArgsLength;
|
|
1427
|
+
}
|
|
1428
|
+
currentBatch.push(filePath);
|
|
1429
|
+
currentBatchLength += entryLength;
|
|
1430
|
+
}
|
|
1431
|
+
if (currentBatch.length > 0) batches.push(currentBatch);
|
|
1432
|
+
return batches;
|
|
1433
|
+
};
|
|
1434
|
+
//#endregion
|
|
1019
1435
|
//#region src/oxlint-config.ts
|
|
1020
1436
|
const esmRequire$1 = createRequire(import.meta.url);
|
|
1021
1437
|
const NEXTJS_RULES = {
|
|
@@ -1044,7 +1460,23 @@ const REACT_NATIVE_RULES = {
|
|
|
1044
1460
|
"react-doctor/rn-no-inline-flatlist-renderitem": "warn",
|
|
1045
1461
|
"react-doctor/rn-no-legacy-shadow-styles": "warn",
|
|
1046
1462
|
"react-doctor/rn-prefer-reanimated": "warn",
|
|
1047
|
-
"react-doctor/rn-no-single-element-style-array": "warn"
|
|
1463
|
+
"react-doctor/rn-no-single-element-style-array": "warn",
|
|
1464
|
+
"react-doctor/rn-prefer-pressable": "warn",
|
|
1465
|
+
"react-doctor/rn-prefer-expo-image": "warn",
|
|
1466
|
+
"react-doctor/rn-no-non-native-navigator": "warn",
|
|
1467
|
+
"react-doctor/rn-no-scroll-state": "error",
|
|
1468
|
+
"react-doctor/rn-no-scrollview-mapped-list": "warn",
|
|
1469
|
+
"react-doctor/rn-no-inline-object-in-list-item": "warn",
|
|
1470
|
+
"react-doctor/rn-animate-layout-property": "error",
|
|
1471
|
+
"react-doctor/rn-prefer-content-inset-adjustment": "warn",
|
|
1472
|
+
"react-doctor/rn-pressable-shared-value-mutation": "warn",
|
|
1473
|
+
"react-doctor/rn-list-data-mapped": "warn",
|
|
1474
|
+
"react-doctor/rn-list-callback-per-row": "warn",
|
|
1475
|
+
"react-doctor/rn-list-recyclable-without-types": "warn",
|
|
1476
|
+
"react-doctor/rn-animation-reaction-as-derived": "warn",
|
|
1477
|
+
"react-doctor/rn-bottom-sheet-prefer-native": "warn",
|
|
1478
|
+
"react-doctor/rn-scrollview-dynamic-padding": "warn",
|
|
1479
|
+
"react-doctor/rn-style-prefer-boxshadow": "warn"
|
|
1048
1480
|
};
|
|
1049
1481
|
const TANSTACK_START_RULES = {
|
|
1050
1482
|
"react-doctor/tanstack-start-route-property-order": "error",
|
|
@@ -1063,22 +1495,41 @@ const TANSTACK_START_RULES = {
|
|
|
1063
1495
|
"react-doctor/tanstack-start-loader-parallel-fetch": "warn"
|
|
1064
1496
|
};
|
|
1065
1497
|
const REACT_COMPILER_RULES = {
|
|
1066
|
-
"react-hooks-js/set-state-in-render": "
|
|
1067
|
-
"react-hooks-js/immutability": "
|
|
1068
|
-
"react-hooks-js/refs": "
|
|
1069
|
-
"react-hooks-js/purity": "
|
|
1070
|
-
"react-hooks-js/hooks": "
|
|
1071
|
-
"react-hooks-js/set-state-in-effect": "
|
|
1072
|
-
"react-hooks-js/globals": "
|
|
1073
|
-
"react-hooks-js/error-boundaries": "
|
|
1074
|
-
"react-hooks-js/preserve-manual-memoization": "
|
|
1075
|
-
"react-hooks-js/unsupported-syntax": "
|
|
1076
|
-
"react-hooks-js/component-hook-factories": "
|
|
1077
|
-
"react-hooks-js/static-components": "
|
|
1078
|
-
"react-hooks-js/use-memo": "
|
|
1079
|
-
"react-hooks-js/void-use-memo": "
|
|
1080
|
-
"react-hooks-js/incompatible-library": "
|
|
1081
|
-
"react-hooks-js/todo": "
|
|
1498
|
+
"react-hooks-js/set-state-in-render": "warn",
|
|
1499
|
+
"react-hooks-js/immutability": "warn",
|
|
1500
|
+
"react-hooks-js/refs": "warn",
|
|
1501
|
+
"react-hooks-js/purity": "warn",
|
|
1502
|
+
"react-hooks-js/hooks": "warn",
|
|
1503
|
+
"react-hooks-js/set-state-in-effect": "warn",
|
|
1504
|
+
"react-hooks-js/globals": "warn",
|
|
1505
|
+
"react-hooks-js/error-boundaries": "warn",
|
|
1506
|
+
"react-hooks-js/preserve-manual-memoization": "warn",
|
|
1507
|
+
"react-hooks-js/unsupported-syntax": "warn",
|
|
1508
|
+
"react-hooks-js/component-hook-factories": "warn",
|
|
1509
|
+
"react-hooks-js/static-components": "warn",
|
|
1510
|
+
"react-hooks-js/use-memo": "warn",
|
|
1511
|
+
"react-hooks-js/void-use-memo": "warn",
|
|
1512
|
+
"react-hooks-js/incompatible-library": "warn",
|
|
1513
|
+
"react-hooks-js/todo": "warn"
|
|
1514
|
+
};
|
|
1515
|
+
const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
|
|
1516
|
+
if (!hasReactCompiler || customRulesOnly) return [];
|
|
1517
|
+
try {
|
|
1518
|
+
return [{
|
|
1519
|
+
name: "react-hooks-js",
|
|
1520
|
+
specifier: esmRequire$1.resolve("eslint-plugin-react-hooks")
|
|
1521
|
+
}];
|
|
1522
|
+
} catch {
|
|
1523
|
+
return [];
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
const TANSTACK_QUERY_RULES = {
|
|
1527
|
+
"react-doctor/query-stable-query-client": "warn",
|
|
1528
|
+
"react-doctor/query-no-rest-destructuring": "warn",
|
|
1529
|
+
"react-doctor/query-no-void-query-fn": "warn",
|
|
1530
|
+
"react-doctor/query-no-query-in-effect": "warn",
|
|
1531
|
+
"react-doctor/query-mutation-missing-invalidation": "warn",
|
|
1532
|
+
"react-doctor/query-no-usequery-for-mutation": "warn"
|
|
1082
1533
|
};
|
|
1083
1534
|
const BUILTIN_REACT_RULES = {
|
|
1084
1535
|
"react/rules-of-hooks": "error",
|
|
@@ -1110,7 +1561,113 @@ const BUILTIN_A11Y_RULES = {
|
|
|
1110
1561
|
"jsx-a11y/no-distracting-elements": "error",
|
|
1111
1562
|
"jsx-a11y/iframe-has-title": "warn"
|
|
1112
1563
|
};
|
|
1113
|
-
const
|
|
1564
|
+
const GLOBAL_REACT_DOCTOR_RULES = {
|
|
1565
|
+
"react-doctor/no-derived-state-effect": "warn",
|
|
1566
|
+
"react-doctor/no-fetch-in-effect": "warn",
|
|
1567
|
+
"react-doctor/no-cascading-set-state": "warn",
|
|
1568
|
+
"react-doctor/no-effect-event-handler": "warn",
|
|
1569
|
+
"react-doctor/no-effect-event-in-deps": "error",
|
|
1570
|
+
"react-doctor/no-prop-callback-in-effect": "warn",
|
|
1571
|
+
"react-doctor/no-derived-useState": "warn",
|
|
1572
|
+
"react-doctor/prefer-useReducer": "warn",
|
|
1573
|
+
"react-doctor/rerender-lazy-state-init": "warn",
|
|
1574
|
+
"react-doctor/rerender-functional-setstate": "warn",
|
|
1575
|
+
"react-doctor/rerender-dependencies": "error",
|
|
1576
|
+
"react-doctor/rerender-state-only-in-handlers": "warn",
|
|
1577
|
+
"react-doctor/rerender-defer-reads-hook": "warn",
|
|
1578
|
+
"react-doctor/advanced-event-handler-refs": "warn",
|
|
1579
|
+
"react-doctor/no-giant-component": "warn",
|
|
1580
|
+
"react-doctor/no-render-in-render": "warn",
|
|
1581
|
+
"react-doctor/no-many-boolean-props": "warn",
|
|
1582
|
+
"react-doctor/no-react19-deprecated-apis": "warn",
|
|
1583
|
+
"react-doctor/no-render-prop-children": "warn",
|
|
1584
|
+
"react-doctor/no-nested-component-definition": "error",
|
|
1585
|
+
"react-doctor/react-compiler-destructure-method": "warn",
|
|
1586
|
+
"react-doctor/no-usememo-simple-expression": "warn",
|
|
1587
|
+
"react-doctor/no-layout-property-animation": "error",
|
|
1588
|
+
"react-doctor/rerender-memo-with-default-value": "warn",
|
|
1589
|
+
"react-doctor/rerender-memo-before-early-return": "warn",
|
|
1590
|
+
"react-doctor/rerender-transitions-scroll": "warn",
|
|
1591
|
+
"react-doctor/rerender-derived-state-from-hook": "warn",
|
|
1592
|
+
"react-doctor/async-defer-await": "warn",
|
|
1593
|
+
"react-doctor/async-await-in-loop": "warn",
|
|
1594
|
+
"react-doctor/rendering-animate-svg-wrapper": "warn",
|
|
1595
|
+
"react-doctor/rendering-hoist-jsx": "warn",
|
|
1596
|
+
"react-doctor/rendering-hydration-mismatch-time": "warn",
|
|
1597
|
+
"react-doctor/no-inline-prop-on-memo-component": "warn",
|
|
1598
|
+
"react-doctor/rendering-hydration-no-flicker": "warn",
|
|
1599
|
+
"react-doctor/rendering-script-defer-async": "warn",
|
|
1600
|
+
"react-doctor/rendering-usetransition-loading": "warn",
|
|
1601
|
+
"react-doctor/no-transition-all": "warn",
|
|
1602
|
+
"react-doctor/no-global-css-variable-animation": "error",
|
|
1603
|
+
"react-doctor/no-large-animated-blur": "warn",
|
|
1604
|
+
"react-doctor/no-scale-from-zero": "warn",
|
|
1605
|
+
"react-doctor/no-permanent-will-change": "warn",
|
|
1606
|
+
"react-doctor/no-eval": "error",
|
|
1607
|
+
"react-doctor/no-secrets-in-client-code": "warn",
|
|
1608
|
+
"react-doctor/no-generic-handler-names": "warn",
|
|
1609
|
+
"react-doctor/js-flatmap-filter": "warn",
|
|
1610
|
+
"react-doctor/js-combine-iterations": "warn",
|
|
1611
|
+
"react-doctor/js-tosorted-immutable": "warn",
|
|
1612
|
+
"react-doctor/js-hoist-regexp": "warn",
|
|
1613
|
+
"react-doctor/js-hoist-intl": "warn",
|
|
1614
|
+
"react-doctor/js-cache-property-access": "warn",
|
|
1615
|
+
"react-doctor/js-length-check-first": "warn",
|
|
1616
|
+
"react-doctor/js-min-max-loop": "warn",
|
|
1617
|
+
"react-doctor/js-set-map-lookups": "warn",
|
|
1618
|
+
"react-doctor/js-batch-dom-css": "warn",
|
|
1619
|
+
"react-doctor/js-index-maps": "warn",
|
|
1620
|
+
"react-doctor/js-cache-storage": "warn",
|
|
1621
|
+
"react-doctor/js-early-exit": "warn",
|
|
1622
|
+
"react-doctor/no-barrel-import": "warn",
|
|
1623
|
+
"react-doctor/no-dynamic-import-path": "warn",
|
|
1624
|
+
"react-doctor/no-full-lodash-import": "warn",
|
|
1625
|
+
"react-doctor/no-moment": "warn",
|
|
1626
|
+
"react-doctor/prefer-dynamic-import": "warn",
|
|
1627
|
+
"react-doctor/use-lazy-motion": "warn",
|
|
1628
|
+
"react-doctor/no-undeferred-third-party": "warn",
|
|
1629
|
+
"react-doctor/no-array-index-as-key": "warn",
|
|
1630
|
+
"react-doctor/no-polymorphic-children": "warn",
|
|
1631
|
+
"react-doctor/rendering-conditional-render": "warn",
|
|
1632
|
+
"react-doctor/rendering-svg-precision": "warn",
|
|
1633
|
+
"react-doctor/no-prevent-default": "warn",
|
|
1634
|
+
"react-doctor/no-document-start-view-transition": "warn",
|
|
1635
|
+
"react-doctor/no-flush-sync": "warn",
|
|
1636
|
+
"react-doctor/server-auth-actions": "error",
|
|
1637
|
+
"react-doctor/server-after-nonblocking": "warn",
|
|
1638
|
+
"react-doctor/server-no-mutable-module-state": "error",
|
|
1639
|
+
"react-doctor/server-cache-with-object-literal": "warn",
|
|
1640
|
+
"react-doctor/server-hoist-static-io": "warn",
|
|
1641
|
+
"react-doctor/server-dedup-props": "warn",
|
|
1642
|
+
"react-doctor/server-sequential-independent-await": "warn",
|
|
1643
|
+
"react-doctor/server-fetch-without-revalidate": "warn",
|
|
1644
|
+
"react-doctor/client-passive-event-listeners": "warn",
|
|
1645
|
+
"react-doctor/client-localstorage-no-version": "warn",
|
|
1646
|
+
"react-doctor/no-inline-bounce-easing": "warn",
|
|
1647
|
+
"react-doctor/no-z-index-9999": "warn",
|
|
1648
|
+
"react-doctor/no-inline-exhaustive-style": "warn",
|
|
1649
|
+
"react-doctor/no-side-tab-border": "warn",
|
|
1650
|
+
"react-doctor/no-pure-black-background": "warn",
|
|
1651
|
+
"react-doctor/no-gradient-text": "warn",
|
|
1652
|
+
"react-doctor/no-dark-mode-glow": "warn",
|
|
1653
|
+
"react-doctor/no-justified-text": "warn",
|
|
1654
|
+
"react-doctor/no-tiny-text": "warn",
|
|
1655
|
+
"react-doctor/no-wide-letter-spacing": "warn",
|
|
1656
|
+
"react-doctor/no-gray-on-colored-background": "warn",
|
|
1657
|
+
"react-doctor/no-layout-transition-inline": "warn",
|
|
1658
|
+
"react-doctor/no-disabled-zoom": "error",
|
|
1659
|
+
"react-doctor/no-outline-none": "warn",
|
|
1660
|
+
"react-doctor/no-long-transition-duration": "warn",
|
|
1661
|
+
"react-doctor/async-parallel": "warn"
|
|
1662
|
+
};
|
|
1663
|
+
const ALL_REACT_DOCTOR_RULE_KEYS = new Set([
|
|
1664
|
+
...Object.keys(GLOBAL_REACT_DOCTOR_RULES),
|
|
1665
|
+
...Object.keys(NEXTJS_RULES),
|
|
1666
|
+
...Object.keys(REACT_NATIVE_RULES),
|
|
1667
|
+
...Object.keys(TANSTACK_START_RULES),
|
|
1668
|
+
...Object.keys(TANSTACK_QUERY_RULES)
|
|
1669
|
+
]);
|
|
1670
|
+
const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, hasTanStackQuery, customRulesOnly = false }) => ({
|
|
1114
1671
|
categories: {
|
|
1115
1672
|
correctness: "off",
|
|
1116
1673
|
suspicious: "off",
|
|
@@ -1120,87 +1677,23 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
|
|
|
1120
1677
|
style: "off",
|
|
1121
1678
|
nursery: "off"
|
|
1122
1679
|
},
|
|
1123
|
-
plugins: [
|
|
1124
|
-
|
|
1125
|
-
"jsx-a11y",
|
|
1126
|
-
...hasReactCompiler ? [] : ["react-perf"]
|
|
1127
|
-
],
|
|
1128
|
-
jsPlugins: [...hasReactCompiler && !customRulesOnly ? [{
|
|
1129
|
-
name: "react-hooks-js",
|
|
1130
|
-
specifier: esmRequire$1.resolve("eslint-plugin-react-hooks")
|
|
1131
|
-
}] : [], pluginPath],
|
|
1680
|
+
plugins: customRulesOnly ? [] : ["react", "jsx-a11y"],
|
|
1681
|
+
jsPlugins: [...resolveReactHooksJsPlugin(hasReactCompiler, customRulesOnly), pluginPath],
|
|
1132
1682
|
rules: {
|
|
1133
1683
|
...customRulesOnly ? {} : BUILTIN_REACT_RULES,
|
|
1134
1684
|
...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
|
|
1135
1685
|
...hasReactCompiler && !customRulesOnly ? REACT_COMPILER_RULES : {},
|
|
1136
|
-
|
|
1137
|
-
"react-doctor/no-fetch-in-effect": "error",
|
|
1138
|
-
"react-doctor/no-cascading-set-state": "warn",
|
|
1139
|
-
"react-doctor/no-effect-event-handler": "warn",
|
|
1140
|
-
"react-doctor/no-derived-useState": "warn",
|
|
1141
|
-
"react-doctor/prefer-useReducer": "warn",
|
|
1142
|
-
"react-doctor/rerender-lazy-state-init": "warn",
|
|
1143
|
-
"react-doctor/rerender-functional-setstate": "warn",
|
|
1144
|
-
"react-doctor/rerender-dependencies": "error",
|
|
1145
|
-
"react-doctor/no-giant-component": "warn",
|
|
1146
|
-
"react-doctor/no-render-in-render": "warn",
|
|
1147
|
-
"react-doctor/no-nested-component-definition": "error",
|
|
1148
|
-
"react-doctor/no-usememo-simple-expression": "warn",
|
|
1149
|
-
"react-doctor/no-layout-property-animation": "error",
|
|
1150
|
-
"react-doctor/rerender-memo-with-default-value": "warn",
|
|
1151
|
-
"react-doctor/rendering-animate-svg-wrapper": "warn",
|
|
1152
|
-
"react-doctor/no-inline-prop-on-memo-component": "warn",
|
|
1153
|
-
"react-doctor/rendering-hydration-no-flicker": "warn",
|
|
1154
|
-
"react-doctor/rendering-script-defer-async": "warn",
|
|
1155
|
-
"react-doctor/no-transition-all": "warn",
|
|
1156
|
-
"react-doctor/no-global-css-variable-animation": "error",
|
|
1157
|
-
"react-doctor/no-large-animated-blur": "warn",
|
|
1158
|
-
"react-doctor/no-scale-from-zero": "warn",
|
|
1159
|
-
"react-doctor/no-permanent-will-change": "warn",
|
|
1160
|
-
"react-doctor/no-secrets-in-client-code": "error",
|
|
1161
|
-
"react-doctor/js-flatmap-filter": "warn",
|
|
1162
|
-
"react-doctor/no-barrel-import": "warn",
|
|
1163
|
-
"react-doctor/no-full-lodash-import": "warn",
|
|
1164
|
-
"react-doctor/no-moment": "warn",
|
|
1165
|
-
"react-doctor/prefer-dynamic-import": "warn",
|
|
1166
|
-
"react-doctor/use-lazy-motion": "warn",
|
|
1167
|
-
"react-doctor/no-undeferred-third-party": "warn",
|
|
1168
|
-
"react-doctor/no-array-index-as-key": "warn",
|
|
1169
|
-
"react-doctor/rendering-conditional-render": "warn",
|
|
1170
|
-
"react-doctor/no-prevent-default": "warn",
|
|
1171
|
-
"react-doctor/server-auth-actions": "error",
|
|
1172
|
-
"react-doctor/server-after-nonblocking": "warn",
|
|
1173
|
-
"react-doctor/client-passive-event-listeners": "warn",
|
|
1174
|
-
"react-doctor/query-stable-query-client": "error",
|
|
1175
|
-
"react-doctor/query-no-rest-destructuring": "warn",
|
|
1176
|
-
"react-doctor/query-no-void-query-fn": "warn",
|
|
1177
|
-
"react-doctor/query-no-query-in-effect": "warn",
|
|
1178
|
-
"react-doctor/query-mutation-missing-invalidation": "warn",
|
|
1179
|
-
"react-doctor/query-no-usequery-for-mutation": "warn",
|
|
1180
|
-
"react-doctor/no-inline-bounce-easing": "warn",
|
|
1181
|
-
"react-doctor/no-z-index-9999": "warn",
|
|
1182
|
-
"react-doctor/no-inline-exhaustive-style": "warn",
|
|
1183
|
-
"react-doctor/no-side-tab-border": "warn",
|
|
1184
|
-
"react-doctor/no-pure-black-background": "warn",
|
|
1185
|
-
"react-doctor/no-gradient-text": "warn",
|
|
1186
|
-
"react-doctor/no-dark-mode-glow": "warn",
|
|
1187
|
-
"react-doctor/no-justified-text": "warn",
|
|
1188
|
-
"react-doctor/no-tiny-text": "warn",
|
|
1189
|
-
"react-doctor/no-wide-letter-spacing": "warn",
|
|
1190
|
-
"react-doctor/no-gray-on-colored-background": "warn",
|
|
1191
|
-
"react-doctor/no-layout-transition-inline": "warn",
|
|
1192
|
-
"react-doctor/no-disabled-zoom": "error",
|
|
1193
|
-
"react-doctor/no-outline-none": "warn",
|
|
1194
|
-
"react-doctor/no-long-transition-duration": "warn",
|
|
1195
|
-
"react-doctor/async-parallel": "warn",
|
|
1686
|
+
...GLOBAL_REACT_DOCTOR_RULES,
|
|
1196
1687
|
...framework === "nextjs" ? NEXTJS_RULES : {},
|
|
1197
1688
|
...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {},
|
|
1198
|
-
...framework === "tanstack-start" ? TANSTACK_START_RULES : {}
|
|
1689
|
+
...framework === "tanstack-start" ? TANSTACK_START_RULES : {},
|
|
1690
|
+
...hasTanStackQuery ? TANSTACK_QUERY_RULES : {}
|
|
1199
1691
|
}
|
|
1200
1692
|
});
|
|
1201
1693
|
//#endregion
|
|
1202
1694
|
//#region src/utils/neutralize-disable-directives.ts
|
|
1203
|
-
const
|
|
1695
|
+
const DISABLE_DIRECTIVE_PATTERN = /(eslint|oxlint)-disable/;
|
|
1696
|
+
const findFilesWithDisableDirectivesViaGit = (rootDirectory, includePaths) => {
|
|
1204
1697
|
const grepArgs = [
|
|
1205
1698
|
"grep",
|
|
1206
1699
|
"-l",
|
|
@@ -1214,14 +1707,65 @@ const findFilesWithDisableDirectives = (rootDirectory, includePaths) => {
|
|
|
1214
1707
|
encoding: "utf-8",
|
|
1215
1708
|
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
1216
1709
|
});
|
|
1217
|
-
if (result.error || result.status === null) return
|
|
1218
|
-
if (result.status
|
|
1710
|
+
if (result.error || result.status === null) return null;
|
|
1711
|
+
if (result.status === 128) return null;
|
|
1219
1712
|
return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
|
|
1220
1713
|
};
|
|
1714
|
+
const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths) => {
|
|
1715
|
+
const matches = [];
|
|
1716
|
+
const checkFile = (relativePath) => {
|
|
1717
|
+
if (!SOURCE_FILE_PATTERN.test(relativePath)) return;
|
|
1718
|
+
const absolutePath = path.join(rootDirectory, relativePath);
|
|
1719
|
+
let content;
|
|
1720
|
+
try {
|
|
1721
|
+
content = fs.readFileSync(absolutePath, "utf-8");
|
|
1722
|
+
} catch {
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
if (DISABLE_DIRECTIVE_PATTERN.test(content)) matches.push(relativePath);
|
|
1726
|
+
};
|
|
1727
|
+
if (includePaths && includePaths.length > 0) {
|
|
1728
|
+
for (const candidate of includePaths) checkFile(candidate);
|
|
1729
|
+
return matches;
|
|
1730
|
+
}
|
|
1731
|
+
const stack = [rootDirectory];
|
|
1732
|
+
while (stack.length > 0) {
|
|
1733
|
+
const current = stack.pop();
|
|
1734
|
+
if (current === void 0) continue;
|
|
1735
|
+
let entries;
|
|
1736
|
+
try {
|
|
1737
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
1738
|
+
} catch {
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
for (const entry of entries) {
|
|
1742
|
+
if (entry.isDirectory()) {
|
|
1743
|
+
if (entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
1744
|
+
stack.push(path.join(current, entry.name));
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
if (!entry.isFile()) continue;
|
|
1748
|
+
const absolute = path.join(current, entry.name);
|
|
1749
|
+
checkFile(path.relative(rootDirectory, absolute));
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return matches;
|
|
1753
|
+
};
|
|
1754
|
+
const findFilesWithDisableDirectives = (rootDirectory, includePaths) => findFilesWithDisableDirectivesViaGit(rootDirectory, includePaths) ?? findFilesWithDisableDirectivesViaFilesystem(rootDirectory, includePaths);
|
|
1221
1755
|
const neutralizeContent = (content) => content.replaceAll("eslint-disable", "eslint_disable").replaceAll("oxlint-disable", "oxlint_disable");
|
|
1222
1756
|
const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
|
|
1223
1757
|
const filePaths = findFilesWithDisableDirectives(rootDirectory, includePaths);
|
|
1224
1758
|
const originalContents = /* @__PURE__ */ new Map();
|
|
1759
|
+
let isRestored = false;
|
|
1760
|
+
const restore = () => {
|
|
1761
|
+
if (isRestored) return;
|
|
1762
|
+
isRestored = true;
|
|
1763
|
+
for (const [absolutePath, originalContent] of originalContents) try {
|
|
1764
|
+
fs.writeFileSync(absolutePath, originalContent);
|
|
1765
|
+
} catch {}
|
|
1766
|
+
};
|
|
1767
|
+
const onExit = () => restore();
|
|
1768
|
+
process.once("exit", onExit);
|
|
1225
1769
|
for (const relativePath of filePaths) {
|
|
1226
1770
|
const absolutePath = path.join(rootDirectory, relativePath);
|
|
1227
1771
|
let originalContent;
|
|
@@ -1237,7 +1781,8 @@ const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
|
|
|
1237
1781
|
}
|
|
1238
1782
|
}
|
|
1239
1783
|
return () => {
|
|
1240
|
-
|
|
1784
|
+
restore();
|
|
1785
|
+
process.removeListener("exit", onExit);
|
|
1241
1786
|
};
|
|
1242
1787
|
};
|
|
1243
1788
|
//#endregion
|
|
@@ -1247,30 +1792,48 @@ const PLUGIN_CATEGORY_MAP = {
|
|
|
1247
1792
|
react: "Correctness",
|
|
1248
1793
|
"react-hooks": "Correctness",
|
|
1249
1794
|
"react-hooks-js": "React Compiler",
|
|
1250
|
-
"react-
|
|
1251
|
-
"jsx-a11y": "Accessibility"
|
|
1795
|
+
"react-doctor": "Other",
|
|
1796
|
+
"jsx-a11y": "Accessibility",
|
|
1797
|
+
knip: "Dead Code"
|
|
1252
1798
|
};
|
|
1253
1799
|
const RULE_CATEGORY_MAP = {
|
|
1254
1800
|
"react-doctor/no-derived-state-effect": "State & Effects",
|
|
1255
1801
|
"react-doctor/no-fetch-in-effect": "State & Effects",
|
|
1256
1802
|
"react-doctor/no-cascading-set-state": "State & Effects",
|
|
1257
1803
|
"react-doctor/no-effect-event-handler": "State & Effects",
|
|
1804
|
+
"react-doctor/no-effect-event-in-deps": "State & Effects",
|
|
1805
|
+
"react-doctor/no-prop-callback-in-effect": "State & Effects",
|
|
1258
1806
|
"react-doctor/no-derived-useState": "State & Effects",
|
|
1259
1807
|
"react-doctor/prefer-useReducer": "State & Effects",
|
|
1260
1808
|
"react-doctor/rerender-lazy-state-init": "Performance",
|
|
1261
1809
|
"react-doctor/rerender-functional-setstate": "Performance",
|
|
1262
1810
|
"react-doctor/rerender-dependencies": "State & Effects",
|
|
1811
|
+
"react-doctor/rerender-state-only-in-handlers": "Performance",
|
|
1812
|
+
"react-doctor/rerender-defer-reads-hook": "Performance",
|
|
1813
|
+
"react-doctor/advanced-event-handler-refs": "Performance",
|
|
1263
1814
|
"react-doctor/no-generic-handler-names": "Architecture",
|
|
1264
1815
|
"react-doctor/no-giant-component": "Architecture",
|
|
1816
|
+
"react-doctor/no-many-boolean-props": "Architecture",
|
|
1817
|
+
"react-doctor/no-react19-deprecated-apis": "Architecture",
|
|
1818
|
+
"react-doctor/no-render-prop-children": "Architecture",
|
|
1265
1819
|
"react-doctor/no-render-in-render": "Architecture",
|
|
1266
1820
|
"react-doctor/no-nested-component-definition": "Correctness",
|
|
1821
|
+
"react-doctor/react-compiler-destructure-method": "Architecture",
|
|
1267
1822
|
"react-doctor/no-usememo-simple-expression": "Performance",
|
|
1268
1823
|
"react-doctor/no-layout-property-animation": "Performance",
|
|
1269
1824
|
"react-doctor/rerender-memo-with-default-value": "Performance",
|
|
1825
|
+
"react-doctor/rerender-memo-before-early-return": "Performance",
|
|
1826
|
+
"react-doctor/rerender-transitions-scroll": "Performance",
|
|
1827
|
+
"react-doctor/rerender-derived-state-from-hook": "Performance",
|
|
1828
|
+
"react-doctor/async-defer-await": "Performance",
|
|
1829
|
+
"react-doctor/async-await-in-loop": "Performance",
|
|
1270
1830
|
"react-doctor/rendering-animate-svg-wrapper": "Performance",
|
|
1831
|
+
"react-doctor/rendering-hoist-jsx": "Performance",
|
|
1832
|
+
"react-doctor/rendering-hydration-mismatch-time": "Correctness",
|
|
1271
1833
|
"react-doctor/rendering-usetransition-loading": "Performance",
|
|
1272
1834
|
"react-doctor/rendering-hydration-no-flicker": "Performance",
|
|
1273
1835
|
"react-doctor/rendering-script-defer-async": "Performance",
|
|
1836
|
+
"react-doctor/no-inline-prop-on-memo-component": "Performance",
|
|
1274
1837
|
"react-doctor/no-transition-all": "Performance",
|
|
1275
1838
|
"react-doctor/no-global-css-variable-animation": "Performance",
|
|
1276
1839
|
"react-doctor/no-large-animated-blur": "Performance",
|
|
@@ -1278,14 +1841,19 @@ const RULE_CATEGORY_MAP = {
|
|
|
1278
1841
|
"react-doctor/no-permanent-will-change": "Performance",
|
|
1279
1842
|
"react-doctor/no-secrets-in-client-code": "Security",
|
|
1280
1843
|
"react-doctor/no-barrel-import": "Bundle Size",
|
|
1844
|
+
"react-doctor/no-dynamic-import-path": "Bundle Size",
|
|
1281
1845
|
"react-doctor/no-full-lodash-import": "Bundle Size",
|
|
1282
1846
|
"react-doctor/no-moment": "Bundle Size",
|
|
1283
1847
|
"react-doctor/prefer-dynamic-import": "Bundle Size",
|
|
1284
1848
|
"react-doctor/use-lazy-motion": "Bundle Size",
|
|
1285
1849
|
"react-doctor/no-undeferred-third-party": "Bundle Size",
|
|
1286
1850
|
"react-doctor/no-array-index-as-key": "Correctness",
|
|
1851
|
+
"react-doctor/no-polymorphic-children": "Architecture",
|
|
1287
1852
|
"react-doctor/rendering-conditional-render": "Correctness",
|
|
1853
|
+
"react-doctor/rendering-svg-precision": "Performance",
|
|
1288
1854
|
"react-doctor/no-prevent-default": "Correctness",
|
|
1855
|
+
"react-doctor/no-document-start-view-transition": "Correctness",
|
|
1856
|
+
"react-doctor/no-flush-sync": "Performance",
|
|
1289
1857
|
"react-doctor/nextjs-no-img-element": "Next.js",
|
|
1290
1858
|
"react-doctor/nextjs-async-client-component": "Next.js",
|
|
1291
1859
|
"react-doctor/nextjs-no-a-element": "Next.js",
|
|
@@ -1304,7 +1872,14 @@ const RULE_CATEGORY_MAP = {
|
|
|
1304
1872
|
"react-doctor/nextjs-no-side-effect-in-get-handler": "Security",
|
|
1305
1873
|
"react-doctor/server-auth-actions": "Server",
|
|
1306
1874
|
"react-doctor/server-after-nonblocking": "Server",
|
|
1875
|
+
"react-doctor/server-no-mutable-module-state": "Server",
|
|
1876
|
+
"react-doctor/server-cache-with-object-literal": "Server",
|
|
1877
|
+
"react-doctor/server-hoist-static-io": "Server",
|
|
1878
|
+
"react-doctor/server-dedup-props": "Server",
|
|
1879
|
+
"react-doctor/server-sequential-independent-await": "Server",
|
|
1880
|
+
"react-doctor/server-fetch-without-revalidate": "Server",
|
|
1307
1881
|
"react-doctor/client-passive-event-listeners": "Performance",
|
|
1882
|
+
"react-doctor/client-localstorage-no-version": "Correctness",
|
|
1308
1883
|
"react-doctor/query-stable-query-client": "TanStack Query",
|
|
1309
1884
|
"react-doctor/query-no-rest-destructuring": "TanStack Query",
|
|
1310
1885
|
"react-doctor/query-no-void-query-fn": "TanStack Query",
|
|
@@ -1327,6 +1902,19 @@ const RULE_CATEGORY_MAP = {
|
|
|
1327
1902
|
"react-doctor/no-outline-none": "Accessibility",
|
|
1328
1903
|
"react-doctor/no-long-transition-duration": "Performance",
|
|
1329
1904
|
"react-doctor/js-flatmap-filter": "Performance",
|
|
1905
|
+
"react-doctor/js-combine-iterations": "Performance",
|
|
1906
|
+
"react-doctor/js-tosorted-immutable": "Performance",
|
|
1907
|
+
"react-doctor/js-hoist-regexp": "Performance",
|
|
1908
|
+
"react-doctor/js-hoist-intl": "Performance",
|
|
1909
|
+
"react-doctor/js-cache-property-access": "Performance",
|
|
1910
|
+
"react-doctor/js-length-check-first": "Performance",
|
|
1911
|
+
"react-doctor/js-min-max-loop": "Performance",
|
|
1912
|
+
"react-doctor/js-set-map-lookups": "Performance",
|
|
1913
|
+
"react-doctor/js-batch-dom-css": "Performance",
|
|
1914
|
+
"react-doctor/js-index-maps": "Performance",
|
|
1915
|
+
"react-doctor/js-cache-storage": "Performance",
|
|
1916
|
+
"react-doctor/js-early-exit": "Performance",
|
|
1917
|
+
"react-doctor/no-eval": "Security",
|
|
1330
1918
|
"react-doctor/async-parallel": "Performance",
|
|
1331
1919
|
"react-doctor/rn-no-raw-text": "React Native",
|
|
1332
1920
|
"react-doctor/rn-no-deprecated-modules": "React Native",
|
|
@@ -1336,6 +1924,22 @@ const RULE_CATEGORY_MAP = {
|
|
|
1336
1924
|
"react-doctor/rn-no-legacy-shadow-styles": "React Native",
|
|
1337
1925
|
"react-doctor/rn-prefer-reanimated": "React Native",
|
|
1338
1926
|
"react-doctor/rn-no-single-element-style-array": "React Native",
|
|
1927
|
+
"react-doctor/rn-prefer-pressable": "React Native",
|
|
1928
|
+
"react-doctor/rn-prefer-expo-image": "React Native",
|
|
1929
|
+
"react-doctor/rn-no-non-native-navigator": "React Native",
|
|
1930
|
+
"react-doctor/rn-no-scroll-state": "React Native",
|
|
1931
|
+
"react-doctor/rn-no-scrollview-mapped-list": "React Native",
|
|
1932
|
+
"react-doctor/rn-no-inline-object-in-list-item": "React Native",
|
|
1933
|
+
"react-doctor/rn-animate-layout-property": "React Native",
|
|
1934
|
+
"react-doctor/rn-prefer-content-inset-adjustment": "React Native",
|
|
1935
|
+
"react-doctor/rn-pressable-shared-value-mutation": "React Native",
|
|
1936
|
+
"react-doctor/rn-list-data-mapped": "React Native",
|
|
1937
|
+
"react-doctor/rn-list-callback-per-row": "React Native",
|
|
1938
|
+
"react-doctor/rn-list-recyclable-without-types": "React Native",
|
|
1939
|
+
"react-doctor/rn-animation-reaction-as-derived": "React Native",
|
|
1940
|
+
"react-doctor/rn-bottom-sheet-prefer-native": "React Native",
|
|
1941
|
+
"react-doctor/rn-scrollview-dynamic-padding": "React Native",
|
|
1942
|
+
"react-doctor/rn-style-prefer-boxshadow": "React Native",
|
|
1339
1943
|
"react-doctor/tanstack-start-route-property-order": "TanStack Start",
|
|
1340
1944
|
"react-doctor/tanstack-start-no-direct-fetch-in-loader": "TanStack Start",
|
|
1341
1945
|
"react-doctor/tanstack-start-server-fn-validate-input": "TanStack Start",
|
|
@@ -1361,17 +1965,44 @@ const RULE_HELP_MAP = {
|
|
|
1361
1965
|
"rerender-lazy-state-init": "Wrap in an arrow function so it only runs once: `useState(() => expensiveComputation())`",
|
|
1362
1966
|
"rerender-functional-setstate": "Use the callback form: `setState(prev => prev + 1)` to always read the latest value",
|
|
1363
1967
|
"rerender-dependencies": "Extract to a useMemo, useRef, or module-level constant so the reference is stable",
|
|
1968
|
+
"no-effect-event-in-deps": "Call the useEffectEvent callback inside the effect body without listing it; its identity is intentionally unstable",
|
|
1969
|
+
"no-prop-callback-in-effect": "Lift the shared state into a Provider so both sides read the same source — no useEffect-driven sync needed",
|
|
1364
1970
|
"no-generic-handler-names": "Rename to describe the action: e.g. `handleSubmit` → `saveUserProfile`, `handleClick` → `toggleSidebar`",
|
|
1365
1971
|
"no-giant-component": "Extract logical sections into focused components: `<UserHeader />`, `<UserActions />`, etc.",
|
|
1972
|
+
"no-many-boolean-props": "Split into compound components or named variants: `<Button.Primary />`, `<DialogConfirm />` instead of stacking `isPrimary`, `isConfirm` flags",
|
|
1973
|
+
"no-react19-deprecated-apis": "Pass `ref` as a regular prop on function components — `forwardRef` is no longer needed in React 19+. Replace `useContext(X)` with `use(X)` for branch-aware context reads.",
|
|
1974
|
+
"no-render-prop-children": "Replace `renderXxx` props with compound subcomponents (e.g. `<Modal.Header>`) or `children` so the parent doesn't dictate every customization point",
|
|
1366
1975
|
"no-render-in-render": "Extract to a named component: `const ListItem = ({ item }) => <div>{item.name}</div>`",
|
|
1367
1976
|
"no-nested-component-definition": "Move to a separate file or to module scope above the parent component",
|
|
1368
1977
|
"no-usememo-simple-expression": "Remove useMemo — property access, math, and ternaries are already cheap without memoization",
|
|
1369
1978
|
"no-layout-property-animation": "Use `transform: translateX()` or `scale()` instead — they run on the compositor and skip layout/paint",
|
|
1370
1979
|
"rerender-memo-with-default-value": "Move to module scope: `const EMPTY_ITEMS: Item[] = []` then use as the default value",
|
|
1371
1980
|
"rendering-animate-svg-wrapper": "Wrap the SVG: `<motion.div animate={...}><svg>...</svg></motion.div>`",
|
|
1981
|
+
"rendering-hoist-jsx": "Move the static JSX to module scope: `const ICON = <svg>...</svg>` outside the component so it isn't recreated each render",
|
|
1982
|
+
"rerender-memo-before-early-return": "Extract the JSX into a memoized child component so the parent's early return short-circuits before the child renders",
|
|
1983
|
+
"rerender-transitions-scroll": "Wrap the setState in startTransition (mark as non-urgent), use useDeferredValue, or stash in a ref + rAF throttle so scroll/pointer events don't trigger a re-render per fire",
|
|
1984
|
+
"rerender-state-only-in-handlers": "Replace useState with useRef when the value is only mutated and never read in render — `ref.current = ...` updates without re-rendering the component",
|
|
1985
|
+
"rerender-defer-reads-hook": "Read the URL state inside the handler (e.g. `new URL(window.location.href).searchParams`) so the component doesn't subscribe and re-render on every URL change",
|
|
1986
|
+
"rerender-derived-state-from-hook": "Use a threshold/media-query hook (e.g. `useMediaQuery(\"(max-width: 767px)\")`) — the component re-renders only when the threshold flips, not every pixel",
|
|
1987
|
+
"advanced-event-handler-refs": "Store the handler in a ref and have the listener read `handlerRef.current()` — the subscription stays put while the latest handler is always called",
|
|
1988
|
+
"async-defer-await": "Move the `await` after the synchronous early-return guard so the skip path stays fast",
|
|
1989
|
+
"async-await-in-loop": "Collect the items and use `await Promise.all(items.map(...))` to run independent operations concurrently",
|
|
1990
|
+
"react-compiler-destructure-method": "Destructure the method up front: `const { push } = useRouter()` then call `push(...)` directly — clearer dependency graph and easier for React Compiler to memoize",
|
|
1991
|
+
"client-localstorage-no-version": "Bake a version into the storage key (e.g. \"myKey:v1\"); a future schema change can ignore old data instead of crashing on it",
|
|
1992
|
+
"server-sequential-independent-await": "Wrap independent awaits in `Promise.all([...])` so they race instead of waterfalling — second call doesn't depend on the first",
|
|
1993
|
+
"server-fetch-without-revalidate": "Pass `{ next: { revalidate: <seconds> } }` (or `cache: \"no-store\"` / `next: { tags: [...] }`) so stale cached data doesn't silently persist",
|
|
1994
|
+
"rn-list-callback-per-row": "Hoist the handler with useCallback at list scope and pass the row id as a primitive prop, so the row's memo() shallow-compare actually hits",
|
|
1995
|
+
"rn-list-recyclable-without-types": "Add `getItemType={item => item.kind}` so FlashList keeps separate recycle pools per item type — heterogeneous rows shouldn't share recycled cells",
|
|
1996
|
+
"rn-style-prefer-boxshadow": "Use the cross-platform CSS `boxShadow` string (RN v7+): `boxShadow: \"0 2px 8px rgba(0,0,0,0.1)\"` instead of platform-specific shadow*/elevation keys",
|
|
1997
|
+
"rendering-hydration-mismatch-time": "Wrap dynamic time/random values in useEffect+useState (client-only) or add suppressHydrationWarning to the parent if intentional",
|
|
1998
|
+
"no-polymorphic-children": "Expose explicit subcomponents (`<Button.Text>`, `<Button.Icon>`) so consumers don't need to switch on `typeof children`",
|
|
1999
|
+
"rendering-svg-precision": "Truncate path/points/transform decimals to 1–2 digits — sub-pixel precision adds bytes with no visible difference",
|
|
2000
|
+
"no-document-start-view-transition": "Render a <ViewTransition> component and update inside startTransition / useDeferredValue — React calls startViewTransition for you",
|
|
2001
|
+
"no-flush-sync": "Use startTransition for non-urgent updates — flushSync forces a sync flush that skips View Transitions and concurrent rendering",
|
|
1372
2002
|
"rendering-usetransition-loading": "Replace with `const [isPending, startTransition] = useTransition()` — avoids a re-render for the loading state",
|
|
1373
2003
|
"rendering-hydration-no-flicker": "Use `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` or add `suppressHydrationWarning` to the element",
|
|
1374
2004
|
"rendering-script-defer-async": "Add `defer` for DOM-dependent scripts or `async` for independent ones (analytics). In Next.js, use `<Script strategy=\"afterInteractive\" />` instead",
|
|
2005
|
+
"no-inline-prop-on-memo-component": "Hoist the inline `() => ...` / `[]` / `{}` to a stable reference (useMemo, useCallback, or module scope) so the memoized child doesn't re-render every parent render",
|
|
1375
2006
|
"no-transition-all": "List specific properties: `transition: \"opacity 200ms, transform 200ms\"` — or in Tailwind use `transition-colors`, `transition-opacity`, or `transition-transform`",
|
|
1376
2007
|
"no-global-css-variable-animation": "Set the variable on the nearest element instead of a parent, or use `@property` with `inherits: false` to prevent cascade. Better yet, use targeted `element.style.transform` updates",
|
|
1377
2008
|
"no-large-animated-blur": "Keep blur radius under 10px, or apply blur to a smaller element. Large blurs multiply GPU memory usage with layer size",
|
|
@@ -1379,6 +2010,7 @@ const RULE_HELP_MAP = {
|
|
|
1379
2010
|
"no-permanent-will-change": "Add will-change on animation start (`onMouseEnter`) and remove on end (`onAnimationEnd`). Permanent promotion wastes GPU memory and can degrade performance",
|
|
1380
2011
|
"no-secrets-in-client-code": "Move to server-side `process.env.SECRET_NAME`. Only `NEXT_PUBLIC_*` vars are safe for the client (and should not contain secrets)",
|
|
1381
2012
|
"no-barrel-import": "Import from the direct path: `import { Button } from './components/Button'` instead of `./components`",
|
|
2013
|
+
"no-dynamic-import-path": "Use a string-literal path: `import('./feature/heavy.js')` so the bundler can split this chunk",
|
|
1382
2014
|
"no-full-lodash-import": "Import the specific function: `import debounce from 'lodash/debounce'` — saves ~70kb",
|
|
1383
2015
|
"no-moment": "Replace with `import { format } from 'date-fns'` (tree-shakeable) or `import dayjs from 'dayjs'` (2kb)",
|
|
1384
2016
|
"prefer-dynamic-import": "Use `const Component = dynamic(() => import('library'), { ssr: false })` from next/dynamic or React.lazy()",
|
|
@@ -1420,7 +2052,11 @@ const RULE_HELP_MAP = {
|
|
|
1420
2052
|
"nextjs-no-side-effect-in-get-handler": "Move the side effect to a POST handler and use a <form> or fetch with method POST — GET requests can be triggered by prefetching and are vulnerable to CSRF",
|
|
1421
2053
|
"server-auth-actions": "Add `const session = await auth()` at the top and throw/redirect if unauthorized before any data access",
|
|
1422
2054
|
"server-after-nonblocking": "`import { after } from 'next/server'` then wrap: `after(() => analytics.track(...))` — response isn't blocked",
|
|
1423
|
-
"
|
|
2055
|
+
"server-no-mutable-module-state": "Move per-request data into the action body, headers/cookies, or a request-scope (React.cache, AsyncLocalStorage). Module-scope `let`/`var` is shared across requests.",
|
|
2056
|
+
"server-cache-with-object-literal": "Pass primitives to React.cache()-wrapped functions — argument identity (not deep equality) is the dedup key, so a fresh `{}` per render bypasses the cache",
|
|
2057
|
+
"server-hoist-static-io": "Hoist the read to module scope: `const FONT_DATA = await fetch(new URL('./fonts/Inter.ttf', import.meta.url)).then(r => r.arrayBuffer())` runs once at module load",
|
|
2058
|
+
"server-dedup-props": "Pass the source array once and derive the projection on the client — passing both doubles RSC serialization bytes",
|
|
2059
|
+
"client-passive-event-listeners": "Add `{ passive: true }` as the third argument: `addEventListener('scroll', handler, { passive: true })`. Only do this if the handler does NOT call `event.preventDefault()` — passive listeners silently ignore `preventDefault()`, which breaks features like pull-to-refresh suppression, custom gestures, and nested-scroll containment.",
|
|
1424
2060
|
"query-stable-query-client": "Move `new QueryClient()` to module scope or wrap in `useState(() => new QueryClient())` — recreating it on every render resets the entire cache",
|
|
1425
2061
|
"query-no-rest-destructuring": "Destructure only the fields you need: `const { data, isLoading } = useQuery(...)` — rest destructuring subscribes to all fields and causes extra re-renders",
|
|
1426
2062
|
"query-no-void-query-fn": "queryFn must return a value for the cache. Use the `enabled` option to conditionally disable the query instead of returning undefined",
|
|
@@ -1428,6 +2064,19 @@ const RULE_HELP_MAP = {
|
|
|
1428
2064
|
"query-mutation-missing-invalidation": "Add `onSuccess: () => queryClient.invalidateQueries({ queryKey: ['...'] })` so cached data stays in sync after the mutation",
|
|
1429
2065
|
"query-no-usequery-for-mutation": "Use `useMutation()` for POST/PUT/DELETE — it provides onSuccess/onError callbacks, doesn't auto-refetch, and correctly models write operations",
|
|
1430
2066
|
"js-flatmap-filter": "Use `.flatMap(item => condition ? [value] : [])` — transforms and filters in a single pass instead of creating an intermediate array",
|
|
2067
|
+
"js-hoist-intl": "Hoist `new Intl.NumberFormat(...)` to module scope or wrap in `useMemo` — Intl constructors allocate dozens of objects per locale lookup",
|
|
2068
|
+
"js-cache-property-access": "Hoist the deep member access into a const at the top of the loop body: `const { x, y } = obj.deeply.nested`",
|
|
2069
|
+
"js-length-check-first": "Short-circuit with `a.length === b.length && a.every((x, i) => x === b[i])` — unequal-length arrays exit immediately",
|
|
2070
|
+
"js-combine-iterations": "Combine `.map().filter()` (or similar chains) into a single pass with `.reduce()` or a `for...of` loop to avoid iterating the array twice",
|
|
2071
|
+
"js-tosorted-immutable": "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` for immutable sorting without the spread allocation",
|
|
2072
|
+
"js-hoist-regexp": "Hoist `new RegExp(...)` (or large regex literals) to a module-level constant so it isn't recompiled on every loop iteration",
|
|
2073
|
+
"js-min-max-loop": "Use `Math.min(...array)` / `Math.max(...array)` instead of sorting just to read the first or last element",
|
|
2074
|
+
"js-set-map-lookups": "Use a `Set` or `Map` for repeated membership tests / keyed lookups — `Array.includes`/`find` is O(n) per call",
|
|
2075
|
+
"js-batch-dom-css": "Batch DOM/CSS reads and writes — interleaving them inside a loop causes layout thrashing. Read first, then write",
|
|
2076
|
+
"js-index-maps": "Build an index `Map` once outside the loop instead of `array.find(...)` inside it",
|
|
2077
|
+
"js-cache-storage": "Cache repeated `localStorage`/`sessionStorage` reads in a local variable — each access serializes/deserializes",
|
|
2078
|
+
"js-early-exit": "Add an early `return` / `continue` to flatten deep nesting and short-circuit when the predicate is already known",
|
|
2079
|
+
"no-eval": "Use `JSON.parse` for serialized data, `Function(...)` (still careful) for trusted templates, or refactor to avoid dynamic code execution",
|
|
1431
2080
|
"async-parallel": "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
|
|
1432
2081
|
"rn-no-raw-text": "Wrap text in a `<Text>` component: `<Text>{value}</Text>` — raw strings outside `<Text>` crash on React Native",
|
|
1433
2082
|
"rn-no-deprecated-modules": "Import from the community package instead — deprecated modules were removed from the react-native core",
|
|
@@ -1437,6 +2086,19 @@ const RULE_HELP_MAP = {
|
|
|
1437
2086
|
"rn-no-legacy-shadow-styles": "Use `boxShadow` for cross-platform shadows on the new architecture instead of platform-specific shadow properties",
|
|
1438
2087
|
"rn-prefer-reanimated": "Use `import Animated from 'react-native-reanimated'` — animations run on the UI thread instead of the JS thread",
|
|
1439
2088
|
"rn-no-single-element-style-array": "Use `style={value}` instead of `style={[value]}` — single-element arrays add unnecessary allocation",
|
|
2089
|
+
"rn-prefer-pressable": "Use `<Pressable>` from react-native (or react-native-gesture-handler) instead of legacy Touchable* components",
|
|
2090
|
+
"rn-prefer-expo-image": "Use `<Image>` from `expo-image` instead of `react-native` — same prop API, plus disk + memory caching, placeholders, and crossfades",
|
|
2091
|
+
"rn-no-non-native-navigator": "Use `@react-navigation/native-stack` (or `native-tabs` in v7+) for platform-native transitions and gestures",
|
|
2092
|
+
"rn-no-scroll-state": "Track scroll position with a Reanimated shared value (`useAnimatedScrollHandler`) or a ref — `setState` on every scroll event causes re-render storms",
|
|
2093
|
+
"rn-no-scrollview-mapped-list": "Use FlashList, LegendList, or FlatList — `<ScrollView>{items.map(...)}</ScrollView>` mounts every row in memory",
|
|
2094
|
+
"rn-no-inline-object-in-list-item": "Hoist style/object props outside renderItem (StyleSheet.create, useMemo at list scope, or pass primitives) so memo() row components stop bailing",
|
|
2095
|
+
"rn-animate-layout-property": "Animate `transform: [{ translateX/Y }, { scale }]` and `opacity` instead of layout props — layout runs on the JS thread; transform/opacity run on the GPU compositor",
|
|
2096
|
+
"rn-prefer-content-inset-adjustment": "Drop the SafeAreaView wrapper and set `contentInsetAdjustmentBehavior=\"automatic\"` on the ScrollView for native safe-area handling",
|
|
2097
|
+
"rn-pressable-shared-value-mutation": "Wrap in <GestureDetector gesture={Gesture.Tap()...}> so the press animation runs on the UI thread instead of bouncing across the JS bridge",
|
|
2098
|
+
"rn-list-data-mapped": "Wrap the projection in `useMemo(() => items.map(...), [items])` so the list's `data` prop has a stable reference across parent renders",
|
|
2099
|
+
"rn-animation-reaction-as-derived": "Replace useAnimatedReaction with `useDerivedValue(() => ..., [deps])` — shorter, native dependency tracking, no side-effect implication",
|
|
2100
|
+
"rn-bottom-sheet-prefer-native": "Use `<Modal presentationStyle=\"formSheet\">` (RN v7+) for native gesture handling and snap points",
|
|
2101
|
+
"rn-scrollview-dynamic-padding": "Use `contentInset={{ bottom: dynamicValue }}` — the OS applies it as an offset without reflowing the scroll content",
|
|
1440
2102
|
"tanstack-start-route-property-order": "Follow the order: params/validateSearch → loaderDeps → context → beforeLoad → loader → head. See https://tanstack.com/router/latest/docs/eslint/create-route-property-order",
|
|
1441
2103
|
"tanstack-start-no-direct-fetch-in-loader": "Use `createServerFn()` from @tanstack/react-start — provides type-safe RPC, input validation, and proper server/client code splitting",
|
|
1442
2104
|
"tanstack-start-server-fn-validate-input": "Add `.inputValidator(schema)` before `.handler()` — data crosses a network boundary and must be validated at runtime",
|
|
@@ -1491,35 +2153,61 @@ const resolvePluginPath = () => {
|
|
|
1491
2153
|
const resolveDiagnosticCategory = (plugin, rule) => {
|
|
1492
2154
|
return RULE_CATEGORY_MAP[`${plugin}/${rule}`] ?? PLUGIN_CATEGORY_MAP[plugin] ?? "Other";
|
|
1493
2155
|
};
|
|
1494
|
-
const
|
|
1495
|
-
const
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
for (const filePath of includePaths) {
|
|
1501
|
-
const entryLength = filePath.length + 1;
|
|
1502
|
-
const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
|
|
1503
|
-
const exceedsFileCount = currentBatch.length >= 500;
|
|
1504
|
-
if (exceedsArgLength || exceedsFileCount) {
|
|
1505
|
-
batches.push(currentBatch);
|
|
1506
|
-
currentBatch = [];
|
|
1507
|
-
currentBatchLength = baseArgsLength;
|
|
1508
|
-
}
|
|
1509
|
-
currentBatch.push(filePath);
|
|
1510
|
-
currentBatchLength += entryLength;
|
|
2156
|
+
const SANITIZED_ENV = (() => {
|
|
2157
|
+
const sanitized = {};
|
|
2158
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
2159
|
+
if (name === "NODE_OPTIONS" || name === "NODE_DEBUG") continue;
|
|
2160
|
+
if (name.startsWith("npm_config_")) continue;
|
|
2161
|
+
sanitized[name] = value;
|
|
1511
2162
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
2163
|
+
return sanitized;
|
|
2164
|
+
})();
|
|
2165
|
+
const OXLINT_SPAWN_TIMEOUT_MS = 5 * 6e4;
|
|
1515
2166
|
const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
|
|
1516
|
-
const child = spawn(nodeBinaryPath, args, {
|
|
2167
|
+
const child = spawn(nodeBinaryPath, args, {
|
|
2168
|
+
cwd: rootDirectory,
|
|
2169
|
+
env: SANITIZED_ENV
|
|
2170
|
+
});
|
|
2171
|
+
const timeoutHandle = setTimeout(() => {
|
|
2172
|
+
child.kill("SIGKILL");
|
|
2173
|
+
reject(/* @__PURE__ */ new Error(`oxlint did not return within ${OXLINT_SPAWN_TIMEOUT_MS / 1e3}s — please report`));
|
|
2174
|
+
}, OXLINT_SPAWN_TIMEOUT_MS);
|
|
2175
|
+
timeoutHandle.unref?.();
|
|
1517
2176
|
const stdoutBuffers = [];
|
|
1518
2177
|
const stderrBuffers = [];
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
2178
|
+
let stdoutByteCount = 0;
|
|
2179
|
+
let stderrByteCount = 0;
|
|
2180
|
+
let didKillForSize = false;
|
|
2181
|
+
const killIfTooLarge = (incomingBytes, isStdout) => {
|
|
2182
|
+
if (isStdout) stdoutByteCount += incomingBytes;
|
|
2183
|
+
else stderrByteCount += incomingBytes;
|
|
2184
|
+
if (stdoutByteCount + stderrByteCount > 52428800 && !didKillForSize) {
|
|
2185
|
+
didKillForSize = true;
|
|
2186
|
+
child.kill("SIGKILL");
|
|
2187
|
+
return true;
|
|
2188
|
+
}
|
|
2189
|
+
return false;
|
|
2190
|
+
};
|
|
2191
|
+
child.stdout.on("data", (buffer) => {
|
|
2192
|
+
if (didKillForSize) return;
|
|
2193
|
+
stdoutBuffers.push(buffer);
|
|
2194
|
+
killIfTooLarge(buffer.length, true);
|
|
2195
|
+
});
|
|
2196
|
+
child.stderr.on("data", (buffer) => {
|
|
2197
|
+
if (didKillForSize) return;
|
|
2198
|
+
stderrBuffers.push(buffer);
|
|
2199
|
+
killIfTooLarge(buffer.length, false);
|
|
2200
|
+
});
|
|
2201
|
+
child.on("error", (error) => {
|
|
2202
|
+
clearTimeout(timeoutHandle);
|
|
2203
|
+
reject(/* @__PURE__ */ new Error(`Failed to run oxlint: ${error.message}`));
|
|
2204
|
+
});
|
|
2205
|
+
child.on("close", (_code, signal) => {
|
|
2206
|
+
clearTimeout(timeoutHandle);
|
|
2207
|
+
if (didKillForSize) {
|
|
2208
|
+
reject(/* @__PURE__ */ new Error(`oxlint output exceeded ${PROXY_OUTPUT_MAX_BYTES} bytes — scan a smaller subset with --diff or --staged`));
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
1523
2211
|
if (signal) {
|
|
1524
2212
|
const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
|
|
1525
2213
|
const hint = signal === "SIGABRT" ? " (out of memory — try scanning fewer files with --diff)" : "";
|
|
@@ -1538,15 +2226,23 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
1538
2226
|
resolve(output);
|
|
1539
2227
|
});
|
|
1540
2228
|
});
|
|
2229
|
+
const isOxlintOutput = (value) => {
|
|
2230
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2231
|
+
const candidate = value;
|
|
2232
|
+
return Array.isArray(candidate.diagnostics);
|
|
2233
|
+
};
|
|
1541
2234
|
const parseOxlintOutput = (stdout) => {
|
|
1542
2235
|
if (!stdout) return [];
|
|
1543
|
-
|
|
2236
|
+
const jsonStart = stdout.indexOf("{");
|
|
2237
|
+
const sanitizedStdout = jsonStart > 0 ? stdout.slice(jsonStart) : stdout;
|
|
2238
|
+
let parsed;
|
|
1544
2239
|
try {
|
|
1545
|
-
|
|
2240
|
+
parsed = JSON.parse(sanitizedStdout);
|
|
1546
2241
|
} catch {
|
|
1547
2242
|
throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, 200)}`);
|
|
1548
2243
|
}
|
|
1549
|
-
|
|
2244
|
+
if (!isOxlintOutput(parsed)) throw new Error(`Unexpected oxlint output shape: ${stdout.slice(0, 200)}`);
|
|
2245
|
+
return parsed.diagnostics.filter((diagnostic) => diagnostic.code && JSX_FILE_PATTERN.test(diagnostic.filename)).map((diagnostic) => {
|
|
1550
2246
|
const { plugin, rule } = parseRuleCode(diagnostic.code);
|
|
1551
2247
|
const primaryLabel = diagnostic.labels[0];
|
|
1552
2248
|
const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule);
|
|
@@ -1563,18 +2259,48 @@ const parseOxlintOutput = (stdout) => {
|
|
|
1563
2259
|
};
|
|
1564
2260
|
});
|
|
1565
2261
|
};
|
|
1566
|
-
const
|
|
2262
|
+
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
2263
|
+
const resolveTsConfigRelativePath = (rootDirectory) => {
|
|
2264
|
+
for (const filename of TSCONFIG_FILENAMES) if (fs.existsSync(path.join(rootDirectory, filename))) return `./${filename}`;
|
|
2265
|
+
return null;
|
|
2266
|
+
};
|
|
2267
|
+
let didValidateRuleRegistration = false;
|
|
2268
|
+
const validateRuleRegistration = () => {
|
|
2269
|
+
if (didValidateRuleRegistration) return;
|
|
2270
|
+
didValidateRuleRegistration = true;
|
|
2271
|
+
const missingHelp = [];
|
|
2272
|
+
const missingCategory = [];
|
|
2273
|
+
for (const fullKey of ALL_REACT_DOCTOR_RULE_KEYS) {
|
|
2274
|
+
const ruleName = fullKey.replace(/^react-doctor\//, "");
|
|
2275
|
+
if (!(fullKey in RULE_CATEGORY_MAP)) missingCategory.push(fullKey);
|
|
2276
|
+
if (!(ruleName in RULE_HELP_MAP)) missingHelp.push(fullKey);
|
|
2277
|
+
}
|
|
2278
|
+
if (missingCategory.length > 0 || missingHelp.length > 0) {
|
|
2279
|
+
const detail = [missingCategory.length > 0 ? `Missing RULE_CATEGORY_MAP entries: ${missingCategory.join(", ")}` : null, missingHelp.length > 0 ? `Missing RULE_HELP_MAP entries: ${missingHelp.join(", ")}` : null].filter((entry) => entry !== null).join("; ");
|
|
2280
|
+
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2283
|
+
const runOxlint = async (options) => {
|
|
2284
|
+
const { rootDirectory, hasTypeScript, framework, hasReactCompiler, hasTanStackQuery, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true } = options;
|
|
2285
|
+
validateRuleRegistration();
|
|
1567
2286
|
if (includePaths !== void 0 && includePaths.length === 0) return [];
|
|
1568
|
-
const
|
|
2287
|
+
const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
2288
|
+
const configPath = path.join(configDirectory, "oxlintrc.json");
|
|
1569
2289
|
const config = createOxlintConfig({
|
|
1570
2290
|
pluginPath: resolvePluginPath(),
|
|
1571
2291
|
framework,
|
|
1572
2292
|
hasReactCompiler,
|
|
2293
|
+
hasTanStackQuery,
|
|
1573
2294
|
customRulesOnly
|
|
1574
2295
|
});
|
|
1575
|
-
const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
2296
|
+
const restoreDisableDirectives = respectInlineDisables ? () => {} : neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
1576
2297
|
try {
|
|
1577
|
-
fs.
|
|
2298
|
+
const fileHandle = fs.openSync(configPath, "wx", 384);
|
|
2299
|
+
try {
|
|
2300
|
+
fs.writeFileSync(fileHandle, JSON.stringify(config));
|
|
2301
|
+
} finally {
|
|
2302
|
+
fs.closeSync(fileHandle);
|
|
2303
|
+
}
|
|
1578
2304
|
const baseArgs = [
|
|
1579
2305
|
resolveOxlintBinary(),
|
|
1580
2306
|
"-c",
|
|
@@ -1582,7 +2308,16 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
|
|
|
1582
2308
|
"--format",
|
|
1583
2309
|
"json"
|
|
1584
2310
|
];
|
|
1585
|
-
if (hasTypeScript)
|
|
2311
|
+
if (hasTypeScript) {
|
|
2312
|
+
const tsconfigRelativePath = resolveTsConfigRelativePath(rootDirectory);
|
|
2313
|
+
if (tsconfigRelativePath) baseArgs.push("--tsconfig", tsconfigRelativePath);
|
|
2314
|
+
}
|
|
2315
|
+
const combinedPatterns = collectIgnorePatterns(rootDirectory);
|
|
2316
|
+
if (combinedPatterns.length > 0) {
|
|
2317
|
+
const combinedIgnorePath = path.join(configDirectory, "combined.ignore");
|
|
2318
|
+
fs.writeFileSync(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
|
|
2319
|
+
baseArgs.push("--ignore-path", combinedIgnorePath);
|
|
2320
|
+
}
|
|
1586
2321
|
const fileBatches = includePaths !== void 0 ? batchIncludePaths(baseArgs, includePaths) : [["."]];
|
|
1587
2322
|
const allDiagnostics = [];
|
|
1588
2323
|
for (const batch of fileBatches) {
|
|
@@ -1592,71 +2327,111 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
|
|
|
1592
2327
|
return allDiagnostics;
|
|
1593
2328
|
} finally {
|
|
1594
2329
|
restoreDisableDirectives();
|
|
1595
|
-
|
|
2330
|
+
fs.rmSync(configDirectory, {
|
|
2331
|
+
recursive: true,
|
|
2332
|
+
force: true
|
|
2333
|
+
});
|
|
1596
2334
|
}
|
|
1597
2335
|
};
|
|
1598
2336
|
//#endregion
|
|
1599
2337
|
//#region src/utils/get-diff-files.ts
|
|
2338
|
+
const runGit = (cwd, args) => {
|
|
2339
|
+
const result = spawnSync("git", args, {
|
|
2340
|
+
cwd,
|
|
2341
|
+
stdio: [
|
|
2342
|
+
"ignore",
|
|
2343
|
+
"pipe",
|
|
2344
|
+
"pipe"
|
|
2345
|
+
],
|
|
2346
|
+
encoding: "utf-8"
|
|
2347
|
+
});
|
|
2348
|
+
if (result.error || result.status !== 0) return null;
|
|
2349
|
+
return result.stdout.toString().trim();
|
|
2350
|
+
};
|
|
1600
2351
|
const getCurrentBranch = (directory) => {
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
return null;
|
|
1609
|
-
}
|
|
2352
|
+
const branch = runGit(directory, [
|
|
2353
|
+
"rev-parse",
|
|
2354
|
+
"--abbrev-ref",
|
|
2355
|
+
"HEAD"
|
|
2356
|
+
]);
|
|
2357
|
+
if (!branch) return null;
|
|
2358
|
+
return branch === "HEAD" ? null : branch;
|
|
1610
2359
|
};
|
|
1611
2360
|
const detectDefaultBranch = (directory) => {
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
});
|
|
1623
|
-
return candidate;
|
|
1624
|
-
} catch {}
|
|
1625
|
-
return null;
|
|
2361
|
+
const reference = runGit(directory, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
|
|
2362
|
+
if (reference) return reference.replace("refs/remotes/origin/", "");
|
|
2363
|
+
const output = runGit(directory, [
|
|
2364
|
+
"for-each-ref",
|
|
2365
|
+
"--format=%(refname:short)",
|
|
2366
|
+
...DEFAULT_BRANCH_CANDIDATES.map((candidate) => `refs/heads/${candidate}`)
|
|
2367
|
+
]);
|
|
2368
|
+
if (output) {
|
|
2369
|
+
const firstLine = output.split("\n")[0]?.trim();
|
|
2370
|
+
if (firstLine) return firstLine;
|
|
1626
2371
|
}
|
|
2372
|
+
return null;
|
|
2373
|
+
};
|
|
2374
|
+
const branchExists = (directory, branch) => {
|
|
2375
|
+
const result = spawnSync("git", [
|
|
2376
|
+
"rev-parse",
|
|
2377
|
+
"--verify",
|
|
2378
|
+
branch
|
|
2379
|
+
], {
|
|
2380
|
+
cwd: directory,
|
|
2381
|
+
stdio: [
|
|
2382
|
+
"ignore",
|
|
2383
|
+
"pipe",
|
|
2384
|
+
"pipe"
|
|
2385
|
+
]
|
|
2386
|
+
});
|
|
2387
|
+
return !result.error && result.status === 0;
|
|
2388
|
+
};
|
|
2389
|
+
const runGitNullSeparated = (cwd, args) => {
|
|
2390
|
+
const result = spawnSync("git", args, {
|
|
2391
|
+
cwd,
|
|
2392
|
+
stdio: [
|
|
2393
|
+
"ignore",
|
|
2394
|
+
"pipe",
|
|
2395
|
+
"pipe"
|
|
2396
|
+
],
|
|
2397
|
+
encoding: "utf-8"
|
|
2398
|
+
});
|
|
2399
|
+
if (result.error || result.status !== 0) return null;
|
|
2400
|
+
return result.stdout.toString().split("\0").filter((filePath) => filePath.length > 0);
|
|
1627
2401
|
};
|
|
1628
2402
|
const getChangedFilesSinceBranch = (directory, baseBranch) => {
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
2403
|
+
const mergeBase = runGit(directory, [
|
|
2404
|
+
"merge-base",
|
|
2405
|
+
baseBranch,
|
|
2406
|
+
"HEAD"
|
|
2407
|
+
]);
|
|
2408
|
+
if (mergeBase === null) return null;
|
|
2409
|
+
return runGitNullSeparated(directory, [
|
|
2410
|
+
"diff",
|
|
2411
|
+
"-z",
|
|
2412
|
+
"--name-only",
|
|
2413
|
+
"--diff-filter=ACMR",
|
|
2414
|
+
"--relative",
|
|
2415
|
+
mergeBase
|
|
2416
|
+
]);
|
|
1642
2417
|
};
|
|
1643
2418
|
const getUncommittedChangedFiles = (directory) => {
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
return [];
|
|
1653
|
-
}
|
|
2419
|
+
return runGitNullSeparated(directory, [
|
|
2420
|
+
"diff",
|
|
2421
|
+
"-z",
|
|
2422
|
+
"--name-only",
|
|
2423
|
+
"--diff-filter=ACMR",
|
|
2424
|
+
"--relative",
|
|
2425
|
+
"HEAD"
|
|
2426
|
+
]) ?? [];
|
|
1654
2427
|
};
|
|
1655
2428
|
const getDiffInfo = (directory, explicitBaseBranch) => {
|
|
2429
|
+
if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) throw new Error("Diff base branch cannot be empty.");
|
|
1656
2430
|
const currentBranch = getCurrentBranch(directory);
|
|
1657
2431
|
if (!currentBranch) return null;
|
|
1658
2432
|
const baseBranch = explicitBaseBranch ?? detectDefaultBranch(directory);
|
|
1659
2433
|
if (!baseBranch) return null;
|
|
2434
|
+
if (explicitBaseBranch && !branchExists(directory, explicitBaseBranch)) throw new Error(`Diff base branch "${explicitBaseBranch}" does not exist (run \`git fetch\` to update remote refs).`);
|
|
1660
2435
|
if (currentBranch === baseBranch) {
|
|
1661
2436
|
const uncommittedFiles = getUncommittedChangedFiles(directory);
|
|
1662
2437
|
if (uncommittedFiles.length === 0) return null;
|
|
@@ -1667,15 +2442,40 @@ const getDiffInfo = (directory, explicitBaseBranch) => {
|
|
|
1667
2442
|
isCurrentChanges: true
|
|
1668
2443
|
};
|
|
1669
2444
|
}
|
|
2445
|
+
const changedFiles = getChangedFilesSinceBranch(directory, baseBranch);
|
|
2446
|
+
if (changedFiles === null) return null;
|
|
1670
2447
|
return {
|
|
1671
2448
|
currentBranch,
|
|
1672
2449
|
baseBranch,
|
|
1673
|
-
changedFiles
|
|
2450
|
+
changedFiles
|
|
1674
2451
|
};
|
|
1675
2452
|
};
|
|
1676
2453
|
const filterSourceFiles = (filePaths) => filePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));
|
|
1677
2454
|
//#endregion
|
|
1678
2455
|
//#region src/index.ts
|
|
2456
|
+
const clearCaches = () => {
|
|
2457
|
+
clearProjectCache();
|
|
2458
|
+
clearConfigCache();
|
|
2459
|
+
clearPackageJsonCache();
|
|
2460
|
+
clearIgnorePatternsCache();
|
|
2461
|
+
};
|
|
2462
|
+
const toJsonReport = (result, options) => buildJsonReport({
|
|
2463
|
+
version: options.version,
|
|
2464
|
+
directory: options.directory ?? result.project.rootDirectory,
|
|
2465
|
+
mode: options.mode ?? "full",
|
|
2466
|
+
diff: null,
|
|
2467
|
+
scans: [{
|
|
2468
|
+
directory: result.project.rootDirectory,
|
|
2469
|
+
result: {
|
|
2470
|
+
diagnostics: result.diagnostics,
|
|
2471
|
+
score: result.score,
|
|
2472
|
+
skippedChecks: [],
|
|
2473
|
+
project: result.project,
|
|
2474
|
+
elapsedMilliseconds: result.elapsedMilliseconds
|
|
2475
|
+
}
|
|
2476
|
+
}],
|
|
2477
|
+
totalElapsedMilliseconds: result.elapsedMilliseconds
|
|
2478
|
+
});
|
|
1679
2479
|
const diagnose = async (directory, options = {}) => {
|
|
1680
2480
|
const resolvedDirectory = path.resolve(directory);
|
|
1681
2481
|
const userConfig = loadConfig(resolvedDirectory);
|
|
@@ -1690,7 +2490,16 @@ const diagnose = async (directory, options = {}) => {
|
|
|
1690
2490
|
calculateDiagnosticsScore: calculateScore,
|
|
1691
2491
|
getExtraDiagnostics: () => isDiffMode ? [] : checkReducedMotion(resolvedDirectory),
|
|
1692
2492
|
createRunners: ({ resolvedDirectory: projectRoot, projectInfo, userConfig: config }) => ({
|
|
1693
|
-
runLint: () => runOxlint(
|
|
2493
|
+
runLint: () => runOxlint({
|
|
2494
|
+
rootDirectory: projectRoot,
|
|
2495
|
+
hasTypeScript: projectInfo.hasTypeScript,
|
|
2496
|
+
framework: projectInfo.framework,
|
|
2497
|
+
hasReactCompiler: projectInfo.hasReactCompiler,
|
|
2498
|
+
hasTanStackQuery: projectInfo.hasTanStackQuery,
|
|
2499
|
+
includePaths: lintIncludePaths,
|
|
2500
|
+
customRulesOnly: config?.customRulesOnly ?? false,
|
|
2501
|
+
respectInlineDisables: options.respectInlineDisables ?? config?.respectInlineDisables ?? true
|
|
2502
|
+
}),
|
|
1694
2503
|
runDeadCode: () => runKnip(projectRoot)
|
|
1695
2504
|
})
|
|
1696
2505
|
}, {
|
|
@@ -1699,6 +2508,6 @@ const diagnose = async (directory, options = {}) => {
|
|
|
1699
2508
|
});
|
|
1700
2509
|
};
|
|
1701
2510
|
//#endregion
|
|
1702
|
-
export { diagnose, filterSourceFiles, getDiffInfo };
|
|
2511
|
+
export { buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, summarizeDiagnostics, toJsonReport };
|
|
1703
2512
|
|
|
1704
2513
|
//# sourceMappingURL=index.js.map
|