react-doctor 0.0.41 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{diagnose-browser-B17IqMa3.d.ts → browser-DFbjNpPb.d.ts} +15 -15
- package/dist/browser-DFbjNpPb.d.ts.map +1 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -3
- package/dist/cli.js +95 -107
- package/dist/cli.js.map +1 -1
- package/dist/index.js +71 -65
- package/dist/index.js.map +1 -1
- package/dist/{process-browser-diagnostics-DpaZeYLI.js → process-browser-diagnostics-BHiLPUJT.js} +5 -25
- package/dist/process-browser-diagnostics-BHiLPUJT.js.map +1 -0
- package/dist/react-doctor-plugin.js +25 -63
- package/dist/react-doctor-plugin.js.map +1 -1
- package/dist/worker.d.ts +1 -1
- package/dist/worker.js +2 -3
- package/package.json +1 -1
- package/dist/diagnose-browser-B17IqMa3.d.ts.map +0 -1
- package/dist/process-browser-diagnostics-DpaZeYLI.js.map +0 -1
package/dist/{process-browser-diagnostics-DpaZeYLI.js → process-browser-diagnostics-BHiLPUJT.js}
RENAMED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
//#region src/constants.ts
|
|
2
2
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
3
|
-
const PERFECT_SCORE = 100;
|
|
4
|
-
const SCORE_GOOD_THRESHOLD = 75;
|
|
5
|
-
const SCORE_OK_THRESHOLD = 50;
|
|
6
3
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
7
4
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
8
|
-
const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
9
5
|
const ERROR_RULE_PENALTY = 1.5;
|
|
10
6
|
const WARNING_RULE_PENALTY = .75;
|
|
11
|
-
const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
12
|
-
|
|
13
7
|
//#endregion
|
|
14
8
|
//#region src/core/calculate-score-locally.ts
|
|
15
9
|
const getScoreLabel = (score) => {
|
|
16
|
-
if (score >=
|
|
17
|
-
if (score >=
|
|
10
|
+
if (score >= 75) return "Great";
|
|
11
|
+
if (score >= 50) return "Needs work";
|
|
18
12
|
return "Critical";
|
|
19
13
|
};
|
|
20
14
|
const countUniqueRules = (diagnostics) => {
|
|
@@ -32,7 +26,7 @@ const countUniqueRules = (diagnostics) => {
|
|
|
32
26
|
};
|
|
33
27
|
const scoreFromRuleCounts = (errorRuleCount, warningRuleCount) => {
|
|
34
28
|
const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;
|
|
35
|
-
return Math.max(0, Math.round(
|
|
29
|
+
return Math.max(0, Math.round(100 - penalty));
|
|
36
30
|
};
|
|
37
31
|
const calculateScoreLocally = (diagnostics) => {
|
|
38
32
|
const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);
|
|
@@ -42,7 +36,6 @@ const calculateScoreLocally = (diagnostics) => {
|
|
|
42
36
|
label: getScoreLabel(score)
|
|
43
37
|
};
|
|
44
38
|
};
|
|
45
|
-
|
|
46
39
|
//#endregion
|
|
47
40
|
//#region src/core/try-score-from-api.ts
|
|
48
41
|
const parseScoreResult = (value) => {
|
|
@@ -74,11 +67,9 @@ const tryScoreFromApi = async (diagnostics, fetchImplementation) => {
|
|
|
74
67
|
clearTimeout(timeoutId);
|
|
75
68
|
}
|
|
76
69
|
};
|
|
77
|
-
|
|
78
70
|
//#endregion
|
|
79
71
|
//#region src/utils/calculate-score-browser.ts
|
|
80
72
|
const calculateScore = async (diagnostics) => await tryScoreFromApi(diagnostics, fetch) ?? calculateScoreLocally(diagnostics);
|
|
81
|
-
|
|
82
73
|
//#endregion
|
|
83
74
|
//#region src/utils/match-glob-pattern.ts
|
|
84
75
|
const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
|
|
@@ -106,7 +97,6 @@ const compileGlobPattern = (pattern) => {
|
|
|
106
97
|
regexSource += "$";
|
|
107
98
|
return new RegExp(regexSource);
|
|
108
99
|
};
|
|
109
|
-
|
|
110
100
|
//#endregion
|
|
111
101
|
//#region src/utils/is-ignored-file.ts
|
|
112
102
|
const toRelativePath = (filePath, rootDirectory) => {
|
|
@@ -121,7 +111,6 @@ const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
|
|
|
121
111
|
const relativePath = toRelativePath(filePath, rootDirectory);
|
|
122
112
|
return patterns.some((pattern) => pattern.test(relativePath));
|
|
123
113
|
};
|
|
124
|
-
|
|
125
114
|
//#endregion
|
|
126
115
|
//#region src/utils/filter-diagnostics.ts
|
|
127
116
|
const resolveCandidateReadPath = (rootDirectory, filePath) => {
|
|
@@ -195,13 +184,11 @@ const filterInlineSuppressions = (diagnostics, rootDirectory, readFileLinesSync)
|
|
|
195
184
|
return true;
|
|
196
185
|
});
|
|
197
186
|
};
|
|
198
|
-
|
|
199
187
|
//#endregion
|
|
200
188
|
//#region src/utils/merge-and-filter-diagnostics.ts
|
|
201
189
|
const mergeAndFilterDiagnostics = (mergedDiagnostics, directory, userConfig, readFileLinesSync) => {
|
|
202
190
|
return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(mergedDiagnostics, userConfig, directory, readFileLinesSync) : mergedDiagnostics, directory, readFileLinesSync);
|
|
203
191
|
};
|
|
204
|
-
|
|
205
192
|
//#endregion
|
|
206
193
|
//#region src/core/build-result.ts
|
|
207
194
|
const buildDiagnoseTimedResult = async (input) => {
|
|
@@ -213,7 +200,6 @@ const buildDiagnoseTimedResult = async (input) => {
|
|
|
213
200
|
elapsedMilliseconds
|
|
214
201
|
};
|
|
215
202
|
};
|
|
216
|
-
|
|
217
203
|
//#endregion
|
|
218
204
|
//#region src/adapters/browser/create-browser-read-file-lines.ts
|
|
219
205
|
const normalizeKey = (rootDirectory, filePath) => {
|
|
@@ -229,7 +215,6 @@ const createBrowserReadFileLinesSync = (rootDirectory, projectFiles) => {
|
|
|
229
215
|
return content.split("\n");
|
|
230
216
|
};
|
|
231
217
|
};
|
|
232
|
-
|
|
233
218
|
//#endregion
|
|
234
219
|
//#region src/adapters/browser/diagnose.ts
|
|
235
220
|
const diagnose = async (input) => {
|
|
@@ -255,7 +240,6 @@ const diagnose = async (input) => {
|
|
|
255
240
|
elapsedMilliseconds: timed.elapsedMilliseconds
|
|
256
241
|
};
|
|
257
242
|
};
|
|
258
|
-
|
|
259
243
|
//#endregion
|
|
260
244
|
//#region src/core/build-diagnose-result.ts
|
|
261
245
|
const buildDiagnoseResult = (params) => ({
|
|
@@ -264,11 +248,9 @@ const buildDiagnoseResult = (params) => ({
|
|
|
264
248
|
project: params.project,
|
|
265
249
|
elapsedMilliseconds: params.elapsedMilliseconds
|
|
266
250
|
});
|
|
267
|
-
|
|
268
251
|
//#endregion
|
|
269
252
|
//#region src/utils/jsx-include-paths.ts
|
|
270
253
|
const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
|
|
271
|
-
|
|
272
254
|
//#endregion
|
|
273
255
|
//#region src/core/diagnose-core.ts
|
|
274
256
|
const diagnoseCore = async (deps, options = {}) => {
|
|
@@ -319,7 +301,6 @@ const diagnoseCore = async (deps, options = {}) => {
|
|
|
319
301
|
elapsedMilliseconds: timed.elapsedMilliseconds
|
|
320
302
|
});
|
|
321
303
|
};
|
|
322
|
-
|
|
323
304
|
//#endregion
|
|
324
305
|
//#region src/adapters/browser/diagnose-browser.ts
|
|
325
306
|
const diagnoseBrowser = async (input, options = {}) => {
|
|
@@ -339,7 +320,6 @@ const diagnoseBrowser = async (input, options = {}) => {
|
|
|
339
320
|
})
|
|
340
321
|
}, options);
|
|
341
322
|
};
|
|
342
|
-
|
|
343
323
|
//#endregion
|
|
344
324
|
//#region src/adapters/browser/process-browser-diagnostics.ts
|
|
345
325
|
const processBrowserDiagnostics = async (input) => {
|
|
@@ -359,7 +339,7 @@ const processBrowserDiagnostics = async (input) => {
|
|
|
359
339
|
score: timed.score
|
|
360
340
|
};
|
|
361
341
|
};
|
|
362
|
-
|
|
363
342
|
//#endregion
|
|
364
343
|
export { calculateScore as a, diagnose as i, diagnoseBrowser as n, calculateScoreLocally as o, diagnoseCore as r, processBrowserDiagnostics as t };
|
|
365
|
-
|
|
344
|
+
|
|
345
|
+
//# sourceMappingURL=process-browser-diagnostics-BHiLPUJT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-browser-diagnostics-BHiLPUJT.js","names":["calculateScoreBrowser","calculateScoreBrowser","calculateScoreBrowser"],"sources":["../src/constants.ts","../src/core/calculate-score-locally.ts","../src/core/try-score-from-api.ts","../src/utils/calculate-score-browser.ts","../src/utils/match-glob-pattern.ts","../src/utils/is-ignored-file.ts","../src/utils/filter-diagnostics.ts","../src/utils/merge-and-filter-diagnostics.ts","../src/core/build-result.ts","../src/adapters/browser/create-browser-read-file-lines.ts","../src/adapters/browser/diagnose.ts","../src/core/build-diagnose-result.ts","../src/utils/jsx-include-paths.ts","../src/core/diagnose-core.ts","../src/adapters/browser/diagnose-browser.ts","../src/adapters/browser/process-browser-diagnostics.ts"],"sourcesContent":["export const SOURCE_FILE_PATTERN = /\\.(tsx?|jsx?)$/;\n\nexport const JSX_FILE_PATTERN = /\\.(tsx|jsx)$/;\n\nexport const MILLISECONDS_PER_SECOND = 1000;\n\nexport const ERROR_PREVIEW_LENGTH_CHARS = 200;\n\nexport const PERFECT_SCORE = 100;\n\nexport const SCORE_GOOD_THRESHOLD = 75;\n\nexport const SCORE_OK_THRESHOLD = 50;\n\nexport const SCORE_BAR_WIDTH_CHARS = 50;\n\nexport const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;\n\nexport const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;\n\nexport const SCORE_API_URL = \"https://www.react.doctor/api/score\";\n\nexport const SHARE_BASE_URL = \"https://www.react.doctor/share\";\n\nexport const FETCH_TIMEOUT_MS = 10_000;\n\nexport const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\n\n// HACK: Windows CreateProcessW limits total command-line length to 32,767 chars.\n// Use a conservative threshold to leave room for the executable path and quoting overhead.\nexport const SPAWN_ARGS_MAX_LENGTH_CHARS = 24_000;\n\n// HACK: oxlint can SIGABRT on very large file sets due to memory pressure.\n// Cap each batch to avoid OOM crashes on projects with 100+ source files.\nexport const OXLINT_MAX_FILES_PER_BATCH = 500;\n\nexport const OFFLINE_MESSAGE = \"Score calculated locally (offline mode).\";\n\nexport const DEFAULT_BRANCH_CANDIDATES = [\"main\", \"master\"];\n\nexport const ERROR_RULE_PENALTY = 1.5;\n\nexport const WARNING_RULE_PENALTY = 0.75;\n\nexport const MAX_KNIP_RETRIES = 5;\n\nexport const KNIP_CONFIG_LOCATIONS = [\n \"knip.json\",\n \"knip.jsonc\",\n \".knip.json\",\n \".knip.jsonc\",\n \"knip.ts\",\n \"knip.js\",\n \"knip.config.ts\",\n \"knip.config.js\",\n];\n\nexport const OXLINT_NODE_REQUIREMENT = \"^20.19.0 || >=22.12.0\";\n\nexport const OXLINT_RECOMMENDED_NODE_MAJOR = 24;\n\nexport const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;\n\nexport const IGNORED_DIRECTORIES = new Set([\"node_modules\", \"dist\", \"build\", \"coverage\"]);\n","import {\n ERROR_RULE_PENALTY,\n PERFECT_SCORE,\n SCORE_GOOD_THRESHOLD,\n SCORE_OK_THRESHOLD,\n WARNING_RULE_PENALTY,\n} from \"../constants.js\";\nimport type { Diagnostic, ScoreResult } from \"../types.js\";\n\nconst getScoreLabel = (score: number): string => {\n if (score >= SCORE_GOOD_THRESHOLD) return \"Great\";\n if (score >= SCORE_OK_THRESHOLD) return \"Needs work\";\n return \"Critical\";\n};\n\nconst countUniqueRules = (\n diagnostics: Diagnostic[],\n): { errorRuleCount: number; warningRuleCount: number } => {\n const errorRules = new Set<string>();\n const warningRules = new Set<string>();\n\n for (const diagnostic of diagnostics) {\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\n if (diagnostic.severity === \"error\") {\n errorRules.add(ruleKey);\n } else {\n warningRules.add(ruleKey);\n }\n }\n\n return { errorRuleCount: errorRules.size, warningRuleCount: warningRules.size };\n};\n\nconst scoreFromRuleCounts = (errorRuleCount: number, warningRuleCount: number): number => {\n const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;\n return Math.max(0, Math.round(PERFECT_SCORE - penalty));\n};\n\nexport const calculateScoreLocally = (diagnostics: Diagnostic[]): ScoreResult => {\n const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);\n const score = scoreFromRuleCounts(errorRuleCount, warningRuleCount);\n return { score, label: getScoreLabel(score) };\n};\n","import { FETCH_TIMEOUT_MS, SCORE_API_URL } from \"../constants.js\";\nimport type { Diagnostic, ScoreResult } from \"../types.js\";\n\ninterface ScoreRequestFetch {\n (input: string | URL, init?: RequestInit): Promise<Response>;\n}\n\nconst parseScoreResult = (value: unknown): ScoreResult | null => {\n if (typeof value !== \"object\" || value === null) return null;\n if (!(\"score\" in value) || !(\"label\" in value)) return null;\n const scoreValue = Reflect.get(value, \"score\");\n const labelValue = Reflect.get(value, \"label\");\n if (typeof scoreValue !== \"number\" || typeof labelValue !== \"string\") return null;\n return { score: scoreValue, label: labelValue };\n};\n\nexport const tryScoreFromApi = async (\n diagnostics: Diagnostic[],\n fetchImplementation: ScoreRequestFetch,\n): Promise<ScoreResult | null> => {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n\n try {\n const response = await fetchImplementation(SCORE_API_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ diagnostics }),\n signal: controller.signal,\n });\n\n if (!response.ok) return null;\n\n return parseScoreResult(await response.json());\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n};\n","import type { Diagnostic, ScoreResult } from \"../types.js\";\nimport { calculateScoreLocally } from \"../core/calculate-score-locally.js\";\nimport { tryScoreFromApi } from \"../core/try-score-from-api.js\";\n\nexport { calculateScoreLocally } from \"../core/calculate-score-locally.js\";\n\nexport const calculateScore = async (diagnostics: Diagnostic[]): Promise<ScoreResult | null> =>\n (await tryScoreFromApi(diagnostics, fetch)) ?? calculateScoreLocally(diagnostics);\n","const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\\]\\\\]/g;\n\nexport const compileGlobPattern = (pattern: string): RegExp => {\n const normalizedPattern = pattern.replace(/\\\\/g, \"/\").replace(/^\\//, \"\");\n\n let regexSource = \"^\";\n let characterIndex = 0;\n\n while (characterIndex < normalizedPattern.length) {\n if (\n normalizedPattern[characterIndex] === \"*\" &&\n normalizedPattern[characterIndex + 1] === \"*\"\n ) {\n if (normalizedPattern[characterIndex + 2] === \"/\") {\n regexSource += \"(?:.+/)?\";\n characterIndex += 3;\n } else {\n regexSource += \".*\";\n characterIndex += 2;\n }\n } else if (normalizedPattern[characterIndex] === \"*\") {\n regexSource += \"[^/]*\";\n characterIndex++;\n } else if (normalizedPattern[characterIndex] === \"?\") {\n regexSource += \"[^/]\";\n characterIndex++;\n } else {\n regexSource += normalizedPattern[characterIndex].replace(REGEX_SPECIAL_CHARACTERS, \"\\\\$&\");\n characterIndex++;\n }\n }\n\n regexSource += \"$\";\n return new RegExp(regexSource);\n};\n\nexport const matchGlobPattern = (filePath: string, pattern: string): boolean => {\n const normalizedPath = filePath.replace(/\\\\/g, \"/\");\n return compileGlobPattern(pattern).test(normalizedPath);\n};\n","import type { ReactDoctorConfig } from \"../types.js\";\nimport { compileGlobPattern } from \"./match-glob-pattern.js\";\n\nconst toRelativePath = (filePath: string, rootDirectory: string): string => {\n const normalizedFilePath = filePath.replace(/\\\\/g, \"/\");\n const normalizedRoot = rootDirectory.replace(/\\\\/g, \"/\").replace(/\\/$/, \"\") + \"/\";\n\n if (normalizedFilePath.startsWith(normalizedRoot)) {\n return normalizedFilePath.slice(normalizedRoot.length);\n }\n\n return normalizedFilePath.replace(/^\\.\\//, \"\");\n};\n\nexport const compileIgnoredFilePatterns = (userConfig: ReactDoctorConfig | null): RegExp[] =>\n Array.isArray(userConfig?.ignore?.files) ? userConfig.ignore.files.map(compileGlobPattern) : [];\n\nexport const isFileIgnoredByPatterns = (\n filePath: string,\n rootDirectory: string,\n patterns: RegExp[],\n): boolean => {\n if (patterns.length === 0) {\n return false;\n }\n\n const relativePath = toRelativePath(filePath, rootDirectory);\n return patterns.some((pattern) => pattern.test(relativePath));\n};\n","import type { Diagnostic, ReactDoctorConfig } from \"../types.js\";\nimport { compileIgnoredFilePatterns, isFileIgnoredByPatterns } from \"./is-ignored-file.js\";\n\nconst resolveCandidateReadPath = (rootDirectory: string, filePath: string): string => {\n const normalizedFile = filePath.replace(/\\\\/g, \"/\");\n if (\n normalizedFile.startsWith(\"/\") ||\n /^[a-zA-Z]:\\//.test(normalizedFile) ||\n /^[a-zA-Z]:\\\\/.test(filePath)\n ) {\n return filePath;\n }\n const root = rootDirectory.replace(/\\\\/g, \"/\").replace(/\\/$/, \"\");\n return `${root}/${normalizedFile.replace(/^\\.\\//, \"\")}`;\n};\n\nconst OPENING_TAG_PATTERN = /<([A-Z][\\w.]*)/;\nconst DISABLE_NEXT_LINE_PATTERN = /\\/\\/\\s*react-doctor-disable-next-line\\b(?:\\s+(.+))?/;\nconst DISABLE_LINE_PATTERN = /\\/\\/\\s*react-doctor-disable-line\\b(?:\\s+(.+))?/;\n\nconst createFileLinesCache = (\n rootDirectory: string,\n readFileLinesSync: (filePath: string) => string[] | null,\n) => {\n const cache = new Map<string, string[] | null>();\n\n return (filePath: string): string[] | null => {\n const cached = cache.get(filePath);\n if (cached !== undefined) return cached;\n const absolutePath = resolveCandidateReadPath(rootDirectory, filePath);\n const lines = readFileLinesSync(absolutePath);\n cache.set(filePath, lines);\n return lines;\n };\n};\n\nconst isInsideTextComponent = (\n lines: string[],\n diagnosticLine: number,\n textComponentNames: Set<string>,\n): boolean => {\n for (let lineIndex = diagnosticLine - 1; lineIndex >= 0; lineIndex--) {\n const match = lines[lineIndex].match(OPENING_TAG_PATTERN);\n if (!match) continue;\n const fullTagName = match[1];\n const leafTagName = fullTagName.includes(\".\")\n ? (fullTagName.split(\".\").at(-1) ?? fullTagName)\n : fullTagName;\n return textComponentNames.has(fullTagName) || textComponentNames.has(leafTagName);\n }\n return false;\n};\n\nconst isRuleSuppressed = (commentRules: string | undefined, ruleId: string): boolean => {\n if (!commentRules?.trim()) return true;\n return commentRules.split(/[,\\s]+/).some((rule) => rule.trim() === ruleId);\n};\n\nexport const filterIgnoredDiagnostics = (\n diagnostics: Diagnostic[],\n config: ReactDoctorConfig,\n rootDirectory: string,\n readFileLinesSync: (filePath: string) => string[] | null,\n): Diagnostic[] => {\n const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);\n const ignoredFilePatterns = compileIgnoredFilePatterns(config);\n const textComponentNames = new Set(\n Array.isArray(config.textComponents) ? config.textComponents : [],\n );\n const hasTextComponents = textComponentNames.size > 0;\n const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);\n\n return diagnostics.filter((diagnostic) => {\n const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;\n if (ignoredRules.has(ruleIdentifier)) {\n return false;\n }\n\n if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) {\n return false;\n }\n\n if (hasTextComponents && diagnostic.rule === \"rn-no-raw-text\" && diagnostic.line > 0) {\n const lines = getFileLines(diagnostic.filePath);\n if (lines && isInsideTextComponent(lines, diagnostic.line, textComponentNames)) {\n return false;\n }\n }\n\n return true;\n });\n};\n\nexport const filterInlineSuppressions = (\n diagnostics: Diagnostic[],\n rootDirectory: string,\n readFileLinesSync: (filePath: string) => string[] | null,\n): Diagnostic[] => {\n const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);\n\n return diagnostics.filter((diagnostic) => {\n if (diagnostic.line <= 0) return true;\n\n const lines = getFileLines(diagnostic.filePath);\n if (!lines) return true;\n\n const ruleId = `${diagnostic.plugin}/${diagnostic.rule}`;\n\n const currentLine = lines[diagnostic.line - 1];\n if (currentLine) {\n const lineMatch = currentLine.match(DISABLE_LINE_PATTERN);\n if (lineMatch && isRuleSuppressed(lineMatch[1], ruleId)) return false;\n }\n\n if (diagnostic.line >= 2) {\n const previousLine = lines[diagnostic.line - 2];\n if (previousLine) {\n const nextLineMatch = previousLine.match(DISABLE_NEXT_LINE_PATTERN);\n if (nextLineMatch && isRuleSuppressed(nextLineMatch[1], ruleId)) return false;\n }\n }\n\n return true;\n });\n};\n","import type { Diagnostic, ReactDoctorConfig } from \"../types.js\";\nimport { filterIgnoredDiagnostics, filterInlineSuppressions } from \"./filter-diagnostics.js\";\n\nexport const mergeAndFilterDiagnostics = (\n mergedDiagnostics: Diagnostic[],\n directory: string,\n userConfig: ReactDoctorConfig | null,\n readFileLinesSync: (filePath: string) => string[] | null,\n): Diagnostic[] => {\n const filtered = userConfig\n ? filterIgnoredDiagnostics(mergedDiagnostics, userConfig, directory, readFileLinesSync)\n : mergedDiagnostics;\n return filterInlineSuppressions(filtered, directory, readFileLinesSync);\n};\n","import type { Diagnostic, ReactDoctorConfig, ScoreResult } from \"../types.js\";\nimport { mergeAndFilterDiagnostics } from \"../utils/merge-and-filter-diagnostics.js\";\n\nexport interface BuildDiagnoseResultInput {\n mergedDiagnostics: Diagnostic[];\n rootDirectory: string;\n userConfig: ReactDoctorConfig | null;\n readFileLinesSync: (filePath: string) => string[] | null;\n startTime: number;\n score?: ScoreResult | null;\n calculateDiagnosticsScore: (diagnostics: Diagnostic[]) => Promise<ScoreResult | null>;\n}\n\nexport interface BuildDiagnoseTimedResult {\n diagnostics: Diagnostic[];\n score: ScoreResult | null;\n elapsedMilliseconds: number;\n}\n\nexport const buildDiagnoseTimedResult = async (\n input: BuildDiagnoseResultInput,\n): Promise<BuildDiagnoseTimedResult> => {\n const diagnostics = mergeAndFilterDiagnostics(\n input.mergedDiagnostics,\n input.rootDirectory,\n input.userConfig,\n input.readFileLinesSync,\n );\n const elapsedMilliseconds = globalThis.performance.now() - input.startTime;\n const score =\n input.score !== undefined ? input.score : await input.calculateDiagnosticsScore(diagnostics);\n return { diagnostics, score, elapsedMilliseconds };\n};\n","const normalizeKey = (rootDirectory: string, filePath: string): string => {\n const normalizedRoot = rootDirectory.replace(/\\\\/g, \"/\").replace(/\\/$/, \"\");\n const normalizedPath = filePath.replace(/\\\\/g, \"/\");\n if (normalizedPath.startsWith(normalizedRoot + \"/\")) {\n return normalizedPath.slice(normalizedRoot.length + 1);\n }\n return normalizedPath.replace(/^\\.\\//, \"\");\n};\n\nexport const createBrowserReadFileLinesSync = (\n rootDirectory: string,\n projectFiles: Record<string, string>,\n): ((absoluteOrRelativePath: string) => string[] | null) => {\n return (absoluteOrRelativePath: string): string[] | null => {\n const key = normalizeKey(rootDirectory, absoluteOrRelativePath);\n const content = projectFiles[key];\n if (content === undefined) return null;\n return content.split(\"\\n\");\n };\n};\n","import type { Diagnostic, ProjectInfo, ReactDoctorConfig, ScoreResult } from \"../../types.js\";\nimport { buildDiagnoseTimedResult } from \"../../core/build-result.js\";\nimport { calculateScore as calculateScoreBrowser } from \"../../utils/calculate-score-browser.js\";\nimport { createBrowserReadFileLinesSync } from \"./create-browser-read-file-lines.js\";\n\nexport interface BrowserDiagnoseInput {\n rootDirectory: string;\n project: ProjectInfo;\n projectFiles: Record<string, string>;\n lintDiagnostics: Diagnostic[];\n deadCodeDiagnostics?: Diagnostic[];\n userConfig?: ReactDoctorConfig | null;\n score?: ScoreResult | null;\n}\n\nexport interface BrowserDiagnoseResult {\n diagnostics: Diagnostic[];\n score: ScoreResult | null;\n project: ProjectInfo;\n elapsedMilliseconds: number;\n}\n\nexport const diagnose = async (input: BrowserDiagnoseInput): Promise<BrowserDiagnoseResult> => {\n if (!input.project.reactVersion) {\n throw new Error(\"No React dependency found in package.json\");\n }\n\n const readFileLinesSync = createBrowserReadFileLinesSync(input.rootDirectory, input.projectFiles);\n const userConfig = input.userConfig ?? null;\n const deadCodeDiagnostics = input.deadCodeDiagnostics ?? [];\n const mergedDiagnostics = [...input.lintDiagnostics, ...deadCodeDiagnostics];\n const startTime = globalThis.performance.now();\n\n const timed = await buildDiagnoseTimedResult({\n mergedDiagnostics,\n rootDirectory: input.rootDirectory,\n userConfig,\n readFileLinesSync,\n startTime,\n score: input.score,\n calculateDiagnosticsScore: calculateScoreBrowser,\n });\n\n return {\n diagnostics: timed.diagnostics,\n score: timed.score,\n project: input.project,\n elapsedMilliseconds: timed.elapsedMilliseconds,\n };\n};\n","import type { Diagnostic, ProjectInfo, ScoreResult } from \"../types.js\";\n\ninterface BuildDiagnoseResultParams {\n diagnostics: Diagnostic[];\n project: ProjectInfo;\n elapsedMilliseconds: number;\n score: ScoreResult | null;\n}\n\ninterface DiagnoseResultShape {\n diagnostics: Diagnostic[];\n score: ScoreResult | null;\n project: ProjectInfo;\n elapsedMilliseconds: number;\n}\n\nexport const buildDiagnoseResult = (params: BuildDiagnoseResultParams): DiagnoseResultShape => ({\n diagnostics: params.diagnostics,\n score: params.score,\n project: params.project,\n elapsedMilliseconds: params.elapsedMilliseconds,\n});\n","import { JSX_FILE_PATTERN } from \"../constants.js\";\n\nexport const computeJsxIncludePaths = (includePaths: string[]): string[] | undefined =>\n includePaths.length > 0\n ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath))\n : undefined;\n","import type { Diagnostic, ProjectInfo, ReactDoctorConfig, ScoreResult } from \"../types.js\";\nimport { buildDiagnoseResult } from \"./build-diagnose-result.js\";\nimport { buildDiagnoseTimedResult } from \"./build-result.js\";\nimport { computeJsxIncludePaths } from \"../utils/jsx-include-paths.js\";\n\nexport interface DiagnoseCoreOptions {\n lint?: boolean;\n deadCode?: boolean;\n includePaths?: string[];\n lintIncludePaths?: string[] | undefined;\n}\n\nexport interface DiagnoseCoreResult {\n diagnostics: Diagnostic[];\n score: ScoreResult | null;\n project: ProjectInfo;\n elapsedMilliseconds: number;\n}\n\nexport interface DiagnoseRunnerContext {\n resolvedDirectory: string;\n projectInfo: ProjectInfo;\n userConfig: ReactDoctorConfig | null;\n lintIncludePaths: string[] | undefined;\n isDiffMode: boolean;\n}\n\nexport interface DiagnoseCoreDeps {\n rootDirectory: string;\n readFileLinesSync: (filePath: string) => string[] | null;\n loadUserConfig: () => ReactDoctorConfig | null;\n discoverProjectInfo: () => ProjectInfo;\n calculateDiagnosticsScore: (diagnostics: Diagnostic[]) => Promise<ScoreResult | null>;\n getExtraDiagnostics?: () => Diagnostic[];\n createRunners: (context: DiagnoseRunnerContext) => {\n runLint: () => Promise<Diagnostic[]>;\n runDeadCode: () => Promise<Diagnostic[]>;\n };\n}\n\nexport const diagnoseCore = async (\n deps: DiagnoseCoreDeps,\n options: DiagnoseCoreOptions = {},\n): Promise<DiagnoseCoreResult> => {\n const { includePaths = [] } = options;\n const isDiffMode = includePaths.length > 0;\n\n const startTime = globalThis.performance.now();\n const resolvedDirectory = deps.rootDirectory;\n const projectInfo = deps.discoverProjectInfo();\n const userConfig = deps.loadUserConfig();\n\n const effectiveLint = options.lint ?? userConfig?.lint ?? true;\n const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;\n\n if (!projectInfo.reactVersion) {\n throw new Error(\"No React dependency found in package.json\");\n }\n\n const lintIncludePaths =\n options.lintIncludePaths !== undefined\n ? options.lintIncludePaths\n : computeJsxIncludePaths(includePaths);\n\n const { runLint, runDeadCode } = deps.createRunners({\n resolvedDirectory,\n projectInfo,\n userConfig,\n lintIncludePaths,\n isDiffMode,\n });\n\n const emptyDiagnostics: Diagnostic[] = [];\n\n const lintPromise = effectiveLint\n ? runLint().catch((error: unknown) => {\n console.error(\"Lint failed:\", error);\n return emptyDiagnostics;\n })\n : Promise.resolve(emptyDiagnostics);\n\n const deadCodePromise =\n effectiveDeadCode && !isDiffMode\n ? runDeadCode().catch((error: unknown) => {\n console.error(\"Dead code analysis failed:\", error);\n return emptyDiagnostics;\n })\n : Promise.resolve(emptyDiagnostics);\n\n const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);\n const environmentDiagnostics = deps.getExtraDiagnostics?.() ?? [];\n const mergedDiagnostics = [...lintDiagnostics, ...deadCodeDiagnostics, ...environmentDiagnostics];\n const timed = await buildDiagnoseTimedResult({\n mergedDiagnostics,\n rootDirectory: resolvedDirectory,\n userConfig,\n readFileLinesSync: deps.readFileLinesSync,\n startTime,\n calculateDiagnosticsScore: deps.calculateDiagnosticsScore,\n });\n\n return buildDiagnoseResult({\n diagnostics: timed.diagnostics,\n score: timed.score,\n project: projectInfo,\n elapsedMilliseconds: timed.elapsedMilliseconds,\n });\n};\n","import type { Diagnostic, ProjectInfo, ReactDoctorConfig } from \"../../types.js\";\nimport type { DiagnoseCoreOptions } from \"../../core/diagnose-core.js\";\nimport { diagnoseCore } from \"../../core/diagnose-core.js\";\nimport { calculateScore as calculateScoreBrowser } from \"../../utils/calculate-score-browser.js\";\nimport { createBrowserReadFileLinesSync } from \"./create-browser-read-file-lines.js\";\n\nexport interface DiagnoseBrowserInput {\n rootDirectory: string;\n project: ProjectInfo;\n projectFiles: Record<string, string>;\n userConfig?: ReactDoctorConfig | null;\n runOxlint: (input: {\n lintIncludePaths: string[] | undefined;\n customRulesOnly: boolean;\n }) => Promise<Diagnostic[]>;\n}\n\nexport const diagnoseBrowser = async (\n input: DiagnoseBrowserInput,\n options: DiagnoseCoreOptions = {},\n) => {\n const readFileLinesSync = createBrowserReadFileLinesSync(input.rootDirectory, input.projectFiles);\n\n return diagnoseCore(\n {\n rootDirectory: input.rootDirectory,\n readFileLinesSync,\n loadUserConfig: () => input.userConfig ?? null,\n discoverProjectInfo: () => input.project,\n calculateDiagnosticsScore: calculateScoreBrowser,\n createRunners: ({ lintIncludePaths, userConfig }) => ({\n runLint: () =>\n input.runOxlint({\n lintIncludePaths,\n customRulesOnly: userConfig?.customRulesOnly ?? false,\n }),\n runDeadCode: async () => [],\n }),\n },\n options,\n );\n};\n","import type { Diagnostic, ReactDoctorConfig, ScoreResult } from \"../../types.js\";\nimport { buildDiagnoseTimedResult } from \"../../core/build-result.js\";\nimport { calculateScore as calculateScoreBrowser } from \"../../utils/calculate-score-browser.js\";\nimport { createBrowserReadFileLinesSync } from \"./create-browser-read-file-lines.js\";\n\nexport interface ProcessBrowserDiagnosticsInput {\n rootDirectory: string;\n projectFiles: Record<string, string>;\n diagnostics: Diagnostic[];\n userConfig?: ReactDoctorConfig | null;\n score?: ScoreResult | null;\n}\n\nexport interface ProcessBrowserDiagnosticsResult {\n diagnostics: Diagnostic[];\n score: ScoreResult | null;\n}\n\nexport const processBrowserDiagnostics = async (\n input: ProcessBrowserDiagnosticsInput,\n): Promise<ProcessBrowserDiagnosticsResult> => {\n const readFileLinesSync = createBrowserReadFileLinesSync(input.rootDirectory, input.projectFiles);\n const userConfig = input.userConfig ?? null;\n const timed = await buildDiagnoseTimedResult({\n mergedDiagnostics: input.diagnostics,\n rootDirectory: input.rootDirectory,\n userConfig,\n readFileLinesSync,\n startTime: globalThis.performance.now(),\n score: input.score,\n calculateDiagnosticsScore: calculateScoreBrowser,\n });\n return { diagnostics: timed.diagnostics, score: timed.score };\n};\n"],"mappings":"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"}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
//#region src/plugin/constants.ts
|
|
2
|
-
const GIANT_COMPONENT_LINE_THRESHOLD = 300;
|
|
3
|
-
const CASCADING_SET_STATE_THRESHOLD = 3;
|
|
4
|
-
const RELATED_USE_STATE_THRESHOLD = 5;
|
|
5
|
-
const DEEP_NESTING_THRESHOLD = 3;
|
|
6
|
-
const DUPLICATE_STORAGE_READ_THRESHOLD = 2;
|
|
7
|
-
const SEQUENTIAL_AWAIT_THRESHOLD = 3;
|
|
8
|
-
const SECRET_MIN_LENGTH_CHARS = 8;
|
|
9
|
-
const AUTH_CHECK_LOOKAHEAD_STATEMENTS = 3;
|
|
10
2
|
const LAYOUT_PROPERTIES = new Set([
|
|
11
3
|
"width",
|
|
12
4
|
"height",
|
|
@@ -204,7 +196,6 @@ const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
|
|
|
204
196
|
];
|
|
205
197
|
const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
|
|
206
198
|
const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
|
|
207
|
-
const SEQUENTIAL_AWAIT_THRESHOLD_FOR_LOADER = 2;
|
|
208
199
|
const TANSTACK_QUERY_HOOKS = new Set([
|
|
209
200
|
"useQuery",
|
|
210
201
|
"useInfiniteQuery",
|
|
@@ -212,7 +203,6 @@ const TANSTACK_QUERY_HOOKS = new Set([
|
|
|
212
203
|
"useSuspenseInfiniteQuery"
|
|
213
204
|
]);
|
|
214
205
|
const TANSTACK_MUTATION_HOOKS = new Set(["useMutation"]);
|
|
215
|
-
const TANSTACK_QUERY_CLIENT_CLASS = "QueryClient";
|
|
216
206
|
const STABLE_HOOK_WRAPPERS = new Set([
|
|
217
207
|
"useState",
|
|
218
208
|
"useMemo",
|
|
@@ -297,10 +287,8 @@ const CHAINABLE_ITERATION_METHODS = new Set([
|
|
|
297
287
|
"flatMap"
|
|
298
288
|
]);
|
|
299
289
|
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
300
|
-
const LARGE_BLUR_THRESHOLD_PX = 10;
|
|
301
290
|
const BLUR_VALUE_PATTERN = /blur\((\d+(?:\.\d+)?)px\)/;
|
|
302
291
|
const ANIMATION_CALLBACK_NAMES = new Set(["requestAnimationFrame", "setInterval"]);
|
|
303
|
-
const RAW_TEXT_PREVIEW_MAX_CHARS = 30;
|
|
304
292
|
const REACT_NATIVE_TEXT_COMPONENTS = new Set([
|
|
305
293
|
"Text",
|
|
306
294
|
"TextInput",
|
|
@@ -369,18 +357,7 @@ const BOUNCE_ANIMATION_NAMES = new Set([
|
|
|
369
357
|
"jiggle",
|
|
370
358
|
"spring"
|
|
371
359
|
]);
|
|
372
|
-
const Z_INDEX_ABSURD_THRESHOLD = 100;
|
|
373
|
-
const INLINE_STYLE_PROPERTY_THRESHOLD = 8;
|
|
374
|
-
const SIDE_TAB_BORDER_WIDTH_WITHOUT_RADIUS_PX = 3;
|
|
375
|
-
const SIDE_TAB_BORDER_WIDTH_WITH_RADIUS_PX = 1;
|
|
376
|
-
const SIDE_TAB_TAILWIND_WIDTH_WITHOUT_RADIUS = 4;
|
|
377
|
-
const DARK_GLOW_BLUR_THRESHOLD_PX = 4;
|
|
378
|
-
const DARK_BACKGROUND_CHANNEL_MAX = 35;
|
|
379
|
-
const COLOR_CHROMA_THRESHOLD = 30;
|
|
380
|
-
const TINY_TEXT_THRESHOLD_PX = 12;
|
|
381
|
-
const WIDE_TRACKING_THRESHOLD_EM = .05;
|
|
382
360
|
const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
|
|
383
|
-
|
|
384
361
|
//#endregion
|
|
385
362
|
//#region src/plugin/helpers.ts
|
|
386
363
|
const walkAst = (node, visitor) => {
|
|
@@ -516,14 +493,13 @@ const extractDestructuredPropNames = (params) => {
|
|
|
516
493
|
} else if (param.type === "Identifier") propNames.add(param.name);
|
|
517
494
|
return propNames;
|
|
518
495
|
};
|
|
519
|
-
|
|
520
496
|
//#endregion
|
|
521
497
|
//#region src/plugin/rules/architecture.ts
|
|
522
498
|
const noGiantComponent = { create: (context) => {
|
|
523
499
|
const reportOversizedComponent = (nameNode, componentName, bodyNode) => {
|
|
524
500
|
if (!bodyNode.loc) return;
|
|
525
501
|
const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
|
|
526
|
-
if (lineCount >
|
|
502
|
+
if (lineCount > 300) context.report({
|
|
527
503
|
node: nameNode,
|
|
528
504
|
message: `Component "${componentName}" is ${lineCount} lines — consider breaking it into smaller focused components`
|
|
529
505
|
});
|
|
@@ -577,7 +553,6 @@ const noNestedComponentDefinition = { create: (context) => {
|
|
|
577
553
|
}
|
|
578
554
|
};
|
|
579
555
|
} };
|
|
580
|
-
|
|
581
556
|
//#endregion
|
|
582
557
|
//#region src/plugin/rules/bundle-size.ts
|
|
583
558
|
const noBarrelImport = { create: (context) => {
|
|
@@ -632,7 +607,6 @@ const noUndeferredThirdParty = { create: (context) => ({ JSXOpeningElement(node)
|
|
|
632
607
|
message: "Synchronous <script> with src — add defer or async to avoid blocking first paint"
|
|
633
608
|
});
|
|
634
609
|
} }) };
|
|
635
|
-
|
|
636
610
|
//#endregion
|
|
637
611
|
//#region src/plugin/rules/client.ts
|
|
638
612
|
const clientPassiveEventListeners = { create: (context) => ({ CallExpression(node) {
|
|
@@ -655,7 +629,6 @@ const clientPassiveEventListeners = { create: (context) => ({ CallExpression(nod
|
|
|
655
629
|
message: `"${eventName}" listener without { passive: true } — blocks scrolling performance`
|
|
656
630
|
});
|
|
657
631
|
} }) };
|
|
658
|
-
|
|
659
632
|
//#endregion
|
|
660
633
|
//#region src/plugin/rules/design.ts
|
|
661
634
|
const isOvershootCubicBezier = (value) => {
|
|
@@ -722,7 +695,7 @@ const parseColorToRgb = (value) => {
|
|
|
722
695
|
};
|
|
723
696
|
return null;
|
|
724
697
|
};
|
|
725
|
-
const hasColorChroma = (parsed) => Math.max(parsed.red, parsed.green, parsed.blue) - Math.min(parsed.red, parsed.green, parsed.blue) >=
|
|
698
|
+
const hasColorChroma = (parsed) => Math.max(parsed.red, parsed.green, parsed.blue) - Math.min(parsed.red, parsed.green, parsed.blue) >= 30;
|
|
726
699
|
const isNeutralBorderColor = (value) => {
|
|
727
700
|
const trimmed = value.trim().toLowerCase();
|
|
728
701
|
if ([
|
|
@@ -768,7 +741,7 @@ const parseShadowLayerBlur = (layer) => {
|
|
|
768
741
|
const hasColoredGlowShadow = (shadowValue) => {
|
|
769
742
|
for (const layer of splitShadowLayers(shadowValue)) {
|
|
770
743
|
const color = extractColorFromShadowLayer(layer);
|
|
771
|
-
if (color && hasColorChroma(color) && parseShadowLayerBlur(layer) >
|
|
744
|
+
if (color && hasColorChroma(color) && parseShadowLayerBlur(layer) > 4) return true;
|
|
772
745
|
}
|
|
773
746
|
return false;
|
|
774
747
|
};
|
|
@@ -777,7 +750,7 @@ const isBackgroundDark = (bgValue) => {
|
|
|
777
750
|
if (isPureBlackColor(trimmed)) return true;
|
|
778
751
|
const parsed = parseColorToRgb(trimmed);
|
|
779
752
|
if (!parsed) return false;
|
|
780
|
-
return parsed.red <=
|
|
753
|
+
return parsed.red <= 35 && parsed.green <= 35 && parsed.blue <= 35;
|
|
781
754
|
};
|
|
782
755
|
const BORDER_SIDE_KEYS = {
|
|
783
756
|
borderLeft: "left",
|
|
@@ -826,7 +799,7 @@ const noZIndex9999 = { create: (context) => ({
|
|
|
826
799
|
for (const property of expression.properties ?? []) {
|
|
827
800
|
if (getStylePropertyKey(property) !== "zIndex") continue;
|
|
828
801
|
const zValue = getStylePropertyNumberValue(property);
|
|
829
|
-
if (zValue !== null && Math.abs(zValue) >=
|
|
802
|
+
if (zValue !== null && Math.abs(zValue) >= 100) context.report({
|
|
830
803
|
node: property,
|
|
831
804
|
message: `z-index: ${zValue} is arbitrarily high — use a deliberate z-index scale (1–50). Extreme values signal a stacking context problem, not a fix`
|
|
832
805
|
});
|
|
@@ -843,7 +816,7 @@ const noZIndex9999 = { create: (context) => ({
|
|
|
843
816
|
if (getStylePropertyKey(child) !== "zIndex") return;
|
|
844
817
|
if (child.value?.type === "Literal" && typeof child.value.value === "number") {
|
|
845
818
|
const zValue = child.value.value;
|
|
846
|
-
if (Math.abs(zValue) >=
|
|
819
|
+
if (Math.abs(zValue) >= 100) context.report({
|
|
847
820
|
node: child,
|
|
848
821
|
message: `z-index: ${zValue} is arbitrarily high — use a deliberate z-index scale (1–50). Extreme values signal a stacking context problem, not a fix`
|
|
849
822
|
});
|
|
@@ -855,7 +828,7 @@ const noInlineExhaustiveStyle = { create: (context) => ({ JSXAttribute(node) {
|
|
|
855
828
|
const expression = getInlineStyleExpression(node);
|
|
856
829
|
if (!expression) return;
|
|
857
830
|
const propertyCount = expression.properties?.filter((property) => property.type === "Property").length ?? 0;
|
|
858
|
-
if (propertyCount >=
|
|
831
|
+
if (propertyCount >= 8) context.report({
|
|
859
832
|
node: expression,
|
|
860
833
|
message: `${propertyCount} inline style properties — extract to a CSS class, CSS module, or styled component for maintainability and reuse`
|
|
861
834
|
});
|
|
@@ -870,7 +843,7 @@ const noSideTabBorder = { create: (context) => ({
|
|
|
870
843
|
const strValue = getStylePropertyStringValue(property);
|
|
871
844
|
if (numValue !== null && numValue > 0 || strValue !== null && parseFloat(strValue) > 0) hasBorderRadius = true;
|
|
872
845
|
}
|
|
873
|
-
const threshold = hasBorderRadius ?
|
|
846
|
+
const threshold = hasBorderRadius ? 1 : 3;
|
|
874
847
|
for (const property of expression.properties ?? []) {
|
|
875
848
|
const key = getStylePropertyKey(property);
|
|
876
849
|
if (!key) continue;
|
|
@@ -911,7 +884,7 @@ const noSideTabBorder = { create: (context) => ({
|
|
|
911
884
|
const sideMatch = classStr.match(/\bborder-[lrse]-(\d+)\b/);
|
|
912
885
|
if (!sideMatch) return;
|
|
913
886
|
if (/\bborder-(?:(?:gray|slate|zinc|neutral|stone)-\d+|white|black|transparent)\b/.test(classStr)) return;
|
|
914
|
-
if (parseInt(sideMatch[1], 10) >= (/\brounded(?:-(?!none\b)\w+)?\b/.test(classStr) && !/\brounded-none\b/.test(classStr) ?
|
|
887
|
+
if (parseInt(sideMatch[1], 10) >= (/\brounded(?:-(?!none\b)\w+)?\b/.test(classStr) && !/\brounded-none\b/.test(classStr) ? 1 : 4)) context.report({
|
|
915
888
|
node,
|
|
916
889
|
message: `Thick one-sided border (${sideMatch[0]}) — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it`
|
|
917
890
|
});
|
|
@@ -1023,9 +996,9 @@ const noTinyText = { create: (context) => ({ JSXAttribute(node) {
|
|
|
1023
996
|
const remMatch = strValue.match(/^([\d.]+)rem$/);
|
|
1024
997
|
if (remMatch) pxValue = parseFloat(remMatch[1]) * 16;
|
|
1025
998
|
}
|
|
1026
|
-
if (pxValue !== null && pxValue > 0 && pxValue <
|
|
999
|
+
if (pxValue !== null && pxValue > 0 && pxValue < 12) context.report({
|
|
1027
1000
|
node: property,
|
|
1028
|
-
message: `Font size ${pxValue}px is too small — body text should be at least
|
|
1001
|
+
message: `Font size ${pxValue}px is too small — body text should be at least 12px for readability, 16px is ideal`
|
|
1029
1002
|
});
|
|
1030
1003
|
}
|
|
1031
1004
|
} }) };
|
|
@@ -1054,7 +1027,7 @@ const noWideLetterSpacing = { create: (context) => ({ JSXAttribute(node) {
|
|
|
1054
1027
|
if (numValue !== null && numValue > 0) letterSpacingEm = numValue / 16;
|
|
1055
1028
|
}
|
|
1056
1029
|
}
|
|
1057
|
-
if (!isUppercase && letterSpacingProperty && letterSpacingEm !== null && letterSpacingEm >
|
|
1030
|
+
if (!isUppercase && letterSpacingProperty && letterSpacingEm !== null && letterSpacingEm > .05) context.report({
|
|
1058
1031
|
node: letterSpacingProperty,
|
|
1059
1032
|
message: `Letter spacing ${letterSpacingEm.toFixed(2)}em on body text disrupts natural character groupings. Reserve wide tracking for short uppercase labels only`
|
|
1060
1033
|
});
|
|
@@ -1164,13 +1137,12 @@ const noLongTransitionDuration = { create: (context) => ({ JSXAttribute(node) {
|
|
|
1164
1137
|
}
|
|
1165
1138
|
if (longestDurationMs > 0) durationMs = longestDurationMs;
|
|
1166
1139
|
}
|
|
1167
|
-
if (durationMs !== null && durationMs >
|
|
1140
|
+
if (durationMs !== null && durationMs > 1e3) context.report({
|
|
1168
1141
|
node: property,
|
|
1169
1142
|
message: `${durationMs}ms transition is too slow for UI feedback — keep transitions under ${LONG_TRANSITION_DURATION_THRESHOLD_MS}ms. Use longer durations only for page-load hero animations`
|
|
1170
1143
|
});
|
|
1171
1144
|
}
|
|
1172
1145
|
} }) };
|
|
1173
|
-
|
|
1174
1146
|
//#endregion
|
|
1175
1147
|
//#region src/plugin/rules/correctness.ts
|
|
1176
1148
|
const extractIndexName = (node) => {
|
|
@@ -1245,7 +1217,6 @@ const renderingConditionalRender = { create: (context) => ({ LogicalExpression(n
|
|
|
1245
1217
|
message: "Conditional rendering with .length can render '0' — use .length > 0 or Boolean(.length)"
|
|
1246
1218
|
});
|
|
1247
1219
|
} }) };
|
|
1248
|
-
|
|
1249
1220
|
//#endregion
|
|
1250
1221
|
//#region src/plugin/rules/js-performance.ts
|
|
1251
1222
|
const jsCombineIterations = { create: (context) => ({ CallExpression(node) {
|
|
@@ -1328,7 +1299,7 @@ const jsCacheStorage = { create: (context) => {
|
|
|
1328
1299
|
const storageKey = String(node.arguments[0].value);
|
|
1329
1300
|
const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
|
|
1330
1301
|
storageReadCounts.set(storageKey, readCount);
|
|
1331
|
-
if (readCount ===
|
|
1302
|
+
if (readCount === 2) {
|
|
1332
1303
|
const storageName = node.callee.object.name;
|
|
1333
1304
|
context.report({
|
|
1334
1305
|
node,
|
|
@@ -1347,7 +1318,7 @@ const jsEarlyExit = { create: (context) => ({ IfStatement(node) {
|
|
|
1347
1318
|
nestingDepth++;
|
|
1348
1319
|
currentBlock = innerStatement.consequent;
|
|
1349
1320
|
}
|
|
1350
|
-
if (nestingDepth >=
|
|
1321
|
+
if (nestingDepth >= 3) context.report({
|
|
1351
1322
|
node,
|
|
1352
1323
|
message: `${nestingDepth + 1} levels of nested if statements — use early returns to flatten`
|
|
1353
1324
|
});
|
|
@@ -1359,7 +1330,7 @@ const asyncParallel = { create: (context) => {
|
|
|
1359
1330
|
if (isTestFile) return;
|
|
1360
1331
|
const consecutiveAwaitStatements = [];
|
|
1361
1332
|
const flushConsecutiveAwaits = () => {
|
|
1362
|
-
if (consecutiveAwaitStatements.length >=
|
|
1333
|
+
if (consecutiveAwaitStatements.length >= 3) reportIfIndependent(consecutiveAwaitStatements, context);
|
|
1363
1334
|
consecutiveAwaitStatements.length = 0;
|
|
1364
1335
|
};
|
|
1365
1336
|
for (const statement of node.body ?? []) if (statement.type === "VariableDeclaration" && statement.declarations?.length === 1 && statement.declarations[0].init?.type === "AwaitExpression" || statement.type === "ExpressionStatement" && statement.expression?.type === "AwaitExpression") consecutiveAwaitStatements.push(statement);
|
|
@@ -1400,7 +1371,6 @@ const jsFlatmapFilter = { create: (context) => ({ CallExpression(node) {
|
|
|
1400
1371
|
message: ".map().filter(Boolean) iterates twice — use .flatMap() to transform and filter in a single pass"
|
|
1401
1372
|
});
|
|
1402
1373
|
} }) };
|
|
1403
|
-
|
|
1404
1374
|
//#endregion
|
|
1405
1375
|
//#region src/plugin/rules/nextjs.ts
|
|
1406
1376
|
const nextjsNoImgElement = { create: (context) => {
|
|
@@ -1654,7 +1624,6 @@ const nextjsNoSideEffectInGetHandler = { create: (context) => ({ ExportNamedDecl
|
|
|
1654
1624
|
message: `GET handler has side effects (${sideEffect}) — use POST to prevent CSRF and unintended prefetch triggers`
|
|
1655
1625
|
});
|
|
1656
1626
|
} }) };
|
|
1657
|
-
|
|
1658
1627
|
//#endregion
|
|
1659
1628
|
//#region src/plugin/rules/performance.ts
|
|
1660
1629
|
const isMemoCall = (node) => {
|
|
@@ -1782,7 +1751,7 @@ const noLargeAnimatedBlur = { create: (context) => ({ JSXAttribute(node) {
|
|
|
1782
1751
|
const match = BLUR_VALUE_PATTERN.exec(property.value.value);
|
|
1783
1752
|
if (!match) continue;
|
|
1784
1753
|
const blurRadius = Number.parseFloat(match[1]);
|
|
1785
|
-
if (blurRadius >
|
|
1754
|
+
if (blurRadius > 10) context.report({
|
|
1786
1755
|
node: property,
|
|
1787
1756
|
message: `blur(${blurRadius}px) is expensive — cost escalates with radius and layer size, can exceed GPU memory on mobile`
|
|
1788
1757
|
});
|
|
@@ -1880,7 +1849,6 @@ const renderingScriptDeferAsync = { create: (context) => ({ JSXOpeningElement(no
|
|
|
1880
1849
|
message: "<script src> without defer or async — blocks HTML parsing and delays First Contentful Paint. Add defer for DOM-dependent scripts or async for independent ones"
|
|
1881
1850
|
});
|
|
1882
1851
|
} }) };
|
|
1883
|
-
|
|
1884
1852
|
//#endregion
|
|
1885
1853
|
//#region src/plugin/rules/react-native.ts
|
|
1886
1854
|
const resolveJsxElementName = (openingElement) => {
|
|
@@ -1890,7 +1858,7 @@ const resolveJsxElementName = (openingElement) => {
|
|
|
1890
1858
|
if (elementName.type === "JSXMemberExpression") return elementName.property?.name ?? null;
|
|
1891
1859
|
return null;
|
|
1892
1860
|
};
|
|
1893
|
-
const truncateText = (text) => text.length >
|
|
1861
|
+
const truncateText = (text) => text.length > 30 ? `${text.slice(0, 30)}...` : text;
|
|
1894
1862
|
const isRawTextContent = (child) => {
|
|
1895
1863
|
if (child.type === "JSXText") return Boolean(child.value?.trim());
|
|
1896
1864
|
if (child.type !== "JSXExpressionContainer" || !child.expression) return false;
|
|
@@ -2043,7 +2011,6 @@ const rnNoSingleElementStyleArray = { create: (context) => ({ JSXAttribute(node)
|
|
|
2043
2011
|
message: `Single-element style array on "${propName}" — use ${propName}={value} instead of ${propName}={[value]} to avoid unnecessary array allocation`
|
|
2044
2012
|
});
|
|
2045
2013
|
} }) };
|
|
2046
|
-
|
|
2047
2014
|
//#endregion
|
|
2048
2015
|
//#region src/plugin/rules/tanstack-query.ts
|
|
2049
2016
|
const queryStableQueryClient = { create: (context) => {
|
|
@@ -2071,7 +2038,7 @@ const queryStableQueryClient = { create: (context) => {
|
|
|
2071
2038
|
NewExpression(node) {
|
|
2072
2039
|
if (componentDepth <= 0) return;
|
|
2073
2040
|
if (stableHookDepth > 0) return;
|
|
2074
|
-
if (node.callee?.type !== "Identifier" || node.callee.name !==
|
|
2041
|
+
if (node.callee?.type !== "Identifier" || node.callee.name !== "QueryClient") return;
|
|
2075
2042
|
context.report({
|
|
2076
2043
|
node,
|
|
2077
2044
|
message: "new QueryClient() inside a component — creates a new cache on every render. Move to module scope or wrap in useState(() => new QueryClient())"
|
|
@@ -2156,7 +2123,6 @@ const queryNoUseQueryForMutation = { create: (context) => ({ CallExpression(node
|
|
|
2156
2123
|
message: `${calleeName}() with a mutating fetch (POST/PUT/DELETE) — use useMutation() instead, which provides onSuccess/onError callbacks and doesn't auto-refetch`
|
|
2157
2124
|
});
|
|
2158
2125
|
} }) };
|
|
2159
|
-
|
|
2160
2126
|
//#endregion
|
|
2161
2127
|
//#region src/plugin/rules/security.ts
|
|
2162
2128
|
const noEval = { create: (context) => ({
|
|
@@ -2187,7 +2153,7 @@ const noSecretsInClientCode = { create: (context) => ({ VariableDeclarator(node)
|
|
|
2187
2153
|
const literalValue = node.init.value;
|
|
2188
2154
|
const trailingSuffix = variableName.split("_").pop()?.toLowerCase() ?? "";
|
|
2189
2155
|
const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
|
|
2190
|
-
if (SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && literalValue.length >
|
|
2156
|
+
if (SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && literalValue.length > 8) {
|
|
2191
2157
|
context.report({
|
|
2192
2158
|
node,
|
|
2193
2159
|
message: `Possible hardcoded secret in "${variableName}" — use environment variables instead`
|
|
@@ -2199,7 +2165,6 @@ const noSecretsInClientCode = { create: (context) => ({ VariableDeclarator(node)
|
|
|
2199
2165
|
message: "Hardcoded secret detected — use environment variables instead"
|
|
2200
2166
|
});
|
|
2201
2167
|
} }) };
|
|
2202
|
-
|
|
2203
2168
|
//#endregion
|
|
2204
2169
|
//#region src/plugin/rules/server.ts
|
|
2205
2170
|
const containsAuthCheck = (statements) => {
|
|
@@ -2223,7 +2188,7 @@ const serverAuthActions = { create: (context) => {
|
|
|
2223
2188
|
const declaration = node.declaration;
|
|
2224
2189
|
if (declaration?.type !== "FunctionDeclaration" || !declaration?.async) return;
|
|
2225
2190
|
if (!(fileHasUseServerDirective || hasUseServerDirective(declaration))) return;
|
|
2226
|
-
if (!containsAuthCheck((declaration.body?.body ?? []).slice(0,
|
|
2191
|
+
if (!containsAuthCheck((declaration.body?.body ?? []).slice(0, 3))) {
|
|
2227
2192
|
const functionName = declaration.id?.name ?? "anonymous";
|
|
2228
2193
|
context.report({
|
|
2229
2194
|
node: declaration.id ?? node,
|
|
@@ -2254,7 +2219,6 @@ const serverAfterNonblocking = { create: (context) => {
|
|
|
2254
2219
|
}
|
|
2255
2220
|
};
|
|
2256
2221
|
} };
|
|
2257
|
-
|
|
2258
2222
|
//#endregion
|
|
2259
2223
|
//#region src/plugin/rules/tanstack-start.ts
|
|
2260
2224
|
const getRouteOptionsObject = (node) => {
|
|
@@ -2573,7 +2537,7 @@ const tanstackStartLoaderParallelFetch = { create: (context) => ({ CallExpressio
|
|
|
2573
2537
|
let sequentialAwaitCount = 0;
|
|
2574
2538
|
for (const statement of functionBody.body ?? []) {
|
|
2575
2539
|
if (hasTopLevelAwait(statement)) sequentialAwaitCount++;
|
|
2576
|
-
if (sequentialAwaitCount >=
|
|
2540
|
+
if (sequentialAwaitCount >= 2) {
|
|
2577
2541
|
context.report({
|
|
2578
2542
|
node: property,
|
|
2579
2543
|
message: "Multiple sequential awaits in loader — use Promise.all() to fetch data in parallel and avoid waterfalls"
|
|
@@ -2583,7 +2547,6 @@ const tanstackStartLoaderParallelFetch = { create: (context) => ({ CallExpressio
|
|
|
2583
2547
|
}
|
|
2584
2548
|
}
|
|
2585
2549
|
} }) };
|
|
2586
|
-
|
|
2587
2550
|
//#endregion
|
|
2588
2551
|
//#region src/plugin/rules/state-and-effects.ts
|
|
2589
2552
|
const noDerivedStateEffect = { create: (context) => ({ CallExpression(node) {
|
|
@@ -2635,7 +2598,7 @@ const noCascadingSetState = { create: (context) => ({ CallExpression(node) {
|
|
|
2635
2598
|
const callback = getEffectCallback(node);
|
|
2636
2599
|
if (!callback) return;
|
|
2637
2600
|
const setStateCallCount = countSetStateCalls(callback);
|
|
2638
|
-
if (setStateCallCount >=
|
|
2601
|
+
if (setStateCallCount >= 3) context.report({
|
|
2639
2602
|
node,
|
|
2640
2603
|
message: `${setStateCallCount} setState calls in a single useEffect — consider using useReducer or deriving state`
|
|
2641
2604
|
});
|
|
@@ -2685,7 +2648,7 @@ const preferUseReducer = { create: (context) => {
|
|
|
2685
2648
|
if (statement.type !== "VariableDeclaration") continue;
|
|
2686
2649
|
for (const declarator of statement.declarations ?? []) if (isHookCall(declarator.init, "useState")) useStateCount++;
|
|
2687
2650
|
}
|
|
2688
|
-
if (useStateCount >=
|
|
2651
|
+
if (useStateCount >= 5) context.report({
|
|
2689
2652
|
node: body,
|
|
2690
2653
|
message: `Component "${componentName}" has ${useStateCount} useState calls — consider useReducer for related state`
|
|
2691
2654
|
});
|
|
@@ -2738,7 +2701,6 @@ const rerenderDependencies = { create: (context) => ({ CallExpression(node) {
|
|
|
2738
2701
|
});
|
|
2739
2702
|
}
|
|
2740
2703
|
} }) };
|
|
2741
|
-
|
|
2742
2704
|
//#endregion
|
|
2743
2705
|
//#region src/plugin/index.ts
|
|
2744
2706
|
const plugin = {
|
|
@@ -2854,7 +2816,7 @@ const plugin = {
|
|
|
2854
2816
|
"no-long-transition-duration": noLongTransitionDuration
|
|
2855
2817
|
}
|
|
2856
2818
|
};
|
|
2857
|
-
|
|
2858
2819
|
//#endregion
|
|
2859
2820
|
export { plugin as default };
|
|
2821
|
+
|
|
2860
2822
|
//# sourceMappingURL=react-doctor-plugin.js.map
|