react-doctor 0.2.0-beta.2 → 0.2.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,17 +1,3083 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { Command } from "commander";
3
- import { CANONICAL_GITHUB_URL, GIT_SHOW_MAX_BUFFER_BYTES, MAX_CATEGORY_GROUPS_SHOWN_NON_VERBOSE, MAX_RULE_GROUPS_PER_CATEGORY_NON_VERBOSE, MILLISECONDS_PER_SECOND, OUTPUT_DETAIL_WRAP_WIDTH_CHARS, OXLINT_NODE_REQUIREMENT, OXLINT_RECOMMENDED_NODE_MAJOR, PERFECT_SCORE, RULE_NAME_COLUMN_WIDTH_CHARS, SCORE_BAR_WIDTH_CHARS, SCORE_GOOD_THRESHOLD, SCORE_OK_THRESHOLD, SHARE_BASE_URL, SKILL_NAME, SOURCE_FILE_PATTERN, SPINNER_INDENT_CHARS, buildJsonReport, buildJsonReportError, calculateScore, combineDiagnostics, computeJsxIncludePaths, filterSourceFiles, formatErrorChain, getDiffInfo, groupBy, highlighter, installNodeViaNvm, isLoggerSilent, isNvmInstalled, loadConfigWithSource, logger, resolveConfigRootDir, resolveLintIncludePaths, resolveNodeForOxlint, runOxlint, setLoggerSilent, toRelativePath } from "@react-doctor/core";
4
- import fs, { accessSync, constants, existsSync, mkdirSync, mkdtempSync, statSync, writeFileSync } from "node:fs";
5
- import { tmpdir } from "node:os";
3
+ import fs, { accessSync, constants, existsSync, mkdirSync, mkdtempSync, readdirSync, statSync, writeFileSync } from "node:fs";
6
4
  import path, { join } from "node:path";
5
+ import { spawn, spawnSync } from "node:child_process";
6
+ import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES } from "oxlint-plugin-react-doctor";
7
+ import os, { tmpdir } from "node:os";
8
+ import * as ts from "typescript";
7
9
  import { performance } from "node:perf_hooks";
8
- import { NoReactDependencyError, discoverProject, discoverReactSubprojects, formatFrameworkName, listWorkspacePackages } from "@react-doctor/project-info";
9
10
  import ora from "ora";
10
11
  import { randomUUID } from "node:crypto";
11
12
  import basePrompts from "prompts";
12
- import { spawnSync } from "node:child_process";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource } from "agent-install";
15
+ //#region ../project-info/dist/index.js
16
+ var ReactDoctorError = class extends Error {
17
+ name = "ReactDoctorError";
18
+ constructor(message, options) {
19
+ super(message, options);
20
+ Object.setPrototypeOf(this, new.target.prototype);
21
+ }
22
+ };
23
+ var NoReactDependencyError = class extends ReactDoctorError {
24
+ name = "NoReactDependencyError";
25
+ directory;
26
+ constructor(directory, options) {
27
+ super(`No React dependency found in ${directory}/package.json. Add "react" to dependencies (or peerDependencies) and re-run.`, options);
28
+ this.directory = directory;
29
+ }
30
+ };
31
+ var PackageJsonNotFoundError = class extends ReactDoctorError {
32
+ name = "PackageJsonNotFoundError";
33
+ directory;
34
+ constructor(directory, options) {
35
+ super(`No package.json found in ${directory}`, options);
36
+ this.directory = directory;
37
+ }
38
+ };
39
+ const isFile = (filePath) => {
40
+ try {
41
+ return fs.statSync(filePath).isFile();
42
+ } catch {
43
+ return false;
44
+ }
45
+ };
46
+ const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
47
+ const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
48
+ const IGNORED_DIRECTORIES = new Set([
49
+ ".git",
50
+ ".next",
51
+ ".nuxt",
52
+ ".output",
53
+ ".svelte-kit",
54
+ ".turbo",
55
+ "build",
56
+ "coverage",
57
+ "dist",
58
+ "node_modules",
59
+ "out",
60
+ "storybook-static"
61
+ ]);
62
+ const countSourceFilesViaFilesystem = (rootDirectory) => {
63
+ let count = 0;
64
+ const stack = [rootDirectory];
65
+ while (stack.length > 0) {
66
+ const currentDirectory = stack.pop();
67
+ const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
68
+ for (const entry of entries) {
69
+ if (entry.isDirectory()) {
70
+ if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(path.join(currentDirectory, entry.name));
71
+ continue;
72
+ }
73
+ if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) count++;
74
+ }
75
+ }
76
+ return count;
77
+ };
78
+ const countSourceFilesViaGit = (rootDirectory) => {
79
+ const result = spawnSync("git", [
80
+ "ls-files",
81
+ "-z",
82
+ "--cached",
83
+ "--others",
84
+ "--exclude-standard"
85
+ ], {
86
+ cwd: rootDirectory,
87
+ encoding: "utf-8",
88
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
89
+ });
90
+ if (result.error || result.status !== 0) return null;
91
+ return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;
92
+ };
93
+ const countSourceFiles = (rootDirectory) => countSourceFilesViaGit(rootDirectory) ?? countSourceFilesViaFilesystem(rootDirectory);
94
+ const cachedPackageJsons = /* @__PURE__ */ new Map();
95
+ const readPackageJsonUncached = (packageJsonPath) => {
96
+ try {
97
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
98
+ } catch (error) {
99
+ if (error instanceof SyntaxError) return {};
100
+ if (error instanceof Error && "code" in error) {
101
+ const { code } = error;
102
+ if (code === "EISDIR" || code === "EACCES") return {};
103
+ }
104
+ throw error;
105
+ }
106
+ };
107
+ const readPackageJson = (packageJsonPath) => {
108
+ const absolutePath = path.resolve(packageJsonPath);
109
+ const cached = cachedPackageJsons.get(absolutePath);
110
+ if (cached !== void 0) return cached;
111
+ const result = readPackageJsonUncached(absolutePath);
112
+ cachedPackageJsons.set(absolutePath, result);
113
+ return result;
114
+ };
115
+ const isMonorepoRoot = (directory) => {
116
+ if (isFile(path.join(directory, "pnpm-workspace.yaml"))) return true;
117
+ if (isFile(path.join(directory, "nx.json"))) return true;
118
+ const packageJsonPath = path.join(directory, "package.json");
119
+ if (!isFile(packageJsonPath)) return false;
120
+ const packageJson = readPackageJson(packageJsonPath);
121
+ return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
122
+ };
123
+ const findMonorepoRoot = (startDirectory) => {
124
+ let currentDirectory = path.dirname(startDirectory);
125
+ while (currentDirectory !== path.dirname(currentDirectory)) {
126
+ if (isMonorepoRoot(currentDirectory)) return currentDirectory;
127
+ currentDirectory = path.dirname(currentDirectory);
128
+ }
129
+ return null;
130
+ };
131
+ const REACT_COMPILER_PACKAGES = new Set([
132
+ "babel-plugin-react-compiler",
133
+ "react-compiler-runtime",
134
+ "eslint-plugin-react-compiler"
135
+ ]);
136
+ const NEXT_CONFIG_FILENAMES = [
137
+ "next.config.js",
138
+ "next.config.mjs",
139
+ "next.config.ts",
140
+ "next.config.cjs"
141
+ ];
142
+ const BABEL_CONFIG_FILENAMES = [
143
+ ".babelrc",
144
+ ".babelrc.json",
145
+ "babel.config.js",
146
+ "babel.config.json",
147
+ "babel.config.cjs",
148
+ "babel.config.mjs"
149
+ ];
150
+ const VITE_CONFIG_FILENAMES = [
151
+ "vite.config.js",
152
+ "vite.config.ts",
153
+ "vite.config.mjs",
154
+ "vite.config.mts",
155
+ "vite.config.cjs",
156
+ "vite.config.cts",
157
+ "vitest.config.ts",
158
+ "vitest.config.js"
159
+ ];
160
+ const EXPO_APP_CONFIG_FILENAMES = [
161
+ "app.json",
162
+ "app.config.js",
163
+ "app.config.ts"
164
+ ];
165
+ const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
166
+ const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
167
+ const hasCompilerPackage = (packageJson) => {
168
+ const allDependencies = {
169
+ ...packageJson.peerDependencies,
170
+ ...packageJson.dependencies,
171
+ ...packageJson.devDependencies
172
+ };
173
+ return Object.keys(allDependencies).some((packageName) => REACT_COMPILER_PACKAGES.has(packageName));
174
+ };
175
+ const hasCompilerInConfigFile = (filePath) => {
176
+ if (!isFile(filePath)) return false;
177
+ const content = fs.readFileSync(filePath, "utf-8");
178
+ return REACT_COMPILER_ENABLED_FLAG_PATTERN.test(content) || REACT_COMPILER_PACKAGE_REFERENCE_PATTERN.test(content);
179
+ };
180
+ const hasCompilerInConfigFiles = (directory, filenames) => filenames.some((filename) => hasCompilerInConfigFile(path.join(directory, filename)));
181
+ const isProjectBoundary$2 = (directory) => {
182
+ if (fs.existsSync(path.join(directory, ".git"))) return true;
183
+ return isMonorepoRoot(directory);
184
+ };
185
+ const detectReactCompiler = (directory, packageJson) => {
186
+ if (hasCompilerPackage(packageJson)) return true;
187
+ if (hasCompilerInConfigFiles(directory, NEXT_CONFIG_FILENAMES)) return true;
188
+ if (hasCompilerInConfigFiles(directory, BABEL_CONFIG_FILENAMES)) return true;
189
+ if (hasCompilerInConfigFiles(directory, VITE_CONFIG_FILENAMES)) return true;
190
+ if (hasCompilerInConfigFiles(directory, EXPO_APP_CONFIG_FILENAMES)) return true;
191
+ if (isProjectBoundary$2(directory)) return false;
192
+ let ancestorDirectory = path.dirname(directory);
193
+ while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
194
+ const ancestorPackagePath = path.join(ancestorDirectory, "package.json");
195
+ if (isFile(ancestorPackagePath)) {
196
+ if (hasCompilerPackage(readPackageJson(ancestorPackagePath))) return true;
197
+ }
198
+ if (isProjectBoundary$2(ancestorDirectory)) return false;
199
+ ancestorDirectory = path.dirname(ancestorDirectory);
200
+ }
201
+ return false;
202
+ };
203
+ const FRAMEWORK_PACKAGES = {
204
+ next: "nextjs",
205
+ "@tanstack/react-start": "tanstack-start",
206
+ vite: "vite",
207
+ "react-scripts": "cra",
208
+ "@remix-run/react": "remix",
209
+ gatsby: "gatsby",
210
+ expo: "expo",
211
+ "react-native": "react-native"
212
+ };
213
+ const FRAMEWORK_DISPLAY_NAMES = {
214
+ nextjs: "Next.js",
215
+ "tanstack-start": "TanStack Start",
216
+ vite: "Vite",
217
+ cra: "Create React App",
218
+ remix: "Remix",
219
+ gatsby: "Gatsby",
220
+ expo: "Expo",
221
+ "react-native": "React Native",
222
+ unknown: "React"
223
+ };
224
+ const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
225
+ const detectFramework = (dependencies) => {
226
+ for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
227
+ return "unknown";
228
+ };
229
+ const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
230
+ const HAS_UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/;
231
+ const OR_SEPARATOR = /\s*\|\|\s*/;
232
+ const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
233
+ const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
234
+ const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
235
+ const NON_LOWER_BOUND_COMPARATOR = /(?:^|[\s,|])(?:>(?!=)|!={0,2})\s*\d/;
236
+ const LOWER_BOUND_MAJOR = /(?:^|[\s,|])(?:>=\s*|[~^=v]\s*)?(\d+)(?=$|[\s,|.*xX-])/g;
237
+ const NPM_ALIAS_VERSION = /^npm:(?:@[^/]+\/[^@]+|[^@]+)@(.+)$/i;
238
+ const normalizeDependencyVersion = (version) => {
239
+ const trimmed = version.trim();
240
+ if (trimmed.length === 0) return null;
241
+ const normalizedVersion = trimmed.match(NPM_ALIAS_VERSION)?.[1]?.trim() ?? trimmed;
242
+ if (UNRESOLVABLE_PROTOCOL_VERSION.test(normalizedVersion)) return null;
243
+ if (DIST_TAG_VERSION.test(normalizedVersion) && !/^v\d/i.test(normalizedVersion)) return null;
244
+ if (WILDCARD_VERSION.test(normalizedVersion)) return null;
245
+ return normalizedVersion;
246
+ };
247
+ const splitDependencyVersionBranches = (version) => version.split(OR_SEPARATOR).filter(Boolean);
248
+ const hasUpperBoundComparator = (version) => HAS_UPPER_BOUND_COMPARATOR.test(version);
249
+ const getBranchLowestMajor = (branch) => {
250
+ if (NON_LOWER_BOUND_COMPARATOR.test(branch)) return null;
251
+ const lowerBoundComparators = branch.replace(UPPER_BOUND_COMPARATOR, " ").trim();
252
+ if (lowerBoundComparators.length === 0) return null;
253
+ let branchLowestMajor = null;
254
+ for (const match of lowerBoundComparators.matchAll(LOWER_BOUND_MAJOR)) {
255
+ const major = Number.parseInt(match[1], 10);
256
+ if (!Number.isFinite(major) || major <= 0) continue;
257
+ if (branchLowestMajor === null || major < branchLowestMajor) branchLowestMajor = major;
258
+ }
259
+ return branchLowestMajor;
260
+ };
261
+ const getLowestDependencyMajor = (version) => {
262
+ const normalizedVersion = normalizeDependencyVersion(version);
263
+ if (normalizedVersion === null) return null;
264
+ let lowestMajor = null;
265
+ for (const branch of splitDependencyVersionBranches(normalizedVersion)) {
266
+ const normalizedBranch = normalizeDependencyVersion(branch);
267
+ if (normalizedBranch === null) return null;
268
+ const branchLowestMajor = getBranchLowestMajor(normalizedBranch);
269
+ if (branchLowestMajor === null && hasUpperBoundComparator(normalizedBranch)) return null;
270
+ if (branchLowestMajor !== null && (lowestMajor === null || branchLowestMajor < lowestMajor)) lowestMajor = branchLowestMajor;
271
+ }
272
+ return lowestMajor;
273
+ };
274
+ const isConcreteDependencyVersion = (version) => {
275
+ const normalizedVersion = normalizeDependencyVersion(version);
276
+ return normalizedVersion !== null && /\d/.test(normalizedVersion);
277
+ };
278
+ const isPlainObject = (value) => {
279
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
280
+ const prototype = Object.getPrototypeOf(value);
281
+ return prototype === null || prototype === Object.prototype;
282
+ };
283
+ const isCatalogReference = (version) => version.startsWith("catalog:");
284
+ const extractCatalogName = (version) => {
285
+ if (!isCatalogReference(version)) return null;
286
+ const name = version.slice(8).trim();
287
+ return name.length > 0 ? name : null;
288
+ };
289
+ const resolveVersionFromCatalog = (catalog, packageName) => {
290
+ const version = catalog[packageName];
291
+ if (typeof version === "string" && !isCatalogReference(version)) return version;
292
+ return null;
293
+ };
294
+ const parsePnpmWorkspaceCatalogs = (rootDirectory) => {
295
+ const workspacePath = path.join(rootDirectory, "pnpm-workspace.yaml");
296
+ if (!isFile(workspacePath)) return {
297
+ defaultCatalog: {},
298
+ namedCatalogs: {}
299
+ };
300
+ const content = fs.readFileSync(workspacePath, "utf-8");
301
+ const defaultCatalog = {};
302
+ const namedCatalogs = {};
303
+ let currentSection = "none";
304
+ let currentCatalogName = "";
305
+ for (const line of content.split("\n")) {
306
+ const trimmed = line.trim();
307
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
308
+ const indentLevel = line.search(/\S/);
309
+ if (indentLevel === 0 && trimmed === "catalog:") {
310
+ currentSection = "catalog";
311
+ continue;
312
+ }
313
+ if (indentLevel === 0 && trimmed === "catalogs:") {
314
+ currentSection = "catalogs";
315
+ continue;
316
+ }
317
+ if (indentLevel === 0) {
318
+ currentSection = "none";
319
+ continue;
320
+ }
321
+ if (currentSection === "catalog" && indentLevel > 0) {
322
+ const colonIndex = trimmed.indexOf(":");
323
+ if (colonIndex > 0) {
324
+ const key = trimmed.slice(0, colonIndex).trim().replace(/["']/g, "");
325
+ const value = trimmed.slice(colonIndex + 1).trim().replace(/["']/g, "");
326
+ if (key && value) defaultCatalog[key] = value;
327
+ }
328
+ continue;
329
+ }
330
+ if (currentSection === "catalogs" && indentLevel > 0) {
331
+ if (trimmed.endsWith(":") && !trimmed.includes(" ")) {
332
+ currentCatalogName = trimmed.slice(0, -1).replace(/["']/g, "");
333
+ currentSection = "named-catalog";
334
+ namedCatalogs[currentCatalogName] = {};
335
+ continue;
336
+ }
337
+ }
338
+ if (currentSection === "named-catalog" && indentLevel > 0) {
339
+ if (indentLevel <= 2 && trimmed.endsWith(":") && !trimmed.includes(" ")) {
340
+ currentCatalogName = trimmed.slice(0, -1).replace(/["']/g, "");
341
+ namedCatalogs[currentCatalogName] = {};
342
+ continue;
343
+ }
344
+ const colonIndex = trimmed.indexOf(":");
345
+ if (colonIndex > 0 && currentCatalogName) {
346
+ const key = trimmed.slice(0, colonIndex).trim().replace(/["']/g, "");
347
+ const value = trimmed.slice(colonIndex + 1).trim().replace(/["']/g, "");
348
+ if (key && value) namedCatalogs[currentCatalogName][key] = value;
349
+ }
350
+ }
351
+ }
352
+ return {
353
+ defaultCatalog,
354
+ namedCatalogs
355
+ };
356
+ };
357
+ const resolveCatalogVersionFromCollection = (catalogs, packageName, options) => {
358
+ const { catalogReference, shouldSearchUnreferencedNamedCatalogs } = options;
359
+ if (catalogReference) {
360
+ const namedCatalog = catalogs.namedCatalogs[catalogReference];
361
+ if (namedCatalog?.[packageName]) return namedCatalog[packageName];
362
+ }
363
+ if (catalogs.defaultCatalog[packageName]) return catalogs.defaultCatalog[packageName];
364
+ if (!shouldSearchUnreferencedNamedCatalogs) return null;
365
+ for (const namedCatalog of Object.values(catalogs.namedCatalogs)) if (namedCatalog[packageName]) return namedCatalog[packageName];
366
+ return null;
367
+ };
368
+ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicitCatalogReference) => {
369
+ const rawVersion = {
370
+ ...packageJson.peerDependencies,
371
+ ...packageJson.dependencies,
372
+ ...packageJson.devDependencies
373
+ }[packageName];
374
+ const hasExplicitCatalogReference = explicitCatalogReference !== void 0;
375
+ const catalogName = hasExplicitCatalogReference ? explicitCatalogReference : rawVersion ? extractCatalogName(rawVersion) : null;
376
+ const shouldSearchUnreferencedNamedCatalogs = !hasExplicitCatalogReference && catalogName === null;
377
+ if (isPlainObject(packageJson.catalog)) {
378
+ const version = resolveVersionFromCatalog(packageJson.catalog, packageName);
379
+ if (version) return version;
380
+ }
381
+ if (isPlainObject(packageJson.catalogs)) {
382
+ const namedCatalog = catalogName ? packageJson.catalogs[catalogName] : void 0;
383
+ if (namedCatalog && isPlainObject(namedCatalog)) {
384
+ const version = resolveVersionFromCatalog(namedCatalog, packageName);
385
+ if (version) return version;
386
+ }
387
+ if (shouldSearchUnreferencedNamedCatalogs) {
388
+ for (const catalogEntries of Object.values(packageJson.catalogs)) if (isPlainObject(catalogEntries)) {
389
+ const version = resolveVersionFromCatalog(catalogEntries, packageName);
390
+ if (version) return version;
391
+ }
392
+ }
393
+ }
394
+ const workspaces = packageJson.workspaces;
395
+ if (workspaces && !Array.isArray(workspaces)) {
396
+ if (isPlainObject(workspaces.catalog)) {
397
+ const version = resolveVersionFromCatalog(workspaces.catalog, packageName);
398
+ if (version) return version;
399
+ }
400
+ if (isPlainObject(workspaces.catalogs)) {
401
+ const namedCatalog = catalogName ? workspaces.catalogs[catalogName] : void 0;
402
+ if (namedCatalog && isPlainObject(namedCatalog)) {
403
+ const version = resolveVersionFromCatalog(namedCatalog, packageName);
404
+ if (version) return version;
405
+ }
406
+ if (shouldSearchUnreferencedNamedCatalogs) {
407
+ for (const catalogEntries of Object.values(workspaces.catalogs)) if (isPlainObject(catalogEntries)) {
408
+ const version = resolveVersionFromCatalog(catalogEntries, packageName);
409
+ if (version) return version;
410
+ }
411
+ }
412
+ }
413
+ }
414
+ if (rootDirectory) {
415
+ const pnpmVersion = resolveCatalogVersionFromCollection(parsePnpmWorkspaceCatalogs(rootDirectory), packageName, {
416
+ catalogReference: catalogName,
417
+ shouldSearchUnreferencedNamedCatalogs
418
+ });
419
+ if (pnpmVersion) return pnpmVersion;
420
+ }
421
+ return null;
422
+ };
423
+ const EMPTY_DEPENDENCY_INFO = {
424
+ reactVersion: null,
425
+ tailwindVersion: null,
426
+ framework: "unknown"
427
+ };
428
+ const pickConcreteVersion = (packageJson, packageName, sections) => {
429
+ for (const section of sections) {
430
+ const version = packageJson[section]?.[packageName];
431
+ if (version === void 0) continue;
432
+ if (isCatalogReference(version)) return null;
433
+ if (isConcreteDependencyVersion(version)) return version;
434
+ }
435
+ return null;
436
+ };
437
+ const extractDependencyInfo = (packageJson) => {
438
+ const allDependencies = {
439
+ ...packageJson.peerDependencies,
440
+ ...packageJson.dependencies,
441
+ ...packageJson.devDependencies
442
+ };
443
+ return {
444
+ reactVersion: pickConcreteVersion(packageJson, "react", [
445
+ "dependencies",
446
+ "peerDependencies",
447
+ "devDependencies"
448
+ ]),
449
+ tailwindVersion: pickConcreteVersion(packageJson, "tailwindcss", [
450
+ "dependencies",
451
+ "devDependencies",
452
+ "peerDependencies"
453
+ ]),
454
+ framework: detectFramework(allDependencies)
455
+ };
456
+ };
457
+ const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
458
+ for (const section of sections) {
459
+ const version = packageJson[section]?.[packageName];
460
+ if (version === void 0) continue;
461
+ return {
462
+ catalogReference: extractCatalogName(version) ?? null,
463
+ hasDeclaration: true,
464
+ version
465
+ };
466
+ }
467
+ return {
468
+ catalogReference: null,
469
+ hasDeclaration: false,
470
+ version: null
471
+ };
472
+ };
473
+ const NX_PROJECT_DISCOVERY_DIRS = [
474
+ "apps",
475
+ "libs",
476
+ "packages"
477
+ ];
478
+ const getNxWorkspaceDirectories = (rootDirectory) => {
479
+ if (!isFile(path.join(rootDirectory, "nx.json"))) return [];
480
+ const collected = [];
481
+ for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
482
+ const candidatePath = path.join(rootDirectory, candidate);
483
+ if (!fs.existsSync(candidatePath) || !fs.statSync(candidatePath).isDirectory()) continue;
484
+ for (const entry of fs.readdirSync(candidatePath, { withFileTypes: true })) {
485
+ if (!entry.isDirectory()) continue;
486
+ const projectDirectory = path.join(candidatePath, entry.name);
487
+ if (isFile(path.join(projectDirectory, "project.json")) || isFile(path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
488
+ }
489
+ }
490
+ return collected;
491
+ };
492
+ const parsePnpmWorkspacePatterns = (rootDirectory) => {
493
+ const workspacePath = path.join(rootDirectory, "pnpm-workspace.yaml");
494
+ if (!isFile(workspacePath)) return [];
495
+ const content = fs.readFileSync(workspacePath, "utf-8");
496
+ const patterns = [];
497
+ let isInsidePackagesBlock = false;
498
+ for (const line of content.split("\n")) {
499
+ const trimmed = line.trim();
500
+ if (trimmed === "packages:") {
501
+ isInsidePackagesBlock = true;
502
+ continue;
503
+ }
504
+ if (isInsidePackagesBlock && trimmed.startsWith("-")) patterns.push(trimmed.replace(/^-\s*/, "").replace(/["']/g, ""));
505
+ else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith("#")) isInsidePackagesBlock = false;
506
+ }
507
+ return patterns;
508
+ };
509
+ const getWorkspacePatterns = (rootDirectory, packageJson) => {
510
+ const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
511
+ if (pnpmPatterns.length > 0) return pnpmPatterns;
512
+ if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
513
+ if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
514
+ const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
515
+ if (nxPatterns.length > 0) return nxPatterns;
516
+ return [];
517
+ };
518
+ const parseReactMajor = (reactVersion) => {
519
+ if (typeof reactVersion !== "string") return null;
520
+ return getLowestDependencyMajor(reactVersion);
521
+ };
522
+ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
523
+ const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
524
+ if (!cleanPattern.includes("*")) {
525
+ const directoryPath = path.join(rootDirectory, cleanPattern);
526
+ if (fs.existsSync(directoryPath) && isFile(path.join(directoryPath, "package.json"))) return [directoryPath];
527
+ return [];
528
+ }
529
+ const wildcardIndex = cleanPattern.indexOf("*");
530
+ const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
531
+ const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
532
+ if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) return [];
533
+ const resolved = [];
534
+ for (const entry of fs.readdirSync(baseDirectory)) {
535
+ const entryPath = path.join(baseDirectory, entry, suffixAfterWildcard);
536
+ if (fs.existsSync(entryPath) && fs.statSync(entryPath).isDirectory() && isFile(path.join(entryPath, "package.json"))) resolved.push(entryPath);
537
+ }
538
+ return resolved;
539
+ };
540
+ const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
541
+ const dependencyDeclaration = getDependencyDeclaration({
542
+ packageJson: workspacePackageJson,
543
+ packageName,
544
+ sections
545
+ });
546
+ if (!dependencyDeclaration.hasDeclaration) return null;
547
+ return concreteVersion ?? resolveCatalogVersion(workspacePackageJson, packageName, workspaceDirectory, dependencyDeclaration.catalogReference) ?? resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, dependencyDeclaration.catalogReference);
548
+ };
549
+ const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
550
+ if (!currentVersion) return true;
551
+ const currentMajor = parseReactMajor(currentVersion);
552
+ const nextMajor = parseReactMajor(nextVersion);
553
+ if (currentMajor === null) return nextMajor !== null;
554
+ if (nextMajor === null) return false;
555
+ return nextMajor < currentMajor;
556
+ };
557
+ const findReactInWorkspaces = (rootDirectory, packageJson) => {
558
+ const patterns = getWorkspacePatterns(rootDirectory, packageJson);
559
+ const result = { ...EMPTY_DEPENDENCY_INFO };
560
+ for (const pattern of patterns) {
561
+ const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
562
+ for (const workspaceDirectory of directories) {
563
+ const workspacePackageJson = readPackageJson(path.join(workspaceDirectory, "package.json"));
564
+ const info = extractDependencyInfo(workspacePackageJson);
565
+ const reactVersion = resolveWorkspaceDependencyVersion({
566
+ concreteVersion: info.reactVersion,
567
+ packageName: "react",
568
+ rootDirectory,
569
+ rootPackageJson: packageJson,
570
+ sections: [
571
+ "dependencies",
572
+ "peerDependencies",
573
+ "devDependencies"
574
+ ],
575
+ workspaceDirectory,
576
+ workspacePackageJson
577
+ });
578
+ const tailwindVersion = resolveWorkspaceDependencyVersion({
579
+ concreteVersion: info.tailwindVersion,
580
+ packageName: "tailwindcss",
581
+ rootDirectory,
582
+ rootPackageJson: packageJson,
583
+ sections: [
584
+ "dependencies",
585
+ "devDependencies",
586
+ "peerDependencies"
587
+ ],
588
+ workspaceDirectory,
589
+ workspacePackageJson
590
+ });
591
+ if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
592
+ if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
593
+ if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
594
+ const resultReactMajor = parseReactMajor(result.reactVersion);
595
+ if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
596
+ }
597
+ }
598
+ return result;
599
+ };
600
+ const REACT_DEPENDENCY_NAMES = new Set([
601
+ "react",
602
+ "react-native",
603
+ "next"
604
+ ]);
605
+ const hasReactDependency = (packageJson) => {
606
+ const allDependencies = {
607
+ ...packageJson.peerDependencies,
608
+ ...packageJson.dependencies,
609
+ ...packageJson.devDependencies
610
+ };
611
+ return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
612
+ };
613
+ const findDependencyInfoFromMonorepoRoot = (directory) => {
614
+ const monorepoRoot = findMonorepoRoot(directory);
615
+ if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
616
+ const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
617
+ if (!isFile(monorepoPackageJsonPath)) return EMPTY_DEPENDENCY_INFO;
618
+ const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
619
+ const rootInfo = extractDependencyInfo(rootPackageJson);
620
+ const leafPackageJsonPath = path.join(directory, "package.json");
621
+ const leafPackageJson = isFile(leafPackageJsonPath) ? readPackageJson(leafPackageJsonPath) : null;
622
+ const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
623
+ packageJson: leafPackageJson,
624
+ packageName: "react",
625
+ sections: [
626
+ "dependencies",
627
+ "peerDependencies",
628
+ "devDependencies"
629
+ ]
630
+ }) : null;
631
+ const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
632
+ packageJson: leafPackageJson,
633
+ packageName: "tailwindcss",
634
+ sections: [
635
+ "dependencies",
636
+ "devDependencies",
637
+ "peerDependencies"
638
+ ]
639
+ }) : null;
640
+ const shouldUseReactFallback = leafPackageJson ? hasReactDependency(leafPackageJson) : true;
641
+ const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
642
+ const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
643
+ const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
644
+ const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
645
+ return {
646
+ reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : null,
647
+ tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
648
+ framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
649
+ };
650
+ };
651
+ const TANSTACK_QUERY_PACKAGES = new Set([
652
+ "@tanstack/react-query",
653
+ "@tanstack/query-core",
654
+ "react-query"
655
+ ]);
656
+ const hasTanStackQuery = (packageJson) => {
657
+ const allDependencies = {
658
+ ...packageJson.peerDependencies,
659
+ ...packageJson.dependencies,
660
+ ...packageJson.devDependencies
661
+ };
662
+ return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
663
+ };
664
+ const hasUpperBoundOnlyPeerRange = (range) => {
665
+ if (typeof range !== "string") return false;
666
+ const normalizedRange = normalizeDependencyVersion(range);
667
+ if (normalizedRange === null) return false;
668
+ return splitDependencyVersionBranches(normalizedRange).some((branch) => {
669
+ const normalizedBranch = normalizeDependencyVersion(branch);
670
+ return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
671
+ });
672
+ };
673
+ const peerRangeMinMajor = (range) => {
674
+ if (typeof range !== "string") return null;
675
+ return getLowestDependencyMajor(range);
676
+ };
677
+ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
678
+ const installedReactMajor = parseReactMajor(reactVersion);
679
+ const peerReactRange = packageJson.peerDependencies?.react;
680
+ if (typeof peerReactRange !== "string") return installedReactMajor;
681
+ const peerFloor = peerRangeMinMajor(peerReactRange);
682
+ if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
683
+ return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
684
+ };
685
+ const listWorkspacePackages = (rootDirectory) => {
686
+ const packageJsonPath = path.join(rootDirectory, "package.json");
687
+ if (!isFile(packageJsonPath)) return [];
688
+ const packageJson = readPackageJson(packageJsonPath);
689
+ const patterns = getWorkspacePatterns(rootDirectory, packageJson);
690
+ if (patterns.length === 0) return [];
691
+ const packages = [];
692
+ const seenDirectories = /* @__PURE__ */ new Set();
693
+ const pushIfNew = (workspacePackage) => {
694
+ if (seenDirectories.has(workspacePackage.directory)) return;
695
+ seenDirectories.add(workspacePackage.directory);
696
+ packages.push(workspacePackage);
697
+ };
698
+ if (hasReactDependency(packageJson)) pushIfNew({
699
+ name: packageJson.name ?? path.basename(rootDirectory),
700
+ directory: rootDirectory
701
+ });
702
+ for (const pattern of patterns) {
703
+ const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
704
+ for (const workspaceDirectory of directories) {
705
+ const workspacePackageJson = readPackageJson(path.join(workspaceDirectory, "package.json"));
706
+ if (!hasReactDependency(workspacePackageJson)) continue;
707
+ pushIfNew({
708
+ name: workspacePackageJson.name ?? path.basename(workspaceDirectory),
709
+ directory: workspaceDirectory
710
+ });
711
+ }
712
+ }
713
+ return packages;
714
+ };
715
+ const toReactWorkspacePackages = (directories) => {
716
+ const packages = [];
717
+ for (const directory of directories) {
718
+ const packageJsonPath = path.join(directory, "package.json");
719
+ if (!isFile(packageJsonPath)) continue;
720
+ const packageJson = readPackageJson(packageJsonPath);
721
+ if (!hasReactDependency(packageJson)) continue;
722
+ const name = packageJson.name ?? path.basename(directory);
723
+ packages.push({
724
+ name,
725
+ directory
726
+ });
727
+ }
728
+ return packages;
729
+ };
730
+ const listManifestWorkspacePackages = (rootDirectory) => {
731
+ if (isFile(path.join(rootDirectory, "package.json"))) return listWorkspacePackages(rootDirectory);
732
+ const patterns = parsePnpmWorkspacePatterns(rootDirectory);
733
+ const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
734
+ return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
735
+ };
736
+ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
737
+ const packages = [];
738
+ const pendingDirectories = [rootDirectory];
739
+ while (pendingDirectories.length > 0) {
740
+ const currentDirectory = pendingDirectories.pop();
741
+ if (!currentDirectory) continue;
742
+ const packageJsonPath = path.join(currentDirectory, "package.json");
743
+ if (isFile(packageJsonPath)) {
744
+ const packageJson = readPackageJson(packageJsonPath);
745
+ if (hasReactDependency(packageJson)) {
746
+ const name = packageJson.name ?? path.basename(currentDirectory);
747
+ packages.push({
748
+ name,
749
+ directory: currentDirectory
750
+ });
751
+ }
752
+ }
753
+ const entries = fs.readdirSync(currentDirectory, { withFileTypes: true }).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
754
+ for (const entry of entries) {
755
+ if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
756
+ pendingDirectories.push(path.join(currentDirectory, entry.name));
757
+ }
758
+ }
759
+ return packages;
760
+ };
761
+ const discoverReactSubprojects = (rootDirectory) => {
762
+ if (!fs.existsSync(rootDirectory) || !fs.statSync(rootDirectory).isDirectory()) return [];
763
+ const manifestPackages = listManifestWorkspacePackages(rootDirectory);
764
+ if (manifestPackages.length > 0) return manifestPackages;
765
+ return discoverReactSubprojectsByFilesystem(rootDirectory);
766
+ };
767
+ const cachedProjectInfos = /* @__PURE__ */ new Map();
768
+ const discoverProject = (directory) => {
769
+ const cached = cachedProjectInfos.get(directory);
770
+ if (cached !== void 0) return cached;
771
+ const packageJsonPath = path.join(directory, "package.json");
772
+ if (!isFile(packageJsonPath)) throw new PackageJsonNotFoundError(directory);
773
+ const packageJson = readPackageJson(packageJsonPath);
774
+ let { reactVersion, tailwindVersion, framework } = extractDependencyInfo(packageJson);
775
+ const reactDeclaration = getDependencyDeclaration({
776
+ packageJson,
777
+ packageName: "react",
778
+ sections: [
779
+ "dependencies",
780
+ "peerDependencies",
781
+ "devDependencies"
782
+ ]
783
+ });
784
+ const tailwindDeclaration = getDependencyDeclaration({
785
+ packageJson,
786
+ packageName: "tailwindcss",
787
+ sections: [
788
+ "dependencies",
789
+ "devDependencies",
790
+ "peerDependencies"
791
+ ]
792
+ });
793
+ if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
794
+ if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
795
+ if (!reactVersion || !tailwindVersion) {
796
+ const monorepoRoot = findMonorepoRoot(directory);
797
+ if (monorepoRoot) {
798
+ const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
799
+ if (isFile(monorepoPackageJsonPath)) {
800
+ const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
801
+ if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
802
+ if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
803
+ }
804
+ }
805
+ }
806
+ if (!reactVersion || framework === "unknown") {
807
+ const workspaceInfo = findReactInWorkspaces(directory, packageJson);
808
+ if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
809
+ if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
810
+ if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
811
+ }
812
+ if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
813
+ const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
814
+ if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
815
+ if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
816
+ if (framework === "unknown") framework = monorepoInfo.framework;
817
+ }
818
+ if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
819
+ if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
820
+ const projectName = packageJson.name ?? path.basename(directory);
821
+ const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
822
+ const sourceFileCount = countSourceFiles(directory);
823
+ const projectInfo = {
824
+ rootDirectory: directory,
825
+ projectName,
826
+ reactVersion,
827
+ reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
828
+ tailwindVersion,
829
+ framework,
830
+ hasTypeScript,
831
+ hasReactCompiler: detectReactCompiler(directory, packageJson),
832
+ hasTanStackQuery: hasTanStackQuery(packageJson),
833
+ sourceFileCount
834
+ };
835
+ cachedProjectInfos.set(directory, projectInfo);
836
+ return projectInfo;
837
+ };
838
+ const parseTailwindMajorMinor = (tailwindVersion) => {
839
+ if (typeof tailwindVersion !== "string") return null;
840
+ const trimmed = tailwindVersion.trim();
841
+ if (trimmed.length === 0) return null;
842
+ const majorMinorMatch = trimmed.match(/(\d+)\.(\d+)/);
843
+ if (majorMinorMatch) {
844
+ const major = Number.parseInt(majorMinorMatch[1], 10);
845
+ const minor = Number.parseInt(majorMinorMatch[2], 10);
846
+ if (!Number.isFinite(major) || major <= 0) return null;
847
+ if (!Number.isFinite(minor) || minor < 0) return null;
848
+ return {
849
+ major,
850
+ minor
851
+ };
852
+ }
853
+ const majorOnlyMatch = trimmed.match(/(\d+)/);
854
+ if (!majorOnlyMatch) return null;
855
+ const major = Number.parseInt(majorOnlyMatch[1], 10);
856
+ if (!Number.isFinite(major) || major <= 0) return null;
857
+ return {
858
+ major,
859
+ minor: 0
860
+ };
861
+ };
862
+ const isTailwindAtLeast = (detected, required) => {
863
+ if (detected === null) return true;
864
+ if (detected.major !== required.major) return detected.major > required.major;
865
+ return detected.minor >= required.minor;
866
+ };
867
+ //#endregion
868
+ //#region ../core/dist/index.js
869
+ var __create = Object.create;
870
+ var __defProp = Object.defineProperty;
871
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
872
+ var __getOwnPropNames = Object.getOwnPropertyNames;
873
+ var __getProtoOf = Object.getPrototypeOf;
874
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
875
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
876
+ var __copyProps = (to, from, except, desc) => {
877
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
878
+ key = keys[i];
879
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
880
+ get: ((k) => from[k]).bind(null, key),
881
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
882
+ });
883
+ }
884
+ return to;
885
+ };
886
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
887
+ value: mod,
888
+ enumerable: true
889
+ }) : target, mod));
890
+ const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
891
+ const compileGlobPattern = (pattern) => {
892
+ const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\//, "");
893
+ let regexSource = "^";
894
+ let characterIndex = 0;
895
+ while (characterIndex < normalizedPattern.length) if (normalizedPattern[characterIndex] === "*" && normalizedPattern[characterIndex + 1] === "*") if (normalizedPattern[characterIndex + 2] === "/") {
896
+ regexSource += "(?:.+/)?";
897
+ characterIndex += 3;
898
+ } else {
899
+ regexSource += ".*";
900
+ characterIndex += 2;
901
+ }
902
+ else if (normalizedPattern[characterIndex] === "*") {
903
+ regexSource += "[^/]*";
904
+ characterIndex++;
905
+ } else if (normalizedPattern[characterIndex] === "?") {
906
+ regexSource += "[^/]";
907
+ characterIndex++;
908
+ } else {
909
+ regexSource += normalizedPattern[characterIndex].replace(REGEX_SPECIAL_CHARACTERS, "\\$&");
910
+ characterIndex++;
911
+ }
912
+ regexSource += "$";
913
+ return new RegExp(regexSource);
914
+ };
915
+ const toRelativePath = (filePath, rootDirectory) => {
916
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
917
+ const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
918
+ if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
919
+ return normalizedFilePath.replace(/^\.\//, "");
920
+ };
921
+ const warnConfigField$1 = (message) => {
922
+ process.stderr.write(`[react-doctor] ${message}\n`);
923
+ };
924
+ const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
925
+ const collectStringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
926
+ const validateOverrideEntry = (entry, index) => {
927
+ if (!isPlainObject(entry)) {
928
+ warnConfigField$1(`ignore.overrides[${index}] must be an object with { files, rules }; ignoring this entry.`);
929
+ return null;
930
+ }
931
+ if (!isStringArray(entry.files)) {
932
+ warnConfigField$1(`ignore.overrides[${index}].files must be an array of strings; ignoring this entry.`);
933
+ return null;
934
+ }
935
+ if (entry.rules !== void 0 && !isStringArray(entry.rules)) {
936
+ warnConfigField$1(`ignore.overrides[${index}].rules must be an array of "plugin/rule" strings or omitted; treating as missing (override would suppress every rule for the matched files).`);
937
+ return { files: entry.files };
938
+ }
939
+ return entry.rules === void 0 ? { files: entry.files } : {
940
+ files: entry.files,
941
+ rules: entry.rules
942
+ };
943
+ };
944
+ const compileIgnoreOverrides = (userConfig) => {
945
+ const overrides = userConfig?.ignore?.overrides;
946
+ if (overrides === void 0) return [];
947
+ if (!Array.isArray(overrides)) {
948
+ warnConfigField$1(`ignore.overrides must be an array of { files, rules } entries; ignoring.`);
949
+ return [];
950
+ }
951
+ return overrides.flatMap((entry, index) => {
952
+ const validated = validateOverrideEntry(entry, index);
953
+ if (!validated) return [];
954
+ const filePatterns = collectStringList(validated.files).map(compileGlobPattern);
955
+ if (filePatterns.length === 0) return [];
956
+ return [{
957
+ filePatterns,
958
+ ruleIds: new Set(collectStringList(validated.rules))
959
+ }];
960
+ });
961
+ };
962
+ const isDiagnosticIgnoredByOverrides = (diagnostic, rootDirectory, overrides) => {
963
+ if (overrides.length === 0) return false;
964
+ const relativeFilePath = toRelativePath(diagnostic.filePath, rootDirectory);
965
+ const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
966
+ return overrides.some((override) => override.filePatterns.some((pattern) => pattern.test(relativeFilePath)) && (override.ruleIds.size === 0 || override.ruleIds.has(ruleIdentifier)));
967
+ };
968
+ const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
969
+ const MILLISECONDS_PER_SECOND = 1e3;
970
+ const SCORE_API_URL = "https://www.react.doctor/api/score";
971
+ const SHARE_BASE_URL = "https://www.react.doctor/share";
972
+ const FETCH_TIMEOUT_MS = 1e4;
973
+ const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
974
+ const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
975
+ const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
976
+ const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
977
+ const CANONICAL_GITHUB_URL = "https://github.com/millionco/react-doctor";
978
+ const SKILL_NAME = "react-doctor";
979
+ const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
980
+ const estimateArgsLength = (args) => args.reduce((total, argument) => total + argument.length + 1, 0);
981
+ const batchIncludePaths = (baseArgs, includePaths) => {
982
+ const baseArgsLength = estimateArgsLength(baseArgs);
983
+ const batches = [];
984
+ let currentBatch = [];
985
+ let currentBatchLength = baseArgsLength;
986
+ for (const filePath of includePaths) {
987
+ const entryLength = filePath.length + 1;
988
+ const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
989
+ const exceedsFileCount = currentBatch.length >= 100;
990
+ if (exceedsArgLength || exceedsFileCount) {
991
+ batches.push(currentBatch);
992
+ currentBatch = [];
993
+ currentBatchLength = baseArgsLength;
994
+ }
995
+ currentBatch.push(filePath);
996
+ currentBatchLength += entryLength;
997
+ }
998
+ if (currentBatch.length > 0) batches.push(currentBatch);
999
+ return batches;
1000
+ };
1001
+ const collectErrorChain = (rootError) => {
1002
+ const errorChain = [];
1003
+ const visitedErrors = /* @__PURE__ */ new Set();
1004
+ let currentError = rootError;
1005
+ while (currentError !== void 0 && !visitedErrors.has(currentError)) {
1006
+ visitedErrors.add(currentError);
1007
+ errorChain.push(currentError);
1008
+ currentError = currentError instanceof Error ? currentError.cause : void 0;
1009
+ }
1010
+ return errorChain;
1011
+ };
1012
+ const formatErrorMessage = (error) => error instanceof Error ? error.message || error.name : String(error);
1013
+ const formatErrorChain = (rootError) => collectErrorChain(rootError).map(formatErrorMessage).join(" → ");
1014
+ const getErrorChainMessages = (rootError) => collectErrorChain(rootError).map(formatErrorMessage);
1015
+ const safeStringify = (value) => {
1016
+ try {
1017
+ return String(value);
1018
+ } catch {
1019
+ return "Unrepresentable error";
1020
+ }
1021
+ };
1022
+ const safeGetErrorChain = (error) => {
1023
+ try {
1024
+ return getErrorChainMessages(error);
1025
+ } catch {
1026
+ return [safeStringify(error)];
1027
+ }
1028
+ };
1029
+ const buildJsonReportError = (input) => {
1030
+ const chain = safeGetErrorChain(input.error);
1031
+ const errorPayload = input.error instanceof Error ? {
1032
+ message: input.error.message || input.error.name || "Error",
1033
+ name: input.error.name || "Error",
1034
+ chain
1035
+ } : {
1036
+ message: safeStringify(input.error),
1037
+ name: "Error",
1038
+ chain
1039
+ };
1040
+ return {
1041
+ schemaVersion: 1,
1042
+ version: input.version,
1043
+ ok: false,
1044
+ directory: input.directory,
1045
+ mode: input.mode ?? "full",
1046
+ diff: null,
1047
+ projects: [],
1048
+ diagnostics: [],
1049
+ summary: {
1050
+ errorCount: 0,
1051
+ warningCount: 0,
1052
+ affectedFileCount: 0,
1053
+ totalDiagnosticCount: 0,
1054
+ score: null,
1055
+ scoreLabel: null
1056
+ },
1057
+ elapsedMilliseconds: input.elapsedMilliseconds,
1058
+ error: errorPayload
1059
+ };
1060
+ };
1061
+ const summarizeDiagnostics = (diagnostics, worstScore = null, worstScoreLabel = null) => {
1062
+ let errorCount = 0;
1063
+ let warningCount = 0;
1064
+ const affectedFiles = /* @__PURE__ */ new Set();
1065
+ for (const diagnostic of diagnostics) {
1066
+ if (diagnostic.severity === "error") errorCount++;
1067
+ else warningCount++;
1068
+ affectedFiles.add(diagnostic.filePath);
1069
+ }
1070
+ return {
1071
+ errorCount,
1072
+ warningCount,
1073
+ affectedFileCount: affectedFiles.size,
1074
+ totalDiagnosticCount: diagnostics.length,
1075
+ score: worstScore,
1076
+ scoreLabel: worstScoreLabel
1077
+ };
1078
+ };
1079
+ const toJsonDiff = (diff) => {
1080
+ if (!diff) return null;
1081
+ return {
1082
+ baseBranch: diff.baseBranch,
1083
+ currentBranch: diff.currentBranch,
1084
+ changedFileCount: diff.changedFiles.length,
1085
+ isCurrentChanges: Boolean(diff.isCurrentChanges)
1086
+ };
1087
+ };
1088
+ const findWorstScoredProject = (projects) => {
1089
+ let worst = null;
1090
+ let worstScore = Number.POSITIVE_INFINITY;
1091
+ for (const project of projects) {
1092
+ const score = project.score?.score;
1093
+ if (typeof score !== "number") continue;
1094
+ if (score < worstScore) {
1095
+ worstScore = score;
1096
+ worst = project;
1097
+ }
1098
+ }
1099
+ return worst;
1100
+ };
1101
+ const buildJsonReport = (input) => {
1102
+ const projects = input.scans.map(({ directory, result }) => ({
1103
+ directory,
1104
+ project: result.project,
1105
+ diagnostics: result.diagnostics,
1106
+ score: result.score,
1107
+ skippedChecks: result.skippedChecks,
1108
+ ...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
1109
+ elapsedMilliseconds: result.elapsedMilliseconds
1110
+ }));
1111
+ const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
1112
+ const worstScoredProject = findWorstScoredProject(projects);
1113
+ const summary = summarizeDiagnostics(flattenedDiagnostics, worstScoredProject?.score?.score ?? null, worstScoredProject?.score?.label ?? null);
1114
+ return {
1115
+ schemaVersion: 1,
1116
+ version: input.version,
1117
+ ok: true,
1118
+ directory: input.directory,
1119
+ mode: input.mode,
1120
+ diff: toJsonDiff(input.diff),
1121
+ projects,
1122
+ diagnostics: flattenedDiagnostics,
1123
+ summary,
1124
+ elapsedMilliseconds: input.totalElapsedMilliseconds,
1125
+ error: null
1126
+ };
1127
+ };
1128
+ const parseScoreResult = (value) => {
1129
+ if (typeof value !== "object" || value === null) return null;
1130
+ if (!("score" in value) || !("label" in value)) return null;
1131
+ const scoreValue = Reflect.get(value, "score");
1132
+ const labelValue = Reflect.get(value, "label");
1133
+ if (typeof scoreValue !== "number" || typeof labelValue !== "string") return null;
1134
+ return {
1135
+ score: scoreValue,
1136
+ label: labelValue
1137
+ };
1138
+ };
1139
+ const stripFilePaths = (diagnostics) => diagnostics.map(({ filePath: _filePath, ...rest }) => rest);
1140
+ const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
1141
+ const describeFailure = (error) => {
1142
+ if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
1143
+ if (error instanceof Error && error.message) return error.message;
1144
+ return String(error);
1145
+ };
1146
+ const calculateScore = async (diagnostics) => {
1147
+ const controller = new AbortController();
1148
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1149
+ try {
1150
+ const response = await fetch(SCORE_API_URL, {
1151
+ method: "POST",
1152
+ headers: { "Content-Type": "application/json" },
1153
+ body: JSON.stringify({ diagnostics: stripFilePaths(diagnostics) }),
1154
+ signal: controller.signal
1155
+ });
1156
+ if (!response.ok) {
1157
+ console.warn(`[react-doctor] Score API returned ${response.status} ${response.statusText}`);
1158
+ return null;
1159
+ }
1160
+ return parseScoreResult(await response.json());
1161
+ } catch (error) {
1162
+ console.warn(`[react-doctor] Score API unreachable (${describeFailure(error)})`);
1163
+ return null;
1164
+ } finally {
1165
+ clearTimeout(timeoutId);
1166
+ }
1167
+ };
1168
+ const EXTENDS_LOCAL_PATH_PREFIXES = [
1169
+ "./",
1170
+ "../",
1171
+ "/"
1172
+ ];
1173
+ const isLocalPathExtend = (entry) => {
1174
+ for (const prefix of EXTENDS_LOCAL_PATH_PREFIXES) if (entry.startsWith(prefix)) return true;
1175
+ return false;
1176
+ };
1177
+ const stripJsoncComments = (raw) => {
1178
+ let result = "";
1179
+ let cursor = 0;
1180
+ let inString = false;
1181
+ let stringQuote = "";
1182
+ while (cursor < raw.length) {
1183
+ const character = raw[cursor];
1184
+ const nextCharacter = raw[cursor + 1];
1185
+ if (inString) {
1186
+ result += character;
1187
+ if (character === "\\" && cursor + 1 < raw.length) {
1188
+ result += nextCharacter;
1189
+ cursor += 2;
1190
+ continue;
1191
+ }
1192
+ if (character === stringQuote) inString = false;
1193
+ cursor += 1;
1194
+ continue;
1195
+ }
1196
+ if (character === "\"" || character === "'") {
1197
+ inString = true;
1198
+ stringQuote = character;
1199
+ result += character;
1200
+ cursor += 1;
1201
+ continue;
1202
+ }
1203
+ if (character === "/" && nextCharacter === "/") {
1204
+ const lineEndIndex = raw.indexOf("\n", cursor);
1205
+ cursor = lineEndIndex === -1 ? raw.length : lineEndIndex;
1206
+ continue;
1207
+ }
1208
+ if (character === "/" && nextCharacter === "*") {
1209
+ const blockEndIndex = raw.indexOf("*/", cursor + 2);
1210
+ cursor = blockEndIndex === -1 ? raw.length : blockEndIndex + 2;
1211
+ continue;
1212
+ }
1213
+ result += character;
1214
+ cursor += 1;
1215
+ }
1216
+ return result;
1217
+ };
1218
+ const parseJsonOrJsonc = (raw) => {
1219
+ try {
1220
+ return JSON.parse(raw);
1221
+ } catch {
1222
+ return JSON.parse(stripJsoncComments(raw));
1223
+ }
1224
+ };
1225
+ const canOxlintExtendConfig = (configPath) => {
1226
+ if (!configPath.endsWith(".eslintrc.json")) return true;
1227
+ let parsed;
1228
+ try {
1229
+ parsed = parseJsonOrJsonc(fs.readFileSync(configPath, "utf-8"));
1230
+ } catch {
1231
+ return true;
1232
+ }
1233
+ if (!isPlainObject(parsed)) return true;
1234
+ const extendsValue = parsed.extends;
1235
+ if (extendsValue === void 0 || extendsValue === null) return true;
1236
+ const extendsEntries = Array.isArray(extendsValue) ? extendsValue : [extendsValue];
1237
+ if (extendsEntries.length === 0) return true;
1238
+ return extendsEntries.some((entry) => typeof entry === "string" && isLocalPathExtend(entry));
1239
+ };
1240
+ const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
1241
+ const REDUCED_MOTION_FILE_GLOBS = [
1242
+ "*.ts",
1243
+ "*.tsx",
1244
+ "*.js",
1245
+ "*.jsx",
1246
+ "*.css",
1247
+ "*.scss"
1248
+ ];
1249
+ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
1250
+ filePath: "package.json",
1251
+ plugin: "react-doctor",
1252
+ rule: "require-reduced-motion",
1253
+ severity: "error",
1254
+ message: "Project uses a motion library but has no prefers-reduced-motion handling — required for accessibility (WCAG 2.3.3)",
1255
+ help: "Add `useReducedMotion()` from your animation library, or a `@media (prefers-reduced-motion: reduce)` CSS query",
1256
+ line: 0,
1257
+ column: 0,
1258
+ category: "Accessibility"
1259
+ };
1260
+ const checkReducedMotion = (rootDirectory) => {
1261
+ const packageJsonPath = path.join(rootDirectory, "package.json");
1262
+ if (!isFile(packageJsonPath)) return [];
1263
+ let hasMotionLibrary = false;
1264
+ try {
1265
+ const packageJson = readPackageJson(packageJsonPath);
1266
+ const allDependencies = {
1267
+ ...packageJson.dependencies,
1268
+ ...packageJson.devDependencies
1269
+ };
1270
+ hasMotionLibrary = Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName));
1271
+ } catch {
1272
+ return [];
1273
+ }
1274
+ if (!hasMotionLibrary) return [];
1275
+ const result = spawnSync("git", [
1276
+ "grep",
1277
+ "-ql",
1278
+ "-E",
1279
+ REDUCED_MOTION_GREP_PATTERN,
1280
+ "--",
1281
+ ...REDUCED_MOTION_FILE_GLOBS
1282
+ ], {
1283
+ cwd: rootDirectory,
1284
+ stdio: [
1285
+ "ignore",
1286
+ "pipe",
1287
+ "pipe"
1288
+ ]
1289
+ });
1290
+ if (result.error) return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
1291
+ if (result.status === 0) return [];
1292
+ return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
1293
+ };
1294
+ const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
1295
+ const FALSY_VALUES = new Set([
1296
+ "false",
1297
+ "0",
1298
+ "off",
1299
+ "no"
1300
+ ]);
1301
+ const isTruthyLinguistAttribute = (token) => {
1302
+ const match = LINGUIST_ATTRIBUTE_PATTERN.exec(token);
1303
+ if (!match) return false;
1304
+ if (match[1] === void 0) return true;
1305
+ return !FALSY_VALUES.has(match[1].toLowerCase());
1306
+ };
1307
+ const parseGitattributesLinguistPaths = (filePath) => {
1308
+ let content;
1309
+ try {
1310
+ content = fs.readFileSync(filePath, "utf-8");
1311
+ } catch {
1312
+ return [];
1313
+ }
1314
+ const paths = [];
1315
+ for (const rawLine of content.split("\n")) {
1316
+ const line = rawLine.trim();
1317
+ if (line.length === 0 || line.startsWith("#")) continue;
1318
+ const tokens = line.split(/\s+/);
1319
+ if (tokens.length < 2) continue;
1320
+ const [pathSpec, ...attributes] = tokens;
1321
+ if (attributes.some(isTruthyLinguistAttribute)) paths.push(pathSpec);
1322
+ }
1323
+ return paths;
1324
+ };
1325
+ var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
1326
+ let p = process || {}, argv = p.argv || [], env = p.env || {};
1327
+ let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
1328
+ let formatter = (open, close, replace = open) => (input) => {
1329
+ let string = "" + input, index = string.indexOf(close, open.length);
1330
+ return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
1331
+ };
1332
+ let replaceClose = (string, close, replace, index) => {
1333
+ let result = "", cursor = 0;
1334
+ do {
1335
+ result += string.substring(cursor, index) + replace;
1336
+ cursor = index + close.length;
1337
+ index = string.indexOf(close, cursor);
1338
+ } while (~index);
1339
+ return result + string.substring(cursor);
1340
+ };
1341
+ let createColors = (enabled = isColorSupported) => {
1342
+ let f = enabled ? formatter : () => String;
1343
+ return {
1344
+ isColorSupported: enabled,
1345
+ reset: f("\x1B[0m", "\x1B[0m"),
1346
+ bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
1347
+ dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
1348
+ italic: f("\x1B[3m", "\x1B[23m"),
1349
+ underline: f("\x1B[4m", "\x1B[24m"),
1350
+ inverse: f("\x1B[7m", "\x1B[27m"),
1351
+ hidden: f("\x1B[8m", "\x1B[28m"),
1352
+ strikethrough: f("\x1B[9m", "\x1B[29m"),
1353
+ black: f("\x1B[30m", "\x1B[39m"),
1354
+ red: f("\x1B[31m", "\x1B[39m"),
1355
+ green: f("\x1B[32m", "\x1B[39m"),
1356
+ yellow: f("\x1B[33m", "\x1B[39m"),
1357
+ blue: f("\x1B[34m", "\x1B[39m"),
1358
+ magenta: f("\x1B[35m", "\x1B[39m"),
1359
+ cyan: f("\x1B[36m", "\x1B[39m"),
1360
+ white: f("\x1B[37m", "\x1B[39m"),
1361
+ gray: f("\x1B[90m", "\x1B[39m"),
1362
+ bgBlack: f("\x1B[40m", "\x1B[49m"),
1363
+ bgRed: f("\x1B[41m", "\x1B[49m"),
1364
+ bgGreen: f("\x1B[42m", "\x1B[49m"),
1365
+ bgYellow: f("\x1B[43m", "\x1B[49m"),
1366
+ bgBlue: f("\x1B[44m", "\x1B[49m"),
1367
+ bgMagenta: f("\x1B[45m", "\x1B[49m"),
1368
+ bgCyan: f("\x1B[46m", "\x1B[49m"),
1369
+ bgWhite: f("\x1B[47m", "\x1B[49m"),
1370
+ blackBright: f("\x1B[90m", "\x1B[39m"),
1371
+ redBright: f("\x1B[91m", "\x1B[39m"),
1372
+ greenBright: f("\x1B[92m", "\x1B[39m"),
1373
+ yellowBright: f("\x1B[93m", "\x1B[39m"),
1374
+ blueBright: f("\x1B[94m", "\x1B[39m"),
1375
+ magentaBright: f("\x1B[95m", "\x1B[39m"),
1376
+ cyanBright: f("\x1B[96m", "\x1B[39m"),
1377
+ whiteBright: f("\x1B[97m", "\x1B[39m"),
1378
+ bgBlackBright: f("\x1B[100m", "\x1B[49m"),
1379
+ bgRedBright: f("\x1B[101m", "\x1B[49m"),
1380
+ bgGreenBright: f("\x1B[102m", "\x1B[49m"),
1381
+ bgYellowBright: f("\x1B[103m", "\x1B[49m"),
1382
+ bgBlueBright: f("\x1B[104m", "\x1B[49m"),
1383
+ bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
1384
+ bgCyanBright: f("\x1B[106m", "\x1B[49m"),
1385
+ bgWhiteBright: f("\x1B[107m", "\x1B[49m")
1386
+ };
1387
+ };
1388
+ module.exports = createColors();
1389
+ module.exports.createColors = createColors;
1390
+ })))(), 1);
1391
+ const highlighter = {
1392
+ error: import_picocolors.default.red,
1393
+ warn: import_picocolors.default.yellow,
1394
+ info: import_picocolors.default.cyan,
1395
+ success: import_picocolors.default.green,
1396
+ dim: import_picocolors.default.dim,
1397
+ gray: import_picocolors.default.gray,
1398
+ bold: import_picocolors.default.bold
1399
+ };
1400
+ let isSilent$1 = false;
1401
+ const setLoggerSilent = (silent) => {
1402
+ isSilent$1 = silent;
1403
+ };
1404
+ const isLoggerSilent = () => isSilent$1;
1405
+ const logger = {
1406
+ error(...args) {
1407
+ if (isSilent$1) return;
1408
+ console.error(highlighter.error(args.join(" ")));
1409
+ },
1410
+ warn(...args) {
1411
+ if (isSilent$1) return;
1412
+ console.warn(highlighter.warn(args.join(" ")));
1413
+ },
1414
+ info(...args) {
1415
+ if (isSilent$1) return;
1416
+ console.log(highlighter.info(args.join(" ")));
1417
+ },
1418
+ success(...args) {
1419
+ if (isSilent$1) return;
1420
+ console.log(highlighter.success(args.join(" ")));
1421
+ },
1422
+ dim(...args) {
1423
+ if (isSilent$1) return;
1424
+ console.log(highlighter.dim(args.join(" ")));
1425
+ },
1426
+ log(...args) {
1427
+ if (isSilent$1) return;
1428
+ console.log(args.join(" "));
1429
+ },
1430
+ break() {
1431
+ if (isSilent$1) return;
1432
+ console.log("");
1433
+ }
1434
+ };
1435
+ const stripGitignoreEscape = (pattern) => {
1436
+ if (pattern.startsWith("\\#") || pattern.startsWith("\\!")) return pattern.slice(1);
1437
+ return pattern;
1438
+ };
1439
+ const readIgnoreFile = (filePath) => {
1440
+ let content;
1441
+ try {
1442
+ content = fs.readFileSync(filePath, "utf-8");
1443
+ } catch (error) {
1444
+ const errnoCode = error?.code;
1445
+ if (errnoCode && errnoCode !== "ENOENT") logger.warn(`Could not read ignore file ${filePath}: ${errnoCode}`);
1446
+ return [];
1447
+ }
1448
+ const patterns = [];
1449
+ for (const line of content.split("\n")) {
1450
+ const trimmed = line.trim();
1451
+ if (trimmed.length === 0) continue;
1452
+ if (trimmed.startsWith("#")) continue;
1453
+ patterns.push(stripGitignoreEscape(trimmed));
1454
+ }
1455
+ return patterns;
1456
+ };
1457
+ const IGNORE_FILENAMES = [
1458
+ ".eslintignore",
1459
+ ".oxlintignore",
1460
+ ".prettierignore"
1461
+ ];
1462
+ const cachedPatternsByRoot = /* @__PURE__ */ new Map();
1463
+ const computeIgnorePatterns = (rootDirectory) => {
1464
+ const seen = /* @__PURE__ */ new Set();
1465
+ const patterns = [];
1466
+ const addPattern = (pattern) => {
1467
+ if (seen.has(pattern)) return;
1468
+ seen.add(pattern);
1469
+ patterns.push(pattern);
1470
+ };
1471
+ for (const filename of IGNORE_FILENAMES) for (const pattern of readIgnoreFile(path.join(rootDirectory, filename))) addPattern(pattern);
1472
+ for (const linguistPath of parseGitattributesLinguistPaths(path.join(rootDirectory, ".gitattributes"))) addPattern(linguistPath);
1473
+ return patterns;
1474
+ };
1475
+ const collectIgnorePatterns = (rootDirectory) => {
1476
+ const cached = cachedPatternsByRoot.get(rootDirectory);
1477
+ if (cached !== void 0) return cached;
1478
+ const patterns = computeIgnorePatterns(rootDirectory);
1479
+ cachedPatternsByRoot.set(rootDirectory, patterns);
1480
+ return patterns;
1481
+ };
1482
+ const createNodeReadFileLinesSync = (rootDirectory) => {
1483
+ return (filePath) => {
1484
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
1485
+ try {
1486
+ return fs.readFileSync(absolutePath, "utf-8").split("\n");
1487
+ } catch {
1488
+ return null;
1489
+ }
1490
+ };
1491
+ };
1492
+ const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
1493
+ const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
1494
+ const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
1495
+ let stringDelimiter = null;
1496
+ for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
1497
+ const character = line[charIndex];
1498
+ if (stringDelimiter !== null) {
1499
+ if (character === "\\") {
1500
+ charIndex++;
1501
+ continue;
1502
+ }
1503
+ if (character === stringDelimiter) stringDelimiter = null;
1504
+ continue;
1505
+ }
1506
+ if (character === "\"" || character === "'" || character === "`") {
1507
+ stringDelimiter = character;
1508
+ continue;
1509
+ }
1510
+ if (character === "/" && line[charIndex + 1] === "/") return true;
1511
+ }
1512
+ return false;
1513
+ };
1514
+ const findOpenerTagOnLine = (line) => {
1515
+ for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
1516
+ if (match.index === void 0) continue;
1517
+ if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
1518
+ }
1519
+ return null;
1520
+ };
1521
+ const findJsxOpenerSpan = (lines, openerLineIndex) => {
1522
+ const openerLine = lines[openerLineIndex];
1523
+ if (openerLine === void 0) return null;
1524
+ const opener = findOpenerTagOnLine(openerLine);
1525
+ if (!opener) return null;
1526
+ const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
1527
+ let braceDepth = 0;
1528
+ let innerAngleDepth = 0;
1529
+ let stringDelimiter = null;
1530
+ for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
1531
+ const currentLine = lines[lineIndex];
1532
+ const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
1533
+ for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
1534
+ const character = currentLine[charIndex];
1535
+ if (stringDelimiter !== null) {
1536
+ if (character === "\\") {
1537
+ charIndex++;
1538
+ continue;
1539
+ }
1540
+ if (character === stringDelimiter) stringDelimiter = null;
1541
+ continue;
1542
+ }
1543
+ if (character === "\"" || character === "'" || character === "`") {
1544
+ stringDelimiter = character;
1545
+ continue;
1546
+ }
1547
+ if (character === "{") {
1548
+ braceDepth++;
1549
+ continue;
1550
+ }
1551
+ if (character === "}") {
1552
+ braceDepth--;
1553
+ continue;
1554
+ }
1555
+ if (braceDepth !== 0) continue;
1556
+ if (character === "<") {
1557
+ const followCharacter = currentLine[charIndex + 1];
1558
+ if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
1559
+ continue;
1560
+ }
1561
+ if (character !== ">") continue;
1562
+ const previousCharacter = currentLine[charIndex - 1];
1563
+ const nextCharacter = currentLine[charIndex + 1];
1564
+ if (previousCharacter === "=" || nextCharacter === "=") continue;
1565
+ if (innerAngleDepth > 0) {
1566
+ innerAngleDepth--;
1567
+ continue;
1568
+ }
1569
+ return lineIndex;
1570
+ }
1571
+ }
1572
+ return null;
1573
+ };
1574
+ const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
1575
+ for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
1576
+ const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
1577
+ if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
1578
+ }
1579
+ return null;
1580
+ };
1581
+ const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
1582
+ const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
1583
+ const collected = [];
1584
+ let isStillInChain = true;
1585
+ for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
1586
+ const candidateLine = lines[candidateIndex];
1587
+ if (candidateLine === void 0) break;
1588
+ const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
1589
+ if (match) {
1590
+ collected.push({
1591
+ commentLineIndex: candidateIndex,
1592
+ ruleList: match[1],
1593
+ isInChain: isStillInChain
1594
+ });
1595
+ continue;
1596
+ }
1597
+ isStillInChain = false;
1598
+ }
1599
+ return collected;
1600
+ };
1601
+ const stripDescriptionTail = (ruleList) => {
1602
+ const descriptionMatch = ruleList.match(/(?:^|\s)--\s/);
1603
+ if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
1604
+ return ruleList.slice(0, descriptionMatch.index);
1605
+ };
1606
+ const isRuleListedInComment = (ruleList, ruleId) => {
1607
+ const trimmed = ruleList?.trim();
1608
+ if (!trimmed) return true;
1609
+ const ruleSection = stripDescriptionTail(trimmed).trim();
1610
+ if (!ruleSection) return true;
1611
+ return ruleSection.split(/[,\s]+/).some((token) => token.trim() === ruleId);
1612
+ };
1613
+ const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
1614
+ const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
1615
+ const hasChainSuppressor = (comments, ruleId) => comments.some((comment) => comment.isInChain && isRuleListedInComment(comment.ruleList, ruleId));
1616
+ const findAdjacentRuleListMismatch = (comments, ruleId) => comments.find((comment) => comment.isInChain && Boolean(comment.ruleList?.trim()) && !isRuleListedInComment(comment.ruleList, ruleId));
1617
+ const findOutOfChainMatch = (comments, ruleId) => comments.find((comment) => !comment.isInChain && isRuleListedInComment(comment.ruleList, ruleId));
1618
+ const buildAdjacentMismatchHint = (comment, ruleId) => {
1619
+ const ruleListText = comment.ruleList?.trim() ?? "";
1620
+ return `An adjacent react-doctor-disable-next-line at line ${comment.commentLineIndex + 1} lists "${ruleListText}" — ${ruleId} is not in that list. Use the comma form: react-doctor-disable-next-line ${ruleListText}, ${ruleId}`;
1621
+ };
1622
+ const buildGapHint = (comment, diagnosticLineIndex, ruleId) => {
1623
+ const commentLineNumber = comment.commentLineIndex + 1;
1624
+ const diagnosticLineNumber = diagnosticLineIndex + 1;
1625
+ return `A react-doctor-disable-next-line for ${ruleId} sits at line ${commentLineNumber}, but ${formatLineGap(diagnosticLineNumber - commentLineNumber - 1)} of code separate it from the diagnostic on line ${diagnosticLineNumber}. Move the comment immediately above line ${diagnosticLineNumber}, or extract the surrounding code into a helper so the suppression is adjacent.`;
1626
+ };
1627
+ const classifyFromComments = (commentsByAnchor, diagnosticLineIndex, ruleId) => {
1628
+ for (const comments of commentsByAnchor) {
1629
+ const adjacentMismatch = findAdjacentRuleListMismatch(comments, ruleId);
1630
+ if (adjacentMismatch) return buildAdjacentMismatchHint(adjacentMismatch, ruleId);
1631
+ const outOfChainMatch = findOutOfChainMatch(comments, ruleId);
1632
+ if (outOfChainMatch) return buildGapHint(outOfChainMatch, diagnosticLineIndex, ruleId);
1633
+ }
1634
+ return null;
1635
+ };
1636
+ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
1637
+ const sameLineMatch = lines[diagnosticLineIndex]?.match(DISABLE_LINE_PATTERN);
1638
+ if (sameLineMatch && isRuleListedInComment(sameLineMatch[1], ruleId)) return {
1639
+ isSuppressed: true,
1640
+ nearMissHint: null
1641
+ };
1642
+ const directComments = findStackedDisableCommentsAbove(lines, diagnosticLineIndex);
1643
+ if (hasChainSuppressor(directComments, ruleId)) return {
1644
+ isSuppressed: true,
1645
+ nearMissHint: null
1646
+ };
1647
+ const openerStartIndex = findEnclosingMultilineJsxOpenerStart(lines, diagnosticLineIndex);
1648
+ const openerComments = openerStartIndex !== null && openerStartIndex > 0 ? findStackedDisableCommentsAbove(lines, openerStartIndex) : [];
1649
+ if (hasChainSuppressor(openerComments, ruleId)) return {
1650
+ isSuppressed: true,
1651
+ nearMissHint: null
1652
+ };
1653
+ return {
1654
+ isSuppressed: false,
1655
+ nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId)
1656
+ };
1657
+ };
1658
+ const compileIgnoredFilePatterns = (userConfig) => {
1659
+ const files = userConfig?.ignore?.files;
1660
+ if (!Array.isArray(files)) return [];
1661
+ return files.filter((entry) => typeof entry === "string").map(compileGlobPattern);
1662
+ };
1663
+ const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
1664
+ if (patterns.length === 0) return false;
1665
+ const relativePath = toRelativePath(filePath, rootDirectory);
1666
+ return patterns.some((pattern) => pattern.test(relativePath));
1667
+ };
1668
+ const OPENING_TAG_PATTERN = /<([A-Z][\w.]*)/;
1669
+ const JSX_CHILD_OPEN_PATTERN = /<[A-Za-z]/;
1670
+ const escapeRegExpSpecials = (rawText) => rawText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1671
+ const resolveCandidateReadPath = (rootDirectory, filePath) => {
1672
+ const normalizedFile = filePath.replace(/\\/g, "/");
1673
+ if (normalizedFile.startsWith("/") || /^[a-zA-Z]:\//.test(normalizedFile) || /^[a-zA-Z]:\\/.test(filePath)) return filePath;
1674
+ return `${rootDirectory.replace(/\\/g, "/").replace(/\/$/, "")}/${normalizedFile.replace(/^\.\//, "")}`;
1675
+ };
1676
+ const createFileLinesCache = (rootDirectory, readFileLinesSync) => {
1677
+ const cache = /* @__PURE__ */ new Map();
1678
+ return (filePath) => {
1679
+ const cached = cache.get(filePath);
1680
+ if (cached !== void 0) return cached;
1681
+ const lines = readFileLinesSync(resolveCandidateReadPath(rootDirectory, filePath));
1682
+ cache.set(filePath, lines);
1683
+ return lines;
1684
+ };
1685
+ };
1686
+ const isInsideTextComponent = (lines, diagnosticLine, textComponentNames) => {
1687
+ for (let lineIndex = diagnosticLine - 1; lineIndex >= 0; lineIndex--) {
1688
+ const match = lines[lineIndex].match(OPENING_TAG_PATTERN);
1689
+ if (!match) continue;
1690
+ const fullTagName = match[1];
1691
+ const leafTagName = fullTagName.includes(".") ? fullTagName.split(".").at(-1) ?? fullTagName : fullTagName;
1692
+ return textComponentNames.has(fullTagName) || textComponentNames.has(leafTagName);
1693
+ }
1694
+ return false;
1695
+ };
1696
+ const findOpenerAtOrAbove = (lines, upperBoundLineIndex) => {
1697
+ for (let lineIndex = upperBoundLineIndex; lineIndex >= 0; lineIndex--) {
1698
+ const match = lines[lineIndex].match(OPENING_TAG_PATTERN);
1699
+ if (!match) continue;
1700
+ const fullName = match[1];
1701
+ return {
1702
+ fullName,
1703
+ leafName: fullName.includes(".") ? fullName.split(".").at(-1) ?? fullName : fullName,
1704
+ lineIndex
1705
+ };
1706
+ }
1707
+ return null;
1708
+ };
1709
+ const resolveJsxRange = (lines, opener) => {
1710
+ const closingPattern = new RegExp(`</(?:${escapeRegExpSpecials(opener.fullName)}|${escapeRegExpSpecials(opener.leafName)})\\s*>`);
1711
+ let closerLineIndex = -1;
1712
+ let closerColumn = -1;
1713
+ for (let lineIndex = opener.lineIndex; lineIndex < lines.length; lineIndex++) {
1714
+ const match = closingPattern.exec(lines[lineIndex]);
1715
+ if (!match) continue;
1716
+ closerLineIndex = lineIndex;
1717
+ closerColumn = match.index;
1718
+ break;
1719
+ }
1720
+ if (closerLineIndex < 0) return null;
1721
+ const openerLine = lines[opener.lineIndex];
1722
+ const tagStartIndex = openerLine.indexOf(`<${opener.fullName}`);
1723
+ if (tagStartIndex < 0) return null;
1724
+ const openerEndIndex = openerLine.indexOf(">", tagStartIndex);
1725
+ let bodyText;
1726
+ if (opener.lineIndex === closerLineIndex) {
1727
+ if (openerEndIndex < 0 || openerEndIndex >= closerColumn) return null;
1728
+ bodyText = openerLine.slice(openerEndIndex + 1, closerColumn);
1729
+ } else {
1730
+ const segments = [];
1731
+ if (openerEndIndex >= 0) segments.push(openerLine.slice(openerEndIndex + 1));
1732
+ for (let lineIndex = opener.lineIndex + 1; lineIndex < closerLineIndex; lineIndex++) segments.push(lines[lineIndex]);
1733
+ segments.push(lines[closerLineIndex].slice(0, closerColumn));
1734
+ bodyText = segments.join("\n");
1735
+ }
1736
+ return {
1737
+ closerLineIndex,
1738
+ closerColumn,
1739
+ bodyText
1740
+ };
1741
+ };
1742
+ const isInsideStringOnlyWrapper = (lines, diagnosticLine, diagnosticColumn, wrapperNames) => {
1743
+ const diagnosticLineIndex = diagnosticLine - 1;
1744
+ const diagnosticColumnIndex = Math.max(0, diagnosticColumn - 1);
1745
+ let upperBoundLineIndex = diagnosticLineIndex;
1746
+ while (upperBoundLineIndex >= 0) {
1747
+ const opener = findOpenerAtOrAbove(lines, upperBoundLineIndex);
1748
+ if (!opener) return false;
1749
+ const range = resolveJsxRange(lines, opener);
1750
+ if (range === null) {
1751
+ upperBoundLineIndex = opener.lineIndex - 1;
1752
+ continue;
1753
+ }
1754
+ if (range.closerLineIndex < diagnosticLineIndex || range.closerLineIndex === diagnosticLineIndex && range.closerColumn <= diagnosticColumnIndex) {
1755
+ upperBoundLineIndex = opener.lineIndex - 1;
1756
+ continue;
1757
+ }
1758
+ if (!wrapperNames.has(opener.fullName) && !wrapperNames.has(opener.leafName)) return false;
1759
+ return !JSX_CHILD_OPEN_PATTERN.test(range.bodyText);
1760
+ }
1761
+ return false;
1762
+ };
1763
+ const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLinesSync) => {
1764
+ const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules.filter((rule) => typeof rule === "string") : []);
1765
+ const ignoredFilePatterns = compileIgnoredFilePatterns(config);
1766
+ const compiledOverrides = compileIgnoreOverrides(config);
1767
+ const textComponentNames = new Set(Array.isArray(config.textComponents) ? config.textComponents.filter((name) => typeof name === "string") : []);
1768
+ const hasTextComponents = textComponentNames.size > 0;
1769
+ const rawTextWrapperComponentNames = new Set(Array.isArray(config.rawTextWrapperComponents) ? config.rawTextWrapperComponents.filter((name) => typeof name === "string") : []);
1770
+ const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
1771
+ const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);
1772
+ return diagnostics.filter((diagnostic) => {
1773
+ const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
1774
+ if (ignoredRules.has(ruleIdentifier)) return false;
1775
+ if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
1776
+ if (isDiagnosticIgnoredByOverrides(diagnostic, rootDirectory, compiledOverrides)) return false;
1777
+ if ((hasTextComponents || hasRawTextWrappers) && diagnostic.rule === "rn-no-raw-text" && diagnostic.line > 0) {
1778
+ const lines = getFileLines(diagnostic.filePath);
1779
+ if (lines) {
1780
+ if (hasTextComponents && isInsideTextComponent(lines, diagnostic.line, textComponentNames)) return false;
1781
+ if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return false;
1782
+ }
1783
+ }
1784
+ return true;
1785
+ });
1786
+ };
1787
+ const filterInlineSuppressions = (diagnostics, rootDirectory, readFileLinesSync) => {
1788
+ const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);
1789
+ return diagnostics.flatMap((diagnostic) => {
1790
+ if (diagnostic.line <= 0) return [diagnostic];
1791
+ const lines = getFileLines(diagnostic.filePath);
1792
+ if (!lines) return [diagnostic];
1793
+ const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
1794
+ const evaluation = evaluateSuppression(lines, diagnostic.line - 1, ruleIdentifier);
1795
+ if (evaluation.isSuppressed) return [];
1796
+ return evaluation.nearMissHint ? [{
1797
+ ...diagnostic,
1798
+ suppressionHint: evaluation.nearMissHint
1799
+ }] : [diagnostic];
1800
+ });
1801
+ };
1802
+ const TEST_FILE_PATH_PATTERN = /(?:^|\/)(?:__tests__|__test__|tests|test|__mocks__|cypress|e2e|playwright)\/|\.(?:test|spec|stories|story|fixture|fixtures)\.(?:[cm]?[jt]sx?)$/;
1803
+ const isTestFilePath = (relativePath) => {
1804
+ if (relativePath.length === 0) return false;
1805
+ const forwardSlashed = relativePath.replaceAll("\\", "/");
1806
+ return TEST_FILE_PATH_PATTERN.test(forwardSlashed);
1807
+ };
1808
+ const testFileResultCache = /* @__PURE__ */ new Map();
1809
+ const shouldAutoSuppress = (diagnostic) => {
1810
+ const filePath = diagnostic.filePath;
1811
+ if ((diagnostic.plugin === "react-doctor" ? reactDoctorPlugin.rules[diagnostic.rule] : null)?.tags?.includes("test-noise")) {
1812
+ let isTest = testFileResultCache.get(filePath);
1813
+ if (isTest === void 0) {
1814
+ isTest = isTestFilePath(filePath);
1815
+ testFileResultCache.set(filePath, isTest);
1816
+ }
1817
+ if (isTest) return true;
1818
+ }
1819
+ return false;
1820
+ };
1821
+ const mergeAndFilterDiagnostics = (mergedDiagnostics, directory, userConfig, readFileLinesSync, options = {}) => {
1822
+ const autoFiltered = mergedDiagnostics.filter((diagnostic) => !shouldAutoSuppress(diagnostic));
1823
+ const filtered = userConfig ? filterIgnoredDiagnostics(autoFiltered, userConfig, directory, readFileLinesSync) : autoFiltered;
1824
+ if (options.respectInlineDisables === false) return filtered;
1825
+ return filterInlineSuppressions(filtered, directory, readFileLinesSync);
1826
+ };
1827
+ const combineDiagnostics = (input) => {
1828
+ const { lintDiagnostics, directory, isDiffMode, userConfig, readFileLinesSync = createNodeReadFileLinesSync(directory), includeEnvironmentChecks = true, respectInlineDisables } = input;
1829
+ const extraDiagnostics = isDiffMode || !includeEnvironmentChecks ? [] : checkReducedMotion(directory);
1830
+ return mergeAndFilterDiagnostics([...lintDiagnostics, ...extraDiagnostics], directory, userConfig, readFileLinesSync, { respectInlineDisables });
1831
+ };
1832
+ const findFirstLintConfigInDirectory = (directory) => {
1833
+ for (const filename of ADOPTABLE_LINT_CONFIG_FILENAMES) {
1834
+ const candidatePath = path.join(directory, filename);
1835
+ if (isFile(candidatePath)) return candidatePath;
1836
+ }
1837
+ return null;
1838
+ };
1839
+ const isProjectBoundary$1 = (directory) => fs.existsSync(path.join(directory, ".git")) || isMonorepoRoot(directory);
1840
+ const detectUserLintConfigPaths = (rootDirectory) => {
1841
+ const directLintConfig = findFirstLintConfigInDirectory(rootDirectory);
1842
+ if (directLintConfig) return [directLintConfig];
1843
+ if (isProjectBoundary$1(rootDirectory)) return [];
1844
+ let ancestorDirectory = path.dirname(rootDirectory);
1845
+ while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
1846
+ const ancestorLintConfig = findFirstLintConfigInDirectory(ancestorDirectory);
1847
+ if (ancestorLintConfig) return [ancestorLintConfig];
1848
+ if (isProjectBoundary$1(ancestorDirectory)) return [];
1849
+ ancestorDirectory = path.dirname(ancestorDirectory);
1850
+ }
1851
+ return [];
1852
+ };
1853
+ const runGit = (cwd, args) => {
1854
+ const result = spawnSync("git", args, {
1855
+ cwd,
1856
+ stdio: [
1857
+ "ignore",
1858
+ "pipe",
1859
+ "pipe"
1860
+ ],
1861
+ encoding: "utf-8"
1862
+ });
1863
+ if (result.error || result.status !== 0) return null;
1864
+ return result.stdout.toString().trim();
1865
+ };
1866
+ const getCurrentBranch = (directory) => {
1867
+ const branch = runGit(directory, [
1868
+ "rev-parse",
1869
+ "--abbrev-ref",
1870
+ "HEAD"
1871
+ ]);
1872
+ if (!branch) return null;
1873
+ return branch === "HEAD" ? null : branch;
1874
+ };
1875
+ const detectDefaultBranch = (directory) => {
1876
+ const reference = runGit(directory, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
1877
+ if (reference) return reference.replace("refs/remotes/origin/", "");
1878
+ const output = runGit(directory, [
1879
+ "for-each-ref",
1880
+ "--format=%(refname:short)",
1881
+ ...DEFAULT_BRANCH_CANDIDATES.map((candidate) => `refs/heads/${candidate}`)
1882
+ ]);
1883
+ if (output) {
1884
+ const firstLine = output.split("\n")[0]?.trim();
1885
+ if (firstLine) return firstLine;
1886
+ }
1887
+ return null;
1888
+ };
1889
+ const branchExists = (directory, branch) => {
1890
+ const result = spawnSync("git", [
1891
+ "rev-parse",
1892
+ "--verify",
1893
+ branch
1894
+ ], {
1895
+ cwd: directory,
1896
+ stdio: [
1897
+ "ignore",
1898
+ "pipe",
1899
+ "pipe"
1900
+ ]
1901
+ });
1902
+ return !result.error && result.status === 0;
1903
+ };
1904
+ const runGitNullSeparated = (cwd, args) => {
1905
+ const result = spawnSync("git", args, {
1906
+ cwd,
1907
+ stdio: [
1908
+ "ignore",
1909
+ "pipe",
1910
+ "pipe"
1911
+ ],
1912
+ encoding: "utf-8"
1913
+ });
1914
+ if (result.error || result.status !== 0) return null;
1915
+ return result.stdout.toString().split("\0").filter((filePath) => filePath.length > 0);
1916
+ };
1917
+ const getChangedFilesSinceBranch = (directory, baseBranch) => {
1918
+ const mergeBase = runGit(directory, [
1919
+ "merge-base",
1920
+ baseBranch,
1921
+ "HEAD"
1922
+ ]);
1923
+ if (mergeBase === null) return null;
1924
+ return runGitNullSeparated(directory, [
1925
+ "diff",
1926
+ "-z",
1927
+ "--name-only",
1928
+ "--diff-filter=ACMR",
1929
+ "--relative",
1930
+ mergeBase
1931
+ ]);
1932
+ };
1933
+ const getUncommittedChangedFiles = (directory) => {
1934
+ return runGitNullSeparated(directory, [
1935
+ "diff",
1936
+ "-z",
1937
+ "--name-only",
1938
+ "--diff-filter=ACMR",
1939
+ "--relative",
1940
+ "HEAD"
1941
+ ]) ?? [];
1942
+ };
1943
+ const getDiffInfo = (directory, explicitBaseBranch) => {
1944
+ if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) throw new Error("Diff base branch cannot be empty.");
1945
+ const currentBranch = getCurrentBranch(directory);
1946
+ if (!currentBranch) return null;
1947
+ const baseBranch = explicitBaseBranch ?? detectDefaultBranch(directory);
1948
+ if (!baseBranch) return null;
1949
+ if (explicitBaseBranch && !branchExists(directory, explicitBaseBranch)) throw new Error(`Diff base branch "${explicitBaseBranch}" does not exist (run \`git fetch\` to update remote refs).`);
1950
+ if (currentBranch === baseBranch) {
1951
+ const uncommittedFiles = getUncommittedChangedFiles(directory);
1952
+ if (uncommittedFiles.length === 0) return null;
1953
+ return {
1954
+ currentBranch,
1955
+ baseBranch,
1956
+ changedFiles: uncommittedFiles,
1957
+ isCurrentChanges: true
1958
+ };
1959
+ }
1960
+ const changedFiles = getChangedFilesSinceBranch(directory, baseBranch);
1961
+ if (changedFiles === null) return null;
1962
+ return {
1963
+ currentBranch,
1964
+ baseBranch,
1965
+ changedFiles
1966
+ };
1967
+ };
1968
+ const filterSourceFiles = (filePaths) => filePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));
1969
+ const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
1970
+ const BOOLEAN_FIELD_NAMES = [
1971
+ "lint",
1972
+ "verbose",
1973
+ "customRulesOnly",
1974
+ "share",
1975
+ "respectInlineDisables",
1976
+ "adoptExistingLintConfig",
1977
+ "offline"
1978
+ ];
1979
+ const STRING_FIELD_NAMES = ["rootDir"];
1980
+ const warnConfigField = (message) => {
1981
+ process.stderr.write(`[react-doctor] ${message}\n`);
1982
+ };
1983
+ const coerceMaybeBooleanString = (fieldName, value) => {
1984
+ if (typeof value === "boolean" || value === void 0) return value;
1985
+ if (value === "true") {
1986
+ warnConfigField(`config field "${fieldName}" is the string "true"; treating as boolean true.`);
1987
+ return true;
1988
+ }
1989
+ if (value === "false") {
1990
+ warnConfigField(`config field "${fieldName}" is the string "false"; treating as boolean false.`);
1991
+ return false;
1992
+ }
1993
+ warnConfigField(`config field "${fieldName}" must be a boolean (got ${typeof value}); ignoring this field.`);
1994
+ };
1995
+ const validateString = (fieldName, value) => {
1996
+ if (typeof value === "string") return value;
1997
+ warnConfigField(`config field "${fieldName}" must be a string (got ${typeof value}); ignoring this field.`);
1998
+ };
1999
+ const validateConfigTypes = (config) => {
2000
+ const validated = { ...config };
2001
+ for (const fieldName of BOOLEAN_FIELD_NAMES) {
2002
+ const original = config[fieldName];
2003
+ if (original === void 0) continue;
2004
+ const coerced = coerceMaybeBooleanString(fieldName, original);
2005
+ if (coerced === void 0) delete validated[fieldName];
2006
+ else validated[fieldName] = coerced;
2007
+ }
2008
+ for (const fieldName of STRING_FIELD_NAMES) {
2009
+ const original = config[fieldName];
2010
+ if (original === void 0) continue;
2011
+ const validatedString = validateString(fieldName, original);
2012
+ if (validatedString === void 0) delete validated[fieldName];
2013
+ else validated[fieldName] = validatedString;
2014
+ }
2015
+ return validated;
2016
+ };
2017
+ const CONFIG_FILENAME = "react-doctor.config.json";
2018
+ const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
2019
+ const loadConfigFromDirectory = (directory) => {
2020
+ const configFilePath = path.join(directory, CONFIG_FILENAME);
2021
+ if (isFile(configFilePath)) try {
2022
+ const fileContent = fs.readFileSync(configFilePath, "utf-8");
2023
+ const parsed = JSON.parse(fileContent);
2024
+ if (isPlainObject(parsed)) return {
2025
+ config: validateConfigTypes(parsed),
2026
+ sourceDirectory: directory
2027
+ };
2028
+ logger.warn(`${CONFIG_FILENAME} must be a JSON object, ignoring.`);
2029
+ } catch (error) {
2030
+ logger.warn(`Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
2031
+ }
2032
+ const packageJsonPath = path.join(directory, "package.json");
2033
+ if (isFile(packageJsonPath)) try {
2034
+ const fileContent = fs.readFileSync(packageJsonPath, "utf-8");
2035
+ const packageJson = JSON.parse(fileContent);
2036
+ if (isPlainObject(packageJson)) {
2037
+ const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];
2038
+ if (isPlainObject(embeddedConfig)) return {
2039
+ config: validateConfigTypes(embeddedConfig),
2040
+ sourceDirectory: directory
2041
+ };
2042
+ }
2043
+ } catch {
2044
+ return null;
2045
+ }
2046
+ return null;
2047
+ };
2048
+ const isProjectBoundary = (directory) => fs.existsSync(path.join(directory, ".git")) || isMonorepoRoot(directory);
2049
+ const cachedConfigs = /* @__PURE__ */ new Map();
2050
+ const loadConfigWithSource = (rootDirectory) => {
2051
+ const cached = cachedConfigs.get(rootDirectory);
2052
+ if (cached !== void 0) return cached;
2053
+ const localConfig = loadConfigFromDirectory(rootDirectory);
2054
+ if (localConfig) {
2055
+ cachedConfigs.set(rootDirectory, localConfig);
2056
+ return localConfig;
2057
+ }
2058
+ if (isProjectBoundary(rootDirectory)) {
2059
+ cachedConfigs.set(rootDirectory, null);
2060
+ return null;
2061
+ }
2062
+ let ancestorDirectory = path.dirname(rootDirectory);
2063
+ while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
2064
+ const ancestorConfig = loadConfigFromDirectory(ancestorDirectory);
2065
+ if (ancestorConfig) {
2066
+ cachedConfigs.set(rootDirectory, ancestorConfig);
2067
+ return ancestorConfig;
2068
+ }
2069
+ if (isProjectBoundary(ancestorDirectory)) {
2070
+ cachedConfigs.set(rootDirectory, null);
2071
+ return null;
2072
+ }
2073
+ ancestorDirectory = path.dirname(ancestorDirectory);
2074
+ }
2075
+ cachedConfigs.set(rootDirectory, null);
2076
+ return null;
2077
+ };
2078
+ const DISABLE_DIRECTIVE_PATTERN = /(eslint|oxlint)-disable/;
2079
+ const findFilesWithDisableDirectivesViaGit = (rootDirectory, includePaths) => {
2080
+ const grepArgs = [
2081
+ "grep",
2082
+ "-l",
2083
+ "--untracked",
2084
+ "-E",
2085
+ "(eslint|oxlint)-disable"
2086
+ ];
2087
+ if (includePaths && includePaths.length > 0) grepArgs.push("--", ...includePaths);
2088
+ const result = spawnSync("git", grepArgs, {
2089
+ cwd: rootDirectory,
2090
+ encoding: "utf-8",
2091
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
2092
+ });
2093
+ if (result.error || result.status === null) return null;
2094
+ if (result.status === 128) return null;
2095
+ return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
2096
+ };
2097
+ const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths) => {
2098
+ const matches = [];
2099
+ const checkFile = (relativePath) => {
2100
+ if (!SOURCE_FILE_PATTERN.test(relativePath)) return;
2101
+ const absolutePath = path.join(rootDirectory, relativePath);
2102
+ let content;
2103
+ try {
2104
+ content = fs.readFileSync(absolutePath, "utf-8");
2105
+ } catch {
2106
+ return;
2107
+ }
2108
+ if (DISABLE_DIRECTIVE_PATTERN.test(content)) matches.push(relativePath);
2109
+ };
2110
+ if (includePaths && includePaths.length > 0) {
2111
+ for (const candidate of includePaths) checkFile(candidate);
2112
+ return matches;
2113
+ }
2114
+ const stack = [rootDirectory];
2115
+ while (stack.length > 0) {
2116
+ const current = stack.pop();
2117
+ if (current === void 0) continue;
2118
+ let entries;
2119
+ try {
2120
+ entries = fs.readdirSync(current, { withFileTypes: true });
2121
+ } catch {
2122
+ continue;
2123
+ }
2124
+ for (const entry of entries) {
2125
+ if (entry.isDirectory()) {
2126
+ if (entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
2127
+ stack.push(path.join(current, entry.name));
2128
+ continue;
2129
+ }
2130
+ if (!entry.isFile()) continue;
2131
+ const absolute = path.join(current, entry.name);
2132
+ checkFile(path.relative(rootDirectory, absolute));
2133
+ }
2134
+ }
2135
+ return matches;
2136
+ };
2137
+ const findFilesWithDisableDirectives = (rootDirectory, includePaths) => findFilesWithDisableDirectivesViaGit(rootDirectory, includePaths) ?? findFilesWithDisableDirectivesViaFilesystem(rootDirectory, includePaths);
2138
+ const neutralizeContent = (content) => content.replaceAll("eslint-disable", "eslint_disable").replaceAll("oxlint-disable", "oxlint_disable");
2139
+ const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
2140
+ const filePaths = findFilesWithDisableDirectives(rootDirectory, includePaths);
2141
+ const originalContents = /* @__PURE__ */ new Map();
2142
+ let isRestored = false;
2143
+ const restore = () => {
2144
+ if (isRestored) return;
2145
+ isRestored = true;
2146
+ for (const [absolutePath, originalContent] of originalContents) try {
2147
+ fs.writeFileSync(absolutePath, originalContent);
2148
+ } catch (error) {
2149
+ process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${error instanceof Error ? error.message : String(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
2150
+ }
2151
+ };
2152
+ const onExit = () => restore();
2153
+ process.once("exit", onExit);
2154
+ for (const relativePath of filePaths) {
2155
+ const absolutePath = path.join(rootDirectory, relativePath);
2156
+ let originalContent;
2157
+ try {
2158
+ originalContent = fs.readFileSync(absolutePath, "utf-8");
2159
+ } catch {
2160
+ continue;
2161
+ }
2162
+ const neutralizedContent = neutralizeContent(originalContent);
2163
+ if (neutralizedContent !== originalContent) {
2164
+ originalContents.set(absolutePath, originalContent);
2165
+ fs.writeFileSync(absolutePath, neutralizedContent);
2166
+ }
2167
+ }
2168
+ return () => {
2169
+ restore();
2170
+ process.removeListener("exit", onExit);
2171
+ };
2172
+ };
2173
+ const parseNodeVersion = (versionString) => {
2174
+ const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
2175
+ return {
2176
+ major,
2177
+ minor,
2178
+ patch
2179
+ };
2180
+ };
2181
+ const isNodeVersionCompatibleWithOxlint = ({ major, minor }) => {
2182
+ if (major === 20 && minor >= 19) return true;
2183
+ if (major === 22 && minor >= 12) return true;
2184
+ if (major > 22) return true;
2185
+ return false;
2186
+ };
2187
+ const isCurrentNodeCompatibleWithOxlint = () => isNodeVersionCompatibleWithOxlint(parseNodeVersion(process.version));
2188
+ const getNvmDirectory = () => {
2189
+ const envNvmDirectory = process.env.NVM_DIR;
2190
+ if (envNvmDirectory && existsSync(envNvmDirectory)) return envNvmDirectory;
2191
+ const defaultNvmDirectory = path.join(os.homedir(), ".nvm");
2192
+ if (existsSync(defaultNvmDirectory)) return defaultNvmDirectory;
2193
+ return null;
2194
+ };
2195
+ const isNvmInstalled = () => getNvmDirectory() !== null;
2196
+ const findCompatibleNvmBinary = () => {
2197
+ const nvmDirectory = getNvmDirectory();
2198
+ if (!nvmDirectory) return null;
2199
+ const versionsDirectory = path.join(nvmDirectory, "versions", "node");
2200
+ if (!existsSync(versionsDirectory)) return null;
2201
+ const compatibleVersions = readdirSync(versionsDirectory).filter((directoryName) => directoryName.startsWith("v")).map((directoryName) => ({
2202
+ directoryName,
2203
+ ...parseNodeVersion(directoryName)
2204
+ })).filter((version) => isNodeVersionCompatibleWithOxlint(version)).sort((versionA, versionB) => versionB.major - versionA.major || versionB.minor - versionA.minor || versionB.patch - versionA.patch);
2205
+ if (compatibleVersions.length === 0) return null;
2206
+ const bestVersion = compatibleVersions[0];
2207
+ const binaryPath = path.join(versionsDirectory, bestVersion.directoryName, "bin", "node");
2208
+ return existsSync(binaryPath) ? binaryPath : null;
2209
+ };
2210
+ const getNodeVersionFromBinary = (binaryPath) => {
2211
+ const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
2212
+ if (result.error || result.status !== 0) return null;
2213
+ return result.stdout.toString().trim();
2214
+ };
2215
+ const installNodeViaNvm = () => {
2216
+ const nvmDirectory = getNvmDirectory();
2217
+ if (!nvmDirectory) return false;
2218
+ const nvmScript = path.join(nvmDirectory, "nvm.sh");
2219
+ if (!existsSync(nvmScript)) return false;
2220
+ const result = spawnSync("bash", ["-c", ". \"$NVM_SCRIPT\" && nvm install \"$NODE_MAJOR\""], {
2221
+ stdio: "inherit",
2222
+ env: {
2223
+ ...process.env,
2224
+ NVM_SCRIPT: nvmScript,
2225
+ NODE_MAJOR: String(24)
2226
+ }
2227
+ });
2228
+ if (result.error || result.status !== 0) return false;
2229
+ return findCompatibleNvmBinary() !== null;
2230
+ };
2231
+ const resolveNodeForOxlint = () => {
2232
+ if (isCurrentNodeCompatibleWithOxlint()) return {
2233
+ binaryPath: process.execPath,
2234
+ isCurrentNode: true,
2235
+ version: process.version
2236
+ };
2237
+ const nvmBinaryPath = findCompatibleNvmBinary();
2238
+ if (!nvmBinaryPath) return null;
2239
+ const version = getNodeVersionFromBinary(nvmBinaryPath);
2240
+ if (!version) return null;
2241
+ return {
2242
+ binaryPath: nvmBinaryPath,
2243
+ isCurrentNode: false,
2244
+ version
2245
+ };
2246
+ };
2247
+ const resolveConfigRootDir = (config, configSourceDirectory) => {
2248
+ if (!config || !configSourceDirectory) return null;
2249
+ const rawRootDir = config.rootDir;
2250
+ if (typeof rawRootDir !== "string") return null;
2251
+ const trimmedRootDir = rawRootDir.trim();
2252
+ if (trimmedRootDir.length === 0) return null;
2253
+ const resolvedRootDir = path.isAbsolute(trimmedRootDir) ? trimmedRootDir : path.resolve(configSourceDirectory, trimmedRootDir);
2254
+ if (resolvedRootDir === configSourceDirectory) return null;
2255
+ if (!fs.existsSync(resolvedRootDir) || !fs.statSync(resolvedRootDir).isDirectory()) {
2256
+ logger.warn(`react-doctor config "rootDir" points to "${rawRootDir}" (resolved to ${resolvedRootDir}), which is not a directory. Ignoring.`);
2257
+ return null;
2258
+ }
2259
+ return resolvedRootDir;
2260
+ };
2261
+ const listSourceFilesViaGit = (rootDirectory) => {
2262
+ const result = spawnSync("git", [
2263
+ "ls-files",
2264
+ "-z",
2265
+ "--cached",
2266
+ "--others",
2267
+ "--exclude-standard"
2268
+ ], {
2269
+ cwd: rootDirectory,
2270
+ encoding: "utf-8",
2271
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
2272
+ });
2273
+ if (result.error || result.status !== 0) return null;
2274
+ return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
2275
+ };
2276
+ const listSourceFilesViaFilesystem = (rootDirectory) => {
2277
+ const filePaths = [];
2278
+ const stack = [rootDirectory];
2279
+ while (stack.length > 0) {
2280
+ const currentDirectory = stack.pop();
2281
+ const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
2282
+ for (const entry of entries) {
2283
+ const absolutePath = path.join(currentDirectory, entry.name);
2284
+ if (entry.isDirectory()) {
2285
+ if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
2286
+ continue;
2287
+ }
2288
+ if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) filePaths.push(path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
2289
+ }
2290
+ }
2291
+ return filePaths;
2292
+ };
2293
+ const listSourceFiles = (rootDirectory) => listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory);
2294
+ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
2295
+ if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
2296
+ const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
2297
+ return listSourceFiles(rootDirectory).filter((filePath) => {
2298
+ if (!JSX_FILE_PATTERN.test(filePath)) return false;
2299
+ return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
2300
+ });
2301
+ };
2302
+ const dedupeDiagnostics = (diagnostics) => {
2303
+ const seenKeys = /* @__PURE__ */ new Set();
2304
+ const uniqueDiagnostics = [];
2305
+ for (const diagnostic of diagnostics) {
2306
+ const key = `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}\u0000${diagnostic.plugin}\u0000${diagnostic.rule}\u0000${diagnostic.severity}\u0000${diagnostic.message}`;
2307
+ if (seenKeys.has(key)) continue;
2308
+ seenKeys.add(key);
2309
+ uniqueDiagnostics.push(diagnostic);
2310
+ }
2311
+ return uniqueDiagnostics;
2312
+ };
2313
+ const buildCapabilities = (project) => {
2314
+ const capabilities = /* @__PURE__ */ new Set();
2315
+ capabilities.add(project.framework);
2316
+ if (project.framework === "expo" || project.framework === "react-native") capabilities.add("react-native");
2317
+ const reactMajor = project.reactMajorVersion;
2318
+ if (reactMajor !== null) for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
2319
+ if (project.tailwindVersion !== null) {
2320
+ capabilities.add("tailwind");
2321
+ if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
2322
+ major: 3,
2323
+ minor: 4
2324
+ })) capabilities.add("tailwind:3.4");
2325
+ }
2326
+ if (project.hasReactCompiler) capabilities.add("react-compiler");
2327
+ if (project.hasTanStackQuery) capabilities.add("tanstack-query");
2328
+ if (project.hasTypeScript) capabilities.add("typescript");
2329
+ return capabilities;
2330
+ };
2331
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags) => {
2332
+ if (requires) {
2333
+ for (const capability of requires) if (!capabilities.has(capability)) return false;
2334
+ }
2335
+ if (tags) {
2336
+ for (const tag of tags) if (ignoredTags.has(tag)) return false;
2337
+ }
2338
+ return true;
2339
+ };
2340
+ const esmRequire$1 = createRequire(import.meta.url);
2341
+ const readPluginRuleNames = (pluginSpecifier) => {
2342
+ try {
2343
+ const pluginModule = esmRequire$1(pluginSpecifier);
2344
+ const rules = pluginModule.rules ?? pluginModule.default?.rules;
2345
+ if (rules === void 0) return /* @__PURE__ */ new Set();
2346
+ return new Set(Object.keys(rules));
2347
+ } catch {
2348
+ return /* @__PURE__ */ new Set();
2349
+ }
2350
+ };
2351
+ const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
2352
+ if (!hasReactCompiler || customRulesOnly) return null;
2353
+ let pluginSpecifier;
2354
+ try {
2355
+ pluginSpecifier = esmRequire$1.resolve("eslint-plugin-react-hooks");
2356
+ } catch {
2357
+ return null;
2358
+ }
2359
+ return {
2360
+ entry: {
2361
+ name: "react-hooks-js",
2362
+ specifier: pluginSpecifier
2363
+ },
2364
+ availableRuleNames: readPluginRuleNames(pluginSpecifier)
2365
+ };
2366
+ };
2367
+ const YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE = "effect";
2368
+ const resolveYouMightNotNeedEffectPlugin = (customRulesOnly) => {
2369
+ if (customRulesOnly) return null;
2370
+ let pluginSpecifier;
2371
+ try {
2372
+ pluginSpecifier = esmRequire$1.resolve("eslint-plugin-react-you-might-not-need-an-effect");
2373
+ } catch {
2374
+ return null;
2375
+ }
2376
+ return {
2377
+ entry: {
2378
+ name: YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE,
2379
+ specifier: pluginSpecifier
2380
+ },
2381
+ availableRuleNames: readPluginRuleNames(pluginSpecifier)
2382
+ };
2383
+ };
2384
+ const filterRulesToAvailable = (rules, pluginNamespace, availableRuleNames) => {
2385
+ if (availableRuleNames.size === 0) return rules;
2386
+ const ruleKeyPrefix = `${pluginNamespace}/`;
2387
+ const filtered = {};
2388
+ for (const [ruleKey, severity] of Object.entries(rules)) {
2389
+ if (!ruleKey.startsWith(ruleKeyPrefix)) {
2390
+ filtered[ruleKey] = severity;
2391
+ continue;
2392
+ }
2393
+ const ruleName = ruleKey.slice(ruleKeyPrefix.length);
2394
+ if (availableRuleNames.has(ruleName)) filtered[ruleKey] = severity;
2395
+ }
2396
+ return filtered;
2397
+ };
2398
+ const REACT_COMPILER_RULES = {
2399
+ "react-hooks-js/set-state-in-render": "error",
2400
+ "react-hooks-js/immutability": "error",
2401
+ "react-hooks-js/refs": "error",
2402
+ "react-hooks-js/purity": "error",
2403
+ "react-hooks-js/hooks": "error",
2404
+ "react-hooks-js/set-state-in-effect": "error",
2405
+ "react-hooks-js/globals": "error",
2406
+ "react-hooks-js/error-boundaries": "error",
2407
+ "react-hooks-js/preserve-manual-memoization": "error",
2408
+ "react-hooks-js/unsupported-syntax": "error",
2409
+ "react-hooks-js/component-hook-factories": "error",
2410
+ "react-hooks-js/static-components": "error",
2411
+ "react-hooks-js/use-memo": "error",
2412
+ "react-hooks-js/void-use-memo": "error",
2413
+ "react-hooks-js/incompatible-library": "error",
2414
+ "react-hooks-js/todo": "error"
2415
+ };
2416
+ const YOU_MIGHT_NOT_NEED_EFFECT_RULES = {
2417
+ "effect/no-derived-state": "warn",
2418
+ "effect/no-chain-state-updates": "warn",
2419
+ "effect/no-event-handler": "warn",
2420
+ "effect/no-adjust-state-on-prop-change": "warn",
2421
+ "effect/no-reset-all-state-on-prop-change": "warn",
2422
+ "effect/no-pass-live-state-to-parent": "warn",
2423
+ "effect/no-pass-data-to-parent": "warn",
2424
+ "effect/no-initialize-state": "warn"
2425
+ };
2426
+ const BUILTIN_REACT_RULES = {
2427
+ "react/rules-of-hooks": "error",
2428
+ "react/no-direct-mutation-state": "error",
2429
+ "react/jsx-no-duplicate-props": "error",
2430
+ "react/jsx-key": "error",
2431
+ "react/no-children-prop": "warn",
2432
+ "react/no-danger": "warn",
2433
+ "react/jsx-no-script-url": "error",
2434
+ "react/no-render-return-value": "warn",
2435
+ "react/no-string-refs": "warn",
2436
+ "react/no-is-mounted": "warn",
2437
+ "react/require-render-return": "error",
2438
+ "react/no-unknown-property": "warn"
2439
+ };
2440
+ const BUILTIN_A11Y_RULES = {
2441
+ "jsx-a11y/alt-text": "error",
2442
+ "jsx-a11y/anchor-is-valid": "warn",
2443
+ "jsx-a11y/click-events-have-key-events": "warn",
2444
+ "jsx-a11y/no-static-element-interactions": "warn",
2445
+ "jsx-a11y/role-has-required-aria-props": "error",
2446
+ "jsx-a11y/no-autofocus": "warn",
2447
+ "jsx-a11y/heading-has-content": "warn",
2448
+ "jsx-a11y/html-has-lang": "warn",
2449
+ "jsx-a11y/no-redundant-roles": "warn",
2450
+ "jsx-a11y/scope": "warn",
2451
+ "jsx-a11y/tabindex-no-positive": "warn",
2452
+ "jsx-a11y/label-has-associated-control": "warn",
2453
+ "jsx-a11y/no-distracting-elements": "error",
2454
+ "jsx-a11y/iframe-has-title": "warn"
2455
+ };
2456
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set() }) => {
2457
+ const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
2458
+ const reactCompilerRules = reactHooksJsPlugin ? filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames) : {};
2459
+ const youMightNotNeedEffectPlugin = resolveYouMightNotNeedEffectPlugin(customRulesOnly);
2460
+ const youMightNotNeedEffectRules = youMightNotNeedEffectPlugin ? filterRulesToAvailable(YOU_MIGHT_NOT_NEED_EFFECT_RULES, YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE, youMightNotNeedEffectPlugin.availableRuleNames) : {};
2461
+ const jsPlugins = [];
2462
+ if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
2463
+ if (youMightNotNeedEffectPlugin) jsPlugins.push(youMightNotNeedEffectPlugin.entry);
2464
+ const capabilities = buildCapabilities(project);
2465
+ const enabledReactDoctorRules = {};
2466
+ for (const [ruleId, rule] of Object.entries(reactDoctorPlugin.rules)) {
2467
+ const fullKey = `react-doctor/${ruleId}`;
2468
+ if (rule.framework !== "global" && !rule.requires) continue;
2469
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags)) continue;
2470
+ enabledReactDoctorRules[fullKey] = rule.severity;
2471
+ }
2472
+ return {
2473
+ ...extendsPaths.length > 0 ? { extends: extendsPaths } : {},
2474
+ categories: {
2475
+ correctness: "off",
2476
+ suspicious: "off",
2477
+ pedantic: "off",
2478
+ perf: "off",
2479
+ restriction: "off",
2480
+ style: "off",
2481
+ nursery: "off"
2482
+ },
2483
+ plugins: customRulesOnly ? [] : ["react", "jsx-a11y"],
2484
+ jsPlugins: [...jsPlugins, pluginPath],
2485
+ rules: {
2486
+ ...customRulesOnly ? {} : BUILTIN_REACT_RULES,
2487
+ ...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
2488
+ ...reactCompilerRules,
2489
+ ...youMightNotNeedEffectRules,
2490
+ ...enabledReactDoctorRules
2491
+ }
2492
+ };
2493
+ };
2494
+ const REACT_MODULE_SOURCE = "react";
2495
+ const REQUIRE_IDENTIFIER = "require";
2496
+ const USE_IDENTIFIER = "use";
2497
+ const LOCAL_BINDING_RESOLUTION = {
2498
+ isReactUseBinding: false,
2499
+ isReactNamespaceBinding: false
2500
+ };
2501
+ const REACT_NAMESPACE_BINDING_RESOLUTION = {
2502
+ isReactUseBinding: false,
2503
+ isReactNamespaceBinding: true
2504
+ };
2505
+ const REACT_USE_BINDING_RESOLUTION = {
2506
+ isReactUseBinding: true,
2507
+ isReactNamespaceBinding: false
2508
+ };
2509
+ const getScriptKind = (filename) => {
2510
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
2511
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
2512
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
2513
+ return ts.ScriptKind.JS;
2514
+ };
2515
+ const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
2516
+ const unwrapExpression = (expression) => {
2517
+ let currentExpression = expression;
2518
+ while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
2519
+ return currentExpression;
2520
+ };
2521
+ const getStaticPropertyName = (node) => {
2522
+ if (!node) return null;
2523
+ if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
2524
+ if (ts.isComputedPropertyName(node)) {
2525
+ const expression = unwrapExpression(node.expression);
2526
+ if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
2527
+ }
2528
+ return null;
2529
+ };
2530
+ const findBindingIdentifier = (bindingName, identifierName) => {
2531
+ if (ts.isIdentifier(bindingName)) return bindingName.text === identifierName ? bindingName : null;
2532
+ for (const element of bindingName.elements) {
2533
+ if (ts.isOmittedExpression(element)) continue;
2534
+ const nestedIdentifier = findBindingIdentifier(element.name, identifierName);
2535
+ if (nestedIdentifier) return nestedIdentifier;
2536
+ }
2537
+ return null;
2538
+ };
2539
+ const bindingNameHasIdentifier = (bindingName, identifierName) => {
2540
+ if (ts.isIdentifier(bindingName)) return bindingName.text === identifierName;
2541
+ return bindingName.elements.some((element) => {
2542
+ if (ts.isOmittedExpression(element)) return false;
2543
+ return bindingNameHasIdentifier(element.name, identifierName);
2544
+ });
2545
+ };
2546
+ const getDirectBindingIdentifier = (bindingName) => ts.isIdentifier(bindingName) ? bindingName : null;
2547
+ const isReactUseObjectBindingElement = (bindingElement) => {
2548
+ const bindingIdentifier = getDirectBindingIdentifier(bindingElement.name);
2549
+ if (!bindingIdentifier) return false;
2550
+ if (!bindingElement.propertyName) return bindingIdentifier.text === USE_IDENTIFIER;
2551
+ return getStaticPropertyName(bindingElement.propertyName) === USE_IDENTIFIER;
2552
+ };
2553
+ const isReactRequireCall = (expression) => {
2554
+ const unwrappedExpression = unwrapExpression(expression);
2555
+ return ts.isCallExpression(unwrappedExpression) && ts.isIdentifier(unwrappedExpression.expression) && unwrappedExpression.expression.text === REQUIRE_IDENTIFIER && unwrappedExpression.arguments.length === 1 && ts.isStringLiteral(unwrappedExpression.arguments[0]) && unwrappedExpression.arguments[0].text === REACT_MODULE_SOURCE;
2556
+ };
2557
+ const getModuleSource = (node) => {
2558
+ let currentNode = node;
2559
+ while (currentNode) {
2560
+ if (ts.isImportDeclaration(currentNode) && ts.isStringLiteral(currentNode.moduleSpecifier)) return currentNode.moduleSpecifier.text;
2561
+ currentNode = currentNode.parent;
2562
+ }
2563
+ return null;
2564
+ };
2565
+ const getImportedName = (importSpecifier) => importSpecifier.propertyName?.text ?? importSpecifier.name.text;
2566
+ const collectReactObjectBindingNames = (bindingPattern, useImportNames) => {
2567
+ for (const bindingElement of bindingPattern.elements) {
2568
+ const bindingIdentifier = getDirectBindingIdentifier(bindingElement.name);
2569
+ if (bindingIdentifier && isReactUseObjectBindingElement(bindingElement)) useImportNames.add(bindingIdentifier.text);
2570
+ }
2571
+ };
2572
+ const isReactObjectBindingName = (bindingPattern, identifierName) => bindingPattern.elements.some((bindingElement) => {
2573
+ if (getDirectBindingIdentifier(bindingElement.name)?.text !== identifierName) return false;
2574
+ return isReactUseObjectBindingElement(bindingElement);
2575
+ });
2576
+ const isReactRequireBindingDeclaration = (node, identifierName) => {
2577
+ if (!ts.isVariableDeclaration(node)) return false;
2578
+ if (!node.initializer) return false;
2579
+ if (!isReactRequireCall(node.initializer)) return false;
2580
+ if (ts.isIdentifier(node.name)) return node.name.text === identifierName;
2581
+ return ts.isObjectBindingPattern(node.name) && isReactObjectBindingName(node.name, identifierName);
2582
+ };
2583
+ const collectReactImportBindings = (sourceFile) => {
2584
+ const namespaceNames = /* @__PURE__ */ new Set();
2585
+ const useImportNames = /* @__PURE__ */ new Set();
2586
+ for (const statement of sourceFile.statements) {
2587
+ if (ts.isImportDeclaration(statement)) {
2588
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
2589
+ if (statement.moduleSpecifier.text !== REACT_MODULE_SOURCE) continue;
2590
+ const importClause = statement.importClause;
2591
+ if (!importClause) continue;
2592
+ if (importClause.name) namespaceNames.add(importClause.name.text);
2593
+ const namedBindings = importClause.namedBindings;
2594
+ if (!namedBindings) continue;
2595
+ if (ts.isNamespaceImport(namedBindings)) {
2596
+ namespaceNames.add(namedBindings.name.text);
2597
+ continue;
2598
+ }
2599
+ for (const importSpecifier of namedBindings.elements) if (getImportedName(importSpecifier) === USE_IDENTIFIER) useImportNames.add(importSpecifier.name.text);
2600
+ continue;
2601
+ }
2602
+ if (!ts.isVariableStatement(statement)) continue;
2603
+ for (const declaration of statement.declarationList.declarations) {
2604
+ if (!declaration.initializer) continue;
2605
+ if (!isReactRequireCall(declaration.initializer)) continue;
2606
+ if (ts.isIdentifier(declaration.name)) {
2607
+ namespaceNames.add(declaration.name.text);
2608
+ continue;
2609
+ }
2610
+ if (ts.isObjectBindingPattern(declaration.name)) collectReactObjectBindingNames(declaration.name, useImportNames);
2611
+ }
2612
+ }
2613
+ return {
2614
+ namespaceNames,
2615
+ useImportNames
2616
+ };
2617
+ };
2618
+ const findBindingElement = (identifier) => {
2619
+ let currentNode = identifier.parent;
2620
+ while (currentNode) {
2621
+ if (ts.isBindingElement(currentNode)) return currentNode;
2622
+ if (ts.isVariableDeclaration(currentNode) || ts.isParameter(currentNode)) return null;
2623
+ currentNode = currentNode.parent;
2624
+ }
2625
+ return null;
2626
+ };
2627
+ const declarationBindsIdentifier = (node, identifierName) => {
2628
+ if (ts.isVariableDeclaration(node) || ts.isParameter(node)) return bindingNameHasIdentifier(node.name, identifierName);
2629
+ if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) return node.name?.text === identifierName;
2630
+ return false;
2631
+ };
2632
+ const isScopeBoundary = (node) => ts.isFunctionLike(node) || ts.isClassLike(node) || ts.isBlock(node) || ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node) || ts.isCatchClause(node) || ts.isSourceFile(node) || ts.isModuleBlock(node);
2633
+ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
2634
+ if (isReactRequireBindingDeclaration(node, identifierName)) return false;
2635
+ if (declarationBindsIdentifier(node, identifierName)) return true;
2636
+ if (node !== scopeNode && isScopeBoundary(node)) return false;
2637
+ let didFindBinding = false;
2638
+ ts.forEachChild(node, (child) => {
2639
+ if (didFindBinding) return;
2640
+ didFindBinding = scopeContainsNonImportBinding(child, scopeNode, identifierName);
2641
+ });
2642
+ return didFindBinding;
2643
+ };
2644
+ const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
2645
+ let currentNode = identifier.parent;
2646
+ while (currentNode) {
2647
+ if (isScopeNode(currentNode)) {
2648
+ if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
2649
+ }
2650
+ if (currentNode === sourceFile) return false;
2651
+ currentNode = currentNode.parent;
2652
+ }
2653
+ return false;
2654
+ };
2655
+ const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
2656
+ const unwrappedExpression = unwrapExpression(expression);
2657
+ if (isReactRequireCall(unwrappedExpression)) return true;
2658
+ if (!ts.isIdentifier(unwrappedExpression)) return false;
2659
+ if (reactImportBindings.namespaceNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
2660
+ return resolveIdentifierBinding(unwrappedExpression, reactImportBindings, sourceFile, visitedDeclarations)?.isReactNamespaceBinding ?? false;
2661
+ };
2662
+ const isReactUseExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
2663
+ if (!expression) return false;
2664
+ const unwrappedExpression = unwrapExpression(expression);
2665
+ if (ts.isIdentifier(unwrappedExpression)) {
2666
+ if (reactImportBindings.useImportNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
2667
+ if (unwrappedExpression.text === USE_IDENTIFIER) return false;
2668
+ return resolveIdentifierBinding(unwrappedExpression, reactImportBindings, sourceFile, visitedDeclarations)?.isReactUseBinding ?? false;
2669
+ }
2670
+ if (ts.isPropertyAccessExpression(unwrappedExpression) && unwrappedExpression.name.text === USE_IDENTIFIER && isReactNamespaceExpression(unwrappedExpression.expression, reactImportBindings, sourceFile, visitedDeclarations)) return true;
2671
+ if (ts.isElementAccessExpression(unwrappedExpression) && ts.isStringLiteral(unwrappedExpression.argumentExpression) && unwrappedExpression.argumentExpression.text === USE_IDENTIFIER) return isReactNamespaceExpression(unwrappedExpression.expression, reactImportBindings, sourceFile, visitedDeclarations);
2672
+ return false;
2673
+ };
2674
+ const isReactUseObjectBinding = (identifier, variableDeclaration, reactImportBindings, sourceFile, visitedDeclarations) => {
2675
+ const bindingElement = findBindingElement(identifier);
2676
+ if (!bindingElement) return false;
2677
+ if (!ts.isObjectBindingPattern(bindingElement.parent)) return false;
2678
+ if (!variableDeclaration.initializer) return false;
2679
+ if (!isReactNamespaceExpression(variableDeclaration.initializer, reactImportBindings, sourceFile, visitedDeclarations)) return false;
2680
+ return isReactUseObjectBindingElement(bindingElement);
2681
+ };
2682
+ const getVariableDeclarationResolution = (variableDeclaration, identifierName, reactImportBindings, sourceFile, visitedDeclarations) => {
2683
+ const bindingIdentifier = findBindingIdentifier(variableDeclaration.name, identifierName);
2684
+ if (!bindingIdentifier) return null;
2685
+ if (visitedDeclarations.has(variableDeclaration)) return null;
2686
+ const nestedVisitedDeclarations = new Set(visitedDeclarations);
2687
+ nestedVisitedDeclarations.add(variableDeclaration);
2688
+ return {
2689
+ isReactNamespaceBinding: ts.isIdentifier(variableDeclaration.name) && variableDeclaration.initializer !== void 0 && isReactNamespaceExpression(variableDeclaration.initializer, reactImportBindings, sourceFile, new Set(nestedVisitedDeclarations)),
2690
+ isReactUseBinding: isReactUseExpression(variableDeclaration.initializer, reactImportBindings, sourceFile, new Set(nestedVisitedDeclarations)) || isReactUseObjectBinding(bindingIdentifier, variableDeclaration, reactImportBindings, sourceFile, new Set(nestedVisitedDeclarations))
2691
+ };
2692
+ };
2693
+ const getImportResolution = (node, identifierName) => {
2694
+ if (ts.isImportSpecifier(node) && node.name.text === identifierName) return getModuleSource(node) === REACT_MODULE_SOURCE && getImportedName(node) === USE_IDENTIFIER ? REACT_USE_BINDING_RESOLUTION : LOCAL_BINDING_RESOLUTION;
2695
+ if (ts.isNamespaceImport(node) && node.name.text === identifierName) return getModuleSource(node) === REACT_MODULE_SOURCE ? REACT_NAMESPACE_BINDING_RESOLUTION : LOCAL_BINDING_RESOLUTION;
2696
+ if (ts.isImportClause(node) && node.name?.text === identifierName) return getModuleSource(node) === REACT_MODULE_SOURCE ? REACT_NAMESPACE_BINDING_RESOLUTION : LOCAL_BINDING_RESOLUTION;
2697
+ return null;
2698
+ };
2699
+ const getDeclarationResolution = (node, identifierName, reactImportBindings, sourceFile, visitedDeclarations) => {
2700
+ const importResolution = getImportResolution(node, identifierName);
2701
+ if (importResolution) return importResolution;
2702
+ if (ts.isVariableDeclaration(node)) return getVariableDeclarationResolution(node, identifierName, reactImportBindings, sourceFile, visitedDeclarations);
2703
+ if (ts.isParameter(node)) return bindingNameHasIdentifier(node.name, identifierName) ? LOCAL_BINDING_RESOLUTION : null;
2704
+ if (ts.isFunctionDeclaration(node) && node.name?.text === identifierName) return LOCAL_BINDING_RESOLUTION;
2705
+ if (ts.isClassDeclaration(node) && node.name?.text === identifierName) return LOCAL_BINDING_RESOLUTION;
2706
+ return null;
2707
+ };
2708
+ const isNestedScopeBoundary = (node, scopeNode) => node !== scopeNode && isScopeBoundary(node);
2709
+ const findResolutionInSubtree = (node, scopeNode, identifierName, reactImportBindings, sourceFile, visitedDeclarations) => {
2710
+ const declarationResolution = getDeclarationResolution(node, identifierName, reactImportBindings, sourceFile, visitedDeclarations);
2711
+ if (declarationResolution) return declarationResolution;
2712
+ if (isNestedScopeBoundary(node, scopeNode)) return null;
2713
+ let resolution = null;
2714
+ ts.forEachChild(node, (child) => {
2715
+ if (resolution) return;
2716
+ resolution = findResolutionInSubtree(child, scopeNode, identifierName, reactImportBindings, sourceFile, visitedDeclarations);
2717
+ });
2718
+ return resolution;
2719
+ };
2720
+ const findResolutionInFunctionParameters = (node, identifierName, reactImportBindings) => {
2721
+ if (!ts.isFunctionLike(node)) return null;
2722
+ for (const parameter of node.parameters) {
2723
+ const parameterResolution = getDeclarationResolution(parameter, identifierName, reactImportBindings, parameter.getSourceFile(), /* @__PURE__ */ new Set());
2724
+ if (parameterResolution) return parameterResolution;
2725
+ }
2726
+ return null;
2727
+ };
2728
+ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, sourceFile, visitedDeclarations) => {
2729
+ const parameterResolution = findResolutionInFunctionParameters(scopeNode, identifierName, reactImportBindings);
2730
+ if (parameterResolution) return parameterResolution;
2731
+ let resolution = null;
2732
+ ts.forEachChild(scopeNode, (child) => {
2733
+ if (resolution) return;
2734
+ resolution = findResolutionInSubtree(child, scopeNode, identifierName, reactImportBindings, sourceFile, visitedDeclarations);
2735
+ });
2736
+ return resolution;
2737
+ };
2738
+ const isScopeNode = isScopeBoundary;
2739
+ const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
2740
+ let currentNode = identifier.parent;
2741
+ while (currentNode) {
2742
+ if (isScopeNode(currentNode)) {
2743
+ const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
2744
+ if (resolution) return resolution;
2745
+ }
2746
+ currentNode = currentNode.parent;
2747
+ }
2748
+ return null;
2749
+ };
2750
+ const isUseCallIdentifier = (node) => node.text === USE_IDENTIFIER && ts.isCallExpression(node.parent) && node.parent.expression === node;
2751
+ const findUseCallIdentifier = (sourceFile, useOffset) => {
2752
+ let matchedIdentifier = null;
2753
+ const visit = (node) => {
2754
+ if (matchedIdentifier) return;
2755
+ if (ts.isIdentifier(node) && isUseCallIdentifier(node) && node.getStart(sourceFile) === useOffset) {
2756
+ matchedIdentifier = node;
2757
+ return;
2758
+ }
2759
+ ts.forEachChild(node, visit);
2760
+ };
2761
+ visit(sourceFile);
2762
+ return matchedIdentifier;
2763
+ };
2764
+ const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
2765
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filename));
2766
+ const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset(sourceText, utf8Offset));
2767
+ if (!useIdentifier) return null;
2768
+ return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
2769
+ };
2770
+ const RULES_OF_HOOKS_CODE = "react-hooks(rules-of-hooks)";
2771
+ const REACT_HOOK_USE_MESSAGE_PREFIX = "React Hook \"use\"";
2772
+ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
2773
+ if (diagnostic.code !== RULES_OF_HOOKS_CODE) return false;
2774
+ if (!diagnostic.message.startsWith(REACT_HOOK_USE_MESSAGE_PREFIX)) return false;
2775
+ const primaryLabel = diagnostic.labels[0];
2776
+ if (!primaryLabel) return false;
2777
+ const absolutePath = path.isAbsolute(diagnostic.filename) ? diagnostic.filename : path.join(rootDirectory, diagnostic.filename);
2778
+ let sourceText;
2779
+ try {
2780
+ sourceText = fs.readFileSync(absolutePath, "utf-8");
2781
+ } catch {
2782
+ return false;
2783
+ }
2784
+ const bindingResolution = resolveUseCallBinding(sourceText, absolutePath, primaryLabel.span.offset);
2785
+ return bindingResolution !== null && !bindingResolution.isReactUseBinding;
2786
+ };
2787
+ const getRuleRecommendation = (ruleName) => reactDoctorPlugin.rules[ruleName]?.recommendation;
2788
+ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
2789
+ const esmRequire = createRequire(import.meta.url);
2790
+ const PLUGIN_CATEGORY_MAP = {
2791
+ react: "Correctness",
2792
+ "react-hooks": "Correctness",
2793
+ "react-hooks-js": "React Compiler",
2794
+ "react-doctor": "Other",
2795
+ "jsx-a11y": "Accessibility",
2796
+ effect: "State & Effects",
2797
+ eslint: "Correctness",
2798
+ oxc: "Correctness",
2799
+ typescript: "Correctness",
2800
+ unicorn: "Correctness",
2801
+ import: "Bundle Size",
2802
+ promise: "Correctness",
2803
+ n: "Correctness",
2804
+ node: "Correctness",
2805
+ vitest: "Correctness",
2806
+ jest: "Correctness",
2807
+ nextjs: "Next.js"
2808
+ };
2809
+ const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
2810
+ const REACT_COMPILER_MESSAGE = "React Compiler can't optimize this code";
2811
+ const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
2812
+ const cleanDiagnosticMessage = (message, help, plugin, rule) => {
2813
+ if (plugin === "react-hooks-js") return {
2814
+ message: REACT_COMPILER_MESSAGE,
2815
+ help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
2816
+ };
2817
+ return {
2818
+ message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
2819
+ help: help || getRuleRecommendation(rule) || ""
2820
+ };
2821
+ };
2822
+ const parseRuleCode = (code) => {
2823
+ const match = code.match(/^(.+)\((.+)\)$/);
2824
+ if (!match) return {
2825
+ plugin: "unknown",
2826
+ rule: code
2827
+ };
2828
+ return {
2829
+ plugin: match[1].replace(/^eslint-plugin-/, ""),
2830
+ rule: match[2]
2831
+ };
2832
+ };
2833
+ const resolveOxlintBinary = () => {
2834
+ const oxlintMainPath = esmRequire.resolve("oxlint");
2835
+ const oxlintPackageDirectory = path.resolve(path.dirname(oxlintMainPath), "..");
2836
+ return path.join(oxlintPackageDirectory, "bin", "oxlint");
2837
+ };
2838
+ const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
2839
+ const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Other";
2840
+ const SANITIZED_ENV = (() => {
2841
+ const sanitized = {};
2842
+ for (const [name, value] of Object.entries(process.env)) {
2843
+ if (name === "NODE_OPTIONS" || name === "NODE_DEBUG") continue;
2844
+ if (name.startsWith("npm_config_")) continue;
2845
+ sanitized[name] = value;
2846
+ }
2847
+ return sanitized;
2848
+ })();
2849
+ const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
2850
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
2851
+ const child = spawn(nodeBinaryPath, args, {
2852
+ cwd: rootDirectory,
2853
+ env: SANITIZED_ENV
2854
+ });
2855
+ const timeoutHandle = setTimeout(() => {
2856
+ child.kill("SIGKILL");
2857
+ reject(/* @__PURE__ */ new Error(`oxlint did not return within ${OXLINT_SPAWN_TIMEOUT_MS / 1e3}s — please report`));
2858
+ }, OXLINT_SPAWN_TIMEOUT_MS);
2859
+ timeoutHandle.unref?.();
2860
+ const stdoutBuffers = [];
2861
+ const stderrBuffers = [];
2862
+ let stdoutByteCount = 0;
2863
+ let stderrByteCount = 0;
2864
+ let didKillForSize = false;
2865
+ const killIfTooLarge = (incomingBytes, isStdout) => {
2866
+ if (isStdout) stdoutByteCount += incomingBytes;
2867
+ else stderrByteCount += incomingBytes;
2868
+ if (stdoutByteCount + stderrByteCount > 52428800 && !didKillForSize) {
2869
+ didKillForSize = true;
2870
+ child.kill("SIGKILL");
2871
+ return true;
2872
+ }
2873
+ return false;
2874
+ };
2875
+ child.stdout.on("data", (buffer) => {
2876
+ if (didKillForSize) return;
2877
+ stdoutBuffers.push(buffer);
2878
+ killIfTooLarge(buffer.length, true);
2879
+ });
2880
+ child.stderr.on("data", (buffer) => {
2881
+ if (didKillForSize) return;
2882
+ stderrBuffers.push(buffer);
2883
+ killIfTooLarge(buffer.length, false);
2884
+ });
2885
+ child.on("error", (error) => {
2886
+ clearTimeout(timeoutHandle);
2887
+ reject(/* @__PURE__ */ new Error(`Failed to run oxlint: ${error.message}`));
2888
+ });
2889
+ child.on("close", (_code, signal) => {
2890
+ clearTimeout(timeoutHandle);
2891
+ if (didKillForSize) {
2892
+ reject(/* @__PURE__ */ new Error(`oxlint output exceeded ${OXLINT_OUTPUT_MAX_BYTES} bytes — scan a smaller subset with --diff or --staged`));
2893
+ return;
2894
+ }
2895
+ if (signal) {
2896
+ const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
2897
+ const hint = signal === "SIGABRT" ? " (out of memory — try scanning fewer files with --diff)" : "";
2898
+ const detail = stderrOutput ? `: ${stderrOutput}` : "";
2899
+ reject(/* @__PURE__ */ new Error(`oxlint was killed by ${signal}${hint}${detail}`));
2900
+ return;
2901
+ }
2902
+ const output = Buffer.concat(stdoutBuffers).toString("utf-8").trim();
2903
+ if (!output) {
2904
+ const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
2905
+ if (stderrOutput) {
2906
+ reject(/* @__PURE__ */ new Error(`Failed to run oxlint: ${stderrOutput}`));
2907
+ return;
2908
+ }
2909
+ }
2910
+ resolve(output);
2911
+ });
2912
+ });
2913
+ const isSplittableOxlintBatchError = (error) => {
2914
+ if (!(error instanceof Error)) return false;
2915
+ return error.message.includes("did not return within") || error.message.includes("output exceeded") || error.message.includes("out of memory");
2916
+ };
2917
+ const isOxlintOutput = (value) => {
2918
+ if (typeof value !== "object" || value === null) return false;
2919
+ const candidate = value;
2920
+ return Array.isArray(candidate.diagnostics);
2921
+ };
2922
+ const parseOxlintOutput = (stdout, rootDirectory) => {
2923
+ if (!stdout) return [];
2924
+ const jsonStart = stdout.indexOf("{");
2925
+ const sanitizedStdout = jsonStart > 0 ? stdout.slice(jsonStart) : stdout;
2926
+ let parsed;
2927
+ try {
2928
+ parsed = JSON.parse(sanitizedStdout);
2929
+ } catch {
2930
+ throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, 200)}`);
2931
+ }
2932
+ if (!isOxlintOutput(parsed)) throw new Error(`Unexpected oxlint output shape: ${stdout.slice(0, 200)}`);
2933
+ return parsed.diagnostics.filter((diagnostic) => diagnostic.code && SOURCE_FILE_PATTERN.test(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
2934
+ const { plugin, rule } = parseRuleCode(diagnostic.code);
2935
+ const primaryLabel = diagnostic.labels[0];
2936
+ const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule);
2937
+ return {
2938
+ filePath: diagnostic.filename,
2939
+ plugin,
2940
+ rule,
2941
+ severity: diagnostic.severity,
2942
+ message: cleaned.message,
2943
+ help: cleaned.help,
2944
+ url: diagnostic.url,
2945
+ line: primaryLabel?.span.line ?? 0,
2946
+ column: primaryLabel?.span.column ?? 0,
2947
+ category: resolveDiagnosticCategory(plugin, rule)
2948
+ };
2949
+ });
2950
+ };
2951
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
2952
+ const resolveTsConfigRelativePath = (rootDirectory) => {
2953
+ for (const filename of TSCONFIG_FILENAMES) if (fs.existsSync(path.join(rootDirectory, filename))) return `./${filename}`;
2954
+ return null;
2955
+ };
2956
+ let didValidateRuleRegistration = false;
2957
+ const validateRuleRegistration = () => {
2958
+ if (didValidateRuleRegistration) return;
2959
+ didValidateRuleRegistration = true;
2960
+ const missingHelp = [];
2961
+ const missingCategory = [];
2962
+ const missingMetadata = [];
2963
+ for (const fullKey of ALL_REACT_DOCTOR_RULE_KEYS) {
2964
+ const ruleName = fullKey.replace(/^react-doctor\//, "");
2965
+ if (!getRuleCategory(ruleName)) missingCategory.push(fullKey);
2966
+ if (!getRuleRecommendation(ruleName)) missingHelp.push(fullKey);
2967
+ if (FRAMEWORK_SPECIFIC_RULE_KEYS.has(fullKey) && !reactDoctorPlugin.rules[ruleName]?.requires) missingMetadata.push(fullKey);
2968
+ }
2969
+ if (missingCategory.length > 0 || missingHelp.length > 0 || missingMetadata.length > 0) {
2970
+ const detail = [
2971
+ missingCategory.length > 0 ? `Missing rule categories (add to defineRule call): ${missingCategory.join(", ")}` : null,
2972
+ missingHelp.length > 0 ? `Missing rule recommendations (add to defineRule call): ${missingHelp.join(", ")}` : null,
2973
+ missingMetadata.length > 0 ? `Missing rule \`requires\` capability gate (add to defineRule call): ${missingMetadata.join(", ")}` : null
2974
+ ].filter((entry) => entry !== null).join("; ");
2975
+ console.warn(`[react-doctor] rule-registration drift: ${detail}`);
2976
+ }
2977
+ };
2978
+ const runOxlint = async (options) => {
2979
+ const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), onPartialFailure } = options;
2980
+ validateRuleRegistration();
2981
+ if (includePaths !== void 0 && includePaths.length === 0) return [];
2982
+ const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
2983
+ const configPath = path.join(configDirectory, "oxlintrc.json");
2984
+ const pluginPath = resolvePluginPath();
2985
+ const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
2986
+ const config = createOxlintConfig({
2987
+ pluginPath,
2988
+ project,
2989
+ customRulesOnly,
2990
+ extendsPaths,
2991
+ ignoredTags
2992
+ });
2993
+ const restoreDisableDirectives = respectInlineDisables ? () => {} : neutralizeDisableDirectives(rootDirectory, includePaths);
2994
+ try {
2995
+ const baseArgs = [
2996
+ resolveOxlintBinary(),
2997
+ "-c",
2998
+ configPath,
2999
+ "--format",
3000
+ "json"
3001
+ ];
3002
+ if (project.hasTypeScript) {
3003
+ const tsconfigRelativePath = resolveTsConfigRelativePath(rootDirectory);
3004
+ if (tsconfigRelativePath) baseArgs.push("--tsconfig", tsconfigRelativePath);
3005
+ }
3006
+ const combinedPatterns = collectIgnorePatterns(rootDirectory);
3007
+ if (combinedPatterns.length > 0) {
3008
+ const combinedIgnorePath = path.join(configDirectory, "combined.ignore");
3009
+ fs.writeFileSync(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
3010
+ baseArgs.push("--ignore-path", combinedIgnorePath);
3011
+ }
3012
+ const fileBatches = batchIncludePaths(baseArgs, includePaths !== void 0 ? includePaths : listSourceFiles(rootDirectory));
3013
+ const writeOxlintConfig = (configToWrite) => {
3014
+ fs.rmSync(configPath, { force: true });
3015
+ const fileHandle = fs.openSync(configPath, "wx", 384);
3016
+ try {
3017
+ fs.writeFileSync(fileHandle, JSON.stringify(configToWrite));
3018
+ } finally {
3019
+ fs.closeSync(fileHandle);
3020
+ }
3021
+ };
3022
+ const spawnLintBatches = async () => {
3023
+ const allDiagnostics = [];
3024
+ const droppedFiles = [];
3025
+ const spawnLintBatch = async (batch) => {
3026
+ const batchArgs = [...baseArgs, ...batch];
3027
+ try {
3028
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), rootDirectory);
3029
+ } catch (error) {
3030
+ if (!isSplittableOxlintBatchError(error)) throw error;
3031
+ if (batch.length <= 1) {
3032
+ droppedFiles.push(...batch);
3033
+ return [];
3034
+ }
3035
+ const splitIndex = Math.ceil(batch.length / 2);
3036
+ return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
3037
+ }
3038
+ };
3039
+ for (const batch of fileBatches) allDiagnostics.push(...await spawnLintBatch(batch));
3040
+ if (droppedFiles.length > 0 && onPartialFailure) {
3041
+ const previewCount = 3;
3042
+ const previewFiles = droppedFiles.slice(0, previewCount).join(", ");
3043
+ const remainderHint = droppedFiles.length > previewCount ? `, +${droppedFiles.length - previewCount} more` : "";
3044
+ onPartialFailure(`${droppedFiles.length} file(s) exceeded the ${OXLINT_SPAWN_TIMEOUT_MS / 1e3}s per-batch oxlint budget and were skipped (${previewFiles}${remainderHint})`);
3045
+ }
3046
+ return dedupeDiagnostics(allDiagnostics);
3047
+ };
3048
+ writeOxlintConfig(config);
3049
+ try {
3050
+ return await spawnLintBatches();
3051
+ } catch (error) {
3052
+ if (extendsPaths.length === 0) throw error;
3053
+ writeOxlintConfig(createOxlintConfig({
3054
+ pluginPath,
3055
+ project,
3056
+ customRulesOnly,
3057
+ extendsPaths: [],
3058
+ ignoredTags
3059
+ }));
3060
+ return await spawnLintBatches();
3061
+ }
3062
+ } finally {
3063
+ restoreDisableDirectives();
3064
+ fs.rmSync(configDirectory, {
3065
+ recursive: true,
3066
+ force: true
3067
+ });
3068
+ }
3069
+ };
3070
+ const groupBy = (items, keyFn) => {
3071
+ const groups = /* @__PURE__ */ new Map();
3072
+ for (const item of items) {
3073
+ const key = keyFn(item);
3074
+ const existing = groups.get(key) ?? [];
3075
+ existing.push(item);
3076
+ groups.set(key, existing);
3077
+ }
3078
+ return groups;
3079
+ };
3080
+ //#endregion
15
3081
  //#region src/cli/utils/build-hidden-diagnostics-summary.ts
16
3082
  const buildHiddenDiagnosticsSummary = (hiddenDiagnostics) => {
17
3083
  const errorCount = hiddenDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
@@ -90,7 +3156,7 @@ const toRuleTitle = (ruleName) => {
90
3156
  };
91
3157
  const computeRuleNameColumnWidth = (ruleKeys) => {
92
3158
  const longestRuleNameLength = ruleKeys.reduce((longest, ruleKey) => Math.max(longest, ruleKey.length), 0);
93
- return Math.max(RULE_NAME_COLUMN_WIDTH_CHARS, longestRuleNameLength);
3159
+ return Math.max(36, longestRuleNameLength);
94
3160
  };
95
3161
  const padRuleNameToColumn = (ruleName, columnWidth) => {
96
3162
  if (ruleName.length >= columnWidth) return ruleName;
@@ -100,7 +3166,7 @@ const grayLine = (text) => {
100
3166
  logger.log(highlighter.gray(text));
101
3167
  };
102
3168
  const grayWrappedLine = (text, linePrefix) => {
103
- grayLine(wrapIndentedText(text, linePrefix, OUTPUT_DETAIL_WRAP_WIDTH_CHARS));
3169
+ grayLine(wrapIndentedText(text, linePrefix, 88));
104
3170
  };
105
3171
  const printCompactRuleGroupLine = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
106
3172
  const firstDiagnostic = ruleDiagnostics[0];
@@ -169,11 +3235,11 @@ const printHiddenDiagnosticsSummary = (hiddenRuleGroups) => {
169
3235
  const printDefaultDiagnostics = (diagnostics, rootDirectory) => {
170
3236
  const categoryGroups = buildCategoryDiagnosticGroups(diagnostics);
171
3237
  const hiddenRuleGroups = [];
172
- const visibleCategoryGroups = categoryGroups.slice(0, MAX_CATEGORY_GROUPS_SHOWN_NON_VERBOSE);
173
- const hiddenCategoryGroups = categoryGroups.slice(MAX_CATEGORY_GROUPS_SHOWN_NON_VERBOSE);
3238
+ const visibleCategoryGroups = categoryGroups.slice(0, 5);
3239
+ const hiddenCategoryGroups = categoryGroups.slice(5);
174
3240
  for (const categoryGroup of visibleCategoryGroups) {
175
- const visibleRuleGroups = categoryGroup.ruleGroups.slice(0, MAX_RULE_GROUPS_PER_CATEGORY_NON_VERBOSE);
176
- const remainingRuleGroups = categoryGroup.ruleGroups.slice(MAX_RULE_GROUPS_PER_CATEGORY_NON_VERBOSE);
3241
+ const visibleRuleGroups = categoryGroup.ruleGroups.slice(0, 3);
3242
+ const remainingRuleGroups = categoryGroup.ruleGroups.slice(3);
177
3243
  printDefaultCategoryGroup(categoryGroup, visibleRuleGroups, rootDirectory);
178
3244
  hiddenRuleGroups.push(...remainingRuleGroups);
179
3245
  }
@@ -192,7 +3258,7 @@ const printDiagnostics = (diagnostics, isVerbose, rootDirectory) => {
192
3258
  });
193
3259
  };
194
3260
  const formatElapsedTime = (elapsedMilliseconds) => {
195
- if (elapsedMilliseconds < MILLISECONDS_PER_SECOND) return `${Math.round(elapsedMilliseconds)}ms`;
3261
+ if (elapsedMilliseconds < 1e3) return `${Math.round(elapsedMilliseconds)}ms`;
196
3262
  return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
197
3263
  };
198
3264
  const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
@@ -244,7 +3310,7 @@ const finalize = (method, originalText, displayText) => {
244
3310
  sharedInstance.stop();
245
3311
  ora({
246
3312
  text: displayText,
247
- indent: SPINNER_INDENT_CHARS
3313
+ indent: 0
248
3314
  }).start()[method](displayText);
249
3315
  const [remainingText] = pendingTexts;
250
3316
  if (remainingText) sharedInstance.text = remainingText;
@@ -256,7 +3322,7 @@ const spinner = (text) => ({ start() {
256
3322
  pendingTexts.add(text);
257
3323
  if (!sharedInstance) sharedInstance = ora({
258
3324
  text,
259
- indent: SPINNER_INDENT_CHARS
3325
+ indent: 0
260
3326
  }).start();
261
3327
  else sharedInstance.text = text;
262
3328
  const handle = {
@@ -294,15 +3360,15 @@ const printProjectDetection = (projectInfo, userConfig, isDiffMode, includePaths
294
3360
  //#endregion
295
3361
  //#region src/cli/utils/colorize-by-score.ts
296
3362
  const colorizeByScore = (text, score) => {
297
- if (score >= SCORE_GOOD_THRESHOLD) return highlighter.success(text);
298
- if (score >= SCORE_OK_THRESHOLD) return highlighter.warn(text);
3363
+ if (score >= 75) return highlighter.success(text);
3364
+ if (score >= 50) return highlighter.warn(text);
299
3365
  return highlighter.error(text);
300
3366
  };
301
3367
  //#endregion
302
3368
  //#region src/cli/utils/render-score-header.ts
303
3369
  const buildScoreBarSegments = (score) => {
304
- const filledCount = Math.round(score / PERFECT_SCORE * SCORE_BAR_WIDTH_CHARS);
305
- const emptyCount = SCORE_BAR_WIDTH_CHARS - filledCount;
3370
+ const filledCount = Math.round(score / 100 * 50);
3371
+ const emptyCount = 50 - filledCount;
306
3372
  return {
307
3373
  filledSegment: "█".repeat(filledCount),
308
3374
  emptySegment: "░".repeat(emptyCount)
@@ -313,8 +3379,8 @@ const buildScoreBar = (score) => {
313
3379
  return colorizeByScore(filledSegment, score) + highlighter.dim(emptySegment);
314
3380
  };
315
3381
  const getDoctorFace = (score) => {
316
- if (score >= SCORE_GOOD_THRESHOLD) return ["◠ ◠", " ▽ "];
317
- if (score >= SCORE_OK_THRESHOLD) return ["• •", " ─ "];
3382
+ if (score >= 75) return ["◠ ◠", " ▽ "];
3383
+ if (score >= 50) return ["• •", " ─ "];
318
3384
  return ["x x", " ▽ "];
319
3385
  };
320
3386
  const BRANDING_LINE = `React Doctor ${highlighter.dim("(www.react.doctor)")}`;
@@ -333,7 +3399,7 @@ const printScoreHeader = (scoreResult) => {
333
3399
  const scoreNumber = colorizeByScore(`${scoreResult.score}`, scoreResult.score);
334
3400
  const scoreLabel = colorizeByScore(scoreResult.label, scoreResult.score);
335
3401
  const rightColumnLines = [
336
- `${scoreNumber} ${highlighter.dim(`/ ${PERFECT_SCORE}`)} ${scoreLabel}`,
3402
+ `${scoreNumber} ${highlighter.dim(`/ 100`)} ${scoreLabel}`,
337
3403
  buildScoreBar(scoreResult.score),
338
3404
  BRANDING_LINE,
339
3405
  ""
@@ -479,7 +3545,7 @@ const resolveOxlintNode = async (isLintEnabled, isQuiet) => {
479
3545
  const { shouldInstallNode } = await prompts({
480
3546
  type: "confirm",
481
3547
  name: "shouldInstallNode",
482
- message: `Install Node ${OXLINT_RECOMMENDED_NODE_MAJOR} via nvm to enable lint checks?`,
3548
+ message: `Install Node 24 via nvm to enable lint checks?`,
483
3549
  initial: true
484
3550
  });
485
3551
  if (shouldInstallNode) {
@@ -496,8 +3562,8 @@ const resolveOxlintNode = async (isLintEnabled, isQuiet) => {
496
3562
  logger.break();
497
3563
  return null;
498
3564
  }
499
- } else if (isNvmInstalled()) logger.dim(` Run: nvm install ${OXLINT_RECOMMENDED_NODE_MAJOR}`);
500
- else logger.dim(` Install nvm (https://github.com/nvm-sh/nvm) and run: nvm install ${OXLINT_RECOMMENDED_NODE_MAJOR}`);
3565
+ } else if (isNvmInstalled()) logger.dim(` Run: nvm install 24`);
3566
+ else logger.dim(` Install nvm (https://github.com/nvm-sh/nvm) and run: nvm install 24`);
501
3567
  logger.break();
502
3568
  return null;
503
3569
  };
@@ -739,7 +3805,7 @@ const CI_ENVIRONMENT_VARIABLES = [
739
3805
  const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || process.env.CI === "true";
740
3806
  //#endregion
741
3807
  //#region src/cli/utils/version.ts
742
- const VERSION = "0.2.0-beta.2";
3808
+ const VERSION = "0.2.0-beta.4";
743
3809
  //#endregion
744
3810
  //#region src/cli/utils/json-mode.ts
745
3811
  let context = null;
@@ -1259,7 +4325,7 @@ const runInstallSkill = async (options = {}) => {
1259
4325
  const selectedAgents = shouldSkipPrompts({ yes: options.yes }) ? detectedAgents : (await prompts({
1260
4326
  type: "multiselect",
1261
4327
  name: "agents",
1262
- message: `Install the ${highlighter.info(SKILL_NAME)} skill for:`,
4328
+ message: `Install the ${highlighter.info("react-doctor")} skill for:`,
1263
4329
  choices: detectedAgents.map((agent) => ({
1264
4330
  title: getSkillAgentConfig(agent).displayName,
1265
4331
  value: agent,