react-doctor 0.1.6 → 0.2.0-beta.0

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.
@@ -0,0 +1,1851 @@
1
+ import { a as FETCH_TIMEOUT_MS, c as PACKAGE_JSON_FILENAME, l as REACT_DOCTOR_CONFIG_FILENAME, o as MILLISECONDS_PER_SECOND, r as getScoreLabel, s as PACKAGE_JSON_CONFIG_KEY, t as calculateScore, u as SCORE_API_URL } from "./score-CzbtoFAu.js";
2
+ import { D as ReactDoctorInvalidConfigError, L as toReactDoctorErrorInfo, N as ReactDoctorRunnerUnavailableError, S as ReactDoctorCheckFailedError, _ as PACKAGE_JSON_FILENAME$1, a as DEPENDENCIES_RULE_ID, d as collectPatternNames, f as walkAst, g as IGNORED_DIRECTORY_NAMES, h as readPackageJson, i as REACT_ARCHITECTURE_RULE_ID, n as createRuleRegistry, o as DEAD_CODE_RULE_ID, p as isNodeOfType, s as runCodebaseAnalysis, v as SOURCE_FILE_EXTENSIONS } from "./rules-BfZ4Ujfv.js";
3
+ import { _ as createReactDoctorOxlintConfig, r as reactDoctorOxlintRuleMetadata, v as getReactDoctorRuleTags } from "./metadata-se470mRG.js";
4
+ import { createRequire } from "node:module";
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { spawn } from "node:child_process";
8
+ import fs$1, { existsSync } from "node:fs";
9
+ import os from "node:os";
10
+ import { fileURLToPath } from "node:url";
11
+ import { parseSync } from "oxc-parser";
12
+ //#region src/core/config.ts
13
+ const configCache = /* @__PURE__ */ new Map();
14
+ const isRecord$1 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
15
+ const pathExists = async (filePath) => {
16
+ try {
17
+ await fs.access(filePath);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ };
23
+ const isDirectory = async (filePath) => {
24
+ try {
25
+ return (await fs.stat(filePath)).isDirectory();
26
+ } catch {
27
+ return false;
28
+ }
29
+ };
30
+ const parseJsonFile = async (filePath) => {
31
+ try {
32
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
33
+ } catch (error) {
34
+ throw new ReactDoctorInvalidConfigError(`Failed to parse ${filePath}.`, { cause: error });
35
+ }
36
+ };
37
+ const assertStringArray = (value, fieldName, context) => {
38
+ if (value === void 0) return void 0;
39
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new ReactDoctorInvalidConfigError(`${context.sourcePath}: "${fieldName}" must be an array of strings.`);
40
+ return value;
41
+ };
42
+ const assertBoolean = (value, fieldName, context) => {
43
+ if (value === void 0) return void 0;
44
+ if (typeof value !== "boolean") throw new ReactDoctorInvalidConfigError(`${context.sourcePath}: "${fieldName}" must be a boolean.`);
45
+ return value;
46
+ };
47
+ const assertString = (value, fieldName, context) => {
48
+ if (value === void 0) return void 0;
49
+ if (typeof value !== "string") throw new ReactDoctorInvalidConfigError(`${context.sourcePath}: "${fieldName}" must be a string.`);
50
+ return value;
51
+ };
52
+ const assertFailOnLevel = (value, context) => {
53
+ if (value === void 0) return void 0;
54
+ if (value === "error" || value === "warning" || value === "none") return value;
55
+ throw new ReactDoctorInvalidConfigError(`${context.sourcePath}: "failOn" must be "error", "warning", or "none".`);
56
+ };
57
+ const assertDiff = (value, context) => {
58
+ if (value === void 0) return void 0;
59
+ if (typeof value === "boolean" || typeof value === "string") return value;
60
+ throw new ReactDoctorInvalidConfigError(`${context.sourcePath}: "diff" must be a boolean or branch name string.`);
61
+ };
62
+ const assertIgnoreConfig = (value, context) => {
63
+ if (value === void 0) return void 0;
64
+ if (!isRecord$1(value)) throw new ReactDoctorInvalidConfigError(`${context.sourcePath}: "ignore" must be an object.`);
65
+ const overrides = [];
66
+ if (value.overrides !== void 0) {
67
+ if (!Array.isArray(value.overrides) || value.overrides.some((override) => !isRecord$1(override))) throw new ReactDoctorInvalidConfigError(`${context.sourcePath}: "ignore.overrides" must be an array of objects.`);
68
+ for (const override of value.overrides) {
69
+ if (!isRecord$1(override)) continue;
70
+ overrides.push({
71
+ files: assertStringArray(override.files, "ignore.overrides[].files", context) ?? [],
72
+ rules: assertStringArray(override.rules, "ignore.overrides[].rules", context)
73
+ });
74
+ }
75
+ }
76
+ return {
77
+ rules: assertStringArray(value.rules, "ignore.rules", context),
78
+ files: assertStringArray(value.files, "ignore.files", context),
79
+ overrides
80
+ };
81
+ };
82
+ const validateConfig = (value, sourcePath) => {
83
+ if (!isRecord$1(value)) throw new ReactDoctorInvalidConfigError(`${sourcePath}: config must be a JSON object.`);
84
+ const context = { sourcePath };
85
+ return {
86
+ ignore: assertIgnoreConfig(value.ignore, context),
87
+ lint: assertBoolean(value.lint, "lint", context),
88
+ deadCode: assertBoolean(value.deadCode, "deadCode", context),
89
+ verbose: assertBoolean(value.verbose, "verbose", context),
90
+ diff: assertDiff(value.diff, context),
91
+ offline: assertBoolean(value.offline, "offline", context),
92
+ failOn: assertFailOnLevel(value.failOn, context),
93
+ customRulesOnly: assertBoolean(value.customRulesOnly, "customRulesOnly", context),
94
+ rootDir: assertString(value.rootDir, "rootDir", context),
95
+ textComponents: assertStringArray(value.textComponents, "textComponents", context),
96
+ rawTextWrapperComponents: assertStringArray(value.rawTextWrapperComponents, "rawTextWrapperComponents", context),
97
+ respectInlineDisables: assertBoolean(value.respectInlineDisables, "respectInlineDisables", context),
98
+ adoptExistingLintConfig: assertBoolean(value.adoptExistingLintConfig, "adoptExistingLintConfig", context),
99
+ includeEcosystemRules: assertBoolean(value.includeEcosystemRules, "includeEcosystemRules", context),
100
+ ignoredTags: assertStringArray(value.ignoredTags, "ignoredTags", context)
101
+ };
102
+ };
103
+ const loadConfigFromDirectory = async (directory) => {
104
+ const configPath = path.join(directory, REACT_DOCTOR_CONFIG_FILENAME);
105
+ if (await pathExists(configPath)) return {
106
+ config: validateConfig(await parseJsonFile(configPath), configPath),
107
+ sourceDirectory: directory,
108
+ sourcePath: configPath
109
+ };
110
+ const packageJsonPath = path.join(directory, PACKAGE_JSON_FILENAME);
111
+ if (!await pathExists(packageJsonPath)) return null;
112
+ const packageJson = await parseJsonFile(packageJsonPath);
113
+ if (!isRecord$1(packageJson) || !isRecord$1(packageJson["reactDoctor"])) return null;
114
+ return {
115
+ config: validateConfig(packageJson[PACKAGE_JSON_CONFIG_KEY], packageJsonPath),
116
+ sourceDirectory: directory,
117
+ sourcePath: `${packageJsonPath}#${PACKAGE_JSON_CONFIG_KEY}`
118
+ };
119
+ };
120
+ const isProjectBoundary = async (directory) => await pathExists(path.join(directory, ".git")) || await pathExists(path.join(directory, "pnpm-workspace.yaml")) || await pathExists(path.join(directory, "turbo.json")) || await pathExists(path.join(directory, "nx.json"));
121
+ const clearReactDoctorConfigCache = () => {
122
+ configCache.clear();
123
+ };
124
+ const loadReactDoctorConfig = async (startDirectory) => {
125
+ const rootDirectory = path.resolve(startDirectory);
126
+ const cachedConfig = configCache.get(rootDirectory);
127
+ if (cachedConfig !== void 0) return cachedConfig;
128
+ let currentDirectory = rootDirectory;
129
+ while (true) {
130
+ const loadedConfig = await loadConfigFromDirectory(currentDirectory);
131
+ if (loadedConfig) {
132
+ configCache.set(rootDirectory, loadedConfig);
133
+ return loadedConfig;
134
+ }
135
+ const parentDirectory = path.dirname(currentDirectory);
136
+ if (currentDirectory === parentDirectory || await isProjectBoundary(currentDirectory)) {
137
+ configCache.set(rootDirectory, null);
138
+ return null;
139
+ }
140
+ currentDirectory = parentDirectory;
141
+ }
142
+ };
143
+ const resolveConfigRootDirectory = async (loadedConfig, fallbackDirectory) => {
144
+ if (!loadedConfig) return fallbackDirectory;
145
+ const rootDir = loadedConfig.config.rootDir?.trim();
146
+ if (!rootDir) return fallbackDirectory;
147
+ const resolvedDirectory = path.isAbsolute(rootDir) ? rootDir : path.resolve(loadedConfig.sourceDirectory, rootDir);
148
+ if (!await isDirectory(resolvedDirectory)) throw new ReactDoctorInvalidConfigError(`${loadedConfig.sourcePath}: "rootDir" resolved to ${resolvedDirectory}, which is not a directory.`);
149
+ return resolvedDirectory;
150
+ };
151
+ //#endregion
152
+ //#region src/core/runners/check-ids.ts
153
+ const OXLINT_CHECK_ID = "react-doctor/oxlint";
154
+ //#endregion
155
+ //#region src/core/scoring-key.ts
156
+ const REACT_DOCTOR_CHECK_PREFIX = "react-doctor/";
157
+ const isCustomCheckId = (checkId) => typeof checkId === "string" && checkId.startsWith(REACT_DOCTOR_CHECK_PREFIX) && checkId !== "react-doctor/oxlint";
158
+ const getScoringRuleKey = (issue) => {
159
+ const checkId = issue.source?.checkId;
160
+ return isCustomCheckId(checkId) ? checkId : issue.source?.ruleId ?? issue.id;
161
+ };
162
+ const getScoringPluginKey = (issue) => issue.source?.pluginName ?? "react-doctor";
163
+ //#endregion
164
+ //#region src/core/issue-to-score-diagnostic.ts
165
+ const issueToScoreDiagnostic = (issue) => {
166
+ if (issue.severity === "info") return null;
167
+ return {
168
+ plugin: getScoringPluginKey(issue),
169
+ rule: getScoringRuleKey(issue),
170
+ category: issue.category,
171
+ severity: issue.severity === "error" ? "error" : "warning"
172
+ };
173
+ };
174
+ const collectScoreDiagnostics = (issues) => {
175
+ const scoringDiagnostics = [];
176
+ for (const issue of issues) {
177
+ const diagnostic = issueToScoreDiagnostic(issue);
178
+ if (diagnostic) scoringDiagnostics.push(diagnostic);
179
+ }
180
+ return scoringDiagnostics;
181
+ };
182
+ //#endregion
183
+ //#region src/core/reports.ts
184
+ const calculateReactDoctorScore = (issues) => {
185
+ const value = calculateScore(collectScoreDiagnostics(issues));
186
+ return {
187
+ value,
188
+ label: getScoreLabel(value)
189
+ };
190
+ };
191
+ const summarizeReactDoctorResult = (result) => {
192
+ const affectedFiles = new Set(result.issues.flatMap((issue) => issue.location?.filePath ? [issue.location.filePath] : []));
193
+ return {
194
+ errorCount: result.issues.filter((issue) => issue.severity === "error").length,
195
+ warningCount: result.issues.filter((issue) => issue.severity === "warning").length,
196
+ affectedFileCount: affectedFiles.size,
197
+ totalIssueCount: result.issues.length,
198
+ score: result.score?.value ?? null,
199
+ scoreLabel: result.score?.label ?? null
200
+ };
201
+ };
202
+ const buildReactDoctorJsonReport = (result) => ({
203
+ schemaVersion: 1,
204
+ ok: result.status === "completed" && !result.issues.some((issue) => issue.severity === "error"),
205
+ project: result.project,
206
+ issues: result.issues,
207
+ checks: result.checks,
208
+ summary: summarizeReactDoctorResult(result),
209
+ startedAt: result.startedAt,
210
+ completedAt: result.completedAt,
211
+ durationMilliseconds: result.durationMilliseconds
212
+ });
213
+ //#endregion
214
+ //#region src/core/project.ts
215
+ const FRAMEWORK_PACKAGES = {
216
+ "@remix-run/react": "remix",
217
+ "@tanstack/react-start": "tanstack-start",
218
+ expo: "expo",
219
+ gatsby: "gatsby",
220
+ next: "nextjs",
221
+ "react-native": "react-native",
222
+ "react-scripts": "cra",
223
+ vite: "vite"
224
+ };
225
+ const REACT_COMPILER_PACKAGES = new Set([
226
+ "babel-plugin-react-compiler",
227
+ "eslint-plugin-react-compiler",
228
+ "react-compiler-runtime"
229
+ ]);
230
+ const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
231
+ const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
232
+ const NEXT_CONFIG_FILENAMES = [
233
+ "next.config.cjs",
234
+ "next.config.js",
235
+ "next.config.mjs",
236
+ "next.config.ts"
237
+ ];
238
+ const BABEL_CONFIG_FILENAMES = [
239
+ ".babelrc",
240
+ ".babelrc.json",
241
+ "babel.config.cjs",
242
+ "babel.config.js",
243
+ "babel.config.json",
244
+ "babel.config.mjs"
245
+ ];
246
+ const VITE_CONFIG_FILENAMES = [
247
+ "vite.config.cjs",
248
+ "vite.config.cts",
249
+ "vite.config.js",
250
+ "vite.config.mjs",
251
+ "vite.config.mts",
252
+ "vite.config.ts",
253
+ "vitest.config.js",
254
+ "vitest.config.ts"
255
+ ];
256
+ const EXPO_CONFIG_FILENAMES = [
257
+ "app.config.js",
258
+ "app.config.ts",
259
+ "app.json"
260
+ ];
261
+ const TANSTACK_AI_PACKAGES = new Set(["@tanstack/ai", "@tanstack/ai-code-mode"]);
262
+ const TANSTACK_QUERY_PACKAGES = new Set([
263
+ "@tanstack/query-core",
264
+ "@tanstack/react-query",
265
+ "react-query"
266
+ ]);
267
+ const SOURCE_FILE_EXTENSION_SET = new Set(SOURCE_FILE_EXTENSIONS);
268
+ const createEmptyCatalogInfo = () => ({
269
+ defaultVersions: /* @__PURE__ */ new Map(),
270
+ groupedVersions: /* @__PURE__ */ new Map()
271
+ });
272
+ const isSourceFileName = (fileName) => SOURCE_FILE_EXTENSION_SET.has(path.extname(fileName));
273
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
274
+ const addCatalogVersions = (target, value) => {
275
+ if (!isRecord(value)) return;
276
+ for (const [packageName, version] of Object.entries(value)) if (typeof version === "string") target.set(packageName, version);
277
+ };
278
+ const addGroupedCatalogVersions = (catalogs, value) => {
279
+ if (!isRecord(value)) return;
280
+ for (const [catalogName, entries] of Object.entries(value)) {
281
+ const versions = catalogs.groupedVersions.get(catalogName) ?? /* @__PURE__ */ new Map();
282
+ addCatalogVersions(versions, entries);
283
+ catalogs.groupedVersions.set(catalogName, versions);
284
+ }
285
+ };
286
+ const mergeManifestCatalogs = (catalogs, manifest) => {
287
+ if (!manifest) return;
288
+ addCatalogVersions(catalogs.defaultVersions, manifest.catalog);
289
+ addGroupedCatalogVersions(catalogs, manifest.catalogs);
290
+ const workspaces = manifest.workspaces;
291
+ if (isRecord(workspaces)) {
292
+ addCatalogVersions(catalogs.defaultVersions, workspaces.catalog);
293
+ addGroupedCatalogVersions(catalogs, workspaces.catalogs);
294
+ }
295
+ };
296
+ const PNPM_WORKSPACE_FILENAME = "pnpm-workspace.yaml";
297
+ const stripYamlComment = (line) => {
298
+ let quote = null;
299
+ for (let index = 0; index < line.length; index++) {
300
+ const character = line[index];
301
+ if ((character === "\"" || character === "'") && line[index - 1] !== "\\") quote = quote === character ? null : quote ?? character;
302
+ if (character === "#" && !quote) return line.slice(0, index);
303
+ }
304
+ return line;
305
+ };
306
+ const stripYamlValue = (value) => stripYamlComment(value).trim().replace(/^["']|["']$/g, "");
307
+ const parsePnpmWorkspaceFile = (content) => {
308
+ const result = {
309
+ patterns: [],
310
+ defaultCatalog: /* @__PURE__ */ new Map(),
311
+ namedCatalogs: /* @__PURE__ */ new Map()
312
+ };
313
+ let section = "none";
314
+ let currentCatalogName = "";
315
+ for (const rawLine of content.split("\n")) {
316
+ const line = stripYamlComment(rawLine);
317
+ if (line.trim().length === 0) continue;
318
+ const indent = line.length - line.trimStart().length;
319
+ const trimmed = line.trim();
320
+ if (indent === 0) {
321
+ if (trimmed === "packages:") {
322
+ section = "packages";
323
+ continue;
324
+ }
325
+ if (trimmed === "catalog:") {
326
+ section = "catalog";
327
+ continue;
328
+ }
329
+ if (trimmed === "catalogs:") {
330
+ section = "catalogs";
331
+ continue;
332
+ }
333
+ if (trimmed.startsWith("-") && section === "packages") {
334
+ const pattern = stripYamlValue(trimmed.slice(1));
335
+ if (pattern) result.patterns.push(pattern);
336
+ continue;
337
+ }
338
+ section = "none";
339
+ continue;
340
+ }
341
+ if (section === "packages") {
342
+ if (trimmed.startsWith("-")) {
343
+ const pattern = stripYamlValue(trimmed.slice(1));
344
+ if (pattern) result.patterns.push(pattern);
345
+ }
346
+ continue;
347
+ }
348
+ if (section === "catalog") {
349
+ const colonIndex = trimmed.indexOf(":");
350
+ if (colonIndex > 0) {
351
+ const key = stripYamlValue(trimmed.slice(0, colonIndex));
352
+ const value = stripYamlValue(trimmed.slice(colonIndex + 1));
353
+ if (key && value) result.defaultCatalog.set(key, value);
354
+ }
355
+ continue;
356
+ }
357
+ if (section === "catalogs") {
358
+ if (trimmed.endsWith(":") && !trimmed.includes(" ")) {
359
+ currentCatalogName = stripYamlValue(trimmed.slice(0, -1));
360
+ result.namedCatalogs.set(currentCatalogName, /* @__PURE__ */ new Map());
361
+ section = "named-catalog";
362
+ }
363
+ continue;
364
+ }
365
+ if (section === "named-catalog") {
366
+ if (indent <= 2 && trimmed.endsWith(":") && !trimmed.includes(" ")) {
367
+ currentCatalogName = stripYamlValue(trimmed.slice(0, -1));
368
+ result.namedCatalogs.set(currentCatalogName, /* @__PURE__ */ new Map());
369
+ continue;
370
+ }
371
+ const colonIndex = trimmed.indexOf(":");
372
+ if (colonIndex > 0 && currentCatalogName) {
373
+ const key = stripYamlValue(trimmed.slice(0, colonIndex));
374
+ const value = stripYamlValue(trimmed.slice(colonIndex + 1));
375
+ if (key && value) {
376
+ const catalog = result.namedCatalogs.get(currentCatalogName);
377
+ if (catalog) catalog.set(key, value);
378
+ }
379
+ }
380
+ }
381
+ }
382
+ return result;
383
+ };
384
+ const readPnpmWorkspaceFile = async (directory) => {
385
+ try {
386
+ return parsePnpmWorkspaceFile(await fs.readFile(path.join(directory, PNPM_WORKSPACE_FILENAME), "utf8"));
387
+ } catch {
388
+ return null;
389
+ }
390
+ };
391
+ const mergePnpmWorkspaceCatalogs = (catalogs, file) => {
392
+ for (const [name, version] of file.defaultCatalog) catalogs.defaultVersions.set(name, version);
393
+ for (const [catalogName, entries] of file.namedCatalogs) {
394
+ const target = catalogs.groupedVersions.get(catalogName) ?? /* @__PURE__ */ new Map();
395
+ for (const [name, version] of entries) target.set(name, version);
396
+ catalogs.groupedVersions.set(catalogName, target);
397
+ }
398
+ };
399
+ const collectAncestorCatalogs = async (rootDirectory) => {
400
+ const catalogs = createEmptyCatalogInfo();
401
+ let currentDirectory = rootDirectory;
402
+ while (true) {
403
+ mergeManifestCatalogs(catalogs, await readPackageJson(currentDirectory));
404
+ const pnpmFile = await readPnpmWorkspaceFile(currentDirectory);
405
+ if (pnpmFile) mergePnpmWorkspaceCatalogs(catalogs, pnpmFile);
406
+ const parentDirectory = path.dirname(currentDirectory);
407
+ if (parentDirectory === currentDirectory) return catalogs;
408
+ currentDirectory = parentDirectory;
409
+ }
410
+ };
411
+ const readNearestPackageInfo = async (rootDirectory) => {
412
+ const catalogs = await collectAncestorCatalogs(rootDirectory);
413
+ let currentDirectory = rootDirectory;
414
+ while (true) {
415
+ const manifest = await readPackageJson(currentDirectory);
416
+ if (manifest) return {
417
+ manifest,
418
+ packageJsonPath: path.join(currentDirectory, PACKAGE_JSON_FILENAME$1),
419
+ catalogs
420
+ };
421
+ const parentDirectory = path.dirname(currentDirectory);
422
+ if (parentDirectory === currentDirectory) return {
423
+ manifest: null,
424
+ packageJsonPath: null,
425
+ catalogs
426
+ };
427
+ currentDirectory = parentDirectory;
428
+ }
429
+ };
430
+ const collectDependencies = (manifest) => new Map([
431
+ ...Object.entries(manifest?.peerDependencies ?? {}),
432
+ ...Object.entries(manifest?.dependencies ?? {}),
433
+ ...Object.entries(manifest?.devDependencies ?? {}),
434
+ ...Object.entries(manifest?.optionalDependencies ?? {})
435
+ ].filter((entry) => typeof entry[1] === "string"));
436
+ const hasAnyDependency = (dependencies, packageNames) => {
437
+ for (const packageName of packageNames) if (dependencies.has(packageName)) return true;
438
+ return false;
439
+ };
440
+ const hasReactCompilerDependency = (manifest) => hasAnyDependency(collectDependencies(manifest), REACT_COMPILER_PACKAGES);
441
+ const detectFramework = (dependencies) => {
442
+ for (const [packageName, framework] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies.has(packageName)) return framework;
443
+ return dependencies.has("react") ? "react" : "unknown";
444
+ };
445
+ const toResolvedDependencyVersion = (packageName, version, catalogs) => {
446
+ if (!version) return null;
447
+ if (version.startsWith("catalog:")) {
448
+ const catalogName = version.slice(8);
449
+ if (!catalogName) return catalogs.defaultVersions.get(packageName) ?? null;
450
+ return catalogs.groupedVersions.get(catalogName)?.get(packageName) ?? null;
451
+ }
452
+ if (version.startsWith("workspace:")) return null;
453
+ return version;
454
+ };
455
+ const parseReactMajorVersion = (version) => {
456
+ if (!version) return null;
457
+ const match = version.match(/\d+/);
458
+ if (!match) return null;
459
+ return Number.parseInt(match[0], 10);
460
+ };
461
+ const getDependencyInfo = (packageInfo) => {
462
+ const { catalogs, manifest } = packageInfo;
463
+ const dependencies = collectDependencies(manifest);
464
+ return {
465
+ reactVersion: toResolvedDependencyVersion("react", dependencies.get("react"), catalogs),
466
+ reactPeerDependencyRange: typeof manifest?.peerDependencies?.react === "string" ? manifest.peerDependencies.react : null,
467
+ tailwindVersion: toResolvedDependencyVersion("tailwindcss", dependencies.get("tailwindcss"), catalogs),
468
+ framework: detectFramework(dependencies),
469
+ hasReactCompiler: hasReactCompilerDependency(manifest),
470
+ hasTanStackAI: hasAnyDependency(dependencies, TANSTACK_AI_PACKAGES),
471
+ hasTanStackQuery: hasAnyDependency(dependencies, TANSTACK_QUERY_PACKAGES)
472
+ };
473
+ };
474
+ const readTextFile = async (filePath) => {
475
+ try {
476
+ return await fs.readFile(filePath, "utf8");
477
+ } catch {
478
+ return null;
479
+ }
480
+ };
481
+ const hasReactCompilerConfigText = (content) => REACT_COMPILER_ENABLED_FLAG_PATTERN.test(content) || REACT_COMPILER_PACKAGE_REFERENCE_PATTERN.test(content);
482
+ const hasReactCompilerInConfigFiles = async (directory, filenames) => {
483
+ for (const filename of filenames) {
484
+ const content = await readTextFile(path.join(directory, filename));
485
+ if (content && hasReactCompilerConfigText(content)) return true;
486
+ }
487
+ return false;
488
+ };
489
+ const hasReactCompilerInLocalConfig = async (directory) => await hasReactCompilerInConfigFiles(directory, NEXT_CONFIG_FILENAMES) || await hasReactCompilerInConfigFiles(directory, BABEL_CONFIG_FILENAMES) || await hasReactCompilerInConfigFiles(directory, VITE_CONFIG_FILENAMES) || await hasReactCompilerInConfigFiles(directory, EXPO_CONFIG_FILENAMES);
490
+ const hasWorkspaceBoundary = (manifest) => Boolean(manifest?.workspaces);
491
+ const hasDirectoryEntry = async (directory, entryName) => {
492
+ try {
493
+ await fs.access(path.join(directory, entryName));
494
+ return true;
495
+ } catch {
496
+ return false;
497
+ }
498
+ };
499
+ const hasReactCompilerInAncestorPackage = async (rootDirectory) => {
500
+ let currentDirectory = path.dirname(rootDirectory);
501
+ while (currentDirectory !== path.dirname(currentDirectory)) {
502
+ const manifest = await readPackageJson(currentDirectory);
503
+ if (hasReactCompilerDependency(manifest)) return true;
504
+ if (hasWorkspaceBoundary(manifest) || await hasDirectoryEntry(currentDirectory, ".git")) return false;
505
+ currentDirectory = path.dirname(currentDirectory);
506
+ }
507
+ return false;
508
+ };
509
+ const detectReactCompiler = async (rootDirectory, manifest) => {
510
+ if (hasReactCompilerDependency(manifest)) return true;
511
+ if (await hasReactCompilerInLocalConfig(rootDirectory)) return true;
512
+ return hasReactCompilerInAncestorPackage(rootDirectory);
513
+ };
514
+ const collectSourceFileInfo = async (rootDirectory) => {
515
+ const sourceFileInfo = {
516
+ count: 0,
517
+ hasTypeScript: false
518
+ };
519
+ const directories = [rootDirectory];
520
+ while (directories.length > 0) {
521
+ const directory = directories.pop();
522
+ if (!directory) continue;
523
+ let entries;
524
+ try {
525
+ entries = await fs.readdir(directory, { withFileTypes: true });
526
+ } catch {
527
+ continue;
528
+ }
529
+ for (const entry of entries) {
530
+ if (entry.isDirectory()) {
531
+ if (!entry.name.startsWith(".") && !IGNORED_DIRECTORY_NAMES.has(entry.name)) directories.push(path.join(directory, entry.name));
532
+ continue;
533
+ }
534
+ if (entry.isFile() && isSourceFileName(entry.name)) {
535
+ sourceFileInfo.count++;
536
+ sourceFileInfo.hasTypeScript ||= /\.(cts|mts|ts|tsx)$/.test(entry.name);
537
+ }
538
+ }
539
+ }
540
+ return sourceFileInfo;
541
+ };
542
+ const toOxlintProjectInfo = (project) => {
543
+ return {
544
+ framework: project.framework === "nextjs" || project.framework === "expo" || project.framework === "react-native" || project.framework === "tanstack-start" ? project.framework : "react",
545
+ hasReactCompiler: project.hasReactCompiler,
546
+ hasTanStackAI: project.hasTanStackAI,
547
+ hasTanStackQuery: project.hasTanStackQuery,
548
+ hasTypeScript: project.hasTypeScript,
549
+ reactMajorVersion: project.reactMajorVersion,
550
+ reactPeerDependencyRange: project.reactPeerDependencyRange,
551
+ tailwindVersion: project.tailwindVersion
552
+ };
553
+ };
554
+ const hasFile = async (filePath) => {
555
+ try {
556
+ return (await fs.stat(filePath)).isFile();
557
+ } catch {
558
+ return false;
559
+ }
560
+ };
561
+ const toNpmWorkspacePatterns = (manifest) => {
562
+ const workspaces = manifest?.workspaces;
563
+ if (!workspaces) return [];
564
+ if (Array.isArray(workspaces)) return workspaces.filter((value) => typeof value === "string");
565
+ if (isRecord(workspaces) && Array.isArray(workspaces.packages)) return workspaces.packages.filter((value) => typeof value === "string");
566
+ return [];
567
+ };
568
+ const isMonorepoRoot = async (directory) => {
569
+ if (toNpmWorkspacePatterns(await readPackageJson(directory)).length > 0) return true;
570
+ return hasFile(path.join(directory, PNPM_WORKSPACE_FILENAME));
571
+ };
572
+ const findAncestorMonorepoRoot = async (startDirectory) => {
573
+ let currentDirectory = path.dirname(startDirectory);
574
+ while (currentDirectory !== path.dirname(currentDirectory)) {
575
+ if (await isMonorepoRoot(currentDirectory)) return currentDirectory;
576
+ currentDirectory = path.dirname(currentDirectory);
577
+ }
578
+ return null;
579
+ };
580
+ const expandWorkspacePattern = async (rootDirectory, pattern) => {
581
+ const normalized = pattern.replace(/\*\*/g, "*");
582
+ const wildcardIndex = normalized.indexOf("*");
583
+ if (wildcardIndex < 0) {
584
+ const directory = path.resolve(rootDirectory, normalized);
585
+ return await hasFile(path.join(directory, "package.json")) ? [directory] : [];
586
+ }
587
+ const prefix = normalized.slice(0, wildcardIndex).replace(/\/$/, "");
588
+ const suffix = normalized.slice(wildcardIndex + 1).replace(/^\//, "");
589
+ const baseDirectory = path.resolve(rootDirectory, prefix || ".");
590
+ let entries;
591
+ try {
592
+ entries = await fs.readdir(baseDirectory, { withFileTypes: true });
593
+ } catch {
594
+ return [];
595
+ }
596
+ const directories = [];
597
+ for (const entry of entries) {
598
+ if (!entry.isDirectory()) continue;
599
+ if (entry.name.startsWith(".") || IGNORED_DIRECTORY_NAMES.has(entry.name)) continue;
600
+ const candidate = path.join(baseDirectory, entry.name, suffix);
601
+ if (await hasFile(path.join(candidate, "package.json"))) directories.push(candidate);
602
+ }
603
+ return directories;
604
+ };
605
+ const findTailwindcssInWorkspaces = async (monorepoRoot, catalogs) => {
606
+ const npmPatterns = toNpmWorkspacePatterns(await readPackageJson(monorepoRoot));
607
+ const pnpmPatterns = (await readPnpmWorkspaceFile(monorepoRoot))?.patterns ?? [];
608
+ const patterns = [...new Set([...npmPatterns, ...pnpmPatterns])].filter((entry) => !entry.startsWith("!"));
609
+ for (const pattern of patterns) {
610
+ const directories = await expandWorkspacePattern(monorepoRoot, pattern);
611
+ for (const directory of directories) {
612
+ const resolved = toResolvedDependencyVersion("tailwindcss", collectDependencies(await readPackageJson(directory)).get("tailwindcss"), catalogs);
613
+ if (resolved) return resolved;
614
+ }
615
+ }
616
+ return null;
617
+ };
618
+ const discoverReactProject = async (rootDirectory) => {
619
+ const resolvedRootDirectory = path.resolve(rootDirectory);
620
+ const packageInfo = await readNearestPackageInfo(resolvedRootDirectory);
621
+ const dependencyInfo = getDependencyInfo(packageInfo);
622
+ let tailwindVersion = dependencyInfo.tailwindVersion;
623
+ if (!tailwindVersion && await isMonorepoRoot(resolvedRootDirectory)) tailwindVersion = await findTailwindcssInWorkspaces(resolvedRootDirectory, packageInfo.catalogs);
624
+ if (!tailwindVersion) {
625
+ const ancestorMonorepoRoot = await findAncestorMonorepoRoot(resolvedRootDirectory);
626
+ if (ancestorMonorepoRoot) tailwindVersion = await findTailwindcssInWorkspaces(ancestorMonorepoRoot, packageInfo.catalogs);
627
+ }
628
+ const sourceFileInfo = await collectSourceFileInfo(resolvedRootDirectory);
629
+ const hasReactCompiler = dependencyInfo.hasReactCompiler || await detectReactCompiler(resolvedRootDirectory, packageInfo.manifest);
630
+ return {
631
+ rootDirectory: resolvedRootDirectory,
632
+ projectName: packageInfo.manifest?.name ?? path.basename(resolvedRootDirectory),
633
+ packageJsonPath: packageInfo.packageJsonPath,
634
+ reactVersion: dependencyInfo.reactVersion,
635
+ reactMajorVersion: parseReactMajorVersion(dependencyInfo.reactVersion),
636
+ reactPeerDependencyRange: dependencyInfo.reactPeerDependencyRange,
637
+ tailwindVersion,
638
+ framework: dependencyInfo.framework,
639
+ hasTypeScript: sourceFileInfo.hasTypeScript,
640
+ hasReactCompiler,
641
+ hasTanStackAI: dependencyInfo.hasTanStackAI,
642
+ hasTanStackQuery: dependencyInfo.hasTanStackQuery,
643
+ sourceFileCount: sourceFileInfo.count
644
+ };
645
+ };
646
+ //#endregion
647
+ //#region src/core/is-test-file-path.ts
648
+ const TEST_FILE_PATH_PATTERN = /(?:^|\/)(?:__tests__|__test__|tests|test|__mocks__|cypress|e2e|playwright)\/|\.(?:test|spec|stories|story|fixture|fixtures)\.(?:[cm]?[jt]sx?)$/;
649
+ const isTestFilePath = (relativePath) => {
650
+ if (relativePath.length === 0) return false;
651
+ const forwardSlashed = relativePath.replaceAll("\\", "/");
652
+ return TEST_FILE_PATH_PATTERN.test(forwardSlashed);
653
+ };
654
+ //#endregion
655
+ //#region src/core/diagnostics.ts
656
+ const TEST_NOISE_TAG = "test-noise";
657
+ const WRAPPED_RULE_ID_PATTERN = /^([a-zA-Z][\w-]*)\(([^)]+)\)$/;
658
+ const REACT_BUILTIN_RULE_PREFIX = /^(?:react|jsx-a11y)\//;
659
+ const JSX_A11Y_RULE_PREFIX = "jsx-a11y/";
660
+ const OG_IMAGE_FILE_PATTERN = /\/(?:opengraph-image|twitter-image|icon|apple-icon)\.[jt]sx?$/;
661
+ const OG_JSX_FILE_PATTERN = /\/(?:api\/)?og(?:\/|$)|\/(?:opengraph-image|twitter-image|icon|apple-icon)\.[jt]sx?$/;
662
+ const NON_REACT_JSX_IMPORT_PATTERN = /(?:^|\n)\s*import\s.*from\s+['"](?:solid-js|preact)/;
663
+ const NON_REACT_JSX_SOURCES = new Set([
664
+ "preact",
665
+ "solid-js",
666
+ "vue",
667
+ "svelte"
668
+ ]);
669
+ const EMOTION_IMPORT_PATTERN = /(?:^|\n)\s*import\s.*from\s+['"]@emotion\/react['"]/;
670
+ const IMAGE_RESPONSE_IMPORT_PATTERN = /(?:^|\n)\s*import\s.*\bImageResponse\b.*from\s+['"](?:next\/og|@vercel\/og)['"]/;
671
+ const SATORI_TW_PROP_PATTERN = /\btw\s*=/;
672
+ const EMOTION_CSS_PROP_PATTERN = /\bcss\s*=/;
673
+ const REACT_DOCTOR_DISABLE_LINE_DIRECTIVE = "react-doctor-disable-line";
674
+ const REACT_DOCTOR_DISABLE_NEXT_LINE_DIRECTIVE = "react-doctor-disable-next-line";
675
+ const REACT_DOCTOR_RULE_NAMESPACE = "react-doctor/";
676
+ const DISABLE_TOKEN_SEPARATOR_PATTERN = /[\s,]+/;
677
+ const DISABLE_COMMENT_BOUNDARY_PATTERN = /\*\/|-->/;
678
+ const REGEX_METACHARACTER_PATTERN = /[.*+?^${}()|[\]\\]/g;
679
+ const ECOSYSTEM_RULE_PREFIX_PATTERN = /^(?:nextjs|rn|tailwind|query|swr|mobx|shadcn|radix|rhf|r3f|storybook|testing)-/;
680
+ const escapeRegExpMetacharacters = (value) => value.replace(REGEX_METACHARACTER_PATTERN, "\\$&");
681
+ const EFFECT_RULE_ALIASES = new Map([
682
+ ["react-doctor/effect-no-event-handler", "effect-event-handler"],
683
+ ["react-doctor/no-effect-event-handler", "effect-event-handler"],
684
+ ["effect/no-event-handler", "effect-event-handler"],
685
+ ["react-doctor/effect-no-derived-state", "effect-derived-state"],
686
+ ["react-doctor/no-derived-state-effect", "effect-derived-state"],
687
+ ["effect/no-derived-state", "effect-derived-state"],
688
+ ["react-doctor/effect-no-chain-state-updates", "effect-chain-state"],
689
+ ["react-doctor/no-effect-chain", "effect-chain-state"],
690
+ ["effect/no-chain-state-updates", "effect-chain-state"],
691
+ ["react-doctor/effect-no-adjust-state-on-prop-change", "effect-adjust-prop"],
692
+ ["effect/no-adjust-state-on-prop-change", "effect-adjust-prop"],
693
+ ["react-doctor/effect-no-initialize-state", "effect-init-state"],
694
+ ["effect/no-initialize-state", "effect-init-state"],
695
+ ["react-doctor/effect-no-pass-data-to-parent", "effect-pass-parent"],
696
+ ["effect/no-pass-data-to-parent", "effect-pass-parent"],
697
+ ["react-doctor/effect-no-pass-live-state-to-parent", "effect-pass-live-state"],
698
+ ["effect/no-pass-live-state-to-parent", "effect-pass-live-state"],
699
+ ["react-doctor/effect-no-reset-all-state-on-prop-change", "effect-reset-state"],
700
+ ["effect/no-reset-all-state-on-prop-change", "effect-reset-state"]
701
+ ]);
702
+ const toCanonicalEffectKey = (ruleId) => EFFECT_RULE_ALIASES.get(ruleId) ?? null;
703
+ const toMetadataRuleKey = (issue) => {
704
+ const ruleId = issue.source?.ruleId;
705
+ if (!ruleId) return null;
706
+ const wrapped = WRAPPED_RULE_ID_PATTERN.exec(ruleId);
707
+ if (wrapped) return `${wrapped[1]}/${wrapped[2]}`;
708
+ if (issue.source?.pluginName && !ruleId.includes("/")) return `${issue.source.pluginName}/${ruleId}`;
709
+ return ruleId;
710
+ };
711
+ const isAutoSuppressedTestNoise = (issue, relativeFilePath) => {
712
+ if (!relativeFilePath) return false;
713
+ const ruleKey = toMetadataRuleKey(issue);
714
+ if (!ruleKey) return false;
715
+ if (!getReactDoctorRuleTags(ruleKey).has(TEST_NOISE_TAG)) return false;
716
+ return isTestFilePath(relativeFilePath);
717
+ };
718
+ const RN_NO_RAW_TEXT_RULE_ID = "rn-no-raw-text";
719
+ const normalizePath = (filePath) => filePath.replace(/\\/g, "/");
720
+ const normalizeRuleId = (issue) => {
721
+ if (issue.source?.pluginName && issue.source.ruleId) return `${issue.source.pluginName}/${issue.source.ruleId}`;
722
+ return issue.source?.ruleId ?? issue.id;
723
+ };
724
+ const stripRuleNamespace = (ruleId) => ruleId.split("/").at(-1) ?? ruleId;
725
+ const matchesRule = (issue, rulePatterns) => {
726
+ const ruleId = normalizeRuleId(issue);
727
+ return rulePatterns.has(ruleId) || rulePatterns.has(stripRuleNamespace(ruleId));
728
+ };
729
+ const matchesPathPattern = (filePath, pattern) => {
730
+ const normalizedFilePath = normalizePath(filePath);
731
+ const normalizedPattern = normalizePath(pattern).replace(/^\.\//, "");
732
+ if (normalizedPattern.endsWith("/**")) {
733
+ const directoryPattern = normalizedPattern.slice(0, -3);
734
+ return normalizedFilePath === directoryPattern || normalizedFilePath.startsWith(`${directoryPattern}/`);
735
+ }
736
+ if (normalizedPattern.includes("*")) return new RegExp(`^${normalizedPattern.split("*").map(escapeRegExpMetacharacters).join(".*")}$`).test(normalizedFilePath);
737
+ return normalizedFilePath === normalizedPattern || normalizedFilePath.startsWith(`${normalizedPattern}/`);
738
+ };
739
+ const toRelativeIssuePath = (issue, rootDirectory) => {
740
+ const filePath = issue.location?.filePath;
741
+ if (!filePath) return "";
742
+ if (!path.isAbsolute(filePath)) return normalizePath(filePath);
743
+ return normalizePath(path.relative(rootDirectory, filePath));
744
+ };
745
+ const compileOverrides = (config) => (config.ignore?.overrides ?? []).map((override) => ({
746
+ files: override.files,
747
+ rules: override.rules ? new Set(override.rules) : null
748
+ }));
749
+ const isIgnoredByOverride = (issue, filePath, overrides) => {
750
+ for (const override of overrides) {
751
+ if (!override.files.some((pattern) => matchesPathPattern(filePath, pattern))) continue;
752
+ if (!override.rules || matchesRule(issue, override.rules)) return true;
753
+ }
754
+ return false;
755
+ };
756
+ const tokenizeReactDoctorDisableDirective = (commentLine, directive) => {
757
+ const directiveIndex = commentLine.indexOf(directive);
758
+ if (directiveIndex === -1) return null;
759
+ const afterDirective = commentLine.slice(directiveIndex + directive.length);
760
+ const boundaryMatch = DISABLE_COMMENT_BOUNDARY_PATTERN.exec(afterDirective);
761
+ return (boundaryMatch ? afterDirective.slice(0, boundaryMatch.index) : afterDirective).split(DISABLE_TOKEN_SEPARATOR_PATTERN).map((token) => token.trim()).filter((token) => token.length > 0);
762
+ };
763
+ const matchesReactDoctorDisableToken = (token, ruleId) => {
764
+ if (token === ruleId) return true;
765
+ if (!token.startsWith(REACT_DOCTOR_RULE_NAMESPACE)) return false;
766
+ return token.slice(13) === ruleId;
767
+ };
768
+ const isLineDisabledByReactDoctorComment = (commentLine, directive, ruleId) => {
769
+ const tokens = tokenizeReactDoctorDisableDirective(commentLine, directive);
770
+ if (tokens === null) return false;
771
+ if (tokens.length === 0) return true;
772
+ return tokens.some((token) => matchesReactDoctorDisableToken(token, ruleId));
773
+ };
774
+ const isDisabledByStackedDisableNextLine = (sourceLines, issueLineIndex, ruleId) => {
775
+ let cursorLineIndex = issueLineIndex - 2;
776
+ while (cursorLineIndex >= 0) {
777
+ const commentLine = sourceLines[cursorLineIndex];
778
+ if (commentLine === void 0) return false;
779
+ const tokens = tokenizeReactDoctorDisableDirective(commentLine, REACT_DOCTOR_DISABLE_NEXT_LINE_DIRECTIVE);
780
+ if (tokens === null) return false;
781
+ if (tokens.length === 0) return true;
782
+ if (tokens.some((token) => matchesReactDoctorDisableToken(token, ruleId))) return true;
783
+ cursorLineIndex -= 1;
784
+ }
785
+ return false;
786
+ };
787
+ const isDisabledByEcosystemDisableNextLine = (previousLine, ruleId) => {
788
+ if (!previousLine.includes("eslint-disable-next-line") && !previousLine.includes("oxlint-disable-next-line")) return false;
789
+ if (previousLine.includes(ruleId)) return true;
790
+ const baseRuleName = ruleId.replace(ECOSYSTEM_RULE_PREFIX_PATTERN, "");
791
+ return baseRuleName !== ruleId && previousLine.includes(baseRuleName);
792
+ };
793
+ const isDisabledByInlineComment = (issue, sourceLines) => {
794
+ const line = issue.location?.line;
795
+ if (!line || !sourceLines) return false;
796
+ const ruleId = stripRuleNamespace(normalizeRuleId(issue));
797
+ if (isLineDisabledByReactDoctorComment(sourceLines[line - 1] ?? "", REACT_DOCTOR_DISABLE_LINE_DIRECTIVE, ruleId)) return true;
798
+ if (isDisabledByStackedDisableNextLine(sourceLines, line, ruleId)) return true;
799
+ return isDisabledByEcosystemDisableNextLine(sourceLines[line - 2] ?? "", ruleId);
800
+ };
801
+ const toLineStartIndex = (sourceLines, line) => {
802
+ let startIndex = 0;
803
+ for (let lineIndex = 0; lineIndex < line - 1; lineIndex++) startIndex += (sourceLines[lineIndex] ?? "").length + 1;
804
+ return startIndex;
805
+ };
806
+ const findComponentMatches = (sourceText, componentName) => {
807
+ const escapedComponentName = escapeRegExpMetacharacters(componentName);
808
+ const componentPattern = new RegExp(`<${escapedComponentName}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${escapedComponentName}>`, "g");
809
+ const matches = [];
810
+ for (const match of sourceText.matchAll(componentPattern)) {
811
+ if (match.index === void 0) continue;
812
+ matches.push({
813
+ innerText: match[1] ?? "",
814
+ startIndex: match.index,
815
+ endIndex: match.index + match[0].length
816
+ });
817
+ }
818
+ return matches;
819
+ };
820
+ const isStringOnlyWrapperContent = (innerText) => {
821
+ const trimmedInnerText = innerText.trim();
822
+ return trimmedInnerText.length > 0 && !/[<{]/.test(trimmedInnerText);
823
+ };
824
+ const isInsideComponentMatch = (issueIndex, match) => issueIndex >= match.startIndex && issueIndex <= match.endIndex;
825
+ const isSuppressedRnRawTextIssue = (issue, config, sourceLines) => {
826
+ if (stripRuleNamespace(normalizeRuleId(issue)) !== RN_NO_RAW_TEXT_RULE_ID) return false;
827
+ const line = issue.location?.line;
828
+ if (!line || !sourceLines) return false;
829
+ const sourceText = sourceLines.join("\n");
830
+ const issueIndex = toLineStartIndex(sourceLines, line);
831
+ for (const componentName of config.textComponents ?? []) if (findComponentMatches(sourceText, componentName).some((match) => isInsideComponentMatch(issueIndex, match))) return true;
832
+ for (const componentName of config.rawTextWrapperComponents ?? []) if (findComponentMatches(sourceText, componentName).some((match) => isInsideComponentMatch(issueIndex, match) && isStringOnlyWrapperContent(match.innerText))) return true;
833
+ return false;
834
+ };
835
+ const isSuppressedUnknownPropertyIssue = (issue, relativeFilePath, sourceLines) => {
836
+ if ((toMetadataRuleKey(issue) ?? normalizeRuleId(issue)) !== "react/no-unknown-property") return false;
837
+ if (!sourceLines) return false;
838
+ const line = issue.location?.line;
839
+ if (!line) return false;
840
+ const sourceLine = sourceLines[line - 1] ?? "";
841
+ const sourceHeader = sourceLines.slice(0, 30).join("\n");
842
+ if (SATORI_TW_PROP_PATTERN.test(sourceLine) && (OG_JSX_FILE_PATTERN.test(relativeFilePath) || IMAGE_RESPONSE_IMPORT_PATTERN.test(sourceHeader))) return true;
843
+ if (EMOTION_CSS_PROP_PATTERN.test(sourceLine) && EMOTION_IMPORT_PATTERN.test(sourceHeader)) return true;
844
+ return sourceLine.includes("<style") && sourceLine.includes("jsx");
845
+ };
846
+ const filterReactDoctorIssues = (issues, config, rootDirectory, readSourceLines, options) => {
847
+ const ignoredRules = new Set(config.ignore?.rules ?? []);
848
+ const ignoredFiles = config.ignore?.files ?? [];
849
+ const overrides = compileOverrides(config);
850
+ const isNonReactJsxProject = options?.jsxImportSource !== void 0 && NON_REACT_JSX_SOURCES.has(options.jsxImportSource);
851
+ const nonReactJsxFileCache = /* @__PURE__ */ new Map();
852
+ const isNonReactJsxFile = (relPath) => {
853
+ const cached = nonReactJsxFileCache.get(relPath);
854
+ if (cached !== void 0) return cached;
855
+ const lines = readSourceLines?.(relPath);
856
+ const isNonReact = Boolean(lines && NON_REACT_JSX_IMPORT_PATTERN.test(lines.slice(0, 30).join("\n")));
857
+ nonReactJsxFileCache.set(relPath, isNonReact);
858
+ return isNonReact;
859
+ };
860
+ const filtered = issues.filter((issue) => {
861
+ const relativeFilePath = toRelativeIssuePath(issue, rootDirectory);
862
+ if (isAutoSuppressedTestNoise(issue, relativeFilePath)) return false;
863
+ const ruleId = normalizeRuleId(issue);
864
+ const unwrappedRuleId = toMetadataRuleKey(issue) ?? ruleId;
865
+ const sourceLines = relativeFilePath ? readSourceLines?.(relativeFilePath) : void 0;
866
+ if (REACT_BUILTIN_RULE_PREFIX.test(unwrappedRuleId) && (isNonReactJsxProject || relativeFilePath && isNonReactJsxFile(relativeFilePath))) return false;
867
+ if (relativeFilePath && isSuppressedUnknownPropertyIssue(issue, relativeFilePath, sourceLines)) return false;
868
+ if (unwrappedRuleId.startsWith(JSX_A11Y_RULE_PREFIX) && relativeFilePath && OG_IMAGE_FILE_PATTERN.test(relativeFilePath)) return false;
869
+ if (matchesRule(issue, ignoredRules)) return false;
870
+ if (relativeFilePath && ignoredFiles.some((pattern) => matchesPathPattern(relativeFilePath, pattern))) return false;
871
+ if (isIgnoredByOverride(issue, relativeFilePath, overrides)) return false;
872
+ if (config.respectInlineDisables !== false && relativeFilePath && isDisabledByInlineComment(issue, sourceLines)) return false;
873
+ if (relativeFilePath && isSuppressedRnRawTextIssue(issue, config, sourceLines)) return false;
874
+ return true;
875
+ });
876
+ const seen = /* @__PURE__ */ new Set();
877
+ return filtered.filter((issue) => {
878
+ const loc = issue.location;
879
+ if (!loc?.filePath || loc.line === void 0) return true;
880
+ const unwrapped = toMetadataRuleKey(issue) ?? normalizeRuleId(issue);
881
+ const baseKey = `${loc.filePath}:${loc.line}`;
882
+ const dedupeKey = `${baseKey}:${unwrapped}`;
883
+ if (seen.has(dedupeKey)) return false;
884
+ seen.add(dedupeKey);
885
+ const canonicalEffect = toCanonicalEffectKey(unwrapped);
886
+ if (canonicalEffect) {
887
+ const effectCanonKey = `${baseKey}:effect-canonical:${canonicalEffect}`;
888
+ if (seen.has(effectCanonKey)) return false;
889
+ seen.add(effectCanonKey);
890
+ }
891
+ return true;
892
+ });
893
+ };
894
+ //#endregion
895
+ //#region src/core/rules/lint/utils/resolve-oxlint-diagnostic-category.ts
896
+ const PLUGIN_CATEGORY_MAP = {
897
+ effect: "State & Effects",
898
+ eslint: "Correctness",
899
+ import: "Bundle Size",
900
+ jest: "Correctness",
901
+ "jsx-a11y": "Accessibility",
902
+ knip: "Dead Code",
903
+ n: "Correctness",
904
+ nextjs: "Next.js",
905
+ node: "Correctness",
906
+ oxc: "Correctness",
907
+ promise: "Correctness",
908
+ react: "Correctness",
909
+ "react-doctor": "Other",
910
+ "react-hooks": "Correctness",
911
+ "react-hooks-js": "React Compiler",
912
+ typescript: "Correctness",
913
+ unicorn: "Correctness",
914
+ vitest: "Correctness"
915
+ };
916
+ const RULE_CATEGORY_MAP = {
917
+ "react-doctor/advanced-event-handler-refs": "Performance",
918
+ "react-doctor/advanced-init-once": "Performance",
919
+ "react-doctor/advanced-use-latest": "Performance",
920
+ "react-doctor/async-api-routes": "Performance",
921
+ "react-doctor/async-await-in-loop": "Performance",
922
+ "react-doctor/async-cheap-condition-before-await": "Performance",
923
+ "react-doctor/async-defer-await": "Performance",
924
+ "react-doctor/async-dependencies": "Performance",
925
+ "react-doctor/async-parallel": "Performance",
926
+ "react-doctor/async-suspense-boundaries": "Performance",
927
+ "react-doctor/bundle-conditional": "Bundle Size",
928
+ "react-doctor/bundle-preload": "Bundle Size",
929
+ "react-doctor/client-event-listeners": "Performance",
930
+ "react-doctor/client-localstorage-no-version": "Correctness",
931
+ "react-doctor/client-passive-event-listeners": "Performance",
932
+ "react-doctor/client-swr-dedup": "Performance",
933
+ "react-doctor/design-no-bold-heading": "Architecture",
934
+ "react-doctor/design-no-three-period-ellipsis": "Architecture",
935
+ "react-doctor/design-no-vague-button-label": "Accessibility",
936
+ "react-doctor/effect-needs-cleanup": "State & Effects",
937
+ "react-doctor/effect-no-adjust-state-on-prop-change": "State & Effects",
938
+ "react-doctor/effect-no-chain-state-updates": "State & Effects",
939
+ "react-doctor/effect-no-derived-state": "State & Effects",
940
+ "react-doctor/effect-no-event-handler": "State & Effects",
941
+ "react-doctor/effect-no-initialize-state": "State & Effects",
942
+ "react-doctor/effect-no-pass-data-to-parent": "State & Effects",
943
+ "react-doctor/effect-no-pass-live-state-to-parent": "State & Effects",
944
+ "react-doctor/effect-no-reset-all-state-on-prop-change": "State & Effects",
945
+ "react-doctor/expo-no-axios": "React Native",
946
+ "react-doctor/i18n-no-dynamic-translation-key": "Architecture",
947
+ "react-doctor/i18n-no-literal-jsx-text": "Architecture",
948
+ "react-doctor/js-batch-dom-css": "Performance",
949
+ "react-doctor/js-cache-function-results": "Performance",
950
+ "react-doctor/js-cache-property-access": "Performance",
951
+ "react-doctor/js-cache-storage": "Performance",
952
+ "react-doctor/js-combine-iterations": "Performance",
953
+ "react-doctor/js-early-exit": "Performance",
954
+ "react-doctor/js-flatmap-filter": "Performance",
955
+ "react-doctor/js-hoist-intl": "Performance",
956
+ "react-doctor/js-hoist-regexp": "Performance",
957
+ "react-doctor/js-index-maps": "Performance",
958
+ "react-doctor/js-length-check-first": "Performance",
959
+ "react-doctor/js-min-max-loop": "Performance",
960
+ "react-doctor/js-request-idle-callback": "Performance",
961
+ "react-doctor/js-set-map-lookups": "Performance",
962
+ "react-doctor/js-tosorted-immutable": "Performance",
963
+ "react-doctor/mobx-observer-named-component": "Correctness",
964
+ "react-doctor/motion-no-hover-transform-on-target": "Performance",
965
+ "react-doctor/motion-no-motion-in-lazymotion-strict": "Bundle Size",
966
+ "react-doctor/nextjs-async-client-component": "Next.js",
967
+ "react-doctor/nextjs-image-missing-sizes": "Next.js",
968
+ "react-doctor/nextjs-inline-script-missing-id": "Next.js",
969
+ "react-doctor/nextjs-missing-metadata": "Next.js",
970
+ "react-doctor/nextjs-no-a-element": "Next.js",
971
+ "react-doctor/nextjs-no-client-fetch-for-server-data": "Next.js",
972
+ "react-doctor/nextjs-no-client-side-redirect": "Next.js",
973
+ "react-doctor/nextjs-no-css-link": "Next.js",
974
+ "react-doctor/nextjs-no-font-link": "Next.js",
975
+ "react-doctor/nextjs-no-head-import": "Next.js",
976
+ "react-doctor/nextjs-no-img-element": "Next.js",
977
+ "react-doctor/nextjs-no-native-script": "Next.js",
978
+ "react-doctor/nextjs-no-polyfill-script": "Next.js",
979
+ "react-doctor/nextjs-no-redirect-in-try-catch": "Next.js",
980
+ "react-doctor/nextjs-no-side-effect-in-get-handler": "Security",
981
+ "react-doctor/nextjs-no-use-search-params-without-suspense": "Next.js",
982
+ "react-doctor/no-aria-expanded-without-controls": "Accessibility",
983
+ "react-doctor/no-aria-invalid-without-describedby": "Accessibility",
984
+ "react-doctor/no-array-index-as-key": "Correctness",
985
+ "react-doctor/no-barrel-import": "Bundle Size",
986
+ "react-doctor/no-blocked-paste": "Accessibility",
987
+ "react-doctor/no-button-navigation": "Accessibility",
988
+ "react-doctor/no-cascading-set-state": "State & Effects",
989
+ "react-doctor/no-dark-mode-glow": "Architecture",
990
+ "react-doctor/no-default-props": "Architecture",
991
+ "react-doctor/no-derived-state-effect": "State & Effects",
992
+ "react-doctor/no-derived-useState": "State & Effects",
993
+ "react-doctor/no-direct-state-mutation": "State & Effects",
994
+ "react-doctor/no-disabled-zoom": "Accessibility",
995
+ "react-doctor/no-document-start-view-transition": "Correctness",
996
+ "react-doctor/no-dynamic-import-path": "Bundle Size",
997
+ "react-doctor/no-effect-chain": "State & Effects",
998
+ "react-doctor/no-effect-event-handler": "State & Effects",
999
+ "react-doctor/no-effect-event-in-deps": "State & Effects",
1000
+ "react-doctor/no-eval": "Security",
1001
+ "react-doctor/no-event-trigger-state": "State & Effects",
1002
+ "react-doctor/no-fetch-in-effect": "State & Effects",
1003
+ "react-doctor/no-flush-sync": "Performance",
1004
+ "react-doctor/no-full-lodash-import": "Bundle Size",
1005
+ "react-doctor/no-generic-handler-names": "Architecture",
1006
+ "react-doctor/no-giant-component": "Architecture",
1007
+ "react-doctor/no-global-css-variable-animation": "Performance",
1008
+ "react-doctor/no-gradient-text": "Architecture",
1009
+ "react-doctor/no-gray-on-colored-background": "Accessibility",
1010
+ "react-doctor/no-icon-only-button-without-label": "Accessibility",
1011
+ "react-doctor/no-inline-bounce-easing": "Performance",
1012
+ "react-doctor/no-inline-exhaustive-style": "Architecture",
1013
+ "react-doctor/no-inline-prop-on-memo-component": "Performance",
1014
+ "react-doctor/no-justified-text": "Accessibility",
1015
+ "react-doctor/no-large-animated-blur": "Performance",
1016
+ "react-doctor/no-layout-property-animation": "Performance",
1017
+ "react-doctor/no-layout-transition-inline": "Performance",
1018
+ "react-doctor/no-legacy-class-lifecycles": "Correctness",
1019
+ "react-doctor/no-legacy-context-api": "Correctness",
1020
+ "react-doctor/no-long-transition-duration": "Performance",
1021
+ "react-doctor/no-many-boolean-props": "Architecture",
1022
+ "react-doctor/no-mirror-prop-effect": "State & Effects",
1023
+ "react-doctor/no-moment": "Bundle Size",
1024
+ "react-doctor/no-mutable-in-deps": "State & Effects",
1025
+ "react-doctor/no-nested-component-definition": "Correctness",
1026
+ "react-doctor/no-outline-none": "Accessibility",
1027
+ "react-doctor/no-permanent-will-change": "Performance",
1028
+ "react-doctor/no-polymorphic-children": "Architecture",
1029
+ "react-doctor/no-prevent-default": "Correctness",
1030
+ "react-doctor/no-prop-callback-in-effect": "State & Effects",
1031
+ "react-doctor/no-pure-black-background": "Architecture",
1032
+ "react-doctor/no-random-key": "Correctness",
1033
+ "react-doctor/no-react-dom-deprecated-apis": "Architecture",
1034
+ "react-doctor/no-react19-deprecated-apis": "Architecture",
1035
+ "react-doctor/no-render-in-render": "Architecture",
1036
+ "react-doctor/no-render-prop-children": "Architecture",
1037
+ "react-doctor/no-scale-from-zero": "Performance",
1038
+ "react-doctor/no-secrets-in-client-code": "Security",
1039
+ "react-doctor/no-set-state-in-render": "State & Effects",
1040
+ "react-doctor/no-settimeout-state-fix": "State & Effects",
1041
+ "react-doctor/no-side-tab-border": "Architecture",
1042
+ "react-doctor/no-swallowed-error": "Security",
1043
+ "react-doctor/no-tiny-text": "Accessibility",
1044
+ "react-doctor/no-transition-all": "Performance",
1045
+ "react-doctor/no-uncontrolled-input": "Correctness",
1046
+ "react-doctor/no-undeferred-third-party": "Bundle Size",
1047
+ "react-doctor/no-usememo-simple-expression": "Performance",
1048
+ "react-doctor/no-wide-letter-spacing": "Architecture",
1049
+ "react-doctor/no-z-index-9999": "Architecture",
1050
+ "react-doctor/prefer-dynamic-import": "Bundle Size",
1051
+ "react-doctor/prefer-use-effect-event": "State & Effects",
1052
+ "react-doctor/prefer-use-sync-external-store": "State & Effects",
1053
+ "react-doctor/prefer-useReducer": "State & Effects",
1054
+ "react-doctor/query-mutation-missing-invalidation": "TanStack Query",
1055
+ "react-doctor/query-no-query-in-effect": "TanStack Query",
1056
+ "react-doctor/query-no-rest-destructuring": "TanStack Query",
1057
+ "react-doctor/query-no-unstable-deps": "TanStack Query",
1058
+ "react-doctor/query-no-unstable-query-key": "TanStack Query",
1059
+ "react-doctor/query-no-usequery-for-mutation": "TanStack Query",
1060
+ "react-doctor/query-no-void-query-fn": "TanStack Query",
1061
+ "react-doctor/query-stable-query-client": "TanStack Query",
1062
+ "react-doctor/r3f-no-clone-in-frame": "Performance",
1063
+ "react-doctor/r3f-no-new-in-frame": "Performance",
1064
+ "react-doctor/r3f-no-set-state-in-frame": "Performance",
1065
+ "react-doctor/radix-aschild-single-child": "Correctness",
1066
+ "react-doctor/react-compiler-destructure-method": "Architecture",
1067
+ "react-doctor/rendering-activity": "Performance",
1068
+ "react-doctor/rendering-animate-svg-wrapper": "Performance",
1069
+ "react-doctor/rendering-conditional-render": "Correctness",
1070
+ "react-doctor/rendering-content-visibility": "Performance",
1071
+ "react-doctor/rendering-hoist-jsx": "Performance",
1072
+ "react-doctor/rendering-hydration-mismatch-time": "Correctness",
1073
+ "react-doctor/rendering-hydration-no-flicker": "Performance",
1074
+ "react-doctor/rendering-hydration-suppress-warning": "Correctness",
1075
+ "react-doctor/rendering-resource-hints": "Performance",
1076
+ "react-doctor/rendering-script-defer-async": "Performance",
1077
+ "react-doctor/rendering-svg-precision": "Performance",
1078
+ "react-doctor/rendering-usetransition-loading": "Performance",
1079
+ "react-doctor/rerender-defer-reads-hook": "Performance",
1080
+ "react-doctor/rerender-dependencies": "State & Effects",
1081
+ "react-doctor/rerender-derived-state-from-hook": "Performance",
1082
+ "react-doctor/rerender-functional-setstate": "Performance",
1083
+ "react-doctor/rerender-lazy-state-init": "Performance",
1084
+ "react-doctor/rerender-memo": "Performance",
1085
+ "react-doctor/rerender-memo-before-early-return": "Performance",
1086
+ "react-doctor/rerender-memo-with-default-value": "Performance",
1087
+ "react-doctor/rerender-split-combined-hooks": "Performance",
1088
+ "react-doctor/rerender-state-only-in-handlers": "Performance",
1089
+ "react-doctor/rerender-transitions-scroll": "Performance",
1090
+ "react-doctor/rerender-use-deferred-value": "Performance",
1091
+ "react-doctor/rerender-use-ref-transient-values": "Performance",
1092
+ "react-doctor/rhf-no-nested-object-setvalue": "Correctness",
1093
+ "react-doctor/rhf-no-watch-render": "Performance",
1094
+ "react-doctor/rn-animate-layout-property": "React Native",
1095
+ "react-doctor/rn-animation-reaction-as-derived": "React Native",
1096
+ "react-doctor/rn-bottom-sheet-prefer-native": "React Native",
1097
+ "react-doctor/rn-list-callback-per-row": "React Native",
1098
+ "react-doctor/rn-list-data-mapped": "React Native",
1099
+ "react-doctor/rn-list-recyclable-without-types": "React Native",
1100
+ "react-doctor/rn-no-deprecated-modules": "React Native",
1101
+ "react-doctor/rn-no-dimensions-get": "React Native",
1102
+ "react-doctor/rn-no-inline-flatlist-renderitem": "React Native",
1103
+ "react-doctor/rn-no-inline-object-in-list-item": "React Native",
1104
+ "react-doctor/rn-no-legacy-expo-packages": "React Native",
1105
+ "react-doctor/rn-no-legacy-shadow-styles": "React Native",
1106
+ "react-doctor/rn-no-non-native-navigator": "React Native",
1107
+ "react-doctor/rn-no-raw-text": "React Native",
1108
+ "react-doctor/rn-no-scroll-state": "React Native",
1109
+ "react-doctor/rn-no-scrollview-mapped-list": "React Native",
1110
+ "react-doctor/rn-no-single-element-style-array": "React Native",
1111
+ "react-doctor/rn-no-web-dom-elements": "React Native",
1112
+ "react-doctor/rn-prefer-content-inset-adjustment": "React Native",
1113
+ "react-doctor/rn-prefer-expo-image": "React Native",
1114
+ "react-doctor/rn-prefer-pressable": "React Native",
1115
+ "react-doctor/rn-prefer-reanimated": "React Native",
1116
+ "react-doctor/rn-pressable-shared-value-mutation": "React Native",
1117
+ "react-doctor/rn-scrollview-content-container-padding": "React Native",
1118
+ "react-doctor/rn-scrollview-dynamic-padding": "React Native",
1119
+ "react-doctor/rn-style-prefer-boxshadow": "React Native",
1120
+ "react-doctor/server-after-nonblocking": "Server",
1121
+ "react-doctor/server-auth-actions": "Server",
1122
+ "react-doctor/server-cache-lru": "Server",
1123
+ "react-doctor/server-cache-react": "Server",
1124
+ "react-doctor/server-cache-with-object-literal": "Server",
1125
+ "react-doctor/server-dedup-props": "Server",
1126
+ "react-doctor/server-fetch-without-revalidate": "Server",
1127
+ "react-doctor/server-hoist-static-io": "Server",
1128
+ "react-doctor/server-no-mutable-module-state": "Server",
1129
+ "react-doctor/server-parallel-fetching": "Server",
1130
+ "react-doctor/server-parallel-nested-fetching": "Server",
1131
+ "react-doctor/server-sequential-independent-await": "Server",
1132
+ "react-doctor/server-serialization": "Server",
1133
+ "react-doctor/shadcn-no-direct-radix-import": "Architecture",
1134
+ "react-doctor/storybook-await-play-interactions": "Correctness",
1135
+ "react-doctor/swr-no-empty-key": "Correctness",
1136
+ "react-doctor/swr-no-unstable-key": "Correctness",
1137
+ "react-doctor/tailwind-no-conflicting-classes": "Correctness",
1138
+ "react-doctor/tailwind-no-default-palette": "Architecture",
1139
+ "react-doctor/tailwind-no-redundant-padding-axes": "Architecture",
1140
+ "react-doctor/tailwind-no-redundant-size-axes": "Architecture",
1141
+ "react-doctor/tailwind-no-space-on-flex-children": "Architecture",
1142
+ "react-doctor/tailwind-oklch-alpha-syntax": "Correctness",
1143
+ "react-doctor/tanstack-ai-chat-lifecycle-middleware": "TanStack AI",
1144
+ "react-doctor/tanstack-ai-no-direct-client-import": "TanStack AI",
1145
+ "react-doctor/tanstack-ai-no-manual-sse-response": "TanStack AI",
1146
+ "react-doctor/tanstack-ai-no-vercel-sdk-patterns": "TanStack AI",
1147
+ "react-doctor/tanstack-ai-output-schema": "TanStack AI",
1148
+ "react-doctor/tanstack-start-get-mutation": "Security",
1149
+ "react-doctor/tanstack-start-loader-parallel-fetch": "Performance",
1150
+ "react-doctor/tanstack-start-missing-head-content": "TanStack Start",
1151
+ "react-doctor/tanstack-start-no-anchor-element": "TanStack Start",
1152
+ "react-doctor/tanstack-start-no-direct-fetch-in-loader": "TanStack Start",
1153
+ "react-doctor/tanstack-start-no-dynamic-server-fn-import": "TanStack Start",
1154
+ "react-doctor/tanstack-start-no-navigate-in-render": "TanStack Start",
1155
+ "react-doctor/tanstack-start-no-secrets-in-loader": "Security",
1156
+ "react-doctor/tanstack-start-no-use-server-in-handler": "TanStack Start",
1157
+ "react-doctor/tanstack-start-no-useeffect-fetch": "TanStack Start",
1158
+ "react-doctor/tanstack-start-redirect-in-try-catch": "TanStack Start",
1159
+ "react-doctor/tanstack-start-route-property-order": "TanStack Start",
1160
+ "react-doctor/tanstack-start-server-fn-method-order": "TanStack Start",
1161
+ "react-doctor/tanstack-start-server-fn-validate-input": "TanStack Start",
1162
+ "react-doctor/testing-await-user-event": "Correctness",
1163
+ "react-doctor/testing-no-container-query": "Correctness",
1164
+ "react-doctor/use-lazy-motion": "Bundle Size"
1165
+ };
1166
+ const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
1167
+ const resolveOxlintDiagnosticCategory = (pluginName, ruleId) => {
1168
+ return lookupOwnString(RULE_CATEGORY_MAP, `${pluginName}/${ruleId}`) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, pluginName) ?? "Other";
1169
+ };
1170
+ //#endregion
1171
+ //#region src/core/runners/collect-ignore-patterns.ts
1172
+ const IGNORE_FILENAMES = [".eslintignore", ".oxlintignore"];
1173
+ const PRETTIERIGNORE_FILENAME = ".prettierignore";
1174
+ const BUILTIN_IGNORE_PATTERNS = [
1175
+ "*.min.js",
1176
+ "*.min.mjs",
1177
+ "*.bundle.js",
1178
+ "*.global.js",
1179
+ "*.umd.js",
1180
+ "*.production.js",
1181
+ "*.development.js",
1182
+ "vendor/**",
1183
+ "vendors/**"
1184
+ ];
1185
+ const SOURCE_EXTENSION_GLOB_PATTERN = /^\*\.(?:[cm]?[jt]sx?|json|jsonc)$/;
1186
+ const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
1187
+ const FALSY_LINGUIST_VALUES = new Set([
1188
+ "false",
1189
+ "0",
1190
+ "off",
1191
+ "no"
1192
+ ]);
1193
+ const stripGitignoreEscape = (pattern) => {
1194
+ if (pattern.startsWith("\\#") || pattern.startsWith("\\!")) return pattern.slice(1);
1195
+ return pattern;
1196
+ };
1197
+ const readIgnoreFile = (filePath) => {
1198
+ let content;
1199
+ try {
1200
+ content = fs$1.readFileSync(filePath, "utf-8");
1201
+ } catch {
1202
+ return [];
1203
+ }
1204
+ const patterns = [];
1205
+ for (const line of content.split("\n")) {
1206
+ const trimmed = line.trim();
1207
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
1208
+ patterns.push(stripGitignoreEscape(trimmed));
1209
+ }
1210
+ return patterns;
1211
+ };
1212
+ const isTruthyLinguistAttribute = (token) => {
1213
+ const match = LINGUIST_ATTRIBUTE_PATTERN.exec(token);
1214
+ if (!match) return false;
1215
+ if (match[1] === void 0) return true;
1216
+ return !FALSY_LINGUIST_VALUES.has(match[1].toLowerCase());
1217
+ };
1218
+ const parseGitattributesLinguistPaths = (filePath) => {
1219
+ let content;
1220
+ try {
1221
+ content = fs$1.readFileSync(filePath, "utf-8");
1222
+ } catch {
1223
+ return [];
1224
+ }
1225
+ const paths = [];
1226
+ for (const rawLine of content.split("\n")) {
1227
+ const line = rawLine.trim();
1228
+ if (line.length === 0 || line.startsWith("#")) continue;
1229
+ const tokens = line.split(/\s+/);
1230
+ if (tokens.length < 2) continue;
1231
+ const [pathSpec, ...attributes] = tokens;
1232
+ if (attributes.some(isTruthyLinguistAttribute)) paths.push(pathSpec);
1233
+ }
1234
+ return paths;
1235
+ };
1236
+ const translatePattern = (pattern, relPath) => {
1237
+ if (relPath === "") return pattern;
1238
+ const isNegation = pattern.startsWith("!");
1239
+ let body = isNegation ? pattern.slice(1) : pattern;
1240
+ const isAnchored = body.startsWith("/");
1241
+ if (isAnchored) body = body.slice(1);
1242
+ if (!isAnchored && !body.includes("/")) return pattern;
1243
+ const prefix = `${relPath}/`;
1244
+ if (!body.startsWith(prefix)) return null;
1245
+ const remaining = body.slice(prefix.length);
1246
+ return `${isNegation ? "!" : ""}${remaining}`;
1247
+ };
1248
+ const collectIgnorePatterns = (rootDirectory) => {
1249
+ const seen = /* @__PURE__ */ new Set();
1250
+ const patterns = [];
1251
+ const add = (pattern) => {
1252
+ if (seen.has(pattern)) return;
1253
+ seen.add(pattern);
1254
+ patterns.push(pattern);
1255
+ };
1256
+ for (const builtinPattern of BUILTIN_IGNORE_PATTERNS) add(builtinPattern);
1257
+ let currentDirectory = rootDirectory;
1258
+ while (true) {
1259
+ const relPath = path.relative(currentDirectory, rootDirectory);
1260
+ for (const fileName of IGNORE_FILENAMES) for (const pattern of readIgnoreFile(path.join(currentDirectory, fileName))) {
1261
+ const translated = translatePattern(pattern, relPath);
1262
+ if (translated !== null) add(translated);
1263
+ }
1264
+ for (const pattern of readIgnoreFile(path.join(currentDirectory, PRETTIERIGNORE_FILENAME))) {
1265
+ if (SOURCE_EXTENSION_GLOB_PATTERN.test(pattern)) continue;
1266
+ const translated = translatePattern(pattern, relPath);
1267
+ if (translated !== null) add(translated);
1268
+ }
1269
+ for (const linguistPath of parseGitattributesLinguistPaths(path.join(currentDirectory, ".gitattributes"))) {
1270
+ const translated = translatePattern(linguistPath, relPath);
1271
+ if (translated !== null) add(translated);
1272
+ }
1273
+ if (fs$1.existsSync(path.join(currentDirectory, ".git"))) break;
1274
+ const parentDirectory = path.dirname(currentDirectory);
1275
+ if (parentDirectory === currentDirectory) break;
1276
+ currentDirectory = parentDirectory;
1277
+ }
1278
+ return patterns;
1279
+ };
1280
+ //#endregion
1281
+ //#region src/core/runners/oxlint.ts
1282
+ const esmRequire = createRequire(import.meta.url);
1283
+ const OXLINT_STDERR_PREVIEW_LENGTH = 2e3;
1284
+ const REACT_RULES_OF_HOOKS_CODE = "react/rules-of-hooks";
1285
+ const REACT_USE_HOOK_MESSAGE_FRAGMENT = "React Hook \"use\"";
1286
+ const USE_IDENTIFIER_NAME = "use";
1287
+ const USER_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
1288
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
1289
+ const resolveTsconfigRelativePath = (rootDirectory) => {
1290
+ for (const fileName of TSCONFIG_FILENAMES) if (existsSync(path.join(rootDirectory, fileName))) return `./${fileName}`;
1291
+ return null;
1292
+ };
1293
+ const metadataByRuleKey = new Map(reactDoctorOxlintRuleMetadata.map((metadata) => [metadata.oxlintRuleKey, metadata]));
1294
+ const RULE_TITLE_WORD_UPPERCASE = /\b(css|html|url|svg|jsx|api|ua|rn)\b/gi;
1295
+ const toRuleTitle = (ruleName) => {
1296
+ const readable = ruleName.replace(/^(no|prefer|require|use)-/, "").replace(/^(nextjs|tanstack-start|tanstack-query|rn|js|server|client|query|effect|design|rendering|rerender|react-compiler|advanced)-/, "").replaceAll("-", " ");
1297
+ return (readable.charAt(0).toUpperCase() + readable.slice(1)).replace(RULE_TITLE_WORD_UPPERCASE, (match) => match.toUpperCase());
1298
+ };
1299
+ const cleanDiagnosticMessage = (raw) => {
1300
+ if (!raw) return "";
1301
+ const trimmed = raw.trim();
1302
+ return (trimmed.split(/\n\s*\n/, 1)[0] ?? trimmed).split("\n")[0]?.trim() ?? "";
1303
+ };
1304
+ const cleanDiagnosticHelp = (raw) => {
1305
+ if (!raw) return void 0;
1306
+ const trimmed = raw.trim();
1307
+ if (trimmed.length === 0) return void 0;
1308
+ return (trimmed.split(/\n\s*\n/, 1)[0] ?? trimmed).replace(/\s+/g, " ").trim();
1309
+ };
1310
+ const resolveOxlintBinary = () => {
1311
+ try {
1312
+ const packageJsonPath = esmRequire.resolve("oxlint/package.json");
1313
+ return path.join(path.dirname(packageJsonPath), "bin/oxlint");
1314
+ } catch (error) {
1315
+ throw new ReactDoctorRunnerUnavailableError(OXLINT_CHECK_ID, "Oxlint is not installed. Add oxlint to the project or install react-doctor dependencies.", { cause: error });
1316
+ }
1317
+ };
1318
+ const resolvePluginPath = () => {
1319
+ const candidatePaths = [fileURLToPath(new URL("./oxlint-plugin.js", import.meta.url)), fileURLToPath(new URL("../../oxlint-plugin.js", import.meta.url))];
1320
+ return candidatePaths.find((candidatePath) => existsSync(candidatePath)) ?? candidatePaths[0];
1321
+ };
1322
+ const detectUserLintConfigPaths = (rootDirectory) => {
1323
+ let currentDirectory = rootDirectory;
1324
+ while (true) {
1325
+ for (const fileName of USER_LINT_CONFIG_FILENAMES) {
1326
+ const configPath = path.join(currentDirectory, fileName);
1327
+ if (existsSync(configPath)) return [configPath];
1328
+ }
1329
+ if (existsSync(path.join(currentDirectory, ".git"))) return [];
1330
+ const parentDirectory = path.dirname(currentDirectory);
1331
+ if (parentDirectory === currentDirectory) return [];
1332
+ currentDirectory = parentDirectory;
1333
+ }
1334
+ };
1335
+ const splitRuleCode = (code) => {
1336
+ const parenMatch = code.match(/^([^(]+)\(([^)]+)\)$/);
1337
+ if (parenMatch) return {
1338
+ pluginName: parenMatch[1],
1339
+ ruleId: parenMatch[2]
1340
+ };
1341
+ const separatorIndex = code.indexOf("/");
1342
+ if (separatorIndex < 0) return {
1343
+ pluginName: "oxlint",
1344
+ ruleId: code
1345
+ };
1346
+ return {
1347
+ pluginName: code.slice(0, separatorIndex),
1348
+ ruleId: code.slice(separatorIndex + 1)
1349
+ };
1350
+ };
1351
+ const toRelativeFilename = (rootDirectory, filename) => {
1352
+ if (!filename) return "";
1353
+ if (!path.isAbsolute(filename)) return filename;
1354
+ return path.relative(rootDirectory, filename);
1355
+ };
1356
+ const toReactDoctorIssue = (diagnostic, rootDirectory) => {
1357
+ const code = diagnostic.code ?? "oxlint/unknown";
1358
+ const ruleSource = splitRuleCode(code);
1359
+ const normalizedCode = `${ruleSource.pluginName}/${ruleSource.ruleId}`;
1360
+ const metadata = metadataByRuleKey.get(code) ?? metadataByRuleKey.get(normalizedCode);
1361
+ const firstSpan = diagnostic.labels?.[0]?.span;
1362
+ const filePath = toRelativeFilename(rootDirectory, diagnostic.filename);
1363
+ const severity = diagnostic.severity === "error" ? "error" : "warning";
1364
+ const fallbackTitle = toRuleTitle(ruleSource.ruleId);
1365
+ const category = resolveOxlintDiagnosticCategory(ruleSource.pluginName, ruleSource.ruleId);
1366
+ return {
1367
+ id: `${OXLINT_CHECK_ID}/${code}/${filePath}/${firstSpan?.line ?? 0}/${firstSpan?.column ?? 0}`,
1368
+ title: metadata?.name ?? fallbackTitle,
1369
+ message: cleanDiagnosticMessage(diagnostic.message ?? code),
1370
+ severity,
1371
+ category,
1372
+ recommendation: metadata?.recommendation ?? cleanDiagnosticHelp(diagnostic.help),
1373
+ location: filePath ? {
1374
+ filePath,
1375
+ line: firstSpan?.line,
1376
+ column: firstSpan?.column,
1377
+ endLine: firstSpan?.endLine,
1378
+ endColumn: firstSpan?.endColumn
1379
+ } : void 0,
1380
+ source: {
1381
+ checkId: OXLINT_CHECK_ID,
1382
+ pluginName: ruleSource.pluginName,
1383
+ ruleId: ruleSource.ruleId
1384
+ }
1385
+ };
1386
+ };
1387
+ const isFunctionNode = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression");
1388
+ const hasUseParameterInScope = (node) => {
1389
+ let currentNode = node.parent;
1390
+ while (currentNode) {
1391
+ if (isFunctionNode(currentNode)) {
1392
+ const parameterNames = /* @__PURE__ */ new Set();
1393
+ for (const parameter of currentNode.params ?? []) collectPatternNames(parameter, parameterNames);
1394
+ if (parameterNames.has(USE_IDENTIFIER_NAME)) return true;
1395
+ }
1396
+ currentNode = currentNode.parent;
1397
+ }
1398
+ return false;
1399
+ };
1400
+ const getNodeRange = (node) => {
1401
+ if (!Array.isArray(node.range)) return null;
1402
+ const [start, end] = node.range;
1403
+ if (typeof start !== "number" || typeof end !== "number") return null;
1404
+ return {
1405
+ start,
1406
+ end
1407
+ };
1408
+ };
1409
+ const toLineRange = (sourceText, line) => {
1410
+ if (!line || line < 1) return null;
1411
+ let currentLine = 1;
1412
+ let lineStart = 0;
1413
+ for (let index = 0; index < sourceText.length; index += 1) {
1414
+ if (currentLine === line) {
1415
+ const newlineIndex = sourceText.indexOf("\n", index);
1416
+ return {
1417
+ start: lineStart,
1418
+ end: newlineIndex === -1 ? sourceText.length : newlineIndex
1419
+ };
1420
+ }
1421
+ if (sourceText[index] === "\n") {
1422
+ currentLine += 1;
1423
+ lineStart = index + 1;
1424
+ }
1425
+ }
1426
+ return currentLine === line ? {
1427
+ start: lineStart,
1428
+ end: sourceText.length
1429
+ } : null;
1430
+ };
1431
+ const rangesOverlap = (firstRange, secondRange) => firstRange.start <= secondRange.end && secondRange.start <= firstRange.end;
1432
+ const isLocalUseRulesOfHooksFalsePositive = (sourceText, filePath, span) => {
1433
+ const lineRange = toLineRange(sourceText, span?.line);
1434
+ if (!lineRange) return false;
1435
+ try {
1436
+ const parseResult = parseSync(filePath, sourceText, {
1437
+ sourceType: "unambiguous",
1438
+ range: true
1439
+ });
1440
+ let didFindLocalUseCall = false;
1441
+ walkAst(parseResult.program, (node) => {
1442
+ if (didFindLocalUseCall) return false;
1443
+ if (!isNodeOfType(node, "CallExpression")) return;
1444
+ if (!isNodeOfType(node.callee, "Identifier")) return;
1445
+ if (node.callee.name !== USE_IDENTIFIER_NAME) return;
1446
+ const callRange = getNodeRange(node);
1447
+ if (!callRange || !rangesOverlap(callRange, lineRange)) return;
1448
+ didFindLocalUseCall = hasUseParameterInScope(node);
1449
+ if (didFindLocalUseCall) return false;
1450
+ });
1451
+ return didFindLocalUseCall;
1452
+ } catch {
1453
+ return false;
1454
+ }
1455
+ };
1456
+ const shouldSuppressLocalUseRulesOfHooksDiagnostic = async (diagnostic, rootDirectory) => {
1457
+ if (diagnostic.code !== REACT_RULES_OF_HOOKS_CODE) return false;
1458
+ if (!diagnostic.message?.includes(REACT_USE_HOOK_MESSAGE_FRAGMENT)) return false;
1459
+ const span = diagnostic.labels?.[0]?.span;
1460
+ const filename = diagnostic.filename;
1461
+ if (!filename || !span?.line) return false;
1462
+ const filePath = path.isAbsolute(filename) ? filename : path.join(rootDirectory, filename);
1463
+ try {
1464
+ return isLocalUseRulesOfHooksFalsePositive(await fs.readFile(filePath, "utf8"), filePath, span);
1465
+ } catch {
1466
+ return false;
1467
+ }
1468
+ };
1469
+ const filterOxlintDiagnostics = async (diagnostics, rootDirectory) => {
1470
+ const filteredDiagnostics = [];
1471
+ for (const diagnostic of diagnostics) {
1472
+ if (await shouldSuppressLocalUseRulesOfHooksDiagnostic(diagnostic, rootDirectory)) continue;
1473
+ filteredDiagnostics.push(diagnostic);
1474
+ }
1475
+ return filteredDiagnostics;
1476
+ };
1477
+ const formatOxlintOutputPreview = (stdout, stderr = "") => {
1478
+ return [stdout, stderr].filter((value) => value.trim().length > 0).join("\n").trim().slice(0, OXLINT_STDERR_PREVIEW_LENGTH);
1479
+ };
1480
+ const parseOxlintOutput = (stdout, stderr = "") => {
1481
+ if (!stdout.trim()) return [];
1482
+ let output;
1483
+ try {
1484
+ output = JSON.parse(stdout);
1485
+ } catch (error) {
1486
+ const preview = formatOxlintOutputPreview(stdout, stderr);
1487
+ throw new ReactDoctorCheckFailedError(OXLINT_CHECK_ID, preview ? `Oxlint failed before returning JSON: ${preview}` : "Oxlint returned invalid JSON.", { cause: error });
1488
+ }
1489
+ return output.diagnostics ?? [];
1490
+ };
1491
+ const spawnOxlint = (args, rootDirectory, signal) => new Promise((resolve, reject) => {
1492
+ const child = spawn(process.execPath, args, {
1493
+ cwd: rootDirectory,
1494
+ signal,
1495
+ stdio: [
1496
+ "ignore",
1497
+ "pipe",
1498
+ "pipe"
1499
+ ]
1500
+ });
1501
+ let stdout = "";
1502
+ let stderr = "";
1503
+ child.stdout.setEncoding("utf8");
1504
+ child.stderr.setEncoding("utf8");
1505
+ child.stdout.on("data", (chunk) => {
1506
+ stdout += chunk;
1507
+ });
1508
+ child.stderr.on("data", (chunk) => {
1509
+ stderr += chunk;
1510
+ });
1511
+ child.on("error", reject);
1512
+ child.on("close", (exitCode) => {
1513
+ if (exitCode === 0 || exitCode === 1) {
1514
+ resolve({
1515
+ stdout,
1516
+ stderr
1517
+ });
1518
+ return;
1519
+ }
1520
+ reject(new ReactDoctorCheckFailedError(OXLINT_CHECK_ID, `Oxlint failed with exit code ${exitCode ?? "unknown"}: ${stderr.slice(0, OXLINT_STDERR_PREVIEW_LENGTH)}`));
1521
+ });
1522
+ });
1523
+ const runOxlint = async (options) => {
1524
+ options.signal?.throwIfAborted();
1525
+ const configDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "react-doctor-oxlint-"));
1526
+ const configPath = path.join(configDirectory, ".oxlintrc.json");
1527
+ const oxlintBinary = resolveOxlintBinary();
1528
+ const config = createReactDoctorOxlintConfig({
1529
+ pluginPath: resolvePluginPath(),
1530
+ projectRootDirectory: options.rootDirectory,
1531
+ project: options.project,
1532
+ customRulesOnly: options.customRulesOnly,
1533
+ includeEcosystemRules: options.includeEcosystemRules,
1534
+ extendsPaths: options.adoptExistingLintConfig === true && !options.customRulesOnly ? detectUserLintConfigPaths(options.rootDirectory) : [],
1535
+ ignoredTags: options.ignoredTags
1536
+ });
1537
+ await fs.writeFile(configPath, JSON.stringify(config), { mode: 384 });
1538
+ try {
1539
+ const args = [
1540
+ oxlintBinary,
1541
+ "-c",
1542
+ configPath,
1543
+ "--format",
1544
+ "json",
1545
+ ...(options.excludePatterns ?? []).flatMap((pattern) => ["--ignore-pattern", pattern])
1546
+ ];
1547
+ if (options.project.hasTypeScript) {
1548
+ const tsconfigRelativePath = resolveTsconfigRelativePath(options.rootDirectory);
1549
+ if (tsconfigRelativePath) args.push("--tsconfig", tsconfigRelativePath);
1550
+ }
1551
+ const combinedPatterns = collectIgnorePatterns(options.rootDirectory);
1552
+ if (combinedPatterns.length > 0) {
1553
+ const combinedIgnorePath = path.join(configDirectory, "combined.ignore");
1554
+ await fs.writeFile(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
1555
+ args.push("--ignore-path", combinedIgnorePath);
1556
+ }
1557
+ args.push(...options.includePaths?.length ? options.includePaths : ["."]);
1558
+ const { stdout, stderr } = await spawnOxlint(args, options.rootDirectory, options.signal);
1559
+ return (await filterOxlintDiagnostics(parseOxlintOutput(stdout, stderr), options.rootDirectory)).map((diagnostic) => toReactDoctorIssue(diagnostic, options.rootDirectory));
1560
+ } finally {
1561
+ await fs.rm(configDirectory, {
1562
+ recursive: true,
1563
+ force: true
1564
+ });
1565
+ }
1566
+ };
1567
+ //#endregion
1568
+ //#region src/core/proxy-fetch.ts
1569
+ const isGlobalProcessLike = (value) => typeof value === "object" && value !== null && "versions" in value;
1570
+ const getGlobalProcess = () => {
1571
+ const candidate = Reflect.get(globalThis, "process");
1572
+ if (!isGlobalProcessLike(candidate)) return void 0;
1573
+ return candidate.versions?.node ? candidate : void 0;
1574
+ };
1575
+ const getProxyUrl = () => {
1576
+ const proc = getGlobalProcess();
1577
+ if (!proc?.env) return void 0;
1578
+ return proc.env.HTTPS_PROXY ?? proc.env.https_proxy ?? proc.env.HTTP_PROXY ?? proc.env.http_proxy;
1579
+ };
1580
+ const dispatcherCache = /* @__PURE__ */ new Map();
1581
+ const loadProxyDispatcher = async (proxyUrl) => {
1582
+ try {
1583
+ const { ProxyAgent } = await import("undici");
1584
+ return new ProxyAgent(proxyUrl);
1585
+ } catch {
1586
+ return null;
1587
+ }
1588
+ };
1589
+ const getProxyDispatcher = (proxyUrl) => {
1590
+ const cached = dispatcherCache.get(proxyUrl);
1591
+ if (cached) return cached;
1592
+ const pending = loadProxyDispatcher(proxyUrl);
1593
+ dispatcherCache.set(proxyUrl, pending);
1594
+ return pending;
1595
+ };
1596
+ const proxyFetch = async (url, init) => {
1597
+ const proxyUrl = getProxyUrl();
1598
+ const dispatcher = proxyUrl ? await getProxyDispatcher(proxyUrl) : null;
1599
+ const fetchInit = {
1600
+ ...init,
1601
+ ...dispatcher ? { dispatcher } : {}
1602
+ };
1603
+ return fetch(url, fetchInit);
1604
+ };
1605
+ //#endregion
1606
+ //#region src/core/try-score-from-api.ts
1607
+ const parseScoreResult = (value) => {
1608
+ if (typeof value !== "object" || value === null) return null;
1609
+ if (!("score" in value) || !("label" in value)) return null;
1610
+ const scoreValue = Reflect.get(value, "score");
1611
+ const labelValue = Reflect.get(value, "label");
1612
+ if (typeof scoreValue !== "number" || typeof labelValue !== "string") return null;
1613
+ return {
1614
+ value: scoreValue,
1615
+ label: labelValue
1616
+ };
1617
+ };
1618
+ const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
1619
+ const describeFailure = (error) => {
1620
+ if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / MILLISECONDS_PER_SECOND}s`;
1621
+ if (error instanceof Error && error.message) return error.message;
1622
+ return String(error);
1623
+ };
1624
+ const tryScoreFromApi = async (issues, fetchImplementation, options = {}) => {
1625
+ if (typeof fetchImplementation !== "function") return null;
1626
+ const warn = options.silent ? () => {} : (message) => console.warn(message);
1627
+ const controller = new AbortController();
1628
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1629
+ try {
1630
+ const response = await fetchImplementation(SCORE_API_URL, {
1631
+ method: "POST",
1632
+ headers: { "Content-Type": "application/json" },
1633
+ body: JSON.stringify({ diagnostics: collectScoreDiagnostics(issues) }),
1634
+ signal: controller.signal
1635
+ });
1636
+ if (!response.ok) {
1637
+ warn(`[react-doctor] Score API returned ${response.status} ${response.statusText} — using local scoring`);
1638
+ return null;
1639
+ }
1640
+ return parseScoreResult(await response.json());
1641
+ } catch (error) {
1642
+ warn(`[react-doctor] Score API unreachable (${describeFailure(error)}) — using local scoring`);
1643
+ return null;
1644
+ } finally {
1645
+ clearTimeout(timeoutId);
1646
+ }
1647
+ };
1648
+ //#endregion
1649
+ //#region src/core/inspect-react-project.ts
1650
+ const mergeConfig = (loadedConfig, options) => ({
1651
+ ...loadedConfig?.config,
1652
+ ...options.config,
1653
+ lint: options.lint ?? options.config?.lint ?? loadedConfig?.config.lint,
1654
+ deadCode: options.deadCode ?? options.config?.deadCode ?? loadedConfig?.config.deadCode,
1655
+ customRulesOnly: options.customRulesOnly ?? options.config?.customRulesOnly ?? loadedConfig?.config.customRulesOnly,
1656
+ respectInlineDisables: options.respectInlineDisables ?? options.config?.respectInlineDisables ?? loadedConfig?.config.respectInlineDisables,
1657
+ offline: options.offline ?? options.config?.offline ?? loadedConfig?.config.offline
1658
+ });
1659
+ const INLINE_CONFIG_SOURCE_PATH = "<inline>";
1660
+ const toInlineLoadedConfig = (config, sourceDirectory) => ({
1661
+ config,
1662
+ sourceDirectory,
1663
+ sourcePath: INLINE_CONFIG_SOURCE_PATH
1664
+ });
1665
+ const withCallerRootDir = (loadedConfig, callerRootDir, callerSourceDirectory) => ({
1666
+ ...loadedConfig,
1667
+ sourceDirectory: callerSourceDirectory,
1668
+ config: {
1669
+ ...loadedConfig.config,
1670
+ rootDir: callerRootDir
1671
+ }
1672
+ });
1673
+ const getLoadedConfig = async (requestedRootDirectory, options) => {
1674
+ if (options.loadedConfig !== void 0) return options.loadedConfig;
1675
+ if (!options.config) return loadReactDoctorConfig(requestedRootDirectory);
1676
+ if (!options.config.rootDir) return null;
1677
+ const diskConfig = await loadReactDoctorConfig(requestedRootDirectory);
1678
+ if (!diskConfig) return toInlineLoadedConfig(options.config, requestedRootDirectory);
1679
+ return withCallerRootDir(diskConfig, options.config.rootDir, requestedRootDirectory);
1680
+ };
1681
+ const mergeRuleSelection = (selection, config) => {
1682
+ const enabledRuleIds = [...selection?.enabledRuleIds ?? []];
1683
+ if (config.deadCode) enabledRuleIds.push(DEAD_CODE_RULE_ID, DEPENDENCIES_RULE_ID, REACT_ARCHITECTURE_RULE_ID);
1684
+ return {
1685
+ enabledRuleIds,
1686
+ disabledRuleIds: selection?.disabledRuleIds
1687
+ };
1688
+ };
1689
+ const readSourceLines = (rootDirectory, filePath) => {
1690
+ try {
1691
+ return fs$1.readFileSync(path.resolve(rootDirectory, filePath), "utf8").split(/\r?\n/);
1692
+ } catch {
1693
+ return;
1694
+ }
1695
+ };
1696
+ const readJsxImportSource = (rootDirectory) => {
1697
+ try {
1698
+ const tsconfigPath = path.resolve(rootDirectory, "tsconfig.json");
1699
+ const cleaned = fs$1.readFileSync(tsconfigPath, "utf8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
1700
+ return JSON.parse(cleaned)?.compilerOptions?.jsxImportSource;
1701
+ } catch {
1702
+ return;
1703
+ }
1704
+ };
1705
+ const createOxlintCheck = async (rootDirectory, config, options, project) => {
1706
+ if (config.lint !== true) return null;
1707
+ const startedMilliseconds = globalThis.performance.now();
1708
+ try {
1709
+ return {
1710
+ id: OXLINT_CHECK_ID,
1711
+ name: "Oxlint",
1712
+ status: "completed",
1713
+ issues: await runOxlint({
1714
+ rootDirectory,
1715
+ includePaths: options.includePaths,
1716
+ excludePatterns: options.excludePatterns,
1717
+ project: toOxlintProjectInfo(project),
1718
+ customRulesOnly: config.customRulesOnly,
1719
+ includeEcosystemRules: config.includeEcosystemRules,
1720
+ adoptExistingLintConfig: config.adoptExistingLintConfig,
1721
+ ignoredTags: config.ignoredTags ? new Set(config.ignoredTags) : void 0,
1722
+ signal: options.signal
1723
+ }),
1724
+ durationMilliseconds: globalThis.performance.now() - startedMilliseconds
1725
+ };
1726
+ } catch (error) {
1727
+ return {
1728
+ id: OXLINT_CHECK_ID,
1729
+ name: "Oxlint",
1730
+ status: "failed",
1731
+ issues: [],
1732
+ durationMilliseconds: globalThis.performance.now() - startedMilliseconds,
1733
+ error: toReactDoctorErrorInfo(error)
1734
+ };
1735
+ }
1736
+ };
1737
+ const applyIssueFiltering = (checks, filteredIssues) => {
1738
+ const issueIds = new Set(filteredIssues.map((issue) => issue.id));
1739
+ return checks.map((check) => ({
1740
+ ...check,
1741
+ issues: check.issues.filter((issue) => issueIds.has(issue.id))
1742
+ }));
1743
+ };
1744
+ const inspectReactProjectCore = async (options = {}) => {
1745
+ options.signal?.throwIfAborted();
1746
+ const startedAt = /* @__PURE__ */ new Date();
1747
+ const startedMilliseconds = globalThis.performance.now();
1748
+ const requestedRootDirectory = path.resolve(options.rootDirectory ?? ".");
1749
+ const loadedConfig = await getLoadedConfig(requestedRootDirectory, options);
1750
+ const rootDirectory = await resolveConfigRootDirectory(loadedConfig, requestedRootDirectory);
1751
+ const config = mergeConfig(loadedConfig, options);
1752
+ const project = await discoverReactProject(rootDirectory);
1753
+ options.signal?.throwIfAborted();
1754
+ const registry = createRuleRegistry();
1755
+ let codebaseAnalysisPromise = null;
1756
+ const getCodebaseAnalysis = () => {
1757
+ codebaseAnalysisPromise ??= runCodebaseAnalysis({
1758
+ rootDirectory,
1759
+ includePaths: options.includePaths,
1760
+ excludePatterns: options.excludePatterns,
1761
+ signal: options.signal
1762
+ });
1763
+ return codebaseAnalysisPromise;
1764
+ };
1765
+ const checks = await registry.runRules({
1766
+ rootDirectory,
1767
+ includePaths: options.includePaths,
1768
+ excludePatterns: options.excludePatterns,
1769
+ selection: mergeRuleSelection(options.rules, config),
1770
+ signal: options.signal,
1771
+ getCodebaseAnalysis
1772
+ });
1773
+ const oxlintCheck = await createOxlintCheck(rootDirectory, config, options, project);
1774
+ const allChecks = oxlintCheck ? [...checks, oxlintCheck] : checks;
1775
+ const completedAt = /* @__PURE__ */ new Date();
1776
+ const issues = filterReactDoctorIssues(allChecks.flatMap((check) => check.issues), config, rootDirectory, (filePath) => readSourceLines(rootDirectory, filePath), { jsxImportSource: readJsxImportSource(rootDirectory) });
1777
+ const filteredChecks = applyIssueFiltering(allChecks, issues);
1778
+ const hasFailedChecks = filteredChecks.some((check) => check.status === "failed");
1779
+ const score = (config.offline ? null : await tryScoreFromApi(issues, proxyFetch, { silent: options.silentLogs === true })) ?? calculateReactDoctorScore(issues);
1780
+ return {
1781
+ status: hasFailedChecks ? "completed-with-errors" : "completed",
1782
+ project,
1783
+ issues,
1784
+ checks: filteredChecks,
1785
+ score,
1786
+ startedAt: startedAt.toISOString(),
1787
+ completedAt: completedAt.toISOString(),
1788
+ durationMilliseconds: globalThis.performance.now() - startedMilliseconds
1789
+ };
1790
+ };
1791
+ //#endregion
1792
+ //#region src/sdk/compat.ts
1793
+ const toDiagnostic = (issue) => ({
1794
+ filePath: issue.location?.filePath ?? "",
1795
+ plugin: issue.source?.pluginName ?? issue.source?.checkId ?? "react-doctor",
1796
+ rule: issue.source?.ruleId ?? issue.id,
1797
+ severity: issue.severity === "error" ? "error" : "warning",
1798
+ message: issue.message,
1799
+ help: issue.recommendation ?? "",
1800
+ line: issue.location?.line ?? 0,
1801
+ column: issue.location?.column ?? 0,
1802
+ category: issue.category
1803
+ });
1804
+ const toScoreResult = (score) => score ? {
1805
+ score: score.value,
1806
+ label: score.label
1807
+ } : null;
1808
+ const toProjectInfo = (result) => ({
1809
+ rootDirectory: result.project.rootDirectory,
1810
+ projectName: result.project.projectName || path.basename(result.project.rootDirectory),
1811
+ reactVersion: result.project.reactVersion,
1812
+ tailwindVersion: result.project.tailwindVersion,
1813
+ framework: result.project.framework,
1814
+ hasTypeScript: result.project.hasTypeScript,
1815
+ hasReactCompiler: result.project.hasReactCompiler,
1816
+ hasTanStackQuery: result.project.hasTanStackQuery,
1817
+ sourceFileCount: result.project.sourceFileCount
1818
+ });
1819
+ const toInspectOptions = (directory, options) => ({
1820
+ rootDirectory: directory,
1821
+ includePaths: options.includePaths,
1822
+ signal: options.signal
1823
+ });
1824
+ const resolveCompatBoolean = (callerValue, diskValue) => callerValue ?? diskValue ?? true;
1825
+ /**
1826
+ * @deprecated Use `createReactDoctor({ rootDirectory }).inspect()` from the main SDK instead.
1827
+ */
1828
+ const diagnose = async (directory, options = {}) => {
1829
+ const loadedConfig = await loadReactDoctorConfig(path.resolve(directory));
1830
+ const diskConfig = loadedConfig?.config;
1831
+ const result = await inspectReactProjectCore({
1832
+ ...toInspectOptions(directory, options),
1833
+ lint: resolveCompatBoolean(options.lint, diskConfig?.lint),
1834
+ deadCode: resolveCompatBoolean(options.deadCode, diskConfig?.deadCode),
1835
+ respectInlineDisables: resolveCompatBoolean(options.respectInlineDisables, diskConfig?.respectInlineDisables),
1836
+ loadedConfig
1837
+ });
1838
+ return {
1839
+ diagnostics: result.issues.map(toDiagnostic),
1840
+ score: toScoreResult(result.score),
1841
+ project: toProjectInfo(result),
1842
+ elapsedMilliseconds: result.durationMilliseconds
1843
+ };
1844
+ };
1845
+ const clearCaches = () => {
1846
+ clearReactDoctorConfigCache();
1847
+ };
1848
+ //#endregion
1849
+ export { filterReactDoctorIssues as a, toOxlintProjectInfo as c, summarizeReactDoctorResult as d, OXLINT_CHECK_ID as f, resolveConfigRootDirectory as h, runOxlint as i, buildReactDoctorJsonReport as l, loadReactDoctorConfig as m, diagnose as n, discoverReactProject as o, clearReactDoctorConfigCache as p, inspectReactProjectCore as r, parseReactMajorVersion as s, clearCaches as t, calculateReactDoctorScore as u };
1850
+
1851
+ //# sourceMappingURL=compat-CM6aj69a.js.map