react-doctor 0.5.6-dev.8908f98 → 0.5.6-dev.937a7ca
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 +264 -88
- package/dist/index.d.ts +4 -3
- package/dist/index.js +46 -15
- package/dist/lsp.js +46 -15
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="ad091b20-c3e2-5c96-93ac-9a910745a035")}catch(e){}}();
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as NodeChildProcess from "node:child_process";
|
|
5
5
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -14,7 +14,7 @@ import * as OS from "node:os";
|
|
|
14
14
|
import os, { tmpdir } from "node:os";
|
|
15
15
|
import { parseJSON5 } from "confbox";
|
|
16
16
|
import * as NodeUrl from "node:url";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
18
|
import { createJiti } from "jiti";
|
|
19
19
|
import * as Crypto from "node:crypto";
|
|
20
20
|
import crypto, { createHash, randomUUID } from "node:crypto";
|
|
@@ -39986,12 +39986,11 @@ const collectKnipPatterns = (rootDirectory, settingName) => {
|
|
|
39986
39986
|
if (!config) return [];
|
|
39987
39987
|
return [...normalizePatternList(config[settingName]), ...collectKnipWorkspacePatterns(config.workspaces, settingName)];
|
|
39988
39988
|
};
|
|
39989
|
-
const collectDeadCodeIgnorePatterns = (rootDirectory
|
|
39989
|
+
const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
39990
39990
|
const seen = /* @__PURE__ */ new Set();
|
|
39991
39991
|
const sources = [
|
|
39992
39992
|
readIgnoreFile(path.join(rootDirectory, ".gitignore")),
|
|
39993
39993
|
collectIgnorePatterns(rootDirectory),
|
|
39994
|
-
userConfig?.ignore?.files ?? [],
|
|
39995
39994
|
collectKnipPatterns(rootDirectory, "ignore")
|
|
39996
39995
|
];
|
|
39997
39996
|
for (const source of sources) for (const pattern of source) seen.add(pattern);
|
|
@@ -40269,11 +40268,10 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
|
|
|
40269
40268
|
});
|
|
40270
40269
|
});
|
|
40271
40270
|
const checkDeadCode = async (options) => {
|
|
40272
|
-
const { userConfig } = options;
|
|
40273
40271
|
const rootDirectory = toCanonicalPath(options.rootDirectory);
|
|
40274
40272
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
40275
40273
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
40276
|
-
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory
|
|
40274
|
+
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
40277
40275
|
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
40278
40276
|
rootDirectory,
|
|
40279
40277
|
entryPatterns,
|
|
@@ -41322,8 +41320,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
41322
41320
|
}
|
|
41323
41321
|
return enabled;
|
|
41324
41322
|
};
|
|
41325
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
|
|
41326
|
-
const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41323
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
|
|
41324
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41327
41325
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
41328
41326
|
const jsPlugins = [];
|
|
41329
41327
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -42020,9 +42018,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
42020
42018
|
try {
|
|
42021
42019
|
parsed = JSON.parse(sanitizedStdout);
|
|
42022
42020
|
} catch {
|
|
42023
|
-
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42021
|
+
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42024
42022
|
}
|
|
42025
|
-
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42023
|
+
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42026
42024
|
const minifiedFileCache = /* @__PURE__ */ new Map();
|
|
42027
42025
|
const isMinifiedDiagnosticFile = (filename) => {
|
|
42028
42026
|
const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
|
|
@@ -42313,6 +42311,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
42313
42311
|
NFS.closeSync(fileHandle);
|
|
42314
42312
|
}
|
|
42315
42313
|
};
|
|
42314
|
+
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
42315
|
+
/**
|
|
42316
|
+
* Detects an oxlint config-load crash caused by the optional
|
|
42317
|
+
* `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
|
|
42318
|
+
* builds the partial-failure note for it; returns `null` when the failure
|
|
42319
|
+
* was anything else.
|
|
42320
|
+
*
|
|
42321
|
+
* oxlint prints a framed error to stdout (not stderr) and exits non-zero
|
|
42322
|
+
* when a `jsPlugins` entry can't be imported; that non-JSON stdout
|
|
42323
|
+
* surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
|
|
42324
|
+
* config load on it, leaving the plugin in would drop every curated
|
|
42325
|
+
* react-doctor diagnostic too — so the caller retries with the plugin
|
|
42326
|
+
* stripped (issue #833). Both markers sit at the start of oxlint's
|
|
42327
|
+
* message, so they survive the `preview` slice even for deep pnpm paths.
|
|
42328
|
+
*/
|
|
42329
|
+
const reactHooksJsPluginDropNote = (error) => {
|
|
42330
|
+
if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
|
|
42331
|
+
const { preview } = error.reason;
|
|
42332
|
+
if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
|
|
42333
|
+
const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
|
|
42334
|
+
return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
|
|
42335
|
+
};
|
|
42316
42336
|
/**
|
|
42317
42337
|
* The oxlint runner. Composed of three pieces in `runners/oxlint/`:
|
|
42318
42338
|
*
|
|
@@ -42340,15 +42360,16 @@ const runOxlint = async (options) => {
|
|
|
42340
42360
|
const pluginPath = resolvePluginPath();
|
|
42341
42361
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
42342
42362
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
42343
|
-
const buildConfig = (
|
|
42363
|
+
const buildConfig = (overrides) => createOxlintConfig({
|
|
42344
42364
|
pluginPath,
|
|
42345
42365
|
project,
|
|
42346
42366
|
customRulesOnly,
|
|
42347
|
-
extendsPaths:
|
|
42367
|
+
extendsPaths: overrides.extendsPaths,
|
|
42348
42368
|
ignoredTags,
|
|
42349
42369
|
serverAuthFunctionNames,
|
|
42350
42370
|
severityControls,
|
|
42351
|
-
userPlugins
|
|
42371
|
+
userPlugins,
|
|
42372
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
42352
42373
|
});
|
|
42353
42374
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
42354
42375
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -42384,12 +42405,22 @@ const runOxlint = async (options) => {
|
|
|
42384
42405
|
outputMaxBytes,
|
|
42385
42406
|
concurrency: options.concurrency
|
|
42386
42407
|
});
|
|
42387
|
-
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
42408
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
42388
42409
|
try {
|
|
42389
42410
|
return await runBatches();
|
|
42390
42411
|
} catch (error) {
|
|
42412
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
42413
|
+
if (reactHooksJsDropNote !== null) {
|
|
42414
|
+
writeOxlintConfig(configPath, buildConfig({
|
|
42415
|
+
extendsPaths,
|
|
42416
|
+
disableReactHooksJsPlugin: true
|
|
42417
|
+
}));
|
|
42418
|
+
const diagnostics = await runBatches();
|
|
42419
|
+
onPartialFailure?.(reactHooksJsDropNote);
|
|
42420
|
+
return diagnostics;
|
|
42421
|
+
}
|
|
42391
42422
|
if (extendsPaths.length === 0) throw error;
|
|
42392
|
-
writeOxlintConfig(configPath, buildConfig([]));
|
|
42423
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
|
|
42393
42424
|
return await runBatches();
|
|
42394
42425
|
}
|
|
42395
42426
|
} finally {
|
|
@@ -43841,7 +43872,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
|
|
|
43841
43872
|
"false"
|
|
43842
43873
|
]);
|
|
43843
43874
|
const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
|
|
43844
|
-
const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(
|
|
43875
|
+
const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
|
|
43845
43876
|
const detectCiProvider = () => {
|
|
43846
43877
|
for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
|
|
43847
43878
|
return isCiFlagSet(process.env.CI) ? "unknown" : null;
|
|
@@ -43866,6 +43897,42 @@ const detectCodingAgent = () => {
|
|
|
43866
43897
|
const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
|
|
43867
43898
|
const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
|
|
43868
43899
|
//#endregion
|
|
43900
|
+
//#region src/cli/utils/detect-terminal-kind.ts
|
|
43901
|
+
const TERMINAL_BY_TERM_PROGRAM = [
|
|
43902
|
+
["vscode", "vscode"],
|
|
43903
|
+
["iTerm.app", "iterm"],
|
|
43904
|
+
["Apple_Terminal", "apple-terminal"],
|
|
43905
|
+
["WezTerm", "wezterm"],
|
|
43906
|
+
["ghostty", "ghostty"],
|
|
43907
|
+
["Hyper", "hyper"],
|
|
43908
|
+
["Tabby", "tabby"],
|
|
43909
|
+
["rio", "rio"]
|
|
43910
|
+
];
|
|
43911
|
+
/**
|
|
43912
|
+
* Best-effort label for the terminal emulator / editor hosting the CLI,
|
|
43913
|
+
* derived from terminal-identity env vars. Recorded as the `terminalKind` run
|
|
43914
|
+
* tag so we can see where React Doctor is actually run (nvim, VS Code, iTerm,
|
|
43915
|
+
* …) — the split Sentry can't otherwise see. Low-cardinality and free of any
|
|
43916
|
+
* username/path/secret, so it's safe as a tag. Editor terminals (nvim/vim)
|
|
43917
|
+
* win over the outer emulator because that's the surface a user is reading in;
|
|
43918
|
+
* "ci" marks a run with no interactive terminal; "unknown" when nothing matches.
|
|
43919
|
+
*/
|
|
43920
|
+
const detectTerminalKind = (env = process.env) => {
|
|
43921
|
+
if (env.NVIM) return "neovim";
|
|
43922
|
+
if (env.VIM_TERMINAL) return "vim";
|
|
43923
|
+
const termProgram = env.TERM_PROGRAM;
|
|
43924
|
+
if (termProgram) {
|
|
43925
|
+
for (const [marker, label] of TERMINAL_BY_TERM_PROGRAM) if (termProgram === marker) return label;
|
|
43926
|
+
}
|
|
43927
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
|
|
43928
|
+
if (env.WT_SESSION) return "windows-terminal";
|
|
43929
|
+
if (env.ALACRITTY_WINDOW_ID || env.TERM === "alacritty") return "alacritty";
|
|
43930
|
+
if (env.VTE_VERSION) return "vte";
|
|
43931
|
+
if (env.TMUX) return "tmux";
|
|
43932
|
+
if (isCiEnvironment(env)) return "ci";
|
|
43933
|
+
return "unknown";
|
|
43934
|
+
};
|
|
43935
|
+
//#endregion
|
|
43869
43936
|
//#region src/cli/utils/is-git-hook-environment.ts
|
|
43870
43937
|
const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
|
|
43871
43938
|
//#endregion
|
|
@@ -43888,6 +43955,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
|
|
|
43888
43955
|
const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
|
|
43889
43956
|
//#endregion
|
|
43890
43957
|
//#region src/cli/utils/constants.ts
|
|
43958
|
+
const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
|
|
43891
43959
|
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
43892
43960
|
const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
|
|
43893
43961
|
const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
|
|
@@ -43972,7 +44040,7 @@ const makeNoopConsole = () => ({
|
|
|
43972
44040
|
});
|
|
43973
44041
|
//#endregion
|
|
43974
44042
|
//#region src/cli/utils/version.ts
|
|
43975
|
-
const VERSION = "0.5.6-dev.
|
|
44043
|
+
const VERSION = "0.5.6-dev.937a7ca";
|
|
43976
44044
|
//#endregion
|
|
43977
44045
|
//#region src/cli/utils/json-mode.ts
|
|
43978
44046
|
let context = null;
|
|
@@ -44122,6 +44190,7 @@ const buildRunContext = () => {
|
|
|
44122
44190
|
viaAction: isOfficialGithubAction(),
|
|
44123
44191
|
codingAgent: detectCodingAgent(),
|
|
44124
44192
|
interactive: !isNonInteractiveEnvironment(),
|
|
44193
|
+
terminalKind: detectTerminalKind(),
|
|
44125
44194
|
jsonMode: isJsonModeActive(),
|
|
44126
44195
|
invokedVia: detectInvokedVia()
|
|
44127
44196
|
};
|
|
@@ -44192,6 +44261,7 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44192
44261
|
viaAction: runContext.viaAction,
|
|
44193
44262
|
codingAgent: runContext.codingAgent,
|
|
44194
44263
|
interactive: runContext.interactive,
|
|
44264
|
+
terminalKind: runContext.terminalKind,
|
|
44195
44265
|
jsonMode: runContext.jsonMode,
|
|
44196
44266
|
invokedVia: runContext.invokedVia,
|
|
44197
44267
|
nodeMajor: runContext.nodeMajor
|
|
@@ -44330,13 +44400,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44330
44400
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44331
44401
|
* standard `SENTRY_RELEASE` override.
|
|
44332
44402
|
*/
|
|
44333
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.
|
|
44403
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.937a7ca`;
|
|
44334
44404
|
/**
|
|
44335
44405
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44336
44406
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44337
44407
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44338
44408
|
*/
|
|
44339
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.
|
|
44409
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.937a7ca") ? "development" : "production");
|
|
44340
44410
|
/**
|
|
44341
44411
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44342
44412
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -48118,7 +48188,7 @@ const AGENT_GUIDANCE_LINES = [
|
|
|
48118
48188
|
"Investigate deeply where relevant: race conditions, security-sensitive flows, state propagation, multi-file refactors, and downstream dependency chains.",
|
|
48119
48189
|
"Ignore pure style preferences, theoretical issues without real impact, missing features, and unrelated pre-existing code.",
|
|
48120
48190
|
"Start with high-confidence fixes that preserve behavior. Leave low-confidence or product-dependent changes as notes.",
|
|
48121
|
-
"Run `npx react-doctor@latest --verbose --
|
|
48191
|
+
"Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
|
|
48122
48192
|
"When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
|
|
48123
48193
|
"Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
|
|
48124
48194
|
"For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
|
|
@@ -48193,6 +48263,15 @@ const boxText = (content, innerWidth) => {
|
|
|
48193
48263
|
].join("\n");
|
|
48194
48264
|
};
|
|
48195
48265
|
//#endregion
|
|
48266
|
+
//#region src/cli/utils/resolve-absolute-path.ts
|
|
48267
|
+
/**
|
|
48268
|
+
* Resolves a diagnostic's `filePath` (relative to its project root, or
|
|
48269
|
+
* already absolute) to an absolute path. Shared by the code-frame reader and
|
|
48270
|
+
* the terminal hyperlink builder so both turn a relative path into the same
|
|
48271
|
+
* on-disk location.
|
|
48272
|
+
*/
|
|
48273
|
+
const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
|
|
48274
|
+
//#endregion
|
|
48196
48275
|
//#region src/cli/utils/build-code-frame.ts
|
|
48197
48276
|
/**
|
|
48198
48277
|
* Renders a syntax-highlighted source excerpt around a diagnostic site
|
|
@@ -48203,7 +48282,7 @@ const boxText = (content, innerWidth) => {
|
|
|
48203
48282
|
*/
|
|
48204
48283
|
const buildCodeFrame = (input) => {
|
|
48205
48284
|
if (input.line <= 0) return null;
|
|
48206
|
-
const absolutePath =
|
|
48285
|
+
const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
|
|
48207
48286
|
let source;
|
|
48208
48287
|
try {
|
|
48209
48288
|
source = NFS.readFileSync(absolutePath, "utf8");
|
|
@@ -48243,6 +48322,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
|
|
|
48243
48322
|
const DIVIDER_INDENT = " ";
|
|
48244
48323
|
const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
|
|
48245
48324
|
//#endregion
|
|
48325
|
+
//#region src/cli/utils/format-hyperlink.ts
|
|
48326
|
+
const OSC = "\x1B]";
|
|
48327
|
+
const ST = "\x1B\\";
|
|
48328
|
+
/**
|
|
48329
|
+
* Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
|
|
48330
|
+
* are exactly `text`; the link is carried in escape sequences a capable
|
|
48331
|
+
* terminal turns into a click target.
|
|
48332
|
+
*/
|
|
48333
|
+
const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
|
|
48334
|
+
//#endregion
|
|
48246
48335
|
//#region src/cli/utils/indent-multiline-text.ts
|
|
48247
48336
|
const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
48248
48337
|
//#endregion
|
|
@@ -48396,17 +48485,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
|
|
|
48396
48485
|
}
|
|
48397
48486
|
return clusters;
|
|
48398
48487
|
};
|
|
48399
|
-
const
|
|
48488
|
+
const formatClusterLocationText = (cluster) => {
|
|
48489
|
+
const { filePath } = cluster.diagnostics[0];
|
|
48490
|
+
if (cluster.startLine <= 0) return filePath;
|
|
48491
|
+
if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
|
|
48492
|
+
return `${filePath}:${cluster.startLine}`;
|
|
48493
|
+
};
|
|
48494
|
+
const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
|
|
48400
48495
|
const lead = cluster.diagnostics[0];
|
|
48401
48496
|
const contextTag = formatFileContextTag(lead);
|
|
48402
|
-
|
|
48403
|
-
if (
|
|
48404
|
-
return `${lead.filePath
|
|
48497
|
+
const location = formatClusterLocationText(cluster);
|
|
48498
|
+
if (!hyperlinks) return `${location}${contextTag}`;
|
|
48499
|
+
return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
|
|
48405
48500
|
};
|
|
48406
|
-
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
|
|
48501
|
+
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
|
|
48407
48502
|
const lead = cluster.diagnostics[0];
|
|
48408
48503
|
const isMultiSite = cluster.diagnostics.length > 1;
|
|
48409
|
-
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
|
|
48504
|
+
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
|
|
48410
48505
|
const codeFrame = renderCodeFrame ? buildCodeFrame({
|
|
48411
48506
|
filePath: lead.filePath,
|
|
48412
48507
|
line: cluster.startLine,
|
|
@@ -48425,7 +48520,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
|
|
|
48425
48520
|
}
|
|
48426
48521
|
return lines;
|
|
48427
48522
|
};
|
|
48428
|
-
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
|
|
48523
|
+
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
|
|
48429
48524
|
const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
|
|
48430
48525
|
const { severity } = representative;
|
|
48431
48526
|
const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
|
|
@@ -48445,7 +48540,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
|
|
|
48445
48540
|
}
|
|
48446
48541
|
const renderCodeFrame = severity === "error";
|
|
48447
48542
|
const sites = renderEverySite ? ruleDiagnostics : [representative];
|
|
48448
|
-
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame));
|
|
48543
|
+
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
|
|
48449
48544
|
return lines;
|
|
48450
48545
|
};
|
|
48451
48546
|
const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
|
|
@@ -48458,7 +48553,7 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
|
|
|
48458
48553
|
return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
|
|
48459
48554
|
};
|
|
48460
48555
|
const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
|
|
48461
|
-
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
|
|
48556
|
+
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
|
|
48462
48557
|
const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
|
|
48463
48558
|
if (topRuleGroups.length === 0) return {
|
|
48464
48559
|
lines: [],
|
|
@@ -48468,7 +48563,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
|
|
|
48468
48563
|
const blockOffsets = [];
|
|
48469
48564
|
for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
|
|
48470
48565
|
blockOffsets.push(lines.length);
|
|
48471
|
-
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
|
|
48566
|
+
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
|
|
48472
48567
|
lines.push("");
|
|
48473
48568
|
}
|
|
48474
48569
|
return {
|
|
@@ -48506,18 +48601,18 @@ const buildOverviewHeaderLines = (diagnostics) => {
|
|
|
48506
48601
|
* single Effect.forEach over Console.log so failures or fiber
|
|
48507
48602
|
* interruption produce predictable partial output.
|
|
48508
48603
|
*/
|
|
48509
|
-
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}) => gen(function* () {
|
|
48604
|
+
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
|
|
48510
48605
|
const sectionPause = onboarding.sectionPause ?? void_;
|
|
48511
48606
|
const animateCountUp = onboarding.animateCountUp ?? false;
|
|
48512
48607
|
const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
|
|
48513
48608
|
let detailLines;
|
|
48514
48609
|
let topErrorBlockOffsets = [];
|
|
48515
48610
|
if (!isVerbose) {
|
|
48516
|
-
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
|
|
48611
|
+
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
|
|
48517
48612
|
detailLines = topErrors.lines;
|
|
48518
48613
|
topErrorBlockOffsets = topErrors.blockOffsets;
|
|
48519
48614
|
} else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
|
|
48520
|
-
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
|
|
48615
|
+
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
|
|
48521
48616
|
});
|
|
48522
48617
|
const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
|
|
48523
48618
|
const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
|
|
@@ -48578,6 +48673,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
|
|
|
48578
48673
|
//#endregion
|
|
48579
48674
|
//#region src/cli/utils/filter-diagnostics-by-categories.ts
|
|
48580
48675
|
const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
|
|
48676
|
+
//#endregion
|
|
48677
|
+
//#region src/cli/utils/supports-hyperlinks.ts
|
|
48678
|
+
const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
|
|
48679
|
+
"iTerm.app",
|
|
48680
|
+
"WezTerm",
|
|
48681
|
+
"vscode",
|
|
48682
|
+
"Hyper",
|
|
48683
|
+
"ghostty",
|
|
48684
|
+
"Tabby",
|
|
48685
|
+
"rio"
|
|
48686
|
+
]);
|
|
48687
|
+
const parseVteVersion = (raw) => {
|
|
48688
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
48689
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
48690
|
+
};
|
|
48691
|
+
/**
|
|
48692
|
+
* Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
|
|
48693
|
+
* from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
|
|
48694
|
+
* overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
|
|
48695
|
+
* forces on), mirroring how the ecosystem's terminal libraries gate the same
|
|
48696
|
+
* feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
|
|
48697
|
+
* raw escape rather than a link). Unknown terminals default to off.
|
|
48698
|
+
*/
|
|
48699
|
+
const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
|
|
48700
|
+
const forced = env.FORCE_HYPERLINK;
|
|
48701
|
+
if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
|
|
48702
|
+
if (stream.isTTY !== true) return false;
|
|
48703
|
+
if (env.TERM === "dumb") return false;
|
|
48704
|
+
if (isCiEnvironment(env)) return false;
|
|
48705
|
+
if (env.WT_SESSION) return true;
|
|
48706
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
|
|
48707
|
+
if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
|
|
48708
|
+
return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
|
|
48709
|
+
};
|
|
48710
|
+
//#endregion
|
|
48711
|
+
//#region src/cli/utils/should-render-hyperlinks.ts
|
|
48712
|
+
/**
|
|
48713
|
+
* Whether to emit OSC 8 clickable `file:line` locations for this run: a
|
|
48714
|
+
* hyperlink-capable terminal AND not a coding agent (whose output parsers
|
|
48715
|
+
* would choke on the escape sequences).
|
|
48716
|
+
*/
|
|
48717
|
+
const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
|
|
48581
48718
|
const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
|
|
48582
48719
|
const FALSY_FLAG_VALUES = new Set([
|
|
48583
48720
|
"",
|
|
@@ -48597,10 +48734,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
|
|
|
48597
48734
|
};
|
|
48598
48735
|
//#endregion
|
|
48599
48736
|
//#region src/cli/utils/onboarding-state.ts
|
|
48600
|
-
const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
|
|
48601
48737
|
const ONBOARDED_AT_KEY = "onboardedAt";
|
|
48602
48738
|
const getOnboardingStore = (options = {}) => new Conf({
|
|
48603
|
-
projectName:
|
|
48739
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
48604
48740
|
cwd: options.cwd
|
|
48605
48741
|
});
|
|
48606
48742
|
const hasCompletedOnboarding = (options = {}) => {
|
|
@@ -49676,6 +49812,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49676
49812
|
}
|
|
49677
49813
|
const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
|
|
49678
49814
|
const pause = onboardingSectionPause(animateRender);
|
|
49815
|
+
const useHyperlinks = shouldRenderHyperlinks(process.stdout);
|
|
49679
49816
|
const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
|
|
49680
49817
|
const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
|
|
49681
49818
|
if (printedDiagnostics.length === 0) {
|
|
@@ -49701,7 +49838,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49701
49838
|
yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
|
|
49702
49839
|
sectionPause: pause,
|
|
49703
49840
|
animateCountUp: animateRender
|
|
49704
|
-
});
|
|
49841
|
+
}, useHyperlinks);
|
|
49705
49842
|
if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
|
|
49706
49843
|
if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
|
|
49707
49844
|
yield* log(highlighter.gray(` ${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface (e.g. design cleanup) — run \`npx react-doctor@latest .\` locally for the full list.`));
|
|
@@ -49894,16 +50031,7 @@ const buildHandoffPayload = (input) => {
|
|
|
49894
50031
|
try {
|
|
49895
50032
|
outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
|
|
49896
50033
|
} catch {}
|
|
49897
|
-
const lines = [
|
|
49898
|
-
"Before fixing anything, first ask me whether I'd like to add React Doctor to CI so every pull request is scanned automatically. Explain that:",
|
|
49899
|
-
`- It's used by teams at ${CI_TRUST_COMPANIES}.`,
|
|
49900
|
-
"- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
|
|
49901
|
-
`- Setup guide: ${CI_URL}`,
|
|
49902
|
-
"If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
|
|
49903
|
-
"",
|
|
49904
|
-
`Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`,
|
|
49905
|
-
""
|
|
49906
|
-
];
|
|
50034
|
+
const lines = [`Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`, ""];
|
|
49907
50035
|
topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
|
|
49908
50036
|
const representative = ruleDiagnostics[0];
|
|
49909
50037
|
const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
|
|
@@ -50273,38 +50401,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
|
|
|
50273
50401
|
//#region src/cli/utils/hash-project-root.ts
|
|
50274
50402
|
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
50275
50403
|
//#endregion
|
|
50276
|
-
//#region src/cli/utils/
|
|
50277
|
-
const
|
|
50278
|
-
const
|
|
50279
|
-
|
|
50280
|
-
|
|
50281
|
-
});
|
|
50282
|
-
|
|
50283
|
-
|
|
50284
|
-
|
|
50285
|
-
|
|
50286
|
-
|
|
50287
|
-
|
|
50288
|
-
|
|
50289
|
-
};
|
|
50290
|
-
const recordActionUpgradeDecision = (projectRoot, outcome, storeOptions = {}) => {
|
|
50291
|
-
try {
|
|
50292
|
-
const store = getActionUpgradeStore(storeOptions);
|
|
50293
|
-
const upgrades = store.get("actionUpgrades", {});
|
|
50294
|
-
store.set("actionUpgrades", {
|
|
50295
|
-
...upgrades,
|
|
50296
|
-
[hashProjectRoot(projectRoot)]: {
|
|
50297
|
-
rootDirectory: Path.resolve(projectRoot),
|
|
50298
|
-
outcome,
|
|
50299
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50404
|
+
//#region src/cli/utils/project-decision-store.ts
|
|
50405
|
+
const createProjectDecisionStore = (storeKey) => {
|
|
50406
|
+
const getStore = (options = {}) => new Conf({
|
|
50407
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
50408
|
+
cwd: options.cwd
|
|
50409
|
+
});
|
|
50410
|
+
return {
|
|
50411
|
+
getConfigPath: (options = {}) => getStore(options).path,
|
|
50412
|
+
hasHandled: (projectRoot, options = {}) => {
|
|
50413
|
+
try {
|
|
50414
|
+
return Boolean(getStore(options).get(storeKey, {})[hashProjectRoot(projectRoot)]);
|
|
50415
|
+
} catch {
|
|
50416
|
+
return true;
|
|
50300
50417
|
}
|
|
50301
|
-
}
|
|
50302
|
-
|
|
50303
|
-
|
|
50304
|
-
|
|
50305
|
-
|
|
50418
|
+
},
|
|
50419
|
+
record: (projectRoot, outcome, options = {}) => {
|
|
50420
|
+
try {
|
|
50421
|
+
const store = getStore(options);
|
|
50422
|
+
store.set(storeKey, {
|
|
50423
|
+
...store.get(storeKey, {}),
|
|
50424
|
+
[hashProjectRoot(projectRoot)]: {
|
|
50425
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
50426
|
+
outcome,
|
|
50427
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50428
|
+
}
|
|
50429
|
+
});
|
|
50430
|
+
return true;
|
|
50431
|
+
} catch {
|
|
50432
|
+
return false;
|
|
50433
|
+
}
|
|
50434
|
+
}
|
|
50435
|
+
};
|
|
50306
50436
|
};
|
|
50307
50437
|
//#endregion
|
|
50438
|
+
//#region src/cli/utils/action-upgrade-prompt.ts
|
|
50439
|
+
const store$1 = createProjectDecisionStore("actionUpgrades");
|
|
50440
|
+
store$1.getConfigPath;
|
|
50441
|
+
const hasHandledActionUpgrade = store$1.hasHandled;
|
|
50442
|
+
const recordActionUpgradeDecision = store$1.record;
|
|
50443
|
+
//#endregion
|
|
50444
|
+
//#region src/cli/utils/ci-prompt-decision.ts
|
|
50445
|
+
const store = createProjectDecisionStore("ciPrompts");
|
|
50446
|
+
store.getConfigPath;
|
|
50447
|
+
const hasHandledCiPrompt = store.hasHandled;
|
|
50448
|
+
const recordCiPromptDecision = store.record;
|
|
50449
|
+
//#endregion
|
|
50308
50450
|
//#region src/cli/utils/open-url.ts
|
|
50309
50451
|
const resolveOpenCommand = (url) => {
|
|
50310
50452
|
if (process$1.platform === "darwin") return {
|
|
@@ -50760,22 +50902,22 @@ const buildAgentHookScript = () => [
|
|
|
50760
50902
|
"",
|
|
50761
50903
|
"run_react_doctor() {",
|
|
50762
50904
|
" if [ -x ./node_modules/.bin/react-doctor ]; then",
|
|
50763
|
-
" ./node_modules/.bin/react-doctor --verbose --
|
|
50905
|
+
" ./node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score",
|
|
50764
50906
|
" return",
|
|
50765
50907
|
" fi",
|
|
50766
50908
|
"",
|
|
50767
50909
|
" if command -v react-doctor >/dev/null 2>&1; then",
|
|
50768
|
-
" react-doctor --verbose --
|
|
50910
|
+
" react-doctor --verbose --scope changed --blocking warning --no-score",
|
|
50769
50911
|
" return",
|
|
50770
50912
|
" fi",
|
|
50771
50913
|
"",
|
|
50772
50914
|
" if command -v pnpm >/dev/null 2>&1; then",
|
|
50773
|
-
" pnpm dlx react-doctor@latest --verbose --
|
|
50915
|
+
" pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score",
|
|
50774
50916
|
" return",
|
|
50775
50917
|
" fi",
|
|
50776
50918
|
"",
|
|
50777
50919
|
" if command -v npx >/dev/null 2>&1; then",
|
|
50778
|
-
" npx --yes react-doctor@latest --verbose --
|
|
50920
|
+
" npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score",
|
|
50779
50921
|
" return",
|
|
50780
50922
|
" fi",
|
|
50781
50923
|
"",
|
|
@@ -51521,10 +51663,12 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51521
51663
|
const existingWorkflow = readReactDoctorWorkflow(projectRoot);
|
|
51522
51664
|
const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
|
|
51523
51665
|
const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
|
|
51524
|
-
const
|
|
51666
|
+
const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
|
|
51667
|
+
const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
|
|
51525
51668
|
const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
|
|
51526
51669
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
51527
51670
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
51671
|
+
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
51528
51672
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
51529
51673
|
type: "multiselect",
|
|
51530
51674
|
name: "agents",
|
|
@@ -51771,18 +51915,24 @@ const handoffToAgent = async (input) => {
|
|
|
51771
51915
|
if (!input.interactive || input.diagnostics.length === 0) return;
|
|
51772
51916
|
cliLogger.break();
|
|
51773
51917
|
const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
|
|
51774
|
-
|
|
51918
|
+
const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
|
|
51919
|
+
if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
|
|
51775
51920
|
const ciOutcome = await askAddToGitHubActions();
|
|
51776
51921
|
recordCount(METRIC.agentHandoff, 1, {
|
|
51777
51922
|
outcome: `ci-${ciOutcome}`,
|
|
51778
51923
|
diagnosticsCount: input.diagnostics.length
|
|
51779
51924
|
});
|
|
51780
51925
|
if (ciOutcome === "cancel") return;
|
|
51926
|
+
recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
|
|
51781
51927
|
if (ciOutcome === "yes") {
|
|
51782
51928
|
await setUpGitHubActions({ rootDirectory: input.rootDirectory });
|
|
51783
51929
|
cliLogger.break();
|
|
51784
51930
|
}
|
|
51785
|
-
} else await maybeOfferActionUpgrade(projectRootForCi);
|
|
51931
|
+
} else if (isGitHubActionsConfigured) await maybeOfferActionUpgrade(projectRootForCi);
|
|
51932
|
+
else recordCount(METRIC.agentHandoff, 1, {
|
|
51933
|
+
outcome: "ci-suppressed",
|
|
51934
|
+
diagnosticsCount: input.diagnostics.length
|
|
51935
|
+
});
|
|
51786
51936
|
const { handoffTarget } = await prompts({
|
|
51787
51937
|
type: "select",
|
|
51788
51938
|
name: "handoffTarget",
|
|
@@ -52088,7 +52238,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52088
52238
|
yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
|
|
52089
52239
|
if (displayDiagnostics.length > 0) {
|
|
52090
52240
|
yield* log("");
|
|
52091
|
-
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender });
|
|
52241
|
+
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
|
|
52092
52242
|
}
|
|
52093
52243
|
const lowestScoredScan = findLowestScoredScan(completedScans);
|
|
52094
52244
|
const aggregateScore = lowestScoredScan?.result.score ?? null;
|
|
@@ -52126,9 +52276,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52126
52276
|
});
|
|
52127
52277
|
//#endregion
|
|
52128
52278
|
//#region src/cli/utils/prompt-install-setup.ts
|
|
52129
|
-
const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
|
|
52130
52279
|
const getSetupPromptStore = (options = {}) => new Conf({
|
|
52131
|
-
projectName:
|
|
52280
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
52132
52281
|
cwd: options.cwd
|
|
52133
52282
|
});
|
|
52134
52283
|
const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
|
|
@@ -52139,6 +52288,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
|
52139
52288
|
return false;
|
|
52140
52289
|
}
|
|
52141
52290
|
};
|
|
52291
|
+
const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
52292
|
+
try {
|
|
52293
|
+
const store = getSetupPromptStore(storeOptions);
|
|
52294
|
+
const projects = store.get("projects", {});
|
|
52295
|
+
const projectKey = getSetupPromptProjectKey(projectRoot);
|
|
52296
|
+
store.set("projects", {
|
|
52297
|
+
...projects,
|
|
52298
|
+
[projectKey]: {
|
|
52299
|
+
...projects[projectKey] ?? {},
|
|
52300
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
52301
|
+
setupPrompt: false
|
|
52302
|
+
}
|
|
52303
|
+
});
|
|
52304
|
+
return true;
|
|
52305
|
+
} catch {
|
|
52306
|
+
return false;
|
|
52307
|
+
}
|
|
52308
|
+
};
|
|
52142
52309
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
52143
52310
|
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
52144
52311
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
@@ -52545,6 +52712,14 @@ const runExplain = async (fileLineArgument, context) => {
|
|
|
52545
52712
|
const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
|
|
52546
52713
|
const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
|
|
52547
52714
|
cliLogger.log(`${severitySymbol} ${colorizedRule} ${highlighter.dim(`(${severityLabel})`)} — ${diagnostic.message}`);
|
|
52715
|
+
const codeFrame = buildCodeFrame({
|
|
52716
|
+
filePath: diagnostic.filePath,
|
|
52717
|
+
line: diagnostic.line,
|
|
52718
|
+
column: diagnostic.column,
|
|
52719
|
+
endLine: diagnostic.endLine,
|
|
52720
|
+
rootDirectory: targetDirectory
|
|
52721
|
+
});
|
|
52722
|
+
if (codeFrame) cliLogger.log(indentMultilineText(codeFrame, " "));
|
|
52548
52723
|
if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
|
|
52549
52724
|
if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
|
|
52550
52725
|
cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
|
|
@@ -52941,6 +53116,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
52941
53116
|
})) {
|
|
52942
53117
|
printAgentInstallHint();
|
|
52943
53118
|
recordCount(METRIC.agentInstallHintShown, 1);
|
|
53119
|
+
disableSetupPrompt(setupProjectRoot);
|
|
52944
53120
|
}
|
|
52945
53121
|
}
|
|
52946
53122
|
} catch (error) {
|
|
@@ -53872,7 +54048,7 @@ ${highlighter.dim("Examples:")}
|
|
|
53872
54048
|
${formatExampleLines([
|
|
53873
54049
|
["react-doctor", "scan the current project"],
|
|
53874
54050
|
["react-doctor ./apps/web", "scan a specific directory"],
|
|
53875
|
-
["react-doctor --
|
|
54051
|
+
["react-doctor --scope changed --base main", "scan only new issues vs. main"],
|
|
53876
54052
|
["react-doctor --project modules/a,modules/b", "score each module separately (names or paths)"],
|
|
53877
54053
|
["react-doctor --staged", "scan staged files (pre-commit hook)"],
|
|
53878
54054
|
["react-doctor --category Security", "show only one diagnostic category"],
|
|
@@ -53948,4 +54124,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
53948
54124
|
export {};
|
|
53949
54125
|
|
|
53950
54126
|
//# sourceMappingURL=cli.js.map
|
|
53951
|
-
//# debugId=
|
|
54127
|
+
//# debugId=ad091b20-c3e2-5c96-93ac-9a910745a035
|
package/dist/index.d.ts
CHANGED
|
@@ -9280,9 +9280,10 @@ interface ReactDoctorConfig {
|
|
|
9280
9280
|
* list, user-provided names are treated as distinctive and never
|
|
9281
9281
|
* subject to receiver-object disambiguation.
|
|
9282
9282
|
*
|
|
9283
|
-
*
|
|
9284
|
-
*
|
|
9285
|
-
*
|
|
9283
|
+
* Common guard conventions are already recognized automatically
|
|
9284
|
+
* (`requireAdmin`, `ensureSignedIn`, `getCurrentUser`, `hasRole`, …),
|
|
9285
|
+
* so this is only needed for domain-specific guards whose names carry
|
|
9286
|
+
* no auth noun — e.g. a project-local `requireWorkspaceMember`.
|
|
9286
9287
|
*/
|
|
9287
9288
|
serverAuthFunctionNames?: string[];
|
|
9288
9289
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4e83db09-9255-525f-b6fd-405784826e7d")}catch(e){}}();
|
|
3
3
|
import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import * as NFS from "node:fs";
|
|
@@ -36750,12 +36750,11 @@ const collectKnipPatterns = (rootDirectory, settingName) => {
|
|
|
36750
36750
|
if (!config) return [];
|
|
36751
36751
|
return [...normalizePatternList(config[settingName]), ...collectKnipWorkspacePatterns(config.workspaces, settingName)];
|
|
36752
36752
|
};
|
|
36753
|
-
const collectDeadCodeIgnorePatterns = (rootDirectory
|
|
36753
|
+
const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
36754
36754
|
const seen = /* @__PURE__ */ new Set();
|
|
36755
36755
|
const sources = [
|
|
36756
36756
|
readIgnoreFile(path.join(rootDirectory, ".gitignore")),
|
|
36757
36757
|
collectIgnorePatterns(rootDirectory),
|
|
36758
|
-
userConfig?.ignore?.files ?? [],
|
|
36759
36758
|
collectKnipPatterns(rootDirectory, "ignore")
|
|
36760
36759
|
];
|
|
36761
36760
|
for (const source of sources) for (const pattern of source) seen.add(pattern);
|
|
@@ -37033,11 +37032,10 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
|
|
|
37033
37032
|
});
|
|
37034
37033
|
});
|
|
37035
37034
|
const checkDeadCode = async (options) => {
|
|
37036
|
-
const { userConfig } = options;
|
|
37037
37035
|
const rootDirectory = toCanonicalPath(options.rootDirectory);
|
|
37038
37036
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
37039
37037
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
37040
|
-
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory
|
|
37038
|
+
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
37041
37039
|
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
37042
37040
|
rootDirectory,
|
|
37043
37041
|
entryPatterns,
|
|
@@ -38086,8 +38084,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
38086
38084
|
}
|
|
38087
38085
|
return enabled;
|
|
38088
38086
|
};
|
|
38089
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
|
|
38090
|
-
const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
38087
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
|
|
38088
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
38091
38089
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
38092
38090
|
const jsPlugins = [];
|
|
38093
38091
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -38784,9 +38782,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
38784
38782
|
try {
|
|
38785
38783
|
parsed = JSON.parse(sanitizedStdout);
|
|
38786
38784
|
} catch {
|
|
38787
|
-
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
38785
|
+
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
38788
38786
|
}
|
|
38789
|
-
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
38787
|
+
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
38790
38788
|
const minifiedFileCache = /* @__PURE__ */ new Map();
|
|
38791
38789
|
const isMinifiedDiagnosticFile = (filename) => {
|
|
38792
38790
|
const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
|
|
@@ -39077,6 +39075,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
39077
39075
|
NFS.closeSync(fileHandle);
|
|
39078
39076
|
}
|
|
39079
39077
|
};
|
|
39078
|
+
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
39079
|
+
/**
|
|
39080
|
+
* Detects an oxlint config-load crash caused by the optional
|
|
39081
|
+
* `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
|
|
39082
|
+
* builds the partial-failure note for it; returns `null` when the failure
|
|
39083
|
+
* was anything else.
|
|
39084
|
+
*
|
|
39085
|
+
* oxlint prints a framed error to stdout (not stderr) and exits non-zero
|
|
39086
|
+
* when a `jsPlugins` entry can't be imported; that non-JSON stdout
|
|
39087
|
+
* surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
|
|
39088
|
+
* config load on it, leaving the plugin in would drop every curated
|
|
39089
|
+
* react-doctor diagnostic too — so the caller retries with the plugin
|
|
39090
|
+
* stripped (issue #833). Both markers sit at the start of oxlint's
|
|
39091
|
+
* message, so they survive the `preview` slice even for deep pnpm paths.
|
|
39092
|
+
*/
|
|
39093
|
+
const reactHooksJsPluginDropNote = (error) => {
|
|
39094
|
+
if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
|
|
39095
|
+
const { preview } = error.reason;
|
|
39096
|
+
if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
|
|
39097
|
+
const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
|
|
39098
|
+
return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
|
|
39099
|
+
};
|
|
39080
39100
|
/**
|
|
39081
39101
|
* The oxlint runner. Composed of three pieces in `runners/oxlint/`:
|
|
39082
39102
|
*
|
|
@@ -39104,15 +39124,16 @@ const runOxlint = async (options) => {
|
|
|
39104
39124
|
const pluginPath = resolvePluginPath();
|
|
39105
39125
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
39106
39126
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
39107
|
-
const buildConfig = (
|
|
39127
|
+
const buildConfig = (overrides) => createOxlintConfig({
|
|
39108
39128
|
pluginPath,
|
|
39109
39129
|
project,
|
|
39110
39130
|
customRulesOnly,
|
|
39111
|
-
extendsPaths:
|
|
39131
|
+
extendsPaths: overrides.extendsPaths,
|
|
39112
39132
|
ignoredTags,
|
|
39113
39133
|
serverAuthFunctionNames,
|
|
39114
39134
|
severityControls,
|
|
39115
|
-
userPlugins
|
|
39135
|
+
userPlugins,
|
|
39136
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
39116
39137
|
});
|
|
39117
39138
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
39118
39139
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -39148,12 +39169,22 @@ const runOxlint = async (options) => {
|
|
|
39148
39169
|
outputMaxBytes,
|
|
39149
39170
|
concurrency: options.concurrency
|
|
39150
39171
|
});
|
|
39151
|
-
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
39172
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
39152
39173
|
try {
|
|
39153
39174
|
return await runBatches();
|
|
39154
39175
|
} catch (error) {
|
|
39176
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
39177
|
+
if (reactHooksJsDropNote !== null) {
|
|
39178
|
+
writeOxlintConfig(configPath, buildConfig({
|
|
39179
|
+
extendsPaths,
|
|
39180
|
+
disableReactHooksJsPlugin: true
|
|
39181
|
+
}));
|
|
39182
|
+
const diagnostics = await runBatches();
|
|
39183
|
+
onPartialFailure?.(reactHooksJsDropNote);
|
|
39184
|
+
return diagnostics;
|
|
39185
|
+
}
|
|
39155
39186
|
if (extendsPaths.length === 0) throw error;
|
|
39156
|
-
writeOxlintConfig(configPath, buildConfig([]));
|
|
39187
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
|
|
39157
39188
|
return await runBatches();
|
|
39158
39189
|
}
|
|
39159
39190
|
} finally {
|
|
@@ -40573,4 +40604,4 @@ const toJsonReport = (result, options) => buildJsonReport({
|
|
|
40573
40604
|
export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
40574
40605
|
|
|
40575
40606
|
//# sourceMappingURL=index.js.map
|
|
40576
|
-
//# debugId=
|
|
40607
|
+
//# debugId=4e83db09-9255-525f-b6fd-405784826e7d
|
package/dist/lsp.js
CHANGED
|
@@ -36736,12 +36736,11 @@ const collectKnipPatterns = (rootDirectory, settingName) => {
|
|
|
36736
36736
|
if (!config) return [];
|
|
36737
36737
|
return [...normalizePatternList(config[settingName]), ...collectKnipWorkspacePatterns(config.workspaces, settingName)];
|
|
36738
36738
|
};
|
|
36739
|
-
const collectDeadCodeIgnorePatterns = (rootDirectory
|
|
36739
|
+
const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
36740
36740
|
const seen = /* @__PURE__ */ new Set();
|
|
36741
36741
|
const sources = [
|
|
36742
36742
|
readIgnoreFile(path.join(rootDirectory, ".gitignore")),
|
|
36743
36743
|
collectIgnorePatterns(rootDirectory),
|
|
36744
|
-
userConfig?.ignore?.files ?? [],
|
|
36745
36744
|
collectKnipPatterns(rootDirectory, "ignore")
|
|
36746
36745
|
];
|
|
36747
36746
|
for (const source of sources) for (const pattern of source) seen.add(pattern);
|
|
@@ -37019,11 +37018,10 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
|
|
|
37019
37018
|
});
|
|
37020
37019
|
});
|
|
37021
37020
|
const checkDeadCode = async (options) => {
|
|
37022
|
-
const { userConfig } = options;
|
|
37023
37021
|
const rootDirectory = toCanonicalPath(options.rootDirectory);
|
|
37024
37022
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
37025
37023
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
37026
|
-
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory
|
|
37024
|
+
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
37027
37025
|
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
37028
37026
|
rootDirectory,
|
|
37029
37027
|
entryPatterns,
|
|
@@ -38072,8 +38070,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
38072
38070
|
}
|
|
38073
38071
|
return enabled;
|
|
38074
38072
|
};
|
|
38075
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
|
|
38076
|
-
const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
38073
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
|
|
38074
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
38077
38075
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
38078
38076
|
const jsPlugins = [];
|
|
38079
38077
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -38770,9 +38768,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
38770
38768
|
try {
|
|
38771
38769
|
parsed = JSON.parse(sanitizedStdout);
|
|
38772
38770
|
} catch {
|
|
38773
|
-
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
38771
|
+
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
38774
38772
|
}
|
|
38775
|
-
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
38773
|
+
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
38776
38774
|
const minifiedFileCache = /* @__PURE__ */ new Map();
|
|
38777
38775
|
const isMinifiedDiagnosticFile = (filename) => {
|
|
38778
38776
|
const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
|
|
@@ -39063,6 +39061,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
39063
39061
|
NFS.closeSync(fileHandle);
|
|
39064
39062
|
}
|
|
39065
39063
|
};
|
|
39064
|
+
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
39065
|
+
/**
|
|
39066
|
+
* Detects an oxlint config-load crash caused by the optional
|
|
39067
|
+
* `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
|
|
39068
|
+
* builds the partial-failure note for it; returns `null` when the failure
|
|
39069
|
+
* was anything else.
|
|
39070
|
+
*
|
|
39071
|
+
* oxlint prints a framed error to stdout (not stderr) and exits non-zero
|
|
39072
|
+
* when a `jsPlugins` entry can't be imported; that non-JSON stdout
|
|
39073
|
+
* surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
|
|
39074
|
+
* config load on it, leaving the plugin in would drop every curated
|
|
39075
|
+
* react-doctor diagnostic too — so the caller retries with the plugin
|
|
39076
|
+
* stripped (issue #833). Both markers sit at the start of oxlint's
|
|
39077
|
+
* message, so they survive the `preview` slice even for deep pnpm paths.
|
|
39078
|
+
*/
|
|
39079
|
+
const reactHooksJsPluginDropNote = (error) => {
|
|
39080
|
+
if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
|
|
39081
|
+
const { preview } = error.reason;
|
|
39082
|
+
if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
|
|
39083
|
+
const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
|
|
39084
|
+
return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
|
|
39085
|
+
};
|
|
39066
39086
|
/**
|
|
39067
39087
|
* The oxlint runner. Composed of three pieces in `runners/oxlint/`:
|
|
39068
39088
|
*
|
|
@@ -39090,15 +39110,16 @@ const runOxlint = async (options) => {
|
|
|
39090
39110
|
const pluginPath = resolvePluginPath();
|
|
39091
39111
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
39092
39112
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
39093
|
-
const buildConfig = (
|
|
39113
|
+
const buildConfig = (overrides) => createOxlintConfig({
|
|
39094
39114
|
pluginPath,
|
|
39095
39115
|
project,
|
|
39096
39116
|
customRulesOnly,
|
|
39097
|
-
extendsPaths:
|
|
39117
|
+
extendsPaths: overrides.extendsPaths,
|
|
39098
39118
|
ignoredTags,
|
|
39099
39119
|
serverAuthFunctionNames,
|
|
39100
39120
|
severityControls,
|
|
39101
|
-
userPlugins
|
|
39121
|
+
userPlugins,
|
|
39122
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
39102
39123
|
});
|
|
39103
39124
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
39104
39125
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -39134,12 +39155,22 @@ const runOxlint = async (options) => {
|
|
|
39134
39155
|
outputMaxBytes,
|
|
39135
39156
|
concurrency: options.concurrency
|
|
39136
39157
|
});
|
|
39137
|
-
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
39158
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
39138
39159
|
try {
|
|
39139
39160
|
return await runBatches();
|
|
39140
39161
|
} catch (error) {
|
|
39162
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
39163
|
+
if (reactHooksJsDropNote !== null) {
|
|
39164
|
+
writeOxlintConfig(configPath, buildConfig({
|
|
39165
|
+
extendsPaths,
|
|
39166
|
+
disableReactHooksJsPlugin: true
|
|
39167
|
+
}));
|
|
39168
|
+
const diagnostics = await runBatches();
|
|
39169
|
+
onPartialFailure?.(reactHooksJsDropNote);
|
|
39170
|
+
return diagnostics;
|
|
39171
|
+
}
|
|
39141
39172
|
if (extendsPaths.length === 0) throw error;
|
|
39142
|
-
writeOxlintConfig(configPath, buildConfig([]));
|
|
39173
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
|
|
39143
39174
|
return await runBatches();
|
|
39144
39175
|
}
|
|
39145
39176
|
} finally {
|
|
@@ -42358,5 +42389,5 @@ const startLanguageServer = () => {
|
|
|
42358
42389
|
};
|
|
42359
42390
|
//#endregion
|
|
42360
42391
|
export { startLanguageServer };
|
|
42361
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
42362
|
-
//# debugId=
|
|
42392
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="7deadb2d-94c2-54e2-bd0d-808ee8d4c380")}catch(e){}}();
|
|
42393
|
+
//# debugId=7deadb2d-94c2-54e2-bd0d-808ee8d4c380
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-doctor",
|
|
3
|
-
"version": "0.5.6-dev.
|
|
3
|
+
"version": "0.5.6-dev.937a7ca",
|
|
4
4
|
"description": "Your agent writes bad React. This catches it",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"vscode-languageserver": "^9.0.1",
|
|
65
65
|
"vscode-languageserver-textdocument": "^1.0.12",
|
|
66
66
|
"vscode-uri": "^3.1.0",
|
|
67
|
-
"oxlint-plugin-react-doctor": "0.5.6-dev.
|
|
67
|
+
"oxlint-plugin-react-doctor": "0.5.6-dev.937a7ca"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/babel__code-frame": "^7.27.0",
|