react-doctor 0.0.26 → 0.0.28
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/cli.js +690 -348
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +211 -133
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,SAAA;AAAA,UAEK,WAAA;EACf,aAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA,EAAW,SAAA;EACX,aAAA;EACA,gBAAA;EACA,eAAA;AAAA;AAAA,UAiCe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UA4Be,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,SAAA;AAAA,UAEK,WAAA;EACf,aAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA,EAAW,SAAA;EACX,aAAA;EACA,gBAAA;EACA,eAAA;AAAA;AAAA,UAiCe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UA4Be,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAyBe,QAAA;EACf,aAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,UA2Ce,uBAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EACf,MAAA,GAAS,uBAAA;EACT,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;AAAA;;;cCrFW,WAAA,GAAe,SAAA,UAAmB,kBAAA,cAA8B,QAAA;AAAA,cAiBhE,iBAAA,GAAqB,SAAA;;;UCnFjB,eAAA;EACf,IAAA;EACA,QAAA;EACA,YAAA;AAAA;AAAA,UAGe,cAAA;EACf,WAAA,EAAa,UAAA;EACb,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,WAAA;EACT,mBAAA;AAAA;AAAA,cAGW,QAAA,GACX,SAAA,UACA,OAAA,GAAS,eAAA,KACR,OAAA,CAAQ,cAAA"}
|
package/dist/index.js
CHANGED
|
@@ -13,14 +13,67 @@ const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
|
|
|
13
13
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
14
14
|
const ERROR_PREVIEW_LENGTH_CHARS = 200;
|
|
15
15
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
16
|
+
const FETCH_TIMEOUT_MS = 1e4;
|
|
16
17
|
const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
18
|
+
const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
|
|
17
19
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
20
|
+
const MAX_KNIP_RETRIES = 5;
|
|
21
|
+
const AMI_WEBSITE_URL = "https://ami.dev";
|
|
22
|
+
const AMI_INSTALL_URL = `${AMI_WEBSITE_URL}/install.sh`;
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/utils/proxy-fetch.ts
|
|
26
|
+
const readNpmConfigValue = (key) => {
|
|
27
|
+
try {
|
|
28
|
+
const value = execSync(`npm config get ${key}`, {
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
stdio: [
|
|
31
|
+
"pipe",
|
|
32
|
+
"pipe",
|
|
33
|
+
"ignore"
|
|
34
|
+
]
|
|
35
|
+
}).trim();
|
|
36
|
+
if (value && value !== "null" && value !== "undefined") return value;
|
|
37
|
+
} catch {}
|
|
38
|
+
};
|
|
39
|
+
const resolveProxyUrl = () => process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy ?? readNpmConfigValue("https-proxy") ?? readNpmConfigValue("proxy");
|
|
40
|
+
let isProxyUrlResolved = false;
|
|
41
|
+
let resolvedProxyUrl;
|
|
42
|
+
const getProxyUrl = () => {
|
|
43
|
+
if (isProxyUrlResolved) return resolvedProxyUrl;
|
|
44
|
+
isProxyUrlResolved = true;
|
|
45
|
+
resolvedProxyUrl = resolveProxyUrl();
|
|
46
|
+
return resolvedProxyUrl;
|
|
47
|
+
};
|
|
48
|
+
const createProxyDispatcher = async (proxyUrl) => {
|
|
49
|
+
try {
|
|
50
|
+
const { ProxyAgent } = await import("undici");
|
|
51
|
+
return new ProxyAgent(proxyUrl);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const proxyFetch = async (url, init) => {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
59
|
+
try {
|
|
60
|
+
const proxyUrl = getProxyUrl();
|
|
61
|
+
const dispatcher = proxyUrl ? await createProxyDispatcher(proxyUrl) : null;
|
|
62
|
+
return await fetch(url, {
|
|
63
|
+
...init,
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
...dispatcher ? { dispatcher } : {}
|
|
66
|
+
});
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
18
71
|
|
|
19
72
|
//#endregion
|
|
20
73
|
//#region src/utils/calculate-score.ts
|
|
21
74
|
const calculateScore = async (diagnostics) => {
|
|
22
75
|
try {
|
|
23
|
-
const response = await
|
|
76
|
+
const response = await proxyFetch(SCORE_API_URL, {
|
|
24
77
|
method: "POST",
|
|
25
78
|
headers: { "Content-Type": "application/json" },
|
|
26
79
|
body: JSON.stringify({ diagnostics })
|
|
@@ -82,6 +135,79 @@ const checkReducedMotion = (rootDirectory) => {
|
|
|
82
135
|
}
|
|
83
136
|
};
|
|
84
137
|
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/utils/match-glob-pattern.ts
|
|
140
|
+
const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
|
|
141
|
+
const compileGlobPattern = (pattern) => {
|
|
142
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
143
|
+
let regexSource = "^";
|
|
144
|
+
let characterIndex = 0;
|
|
145
|
+
while (characterIndex < normalizedPattern.length) if (normalizedPattern[characterIndex] === "*" && normalizedPattern[characterIndex + 1] === "*") if (normalizedPattern[characterIndex + 2] === "/") {
|
|
146
|
+
regexSource += "(?:.+/)?";
|
|
147
|
+
characterIndex += 3;
|
|
148
|
+
} else {
|
|
149
|
+
regexSource += ".*";
|
|
150
|
+
characterIndex += 2;
|
|
151
|
+
}
|
|
152
|
+
else if (normalizedPattern[characterIndex] === "*") {
|
|
153
|
+
regexSource += "[^/]*";
|
|
154
|
+
characterIndex++;
|
|
155
|
+
} else if (normalizedPattern[characterIndex] === "?") {
|
|
156
|
+
regexSource += "[^/]";
|
|
157
|
+
characterIndex++;
|
|
158
|
+
} else {
|
|
159
|
+
regexSource += normalizedPattern[characterIndex].replace(REGEX_SPECIAL_CHARACTERS, "\\$&");
|
|
160
|
+
characterIndex++;
|
|
161
|
+
}
|
|
162
|
+
regexSource += "$";
|
|
163
|
+
return new RegExp(regexSource);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/utils/filter-diagnostics.ts
|
|
168
|
+
const filterIgnoredDiagnostics = (diagnostics, config) => {
|
|
169
|
+
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
|
|
170
|
+
const ignoredFilePatterns = Array.isArray(config.ignore?.files) ? config.ignore.files.map(compileGlobPattern) : [];
|
|
171
|
+
if (ignoredRules.size === 0 && ignoredFilePatterns.length === 0) return diagnostics;
|
|
172
|
+
return diagnostics.filter((diagnostic) => {
|
|
173
|
+
const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
174
|
+
if (ignoredRules.has(ruleIdentifier)) return false;
|
|
175
|
+
const normalizedPath = diagnostic.filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
176
|
+
if (ignoredFilePatterns.some((pattern) => pattern.test(normalizedPath))) return false;
|
|
177
|
+
return true;
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/utils/combine-diagnostics.ts
|
|
183
|
+
const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
|
|
184
|
+
const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isDiffMode, userConfig) => {
|
|
185
|
+
const allDiagnostics = [
|
|
186
|
+
...lintDiagnostics,
|
|
187
|
+
...deadCodeDiagnostics,
|
|
188
|
+
...isDiffMode ? [] : checkReducedMotion(directory)
|
|
189
|
+
];
|
|
190
|
+
return userConfig ? filterIgnoredDiagnostics(allDiagnostics, userConfig) : allDiagnostics;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/utils/find-monorepo-root.ts
|
|
195
|
+
const isMonorepoRoot = (directory) => {
|
|
196
|
+
if (fs.existsSync(path.join(directory, "pnpm-workspace.yaml"))) return true;
|
|
197
|
+
const packageJsonPath = path.join(directory, "package.json");
|
|
198
|
+
if (!fs.existsSync(packageJsonPath)) return false;
|
|
199
|
+
const packageJson = readPackageJson(packageJsonPath);
|
|
200
|
+
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
201
|
+
};
|
|
202
|
+
const findMonorepoRoot = (startDirectory) => {
|
|
203
|
+
let currentDirectory = path.dirname(startDirectory);
|
|
204
|
+
while (currentDirectory !== path.dirname(currentDirectory)) {
|
|
205
|
+
if (isMonorepoRoot(currentDirectory)) return currentDirectory;
|
|
206
|
+
currentDirectory = path.dirname(currentDirectory);
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
};
|
|
210
|
+
|
|
85
211
|
//#endregion
|
|
86
212
|
//#region src/utils/discover-project.ts
|
|
87
213
|
const REACT_COMPILER_PACKAGES = new Set([
|
|
@@ -183,27 +309,14 @@ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
|
|
|
183
309
|
if (fs.existsSync(directoryPath) && fs.existsSync(path.join(directoryPath, "package.json"))) return [directoryPath];
|
|
184
310
|
return [];
|
|
185
311
|
}
|
|
186
|
-
const
|
|
312
|
+
const wildcardIndex = cleanPattern.indexOf("*");
|
|
313
|
+
const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
|
|
314
|
+
const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
|
|
187
315
|
if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) return [];
|
|
188
|
-
return fs.readdirSync(baseDirectory).map((entry) => path.join(baseDirectory, entry)).filter((entryPath) => fs.statSync(entryPath).isDirectory() && fs.existsSync(path.join(entryPath, "package.json")));
|
|
189
|
-
};
|
|
190
|
-
const isMonorepoRoot = (directory) => {
|
|
191
|
-
if (fs.existsSync(path.join(directory, "pnpm-workspace.yaml"))) return true;
|
|
192
|
-
const packageJsonPath = path.join(directory, "package.json");
|
|
193
|
-
if (!fs.existsSync(packageJsonPath)) return false;
|
|
194
|
-
const packageJson = readPackageJson(packageJsonPath);
|
|
195
|
-
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
196
|
-
};
|
|
197
|
-
const findMonorepoRoot$1 = (startDirectory) => {
|
|
198
|
-
let currentDirectory = path.dirname(startDirectory);
|
|
199
|
-
while (currentDirectory !== path.dirname(currentDirectory)) {
|
|
200
|
-
if (isMonorepoRoot(currentDirectory)) return currentDirectory;
|
|
201
|
-
currentDirectory = path.dirname(currentDirectory);
|
|
202
|
-
}
|
|
203
|
-
return null;
|
|
316
|
+
return fs.readdirSync(baseDirectory).map((entry) => path.join(baseDirectory, entry, suffixAfterWildcard)).filter((entryPath) => fs.existsSync(entryPath) && fs.statSync(entryPath).isDirectory() && fs.existsSync(path.join(entryPath, "package.json")));
|
|
204
317
|
};
|
|
205
318
|
const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
206
|
-
const monorepoRoot = findMonorepoRoot
|
|
319
|
+
const monorepoRoot = findMonorepoRoot(directory);
|
|
207
320
|
if (!monorepoRoot) return {
|
|
208
321
|
reactVersion: null,
|
|
209
322
|
framework: "unknown"
|
|
@@ -289,49 +402,6 @@ const discoverProject = (directory) => {
|
|
|
289
402
|
};
|
|
290
403
|
};
|
|
291
404
|
|
|
292
|
-
//#endregion
|
|
293
|
-
//#region src/utils/match-glob-pattern.ts
|
|
294
|
-
const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
|
|
295
|
-
const compileGlobPattern = (pattern) => {
|
|
296
|
-
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
297
|
-
let regexSource = "^";
|
|
298
|
-
let characterIndex = 0;
|
|
299
|
-
while (characterIndex < normalizedPattern.length) if (normalizedPattern[characterIndex] === "*" && normalizedPattern[characterIndex + 1] === "*") if (normalizedPattern[characterIndex + 2] === "/") {
|
|
300
|
-
regexSource += "(?:.+/)?";
|
|
301
|
-
characterIndex += 3;
|
|
302
|
-
} else {
|
|
303
|
-
regexSource += ".*";
|
|
304
|
-
characterIndex += 2;
|
|
305
|
-
}
|
|
306
|
-
else if (normalizedPattern[characterIndex] === "*") {
|
|
307
|
-
regexSource += "[^/]*";
|
|
308
|
-
characterIndex++;
|
|
309
|
-
} else if (normalizedPattern[characterIndex] === "?") {
|
|
310
|
-
regexSource += "[^/]";
|
|
311
|
-
characterIndex++;
|
|
312
|
-
} else {
|
|
313
|
-
regexSource += normalizedPattern[characterIndex].replace(REGEX_SPECIAL_CHARACTERS, "\\$&");
|
|
314
|
-
characterIndex++;
|
|
315
|
-
}
|
|
316
|
-
regexSource += "$";
|
|
317
|
-
return new RegExp(regexSource);
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
//#endregion
|
|
321
|
-
//#region src/utils/filter-diagnostics.ts
|
|
322
|
-
const filterIgnoredDiagnostics = (diagnostics, config) => {
|
|
323
|
-
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
|
|
324
|
-
const ignoredFilePatterns = Array.isArray(config.ignore?.files) ? config.ignore.files.map(compileGlobPattern) : [];
|
|
325
|
-
if (ignoredRules.size === 0 && ignoredFilePatterns.length === 0) return diagnostics;
|
|
326
|
-
return diagnostics.filter((diagnostic) => {
|
|
327
|
-
const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
328
|
-
if (ignoredRules.has(ruleIdentifier)) return false;
|
|
329
|
-
const normalizedPath = diagnostic.filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
330
|
-
if (ignoredFilePatterns.some((pattern) => pattern.test(normalizedPath))) return false;
|
|
331
|
-
return true;
|
|
332
|
-
});
|
|
333
|
-
};
|
|
334
|
-
|
|
335
405
|
//#endregion
|
|
336
406
|
//#region src/utils/load-config.ts
|
|
337
407
|
const CONFIG_FILENAME = "react-doctor.config.json";
|
|
@@ -416,24 +486,10 @@ const silenced = async (fn) => {
|
|
|
416
486
|
console.error = originalError;
|
|
417
487
|
}
|
|
418
488
|
};
|
|
419
|
-
const findMonorepoRoot = (directory) => {
|
|
420
|
-
let currentDirectory = path.dirname(directory);
|
|
421
|
-
while (currentDirectory !== path.dirname(currentDirectory)) {
|
|
422
|
-
if (fs.existsSync(path.join(currentDirectory, "pnpm-workspace.yaml")) || (() => {
|
|
423
|
-
const packageJsonPath = path.join(currentDirectory, "package.json");
|
|
424
|
-
if (!fs.existsSync(packageJsonPath)) return false;
|
|
425
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
426
|
-
return Array.isArray(packageJson.workspaces) || packageJson.workspaces?.packages;
|
|
427
|
-
})()) return currentDirectory;
|
|
428
|
-
currentDirectory = path.dirname(currentDirectory);
|
|
429
|
-
}
|
|
430
|
-
return null;
|
|
431
|
-
};
|
|
432
489
|
const CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\/([a-z-]+)\.config\./;
|
|
433
490
|
const extractFailedPluginName = (error) => {
|
|
434
491
|
return String(error).match(CONFIG_LOADING_ERROR_PATTERN)?.[1] ?? null;
|
|
435
492
|
};
|
|
436
|
-
const MAX_KNIP_RETRIES = 5;
|
|
437
493
|
const runKnipWithOptions = async (knipCwd, workspaceName) => {
|
|
438
494
|
const options = await silenced(() => createOptions({
|
|
439
495
|
cwd: knipCwd,
|
|
@@ -818,7 +874,70 @@ const resolvePluginPath = () => {
|
|
|
818
874
|
const resolveDiagnosticCategory = (plugin, rule) => {
|
|
819
875
|
return RULE_CATEGORY_MAP[`${plugin}/${rule}`] ?? PLUGIN_CATEGORY_MAP[plugin] ?? "Other";
|
|
820
876
|
};
|
|
821
|
-
const
|
|
877
|
+
const estimateArgsLength = (args) => args.reduce((total, argument) => total + argument.length + 1, 0);
|
|
878
|
+
const batchIncludePaths = (baseArgs, includePaths) => {
|
|
879
|
+
const baseArgsLength = estimateArgsLength(baseArgs);
|
|
880
|
+
const batches = [];
|
|
881
|
+
let currentBatch = [];
|
|
882
|
+
let currentBatchLength = baseArgsLength;
|
|
883
|
+
for (const filePath of includePaths) {
|
|
884
|
+
const entryLength = filePath.length + 1;
|
|
885
|
+
if (currentBatch.length > 0 && currentBatchLength + entryLength > SPAWN_ARGS_MAX_LENGTH_CHARS) {
|
|
886
|
+
batches.push(currentBatch);
|
|
887
|
+
currentBatch = [];
|
|
888
|
+
currentBatchLength = baseArgsLength;
|
|
889
|
+
}
|
|
890
|
+
currentBatch.push(filePath);
|
|
891
|
+
currentBatchLength += entryLength;
|
|
892
|
+
}
|
|
893
|
+
if (currentBatch.length > 0) batches.push(currentBatch);
|
|
894
|
+
return batches;
|
|
895
|
+
};
|
|
896
|
+
const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
|
|
897
|
+
const child = spawn(nodeBinaryPath, args, { cwd: rootDirectory });
|
|
898
|
+
const stdoutBuffers = [];
|
|
899
|
+
const stderrBuffers = [];
|
|
900
|
+
child.stdout.on("data", (buffer) => stdoutBuffers.push(buffer));
|
|
901
|
+
child.stderr.on("data", (buffer) => stderrBuffers.push(buffer));
|
|
902
|
+
child.on("error", (error) => reject(/* @__PURE__ */ new Error(`Failed to run oxlint: ${error.message}`)));
|
|
903
|
+
child.on("close", () => {
|
|
904
|
+
const output = Buffer.concat(stdoutBuffers).toString("utf-8").trim();
|
|
905
|
+
if (!output) {
|
|
906
|
+
const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
|
|
907
|
+
if (stderrOutput) {
|
|
908
|
+
reject(/* @__PURE__ */ new Error(`Failed to run oxlint: ${stderrOutput}`));
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
resolve(output);
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
const parseOxlintOutput = (stdout) => {
|
|
916
|
+
if (!stdout) return [];
|
|
917
|
+
let output;
|
|
918
|
+
try {
|
|
919
|
+
output = JSON.parse(stdout);
|
|
920
|
+
} catch {
|
|
921
|
+
throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, ERROR_PREVIEW_LENGTH_CHARS)}`);
|
|
922
|
+
}
|
|
923
|
+
return output.diagnostics.filter((diagnostic) => diagnostic.code && JSX_FILE_PATTERN.test(diagnostic.filename)).map((diagnostic) => {
|
|
924
|
+
const { plugin, rule } = parseRuleCode(diagnostic.code);
|
|
925
|
+
const primaryLabel = diagnostic.labels[0];
|
|
926
|
+
const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule);
|
|
927
|
+
return {
|
|
928
|
+
filePath: diagnostic.filename,
|
|
929
|
+
plugin,
|
|
930
|
+
rule,
|
|
931
|
+
severity: diagnostic.severity,
|
|
932
|
+
message: cleaned.message,
|
|
933
|
+
help: cleaned.help,
|
|
934
|
+
line: primaryLabel?.span.line ?? 0,
|
|
935
|
+
column: primaryLabel?.span.column ?? 0,
|
|
936
|
+
category: resolveDiagnosticCategory(plugin, rule)
|
|
937
|
+
};
|
|
938
|
+
});
|
|
939
|
+
};
|
|
940
|
+
const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompiler, includePaths, nodeBinaryPath = process.execPath) => {
|
|
822
941
|
if (includePaths !== void 0 && includePaths.length === 0) return [];
|
|
823
942
|
const configPath = path.join(os.tmpdir(), `react-doctor-oxlintrc-${process.pid}.json`);
|
|
824
943
|
const config = createOxlintConfig({
|
|
@@ -829,58 +948,21 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
|
|
|
829
948
|
const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory);
|
|
830
949
|
try {
|
|
831
950
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
832
|
-
const
|
|
951
|
+
const baseArgs = [
|
|
833
952
|
resolveOxlintBinary(),
|
|
834
953
|
"-c",
|
|
835
954
|
configPath,
|
|
836
955
|
"--format",
|
|
837
956
|
"json"
|
|
838
957
|
];
|
|
839
|
-
if (hasTypeScript)
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
const stderrBuffers = [];
|
|
846
|
-
child.stdout.on("data", (buffer) => stdoutBuffers.push(buffer));
|
|
847
|
-
child.stderr.on("data", (buffer) => stderrBuffers.push(buffer));
|
|
848
|
-
child.on("error", (error) => reject(/* @__PURE__ */ new Error(`Failed to run oxlint: ${error.message}`)));
|
|
849
|
-
child.on("close", () => {
|
|
850
|
-
const output = Buffer.concat(stdoutBuffers).toString("utf-8").trim();
|
|
851
|
-
if (!output) {
|
|
852
|
-
const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
|
|
853
|
-
if (stderrOutput) {
|
|
854
|
-
reject(/* @__PURE__ */ new Error(`Failed to run oxlint: ${stderrOutput}`));
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
resolve(output);
|
|
859
|
-
});
|
|
860
|
-
});
|
|
861
|
-
if (!stdout) return [];
|
|
862
|
-
let output;
|
|
863
|
-
try {
|
|
864
|
-
output = JSON.parse(stdout);
|
|
865
|
-
} catch {
|
|
866
|
-
throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, ERROR_PREVIEW_LENGTH_CHARS)}`);
|
|
958
|
+
if (hasTypeScript) baseArgs.push("--tsconfig", "./tsconfig.json");
|
|
959
|
+
const fileBatches = includePaths !== void 0 ? batchIncludePaths(baseArgs, includePaths) : [["."]];
|
|
960
|
+
const allDiagnostics = [];
|
|
961
|
+
for (const batch of fileBatches) {
|
|
962
|
+
const stdout = await spawnOxlint([...baseArgs, ...batch], rootDirectory, nodeBinaryPath);
|
|
963
|
+
allDiagnostics.push(...parseOxlintOutput(stdout));
|
|
867
964
|
}
|
|
868
|
-
return
|
|
869
|
-
const { plugin, rule } = parseRuleCode(diagnostic.code);
|
|
870
|
-
const primaryLabel = diagnostic.labels[0];
|
|
871
|
-
const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule);
|
|
872
|
-
return {
|
|
873
|
-
filePath: diagnostic.filename,
|
|
874
|
-
plugin,
|
|
875
|
-
rule,
|
|
876
|
-
severity: diagnostic.severity,
|
|
877
|
-
message: cleaned.message,
|
|
878
|
-
help: cleaned.help,
|
|
879
|
-
line: primaryLabel?.span.line ?? 0,
|
|
880
|
-
column: primaryLabel?.span.column ?? 0,
|
|
881
|
-
category: resolveDiagnosticCategory(plugin, rule)
|
|
882
|
-
};
|
|
883
|
-
});
|
|
965
|
+
return allDiagnostics;
|
|
884
966
|
} finally {
|
|
885
967
|
restoreDisableDirectives();
|
|
886
968
|
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
|
@@ -979,22 +1061,18 @@ const diagnose = async (directory, options = {}) => {
|
|
|
979
1061
|
const effectiveLint = options.lint ?? userConfig?.lint ?? true;
|
|
980
1062
|
const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;
|
|
981
1063
|
if (!projectInfo.reactVersion) throw new Error("No React dependency found in package.json");
|
|
982
|
-
const jsxIncludePaths =
|
|
1064
|
+
const jsxIncludePaths = computeJsxIncludePaths(includePaths);
|
|
1065
|
+
const emptyDiagnostics = [];
|
|
983
1066
|
const lintPromise = effectiveLint ? runOxlint(resolvedDirectory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler, jsxIncludePaths).catch((error) => {
|
|
984
1067
|
console.error("Lint failed:", error);
|
|
985
|
-
return
|
|
986
|
-
}) : Promise.resolve(
|
|
1068
|
+
return emptyDiagnostics;
|
|
1069
|
+
}) : Promise.resolve(emptyDiagnostics);
|
|
987
1070
|
const deadCodePromise = effectiveDeadCode && !isDiffMode ? runKnip(resolvedDirectory).catch((error) => {
|
|
988
1071
|
console.error("Dead code analysis failed:", error);
|
|
989
|
-
return
|
|
990
|
-
}) : Promise.resolve(
|
|
1072
|
+
return emptyDiagnostics;
|
|
1073
|
+
}) : Promise.resolve(emptyDiagnostics);
|
|
991
1074
|
const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);
|
|
992
|
-
const
|
|
993
|
-
...lintDiagnostics,
|
|
994
|
-
...deadCodeDiagnostics,
|
|
995
|
-
...isDiffMode ? [] : checkReducedMotion(resolvedDirectory)
|
|
996
|
-
];
|
|
997
|
-
const diagnostics = userConfig ? filterIgnoredDiagnostics(allDiagnostics, userConfig) : allDiagnostics;
|
|
1075
|
+
const diagnostics = combineDiagnostics(lintDiagnostics, deadCodeDiagnostics, resolvedDirectory, isDiffMode, userConfig);
|
|
998
1076
|
const elapsedMilliseconds = performance.now() - startTime;
|
|
999
1077
|
return {
|
|
1000
1078
|
diagnostics,
|