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/dist/index.js
CHANGED
|
@@ -12,11 +12,18 @@ 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;
|
|
21
28
|
const AMI_WEBSITE_URL = "https://ami.dev";
|
|
22
29
|
const AMI_INSTALL_URL = `${AMI_WEBSITE_URL}/install.sh`;
|
|
@@ -71,6 +78,46 @@ const proxyFetch = async (url, init) => {
|
|
|
71
78
|
|
|
72
79
|
//#endregion
|
|
73
80
|
//#region src/utils/calculate-score.ts
|
|
81
|
+
const getScoreLabel = (score) => {
|
|
82
|
+
if (score >= SCORE_GOOD_THRESHOLD) return "Great";
|
|
83
|
+
if (score >= SCORE_OK_THRESHOLD) return "Needs work";
|
|
84
|
+
return "Critical";
|
|
85
|
+
};
|
|
86
|
+
const countUniqueRules = (diagnostics) => {
|
|
87
|
+
const errorRules = /* @__PURE__ */ new Set();
|
|
88
|
+
const warningRules = /* @__PURE__ */ new Set();
|
|
89
|
+
for (const diagnostic of diagnostics) {
|
|
90
|
+
const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
91
|
+
if (diagnostic.severity === "error") errorRules.add(ruleKey);
|
|
92
|
+
else warningRules.add(ruleKey);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
errorRuleCount: errorRules.size,
|
|
96
|
+
warningRuleCount: warningRules.size
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
const scoreFromRuleCounts = (errorRuleCount, warningRuleCount) => {
|
|
100
|
+
const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;
|
|
101
|
+
return Math.max(0, Math.round(PERFECT_SCORE - penalty));
|
|
102
|
+
};
|
|
103
|
+
const estimateScoreLocally = (diagnostics) => {
|
|
104
|
+
const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);
|
|
105
|
+
const currentScore = scoreFromRuleCounts(errorRuleCount, warningRuleCount);
|
|
106
|
+
const estimatedScore = scoreFromRuleCounts(Math.round(errorRuleCount * (1 - ERROR_ESTIMATED_FIX_RATE)), Math.round(warningRuleCount * (1 - WARNING_ESTIMATED_FIX_RATE)));
|
|
107
|
+
return {
|
|
108
|
+
currentScore,
|
|
109
|
+
currentLabel: getScoreLabel(currentScore),
|
|
110
|
+
estimatedScore,
|
|
111
|
+
estimatedLabel: getScoreLabel(estimatedScore)
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
const calculateScoreLocally = (diagnostics) => {
|
|
115
|
+
const { currentScore, currentLabel } = estimateScoreLocally(diagnostics);
|
|
116
|
+
return {
|
|
117
|
+
score: currentScore,
|
|
118
|
+
label: currentLabel
|
|
119
|
+
};
|
|
120
|
+
};
|
|
74
121
|
const calculateScore = async (diagnostics) => {
|
|
75
122
|
try {
|
|
76
123
|
const response = await proxyFetch(SCORE_API_URL, {
|
|
@@ -78,10 +125,10 @@ const calculateScore = async (diagnostics) => {
|
|
|
78
125
|
headers: { "Content-Type": "application/json" },
|
|
79
126
|
body: JSON.stringify({ diagnostics })
|
|
80
127
|
});
|
|
81
|
-
if (!response.ok) return
|
|
128
|
+
if (!response.ok) return calculateScoreLocally(diagnostics);
|
|
82
129
|
return await response.json();
|
|
83
130
|
} catch {
|
|
84
|
-
return
|
|
131
|
+
return calculateScoreLocally(diagnostics);
|
|
85
132
|
}
|
|
86
133
|
};
|
|
87
134
|
|
|
@@ -89,13 +136,33 @@ const calculateScore = async (diagnostics) => {
|
|
|
89
136
|
//#region src/plugin/constants.ts
|
|
90
137
|
const MOTION_LIBRARY_PACKAGES = new Set(["framer-motion", "motion"]);
|
|
91
138
|
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/utils/is-file.ts
|
|
141
|
+
const isFile = (filePath) => {
|
|
142
|
+
try {
|
|
143
|
+
return fs.statSync(filePath).isFile();
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
92
149
|
//#endregion
|
|
93
150
|
//#region src/utils/read-package-json.ts
|
|
94
|
-
const readPackageJson = (packageJsonPath) =>
|
|
151
|
+
const readPackageJson = (packageJsonPath) => {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof Error && "code" in error) {
|
|
156
|
+
const { code } = error;
|
|
157
|
+
if (code === "EISDIR" || code === "EACCES") return {};
|
|
158
|
+
}
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
95
162
|
|
|
96
163
|
//#endregion
|
|
97
164
|
//#region src/utils/check-reduced-motion.ts
|
|
98
|
-
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion";
|
|
165
|
+
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
|
|
99
166
|
const REDUCED_MOTION_FILE_GLOBS = "\"*.ts\" \"*.tsx\" \"*.js\" \"*.jsx\" \"*.css\" \"*.scss\"";
|
|
100
167
|
const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
101
168
|
filePath: "package.json",
|
|
@@ -111,7 +178,7 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
|
111
178
|
};
|
|
112
179
|
const checkReducedMotion = (rootDirectory) => {
|
|
113
180
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
114
|
-
if (!
|
|
181
|
+
if (!isFile(packageJsonPath)) return [];
|
|
115
182
|
let hasMotionLibrary = false;
|
|
116
183
|
try {
|
|
117
184
|
const packageJson = readPackageJson(packageJsonPath);
|
|
@@ -139,7 +206,7 @@ const checkReducedMotion = (rootDirectory) => {
|
|
|
139
206
|
//#region src/utils/match-glob-pattern.ts
|
|
140
207
|
const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
|
|
141
208
|
const compileGlobPattern = (pattern) => {
|
|
142
|
-
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
209
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\//, "");
|
|
143
210
|
let regexSource = "^";
|
|
144
211
|
let characterIndex = 0;
|
|
145
212
|
while (characterIndex < normalizedPattern.length) if (normalizedPattern[characterIndex] === "*" && normalizedPattern[characterIndex + 1] === "*") if (normalizedPattern[characterIndex + 2] === "/") {
|
|
@@ -177,26 +244,67 @@ const filterIgnoredDiagnostics = (diagnostics, config) => {
|
|
|
177
244
|
return true;
|
|
178
245
|
});
|
|
179
246
|
};
|
|
247
|
+
const DISABLE_NEXT_LINE_PATTERN = /\/\/\s*react-doctor-disable-next-line\b(?:\s+(.+))?/;
|
|
248
|
+
const DISABLE_LINE_PATTERN = /\/\/\s*react-doctor-disable-line\b(?:\s+(.+))?/;
|
|
249
|
+
const isRuleSuppressed = (commentRules, ruleId) => {
|
|
250
|
+
if (!commentRules?.trim()) return true;
|
|
251
|
+
return commentRules.split(/[,\s]+/).some((rule) => rule.trim() === ruleId);
|
|
252
|
+
};
|
|
253
|
+
const filterInlineSuppressions = (diagnostics, rootDirectory) => {
|
|
254
|
+
const fileLineCache = /* @__PURE__ */ new Map();
|
|
255
|
+
const getFileLines = (filePath) => {
|
|
256
|
+
const cached = fileLineCache.get(filePath);
|
|
257
|
+
if (cached !== void 0) return cached;
|
|
258
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
|
|
259
|
+
try {
|
|
260
|
+
const lines = fs.readFileSync(absolutePath, "utf-8").split("\n");
|
|
261
|
+
fileLineCache.set(filePath, lines);
|
|
262
|
+
return lines;
|
|
263
|
+
} catch {
|
|
264
|
+
fileLineCache.set(filePath, null);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
return diagnostics.filter((diagnostic) => {
|
|
269
|
+
if (diagnostic.line <= 0) return true;
|
|
270
|
+
const lines = getFileLines(diagnostic.filePath);
|
|
271
|
+
if (!lines) return true;
|
|
272
|
+
const ruleId = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
273
|
+
const currentLine = lines[diagnostic.line - 1];
|
|
274
|
+
if (currentLine) {
|
|
275
|
+
const lineMatch = currentLine.match(DISABLE_LINE_PATTERN);
|
|
276
|
+
if (lineMatch && isRuleSuppressed(lineMatch[1], ruleId)) return false;
|
|
277
|
+
}
|
|
278
|
+
if (diagnostic.line >= 2) {
|
|
279
|
+
const prevLine = lines[diagnostic.line - 2];
|
|
280
|
+
if (prevLine) {
|
|
281
|
+
const nextLineMatch = prevLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
282
|
+
if (nextLineMatch && isRuleSuppressed(nextLineMatch[1], ruleId)) return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return true;
|
|
286
|
+
});
|
|
287
|
+
};
|
|
180
288
|
|
|
181
289
|
//#endregion
|
|
182
290
|
//#region src/utils/combine-diagnostics.ts
|
|
183
291
|
const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
|
|
184
292
|
const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isDiffMode, userConfig) => {
|
|
185
|
-
const
|
|
293
|
+
const merged = [
|
|
186
294
|
...lintDiagnostics,
|
|
187
295
|
...deadCodeDiagnostics,
|
|
188
296
|
...isDiffMode ? [] : checkReducedMotion(directory)
|
|
189
297
|
];
|
|
190
|
-
return userConfig ? filterIgnoredDiagnostics(
|
|
298
|
+
return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(merged, userConfig) : merged, directory);
|
|
191
299
|
};
|
|
192
300
|
|
|
193
301
|
//#endregion
|
|
194
302
|
//#region src/utils/find-monorepo-root.ts
|
|
195
303
|
const isMonorepoRoot = (directory) => {
|
|
196
|
-
if (
|
|
197
|
-
if (
|
|
304
|
+
if (isFile(path.join(directory, "pnpm-workspace.yaml"))) return true;
|
|
305
|
+
if (isFile(path.join(directory, "nx.json"))) return true;
|
|
198
306
|
const packageJsonPath = path.join(directory, "package.json");
|
|
199
|
-
if (!
|
|
307
|
+
if (!isFile(packageJsonPath)) return false;
|
|
200
308
|
const packageJson = readPackageJson(packageJsonPath);
|
|
201
309
|
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
202
310
|
};
|
|
@@ -209,6 +317,10 @@ const findMonorepoRoot = (startDirectory) => {
|
|
|
209
317
|
return null;
|
|
210
318
|
};
|
|
211
319
|
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/utils/is-plain-object.ts
|
|
322
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
323
|
+
|
|
212
324
|
//#endregion
|
|
213
325
|
//#region src/utils/discover-project.ts
|
|
214
326
|
const REACT_COMPILER_PACKAGES = new Set([
|
|
@@ -297,16 +409,37 @@ const detectFramework = (dependencies) => {
|
|
|
297
409
|
for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
|
|
298
410
|
return "unknown";
|
|
299
411
|
};
|
|
412
|
+
const isCatalogReference = (version) => version.startsWith("catalog:");
|
|
413
|
+
const resolveVersionFromCatalog = (catalog, packageName) => {
|
|
414
|
+
const version = catalog[packageName];
|
|
415
|
+
if (typeof version === "string" && !isCatalogReference(version)) return version;
|
|
416
|
+
return null;
|
|
417
|
+
};
|
|
418
|
+
const resolveCatalogVersion = (packageJson, packageName) => {
|
|
419
|
+
const raw = packageJson;
|
|
420
|
+
if (isPlainObject(raw.catalog)) {
|
|
421
|
+
const version = resolveVersionFromCatalog(raw.catalog, packageName);
|
|
422
|
+
if (version) return version;
|
|
423
|
+
}
|
|
424
|
+
if (isPlainObject(raw.catalogs)) {
|
|
425
|
+
for (const catalogEntries of Object.values(raw.catalogs)) if (isPlainObject(catalogEntries)) {
|
|
426
|
+
const version = resolveVersionFromCatalog(catalogEntries, packageName);
|
|
427
|
+
if (version) return version;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
};
|
|
300
432
|
const extractDependencyInfo = (packageJson) => {
|
|
301
433
|
const allDependencies = collectAllDependencies(packageJson);
|
|
434
|
+
const rawVersion = allDependencies.react ?? null;
|
|
302
435
|
return {
|
|
303
|
-
reactVersion:
|
|
436
|
+
reactVersion: rawVersion && !isCatalogReference(rawVersion) ? rawVersion : null,
|
|
304
437
|
framework: detectFramework(allDependencies)
|
|
305
438
|
};
|
|
306
439
|
};
|
|
307
440
|
const parsePnpmWorkspacePatterns = (rootDirectory) => {
|
|
308
441
|
const workspacePath = path.join(rootDirectory, "pnpm-workspace.yaml");
|
|
309
|
-
if (!
|
|
442
|
+
if (!isFile(workspacePath)) return [];
|
|
310
443
|
const content = fs.readFileSync(workspacePath, "utf-8");
|
|
311
444
|
const patterns = [];
|
|
312
445
|
let isInsidePackagesBlock = false;
|
|
@@ -332,14 +465,14 @@ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
|
|
|
332
465
|
const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
|
|
333
466
|
if (!cleanPattern.includes("*")) {
|
|
334
467
|
const directoryPath = path.join(rootDirectory, cleanPattern);
|
|
335
|
-
if (fs.existsSync(directoryPath) &&
|
|
468
|
+
if (fs.existsSync(directoryPath) && isFile(path.join(directoryPath, "package.json"))) return [directoryPath];
|
|
336
469
|
return [];
|
|
337
470
|
}
|
|
338
471
|
const wildcardIndex = cleanPattern.indexOf("*");
|
|
339
472
|
const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
|
|
340
473
|
const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
|
|
341
474
|
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() &&
|
|
475
|
+
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
476
|
};
|
|
344
477
|
const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
345
478
|
const monorepoRoot = findMonorepoRoot(directory);
|
|
@@ -347,11 +480,17 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
347
480
|
reactVersion: null,
|
|
348
481
|
framework: "unknown"
|
|
349
482
|
};
|
|
350
|
-
const
|
|
483
|
+
const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
|
|
484
|
+
if (!isFile(monorepoPackageJsonPath)) return {
|
|
485
|
+
reactVersion: null,
|
|
486
|
+
framework: "unknown"
|
|
487
|
+
};
|
|
488
|
+
const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
|
|
351
489
|
const rootInfo = extractDependencyInfo(rootPackageJson);
|
|
490
|
+
const catalogVersion = resolveCatalogVersion(rootPackageJson, "react");
|
|
352
491
|
const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
|
|
353
492
|
return {
|
|
354
|
-
reactVersion: rootInfo.reactVersion ?? workspaceInfo.reactVersion,
|
|
493
|
+
reactVersion: rootInfo.reactVersion ?? catalogVersion ?? workspaceInfo.reactVersion,
|
|
355
494
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
356
495
|
};
|
|
357
496
|
};
|
|
@@ -377,7 +516,7 @@ const hasCompilerPackage = (packageJson) => {
|
|
|
377
516
|
return Object.keys(allDependencies).some((packageName) => REACT_COMPILER_PACKAGES.has(packageName));
|
|
378
517
|
};
|
|
379
518
|
const fileContainsPattern = (filePath, pattern) => {
|
|
380
|
-
if (!
|
|
519
|
+
if (!isFile(filePath)) return false;
|
|
381
520
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
382
521
|
return pattern.test(content);
|
|
383
522
|
};
|
|
@@ -391,7 +530,7 @@ const detectReactCompiler = (directory, packageJson) => {
|
|
|
391
530
|
let ancestorDirectory = path.dirname(directory);
|
|
392
531
|
while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
|
|
393
532
|
const ancestorPackagePath = path.join(ancestorDirectory, "package.json");
|
|
394
|
-
if (
|
|
533
|
+
if (isFile(ancestorPackagePath)) {
|
|
395
534
|
if (hasCompilerPackage(readPackageJson(ancestorPackagePath))) return true;
|
|
396
535
|
}
|
|
397
536
|
ancestorDirectory = path.dirname(ancestorDirectory);
|
|
@@ -400,9 +539,10 @@ const detectReactCompiler = (directory, packageJson) => {
|
|
|
400
539
|
};
|
|
401
540
|
const discoverProject = (directory) => {
|
|
402
541
|
const packageJsonPath = path.join(directory, "package.json");
|
|
403
|
-
if (!
|
|
542
|
+
if (!isFile(packageJsonPath)) throw new Error(`No package.json found in ${directory}`);
|
|
404
543
|
const packageJson = readPackageJson(packageJsonPath);
|
|
405
544
|
let { reactVersion, framework } = extractDependencyInfo(packageJson);
|
|
545
|
+
if (!reactVersion) reactVersion = resolveCatalogVersion(packageJson, "react");
|
|
406
546
|
if (!reactVersion || framework === "unknown") {
|
|
407
547
|
const workspaceInfo = findReactInWorkspaces(directory, packageJson);
|
|
408
548
|
if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
|
|
@@ -432,10 +572,9 @@ const discoverProject = (directory) => {
|
|
|
432
572
|
//#region src/utils/load-config.ts
|
|
433
573
|
const CONFIG_FILENAME = "react-doctor.config.json";
|
|
434
574
|
const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
|
|
435
|
-
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
436
575
|
const loadConfig = (rootDirectory) => {
|
|
437
576
|
const configFilePath = path.join(rootDirectory, CONFIG_FILENAME);
|
|
438
|
-
if (
|
|
577
|
+
if (isFile(configFilePath)) try {
|
|
439
578
|
const fileContent = fs.readFileSync(configFilePath, "utf-8");
|
|
440
579
|
const parsed = JSON.parse(fileContent);
|
|
441
580
|
if (isPlainObject(parsed)) return parsed;
|
|
@@ -444,7 +583,7 @@ const loadConfig = (rootDirectory) => {
|
|
|
444
583
|
console.warn(`Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
|
|
445
584
|
}
|
|
446
585
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
447
|
-
if (
|
|
586
|
+
if (isFile(packageJsonPath)) try {
|
|
448
587
|
const fileContent = fs.readFileSync(packageJsonPath, "utf-8");
|
|
449
588
|
const embeddedConfig = JSON.parse(fileContent)[PACKAGE_JSON_CONFIG_KEY];
|
|
450
589
|
if (isPlainObject(embeddedConfig)) return embeddedConfig;
|
|
@@ -542,7 +681,7 @@ const runKnip = async (rootDirectory) => {
|
|
|
542
681
|
let knipResult;
|
|
543
682
|
if (monorepoRoot) {
|
|
544
683
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
545
|
-
const workspaceName = (
|
|
684
|
+
const workspaceName = (isFile(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) : {}).name ?? path.basename(rootDirectory);
|
|
546
685
|
try {
|
|
547
686
|
knipResult = await runKnipWithOptions(monorepoRoot, workspaceName);
|
|
548
687
|
} catch {
|
|
@@ -825,7 +964,7 @@ const RULE_CATEGORY_MAP = {
|
|
|
825
964
|
"react-doctor/rn-no-single-element-style-array": "React Native"
|
|
826
965
|
};
|
|
827
966
|
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}
|
|
967
|
+
"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
968
|
"no-fetch-in-effect": "Use `useQuery()` from @tanstack/react-query, `useSWR()`, or fetch in a Server Component instead",
|
|
830
969
|
"no-cascading-set-state": "Combine into useReducer: `const [state, dispatch] = useReducer(reducer, initialState)`",
|
|
831
970
|
"no-effect-event-handler": "Move the conditional logic into onClick, onChange, or onSubmit handlers directly",
|
|
@@ -865,7 +1004,7 @@ const RULE_HELP_MAP = {
|
|
|
865
1004
|
"nextjs-no-use-search-params-without-suspense": "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
|
|
866
1005
|
"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
1006
|
"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
|
|
1007
|
+
"nextjs-no-client-side-redirect": "Use `redirect('/path')` from 'next/navigation' directly (works in both server and client components), or handle in middleware",
|
|
869
1008
|
"nextjs-no-redirect-in-try-catch": "Move the redirect/notFound call outside the try block, or add `unstable_rethrow(error)` in the catch",
|
|
870
1009
|
"nextjs-image-missing-sizes": "Add sizes for responsive behavior: `sizes=\"(max-width: 768px) 100vw, 50vw\"` matching your layout breakpoints",
|
|
871
1010
|
"nextjs-no-native-script": "`import Script from \"next/script\"` — use `strategy=\"afterInteractive\"` for analytics or `\"lazyOnload\"` for widgets",
|
|
@@ -953,7 +1092,14 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
953
1092
|
child.stdout.on("data", (buffer) => stdoutBuffers.push(buffer));
|
|
954
1093
|
child.stderr.on("data", (buffer) => stderrBuffers.push(buffer));
|
|
955
1094
|
child.on("error", (error) => reject(/* @__PURE__ */ new Error(`Failed to run oxlint: ${error.message}`)));
|
|
956
|
-
child.on("close", () => {
|
|
1095
|
+
child.on("close", (code, signal) => {
|
|
1096
|
+
if (signal) {
|
|
1097
|
+
const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
|
|
1098
|
+
const hint = signal === "SIGABRT" ? " (out of memory — try scanning fewer files with --diff)" : "";
|
|
1099
|
+
const detail = stderrOutput ? `: ${stderrOutput}` : "";
|
|
1100
|
+
reject(/* @__PURE__ */ new Error(`oxlint was killed by ${signal}${hint}${detail}`));
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
957
1103
|
const output = Buffer.concat(stdoutBuffers).toString("utf-8").trim();
|
|
958
1104
|
if (!output) {
|
|
959
1105
|
const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
|