react-doctor 0.0.29 → 0.0.30
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 +147 -40
- package/dist/cli.js.map +1 -1
- package/dist/index.js +173 -27
- package/dist/index.js.map +1 -1
- package/dist/react-doctor-plugin.js +3 -3
- package/dist/react-doctor-plugin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Options:
|
|
|
85
85
|
-y, --yes skip prompts, scan all workspace projects
|
|
86
86
|
--project <name> select workspace project (comma-separated for multiple)
|
|
87
87
|
--diff [base] scan only files changed vs base branch
|
|
88
|
-
--
|
|
88
|
+
--ami enable Ami-related prompts
|
|
89
89
|
--fix open Ami to auto-fix all issues
|
|
90
90
|
-h, --help display help for command
|
|
91
91
|
```
|
package/dist/cli.js
CHANGED
|
@@ -33,7 +33,6 @@ 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
35
|
const OFFLINE_MESSAGE = "You are offline, could not calculate score. Reconnect to calculate.";
|
|
36
|
-
const OFFLINE_FLAG_MESSAGE = "Score not calculated. Remove --offline to calculate score.";
|
|
37
36
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
38
37
|
const ERROR_RULE_PENALTY = 1.5;
|
|
39
38
|
const WARNING_RULE_PENALTY = .75;
|
|
@@ -129,6 +128,13 @@ const estimateScoreLocally = (diagnostics) => {
|
|
|
129
128
|
estimatedLabel: getScoreLabel(estimatedScore)
|
|
130
129
|
};
|
|
131
130
|
};
|
|
131
|
+
const calculateScoreLocally = (diagnostics) => {
|
|
132
|
+
const { currentScore, currentLabel } = estimateScoreLocally(diagnostics);
|
|
133
|
+
return {
|
|
134
|
+
score: currentScore,
|
|
135
|
+
label: currentLabel
|
|
136
|
+
};
|
|
137
|
+
};
|
|
132
138
|
const calculateScore = async (diagnostics) => {
|
|
133
139
|
try {
|
|
134
140
|
const response = await proxyFetch(SCORE_API_URL, {
|
|
@@ -136,10 +142,10 @@ const calculateScore = async (diagnostics) => {
|
|
|
136
142
|
headers: { "Content-Type": "application/json" },
|
|
137
143
|
body: JSON.stringify({ diagnostics })
|
|
138
144
|
});
|
|
139
|
-
if (!response.ok) return
|
|
145
|
+
if (!response.ok) return calculateScoreLocally(diagnostics);
|
|
140
146
|
return await response.json();
|
|
141
147
|
} catch {
|
|
142
|
-
return
|
|
148
|
+
return calculateScoreLocally(diagnostics);
|
|
143
149
|
}
|
|
144
150
|
};
|
|
145
151
|
const fetchEstimatedScore = async (diagnostics) => {
|
|
@@ -178,13 +184,33 @@ const colorizeByScore = (text, score) => {
|
|
|
178
184
|
//#region src/plugin/constants.ts
|
|
179
185
|
const MOTION_LIBRARY_PACKAGES = new Set(["framer-motion", "motion"]);
|
|
180
186
|
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/utils/is-file.ts
|
|
189
|
+
const isFile = (filePath) => {
|
|
190
|
+
try {
|
|
191
|
+
return fs.statSync(filePath).isFile();
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
181
197
|
//#endregion
|
|
182
198
|
//#region src/utils/read-package-json.ts
|
|
183
|
-
const readPackageJson = (packageJsonPath) =>
|
|
199
|
+
const readPackageJson = (packageJsonPath) => {
|
|
200
|
+
try {
|
|
201
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error instanceof Error && "code" in error) {
|
|
204
|
+
const { code } = error;
|
|
205
|
+
if (code === "EISDIR" || code === "EACCES") return {};
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
184
210
|
|
|
185
211
|
//#endregion
|
|
186
212
|
//#region src/utils/check-reduced-motion.ts
|
|
187
|
-
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion";
|
|
213
|
+
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
|
|
188
214
|
const REDUCED_MOTION_FILE_GLOBS = "\"*.ts\" \"*.tsx\" \"*.js\" \"*.jsx\" \"*.css\" \"*.scss\"";
|
|
189
215
|
const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
190
216
|
filePath: "package.json",
|
|
@@ -200,7 +226,7 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
|
200
226
|
};
|
|
201
227
|
const checkReducedMotion = (rootDirectory) => {
|
|
202
228
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
203
|
-
if (!
|
|
229
|
+
if (!isFile(packageJsonPath)) return [];
|
|
204
230
|
let hasMotionLibrary = false;
|
|
205
231
|
try {
|
|
206
232
|
const packageJson = readPackageJson(packageJsonPath);
|
|
@@ -228,7 +254,7 @@ const checkReducedMotion = (rootDirectory) => {
|
|
|
228
254
|
//#region src/utils/match-glob-pattern.ts
|
|
229
255
|
const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
|
|
230
256
|
const compileGlobPattern = (pattern) => {
|
|
231
|
-
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
257
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\//, "");
|
|
232
258
|
let regexSource = "^";
|
|
233
259
|
let characterIndex = 0;
|
|
234
260
|
while (characterIndex < normalizedPattern.length) if (normalizedPattern[characterIndex] === "*" && normalizedPattern[characterIndex + 1] === "*") if (normalizedPattern[characterIndex + 2] === "/") {
|
|
@@ -266,26 +292,67 @@ const filterIgnoredDiagnostics = (diagnostics, config) => {
|
|
|
266
292
|
return true;
|
|
267
293
|
});
|
|
268
294
|
};
|
|
295
|
+
const DISABLE_NEXT_LINE_PATTERN = /\/\/\s*react-doctor-disable-next-line\b(?:\s+(.+))?/;
|
|
296
|
+
const DISABLE_LINE_PATTERN = /\/\/\s*react-doctor-disable-line\b(?:\s+(.+))?/;
|
|
297
|
+
const isRuleSuppressed = (commentRules, ruleId) => {
|
|
298
|
+
if (!commentRules?.trim()) return true;
|
|
299
|
+
return commentRules.split(/[,\s]+/).some((rule) => rule.trim() === ruleId);
|
|
300
|
+
};
|
|
301
|
+
const filterInlineSuppressions = (diagnostics, rootDirectory) => {
|
|
302
|
+
const fileLineCache = /* @__PURE__ */ new Map();
|
|
303
|
+
const getFileLines = (filePath) => {
|
|
304
|
+
const cached = fileLineCache.get(filePath);
|
|
305
|
+
if (cached !== void 0) return cached;
|
|
306
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
|
|
307
|
+
try {
|
|
308
|
+
const lines = fs.readFileSync(absolutePath, "utf-8").split("\n");
|
|
309
|
+
fileLineCache.set(filePath, lines);
|
|
310
|
+
return lines;
|
|
311
|
+
} catch {
|
|
312
|
+
fileLineCache.set(filePath, null);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
return diagnostics.filter((diagnostic) => {
|
|
317
|
+
if (diagnostic.line <= 0) return true;
|
|
318
|
+
const lines = getFileLines(diagnostic.filePath);
|
|
319
|
+
if (!lines) return true;
|
|
320
|
+
const ruleId = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
321
|
+
const currentLine = lines[diagnostic.line - 1];
|
|
322
|
+
if (currentLine) {
|
|
323
|
+
const lineMatch = currentLine.match(DISABLE_LINE_PATTERN);
|
|
324
|
+
if (lineMatch && isRuleSuppressed(lineMatch[1], ruleId)) return false;
|
|
325
|
+
}
|
|
326
|
+
if (diagnostic.line >= 2) {
|
|
327
|
+
const prevLine = lines[diagnostic.line - 2];
|
|
328
|
+
if (prevLine) {
|
|
329
|
+
const nextLineMatch = prevLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
330
|
+
if (nextLineMatch && isRuleSuppressed(nextLineMatch[1], ruleId)) return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
});
|
|
335
|
+
};
|
|
269
336
|
|
|
270
337
|
//#endregion
|
|
271
338
|
//#region src/utils/combine-diagnostics.ts
|
|
272
339
|
const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
|
|
273
340
|
const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isDiffMode, userConfig) => {
|
|
274
|
-
const
|
|
341
|
+
const merged = [
|
|
275
342
|
...lintDiagnostics,
|
|
276
343
|
...deadCodeDiagnostics,
|
|
277
344
|
...isDiffMode ? [] : checkReducedMotion(directory)
|
|
278
345
|
];
|
|
279
|
-
return userConfig ? filterIgnoredDiagnostics(
|
|
346
|
+
return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(merged, userConfig) : merged, directory);
|
|
280
347
|
};
|
|
281
348
|
|
|
282
349
|
//#endregion
|
|
283
350
|
//#region src/utils/find-monorepo-root.ts
|
|
284
351
|
const isMonorepoRoot = (directory) => {
|
|
285
|
-
if (
|
|
286
|
-
if (
|
|
352
|
+
if (isFile(path.join(directory, "pnpm-workspace.yaml"))) return true;
|
|
353
|
+
if (isFile(path.join(directory, "nx.json"))) return true;
|
|
287
354
|
const packageJsonPath = path.join(directory, "package.json");
|
|
288
|
-
if (!
|
|
355
|
+
if (!isFile(packageJsonPath)) return false;
|
|
289
356
|
const packageJson = readPackageJson(packageJsonPath);
|
|
290
357
|
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
291
358
|
};
|
|
@@ -298,6 +365,10 @@ const findMonorepoRoot = (startDirectory) => {
|
|
|
298
365
|
return null;
|
|
299
366
|
};
|
|
300
367
|
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/utils/is-plain-object.ts
|
|
370
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
371
|
+
|
|
301
372
|
//#endregion
|
|
302
373
|
//#region src/utils/discover-project.ts
|
|
303
374
|
const REACT_COMPILER_PACKAGES = new Set([
|
|
@@ -397,16 +468,37 @@ const detectFramework = (dependencies) => {
|
|
|
397
468
|
for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
|
|
398
469
|
return "unknown";
|
|
399
470
|
};
|
|
471
|
+
const isCatalogReference = (version) => version.startsWith("catalog:");
|
|
472
|
+
const resolveVersionFromCatalog = (catalog, packageName) => {
|
|
473
|
+
const version = catalog[packageName];
|
|
474
|
+
if (typeof version === "string" && !isCatalogReference(version)) return version;
|
|
475
|
+
return null;
|
|
476
|
+
};
|
|
477
|
+
const resolveCatalogVersion = (packageJson, packageName) => {
|
|
478
|
+
const raw = packageJson;
|
|
479
|
+
if (isPlainObject(raw.catalog)) {
|
|
480
|
+
const version = resolveVersionFromCatalog(raw.catalog, packageName);
|
|
481
|
+
if (version) return version;
|
|
482
|
+
}
|
|
483
|
+
if (isPlainObject(raw.catalogs)) {
|
|
484
|
+
for (const catalogEntries of Object.values(raw.catalogs)) if (isPlainObject(catalogEntries)) {
|
|
485
|
+
const version = resolveVersionFromCatalog(catalogEntries, packageName);
|
|
486
|
+
if (version) return version;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
};
|
|
400
491
|
const extractDependencyInfo = (packageJson) => {
|
|
401
492
|
const allDependencies = collectAllDependencies(packageJson);
|
|
493
|
+
const rawVersion = allDependencies.react ?? null;
|
|
402
494
|
return {
|
|
403
|
-
reactVersion:
|
|
495
|
+
reactVersion: rawVersion && !isCatalogReference(rawVersion) ? rawVersion : null,
|
|
404
496
|
framework: detectFramework(allDependencies)
|
|
405
497
|
};
|
|
406
498
|
};
|
|
407
499
|
const parsePnpmWorkspacePatterns = (rootDirectory) => {
|
|
408
500
|
const workspacePath = path.join(rootDirectory, "pnpm-workspace.yaml");
|
|
409
|
-
if (!
|
|
501
|
+
if (!isFile(workspacePath)) return [];
|
|
410
502
|
const content = fs.readFileSync(workspacePath, "utf-8");
|
|
411
503
|
const patterns = [];
|
|
412
504
|
let isInsidePackagesBlock = false;
|
|
@@ -432,14 +524,14 @@ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
|
|
|
432
524
|
const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
|
|
433
525
|
if (!cleanPattern.includes("*")) {
|
|
434
526
|
const directoryPath = path.join(rootDirectory, cleanPattern);
|
|
435
|
-
if (fs.existsSync(directoryPath) &&
|
|
527
|
+
if (fs.existsSync(directoryPath) && isFile(path.join(directoryPath, "package.json"))) return [directoryPath];
|
|
436
528
|
return [];
|
|
437
529
|
}
|
|
438
530
|
const wildcardIndex = cleanPattern.indexOf("*");
|
|
439
531
|
const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
|
|
440
532
|
const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
|
|
441
533
|
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() &&
|
|
534
|
+
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
535
|
};
|
|
444
536
|
const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
445
537
|
const monorepoRoot = findMonorepoRoot(directory);
|
|
@@ -447,11 +539,17 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
447
539
|
reactVersion: null,
|
|
448
540
|
framework: "unknown"
|
|
449
541
|
};
|
|
450
|
-
const
|
|
542
|
+
const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
|
|
543
|
+
if (!isFile(monorepoPackageJsonPath)) return {
|
|
544
|
+
reactVersion: null,
|
|
545
|
+
framework: "unknown"
|
|
546
|
+
};
|
|
547
|
+
const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
|
|
451
548
|
const rootInfo = extractDependencyInfo(rootPackageJson);
|
|
549
|
+
const catalogVersion = resolveCatalogVersion(rootPackageJson, "react");
|
|
452
550
|
const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
|
|
453
551
|
return {
|
|
454
|
-
reactVersion: rootInfo.reactVersion ?? workspaceInfo.reactVersion,
|
|
552
|
+
reactVersion: rootInfo.reactVersion ?? catalogVersion ?? workspaceInfo.reactVersion,
|
|
455
553
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
456
554
|
};
|
|
457
555
|
};
|
|
@@ -485,7 +583,7 @@ const discoverReactSubprojects = (rootDirectory) => {
|
|
|
485
583
|
if (!fs.existsSync(rootDirectory) || !fs.statSync(rootDirectory).isDirectory()) return [];
|
|
486
584
|
const packages = [];
|
|
487
585
|
const rootPackageJsonPath = path.join(rootDirectory, "package.json");
|
|
488
|
-
if (
|
|
586
|
+
if (isFile(rootPackageJsonPath)) {
|
|
489
587
|
const rootPackageJson = readPackageJson(rootPackageJsonPath);
|
|
490
588
|
if (hasReactDependency(rootPackageJson)) {
|
|
491
589
|
const name = rootPackageJson.name ?? path.basename(rootDirectory);
|
|
@@ -500,7 +598,7 @@ const discoverReactSubprojects = (rootDirectory) => {
|
|
|
500
598
|
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
501
599
|
const subdirectory = path.join(rootDirectory, entry.name);
|
|
502
600
|
const packageJsonPath = path.join(subdirectory, "package.json");
|
|
503
|
-
if (!
|
|
601
|
+
if (!isFile(packageJsonPath)) continue;
|
|
504
602
|
const packageJson = readPackageJson(packageJsonPath);
|
|
505
603
|
if (!hasReactDependency(packageJson)) continue;
|
|
506
604
|
const name = packageJson.name ?? entry.name;
|
|
@@ -513,7 +611,7 @@ const discoverReactSubprojects = (rootDirectory) => {
|
|
|
513
611
|
};
|
|
514
612
|
const listWorkspacePackages = (rootDirectory) => {
|
|
515
613
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
516
|
-
if (!
|
|
614
|
+
if (!isFile(packageJsonPath)) return [];
|
|
517
615
|
const patterns = getWorkspacePatterns(rootDirectory, readPackageJson(packageJsonPath));
|
|
518
616
|
if (patterns.length === 0) return [];
|
|
519
617
|
const packages = [];
|
|
@@ -536,7 +634,7 @@ const hasCompilerPackage = (packageJson) => {
|
|
|
536
634
|
return Object.keys(allDependencies).some((packageName) => REACT_COMPILER_PACKAGES.has(packageName));
|
|
537
635
|
};
|
|
538
636
|
const fileContainsPattern = (filePath, pattern) => {
|
|
539
|
-
if (!
|
|
637
|
+
if (!isFile(filePath)) return false;
|
|
540
638
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
541
639
|
return pattern.test(content);
|
|
542
640
|
};
|
|
@@ -550,7 +648,7 @@ const detectReactCompiler = (directory, packageJson) => {
|
|
|
550
648
|
let ancestorDirectory = path.dirname(directory);
|
|
551
649
|
while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
|
|
552
650
|
const ancestorPackagePath = path.join(ancestorDirectory, "package.json");
|
|
553
|
-
if (
|
|
651
|
+
if (isFile(ancestorPackagePath)) {
|
|
554
652
|
if (hasCompilerPackage(readPackageJson(ancestorPackagePath))) return true;
|
|
555
653
|
}
|
|
556
654
|
ancestorDirectory = path.dirname(ancestorDirectory);
|
|
@@ -559,9 +657,10 @@ const detectReactCompiler = (directory, packageJson) => {
|
|
|
559
657
|
};
|
|
560
658
|
const discoverProject = (directory) => {
|
|
561
659
|
const packageJsonPath = path.join(directory, "package.json");
|
|
562
|
-
if (!
|
|
660
|
+
if (!isFile(packageJsonPath)) throw new Error(`No package.json found in ${directory}`);
|
|
563
661
|
const packageJson = readPackageJson(packageJsonPath);
|
|
564
662
|
let { reactVersion, framework } = extractDependencyInfo(packageJson);
|
|
663
|
+
if (!reactVersion) reactVersion = resolveCatalogVersion(packageJson, "react");
|
|
565
664
|
if (!reactVersion || framework === "unknown") {
|
|
566
665
|
const workspaceInfo = findReactInWorkspaces(directory, packageJson);
|
|
567
666
|
if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
|
|
@@ -661,10 +760,9 @@ const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText
|
|
|
661
760
|
//#region src/utils/load-config.ts
|
|
662
761
|
const CONFIG_FILENAME = "react-doctor.config.json";
|
|
663
762
|
const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
|
|
664
|
-
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
665
763
|
const loadConfig = (rootDirectory) => {
|
|
666
764
|
const configFilePath = path.join(rootDirectory, CONFIG_FILENAME);
|
|
667
|
-
if (
|
|
765
|
+
if (isFile(configFilePath)) try {
|
|
668
766
|
const fileContent = fs.readFileSync(configFilePath, "utf-8");
|
|
669
767
|
const parsed = JSON.parse(fileContent);
|
|
670
768
|
if (isPlainObject(parsed)) return parsed;
|
|
@@ -673,7 +771,7 @@ const loadConfig = (rootDirectory) => {
|
|
|
673
771
|
console.warn(`Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
|
|
674
772
|
}
|
|
675
773
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
676
|
-
if (
|
|
774
|
+
if (isFile(packageJsonPath)) try {
|
|
677
775
|
const fileContent = fs.readFileSync(packageJsonPath, "utf-8");
|
|
678
776
|
const embeddedConfig = JSON.parse(fileContent)[PACKAGE_JSON_CONFIG_KEY];
|
|
679
777
|
if (isPlainObject(embeddedConfig)) return embeddedConfig;
|
|
@@ -932,7 +1030,7 @@ const runKnip = async (rootDirectory) => {
|
|
|
932
1030
|
let knipResult;
|
|
933
1031
|
if (monorepoRoot) {
|
|
934
1032
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
935
|
-
const workspaceName = (
|
|
1033
|
+
const workspaceName = (isFile(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) : {}).name ?? path.basename(rootDirectory);
|
|
936
1034
|
try {
|
|
937
1035
|
knipResult = await runKnipWithOptions(monorepoRoot, workspaceName);
|
|
938
1036
|
} catch {
|
|
@@ -1215,7 +1313,7 @@ const RULE_CATEGORY_MAP = {
|
|
|
1215
1313
|
"react-doctor/rn-no-single-element-style-array": "React Native"
|
|
1216
1314
|
};
|
|
1217
1315
|
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}
|
|
1316
|
+
"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
1317
|
"no-fetch-in-effect": "Use `useQuery()` from @tanstack/react-query, `useSWR()`, or fetch in a Server Component instead",
|
|
1220
1318
|
"no-cascading-set-state": "Combine into useReducer: `const [state, dispatch] = useReducer(reducer, initialState)`",
|
|
1221
1319
|
"no-effect-event-handler": "Move the conditional logic into onClick, onChange, or onSubmit handlers directly",
|
|
@@ -1255,7 +1353,7 @@ const RULE_HELP_MAP = {
|
|
|
1255
1353
|
"nextjs-no-use-search-params-without-suspense": "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
|
|
1256
1354
|
"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
1355
|
"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
|
|
1356
|
+
"nextjs-no-client-side-redirect": "Use `redirect('/path')` from 'next/navigation' directly (works in both server and client components), or handle in middleware",
|
|
1259
1357
|
"nextjs-no-redirect-in-try-catch": "Move the redirect/notFound call outside the try block, or add `unstable_rethrow(error)` in the catch",
|
|
1260
1358
|
"nextjs-image-missing-sizes": "Add sizes for responsive behavior: `sizes=\"(max-width: 768px) 100vw, 50vw\"` matching your layout breakpoints",
|
|
1261
1359
|
"nextjs-no-native-script": "`import Script from \"next/script\"` — use `strategy=\"afterInteractive\"` for analytics or `\"lazyOnload\"` for widgets",
|
|
@@ -1343,7 +1441,14 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
1343
1441
|
child.stdout.on("data", (buffer) => stdoutBuffers.push(buffer));
|
|
1344
1442
|
child.stderr.on("data", (buffer) => stderrBuffers.push(buffer));
|
|
1345
1443
|
child.on("error", (error) => reject(/* @__PURE__ */ new Error(`Failed to run oxlint: ${error.message}`)));
|
|
1346
|
-
child.on("close", () => {
|
|
1444
|
+
child.on("close", (code, signal) => {
|
|
1445
|
+
if (signal) {
|
|
1446
|
+
const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
|
|
1447
|
+
const hint = signal === "SIGABRT" ? " (out of memory — try scanning fewer files with --diff)" : "";
|
|
1448
|
+
const detail = stderrOutput ? `: ${stderrOutput}` : "";
|
|
1449
|
+
reject(/* @__PURE__ */ new Error(`oxlint was killed by ${signal}${hint}${detail}`));
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1347
1452
|
const output = Buffer.concat(stdoutBuffers).toString("utf-8").trim();
|
|
1348
1453
|
if (!output) {
|
|
1349
1454
|
const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
|
|
@@ -1614,7 +1719,7 @@ const buildCountsSummaryLine = (diagnostics, totalSourceFileCount, elapsedMillis
|
|
|
1614
1719
|
renderedParts.push(highlighter.dim(fileCountText), highlighter.dim(elapsedTimeText));
|
|
1615
1720
|
return createFramedLine(plainParts.join(" "), renderedParts.join(" "));
|
|
1616
1721
|
};
|
|
1617
|
-
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName, totalSourceFileCount, noScoreMessage) => {
|
|
1722
|
+
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName, totalSourceFileCount, noScoreMessage, isOffline) => {
|
|
1618
1723
|
printFramedBox([...buildBrandingLines(scoreResult, noScoreMessage), buildCountsSummaryLine(diagnostics, totalSourceFileCount, elapsedMilliseconds)]);
|
|
1619
1724
|
try {
|
|
1620
1725
|
const diagnosticsDirectory = writeDiagnosticsDirectory(diagnostics);
|
|
@@ -1623,9 +1728,11 @@ const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName
|
|
|
1623
1728
|
} catch {
|
|
1624
1729
|
logger.break();
|
|
1625
1730
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1731
|
+
if (!isOffline) {
|
|
1732
|
+
const shareUrl = buildShareUrl(diagnostics, scoreResult, projectName);
|
|
1733
|
+
logger.break();
|
|
1734
|
+
logger.dim(` Share your results: ${highlighter.info(shareUrl)}`);
|
|
1735
|
+
}
|
|
1629
1736
|
};
|
|
1630
1737
|
const resolveOxlintNode = async (isLintEnabled, isScoreOnly) => {
|
|
1631
1738
|
if (!isLintEnabled) return null;
|
|
@@ -1745,8 +1852,8 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1745
1852
|
if (didLintFail) skippedChecks.push("lint");
|
|
1746
1853
|
if (didDeadCodeFail) skippedChecks.push("dead code");
|
|
1747
1854
|
const hasSkippedChecks = skippedChecks.length > 0;
|
|
1748
|
-
const scoreResult = options.offline ?
|
|
1749
|
-
const noScoreMessage =
|
|
1855
|
+
const scoreResult = options.offline ? calculateScoreLocally(diagnostics) : await calculateScore(diagnostics);
|
|
1856
|
+
const noScoreMessage = OFFLINE_MESSAGE;
|
|
1750
1857
|
if (options.scoreOnly) {
|
|
1751
1858
|
if (scoreResult) logger.log(`${scoreResult.score}`);
|
|
1752
1859
|
else logger.dim(noScoreMessage);
|
|
@@ -1777,7 +1884,7 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1777
1884
|
}
|
|
1778
1885
|
printDiagnostics(diagnostics, options.verbose);
|
|
1779
1886
|
const displayedSourceFileCount = isDiffMode ? includePaths.length : projectInfo.sourceFileCount;
|
|
1780
|
-
printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName, displayedSourceFileCount, noScoreMessage);
|
|
1887
|
+
printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName, displayedSourceFileCount, noScoreMessage, options.offline);
|
|
1781
1888
|
if (hasSkippedChecks) {
|
|
1782
1889
|
const skippedLabel = skippedChecks.join(" and ");
|
|
1783
1890
|
logger.break();
|
|
@@ -2104,7 +2211,7 @@ const maybePromptSkillInstall = async (shouldSkipPrompts) => {
|
|
|
2104
2211
|
|
|
2105
2212
|
//#endregion
|
|
2106
2213
|
//#region src/cli.ts
|
|
2107
|
-
const VERSION = "0.0.
|
|
2214
|
+
const VERSION = "0.0.30";
|
|
2108
2215
|
const VALID_FAIL_ON_LEVELS = new Set([
|
|
2109
2216
|
"error",
|
|
2110
2217
|
"warning",
|
|
@@ -2167,7 +2274,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isSco
|
|
|
2167
2274
|
});
|
|
2168
2275
|
return Boolean(shouldScanChangedOnly);
|
|
2169
2276
|
};
|
|
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("--
|
|
2277
|
+
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
2278
|
const isScoreOnly = flags.score;
|
|
2172
2279
|
try {
|
|
2173
2280
|
const resolvedDirectory = path.resolve(directory);
|