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/cli.js
CHANGED
|
@@ -32,8 +32,7 @@ const OPEN_BASE_URL = "https://www.react.doctor/open";
|
|
|
32
32
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
33
33
|
const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
34
34
|
const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
|
|
35
|
-
const OFFLINE_MESSAGE = "
|
|
36
|
-
const OFFLINE_FLAG_MESSAGE = "Score not calculated. Remove --offline to calculate score.";
|
|
35
|
+
const OFFLINE_MESSAGE = "Score calculated locally (offline mode).";
|
|
37
36
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
38
37
|
const ERROR_RULE_PENALTY = 1.5;
|
|
39
38
|
const WARNING_RULE_PENALTY = .75;
|
|
@@ -42,6 +41,12 @@ const WARNING_ESTIMATED_FIX_RATE = .8;
|
|
|
42
41
|
const MAX_KNIP_RETRIES = 5;
|
|
43
42
|
const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
|
|
44
43
|
const OXLINT_RECOMMENDED_NODE_MAJOR = 24;
|
|
44
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
45
|
+
"node_modules",
|
|
46
|
+
"dist",
|
|
47
|
+
"build",
|
|
48
|
+
"coverage"
|
|
49
|
+
]);
|
|
45
50
|
const AMI_WEBSITE_URL = "https://ami.dev";
|
|
46
51
|
const AMI_INSTALL_URL = `${AMI_WEBSITE_URL}/install.sh`;
|
|
47
52
|
const AMI_RELEASES_URL = "https://github.com/millionco/ami-releases/releases";
|
|
@@ -129,6 +134,13 @@ const estimateScoreLocally = (diagnostics) => {
|
|
|
129
134
|
estimatedLabel: getScoreLabel(estimatedScore)
|
|
130
135
|
};
|
|
131
136
|
};
|
|
137
|
+
const calculateScoreLocally = (diagnostics) => {
|
|
138
|
+
const { currentScore, currentLabel } = estimateScoreLocally(diagnostics);
|
|
139
|
+
return {
|
|
140
|
+
score: currentScore,
|
|
141
|
+
label: currentLabel
|
|
142
|
+
};
|
|
143
|
+
};
|
|
132
144
|
const calculateScore = async (diagnostics) => {
|
|
133
145
|
try {
|
|
134
146
|
const response = await proxyFetch(SCORE_API_URL, {
|
|
@@ -136,10 +148,10 @@ const calculateScore = async (diagnostics) => {
|
|
|
136
148
|
headers: { "Content-Type": "application/json" },
|
|
137
149
|
body: JSON.stringify({ diagnostics })
|
|
138
150
|
});
|
|
139
|
-
if (!response.ok) return
|
|
151
|
+
if (!response.ok) return calculateScoreLocally(diagnostics);
|
|
140
152
|
return await response.json();
|
|
141
153
|
} catch {
|
|
142
|
-
return
|
|
154
|
+
return calculateScoreLocally(diagnostics);
|
|
143
155
|
}
|
|
144
156
|
};
|
|
145
157
|
const fetchEstimatedScore = async (diagnostics) => {
|
|
@@ -178,13 +190,33 @@ const colorizeByScore = (text, score) => {
|
|
|
178
190
|
//#region src/plugin/constants.ts
|
|
179
191
|
const MOTION_LIBRARY_PACKAGES = new Set(["framer-motion", "motion"]);
|
|
180
192
|
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/utils/is-file.ts
|
|
195
|
+
const isFile = (filePath) => {
|
|
196
|
+
try {
|
|
197
|
+
return fs.statSync(filePath).isFile();
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
181
203
|
//#endregion
|
|
182
204
|
//#region src/utils/read-package-json.ts
|
|
183
|
-
const readPackageJson = (packageJsonPath) =>
|
|
205
|
+
const readPackageJson = (packageJsonPath) => {
|
|
206
|
+
try {
|
|
207
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error instanceof Error && "code" in error) {
|
|
210
|
+
const { code } = error;
|
|
211
|
+
if (code === "EISDIR" || code === "EACCES") return {};
|
|
212
|
+
}
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
184
216
|
|
|
185
217
|
//#endregion
|
|
186
218
|
//#region src/utils/check-reduced-motion.ts
|
|
187
|
-
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion";
|
|
219
|
+
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
|
|
188
220
|
const REDUCED_MOTION_FILE_GLOBS = "\"*.ts\" \"*.tsx\" \"*.js\" \"*.jsx\" \"*.css\" \"*.scss\"";
|
|
189
221
|
const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
190
222
|
filePath: "package.json",
|
|
@@ -200,7 +232,7 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
|
200
232
|
};
|
|
201
233
|
const checkReducedMotion = (rootDirectory) => {
|
|
202
234
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
203
|
-
if (!
|
|
235
|
+
if (!isFile(packageJsonPath)) return [];
|
|
204
236
|
let hasMotionLibrary = false;
|
|
205
237
|
try {
|
|
206
238
|
const packageJson = readPackageJson(packageJsonPath);
|
|
@@ -228,7 +260,7 @@ const checkReducedMotion = (rootDirectory) => {
|
|
|
228
260
|
//#region src/utils/match-glob-pattern.ts
|
|
229
261
|
const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
|
|
230
262
|
const compileGlobPattern = (pattern) => {
|
|
231
|
-
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
263
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\//, "");
|
|
232
264
|
let regexSource = "^";
|
|
233
265
|
let characterIndex = 0;
|
|
234
266
|
while (characterIndex < normalizedPattern.length) if (normalizedPattern[characterIndex] === "*" && normalizedPattern[characterIndex + 1] === "*") if (normalizedPattern[characterIndex + 2] === "/") {
|
|
@@ -252,17 +284,72 @@ const compileGlobPattern = (pattern) => {
|
|
|
252
284
|
return new RegExp(regexSource);
|
|
253
285
|
};
|
|
254
286
|
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/utils/is-ignored-file.ts
|
|
289
|
+
const toRelativePath = (filePath, rootDirectory) => {
|
|
290
|
+
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
291
|
+
const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
|
|
292
|
+
if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
|
|
293
|
+
return normalizedFilePath.replace(/^\.\//, "");
|
|
294
|
+
};
|
|
295
|
+
const compileIgnoredFilePatterns = (userConfig) => Array.isArray(userConfig?.ignore?.files) ? userConfig.ignore.files.map(compileGlobPattern) : [];
|
|
296
|
+
const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
|
|
297
|
+
if (patterns.length === 0) return false;
|
|
298
|
+
const relativePath = toRelativePath(filePath, rootDirectory);
|
|
299
|
+
return patterns.some((pattern) => pattern.test(relativePath));
|
|
300
|
+
};
|
|
301
|
+
|
|
255
302
|
//#endregion
|
|
256
303
|
//#region src/utils/filter-diagnostics.ts
|
|
257
|
-
const filterIgnoredDiagnostics = (diagnostics, config) => {
|
|
304
|
+
const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory) => {
|
|
258
305
|
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
|
|
259
|
-
const ignoredFilePatterns =
|
|
306
|
+
const ignoredFilePatterns = compileIgnoredFilePatterns(config);
|
|
260
307
|
if (ignoredRules.size === 0 && ignoredFilePatterns.length === 0) return diagnostics;
|
|
261
308
|
return diagnostics.filter((diagnostic) => {
|
|
262
309
|
const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
263
310
|
if (ignoredRules.has(ruleIdentifier)) return false;
|
|
264
|
-
|
|
265
|
-
|
|
311
|
+
if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
|
|
312
|
+
return true;
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
const DISABLE_NEXT_LINE_PATTERN = /\/\/\s*react-doctor-disable-next-line\b(?:\s+(.+))?/;
|
|
316
|
+
const DISABLE_LINE_PATTERN = /\/\/\s*react-doctor-disable-line\b(?:\s+(.+))?/;
|
|
317
|
+
const isRuleSuppressed = (commentRules, ruleId) => {
|
|
318
|
+
if (!commentRules?.trim()) return true;
|
|
319
|
+
return commentRules.split(/[,\s]+/).some((rule) => rule.trim() === ruleId);
|
|
320
|
+
};
|
|
321
|
+
const filterInlineSuppressions = (diagnostics, rootDirectory) => {
|
|
322
|
+
const fileLineCache = /* @__PURE__ */ new Map();
|
|
323
|
+
const getFileLines = (filePath) => {
|
|
324
|
+
const cached = fileLineCache.get(filePath);
|
|
325
|
+
if (cached !== void 0) return cached;
|
|
326
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
|
|
327
|
+
try {
|
|
328
|
+
const lines = fs.readFileSync(absolutePath, "utf-8").split("\n");
|
|
329
|
+
fileLineCache.set(filePath, lines);
|
|
330
|
+
return lines;
|
|
331
|
+
} catch {
|
|
332
|
+
fileLineCache.set(filePath, null);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
return diagnostics.filter((diagnostic) => {
|
|
337
|
+
if (diagnostic.line <= 0) return true;
|
|
338
|
+
const lines = getFileLines(diagnostic.filePath);
|
|
339
|
+
if (!lines) return true;
|
|
340
|
+
const ruleId = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
341
|
+
const currentLine = lines[diagnostic.line - 1];
|
|
342
|
+
if (currentLine) {
|
|
343
|
+
const lineMatch = currentLine.match(DISABLE_LINE_PATTERN);
|
|
344
|
+
if (lineMatch && isRuleSuppressed(lineMatch[1], ruleId)) return false;
|
|
345
|
+
}
|
|
346
|
+
if (diagnostic.line >= 2) {
|
|
347
|
+
const prevLine = lines[diagnostic.line - 2];
|
|
348
|
+
if (prevLine) {
|
|
349
|
+
const nextLineMatch = prevLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
350
|
+
if (nextLineMatch && isRuleSuppressed(nextLineMatch[1], ruleId)) return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
266
353
|
return true;
|
|
267
354
|
});
|
|
268
355
|
};
|
|
@@ -271,21 +358,21 @@ const filterIgnoredDiagnostics = (diagnostics, config) => {
|
|
|
271
358
|
//#region src/utils/combine-diagnostics.ts
|
|
272
359
|
const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
|
|
273
360
|
const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isDiffMode, userConfig) => {
|
|
274
|
-
const
|
|
361
|
+
const merged = [
|
|
275
362
|
...lintDiagnostics,
|
|
276
363
|
...deadCodeDiagnostics,
|
|
277
364
|
...isDiffMode ? [] : checkReducedMotion(directory)
|
|
278
365
|
];
|
|
279
|
-
return userConfig ? filterIgnoredDiagnostics(
|
|
366
|
+
return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(merged, userConfig, directory) : merged, directory);
|
|
280
367
|
};
|
|
281
368
|
|
|
282
369
|
//#endregion
|
|
283
370
|
//#region src/utils/find-monorepo-root.ts
|
|
284
371
|
const isMonorepoRoot = (directory) => {
|
|
285
|
-
if (
|
|
286
|
-
if (
|
|
372
|
+
if (isFile(path.join(directory, "pnpm-workspace.yaml"))) return true;
|
|
373
|
+
if (isFile(path.join(directory, "nx.json"))) return true;
|
|
287
374
|
const packageJsonPath = path.join(directory, "package.json");
|
|
288
|
-
if (!
|
|
375
|
+
if (!isFile(packageJsonPath)) return false;
|
|
289
376
|
const packageJson = readPackageJson(packageJsonPath);
|
|
290
377
|
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
291
378
|
};
|
|
@@ -298,6 +385,10 @@ const findMonorepoRoot = (startDirectory) => {
|
|
|
298
385
|
return null;
|
|
299
386
|
};
|
|
300
387
|
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/utils/is-plain-object.ts
|
|
390
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
391
|
+
|
|
301
392
|
//#endregion
|
|
302
393
|
//#region src/utils/discover-project.ts
|
|
303
394
|
const REACT_COMPILER_PACKAGES = new Set([
|
|
@@ -351,12 +442,6 @@ const FRAMEWORK_DISPLAY_NAMES = {
|
|
|
351
442
|
unknown: "React"
|
|
352
443
|
};
|
|
353
444
|
const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
|
|
354
|
-
const IGNORED_DIRECTORIES = new Set([
|
|
355
|
-
"node_modules",
|
|
356
|
-
"dist",
|
|
357
|
-
"build",
|
|
358
|
-
"coverage"
|
|
359
|
-
]);
|
|
360
445
|
const countSourceFilesViaFilesystem = (rootDirectory) => {
|
|
361
446
|
let count = 0;
|
|
362
447
|
const stack = [rootDirectory];
|
|
@@ -397,16 +482,129 @@ const detectFramework = (dependencies) => {
|
|
|
397
482
|
for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
|
|
398
483
|
return "unknown";
|
|
399
484
|
};
|
|
485
|
+
const isCatalogReference = (version) => version.startsWith("catalog:");
|
|
486
|
+
const extractCatalogName = (version) => {
|
|
487
|
+
if (!isCatalogReference(version)) return null;
|
|
488
|
+
const name = version.slice(8).trim();
|
|
489
|
+
return name.length > 0 ? name : null;
|
|
490
|
+
};
|
|
491
|
+
const resolveVersionFromCatalog = (catalog, packageName) => {
|
|
492
|
+
const version = catalog[packageName];
|
|
493
|
+
if (typeof version === "string" && !isCatalogReference(version)) return version;
|
|
494
|
+
return null;
|
|
495
|
+
};
|
|
496
|
+
const parsePnpmWorkspaceCatalogs = (rootDirectory) => {
|
|
497
|
+
const workspacePath = path.join(rootDirectory, "pnpm-workspace.yaml");
|
|
498
|
+
if (!isFile(workspacePath)) return {
|
|
499
|
+
defaultCatalog: {},
|
|
500
|
+
namedCatalogs: {}
|
|
501
|
+
};
|
|
502
|
+
const content = fs.readFileSync(workspacePath, "utf-8");
|
|
503
|
+
const defaultCatalog = {};
|
|
504
|
+
const namedCatalogs = {};
|
|
505
|
+
let currentSection = "none";
|
|
506
|
+
let currentCatalogName = "";
|
|
507
|
+
for (const line of content.split("\n")) {
|
|
508
|
+
const trimmed = line.trim();
|
|
509
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
510
|
+
const indentLevel = line.search(/\S/);
|
|
511
|
+
if (indentLevel === 0 && trimmed === "catalog:") {
|
|
512
|
+
currentSection = "catalog";
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (indentLevel === 0 && trimmed === "catalogs:") {
|
|
516
|
+
currentSection = "catalogs";
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (indentLevel === 0) {
|
|
520
|
+
currentSection = "none";
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (currentSection === "catalog" && indentLevel > 0) {
|
|
524
|
+
const colonIndex = trimmed.indexOf(":");
|
|
525
|
+
if (colonIndex > 0) {
|
|
526
|
+
const key = trimmed.slice(0, colonIndex).trim().replace(/["']/g, "");
|
|
527
|
+
const value = trimmed.slice(colonIndex + 1).trim().replace(/["']/g, "");
|
|
528
|
+
if (key && value) defaultCatalog[key] = value;
|
|
529
|
+
}
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (currentSection === "catalogs" && indentLevel > 0) {
|
|
533
|
+
if (trimmed.endsWith(":") && !trimmed.includes(" ")) {
|
|
534
|
+
currentCatalogName = trimmed.slice(0, -1).replace(/["']/g, "");
|
|
535
|
+
currentSection = "named-catalog";
|
|
536
|
+
namedCatalogs[currentCatalogName] = {};
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (currentSection === "named-catalog" && indentLevel > 0) {
|
|
541
|
+
if (indentLevel <= 2 && trimmed.endsWith(":") && !trimmed.includes(" ")) {
|
|
542
|
+
currentCatalogName = trimmed.slice(0, -1).replace(/["']/g, "");
|
|
543
|
+
namedCatalogs[currentCatalogName] = {};
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const colonIndex = trimmed.indexOf(":");
|
|
547
|
+
if (colonIndex > 0 && currentCatalogName) {
|
|
548
|
+
const key = trimmed.slice(0, colonIndex).trim().replace(/["']/g, "");
|
|
549
|
+
const value = trimmed.slice(colonIndex + 1).trim().replace(/["']/g, "");
|
|
550
|
+
if (key && value) namedCatalogs[currentCatalogName][key] = value;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
defaultCatalog,
|
|
556
|
+
namedCatalogs
|
|
557
|
+
};
|
|
558
|
+
};
|
|
559
|
+
const resolveCatalogVersionFromCollection = (catalogs, packageName, catalogReference) => {
|
|
560
|
+
if (catalogReference) {
|
|
561
|
+
const namedCatalog = catalogs.namedCatalogs[catalogReference];
|
|
562
|
+
if (namedCatalog?.[packageName]) return namedCatalog[packageName];
|
|
563
|
+
}
|
|
564
|
+
if (catalogs.defaultCatalog[packageName]) return catalogs.defaultCatalog[packageName];
|
|
565
|
+
for (const namedCatalog of Object.values(catalogs.namedCatalogs)) if (namedCatalog[packageName]) return namedCatalog[packageName];
|
|
566
|
+
return null;
|
|
567
|
+
};
|
|
568
|
+
const resolveCatalogVersion = (packageJson, packageName, rootDirectory) => {
|
|
569
|
+
const rawVersion = collectAllDependencies(packageJson)[packageName];
|
|
570
|
+
const catalogName = rawVersion ? extractCatalogName(rawVersion) : null;
|
|
571
|
+
const raw = packageJson;
|
|
572
|
+
if (isPlainObject(raw.catalog)) {
|
|
573
|
+
const version = resolveVersionFromCatalog(raw.catalog, packageName);
|
|
574
|
+
if (version) return version;
|
|
575
|
+
}
|
|
576
|
+
if (isPlainObject(raw.catalogs)) {
|
|
577
|
+
if (catalogName && isPlainObject(raw.catalogs[catalogName])) {
|
|
578
|
+
const version = resolveVersionFromCatalog(raw.catalogs[catalogName], packageName);
|
|
579
|
+
if (version) return version;
|
|
580
|
+
}
|
|
581
|
+
for (const catalogEntries of Object.values(raw.catalogs)) if (isPlainObject(catalogEntries)) {
|
|
582
|
+
const version = resolveVersionFromCatalog(catalogEntries, packageName);
|
|
583
|
+
if (version) return version;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const workspaces = packageJson.workspaces;
|
|
587
|
+
if (workspaces && !Array.isArray(workspaces) && isPlainObject(workspaces.catalog)) {
|
|
588
|
+
const version = resolveVersionFromCatalog(workspaces.catalog, packageName);
|
|
589
|
+
if (version) return version;
|
|
590
|
+
}
|
|
591
|
+
if (rootDirectory) {
|
|
592
|
+
const pnpmVersion = resolveCatalogVersionFromCollection(parsePnpmWorkspaceCatalogs(rootDirectory), packageName, catalogName);
|
|
593
|
+
if (pnpmVersion) return pnpmVersion;
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
};
|
|
400
597
|
const extractDependencyInfo = (packageJson) => {
|
|
401
598
|
const allDependencies = collectAllDependencies(packageJson);
|
|
599
|
+
const rawVersion = allDependencies.react ?? null;
|
|
402
600
|
return {
|
|
403
|
-
reactVersion:
|
|
601
|
+
reactVersion: rawVersion && !isCatalogReference(rawVersion) ? rawVersion : null,
|
|
404
602
|
framework: detectFramework(allDependencies)
|
|
405
603
|
};
|
|
406
604
|
};
|
|
407
605
|
const parsePnpmWorkspacePatterns = (rootDirectory) => {
|
|
408
606
|
const workspacePath = path.join(rootDirectory, "pnpm-workspace.yaml");
|
|
409
|
-
if (!
|
|
607
|
+
if (!isFile(workspacePath)) return [];
|
|
410
608
|
const content = fs.readFileSync(workspacePath, "utf-8");
|
|
411
609
|
const patterns = [];
|
|
412
610
|
let isInsidePackagesBlock = false;
|
|
@@ -432,14 +630,14 @@ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
|
|
|
432
630
|
const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
|
|
433
631
|
if (!cleanPattern.includes("*")) {
|
|
434
632
|
const directoryPath = path.join(rootDirectory, cleanPattern);
|
|
435
|
-
if (fs.existsSync(directoryPath) &&
|
|
633
|
+
if (fs.existsSync(directoryPath) && isFile(path.join(directoryPath, "package.json"))) return [directoryPath];
|
|
436
634
|
return [];
|
|
437
635
|
}
|
|
438
636
|
const wildcardIndex = cleanPattern.indexOf("*");
|
|
439
637
|
const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
|
|
440
638
|
const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
|
|
441
639
|
if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) return [];
|
|
442
|
-
return fs.readdirSync(baseDirectory).map((entry) => path.join(baseDirectory, entry, suffixAfterWildcard)).filter((entryPath) => fs.existsSync(entryPath) && fs.statSync(entryPath).isDirectory() &&
|
|
640
|
+
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")));
|
|
443
641
|
};
|
|
444
642
|
const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
445
643
|
const monorepoRoot = findMonorepoRoot(directory);
|
|
@@ -447,11 +645,17 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
447
645
|
reactVersion: null,
|
|
448
646
|
framework: "unknown"
|
|
449
647
|
};
|
|
450
|
-
const
|
|
648
|
+
const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
|
|
649
|
+
if (!isFile(monorepoPackageJsonPath)) return {
|
|
650
|
+
reactVersion: null,
|
|
651
|
+
framework: "unknown"
|
|
652
|
+
};
|
|
653
|
+
const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
|
|
451
654
|
const rootInfo = extractDependencyInfo(rootPackageJson);
|
|
655
|
+
const catalogVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot);
|
|
452
656
|
const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
|
|
453
657
|
return {
|
|
454
|
-
reactVersion: rootInfo.reactVersion ?? workspaceInfo.reactVersion,
|
|
658
|
+
reactVersion: rootInfo.reactVersion ?? catalogVersion ?? workspaceInfo.reactVersion,
|
|
455
659
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
456
660
|
};
|
|
457
661
|
};
|
|
@@ -485,7 +689,7 @@ const discoverReactSubprojects = (rootDirectory) => {
|
|
|
485
689
|
if (!fs.existsSync(rootDirectory) || !fs.statSync(rootDirectory).isDirectory()) return [];
|
|
486
690
|
const packages = [];
|
|
487
691
|
const rootPackageJsonPath = path.join(rootDirectory, "package.json");
|
|
488
|
-
if (
|
|
692
|
+
if (isFile(rootPackageJsonPath)) {
|
|
489
693
|
const rootPackageJson = readPackageJson(rootPackageJsonPath);
|
|
490
694
|
if (hasReactDependency(rootPackageJson)) {
|
|
491
695
|
const name = rootPackageJson.name ?? path.basename(rootDirectory);
|
|
@@ -500,7 +704,7 @@ const discoverReactSubprojects = (rootDirectory) => {
|
|
|
500
704
|
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
501
705
|
const subdirectory = path.join(rootDirectory, entry.name);
|
|
502
706
|
const packageJsonPath = path.join(subdirectory, "package.json");
|
|
503
|
-
if (!
|
|
707
|
+
if (!isFile(packageJsonPath)) continue;
|
|
504
708
|
const packageJson = readPackageJson(packageJsonPath);
|
|
505
709
|
if (!hasReactDependency(packageJson)) continue;
|
|
506
710
|
const name = packageJson.name ?? entry.name;
|
|
@@ -513,10 +717,18 @@ const discoverReactSubprojects = (rootDirectory) => {
|
|
|
513
717
|
};
|
|
514
718
|
const listWorkspacePackages = (rootDirectory) => {
|
|
515
719
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
516
|
-
if (!
|
|
517
|
-
const
|
|
720
|
+
if (!isFile(packageJsonPath)) return [];
|
|
721
|
+
const packageJson = readPackageJson(packageJsonPath);
|
|
722
|
+
const patterns = getWorkspacePatterns(rootDirectory, packageJson);
|
|
518
723
|
if (patterns.length === 0) return [];
|
|
519
724
|
const packages = [];
|
|
725
|
+
if (hasReactDependency(packageJson)) {
|
|
726
|
+
const rootName = packageJson.name ?? path.basename(rootDirectory);
|
|
727
|
+
packages.push({
|
|
728
|
+
name: rootName,
|
|
729
|
+
directory: rootDirectory
|
|
730
|
+
});
|
|
731
|
+
}
|
|
520
732
|
for (const pattern of patterns) {
|
|
521
733
|
const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
|
|
522
734
|
for (const workspaceDirectory of directories) {
|
|
@@ -536,7 +748,7 @@ const hasCompilerPackage = (packageJson) => {
|
|
|
536
748
|
return Object.keys(allDependencies).some((packageName) => REACT_COMPILER_PACKAGES.has(packageName));
|
|
537
749
|
};
|
|
538
750
|
const fileContainsPattern = (filePath, pattern) => {
|
|
539
|
-
if (!
|
|
751
|
+
if (!isFile(filePath)) return false;
|
|
540
752
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
541
753
|
return pattern.test(content);
|
|
542
754
|
};
|
|
@@ -550,7 +762,7 @@ const detectReactCompiler = (directory, packageJson) => {
|
|
|
550
762
|
let ancestorDirectory = path.dirname(directory);
|
|
551
763
|
while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
|
|
552
764
|
const ancestorPackagePath = path.join(ancestorDirectory, "package.json");
|
|
553
|
-
if (
|
|
765
|
+
if (isFile(ancestorPackagePath)) {
|
|
554
766
|
if (hasCompilerPackage(readPackageJson(ancestorPackagePath))) return true;
|
|
555
767
|
}
|
|
556
768
|
ancestorDirectory = path.dirname(ancestorDirectory);
|
|
@@ -559,9 +771,17 @@ const detectReactCompiler = (directory, packageJson) => {
|
|
|
559
771
|
};
|
|
560
772
|
const discoverProject = (directory) => {
|
|
561
773
|
const packageJsonPath = path.join(directory, "package.json");
|
|
562
|
-
if (!
|
|
774
|
+
if (!isFile(packageJsonPath)) throw new Error(`No package.json found in ${directory}`);
|
|
563
775
|
const packageJson = readPackageJson(packageJsonPath);
|
|
564
776
|
let { reactVersion, framework } = extractDependencyInfo(packageJson);
|
|
777
|
+
if (!reactVersion) reactVersion = resolveCatalogVersion(packageJson, "react", directory);
|
|
778
|
+
if (!reactVersion) {
|
|
779
|
+
const monorepoRoot = findMonorepoRoot(directory);
|
|
780
|
+
if (monorepoRoot) {
|
|
781
|
+
const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
|
|
782
|
+
if (isFile(monorepoPackageJsonPath)) reactVersion = resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), "react", monorepoRoot);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
565
785
|
if (!reactVersion || framework === "unknown") {
|
|
566
786
|
const workspaceInfo = findReactInWorkspaces(directory, packageJson);
|
|
567
787
|
if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
|
|
@@ -661,10 +881,9 @@ const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText
|
|
|
661
881
|
//#region src/utils/load-config.ts
|
|
662
882
|
const CONFIG_FILENAME = "react-doctor.config.json";
|
|
663
883
|
const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
|
|
664
|
-
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
665
884
|
const loadConfig = (rootDirectory) => {
|
|
666
885
|
const configFilePath = path.join(rootDirectory, CONFIG_FILENAME);
|
|
667
|
-
if (
|
|
886
|
+
if (isFile(configFilePath)) try {
|
|
668
887
|
const fileContent = fs.readFileSync(configFilePath, "utf-8");
|
|
669
888
|
const parsed = JSON.parse(fileContent);
|
|
670
889
|
if (isPlainObject(parsed)) return parsed;
|
|
@@ -673,7 +892,7 @@ const loadConfig = (rootDirectory) => {
|
|
|
673
892
|
console.warn(`Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
|
|
674
893
|
}
|
|
675
894
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
676
|
-
if (
|
|
895
|
+
if (isFile(packageJsonPath)) try {
|
|
677
896
|
const fileContent = fs.readFileSync(packageJsonPath, "utf-8");
|
|
678
897
|
const embeddedConfig = JSON.parse(fileContent)[PACKAGE_JSON_CONFIG_KEY];
|
|
679
898
|
if (isPlainObject(embeddedConfig)) return embeddedConfig;
|
|
@@ -844,6 +1063,49 @@ const resolveNodeForOxlint = () => {
|
|
|
844
1063
|
};
|
|
845
1064
|
};
|
|
846
1065
|
|
|
1066
|
+
//#endregion
|
|
1067
|
+
//#region src/utils/resolve-lint-include-paths.ts
|
|
1068
|
+
const listSourceFilesViaGit = (rootDirectory) => {
|
|
1069
|
+
const result = spawnSync("git", [
|
|
1070
|
+
"ls-files",
|
|
1071
|
+
"--cached",
|
|
1072
|
+
"--others",
|
|
1073
|
+
"--exclude-standard"
|
|
1074
|
+
], {
|
|
1075
|
+
cwd: rootDirectory,
|
|
1076
|
+
encoding: "utf-8",
|
|
1077
|
+
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
1078
|
+
});
|
|
1079
|
+
if (result.error || result.status !== 0) return null;
|
|
1080
|
+
return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
|
|
1081
|
+
};
|
|
1082
|
+
const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
1083
|
+
const filePaths = [];
|
|
1084
|
+
const stack = [rootDirectory];
|
|
1085
|
+
while (stack.length > 0) {
|
|
1086
|
+
const currentDirectory = stack.pop();
|
|
1087
|
+
const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
|
|
1088
|
+
for (const entry of entries) {
|
|
1089
|
+
const absolutePath = path.join(currentDirectory, entry.name);
|
|
1090
|
+
if (entry.isDirectory()) {
|
|
1091
|
+
if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) filePaths.push(path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return filePaths;
|
|
1098
|
+
};
|
|
1099
|
+
const listSourceFiles = (rootDirectory) => listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory);
|
|
1100
|
+
const resolveLintIncludePaths = (rootDirectory, userConfig) => {
|
|
1101
|
+
if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
|
|
1102
|
+
const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
|
|
1103
|
+
return listSourceFiles(rootDirectory).filter((filePath) => {
|
|
1104
|
+
if (!JSX_FILE_PATTERN.test(filePath)) return false;
|
|
1105
|
+
return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
|
|
1106
|
+
});
|
|
1107
|
+
};
|
|
1108
|
+
|
|
847
1109
|
//#endregion
|
|
848
1110
|
//#region src/utils/run-knip.ts
|
|
849
1111
|
const KNIP_CATEGORY_MAP = {
|
|
@@ -932,7 +1194,7 @@ const runKnip = async (rootDirectory) => {
|
|
|
932
1194
|
let knipResult;
|
|
933
1195
|
if (monorepoRoot) {
|
|
934
1196
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
935
|
-
const workspaceName = (
|
|
1197
|
+
const workspaceName = (isFile(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) : {}).name ?? path.basename(rootDirectory);
|
|
936
1198
|
try {
|
|
937
1199
|
knipResult = await runKnipWithOptions(monorepoRoot, workspaceName);
|
|
938
1200
|
} catch {
|
|
@@ -1102,24 +1364,27 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
|
|
|
1102
1364
|
|
|
1103
1365
|
//#endregion
|
|
1104
1366
|
//#region src/utils/neutralize-disable-directives.ts
|
|
1105
|
-
const findFilesWithDisableDirectives = (rootDirectory) => {
|
|
1106
|
-
const
|
|
1367
|
+
const findFilesWithDisableDirectives = (rootDirectory, includePaths) => {
|
|
1368
|
+
const grepArgs = [
|
|
1107
1369
|
"grep",
|
|
1108
1370
|
"-l",
|
|
1109
1371
|
"--untracked",
|
|
1110
1372
|
"-E",
|
|
1111
1373
|
"(eslint|oxlint)-disable"
|
|
1112
|
-
]
|
|
1374
|
+
];
|
|
1375
|
+
if (includePaths && includePaths.length > 0) grepArgs.push("--", ...includePaths);
|
|
1376
|
+
const result = spawnSync("git", grepArgs, {
|
|
1113
1377
|
cwd: rootDirectory,
|
|
1114
1378
|
encoding: "utf-8",
|
|
1115
1379
|
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
1116
1380
|
});
|
|
1117
1381
|
if (result.error || result.status === null) return [];
|
|
1382
|
+
if (result.status !== 0 && result.stdout.trim().length === 0) return [];
|
|
1118
1383
|
return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
|
|
1119
1384
|
};
|
|
1120
1385
|
const neutralizeContent = (content) => content.replaceAll("eslint-disable", "eslint_disable").replaceAll("oxlint-disable", "oxlint_disable");
|
|
1121
|
-
const neutralizeDisableDirectives = (rootDirectory) => {
|
|
1122
|
-
const filePaths = findFilesWithDisableDirectives(rootDirectory);
|
|
1386
|
+
const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
|
|
1387
|
+
const filePaths = findFilesWithDisableDirectives(rootDirectory, includePaths);
|
|
1123
1388
|
const originalContents = /* @__PURE__ */ new Map();
|
|
1124
1389
|
for (const relativePath of filePaths) {
|
|
1125
1390
|
const absolutePath = path.join(rootDirectory, relativePath);
|
|
@@ -1215,7 +1480,7 @@ const RULE_CATEGORY_MAP = {
|
|
|
1215
1480
|
"react-doctor/rn-no-single-element-style-array": "React Native"
|
|
1216
1481
|
};
|
|
1217
1482
|
const RULE_HELP_MAP = {
|
|
1218
|
-
"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}
|
|
1483
|
+
"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",
|
|
1219
1484
|
"no-fetch-in-effect": "Use `useQuery()` from @tanstack/react-query, `useSWR()`, or fetch in a Server Component instead",
|
|
1220
1485
|
"no-cascading-set-state": "Combine into useReducer: `const [state, dispatch] = useReducer(reducer, initialState)`",
|
|
1221
1486
|
"no-effect-event-handler": "Move the conditional logic into onClick, onChange, or onSubmit handlers directly",
|
|
@@ -1255,7 +1520,7 @@ const RULE_HELP_MAP = {
|
|
|
1255
1520
|
"nextjs-no-use-search-params-without-suspense": "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
|
|
1256
1521
|
"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",
|
|
1257
1522
|
"nextjs-missing-metadata": "Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`",
|
|
1258
|
-
"nextjs-no-client-side-redirect": "Use `redirect('/path')` from 'next/navigation' in
|
|
1523
|
+
"nextjs-no-client-side-redirect": "Use `redirect('/path')` from 'next/navigation' directly (works in both server and client components), or handle in middleware",
|
|
1259
1524
|
"nextjs-no-redirect-in-try-catch": "Move the redirect/notFound call outside the try block, or add `unstable_rethrow(error)` in the catch",
|
|
1260
1525
|
"nextjs-image-missing-sizes": "Add sizes for responsive behavior: `sizes=\"(max-width: 768px) 100vw, 50vw\"` matching your layout breakpoints",
|
|
1261
1526
|
"nextjs-no-native-script": "`import Script from \"next/script\"` — use `strategy=\"afterInteractive\"` for analytics or `\"lazyOnload\"` for widgets",
|
|
@@ -1343,7 +1608,14 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
1343
1608
|
child.stdout.on("data", (buffer) => stdoutBuffers.push(buffer));
|
|
1344
1609
|
child.stderr.on("data", (buffer) => stderrBuffers.push(buffer));
|
|
1345
1610
|
child.on("error", (error) => reject(/* @__PURE__ */ new Error(`Failed to run oxlint: ${error.message}`)));
|
|
1346
|
-
child.on("close", () => {
|
|
1611
|
+
child.on("close", (code, signal) => {
|
|
1612
|
+
if (signal) {
|
|
1613
|
+
const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
|
|
1614
|
+
const hint = signal === "SIGABRT" ? " (out of memory — try scanning fewer files with --diff)" : "";
|
|
1615
|
+
const detail = stderrOutput ? `: ${stderrOutput}` : "";
|
|
1616
|
+
reject(/* @__PURE__ */ new Error(`oxlint was killed by ${signal}${hint}${detail}`));
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1347
1619
|
const output = Buffer.concat(stdoutBuffers).toString("utf-8").trim();
|
|
1348
1620
|
if (!output) {
|
|
1349
1621
|
const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
|
|
@@ -1388,7 +1660,7 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
|
|
|
1388
1660
|
framework,
|
|
1389
1661
|
hasReactCompiler
|
|
1390
1662
|
});
|
|
1391
|
-
const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory);
|
|
1663
|
+
const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
1392
1664
|
try {
|
|
1393
1665
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1394
1666
|
const baseArgs = [
|
|
@@ -1614,7 +1886,7 @@ const buildCountsSummaryLine = (diagnostics, totalSourceFileCount, elapsedMillis
|
|
|
1614
1886
|
renderedParts.push(highlighter.dim(fileCountText), highlighter.dim(elapsedTimeText));
|
|
1615
1887
|
return createFramedLine(plainParts.join(" "), renderedParts.join(" "));
|
|
1616
1888
|
};
|
|
1617
|
-
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName, totalSourceFileCount, noScoreMessage) => {
|
|
1889
|
+
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName, totalSourceFileCount, noScoreMessage, isOffline) => {
|
|
1618
1890
|
printFramedBox([...buildBrandingLines(scoreResult, noScoreMessage), buildCountsSummaryLine(diagnostics, totalSourceFileCount, elapsedMilliseconds)]);
|
|
1619
1891
|
try {
|
|
1620
1892
|
const diagnosticsDirectory = writeDiagnosticsDirectory(diagnostics);
|
|
@@ -1623,9 +1895,11 @@ const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName
|
|
|
1623
1895
|
} catch {
|
|
1624
1896
|
logger.break();
|
|
1625
1897
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1898
|
+
if (!isOffline) {
|
|
1899
|
+
const shareUrl = buildShareUrl(diagnostics, scoreResult, projectName);
|
|
1900
|
+
logger.break();
|
|
1901
|
+
logger.dim(` Share your results: ${highlighter.info(shareUrl)}`);
|
|
1902
|
+
}
|
|
1629
1903
|
};
|
|
1630
1904
|
const resolveOxlintNode = async (isLintEnabled, isScoreOnly) => {
|
|
1631
1905
|
if (!isLintEnabled) return null;
|
|
@@ -1673,7 +1947,7 @@ const mergeScanOptions = (inputOptions, userConfig) => ({
|
|
|
1673
1947
|
offline: inputOptions.offline ?? false,
|
|
1674
1948
|
includePaths: inputOptions.includePaths ?? []
|
|
1675
1949
|
});
|
|
1676
|
-
const printProjectDetection = (projectInfo, userConfig, isDiffMode, includePaths) => {
|
|
1950
|
+
const printProjectDetection = (projectInfo, userConfig, isDiffMode, includePaths, lintSourceFileCount) => {
|
|
1677
1951
|
const frameworkLabel = formatFrameworkName(projectInfo.framework);
|
|
1678
1952
|
const languageLabel = projectInfo.hasTypeScript ? "TypeScript" : "JavaScript";
|
|
1679
1953
|
const completeStep = (message) => {
|
|
@@ -1684,7 +1958,7 @@ const printProjectDetection = (projectInfo, userConfig, isDiffMode, includePaths
|
|
|
1684
1958
|
completeStep(`Detecting language. Found ${highlighter.info(languageLabel)}.`);
|
|
1685
1959
|
completeStep(`Detecting React Compiler. ${projectInfo.hasReactCompiler ? highlighter.info("Found React Compiler.") : "Not found."}`);
|
|
1686
1960
|
if (isDiffMode) completeStep(`Scanning ${highlighter.info(`${includePaths.length}`)} changed source files.`);
|
|
1687
|
-
else completeStep(`Found ${highlighter.info(`${projectInfo.sourceFileCount}`)} source files.`);
|
|
1961
|
+
else completeStep(`Found ${highlighter.info(`${lintSourceFileCount ?? projectInfo.sourceFileCount}`)} source files.`);
|
|
1688
1962
|
if (userConfig) completeStep(`Loaded ${highlighter.info("react-doctor config")}.`);
|
|
1689
1963
|
logger.break();
|
|
1690
1964
|
};
|
|
@@ -1696,8 +1970,9 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1696
1970
|
const { includePaths } = options;
|
|
1697
1971
|
const isDiffMode = includePaths.length > 0;
|
|
1698
1972
|
if (!projectInfo.reactVersion) throw new Error("No React dependency found in package.json");
|
|
1699
|
-
|
|
1700
|
-
const
|
|
1973
|
+
const lintIncludePaths = computeJsxIncludePaths(includePaths) ?? resolveLintIncludePaths(directory, userConfig);
|
|
1974
|
+
const lintSourceFileCount = lintIncludePaths?.length ?? projectInfo.sourceFileCount;
|
|
1975
|
+
if (!options.scoreOnly) printProjectDetection(projectInfo, userConfig, isDiffMode, includePaths, lintSourceFileCount);
|
|
1701
1976
|
let didLintFail = false;
|
|
1702
1977
|
let didDeadCodeFail = false;
|
|
1703
1978
|
const resolvedNodeBinaryPath = await resolveOxlintNode(options.lint, options.scoreOnly);
|
|
@@ -1705,7 +1980,7 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1705
1980
|
const lintPromise = resolvedNodeBinaryPath ? (async () => {
|
|
1706
1981
|
const lintSpinner = options.scoreOnly ? null : spinner("Running lint checks...").start();
|
|
1707
1982
|
try {
|
|
1708
|
-
const lintDiagnostics = await runOxlint(directory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler,
|
|
1983
|
+
const lintDiagnostics = await runOxlint(directory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler, lintIncludePaths, resolvedNodeBinaryPath);
|
|
1709
1984
|
lintSpinner?.succeed("Running lint checks.");
|
|
1710
1985
|
return lintDiagnostics;
|
|
1711
1986
|
} catch (error) {
|
|
@@ -1745,8 +2020,8 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1745
2020
|
if (didLintFail) skippedChecks.push("lint");
|
|
1746
2021
|
if (didDeadCodeFail) skippedChecks.push("dead code");
|
|
1747
2022
|
const hasSkippedChecks = skippedChecks.length > 0;
|
|
1748
|
-
const scoreResult = options.offline ?
|
|
1749
|
-
const noScoreMessage =
|
|
2023
|
+
const scoreResult = options.offline ? calculateScoreLocally(diagnostics) : await calculateScore(diagnostics);
|
|
2024
|
+
const noScoreMessage = OFFLINE_MESSAGE;
|
|
1750
2025
|
if (options.scoreOnly) {
|
|
1751
2026
|
if (scoreResult) logger.log(`${scoreResult.score}`);
|
|
1752
2027
|
else logger.dim(noScoreMessage);
|
|
@@ -1776,8 +2051,8 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1776
2051
|
};
|
|
1777
2052
|
}
|
|
1778
2053
|
printDiagnostics(diagnostics, options.verbose);
|
|
1779
|
-
const displayedSourceFileCount = isDiffMode ? includePaths.length :
|
|
1780
|
-
printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName, displayedSourceFileCount, noScoreMessage);
|
|
2054
|
+
const displayedSourceFileCount = isDiffMode ? includePaths.length : lintSourceFileCount;
|
|
2055
|
+
printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName, displayedSourceFileCount, noScoreMessage, options.offline);
|
|
1781
2056
|
if (hasSkippedChecks) {
|
|
1782
2057
|
const skippedLabel = skippedChecks.join(" and ");
|
|
1783
2058
|
logger.break();
|
|
@@ -2104,7 +2379,7 @@ const maybePromptSkillInstall = async (shouldSkipPrompts) => {
|
|
|
2104
2379
|
|
|
2105
2380
|
//#endregion
|
|
2106
2381
|
//#region src/cli.ts
|
|
2107
|
-
const VERSION = "0.0.
|
|
2382
|
+
const VERSION = "0.0.31";
|
|
2108
2383
|
const VALID_FAIL_ON_LEVELS = new Set([
|
|
2109
2384
|
"error",
|
|
2110
2385
|
"warning",
|
|
@@ -2157,7 +2432,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isSco
|
|
|
2157
2432
|
if (effectiveDiff === false || !diffInfo) return false;
|
|
2158
2433
|
const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);
|
|
2159
2434
|
if (changedSourceFiles.length === 0) return false;
|
|
2160
|
-
if (shouldSkipPrompts) return
|
|
2435
|
+
if (shouldSkipPrompts) return false;
|
|
2161
2436
|
if (isScoreOnly) return false;
|
|
2162
2437
|
const { shouldScanChangedOnly } = await prompts({
|
|
2163
2438
|
type: "confirm",
|
|
@@ -2167,7 +2442,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isSco
|
|
|
2167
2442
|
});
|
|
2168
2443
|
return Boolean(shouldScanChangedOnly);
|
|
2169
2444
|
};
|
|
2170
|
-
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead code detection").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch").option("--offline", "skip telemetry (anonymous, not stored, only used to calculate score)").option("--
|
|
2445
|
+
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead code detection").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch").option("--offline", "skip telemetry (anonymous, not stored, only used to calculate score)").option("--ami", "enable Ami-related prompts").option("--fail-on <level>", "exit with error code on diagnostics: error, warning, none", "none").option("--fix", "open Ami to auto-fix all issues").action(async (directory, flags) => {
|
|
2171
2446
|
const isScoreOnly = flags.score;
|
|
2172
2447
|
try {
|
|
2173
2448
|
const resolvedDirectory = path.resolve(directory);
|