react-doctor 0.0.42 → 0.0.45

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