react-doctor 0.0.29 → 0.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +340 -65
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +350 -45
- package/dist/index.js.map +1 -1
- package/dist/react-doctor-plugin.js +24 -9
- package/dist/react-doctor-plugin.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,WAAA;AAAA,KAEA,SAAA;AAAA,UAUK,WAAA;EACf,aAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA,EAAW,SAAA;EACX,aAAA;EACA,gBAAA;EACA,eAAA;AAAA;AAAA,UAiCe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UA4Be,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAyBe,QAAA;EACf,aAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,UA2Ce,uBAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EACf,MAAA,GAAS,uBAAA;EACT,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA,GAAS,WAAA;AAAA;;;cChGE,WAAA,GAAe,SAAA,UAAmB,kBAAA,cAA8B,QAAA;AAAA,cAiBhE,iBAAA,GAAqB,SAAA;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,WAAA;AAAA,KAEA,SAAA;AAAA,UAUK,WAAA;EACf,aAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA,EAAW,SAAA;EACX,aAAA;EACA,gBAAA;EACA,eAAA;AAAA;AAAA,UAiCe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UA4Be,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAyBe,QAAA;EACf,aAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,UA2Ce,uBAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EACf,MAAA,GAAS,uBAAA;EACT,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA,GAAS,WAAA;AAAA;;;cChGE,WAAA,GAAe,SAAA,UAAmB,kBAAA,cAA8B,QAAA;AAAA,cAiBhE,iBAAA,GAAqB,SAAA;;;UClFjB,eAAA;EACf,IAAA;EACA,QAAA;EACA,YAAA;AAAA;AAAA,UAGe,cAAA;EACf,WAAA,EAAa,UAAA;EACb,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,WAAA;EACT,mBAAA;AAAA;AAAA,cAGW,QAAA,GACX,SAAA,UACA,OAAA,GAAS,eAAA,KACR,OAAA,CAAQ,cAAA"}
|
package/dist/index.js
CHANGED
|
@@ -12,12 +12,25 @@ import { fileURLToPath } from "node:url";
|
|
|
12
12
|
const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
|
|
13
13
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
14
14
|
const ERROR_PREVIEW_LENGTH_CHARS = 200;
|
|
15
|
+
const PERFECT_SCORE = 100;
|
|
16
|
+
const SCORE_GOOD_THRESHOLD = 75;
|
|
17
|
+
const SCORE_OK_THRESHOLD = 50;
|
|
15
18
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
16
19
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
17
20
|
const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
18
21
|
const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
|
|
19
22
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
23
|
+
const ERROR_RULE_PENALTY = 1.5;
|
|
24
|
+
const WARNING_RULE_PENALTY = .75;
|
|
25
|
+
const ERROR_ESTIMATED_FIX_RATE = .85;
|
|
26
|
+
const WARNING_ESTIMATED_FIX_RATE = .8;
|
|
20
27
|
const MAX_KNIP_RETRIES = 5;
|
|
28
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
29
|
+
"node_modules",
|
|
30
|
+
"dist",
|
|
31
|
+
"build",
|
|
32
|
+
"coverage"
|
|
33
|
+
]);
|
|
21
34
|
const AMI_WEBSITE_URL = "https://ami.dev";
|
|
22
35
|
const AMI_INSTALL_URL = `${AMI_WEBSITE_URL}/install.sh`;
|
|
23
36
|
|
|
@@ -71,6 +84,46 @@ const proxyFetch = async (url, init) => {
|
|
|
71
84
|
|
|
72
85
|
//#endregion
|
|
73
86
|
//#region src/utils/calculate-score.ts
|
|
87
|
+
const getScoreLabel = (score) => {
|
|
88
|
+
if (score >= SCORE_GOOD_THRESHOLD) return "Great";
|
|
89
|
+
if (score >= SCORE_OK_THRESHOLD) return "Needs work";
|
|
90
|
+
return "Critical";
|
|
91
|
+
};
|
|
92
|
+
const countUniqueRules = (diagnostics) => {
|
|
93
|
+
const errorRules = /* @__PURE__ */ new Set();
|
|
94
|
+
const warningRules = /* @__PURE__ */ new Set();
|
|
95
|
+
for (const diagnostic of diagnostics) {
|
|
96
|
+
const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
97
|
+
if (diagnostic.severity === "error") errorRules.add(ruleKey);
|
|
98
|
+
else warningRules.add(ruleKey);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
errorRuleCount: errorRules.size,
|
|
102
|
+
warningRuleCount: warningRules.size
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
const scoreFromRuleCounts = (errorRuleCount, warningRuleCount) => {
|
|
106
|
+
const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;
|
|
107
|
+
return Math.max(0, Math.round(PERFECT_SCORE - penalty));
|
|
108
|
+
};
|
|
109
|
+
const estimateScoreLocally = (diagnostics) => {
|
|
110
|
+
const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);
|
|
111
|
+
const currentScore = scoreFromRuleCounts(errorRuleCount, warningRuleCount);
|
|
112
|
+
const estimatedScore = scoreFromRuleCounts(Math.round(errorRuleCount * (1 - ERROR_ESTIMATED_FIX_RATE)), Math.round(warningRuleCount * (1 - WARNING_ESTIMATED_FIX_RATE)));
|
|
113
|
+
return {
|
|
114
|
+
currentScore,
|
|
115
|
+
currentLabel: getScoreLabel(currentScore),
|
|
116
|
+
estimatedScore,
|
|
117
|
+
estimatedLabel: getScoreLabel(estimatedScore)
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
const calculateScoreLocally = (diagnostics) => {
|
|
121
|
+
const { currentScore, currentLabel } = estimateScoreLocally(diagnostics);
|
|
122
|
+
return {
|
|
123
|
+
score: currentScore,
|
|
124
|
+
label: currentLabel
|
|
125
|
+
};
|
|
126
|
+
};
|
|
74
127
|
const calculateScore = async (diagnostics) => {
|
|
75
128
|
try {
|
|
76
129
|
const response = await proxyFetch(SCORE_API_URL, {
|
|
@@ -78,10 +131,10 @@ const calculateScore = async (diagnostics) => {
|
|
|
78
131
|
headers: { "Content-Type": "application/json" },
|
|
79
132
|
body: JSON.stringify({ diagnostics })
|
|
80
133
|
});
|
|
81
|
-
if (!response.ok) return
|
|
134
|
+
if (!response.ok) return calculateScoreLocally(diagnostics);
|
|
82
135
|
return await response.json();
|
|
83
136
|
} catch {
|
|
84
|
-
return
|
|
137
|
+
return calculateScoreLocally(diagnostics);
|
|
85
138
|
}
|
|
86
139
|
};
|
|
87
140
|
|
|
@@ -89,13 +142,33 @@ const calculateScore = async (diagnostics) => {
|
|
|
89
142
|
//#region src/plugin/constants.ts
|
|
90
143
|
const MOTION_LIBRARY_PACKAGES = new Set(["framer-motion", "motion"]);
|
|
91
144
|
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/utils/is-file.ts
|
|
147
|
+
const isFile = (filePath) => {
|
|
148
|
+
try {
|
|
149
|
+
return fs.statSync(filePath).isFile();
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
92
155
|
//#endregion
|
|
93
156
|
//#region src/utils/read-package-json.ts
|
|
94
|
-
const readPackageJson = (packageJsonPath) =>
|
|
157
|
+
const readPackageJson = (packageJsonPath) => {
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof Error && "code" in error) {
|
|
162
|
+
const { code } = error;
|
|
163
|
+
if (code === "EISDIR" || code === "EACCES") return {};
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
95
168
|
|
|
96
169
|
//#endregion
|
|
97
170
|
//#region src/utils/check-reduced-motion.ts
|
|
98
|
-
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion";
|
|
171
|
+
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
|
|
99
172
|
const REDUCED_MOTION_FILE_GLOBS = "\"*.ts\" \"*.tsx\" \"*.js\" \"*.jsx\" \"*.css\" \"*.scss\"";
|
|
100
173
|
const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
101
174
|
filePath: "package.json",
|
|
@@ -111,7 +184,7 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
|
111
184
|
};
|
|
112
185
|
const checkReducedMotion = (rootDirectory) => {
|
|
113
186
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
114
|
-
if (!
|
|
187
|
+
if (!isFile(packageJsonPath)) return [];
|
|
115
188
|
let hasMotionLibrary = false;
|
|
116
189
|
try {
|
|
117
190
|
const packageJson = readPackageJson(packageJsonPath);
|
|
@@ -139,7 +212,7 @@ const checkReducedMotion = (rootDirectory) => {
|
|
|
139
212
|
//#region src/utils/match-glob-pattern.ts
|
|
140
213
|
const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
|
|
141
214
|
const compileGlobPattern = (pattern) => {
|
|
142
|
-
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
215
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\//, "");
|
|
143
216
|
let regexSource = "^";
|
|
144
217
|
let characterIndex = 0;
|
|
145
218
|
while (characterIndex < normalizedPattern.length) if (normalizedPattern[characterIndex] === "*" && normalizedPattern[characterIndex + 1] === "*") if (normalizedPattern[characterIndex + 2] === "/") {
|
|
@@ -163,17 +236,72 @@ const compileGlobPattern = (pattern) => {
|
|
|
163
236
|
return new RegExp(regexSource);
|
|
164
237
|
};
|
|
165
238
|
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/utils/is-ignored-file.ts
|
|
241
|
+
const toRelativePath = (filePath, rootDirectory) => {
|
|
242
|
+
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
243
|
+
const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
|
|
244
|
+
if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
|
|
245
|
+
return normalizedFilePath.replace(/^\.\//, "");
|
|
246
|
+
};
|
|
247
|
+
const compileIgnoredFilePatterns = (userConfig) => Array.isArray(userConfig?.ignore?.files) ? userConfig.ignore.files.map(compileGlobPattern) : [];
|
|
248
|
+
const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
|
|
249
|
+
if (patterns.length === 0) return false;
|
|
250
|
+
const relativePath = toRelativePath(filePath, rootDirectory);
|
|
251
|
+
return patterns.some((pattern) => pattern.test(relativePath));
|
|
252
|
+
};
|
|
253
|
+
|
|
166
254
|
//#endregion
|
|
167
255
|
//#region src/utils/filter-diagnostics.ts
|
|
168
|
-
const filterIgnoredDiagnostics = (diagnostics, config) => {
|
|
256
|
+
const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory) => {
|
|
169
257
|
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
|
|
170
|
-
const ignoredFilePatterns =
|
|
258
|
+
const ignoredFilePatterns = compileIgnoredFilePatterns(config);
|
|
171
259
|
if (ignoredRules.size === 0 && ignoredFilePatterns.length === 0) return diagnostics;
|
|
172
260
|
return diagnostics.filter((diagnostic) => {
|
|
173
261
|
const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
174
262
|
if (ignoredRules.has(ruleIdentifier)) return false;
|
|
175
|
-
|
|
176
|
-
|
|
263
|
+
if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
|
|
264
|
+
return true;
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
const DISABLE_NEXT_LINE_PATTERN = /\/\/\s*react-doctor-disable-next-line\b(?:\s+(.+))?/;
|
|
268
|
+
const DISABLE_LINE_PATTERN = /\/\/\s*react-doctor-disable-line\b(?:\s+(.+))?/;
|
|
269
|
+
const isRuleSuppressed = (commentRules, ruleId) => {
|
|
270
|
+
if (!commentRules?.trim()) return true;
|
|
271
|
+
return commentRules.split(/[,\s]+/).some((rule) => rule.trim() === ruleId);
|
|
272
|
+
};
|
|
273
|
+
const filterInlineSuppressions = (diagnostics, rootDirectory) => {
|
|
274
|
+
const fileLineCache = /* @__PURE__ */ new Map();
|
|
275
|
+
const getFileLines = (filePath) => {
|
|
276
|
+
const cached = fileLineCache.get(filePath);
|
|
277
|
+
if (cached !== void 0) return cached;
|
|
278
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
|
|
279
|
+
try {
|
|
280
|
+
const lines = fs.readFileSync(absolutePath, "utf-8").split("\n");
|
|
281
|
+
fileLineCache.set(filePath, lines);
|
|
282
|
+
return lines;
|
|
283
|
+
} catch {
|
|
284
|
+
fileLineCache.set(filePath, null);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
return diagnostics.filter((diagnostic) => {
|
|
289
|
+
if (diagnostic.line <= 0) return true;
|
|
290
|
+
const lines = getFileLines(diagnostic.filePath);
|
|
291
|
+
if (!lines) return true;
|
|
292
|
+
const ruleId = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
293
|
+
const currentLine = lines[diagnostic.line - 1];
|
|
294
|
+
if (currentLine) {
|
|
295
|
+
const lineMatch = currentLine.match(DISABLE_LINE_PATTERN);
|
|
296
|
+
if (lineMatch && isRuleSuppressed(lineMatch[1], ruleId)) return false;
|
|
297
|
+
}
|
|
298
|
+
if (diagnostic.line >= 2) {
|
|
299
|
+
const prevLine = lines[diagnostic.line - 2];
|
|
300
|
+
if (prevLine) {
|
|
301
|
+
const nextLineMatch = prevLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
302
|
+
if (nextLineMatch && isRuleSuppressed(nextLineMatch[1], ruleId)) return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
177
305
|
return true;
|
|
178
306
|
});
|
|
179
307
|
};
|
|
@@ -182,21 +310,21 @@ const filterIgnoredDiagnostics = (diagnostics, config) => {
|
|
|
182
310
|
//#region src/utils/combine-diagnostics.ts
|
|
183
311
|
const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
|
|
184
312
|
const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isDiffMode, userConfig) => {
|
|
185
|
-
const
|
|
313
|
+
const merged = [
|
|
186
314
|
...lintDiagnostics,
|
|
187
315
|
...deadCodeDiagnostics,
|
|
188
316
|
...isDiffMode ? [] : checkReducedMotion(directory)
|
|
189
317
|
];
|
|
190
|
-
return userConfig ? filterIgnoredDiagnostics(
|
|
318
|
+
return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(merged, userConfig, directory) : merged, directory);
|
|
191
319
|
};
|
|
192
320
|
|
|
193
321
|
//#endregion
|
|
194
322
|
//#region src/utils/find-monorepo-root.ts
|
|
195
323
|
const isMonorepoRoot = (directory) => {
|
|
196
|
-
if (
|
|
197
|
-
if (
|
|
324
|
+
if (isFile(path.join(directory, "pnpm-workspace.yaml"))) return true;
|
|
325
|
+
if (isFile(path.join(directory, "nx.json"))) return true;
|
|
198
326
|
const packageJsonPath = path.join(directory, "package.json");
|
|
199
|
-
if (!
|
|
327
|
+
if (!isFile(packageJsonPath)) return false;
|
|
200
328
|
const packageJson = readPackageJson(packageJsonPath);
|
|
201
329
|
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
202
330
|
};
|
|
@@ -209,6 +337,10 @@ const findMonorepoRoot = (startDirectory) => {
|
|
|
209
337
|
return null;
|
|
210
338
|
};
|
|
211
339
|
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/utils/is-plain-object.ts
|
|
342
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
343
|
+
|
|
212
344
|
//#endregion
|
|
213
345
|
//#region src/utils/discover-project.ts
|
|
214
346
|
const REACT_COMPILER_PACKAGES = new Set([
|
|
@@ -251,12 +383,6 @@ const FRAMEWORK_PACKAGES = {
|
|
|
251
383
|
expo: "expo",
|
|
252
384
|
"react-native": "react-native"
|
|
253
385
|
};
|
|
254
|
-
const IGNORED_DIRECTORIES = new Set([
|
|
255
|
-
"node_modules",
|
|
256
|
-
"dist",
|
|
257
|
-
"build",
|
|
258
|
-
"coverage"
|
|
259
|
-
]);
|
|
260
386
|
const countSourceFilesViaFilesystem = (rootDirectory) => {
|
|
261
387
|
let count = 0;
|
|
262
388
|
const stack = [rootDirectory];
|
|
@@ -297,16 +423,129 @@ const detectFramework = (dependencies) => {
|
|
|
297
423
|
for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
|
|
298
424
|
return "unknown";
|
|
299
425
|
};
|
|
426
|
+
const isCatalogReference = (version) => version.startsWith("catalog:");
|
|
427
|
+
const extractCatalogName = (version) => {
|
|
428
|
+
if (!isCatalogReference(version)) return null;
|
|
429
|
+
const name = version.slice(8).trim();
|
|
430
|
+
return name.length > 0 ? name : null;
|
|
431
|
+
};
|
|
432
|
+
const resolveVersionFromCatalog = (catalog, packageName) => {
|
|
433
|
+
const version = catalog[packageName];
|
|
434
|
+
if (typeof version === "string" && !isCatalogReference(version)) return version;
|
|
435
|
+
return null;
|
|
436
|
+
};
|
|
437
|
+
const parsePnpmWorkspaceCatalogs = (rootDirectory) => {
|
|
438
|
+
const workspacePath = path.join(rootDirectory, "pnpm-workspace.yaml");
|
|
439
|
+
if (!isFile(workspacePath)) return {
|
|
440
|
+
defaultCatalog: {},
|
|
441
|
+
namedCatalogs: {}
|
|
442
|
+
};
|
|
443
|
+
const content = fs.readFileSync(workspacePath, "utf-8");
|
|
444
|
+
const defaultCatalog = {};
|
|
445
|
+
const namedCatalogs = {};
|
|
446
|
+
let currentSection = "none";
|
|
447
|
+
let currentCatalogName = "";
|
|
448
|
+
for (const line of content.split("\n")) {
|
|
449
|
+
const trimmed = line.trim();
|
|
450
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
451
|
+
const indentLevel = line.search(/\S/);
|
|
452
|
+
if (indentLevel === 0 && trimmed === "catalog:") {
|
|
453
|
+
currentSection = "catalog";
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (indentLevel === 0 && trimmed === "catalogs:") {
|
|
457
|
+
currentSection = "catalogs";
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (indentLevel === 0) {
|
|
461
|
+
currentSection = "none";
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (currentSection === "catalog" && indentLevel > 0) {
|
|
465
|
+
const colonIndex = trimmed.indexOf(":");
|
|
466
|
+
if (colonIndex > 0) {
|
|
467
|
+
const key = trimmed.slice(0, colonIndex).trim().replace(/["']/g, "");
|
|
468
|
+
const value = trimmed.slice(colonIndex + 1).trim().replace(/["']/g, "");
|
|
469
|
+
if (key && value) defaultCatalog[key] = value;
|
|
470
|
+
}
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (currentSection === "catalogs" && indentLevel > 0) {
|
|
474
|
+
if (trimmed.endsWith(":") && !trimmed.includes(" ")) {
|
|
475
|
+
currentCatalogName = trimmed.slice(0, -1).replace(/["']/g, "");
|
|
476
|
+
currentSection = "named-catalog";
|
|
477
|
+
namedCatalogs[currentCatalogName] = {};
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (currentSection === "named-catalog" && indentLevel > 0) {
|
|
482
|
+
if (indentLevel <= 2 && trimmed.endsWith(":") && !trimmed.includes(" ")) {
|
|
483
|
+
currentCatalogName = trimmed.slice(0, -1).replace(/["']/g, "");
|
|
484
|
+
namedCatalogs[currentCatalogName] = {};
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const colonIndex = trimmed.indexOf(":");
|
|
488
|
+
if (colonIndex > 0 && currentCatalogName) {
|
|
489
|
+
const key = trimmed.slice(0, colonIndex).trim().replace(/["']/g, "");
|
|
490
|
+
const value = trimmed.slice(colonIndex + 1).trim().replace(/["']/g, "");
|
|
491
|
+
if (key && value) namedCatalogs[currentCatalogName][key] = value;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
defaultCatalog,
|
|
497
|
+
namedCatalogs
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
const resolveCatalogVersionFromCollection = (catalogs, packageName, catalogReference) => {
|
|
501
|
+
if (catalogReference) {
|
|
502
|
+
const namedCatalog = catalogs.namedCatalogs[catalogReference];
|
|
503
|
+
if (namedCatalog?.[packageName]) return namedCatalog[packageName];
|
|
504
|
+
}
|
|
505
|
+
if (catalogs.defaultCatalog[packageName]) return catalogs.defaultCatalog[packageName];
|
|
506
|
+
for (const namedCatalog of Object.values(catalogs.namedCatalogs)) if (namedCatalog[packageName]) return namedCatalog[packageName];
|
|
507
|
+
return null;
|
|
508
|
+
};
|
|
509
|
+
const resolveCatalogVersion = (packageJson, packageName, rootDirectory) => {
|
|
510
|
+
const rawVersion = collectAllDependencies(packageJson)[packageName];
|
|
511
|
+
const catalogName = rawVersion ? extractCatalogName(rawVersion) : null;
|
|
512
|
+
const raw = packageJson;
|
|
513
|
+
if (isPlainObject(raw.catalog)) {
|
|
514
|
+
const version = resolveVersionFromCatalog(raw.catalog, packageName);
|
|
515
|
+
if (version) return version;
|
|
516
|
+
}
|
|
517
|
+
if (isPlainObject(raw.catalogs)) {
|
|
518
|
+
if (catalogName && isPlainObject(raw.catalogs[catalogName])) {
|
|
519
|
+
const version = resolveVersionFromCatalog(raw.catalogs[catalogName], packageName);
|
|
520
|
+
if (version) return version;
|
|
521
|
+
}
|
|
522
|
+
for (const catalogEntries of Object.values(raw.catalogs)) if (isPlainObject(catalogEntries)) {
|
|
523
|
+
const version = resolveVersionFromCatalog(catalogEntries, packageName);
|
|
524
|
+
if (version) return version;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const workspaces = packageJson.workspaces;
|
|
528
|
+
if (workspaces && !Array.isArray(workspaces) && isPlainObject(workspaces.catalog)) {
|
|
529
|
+
const version = resolveVersionFromCatalog(workspaces.catalog, packageName);
|
|
530
|
+
if (version) return version;
|
|
531
|
+
}
|
|
532
|
+
if (rootDirectory) {
|
|
533
|
+
const pnpmVersion = resolveCatalogVersionFromCollection(parsePnpmWorkspaceCatalogs(rootDirectory), packageName, catalogName);
|
|
534
|
+
if (pnpmVersion) return pnpmVersion;
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
};
|
|
300
538
|
const extractDependencyInfo = (packageJson) => {
|
|
301
539
|
const allDependencies = collectAllDependencies(packageJson);
|
|
540
|
+
const rawVersion = allDependencies.react ?? null;
|
|
302
541
|
return {
|
|
303
|
-
reactVersion:
|
|
542
|
+
reactVersion: rawVersion && !isCatalogReference(rawVersion) ? rawVersion : null,
|
|
304
543
|
framework: detectFramework(allDependencies)
|
|
305
544
|
};
|
|
306
545
|
};
|
|
307
546
|
const parsePnpmWorkspacePatterns = (rootDirectory) => {
|
|
308
547
|
const workspacePath = path.join(rootDirectory, "pnpm-workspace.yaml");
|
|
309
|
-
if (!
|
|
548
|
+
if (!isFile(workspacePath)) return [];
|
|
310
549
|
const content = fs.readFileSync(workspacePath, "utf-8");
|
|
311
550
|
const patterns = [];
|
|
312
551
|
let isInsidePackagesBlock = false;
|
|
@@ -332,14 +571,14 @@ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
|
|
|
332
571
|
const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
|
|
333
572
|
if (!cleanPattern.includes("*")) {
|
|
334
573
|
const directoryPath = path.join(rootDirectory, cleanPattern);
|
|
335
|
-
if (fs.existsSync(directoryPath) &&
|
|
574
|
+
if (fs.existsSync(directoryPath) && isFile(path.join(directoryPath, "package.json"))) return [directoryPath];
|
|
336
575
|
return [];
|
|
337
576
|
}
|
|
338
577
|
const wildcardIndex = cleanPattern.indexOf("*");
|
|
339
578
|
const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
|
|
340
579
|
const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
|
|
341
580
|
if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) return [];
|
|
342
|
-
return fs.readdirSync(baseDirectory).map((entry) => path.join(baseDirectory, entry, suffixAfterWildcard)).filter((entryPath) => fs.existsSync(entryPath) && fs.statSync(entryPath).isDirectory() &&
|
|
581
|
+
return fs.readdirSync(baseDirectory).map((entry) => path.join(baseDirectory, entry, suffixAfterWildcard)).filter((entryPath) => fs.existsSync(entryPath) && fs.statSync(entryPath).isDirectory() && isFile(path.join(entryPath, "package.json")));
|
|
343
582
|
};
|
|
344
583
|
const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
345
584
|
const monorepoRoot = findMonorepoRoot(directory);
|
|
@@ -347,11 +586,17 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
347
586
|
reactVersion: null,
|
|
348
587
|
framework: "unknown"
|
|
349
588
|
};
|
|
350
|
-
const
|
|
589
|
+
const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
|
|
590
|
+
if (!isFile(monorepoPackageJsonPath)) return {
|
|
591
|
+
reactVersion: null,
|
|
592
|
+
framework: "unknown"
|
|
593
|
+
};
|
|
594
|
+
const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
|
|
351
595
|
const rootInfo = extractDependencyInfo(rootPackageJson);
|
|
596
|
+
const catalogVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot);
|
|
352
597
|
const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
|
|
353
598
|
return {
|
|
354
|
-
reactVersion: rootInfo.reactVersion ?? workspaceInfo.reactVersion,
|
|
599
|
+
reactVersion: rootInfo.reactVersion ?? catalogVersion ?? workspaceInfo.reactVersion,
|
|
355
600
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
356
601
|
};
|
|
357
602
|
};
|
|
@@ -377,7 +622,7 @@ const hasCompilerPackage = (packageJson) => {
|
|
|
377
622
|
return Object.keys(allDependencies).some((packageName) => REACT_COMPILER_PACKAGES.has(packageName));
|
|
378
623
|
};
|
|
379
624
|
const fileContainsPattern = (filePath, pattern) => {
|
|
380
|
-
if (!
|
|
625
|
+
if (!isFile(filePath)) return false;
|
|
381
626
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
382
627
|
return pattern.test(content);
|
|
383
628
|
};
|
|
@@ -391,7 +636,7 @@ const detectReactCompiler = (directory, packageJson) => {
|
|
|
391
636
|
let ancestorDirectory = path.dirname(directory);
|
|
392
637
|
while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
|
|
393
638
|
const ancestorPackagePath = path.join(ancestorDirectory, "package.json");
|
|
394
|
-
if (
|
|
639
|
+
if (isFile(ancestorPackagePath)) {
|
|
395
640
|
if (hasCompilerPackage(readPackageJson(ancestorPackagePath))) return true;
|
|
396
641
|
}
|
|
397
642
|
ancestorDirectory = path.dirname(ancestorDirectory);
|
|
@@ -400,9 +645,17 @@ const detectReactCompiler = (directory, packageJson) => {
|
|
|
400
645
|
};
|
|
401
646
|
const discoverProject = (directory) => {
|
|
402
647
|
const packageJsonPath = path.join(directory, "package.json");
|
|
403
|
-
if (!
|
|
648
|
+
if (!isFile(packageJsonPath)) throw new Error(`No package.json found in ${directory}`);
|
|
404
649
|
const packageJson = readPackageJson(packageJsonPath);
|
|
405
650
|
let { reactVersion, framework } = extractDependencyInfo(packageJson);
|
|
651
|
+
if (!reactVersion) reactVersion = resolveCatalogVersion(packageJson, "react", directory);
|
|
652
|
+
if (!reactVersion) {
|
|
653
|
+
const monorepoRoot = findMonorepoRoot(directory);
|
|
654
|
+
if (monorepoRoot) {
|
|
655
|
+
const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
|
|
656
|
+
if (isFile(monorepoPackageJsonPath)) reactVersion = resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), "react", monorepoRoot);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
406
659
|
if (!reactVersion || framework === "unknown") {
|
|
407
660
|
const workspaceInfo = findReactInWorkspaces(directory, packageJson);
|
|
408
661
|
if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
|
|
@@ -432,10 +685,9 @@ const discoverProject = (directory) => {
|
|
|
432
685
|
//#region src/utils/load-config.ts
|
|
433
686
|
const CONFIG_FILENAME = "react-doctor.config.json";
|
|
434
687
|
const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
|
|
435
|
-
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
436
688
|
const loadConfig = (rootDirectory) => {
|
|
437
689
|
const configFilePath = path.join(rootDirectory, CONFIG_FILENAME);
|
|
438
|
-
if (
|
|
690
|
+
if (isFile(configFilePath)) try {
|
|
439
691
|
const fileContent = fs.readFileSync(configFilePath, "utf-8");
|
|
440
692
|
const parsed = JSON.parse(fileContent);
|
|
441
693
|
if (isPlainObject(parsed)) return parsed;
|
|
@@ -444,7 +696,7 @@ const loadConfig = (rootDirectory) => {
|
|
|
444
696
|
console.warn(`Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
|
|
445
697
|
}
|
|
446
698
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
447
|
-
if (
|
|
699
|
+
if (isFile(packageJsonPath)) try {
|
|
448
700
|
const fileContent = fs.readFileSync(packageJsonPath, "utf-8");
|
|
449
701
|
const embeddedConfig = JSON.parse(fileContent)[PACKAGE_JSON_CONFIG_KEY];
|
|
450
702
|
if (isPlainObject(embeddedConfig)) return embeddedConfig;
|
|
@@ -454,6 +706,49 @@ const loadConfig = (rootDirectory) => {
|
|
|
454
706
|
return null;
|
|
455
707
|
};
|
|
456
708
|
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/utils/resolve-lint-include-paths.ts
|
|
711
|
+
const listSourceFilesViaGit = (rootDirectory) => {
|
|
712
|
+
const result = spawnSync("git", [
|
|
713
|
+
"ls-files",
|
|
714
|
+
"--cached",
|
|
715
|
+
"--others",
|
|
716
|
+
"--exclude-standard"
|
|
717
|
+
], {
|
|
718
|
+
cwd: rootDirectory,
|
|
719
|
+
encoding: "utf-8",
|
|
720
|
+
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
721
|
+
});
|
|
722
|
+
if (result.error || result.status !== 0) return null;
|
|
723
|
+
return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
|
|
724
|
+
};
|
|
725
|
+
const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
726
|
+
const filePaths = [];
|
|
727
|
+
const stack = [rootDirectory];
|
|
728
|
+
while (stack.length > 0) {
|
|
729
|
+
const currentDirectory = stack.pop();
|
|
730
|
+
const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
|
|
731
|
+
for (const entry of entries) {
|
|
732
|
+
const absolutePath = path.join(currentDirectory, entry.name);
|
|
733
|
+
if (entry.isDirectory()) {
|
|
734
|
+
if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) filePaths.push(path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return filePaths;
|
|
741
|
+
};
|
|
742
|
+
const listSourceFiles = (rootDirectory) => listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory);
|
|
743
|
+
const resolveLintIncludePaths = (rootDirectory, userConfig) => {
|
|
744
|
+
if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
|
|
745
|
+
const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
|
|
746
|
+
return listSourceFiles(rootDirectory).filter((filePath) => {
|
|
747
|
+
if (!JSX_FILE_PATTERN.test(filePath)) return false;
|
|
748
|
+
return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
|
|
749
|
+
});
|
|
750
|
+
};
|
|
751
|
+
|
|
457
752
|
//#endregion
|
|
458
753
|
//#region src/utils/run-knip.ts
|
|
459
754
|
const KNIP_CATEGORY_MAP = {
|
|
@@ -542,7 +837,7 @@ const runKnip = async (rootDirectory) => {
|
|
|
542
837
|
let knipResult;
|
|
543
838
|
if (monorepoRoot) {
|
|
544
839
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
545
|
-
const workspaceName = (
|
|
840
|
+
const workspaceName = (isFile(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) : {}).name ?? path.basename(rootDirectory);
|
|
546
841
|
try {
|
|
547
842
|
knipResult = await runKnipWithOptions(monorepoRoot, workspaceName);
|
|
548
843
|
} catch {
|
|
@@ -712,24 +1007,27 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
|
|
|
712
1007
|
|
|
713
1008
|
//#endregion
|
|
714
1009
|
//#region src/utils/neutralize-disable-directives.ts
|
|
715
|
-
const findFilesWithDisableDirectives = (rootDirectory) => {
|
|
716
|
-
const
|
|
1010
|
+
const findFilesWithDisableDirectives = (rootDirectory, includePaths) => {
|
|
1011
|
+
const grepArgs = [
|
|
717
1012
|
"grep",
|
|
718
1013
|
"-l",
|
|
719
1014
|
"--untracked",
|
|
720
1015
|
"-E",
|
|
721
1016
|
"(eslint|oxlint)-disable"
|
|
722
|
-
]
|
|
1017
|
+
];
|
|
1018
|
+
if (includePaths && includePaths.length > 0) grepArgs.push("--", ...includePaths);
|
|
1019
|
+
const result = spawnSync("git", grepArgs, {
|
|
723
1020
|
cwd: rootDirectory,
|
|
724
1021
|
encoding: "utf-8",
|
|
725
1022
|
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
726
1023
|
});
|
|
727
1024
|
if (result.error || result.status === null) return [];
|
|
1025
|
+
if (result.status !== 0 && result.stdout.trim().length === 0) return [];
|
|
728
1026
|
return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
|
|
729
1027
|
};
|
|
730
1028
|
const neutralizeContent = (content) => content.replaceAll("eslint-disable", "eslint_disable").replaceAll("oxlint-disable", "oxlint_disable");
|
|
731
|
-
const neutralizeDisableDirectives = (rootDirectory) => {
|
|
732
|
-
const filePaths = findFilesWithDisableDirectives(rootDirectory);
|
|
1029
|
+
const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
|
|
1030
|
+
const filePaths = findFilesWithDisableDirectives(rootDirectory, includePaths);
|
|
733
1031
|
const originalContents = /* @__PURE__ */ new Map();
|
|
734
1032
|
for (const relativePath of filePaths) {
|
|
735
1033
|
const absolutePath = path.join(rootDirectory, relativePath);
|
|
@@ -825,7 +1123,7 @@ const RULE_CATEGORY_MAP = {
|
|
|
825
1123
|
"react-doctor/rn-no-single-element-style-array": "React Native"
|
|
826
1124
|
};
|
|
827
1125
|
const RULE_HELP_MAP = {
|
|
828
|
-
"no-derived-state-effect": "For derived state, compute inline: `const x = fn(dep)`. For state resets on prop change, use a key prop: `<Component key={prop}
|
|
1126
|
+
"no-derived-state-effect": "For derived state, compute inline: `const x = fn(dep)`. For state resets on prop change, use a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
829
1127
|
"no-fetch-in-effect": "Use `useQuery()` from @tanstack/react-query, `useSWR()`, or fetch in a Server Component instead",
|
|
830
1128
|
"no-cascading-set-state": "Combine into useReducer: `const [state, dispatch] = useReducer(reducer, initialState)`",
|
|
831
1129
|
"no-effect-event-handler": "Move the conditional logic into onClick, onChange, or onSubmit handlers directly",
|
|
@@ -865,7 +1163,7 @@ const RULE_HELP_MAP = {
|
|
|
865
1163
|
"nextjs-no-use-search-params-without-suspense": "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
|
|
866
1164
|
"nextjs-no-client-fetch-for-server-data": "Remove 'use client' and fetch directly in the Server Component — no API round-trip, secrets stay on server",
|
|
867
1165
|
"nextjs-missing-metadata": "Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`",
|
|
868
|
-
"nextjs-no-client-side-redirect": "Use `redirect('/path')` from 'next/navigation' in
|
|
1166
|
+
"nextjs-no-client-side-redirect": "Use `redirect('/path')` from 'next/navigation' directly (works in both server and client components), or handle in middleware",
|
|
869
1167
|
"nextjs-no-redirect-in-try-catch": "Move the redirect/notFound call outside the try block, or add `unstable_rethrow(error)` in the catch",
|
|
870
1168
|
"nextjs-image-missing-sizes": "Add sizes for responsive behavior: `sizes=\"(max-width: 768px) 100vw, 50vw\"` matching your layout breakpoints",
|
|
871
1169
|
"nextjs-no-native-script": "`import Script from \"next/script\"` — use `strategy=\"afterInteractive\"` for analytics or `\"lazyOnload\"` for widgets",
|
|
@@ -953,7 +1251,14 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
953
1251
|
child.stdout.on("data", (buffer) => stdoutBuffers.push(buffer));
|
|
954
1252
|
child.stderr.on("data", (buffer) => stderrBuffers.push(buffer));
|
|
955
1253
|
child.on("error", (error) => reject(/* @__PURE__ */ new Error(`Failed to run oxlint: ${error.message}`)));
|
|
956
|
-
child.on("close", () => {
|
|
1254
|
+
child.on("close", (code, signal) => {
|
|
1255
|
+
if (signal) {
|
|
1256
|
+
const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
|
|
1257
|
+
const hint = signal === "SIGABRT" ? " (out of memory — try scanning fewer files with --diff)" : "";
|
|
1258
|
+
const detail = stderrOutput ? `: ${stderrOutput}` : "";
|
|
1259
|
+
reject(/* @__PURE__ */ new Error(`oxlint was killed by ${signal}${hint}${detail}`));
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
957
1262
|
const output = Buffer.concat(stdoutBuffers).toString("utf-8").trim();
|
|
958
1263
|
if (!output) {
|
|
959
1264
|
const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
|
|
@@ -998,7 +1303,7 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
|
|
|
998
1303
|
framework,
|
|
999
1304
|
hasReactCompiler
|
|
1000
1305
|
});
|
|
1001
|
-
const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory);
|
|
1306
|
+
const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
1002
1307
|
try {
|
|
1003
1308
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1004
1309
|
const baseArgs = [
|
|
@@ -1114,9 +1419,9 @@ const diagnose = async (directory, options = {}) => {
|
|
|
1114
1419
|
const effectiveLint = options.lint ?? userConfig?.lint ?? true;
|
|
1115
1420
|
const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;
|
|
1116
1421
|
if (!projectInfo.reactVersion) throw new Error("No React dependency found in package.json");
|
|
1117
|
-
const
|
|
1422
|
+
const lintIncludePaths = computeJsxIncludePaths(includePaths) ?? resolveLintIncludePaths(resolvedDirectory, userConfig);
|
|
1118
1423
|
const emptyDiagnostics = [];
|
|
1119
|
-
const lintPromise = effectiveLint ? runOxlint(resolvedDirectory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler,
|
|
1424
|
+
const lintPromise = effectiveLint ? runOxlint(resolvedDirectory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler, lintIncludePaths).catch((error) => {
|
|
1120
1425
|
console.error("Lint failed:", error);
|
|
1121
1426
|
return emptyDiagnostics;
|
|
1122
1427
|
}) : Promise.resolve(emptyDiagnostics);
|