react-doctor 0.0.41 → 0.0.44
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 +169 -41
- package/bin/react-doctor.js +13 -0
- package/dist/{process-browser-diagnostics-DpaZeYLI.js → browser-BOxs7MrK.js} +39 -45
- package/dist/{diagnose-browser-B17IqMa3.d.ts → browser-Dcq3yn-p.d.ts} +32 -17
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -3
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +1470 -517
- package/dist/index.d.ts +119 -12
- package/dist/index.js +1178 -363
- package/dist/react-doctor-plugin.js +2339 -169
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +2 -3
- package/package.json +35 -13
- package/dist/cli.js.map +0 -1
- package/dist/diagnose-browser-B17IqMa3.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/process-browser-diagnostics-DpaZeYLI.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,20 +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
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.`;
|
|
18
37
|
//#endregion
|
|
19
38
|
//#region src/utils/match-glob-pattern.ts
|
|
20
39
|
const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
|
|
@@ -42,7 +61,6 @@ const compileGlobPattern = (pattern) => {
|
|
|
42
61
|
regexSource += "$";
|
|
43
62
|
return new RegExp(regexSource);
|
|
44
63
|
};
|
|
45
|
-
|
|
46
64
|
//#endregion
|
|
47
65
|
//#region src/utils/is-ignored-file.ts
|
|
48
66
|
const toRelativePath = (filePath, rootDirectory) => {
|
|
@@ -51,13 +69,16 @@ const toRelativePath = (filePath, rootDirectory) => {
|
|
|
51
69
|
if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
|
|
52
70
|
return normalizedFilePath.replace(/^\.\//, "");
|
|
53
71
|
};
|
|
54
|
-
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
|
+
};
|
|
55
77
|
const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
|
|
56
78
|
if (patterns.length === 0) return false;
|
|
57
79
|
const relativePath = toRelativePath(filePath, rootDirectory);
|
|
58
80
|
return patterns.some((pattern) => pattern.test(relativePath));
|
|
59
81
|
};
|
|
60
|
-
|
|
61
82
|
//#endregion
|
|
62
83
|
//#region src/utils/filter-diagnostics.ts
|
|
63
84
|
const resolveCandidateReadPath = (rootDirectory, filePath) => {
|
|
@@ -93,9 +114,9 @@ const isRuleSuppressed = (commentRules, ruleId) => {
|
|
|
93
114
|
return commentRules.split(/[,\s]+/).some((rule) => rule.trim() === ruleId);
|
|
94
115
|
};
|
|
95
116
|
const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLinesSync) => {
|
|
96
|
-
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") : []);
|
|
97
118
|
const ignoredFilePatterns = compileIgnoredFilePatterns(config);
|
|
98
|
-
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") : []);
|
|
99
120
|
const hasTextComponents = textComponentNames.size > 0;
|
|
100
121
|
const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);
|
|
101
122
|
return diagnostics.filter((diagnostic) => {
|
|
@@ -131,13 +152,11 @@ const filterInlineSuppressions = (diagnostics, rootDirectory, readFileLinesSync)
|
|
|
131
152
|
return true;
|
|
132
153
|
});
|
|
133
154
|
};
|
|
134
|
-
|
|
135
155
|
//#endregion
|
|
136
156
|
//#region src/utils/merge-and-filter-diagnostics.ts
|
|
137
157
|
const mergeAndFilterDiagnostics = (mergedDiagnostics, directory, userConfig, readFileLinesSync) => {
|
|
138
158
|
return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(mergedDiagnostics, userConfig, directory, readFileLinesSync) : mergedDiagnostics, directory, readFileLinesSync);
|
|
139
159
|
};
|
|
140
|
-
|
|
141
160
|
//#endregion
|
|
142
161
|
//#region src/core/build-result.ts
|
|
143
162
|
const buildDiagnoseTimedResult = async (input) => {
|
|
@@ -149,36 +168,9 @@ const buildDiagnoseTimedResult = async (input) => {
|
|
|
149
168
|
elapsedMilliseconds
|
|
150
169
|
};
|
|
151
170
|
};
|
|
152
|
-
|
|
153
|
-
//#endregion
|
|
154
|
-
//#region src/constants.ts
|
|
155
|
-
const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
|
|
156
|
-
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
157
|
-
const ERROR_PREVIEW_LENGTH_CHARS = 200;
|
|
158
|
-
const PERFECT_SCORE = 100;
|
|
159
|
-
const SCORE_GOOD_THRESHOLD = 75;
|
|
160
|
-
const SCORE_OK_THRESHOLD = 50;
|
|
161
|
-
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
162
|
-
const FETCH_TIMEOUT_MS = 1e4;
|
|
163
|
-
const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
164
|
-
const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
|
|
165
|
-
const OXLINT_MAX_FILES_PER_BATCH = 500;
|
|
166
|
-
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
167
|
-
const ERROR_RULE_PENALTY = 1.5;
|
|
168
|
-
const WARNING_RULE_PENALTY = .75;
|
|
169
|
-
const MAX_KNIP_RETRIES = 5;
|
|
170
|
-
const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
171
|
-
const IGNORED_DIRECTORIES = new Set([
|
|
172
|
-
"node_modules",
|
|
173
|
-
"dist",
|
|
174
|
-
"build",
|
|
175
|
-
"coverage"
|
|
176
|
-
]);
|
|
177
|
-
|
|
178
171
|
//#endregion
|
|
179
172
|
//#region src/utils/jsx-include-paths.ts
|
|
180
173
|
const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
|
|
181
|
-
|
|
182
174
|
//#endregion
|
|
183
175
|
//#region src/core/diagnose-core.ts
|
|
184
176
|
const diagnoseCore = async (deps, options = {}) => {
|
|
@@ -190,7 +182,7 @@ const diagnoseCore = async (deps, options = {}) => {
|
|
|
190
182
|
const userConfig = deps.loadUserConfig();
|
|
191
183
|
const effectiveLint = options.lint ?? userConfig?.lint ?? true;
|
|
192
184
|
const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;
|
|
193
|
-
if (!projectInfo.reactVersion) throw new Error(
|
|
185
|
+
if (!projectInfo.reactVersion) throw new Error(buildNoReactDependencyError(deps.rootDirectory));
|
|
194
186
|
const lintIncludePaths = options.lintIncludePaths !== void 0 ? options.lintIncludePaths : computeJsxIncludePaths(includePaths);
|
|
195
187
|
const { runLint, runDeadCode } = deps.createRunners({
|
|
196
188
|
resolvedDirectory,
|
|
@@ -208,7 +200,11 @@ const diagnoseCore = async (deps, options = {}) => {
|
|
|
208
200
|
console.error("Dead code analysis failed:", error);
|
|
209
201
|
return emptyDiagnostics;
|
|
210
202
|
}) : Promise.resolve(emptyDiagnostics);
|
|
211
|
-
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);
|
|
212
208
|
const environmentDiagnostics = deps.getExtraDiagnostics?.() ?? [];
|
|
213
209
|
const timed = await buildDiagnoseTimedResult({
|
|
214
210
|
mergedDiagnostics: [
|
|
@@ -222,18 +218,149 @@ const diagnoseCore = async (deps, options = {}) => {
|
|
|
222
218
|
startTime,
|
|
223
219
|
calculateDiagnosticsScore: deps.calculateDiagnosticsScore
|
|
224
220
|
});
|
|
225
|
-
return
|
|
221
|
+
return {
|
|
226
222
|
diagnostics: timed.diagnostics,
|
|
227
223
|
score: timed.score,
|
|
228
224
|
project: projectInfo,
|
|
229
225
|
elapsedMilliseconds: timed.elapsedMilliseconds
|
|
230
|
-
}
|
|
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
|
+
};
|
|
231
360
|
};
|
|
232
|
-
|
|
233
361
|
//#endregion
|
|
234
362
|
//#region src/plugin/constants.ts
|
|
235
363
|
const MOTION_LIBRARY_PACKAGES = new Set(["framer-motion", "motion"]);
|
|
236
|
-
|
|
237
364
|
//#endregion
|
|
238
365
|
//#region src/utils/is-file.ts
|
|
239
366
|
const isFile = (filePath) => {
|
|
@@ -243,10 +370,13 @@ const isFile = (filePath) => {
|
|
|
243
370
|
return false;
|
|
244
371
|
}
|
|
245
372
|
};
|
|
246
|
-
|
|
247
373
|
//#endregion
|
|
248
374
|
//#region src/utils/read-package-json.ts
|
|
249
|
-
const
|
|
375
|
+
const cachedPackageJsons = /* @__PURE__ */ new Map();
|
|
376
|
+
const clearPackageJsonCache = () => {
|
|
377
|
+
cachedPackageJsons.clear();
|
|
378
|
+
};
|
|
379
|
+
const readPackageJsonUncached = (packageJsonPath) => {
|
|
250
380
|
try {
|
|
251
381
|
return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
252
382
|
} catch (error) {
|
|
@@ -258,11 +388,25 @@ const readPackageJson = (packageJsonPath) => {
|
|
|
258
388
|
throw error;
|
|
259
389
|
}
|
|
260
390
|
};
|
|
261
|
-
|
|
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
|
+
};
|
|
262
399
|
//#endregion
|
|
263
400
|
//#region src/utils/check-reduced-motion.ts
|
|
264
401
|
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
|
|
265
|
-
const REDUCED_MOTION_FILE_GLOBS =
|
|
402
|
+
const REDUCED_MOTION_FILE_GLOBS = [
|
|
403
|
+
"*.ts",
|
|
404
|
+
"*.tsx",
|
|
405
|
+
"*.js",
|
|
406
|
+
"*.jsx",
|
|
407
|
+
"*.css",
|
|
408
|
+
"*.scss"
|
|
409
|
+
];
|
|
266
410
|
const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
267
411
|
filePath: "package.json",
|
|
268
412
|
plugin: "react-doctor",
|
|
@@ -272,8 +416,7 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
|
272
416
|
help: "Add `useReducedMotion()` from your animation library, or a `@media (prefers-reduced-motion: reduce)` CSS query",
|
|
273
417
|
line: 0,
|
|
274
418
|
column: 0,
|
|
275
|
-
category: "Accessibility"
|
|
276
|
-
weight: 2
|
|
419
|
+
category: "Accessibility"
|
|
277
420
|
};
|
|
278
421
|
const checkReducedMotion = (rootDirectory) => {
|
|
279
422
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
@@ -290,17 +433,144 @@ const checkReducedMotion = (rootDirectory) => {
|
|
|
290
433
|
return [];
|
|
291
434
|
}
|
|
292
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;
|
|
293
472
|
try {
|
|
294
|
-
|
|
295
|
-
cwd: rootDirectory,
|
|
296
|
-
stdio: "pipe"
|
|
297
|
-
});
|
|
298
|
-
return [];
|
|
473
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
299
474
|
} catch {
|
|
300
|
-
return [
|
|
475
|
+
return [];
|
|
301
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("");
|
|
518
|
+
}
|
|
519
|
+
};
|
|
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;
|
|
302
573
|
};
|
|
303
|
-
|
|
304
574
|
//#endregion
|
|
305
575
|
//#region src/utils/find-monorepo-root.ts
|
|
306
576
|
const isMonorepoRoot = (directory) => {
|
|
@@ -319,11 +589,13 @@ const findMonorepoRoot = (startDirectory) => {
|
|
|
319
589
|
}
|
|
320
590
|
return null;
|
|
321
591
|
};
|
|
322
|
-
|
|
323
592
|
//#endregion
|
|
324
593
|
//#region src/utils/is-plain-object.ts
|
|
325
|
-
const isPlainObject = (value) =>
|
|
326
|
-
|
|
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
|
+
};
|
|
327
599
|
//#endregion
|
|
328
600
|
//#region src/utils/discover-project.ts
|
|
329
601
|
const REACT_COMPILER_PACKAGES = new Set([
|
|
@@ -331,6 +603,11 @@ const REACT_COMPILER_PACKAGES = new Set([
|
|
|
331
603
|
"react-compiler-runtime",
|
|
332
604
|
"eslint-plugin-react-compiler"
|
|
333
605
|
]);
|
|
606
|
+
const TANSTACK_QUERY_PACKAGES = new Set([
|
|
607
|
+
"@tanstack/react-query",
|
|
608
|
+
"@tanstack/query-core",
|
|
609
|
+
"react-query"
|
|
610
|
+
]);
|
|
334
611
|
const NEXT_CONFIG_FILENAMES = [
|
|
335
612
|
"next.config.js",
|
|
336
613
|
"next.config.mjs",
|
|
@@ -349,7 +626,11 @@ const VITE_CONFIG_FILENAMES = [
|
|
|
349
626
|
"vite.config.js",
|
|
350
627
|
"vite.config.ts",
|
|
351
628
|
"vite.config.mjs",
|
|
352
|
-
"vite.config.
|
|
629
|
+
"vite.config.mts",
|
|
630
|
+
"vite.config.cjs",
|
|
631
|
+
"vite.config.cts",
|
|
632
|
+
"vitest.config.ts",
|
|
633
|
+
"vitest.config.js"
|
|
353
634
|
];
|
|
354
635
|
const EXPO_APP_CONFIG_FILENAMES = [
|
|
355
636
|
"app.json",
|
|
@@ -357,7 +638,7 @@ const EXPO_APP_CONFIG_FILENAMES = [
|
|
|
357
638
|
"app.config.ts"
|
|
358
639
|
];
|
|
359
640
|
const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
|
|
360
|
-
const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*true\b/;
|
|
641
|
+
const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
|
|
361
642
|
const FRAMEWORK_PACKAGES = {
|
|
362
643
|
next: "nextjs",
|
|
363
644
|
"@tanstack/react-start": "tanstack-start",
|
|
@@ -387,6 +668,7 @@ const countSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
387
668
|
const countSourceFilesViaGit = (rootDirectory) => {
|
|
388
669
|
const result = spawnSync("git", [
|
|
389
670
|
"ls-files",
|
|
671
|
+
"-z",
|
|
390
672
|
"--cached",
|
|
391
673
|
"--others",
|
|
392
674
|
"--exclude-standard"
|
|
@@ -396,7 +678,7 @@ const countSourceFilesViaGit = (rootDirectory) => {
|
|
|
396
678
|
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
397
679
|
});
|
|
398
680
|
if (result.error || result.status !== 0) return null;
|
|
399
|
-
return result.stdout.split("\
|
|
681
|
+
return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;
|
|
400
682
|
};
|
|
401
683
|
const countSourceFiles = (rootDirectory) => countSourceFilesViaGit(rootDirectory) ?? countSourceFilesViaFilesystem(rootDirectory);
|
|
402
684
|
const collectAllDependencies = (packageJson) => ({
|
|
@@ -494,17 +776,17 @@ const resolveCatalogVersionFromCollection = (catalogs, packageName, catalogRefer
|
|
|
494
776
|
const resolveCatalogVersion = (packageJson, packageName, rootDirectory) => {
|
|
495
777
|
const rawVersion = collectAllDependencies(packageJson)[packageName];
|
|
496
778
|
const catalogName = rawVersion ? extractCatalogName(rawVersion) : null;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const version = resolveVersionFromCatalog(raw.catalog, packageName);
|
|
779
|
+
if (isPlainObject(packageJson.catalog)) {
|
|
780
|
+
const version = resolveVersionFromCatalog(packageJson.catalog, packageName);
|
|
500
781
|
if (version) return version;
|
|
501
782
|
}
|
|
502
|
-
if (isPlainObject(
|
|
503
|
-
|
|
504
|
-
|
|
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);
|
|
505
787
|
if (version) return version;
|
|
506
788
|
}
|
|
507
|
-
for (const catalogEntries of Object.values(
|
|
789
|
+
for (const catalogEntries of Object.values(packageJson.catalogs)) if (isPlainObject(catalogEntries)) {
|
|
508
790
|
const version = resolveVersionFromCatalog(catalogEntries, packageName);
|
|
509
791
|
if (version) return version;
|
|
510
792
|
}
|
|
@@ -545,11 +827,32 @@ const parsePnpmWorkspacePatterns = (rootDirectory) => {
|
|
|
545
827
|
}
|
|
546
828
|
return patterns;
|
|
547
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
|
+
};
|
|
548
849
|
const getWorkspacePatterns = (rootDirectory, packageJson) => {
|
|
549
850
|
const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
|
|
550
851
|
if (pnpmPatterns.length > 0) return pnpmPatterns;
|
|
551
852
|
if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
|
|
552
853
|
if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
|
|
854
|
+
const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
|
|
855
|
+
if (nxPatterns.length > 0) return nxPatterns;
|
|
553
856
|
return [];
|
|
554
857
|
};
|
|
555
858
|
const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
|
|
@@ -612,23 +915,35 @@ const hasCompilerInConfigFile = (filePath) => {
|
|
|
612
915
|
return REACT_COMPILER_ENABLED_FLAG_PATTERN.test(content) || REACT_COMPILER_PACKAGE_REFERENCE_PATTERN.test(content);
|
|
613
916
|
};
|
|
614
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
|
+
};
|
|
615
922
|
const detectReactCompiler = (directory, packageJson) => {
|
|
616
923
|
if (hasCompilerPackage(packageJson)) return true;
|
|
617
924
|
if (hasCompilerInConfigFiles(directory, NEXT_CONFIG_FILENAMES)) return true;
|
|
618
925
|
if (hasCompilerInConfigFiles(directory, BABEL_CONFIG_FILENAMES)) return true;
|
|
619
926
|
if (hasCompilerInConfigFiles(directory, VITE_CONFIG_FILENAMES)) return true;
|
|
620
927
|
if (hasCompilerInConfigFiles(directory, EXPO_APP_CONFIG_FILENAMES)) return true;
|
|
928
|
+
if (isProjectBoundary$1(directory)) return false;
|
|
621
929
|
let ancestorDirectory = path.dirname(directory);
|
|
622
930
|
while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
|
|
623
931
|
const ancestorPackagePath = path.join(ancestorDirectory, "package.json");
|
|
624
932
|
if (isFile(ancestorPackagePath)) {
|
|
625
933
|
if (hasCompilerPackage(readPackageJson(ancestorPackagePath))) return true;
|
|
626
934
|
}
|
|
935
|
+
if (isProjectBoundary$1(ancestorDirectory)) return false;
|
|
627
936
|
ancestorDirectory = path.dirname(ancestorDirectory);
|
|
628
937
|
}
|
|
629
938
|
return false;
|
|
630
939
|
};
|
|
940
|
+
const cachedProjectInfos = /* @__PURE__ */ new Map();
|
|
941
|
+
const clearProjectCache = () => {
|
|
942
|
+
cachedProjectInfos.clear();
|
|
943
|
+
};
|
|
631
944
|
const discoverProject = (directory) => {
|
|
945
|
+
const cached = cachedProjectInfos.get(directory);
|
|
946
|
+
if (cached !== void 0) return cached;
|
|
632
947
|
const packageJsonPath = path.join(directory, "package.json");
|
|
633
948
|
if (!isFile(packageJsonPath)) throw new Error(`No package.json found in ${directory}`);
|
|
634
949
|
const packageJson = readPackageJson(packageJsonPath);
|
|
@@ -655,17 +970,57 @@ const discoverProject = (directory) => {
|
|
|
655
970
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
|
|
656
971
|
const sourceFileCount = countSourceFiles(directory);
|
|
657
972
|
const hasReactCompiler = detectReactCompiler(directory, packageJson);
|
|
658
|
-
|
|
973
|
+
const allDependencies = collectAllDependencies(packageJson);
|
|
974
|
+
const hasTanStackQuery = Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
|
|
975
|
+
const projectInfo = {
|
|
659
976
|
rootDirectory: directory,
|
|
660
977
|
projectName,
|
|
661
978
|
reactVersion,
|
|
662
979
|
framework,
|
|
663
980
|
hasTypeScript,
|
|
664
981
|
hasReactCompiler,
|
|
982
|
+
hasTanStackQuery,
|
|
665
983
|
sourceFileCount
|
|
666
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;
|
|
667
1023
|
};
|
|
668
|
-
|
|
669
1024
|
//#endregion
|
|
670
1025
|
//#region src/utils/load-config.ts
|
|
671
1026
|
const CONFIG_FILENAME = "react-doctor.config.json";
|
|
@@ -675,33 +1030,57 @@ const loadConfigFromDirectory = (directory) => {
|
|
|
675
1030
|
if (isFile(configFilePath)) try {
|
|
676
1031
|
const fileContent = fs.readFileSync(configFilePath, "utf-8");
|
|
677
1032
|
const parsed = JSON.parse(fileContent);
|
|
678
|
-
if (isPlainObject(parsed)) return parsed;
|
|
679
|
-
|
|
1033
|
+
if (isPlainObject(parsed)) return validateConfigTypes(parsed);
|
|
1034
|
+
logger.warn(`${CONFIG_FILENAME} must be a JSON object, ignoring.`);
|
|
680
1035
|
} catch (error) {
|
|
681
|
-
|
|
1036
|
+
logger.warn(`Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
|
|
682
1037
|
}
|
|
683
1038
|
const packageJsonPath = path.join(directory, "package.json");
|
|
684
1039
|
if (isFile(packageJsonPath)) try {
|
|
685
1040
|
const fileContent = fs.readFileSync(packageJsonPath, "utf-8");
|
|
686
|
-
const
|
|
687
|
-
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
|
+
}
|
|
688
1046
|
} catch {
|
|
689
1047
|
return null;
|
|
690
1048
|
}
|
|
691
1049
|
return null;
|
|
692
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
|
+
};
|
|
693
1056
|
const loadConfig = (rootDirectory) => {
|
|
1057
|
+
const cached = cachedConfigs.get(rootDirectory);
|
|
1058
|
+
if (cached !== void 0) return cached;
|
|
694
1059
|
const localConfig = loadConfigFromDirectory(rootDirectory);
|
|
695
|
-
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
|
+
}
|
|
696
1068
|
let ancestorDirectory = path.dirname(rootDirectory);
|
|
697
1069
|
while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
|
|
698
1070
|
const ancestorConfig = loadConfigFromDirectory(ancestorDirectory);
|
|
699
|
-
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
|
+
}
|
|
700
1079
|
ancestorDirectory = path.dirname(ancestorDirectory);
|
|
701
1080
|
}
|
|
1081
|
+
cachedConfigs.set(rootDirectory, null);
|
|
702
1082
|
return null;
|
|
703
1083
|
};
|
|
704
|
-
|
|
705
1084
|
//#endregion
|
|
706
1085
|
//#region src/utils/read-file-lines-node.ts
|
|
707
1086
|
const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
@@ -714,22 +1093,23 @@ const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
|
714
1093
|
}
|
|
715
1094
|
};
|
|
716
1095
|
};
|
|
717
|
-
|
|
718
1096
|
//#endregion
|
|
719
1097
|
//#region src/utils/resolve-lint-include-paths.ts
|
|
720
1098
|
const listSourceFilesViaGit = (rootDirectory) => {
|
|
721
1099
|
const result = spawnSync("git", [
|
|
722
1100
|
"ls-files",
|
|
1101
|
+
"-z",
|
|
723
1102
|
"--cached",
|
|
724
1103
|
"--others",
|
|
725
|
-
"--exclude-standard"
|
|
1104
|
+
"--exclude-standard",
|
|
1105
|
+
"--recurse-submodules"
|
|
726
1106
|
], {
|
|
727
1107
|
cwd: rootDirectory,
|
|
728
1108
|
encoding: "utf-8",
|
|
729
1109
|
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
730
1110
|
});
|
|
731
1111
|
if (result.error || result.status !== 0) return null;
|
|
732
|
-
return result.stdout.split("\
|
|
1112
|
+
return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
|
|
733
1113
|
};
|
|
734
1114
|
const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
735
1115
|
const filePaths = [];
|
|
@@ -757,12 +1137,11 @@ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
|
|
|
757
1137
|
return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
|
|
758
1138
|
});
|
|
759
1139
|
};
|
|
760
|
-
|
|
761
1140
|
//#endregion
|
|
762
1141
|
//#region src/core/calculate-score-locally.ts
|
|
763
1142
|
const getScoreLabel = (score) => {
|
|
764
|
-
if (score >=
|
|
765
|
-
if (score >=
|
|
1143
|
+
if (score >= 75) return "Great";
|
|
1144
|
+
if (score >= 50) return "Needs work";
|
|
766
1145
|
return "Critical";
|
|
767
1146
|
};
|
|
768
1147
|
const countUniqueRules = (diagnostics) => {
|
|
@@ -780,7 +1159,7 @@ const countUniqueRules = (diagnostics) => {
|
|
|
780
1159
|
};
|
|
781
1160
|
const scoreFromRuleCounts = (errorRuleCount, warningRuleCount) => {
|
|
782
1161
|
const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;
|
|
783
|
-
return Math.max(0, Math.round(
|
|
1162
|
+
return Math.max(0, Math.round(100 - penalty));
|
|
784
1163
|
};
|
|
785
1164
|
const calculateScoreLocally = (diagnostics) => {
|
|
786
1165
|
const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);
|
|
@@ -790,7 +1169,6 @@ const calculateScoreLocally = (diagnostics) => {
|
|
|
790
1169
|
label: getScoreLabel(score)
|
|
791
1170
|
};
|
|
792
1171
|
};
|
|
793
|
-
|
|
794
1172
|
//#endregion
|
|
795
1173
|
//#region src/core/try-score-from-api.ts
|
|
796
1174
|
const parseScoreResult = (value) => {
|
|
@@ -804,44 +1182,51 @@ const parseScoreResult = (value) => {
|
|
|
804
1182
|
label: labelValue
|
|
805
1183
|
};
|
|
806
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
|
+
};
|
|
807
1192
|
const tryScoreFromApi = async (diagnostics, fetchImplementation) => {
|
|
1193
|
+
if (typeof fetchImplementation !== "function") return null;
|
|
808
1194
|
const controller = new AbortController();
|
|
809
1195
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
810
1196
|
try {
|
|
811
1197
|
const response = await fetchImplementation(SCORE_API_URL, {
|
|
812
1198
|
method: "POST",
|
|
813
1199
|
headers: { "Content-Type": "application/json" },
|
|
814
|
-
body: JSON.stringify({ diagnostics }),
|
|
1200
|
+
body: JSON.stringify({ diagnostics: stripFilePaths(diagnostics) }),
|
|
815
1201
|
signal: controller.signal
|
|
816
1202
|
});
|
|
817
|
-
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
|
+
}
|
|
818
1207
|
return parseScoreResult(await response.json());
|
|
819
|
-
} catch {
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
console.warn(`[react-doctor] Score API unreachable (${describeFailure(error)}) — using local scoring`);
|
|
820
1210
|
return null;
|
|
821
1211
|
} finally {
|
|
822
1212
|
clearTimeout(timeoutId);
|
|
823
1213
|
}
|
|
824
1214
|
};
|
|
825
|
-
|
|
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);
|
|
826
1219
|
//#endregion
|
|
827
1220
|
//#region src/utils/proxy-fetch.ts
|
|
828
1221
|
const getGlobalProcess = () => {
|
|
829
1222
|
const candidate = globalThis.process;
|
|
830
1223
|
return candidate?.versions?.node ? candidate : void 0;
|
|
831
1224
|
};
|
|
832
|
-
const
|
|
1225
|
+
const getProxyUrl = () => {
|
|
833
1226
|
const proc = getGlobalProcess();
|
|
834
1227
|
if (!proc?.env) return void 0;
|
|
835
1228
|
return proc.env.HTTPS_PROXY ?? proc.env.https_proxy ?? proc.env.HTTP_PROXY ?? proc.env.http_proxy;
|
|
836
1229
|
};
|
|
837
|
-
let isProxyUrlResolved = false;
|
|
838
|
-
let resolvedProxyUrl;
|
|
839
|
-
const getProxyUrl = () => {
|
|
840
|
-
if (isProxyUrlResolved) return resolvedProxyUrl;
|
|
841
|
-
isProxyUrlResolved = true;
|
|
842
|
-
resolvedProxyUrl = readEnvProxy();
|
|
843
|
-
return resolvedProxyUrl;
|
|
844
|
-
};
|
|
845
1230
|
const createProxyDispatcher = async (proxyUrl) => {
|
|
846
1231
|
try {
|
|
847
1232
|
const { ProxyAgent } = await import("undici");
|
|
@@ -851,29 +1236,17 @@ const createProxyDispatcher = async (proxyUrl) => {
|
|
|
851
1236
|
}
|
|
852
1237
|
};
|
|
853
1238
|
const proxyFetch = async (url, init) => {
|
|
854
|
-
const
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
signal: controller.signal,
|
|
862
|
-
...dispatcher ? { dispatcher } : {}
|
|
863
|
-
});
|
|
864
|
-
} finally {
|
|
865
|
-
clearTimeout(timeoutId);
|
|
866
|
-
}
|
|
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);
|
|
867
1246
|
};
|
|
868
|
-
|
|
869
1247
|
//#endregion
|
|
870
1248
|
//#region src/utils/calculate-score-node.ts
|
|
871
|
-
const calculateScore =
|
|
872
|
-
const apiScore = await tryScoreFromApi(diagnostics, proxyFetch);
|
|
873
|
-
if (apiScore) return apiScore;
|
|
874
|
-
return calculateScoreLocally(diagnostics);
|
|
875
|
-
};
|
|
876
|
-
|
|
1249
|
+
const calculateScore = (diagnostics) => calculateScore$1(diagnostics, proxyFetch);
|
|
877
1250
|
//#endregion
|
|
878
1251
|
//#region src/utils/collect-unused-file-paths.ts
|
|
879
1252
|
const collectUnusedFilePaths = (filesIssues) => {
|
|
@@ -887,40 +1260,64 @@ const collectUnusedFilePaths = (filesIssues) => {
|
|
|
887
1260
|
}
|
|
888
1261
|
return unusedFilePaths;
|
|
889
1262
|
};
|
|
890
|
-
|
|
1263
|
+
//#endregion
|
|
1264
|
+
//#region src/utils/extract-failed-plugin-name.ts
|
|
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;
|
|
1267
|
+
const extractFailedPluginName = (error) => {
|
|
1268
|
+
for (const errorMessage of getErrorChainMessages(error)) {
|
|
1269
|
+
const pluginNameMatch = errorMessage.match(PLUGIN_CONFIG_PATTERN);
|
|
1270
|
+
if (pluginNameMatch?.[1]) return pluginNameMatch[1].toLowerCase();
|
|
1271
|
+
const rcMatch = errorMessage.match(RC_DOTFILE_PATTERN);
|
|
1272
|
+
if (rcMatch?.[1]) return rcMatch[1].toLowerCase();
|
|
1273
|
+
}
|
|
1274
|
+
return null;
|
|
1275
|
+
};
|
|
1276
|
+
//#endregion
|
|
1277
|
+
//#region src/utils/has-knip-config.ts
|
|
1278
|
+
const hasKnipConfig = (directory) => KNIP_CONFIG_LOCATIONS.some((configFilename) => isFile(path.join(directory, configFilename)));
|
|
891
1279
|
//#endregion
|
|
892
1280
|
//#region src/utils/run-knip.ts
|
|
893
|
-
const
|
|
894
|
-
files:
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
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"
|
|
910
1307
|
};
|
|
911
1308
|
const collectIssueRecords = (records, issueType, rootDirectory) => {
|
|
1309
|
+
const descriptor = KNIP_ISSUE_TYPE_DESCRIPTORS[issueType] ?? FALLBACK_KNIP_DESCRIPTOR;
|
|
912
1310
|
const diagnostics = [];
|
|
913
1311
|
for (const issues of Object.values(records)) for (const issue of Object.values(issues)) diagnostics.push({
|
|
914
1312
|
filePath: path.relative(rootDirectory, issue.filePath),
|
|
915
1313
|
plugin: "knip",
|
|
916
1314
|
rule: issueType,
|
|
917
|
-
severity:
|
|
918
|
-
message: `${
|
|
1315
|
+
severity: descriptor.severity,
|
|
1316
|
+
message: `${descriptor.message}: ${issue.symbol}`,
|
|
919
1317
|
help: "",
|
|
920
1318
|
line: 0,
|
|
921
1319
|
column: 0,
|
|
922
|
-
category:
|
|
923
|
-
weight: 1
|
|
1320
|
+
category: descriptor.category
|
|
924
1321
|
});
|
|
925
1322
|
return diagnostics;
|
|
926
1323
|
};
|
|
@@ -929,10 +1326,11 @@ const silenced = async (fn) => {
|
|
|
929
1326
|
const originalInfo = console.info;
|
|
930
1327
|
const originalWarn = console.warn;
|
|
931
1328
|
const originalError = console.error;
|
|
932
|
-
|
|
933
|
-
console.
|
|
934
|
-
console.
|
|
935
|
-
console.
|
|
1329
|
+
const noop = () => {};
|
|
1330
|
+
console.log = noop;
|
|
1331
|
+
console.info = noop;
|
|
1332
|
+
console.warn = noop;
|
|
1333
|
+
console.error = noop;
|
|
936
1334
|
try {
|
|
937
1335
|
return await fn();
|
|
938
1336
|
} finally {
|
|
@@ -942,12 +1340,15 @@ const silenced = async (fn) => {
|
|
|
942
1340
|
console.error = originalError;
|
|
943
1341
|
}
|
|
944
1342
|
};
|
|
945
|
-
const
|
|
946
|
-
const
|
|
947
|
-
|
|
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)));
|
|
1345
|
+
const tryDisableFailedPlugin = (error, parsedConfig, disabledPlugins) => {
|
|
1346
|
+
const failedPlugin = extractFailedPluginName(error);
|
|
1347
|
+
if (!failedPlugin || !(failedPlugin in parsedConfig) || disabledPlugins.has(failedPlugin)) return false;
|
|
1348
|
+
disabledPlugins.add(failedPlugin);
|
|
1349
|
+
parsedConfig[failedPlugin] = false;
|
|
1350
|
+
return true;
|
|
948
1351
|
};
|
|
949
|
-
const TSCONFIG_FILENAMES = ["tsconfig.base.json", "tsconfig.json"];
|
|
950
|
-
const resolveTsConfigFile = (directory) => TSCONFIG_FILENAMES.find((filename) => fs.existsSync(path.join(directory, filename)));
|
|
951
1352
|
const runKnipWithOptions = async (knipCwd, workspaceName) => {
|
|
952
1353
|
const tsConfigFile = resolveTsConfigFile(knipCwd);
|
|
953
1354
|
const options = await silenced(() => createOptions({
|
|
@@ -957,45 +1358,48 @@ const runKnipWithOptions = async (knipCwd, workspaceName) => {
|
|
|
957
1358
|
...tsConfigFile ? { tsConfigFile } : {}
|
|
958
1359
|
}));
|
|
959
1360
|
const parsedConfig = options.parsedConfig;
|
|
960
|
-
|
|
1361
|
+
const disabledPlugins = /* @__PURE__ */ new Set();
|
|
1362
|
+
let lastKnipError;
|
|
1363
|
+
for (let attempt = 0; attempt < 6; attempt++) try {
|
|
961
1364
|
return await silenced(() => main(options));
|
|
962
1365
|
} catch (error) {
|
|
963
|
-
|
|
964
|
-
if (!
|
|
965
|
-
parsedConfig[failedPlugin] = false;
|
|
1366
|
+
lastKnipError = error;
|
|
1367
|
+
if (!tryDisableFailedPlugin(error, parsedConfig, disabledPlugins)) throw error;
|
|
966
1368
|
}
|
|
967
|
-
throw
|
|
1369
|
+
throw lastKnipError;
|
|
968
1370
|
};
|
|
969
1371
|
const hasNodeModules = (directory) => {
|
|
970
1372
|
const nodeModulesPath = path.join(directory, "node_modules");
|
|
971
1373
|
return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();
|
|
972
1374
|
};
|
|
1375
|
+
const resolveWorkspaceName = (rootDirectory) => {
|
|
1376
|
+
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
1377
|
+
return (isFile(packageJsonPath) ? readPackageJson(packageJsonPath) : {}).name ?? path.basename(rootDirectory);
|
|
1378
|
+
};
|
|
1379
|
+
const runKnipForProject = async (rootDirectory, monorepoRoot) => {
|
|
1380
|
+
if (!monorepoRoot || hasKnipConfig(rootDirectory)) return runKnipWithOptions(rootDirectory);
|
|
1381
|
+
try {
|
|
1382
|
+
return await runKnipWithOptions(monorepoRoot, resolveWorkspaceName(rootDirectory));
|
|
1383
|
+
} catch {
|
|
1384
|
+
return runKnipWithOptions(rootDirectory);
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
973
1387
|
const runKnip = async (rootDirectory) => {
|
|
974
1388
|
const monorepoRoot = findMonorepoRoot(rootDirectory);
|
|
975
1389
|
if (!(hasNodeModules(rootDirectory) || monorepoRoot !== null && hasNodeModules(monorepoRoot))) return [];
|
|
976
|
-
|
|
977
|
-
if (monorepoRoot) {
|
|
978
|
-
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
979
|
-
const workspaceName = (isFile(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) : {}).name ?? path.basename(rootDirectory);
|
|
980
|
-
try {
|
|
981
|
-
knipResult = await runKnipWithOptions(monorepoRoot, workspaceName);
|
|
982
|
-
} catch {
|
|
983
|
-
knipResult = await runKnipWithOptions(rootDirectory);
|
|
984
|
-
}
|
|
985
|
-
} else knipResult = await runKnipWithOptions(rootDirectory);
|
|
986
|
-
const { issues } = knipResult;
|
|
1390
|
+
const { issues } = await runKnipForProject(rootDirectory, monorepoRoot);
|
|
987
1391
|
const diagnostics = [];
|
|
1392
|
+
const filesDescriptor = KNIP_ISSUE_TYPE_DESCRIPTORS.files;
|
|
988
1393
|
for (const unusedFilePath of collectUnusedFilePaths(issues.files)) diagnostics.push({
|
|
989
1394
|
filePath: path.relative(rootDirectory, unusedFilePath),
|
|
990
1395
|
plugin: "knip",
|
|
991
1396
|
rule: "files",
|
|
992
|
-
severity:
|
|
993
|
-
message:
|
|
1397
|
+
severity: filesDescriptor.severity,
|
|
1398
|
+
message: filesDescriptor.message,
|
|
994
1399
|
help: "This file is not imported by any other file in the project.",
|
|
995
1400
|
line: 0,
|
|
996
1401
|
column: 0,
|
|
997
|
-
category:
|
|
998
|
-
weight: 1
|
|
1402
|
+
category: filesDescriptor.category
|
|
999
1403
|
});
|
|
1000
1404
|
for (const issueType of [
|
|
1001
1405
|
"exports",
|
|
@@ -1004,7 +1408,29 @@ const runKnip = async (rootDirectory) => {
|
|
|
1004
1408
|
]) diagnostics.push(...collectIssueRecords(issues[issueType], issueType, rootDirectory));
|
|
1005
1409
|
return diagnostics;
|
|
1006
1410
|
};
|
|
1007
|
-
|
|
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
|
+
};
|
|
1008
1434
|
//#endregion
|
|
1009
1435
|
//#region src/oxlint-config.ts
|
|
1010
1436
|
const esmRequire$1 = createRequire(import.meta.url);
|
|
@@ -1034,7 +1460,23 @@ const REACT_NATIVE_RULES = {
|
|
|
1034
1460
|
"react-doctor/rn-no-inline-flatlist-renderitem": "warn",
|
|
1035
1461
|
"react-doctor/rn-no-legacy-shadow-styles": "warn",
|
|
1036
1462
|
"react-doctor/rn-prefer-reanimated": "warn",
|
|
1037
|
-
"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"
|
|
1038
1480
|
};
|
|
1039
1481
|
const TANSTACK_START_RULES = {
|
|
1040
1482
|
"react-doctor/tanstack-start-route-property-order": "error",
|
|
@@ -1053,22 +1495,41 @@ const TANSTACK_START_RULES = {
|
|
|
1053
1495
|
"react-doctor/tanstack-start-loader-parallel-fetch": "warn"
|
|
1054
1496
|
};
|
|
1055
1497
|
const REACT_COMPILER_RULES = {
|
|
1056
|
-
"react-hooks-js/set-state-in-render": "
|
|
1057
|
-
"react-hooks-js/immutability": "
|
|
1058
|
-
"react-hooks-js/refs": "
|
|
1059
|
-
"react-hooks-js/purity": "
|
|
1060
|
-
"react-hooks-js/hooks": "
|
|
1061
|
-
"react-hooks-js/set-state-in-effect": "
|
|
1062
|
-
"react-hooks-js/globals": "
|
|
1063
|
-
"react-hooks-js/error-boundaries": "
|
|
1064
|
-
"react-hooks-js/preserve-manual-memoization": "
|
|
1065
|
-
"react-hooks-js/unsupported-syntax": "
|
|
1066
|
-
"react-hooks-js/component-hook-factories": "
|
|
1067
|
-
"react-hooks-js/static-components": "
|
|
1068
|
-
"react-hooks-js/use-memo": "
|
|
1069
|
-
"react-hooks-js/void-use-memo": "
|
|
1070
|
-
"react-hooks-js/incompatible-library": "
|
|
1071
|
-
"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"
|
|
1072
1533
|
};
|
|
1073
1534
|
const BUILTIN_REACT_RULES = {
|
|
1074
1535
|
"react/rules-of-hooks": "error",
|
|
@@ -1100,7 +1561,113 @@ const BUILTIN_A11Y_RULES = {
|
|
|
1100
1561
|
"jsx-a11y/no-distracting-elements": "error",
|
|
1101
1562
|
"jsx-a11y/iframe-has-title": "warn"
|
|
1102
1563
|
};
|
|
1103
|
-
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 }) => ({
|
|
1104
1671
|
categories: {
|
|
1105
1672
|
correctness: "off",
|
|
1106
1673
|
suspicious: "off",
|
|
@@ -1110,88 +1677,23 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
|
|
|
1110
1677
|
style: "off",
|
|
1111
1678
|
nursery: "off"
|
|
1112
1679
|
},
|
|
1113
|
-
plugins: [
|
|
1114
|
-
|
|
1115
|
-
"jsx-a11y",
|
|
1116
|
-
...hasReactCompiler ? [] : ["react-perf"]
|
|
1117
|
-
],
|
|
1118
|
-
jsPlugins: [...hasReactCompiler && !customRulesOnly ? [{
|
|
1119
|
-
name: "react-hooks-js",
|
|
1120
|
-
specifier: esmRequire$1.resolve("eslint-plugin-react-hooks")
|
|
1121
|
-
}] : [], pluginPath],
|
|
1680
|
+
plugins: customRulesOnly ? [] : ["react", "jsx-a11y"],
|
|
1681
|
+
jsPlugins: [...resolveReactHooksJsPlugin(hasReactCompiler, customRulesOnly), pluginPath],
|
|
1122
1682
|
rules: {
|
|
1123
1683
|
...customRulesOnly ? {} : BUILTIN_REACT_RULES,
|
|
1124
1684
|
...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
|
|
1125
1685
|
...hasReactCompiler && !customRulesOnly ? REACT_COMPILER_RULES : {},
|
|
1126
|
-
|
|
1127
|
-
"react-doctor/no-fetch-in-effect": "error",
|
|
1128
|
-
"react-doctor/no-cascading-set-state": "warn",
|
|
1129
|
-
"react-doctor/no-effect-event-handler": "warn",
|
|
1130
|
-
"react-doctor/no-derived-useState": "warn",
|
|
1131
|
-
"react-doctor/prefer-useReducer": "warn",
|
|
1132
|
-
"react-doctor/rerender-lazy-state-init": "warn",
|
|
1133
|
-
"react-doctor/rerender-functional-setstate": "warn",
|
|
1134
|
-
"react-doctor/rerender-dependencies": "error",
|
|
1135
|
-
"react-doctor/no-giant-component": "warn",
|
|
1136
|
-
"react-doctor/no-render-in-render": "warn",
|
|
1137
|
-
"react-doctor/no-nested-component-definition": "error",
|
|
1138
|
-
"react-doctor/no-usememo-simple-expression": "warn",
|
|
1139
|
-
"react-doctor/no-layout-property-animation": "error",
|
|
1140
|
-
"react-doctor/rerender-memo-with-default-value": "warn",
|
|
1141
|
-
"react-doctor/rendering-animate-svg-wrapper": "warn",
|
|
1142
|
-
"react-doctor/no-inline-prop-on-memo-component": "warn",
|
|
1143
|
-
"react-doctor/rendering-hydration-no-flicker": "warn",
|
|
1144
|
-
"react-doctor/rendering-script-defer-async": "warn",
|
|
1145
|
-
"react-doctor/no-transition-all": "warn",
|
|
1146
|
-
"react-doctor/no-global-css-variable-animation": "error",
|
|
1147
|
-
"react-doctor/no-large-animated-blur": "warn",
|
|
1148
|
-
"react-doctor/no-scale-from-zero": "warn",
|
|
1149
|
-
"react-doctor/no-permanent-will-change": "warn",
|
|
1150
|
-
"react-doctor/no-secrets-in-client-code": "error",
|
|
1151
|
-
"react-doctor/js-flatmap-filter": "warn",
|
|
1152
|
-
"react-doctor/no-barrel-import": "warn",
|
|
1153
|
-
"react-doctor/no-full-lodash-import": "warn",
|
|
1154
|
-
"react-doctor/no-moment": "warn",
|
|
1155
|
-
"react-doctor/prefer-dynamic-import": "warn",
|
|
1156
|
-
"react-doctor/use-lazy-motion": "warn",
|
|
1157
|
-
"react-doctor/no-undeferred-third-party": "warn",
|
|
1158
|
-
"react-doctor/no-array-index-as-key": "warn",
|
|
1159
|
-
"react-doctor/rendering-conditional-render": "warn",
|
|
1160
|
-
"react-doctor/no-prevent-default": "warn",
|
|
1161
|
-
"react-doctor/server-auth-actions": "error",
|
|
1162
|
-
"react-doctor/server-after-nonblocking": "warn",
|
|
1163
|
-
"react-doctor/client-passive-event-listeners": "warn",
|
|
1164
|
-
"react-doctor/query-stable-query-client": "error",
|
|
1165
|
-
"react-doctor/query-no-rest-destructuring": "warn",
|
|
1166
|
-
"react-doctor/query-no-void-query-fn": "warn",
|
|
1167
|
-
"react-doctor/query-no-query-in-effect": "warn",
|
|
1168
|
-
"react-doctor/query-mutation-missing-invalidation": "warn",
|
|
1169
|
-
"react-doctor/query-no-usequery-for-mutation": "warn",
|
|
1170
|
-
"react-doctor/no-inline-bounce-easing": "warn",
|
|
1171
|
-
"react-doctor/no-z-index-9999": "warn",
|
|
1172
|
-
"react-doctor/no-inline-exhaustive-style": "warn",
|
|
1173
|
-
"react-doctor/no-side-tab-border": "warn",
|
|
1174
|
-
"react-doctor/no-pure-black-background": "warn",
|
|
1175
|
-
"react-doctor/no-gradient-text": "warn",
|
|
1176
|
-
"react-doctor/no-dark-mode-glow": "warn",
|
|
1177
|
-
"react-doctor/no-justified-text": "warn",
|
|
1178
|
-
"react-doctor/no-tiny-text": "warn",
|
|
1179
|
-
"react-doctor/no-wide-letter-spacing": "warn",
|
|
1180
|
-
"react-doctor/no-gray-on-colored-background": "warn",
|
|
1181
|
-
"react-doctor/no-layout-transition-inline": "warn",
|
|
1182
|
-
"react-doctor/no-disabled-zoom": "error",
|
|
1183
|
-
"react-doctor/no-outline-none": "warn",
|
|
1184
|
-
"react-doctor/no-long-transition-duration": "warn",
|
|
1185
|
-
"react-doctor/async-parallel": "warn",
|
|
1686
|
+
...GLOBAL_REACT_DOCTOR_RULES,
|
|
1186
1687
|
...framework === "nextjs" ? NEXTJS_RULES : {},
|
|
1187
1688
|
...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {},
|
|
1188
|
-
...framework === "tanstack-start" ? TANSTACK_START_RULES : {}
|
|
1689
|
+
...framework === "tanstack-start" ? TANSTACK_START_RULES : {},
|
|
1690
|
+
...hasTanStackQuery ? TANSTACK_QUERY_RULES : {}
|
|
1189
1691
|
}
|
|
1190
1692
|
});
|
|
1191
|
-
|
|
1192
1693
|
//#endregion
|
|
1193
1694
|
//#region src/utils/neutralize-disable-directives.ts
|
|
1194
|
-
const
|
|
1695
|
+
const DISABLE_DIRECTIVE_PATTERN = /(eslint|oxlint)-disable/;
|
|
1696
|
+
const findFilesWithDisableDirectivesViaGit = (rootDirectory, includePaths) => {
|
|
1195
1697
|
const grepArgs = [
|
|
1196
1698
|
"grep",
|
|
1197
1699
|
"-l",
|
|
@@ -1205,14 +1707,65 @@ const findFilesWithDisableDirectives = (rootDirectory, includePaths) => {
|
|
|
1205
1707
|
encoding: "utf-8",
|
|
1206
1708
|
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
1207
1709
|
});
|
|
1208
|
-
if (result.error || result.status === null) return
|
|
1209
|
-
if (result.status
|
|
1710
|
+
if (result.error || result.status === null) return null;
|
|
1711
|
+
if (result.status === 128) return null;
|
|
1210
1712
|
return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
|
|
1211
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);
|
|
1212
1755
|
const neutralizeContent = (content) => content.replaceAll("eslint-disable", "eslint_disable").replaceAll("oxlint-disable", "oxlint_disable");
|
|
1213
1756
|
const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
|
|
1214
1757
|
const filePaths = findFilesWithDisableDirectives(rootDirectory, includePaths);
|
|
1215
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);
|
|
1216
1769
|
for (const relativePath of filePaths) {
|
|
1217
1770
|
const absolutePath = path.join(rootDirectory, relativePath);
|
|
1218
1771
|
let originalContent;
|
|
@@ -1228,10 +1781,10 @@ const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
|
|
|
1228
1781
|
}
|
|
1229
1782
|
}
|
|
1230
1783
|
return () => {
|
|
1231
|
-
|
|
1784
|
+
restore();
|
|
1785
|
+
process.removeListener("exit", onExit);
|
|
1232
1786
|
};
|
|
1233
1787
|
};
|
|
1234
|
-
|
|
1235
1788
|
//#endregion
|
|
1236
1789
|
//#region src/utils/run-oxlint.ts
|
|
1237
1790
|
const esmRequire = createRequire(import.meta.url);
|
|
@@ -1239,30 +1792,48 @@ const PLUGIN_CATEGORY_MAP = {
|
|
|
1239
1792
|
react: "Correctness",
|
|
1240
1793
|
"react-hooks": "Correctness",
|
|
1241
1794
|
"react-hooks-js": "React Compiler",
|
|
1242
|
-
"react-
|
|
1243
|
-
"jsx-a11y": "Accessibility"
|
|
1795
|
+
"react-doctor": "Other",
|
|
1796
|
+
"jsx-a11y": "Accessibility",
|
|
1797
|
+
knip: "Dead Code"
|
|
1244
1798
|
};
|
|
1245
1799
|
const RULE_CATEGORY_MAP = {
|
|
1246
1800
|
"react-doctor/no-derived-state-effect": "State & Effects",
|
|
1247
1801
|
"react-doctor/no-fetch-in-effect": "State & Effects",
|
|
1248
1802
|
"react-doctor/no-cascading-set-state": "State & Effects",
|
|
1249
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",
|
|
1250
1806
|
"react-doctor/no-derived-useState": "State & Effects",
|
|
1251
1807
|
"react-doctor/prefer-useReducer": "State & Effects",
|
|
1252
1808
|
"react-doctor/rerender-lazy-state-init": "Performance",
|
|
1253
1809
|
"react-doctor/rerender-functional-setstate": "Performance",
|
|
1254
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",
|
|
1255
1814
|
"react-doctor/no-generic-handler-names": "Architecture",
|
|
1256
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",
|
|
1257
1819
|
"react-doctor/no-render-in-render": "Architecture",
|
|
1258
1820
|
"react-doctor/no-nested-component-definition": "Correctness",
|
|
1821
|
+
"react-doctor/react-compiler-destructure-method": "Architecture",
|
|
1259
1822
|
"react-doctor/no-usememo-simple-expression": "Performance",
|
|
1260
1823
|
"react-doctor/no-layout-property-animation": "Performance",
|
|
1261
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",
|
|
1262
1830
|
"react-doctor/rendering-animate-svg-wrapper": "Performance",
|
|
1831
|
+
"react-doctor/rendering-hoist-jsx": "Performance",
|
|
1832
|
+
"react-doctor/rendering-hydration-mismatch-time": "Correctness",
|
|
1263
1833
|
"react-doctor/rendering-usetransition-loading": "Performance",
|
|
1264
1834
|
"react-doctor/rendering-hydration-no-flicker": "Performance",
|
|
1265
1835
|
"react-doctor/rendering-script-defer-async": "Performance",
|
|
1836
|
+
"react-doctor/no-inline-prop-on-memo-component": "Performance",
|
|
1266
1837
|
"react-doctor/no-transition-all": "Performance",
|
|
1267
1838
|
"react-doctor/no-global-css-variable-animation": "Performance",
|
|
1268
1839
|
"react-doctor/no-large-animated-blur": "Performance",
|
|
@@ -1270,14 +1841,19 @@ const RULE_CATEGORY_MAP = {
|
|
|
1270
1841
|
"react-doctor/no-permanent-will-change": "Performance",
|
|
1271
1842
|
"react-doctor/no-secrets-in-client-code": "Security",
|
|
1272
1843
|
"react-doctor/no-barrel-import": "Bundle Size",
|
|
1844
|
+
"react-doctor/no-dynamic-import-path": "Bundle Size",
|
|
1273
1845
|
"react-doctor/no-full-lodash-import": "Bundle Size",
|
|
1274
1846
|
"react-doctor/no-moment": "Bundle Size",
|
|
1275
1847
|
"react-doctor/prefer-dynamic-import": "Bundle Size",
|
|
1276
1848
|
"react-doctor/use-lazy-motion": "Bundle Size",
|
|
1277
1849
|
"react-doctor/no-undeferred-third-party": "Bundle Size",
|
|
1278
1850
|
"react-doctor/no-array-index-as-key": "Correctness",
|
|
1851
|
+
"react-doctor/no-polymorphic-children": "Architecture",
|
|
1279
1852
|
"react-doctor/rendering-conditional-render": "Correctness",
|
|
1853
|
+
"react-doctor/rendering-svg-precision": "Performance",
|
|
1280
1854
|
"react-doctor/no-prevent-default": "Correctness",
|
|
1855
|
+
"react-doctor/no-document-start-view-transition": "Correctness",
|
|
1856
|
+
"react-doctor/no-flush-sync": "Performance",
|
|
1281
1857
|
"react-doctor/nextjs-no-img-element": "Next.js",
|
|
1282
1858
|
"react-doctor/nextjs-async-client-component": "Next.js",
|
|
1283
1859
|
"react-doctor/nextjs-no-a-element": "Next.js",
|
|
@@ -1296,7 +1872,14 @@ const RULE_CATEGORY_MAP = {
|
|
|
1296
1872
|
"react-doctor/nextjs-no-side-effect-in-get-handler": "Security",
|
|
1297
1873
|
"react-doctor/server-auth-actions": "Server",
|
|
1298
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",
|
|
1299
1881
|
"react-doctor/client-passive-event-listeners": "Performance",
|
|
1882
|
+
"react-doctor/client-localstorage-no-version": "Correctness",
|
|
1300
1883
|
"react-doctor/query-stable-query-client": "TanStack Query",
|
|
1301
1884
|
"react-doctor/query-no-rest-destructuring": "TanStack Query",
|
|
1302
1885
|
"react-doctor/query-no-void-query-fn": "TanStack Query",
|
|
@@ -1319,6 +1902,19 @@ const RULE_CATEGORY_MAP = {
|
|
|
1319
1902
|
"react-doctor/no-outline-none": "Accessibility",
|
|
1320
1903
|
"react-doctor/no-long-transition-duration": "Performance",
|
|
1321
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",
|
|
1322
1918
|
"react-doctor/async-parallel": "Performance",
|
|
1323
1919
|
"react-doctor/rn-no-raw-text": "React Native",
|
|
1324
1920
|
"react-doctor/rn-no-deprecated-modules": "React Native",
|
|
@@ -1328,6 +1924,22 @@ const RULE_CATEGORY_MAP = {
|
|
|
1328
1924
|
"react-doctor/rn-no-legacy-shadow-styles": "React Native",
|
|
1329
1925
|
"react-doctor/rn-prefer-reanimated": "React Native",
|
|
1330
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",
|
|
1331
1943
|
"react-doctor/tanstack-start-route-property-order": "TanStack Start",
|
|
1332
1944
|
"react-doctor/tanstack-start-no-direct-fetch-in-loader": "TanStack Start",
|
|
1333
1945
|
"react-doctor/tanstack-start-server-fn-validate-input": "TanStack Start",
|
|
@@ -1353,17 +1965,44 @@ const RULE_HELP_MAP = {
|
|
|
1353
1965
|
"rerender-lazy-state-init": "Wrap in an arrow function so it only runs once: `useState(() => expensiveComputation())`",
|
|
1354
1966
|
"rerender-functional-setstate": "Use the callback form: `setState(prev => prev + 1)` to always read the latest value",
|
|
1355
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",
|
|
1356
1970
|
"no-generic-handler-names": "Rename to describe the action: e.g. `handleSubmit` → `saveUserProfile`, `handleClick` → `toggleSidebar`",
|
|
1357
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",
|
|
1358
1975
|
"no-render-in-render": "Extract to a named component: `const ListItem = ({ item }) => <div>{item.name}</div>`",
|
|
1359
1976
|
"no-nested-component-definition": "Move to a separate file or to module scope above the parent component",
|
|
1360
1977
|
"no-usememo-simple-expression": "Remove useMemo — property access, math, and ternaries are already cheap without memoization",
|
|
1361
1978
|
"no-layout-property-animation": "Use `transform: translateX()` or `scale()` instead — they run on the compositor and skip layout/paint",
|
|
1362
1979
|
"rerender-memo-with-default-value": "Move to module scope: `const EMPTY_ITEMS: Item[] = []` then use as the default value",
|
|
1363
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",
|
|
1364
2002
|
"rendering-usetransition-loading": "Replace with `const [isPending, startTransition] = useTransition()` — avoids a re-render for the loading state",
|
|
1365
2003
|
"rendering-hydration-no-flicker": "Use `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` or add `suppressHydrationWarning` to the element",
|
|
1366
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",
|
|
1367
2006
|
"no-transition-all": "List specific properties: `transition: \"opacity 200ms, transform 200ms\"` — or in Tailwind use `transition-colors`, `transition-opacity`, or `transition-transform`",
|
|
1368
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",
|
|
1369
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",
|
|
@@ -1371,6 +2010,7 @@ const RULE_HELP_MAP = {
|
|
|
1371
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",
|
|
1372
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)",
|
|
1373
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",
|
|
1374
2014
|
"no-full-lodash-import": "Import the specific function: `import debounce from 'lodash/debounce'` — saves ~70kb",
|
|
1375
2015
|
"no-moment": "Replace with `import { format } from 'date-fns'` (tree-shakeable) or `import dayjs from 'dayjs'` (2kb)",
|
|
1376
2016
|
"prefer-dynamic-import": "Use `const Component = dynamic(() => import('library'), { ssr: false })` from next/dynamic or React.lazy()",
|
|
@@ -1412,7 +2052,11 @@ const RULE_HELP_MAP = {
|
|
|
1412
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",
|
|
1413
2053
|
"server-auth-actions": "Add `const session = await auth()` at the top and throw/redirect if unauthorized before any data access",
|
|
1414
2054
|
"server-after-nonblocking": "`import { after } from 'next/server'` then wrap: `after(() => analytics.track(...))` — response isn't blocked",
|
|
1415
|
-
"
|
|
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.",
|
|
1416
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",
|
|
1417
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",
|
|
1418
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",
|
|
@@ -1420,6 +2064,19 @@ const RULE_HELP_MAP = {
|
|
|
1420
2064
|
"query-mutation-missing-invalidation": "Add `onSuccess: () => queryClient.invalidateQueries({ queryKey: ['...'] })` so cached data stays in sync after the mutation",
|
|
1421
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",
|
|
1422
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",
|
|
1423
2080
|
"async-parallel": "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
|
|
1424
2081
|
"rn-no-raw-text": "Wrap text in a `<Text>` component: `<Text>{value}</Text>` — raw strings outside `<Text>` crash on React Native",
|
|
1425
2082
|
"rn-no-deprecated-modules": "Import from the community package instead — deprecated modules were removed from the react-native core",
|
|
@@ -1429,6 +2086,19 @@ const RULE_HELP_MAP = {
|
|
|
1429
2086
|
"rn-no-legacy-shadow-styles": "Use `boxShadow` for cross-platform shadows on the new architecture instead of platform-specific shadow properties",
|
|
1430
2087
|
"rn-prefer-reanimated": "Use `import Animated from 'react-native-reanimated'` — animations run on the UI thread instead of the JS thread",
|
|
1431
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",
|
|
1432
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",
|
|
1433
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",
|
|
1434
2104
|
"tanstack-start-server-fn-validate-input": "Add `.inputValidator(schema)` before `.handler()` — data crosses a network boundary and must be validated at runtime",
|
|
@@ -1483,35 +2153,61 @@ const resolvePluginPath = () => {
|
|
|
1483
2153
|
const resolveDiagnosticCategory = (plugin, rule) => {
|
|
1484
2154
|
return RULE_CATEGORY_MAP[`${plugin}/${rule}`] ?? PLUGIN_CATEGORY_MAP[plugin] ?? "Other";
|
|
1485
2155
|
};
|
|
1486
|
-
const
|
|
1487
|
-
const
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
for (const filePath of includePaths) {
|
|
1493
|
-
const entryLength = filePath.length + 1;
|
|
1494
|
-
const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > SPAWN_ARGS_MAX_LENGTH_CHARS;
|
|
1495
|
-
const exceedsFileCount = currentBatch.length >= OXLINT_MAX_FILES_PER_BATCH;
|
|
1496
|
-
if (exceedsArgLength || exceedsFileCount) {
|
|
1497
|
-
batches.push(currentBatch);
|
|
1498
|
-
currentBatch = [];
|
|
1499
|
-
currentBatchLength = baseArgsLength;
|
|
1500
|
-
}
|
|
1501
|
-
currentBatch.push(filePath);
|
|
1502
|
-
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;
|
|
1503
2162
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
2163
|
+
return sanitized;
|
|
2164
|
+
})();
|
|
2165
|
+
const OXLINT_SPAWN_TIMEOUT_MS = 5 * 6e4;
|
|
1507
2166
|
const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
|
|
1508
|
-
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?.();
|
|
1509
2176
|
const stdoutBuffers = [];
|
|
1510
2177
|
const stderrBuffers = [];
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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
|
+
}
|
|
1515
2211
|
if (signal) {
|
|
1516
2212
|
const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
|
|
1517
2213
|
const hint = signal === "SIGABRT" ? " (out of memory — try scanning fewer files with --diff)" : "";
|
|
@@ -1530,15 +2226,23 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
1530
2226
|
resolve(output);
|
|
1531
2227
|
});
|
|
1532
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
|
+
};
|
|
1533
2234
|
const parseOxlintOutput = (stdout) => {
|
|
1534
2235
|
if (!stdout) return [];
|
|
1535
|
-
|
|
2236
|
+
const jsonStart = stdout.indexOf("{");
|
|
2237
|
+
const sanitizedStdout = jsonStart > 0 ? stdout.slice(jsonStart) : stdout;
|
|
2238
|
+
let parsed;
|
|
1536
2239
|
try {
|
|
1537
|
-
|
|
2240
|
+
parsed = JSON.parse(sanitizedStdout);
|
|
1538
2241
|
} catch {
|
|
1539
|
-
throw new Error(`Failed to parse oxlint output: ${stdout.slice(0,
|
|
2242
|
+
throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, 200)}`);
|
|
1540
2243
|
}
|
|
1541
|
-
|
|
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) => {
|
|
1542
2246
|
const { plugin, rule } = parseRuleCode(diagnostic.code);
|
|
1543
2247
|
const primaryLabel = diagnostic.labels[0];
|
|
1544
2248
|
const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule);
|
|
@@ -1555,18 +2259,48 @@ const parseOxlintOutput = (stdout) => {
|
|
|
1555
2259
|
};
|
|
1556
2260
|
});
|
|
1557
2261
|
};
|
|
1558
|
-
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();
|
|
1559
2286
|
if (includePaths !== void 0 && includePaths.length === 0) return [];
|
|
1560
|
-
const
|
|
2287
|
+
const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
2288
|
+
const configPath = path.join(configDirectory, "oxlintrc.json");
|
|
1561
2289
|
const config = createOxlintConfig({
|
|
1562
2290
|
pluginPath: resolvePluginPath(),
|
|
1563
2291
|
framework,
|
|
1564
2292
|
hasReactCompiler,
|
|
2293
|
+
hasTanStackQuery,
|
|
1565
2294
|
customRulesOnly
|
|
1566
2295
|
});
|
|
1567
|
-
const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
2296
|
+
const restoreDisableDirectives = respectInlineDisables ? () => {} : neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
1568
2297
|
try {
|
|
1569
|
-
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
|
+
}
|
|
1570
2304
|
const baseArgs = [
|
|
1571
2305
|
resolveOxlintBinary(),
|
|
1572
2306
|
"-c",
|
|
@@ -1574,7 +2308,16 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
|
|
|
1574
2308
|
"--format",
|
|
1575
2309
|
"json"
|
|
1576
2310
|
];
|
|
1577
|
-
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
|
+
}
|
|
1578
2321
|
const fileBatches = includePaths !== void 0 ? batchIncludePaths(baseArgs, includePaths) : [["."]];
|
|
1579
2322
|
const allDiagnostics = [];
|
|
1580
2323
|
for (const batch of fileBatches) {
|
|
@@ -1584,72 +2327,111 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
|
|
|
1584
2327
|
return allDiagnostics;
|
|
1585
2328
|
} finally {
|
|
1586
2329
|
restoreDisableDirectives();
|
|
1587
|
-
|
|
2330
|
+
fs.rmSync(configDirectory, {
|
|
2331
|
+
recursive: true,
|
|
2332
|
+
force: true
|
|
2333
|
+
});
|
|
1588
2334
|
}
|
|
1589
2335
|
};
|
|
1590
|
-
|
|
1591
2336
|
//#endregion
|
|
1592
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
|
+
};
|
|
1593
2351
|
const getCurrentBranch = (directory) => {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
return null;
|
|
1602
|
-
}
|
|
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;
|
|
1603
2359
|
};
|
|
1604
2360
|
const detectDefaultBranch = (directory) => {
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
});
|
|
1616
|
-
return candidate;
|
|
1617
|
-
} catch {}
|
|
1618
|
-
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;
|
|
1619
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);
|
|
1620
2401
|
};
|
|
1621
2402
|
const getChangedFilesSinceBranch = (directory, baseBranch) => {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
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
|
+
]);
|
|
1635
2417
|
};
|
|
1636
2418
|
const getUncommittedChangedFiles = (directory) => {
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
return [];
|
|
1646
|
-
}
|
|
2419
|
+
return runGitNullSeparated(directory, [
|
|
2420
|
+
"diff",
|
|
2421
|
+
"-z",
|
|
2422
|
+
"--name-only",
|
|
2423
|
+
"--diff-filter=ACMR",
|
|
2424
|
+
"--relative",
|
|
2425
|
+
"HEAD"
|
|
2426
|
+
]) ?? [];
|
|
1647
2427
|
};
|
|
1648
2428
|
const getDiffInfo = (directory, explicitBaseBranch) => {
|
|
2429
|
+
if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) throw new Error("Diff base branch cannot be empty.");
|
|
1649
2430
|
const currentBranch = getCurrentBranch(directory);
|
|
1650
2431
|
if (!currentBranch) return null;
|
|
1651
2432
|
const baseBranch = explicitBaseBranch ?? detectDefaultBranch(directory);
|
|
1652
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).`);
|
|
1653
2435
|
if (currentBranch === baseBranch) {
|
|
1654
2436
|
const uncommittedFiles = getUncommittedChangedFiles(directory);
|
|
1655
2437
|
if (uncommittedFiles.length === 0) return null;
|
|
@@ -1660,16 +2442,40 @@ const getDiffInfo = (directory, explicitBaseBranch) => {
|
|
|
1660
2442
|
isCurrentChanges: true
|
|
1661
2443
|
};
|
|
1662
2444
|
}
|
|
2445
|
+
const changedFiles = getChangedFilesSinceBranch(directory, baseBranch);
|
|
2446
|
+
if (changedFiles === null) return null;
|
|
1663
2447
|
return {
|
|
1664
2448
|
currentBranch,
|
|
1665
2449
|
baseBranch,
|
|
1666
|
-
changedFiles
|
|
2450
|
+
changedFiles
|
|
1667
2451
|
};
|
|
1668
2452
|
};
|
|
1669
2453
|
const filterSourceFiles = (filePaths) => filePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));
|
|
1670
|
-
|
|
1671
2454
|
//#endregion
|
|
1672
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
|
+
});
|
|
1673
2479
|
const diagnose = async (directory, options = {}) => {
|
|
1674
2480
|
const resolvedDirectory = path.resolve(directory);
|
|
1675
2481
|
const userConfig = loadConfig(resolvedDirectory);
|
|
@@ -1684,7 +2490,16 @@ const diagnose = async (directory, options = {}) => {
|
|
|
1684
2490
|
calculateDiagnosticsScore: calculateScore,
|
|
1685
2491
|
getExtraDiagnostics: () => isDiffMode ? [] : checkReducedMotion(resolvedDirectory),
|
|
1686
2492
|
createRunners: ({ resolvedDirectory: projectRoot, projectInfo, userConfig: config }) => ({
|
|
1687
|
-
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
|
+
}),
|
|
1688
2503
|
runDeadCode: () => runKnip(projectRoot)
|
|
1689
2504
|
})
|
|
1690
2505
|
}, {
|
|
@@ -1692,7 +2507,7 @@ const diagnose = async (directory, options = {}) => {
|
|
|
1692
2507
|
lintIncludePaths
|
|
1693
2508
|
});
|
|
1694
2509
|
};
|
|
1695
|
-
|
|
1696
2510
|
//#endregion
|
|
1697
|
-
export { diagnose, filterSourceFiles, getDiffInfo };
|
|
2511
|
+
export { buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, summarizeDiagnostics, toJsonReport };
|
|
2512
|
+
|
|
1698
2513
|
//# sourceMappingURL=index.js.map
|