react-doctor 0.5.6-dev.81bbfcc → 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 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]="54905d6a-f06f-5f80-9fb6-7d4ab857d553")}catch(e){}}();
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";
@@ -39692,15 +39692,10 @@ const buildCapabilities = (project) => {
39692
39692
  }
39693
39693
  if (project.tailwindVersion !== null) {
39694
39694
  capabilities.add("tailwind");
39695
- const tailwind = parseTailwindMajorMinor(project.tailwindVersion);
39696
- if (isTailwindAtLeast(tailwind, {
39695
+ if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
39697
39696
  major: 3,
39698
39697
  minor: 4
39699
39698
  })) capabilities.add("tailwind:3.4");
39700
- if (tailwind !== null && isTailwindAtLeast(tailwind, {
39701
- major: 4,
39702
- minor: 0
39703
- })) capabilities.add("tailwind:4");
39704
39699
  }
39705
39700
  if (project.zodVersion !== null) {
39706
39701
  capabilities.add("zod");
@@ -41325,8 +41320,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
41325
41320
  }
41326
41321
  return enabled;
41327
41322
  };
41328
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
41329
- 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);
41330
41325
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
41331
41326
  const jsPlugins = [];
41332
41327
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -42023,9 +42018,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
42023
42018
  try {
42024
42019
  parsed = JSON.parse(sanitizedStdout);
42025
42020
  } catch {
42026
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
42021
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
42027
42022
  }
42028
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
42023
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
42029
42024
  const minifiedFileCache = /* @__PURE__ */ new Map();
42030
42025
  const isMinifiedDiagnosticFile = (filename) => {
42031
42026
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -42316,6 +42311,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
42316
42311
  NFS.closeSync(fileHandle);
42317
42312
  }
42318
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
+ };
42319
42336
  /**
42320
42337
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
42321
42338
  *
@@ -42343,15 +42360,16 @@ const runOxlint = async (options) => {
42343
42360
  const pluginPath = resolvePluginPath();
42344
42361
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
42345
42362
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
42346
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
42363
+ const buildConfig = (overrides) => createOxlintConfig({
42347
42364
  pluginPath,
42348
42365
  project,
42349
42366
  customRulesOnly,
42350
- extendsPaths: extendsForThisAttempt,
42367
+ extendsPaths: overrides.extendsPaths,
42351
42368
  ignoredTags,
42352
42369
  serverAuthFunctionNames,
42353
42370
  severityControls,
42354
- userPlugins
42371
+ userPlugins,
42372
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
42355
42373
  });
42356
42374
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
42357
42375
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -42387,12 +42405,22 @@ const runOxlint = async (options) => {
42387
42405
  outputMaxBytes,
42388
42406
  concurrency: options.concurrency
42389
42407
  });
42390
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
42408
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
42391
42409
  try {
42392
42410
  return await runBatches();
42393
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
+ }
42394
42422
  if (extendsPaths.length === 0) throw error;
42395
- writeOxlintConfig(configPath, buildConfig([]));
42423
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
42396
42424
  return await runBatches();
42397
42425
  }
42398
42426
  } finally {
@@ -43844,7 +43872,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
43844
43872
  "false"
43845
43873
  ]);
43846
43874
  const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
43847
- const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(process.env[environmentVariable])) || isCiFlagSet(process.env.CI);
43875
+ const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
43848
43876
  const detectCiProvider = () => {
43849
43877
  for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
43850
43878
  return isCiFlagSet(process.env.CI) ? "unknown" : null;
@@ -43869,6 +43897,42 @@ const detectCodingAgent = () => {
43869
43897
  const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
43870
43898
  const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
43871
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
43872
43936
  //#region src/cli/utils/is-git-hook-environment.ts
43873
43937
  const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
43874
43938
  //#endregion
@@ -43891,6 +43955,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
43891
43955
  const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
43892
43956
  //#endregion
43893
43957
  //#region src/cli/utils/constants.ts
43958
+ const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
43894
43959
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
43895
43960
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
43896
43961
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
@@ -43975,7 +44040,7 @@ const makeNoopConsole = () => ({
43975
44040
  });
43976
44041
  //#endregion
43977
44042
  //#region src/cli/utils/version.ts
43978
- const VERSION = "0.5.6-dev.81bbfcc";
44043
+ const VERSION = "0.5.6-dev.937a7ca";
43979
44044
  //#endregion
43980
44045
  //#region src/cli/utils/json-mode.ts
43981
44046
  let context = null;
@@ -44125,6 +44190,7 @@ const buildRunContext = () => {
44125
44190
  viaAction: isOfficialGithubAction(),
44126
44191
  codingAgent: detectCodingAgent(),
44127
44192
  interactive: !isNonInteractiveEnvironment(),
44193
+ terminalKind: detectTerminalKind(),
44128
44194
  jsonMode: isJsonModeActive(),
44129
44195
  invokedVia: detectInvokedVia()
44130
44196
  };
@@ -44195,6 +44261,7 @@ const buildSentryScope = (runContext = buildRunContext()) => {
44195
44261
  viaAction: runContext.viaAction,
44196
44262
  codingAgent: runContext.codingAgent,
44197
44263
  interactive: runContext.interactive,
44264
+ terminalKind: runContext.terminalKind,
44198
44265
  jsonMode: runContext.jsonMode,
44199
44266
  invokedVia: runContext.invokedVia,
44200
44267
  nodeMajor: runContext.nodeMajor
@@ -44333,13 +44400,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
44333
44400
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
44334
44401
  * standard `SENTRY_RELEASE` override.
44335
44402
  */
44336
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.81bbfcc`;
44403
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.937a7ca`;
44337
44404
  /**
44338
44405
  * Deployment environment shown in Sentry's environment filter. Defaults to
44339
44406
  * `production` for tagged releases and `development` for dev/unbuilt versions,
44340
44407
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
44341
44408
  */
44342
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.81bbfcc") ? "development" : "production");
44409
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.937a7ca") ? "development" : "production");
44343
44410
  /**
44344
44411
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
44345
44412
  * (set to `0` to disable tracing) and falls back to
@@ -48196,6 +48263,15 @@ const boxText = (content, innerWidth) => {
48196
48263
  ].join("\n");
48197
48264
  };
48198
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
48199
48275
  //#region src/cli/utils/build-code-frame.ts
48200
48276
  /**
48201
48277
  * Renders a syntax-highlighted source excerpt around a diagnostic site
@@ -48206,7 +48282,7 @@ const boxText = (content, innerWidth) => {
48206
48282
  */
48207
48283
  const buildCodeFrame = (input) => {
48208
48284
  if (input.line <= 0) return null;
48209
- const absolutePath = Path.isAbsolute(input.filePath) ? input.filePath : Path.resolve(input.rootDirectory || ".", input.filePath);
48285
+ const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
48210
48286
  let source;
48211
48287
  try {
48212
48288
  source = NFS.readFileSync(absolutePath, "utf8");
@@ -48246,6 +48322,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
48246
48322
  const DIVIDER_INDENT = " ";
48247
48323
  const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
48248
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
48249
48335
  //#region src/cli/utils/indent-multiline-text.ts
48250
48336
  const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
48251
48337
  //#endregion
@@ -48399,17 +48485,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
48399
48485
  }
48400
48486
  return clusters;
48401
48487
  };
48402
- const formatClusterLocation = (cluster) => {
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) => {
48403
48495
  const lead = cluster.diagnostics[0];
48404
48496
  const contextTag = formatFileContextTag(lead);
48405
- if (cluster.startLine <= 0) return `${lead.filePath}${contextTag}`;
48406
- if (cluster.endLine > cluster.startLine) return `${lead.filePath}:${cluster.startLine}-${cluster.endLine}${contextTag}`;
48407
- return `${lead.filePath}:${cluster.startLine}${contextTag}`;
48497
+ const location = formatClusterLocationText(cluster);
48498
+ if (!hyperlinks) return `${location}${contextTag}`;
48499
+ return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
48408
48500
  };
48409
- const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
48501
+ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
48410
48502
  const lead = cluster.diagnostics[0];
48411
48503
  const isMultiSite = cluster.diagnostics.length > 1;
48412
- const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
48504
+ const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
48413
48505
  const codeFrame = renderCodeFrame ? buildCodeFrame({
48414
48506
  filePath: lead.filePath,
48415
48507
  line: cluster.startLine,
@@ -48428,7 +48520,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
48428
48520
  }
48429
48521
  return lines;
48430
48522
  };
48431
- const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
48523
+ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
48432
48524
  const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
48433
48525
  const { severity } = representative;
48434
48526
  const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
@@ -48448,7 +48540,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
48448
48540
  }
48449
48541
  const renderCodeFrame = severity === "error";
48450
48542
  const sites = renderEverySite ? ruleDiagnostics : [representative];
48451
- 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));
48452
48544
  return lines;
48453
48545
  };
48454
48546
  const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
@@ -48461,7 +48553,7 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
48461
48553
  return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
48462
48554
  };
48463
48555
  const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
48464
- const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
48556
+ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
48465
48557
  const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
48466
48558
  if (topRuleGroups.length === 0) return {
48467
48559
  lines: [],
@@ -48471,7 +48563,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
48471
48563
  const blockOffsets = [];
48472
48564
  for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
48473
48565
  blockOffsets.push(lines.length);
48474
- lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
48566
+ lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
48475
48567
  lines.push("");
48476
48568
  }
48477
48569
  return {
@@ -48509,18 +48601,18 @@ const buildOverviewHeaderLines = (diagnostics) => {
48509
48601
  * single Effect.forEach over Console.log so failures or fiber
48510
48602
  * interruption produce predictable partial output.
48511
48603
  */
48512
- 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* () {
48513
48605
  const sectionPause = onboarding.sectionPause ?? void_;
48514
48606
  const animateCountUp = onboarding.animateCountUp ?? false;
48515
48607
  const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
48516
48608
  let detailLines;
48517
48609
  let topErrorBlockOffsets = [];
48518
48610
  if (!isVerbose) {
48519
- const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
48611
+ const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
48520
48612
  detailLines = topErrors.lines;
48521
48613
  topErrorBlockOffsets = topErrors.blockOffsets;
48522
48614
  } else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
48523
- return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
48615
+ return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
48524
48616
  });
48525
48617
  const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
48526
48618
  const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
@@ -48581,6 +48673,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
48581
48673
  //#endregion
48582
48674
  //#region src/cli/utils/filter-diagnostics-by-categories.ts
48583
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();
48584
48718
  const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
48585
48719
  const FALSY_FLAG_VALUES = new Set([
48586
48720
  "",
@@ -48600,10 +48734,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
48600
48734
  };
48601
48735
  //#endregion
48602
48736
  //#region src/cli/utils/onboarding-state.ts
48603
- const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
48604
48737
  const ONBOARDED_AT_KEY = "onboardedAt";
48605
48738
  const getOnboardingStore = (options = {}) => new Conf({
48606
- projectName: GLOBAL_CONFIG_PROJECT_NAME$2,
48739
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
48607
48740
  cwd: options.cwd
48608
48741
  });
48609
48742
  const hasCompletedOnboarding = (options = {}) => {
@@ -49679,6 +49812,7 @@ const finalizeAndRender = (input) => gen(function* () {
49679
49812
  }
49680
49813
  const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
49681
49814
  const pause = onboardingSectionPause(animateRender);
49815
+ const useHyperlinks = shouldRenderHyperlinks(process.stdout);
49682
49816
  const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
49683
49817
  const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
49684
49818
  if (printedDiagnostics.length === 0) {
@@ -49704,7 +49838,7 @@ const finalizeAndRender = (input) => gen(function* () {
49704
49838
  yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
49705
49839
  sectionPause: pause,
49706
49840
  animateCountUp: animateRender
49707
- });
49841
+ }, useHyperlinks);
49708
49842
  if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
49709
49843
  if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
49710
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.`));
@@ -49897,16 +50031,7 @@ const buildHandoffPayload = (input) => {
49897
50031
  try {
49898
50032
  outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
49899
50033
  } catch {}
49900
- const lines = [
49901
- "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:",
49902
- `- It's used by teams at ${CI_TRUST_COMPANIES}.`,
49903
- "- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
49904
- `- Setup guide: ${CI_URL}`,
49905
- "If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
49906
- "",
49907
- `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.`,
49908
- ""
49909
- ];
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.`, ""];
49910
50035
  topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
49911
50036
  const representative = ruleDiagnostics[0];
49912
50037
  const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
@@ -50276,38 +50401,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
50276
50401
  //#region src/cli/utils/hash-project-root.ts
50277
50402
  const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
50278
50403
  //#endregion
50279
- //#region src/cli/utils/action-upgrade-prompt.ts
50280
- const GLOBAL_CONFIG_PROJECT_NAME$1 = "react-doctor";
50281
- const getActionUpgradeStore = (options = {}) => new Conf({
50282
- projectName: GLOBAL_CONFIG_PROJECT_NAME$1,
50283
- cwd: options.cwd
50284
- });
50285
- const hasHandledActionUpgrade = (projectRoot, storeOptions = {}) => {
50286
- try {
50287
- const upgrades = getActionUpgradeStore(storeOptions).get("actionUpgrades", {});
50288
- return Boolean(upgrades[hashProjectRoot(projectRoot)]);
50289
- } catch {
50290
- return true;
50291
- }
50292
- };
50293
- const recordActionUpgradeDecision = (projectRoot, outcome, storeOptions = {}) => {
50294
- try {
50295
- const store = getActionUpgradeStore(storeOptions);
50296
- const upgrades = store.get("actionUpgrades", {});
50297
- store.set("actionUpgrades", {
50298
- ...upgrades,
50299
- [hashProjectRoot(projectRoot)]: {
50300
- rootDirectory: Path.resolve(projectRoot),
50301
- outcome,
50302
- 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;
50303
50417
  }
50304
- });
50305
- return true;
50306
- } catch {
50307
- return false;
50308
- }
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
+ };
50309
50436
  };
50310
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
50311
50450
  //#region src/cli/utils/open-url.ts
50312
50451
  const resolveOpenCommand = (url) => {
50313
50452
  if (process$1.platform === "darwin") return {
@@ -51524,10 +51663,12 @@ const runInstallReactDoctor = async (options = {}) => {
51524
51663
  const existingWorkflow = readReactDoctorWorkflow(projectRoot);
51525
51664
  const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
51526
51665
  const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
51527
- const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || !skipPrompts && await askAddToGitHubActions(prompt) === "yes");
51666
+ const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
51667
+ const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
51528
51668
  const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
51529
51669
  const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
51530
51670
  if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
51671
+ if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
51531
51672
  const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
51532
51673
  type: "multiselect",
51533
51674
  name: "agents",
@@ -51774,18 +51915,24 @@ const handoffToAgent = async (input) => {
51774
51915
  if (!input.interactive || input.diagnostics.length === 0) return;
51775
51916
  cliLogger.break();
51776
51917
  const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
51777
- if (!isReactDoctorWorkflowInstalled(projectRootForCi)) {
51918
+ const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
51919
+ if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
51778
51920
  const ciOutcome = await askAddToGitHubActions();
51779
51921
  recordCount(METRIC.agentHandoff, 1, {
51780
51922
  outcome: `ci-${ciOutcome}`,
51781
51923
  diagnosticsCount: input.diagnostics.length
51782
51924
  });
51783
51925
  if (ciOutcome === "cancel") return;
51926
+ recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
51784
51927
  if (ciOutcome === "yes") {
51785
51928
  await setUpGitHubActions({ rootDirectory: input.rootDirectory });
51786
51929
  cliLogger.break();
51787
51930
  }
51788
- } 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
+ });
51789
51936
  const { handoffTarget } = await prompts({
51790
51937
  type: "select",
51791
51938
  name: "handoffTarget",
@@ -52091,7 +52238,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
52091
52238
  yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
52092
52239
  if (displayDiagnostics.length > 0) {
52093
52240
  yield* log("");
52094
- 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));
52095
52242
  }
52096
52243
  const lowestScoredScan = findLowestScoredScan(completedScans);
52097
52244
  const aggregateScore = lowestScoredScan?.result.score ?? null;
@@ -52129,9 +52276,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
52129
52276
  });
52130
52277
  //#endregion
52131
52278
  //#region src/cli/utils/prompt-install-setup.ts
52132
- const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
52133
52279
  const getSetupPromptStore = (options = {}) => new Conf({
52134
- projectName: GLOBAL_CONFIG_PROJECT_NAME,
52280
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
52135
52281
  cwd: options.cwd
52136
52282
  });
52137
52283
  const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
@@ -52142,6 +52288,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
52142
52288
  return false;
52143
52289
  }
52144
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
+ };
52145
52309
  const resolveInstallSetupProjectRoot = (options) => {
52146
52310
  if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
52147
52311
  const packageDirectories = /* @__PURE__ */ new Set();
@@ -52548,6 +52712,14 @@ const runExplain = async (fileLineArgument, context) => {
52548
52712
  const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
52549
52713
  const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
52550
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, " "));
52551
52723
  if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
52552
52724
  if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
52553
52725
  cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
@@ -52944,6 +53116,7 @@ const inspectAction = async (directory, flags) => {
52944
53116
  })) {
52945
53117
  printAgentInstallHint();
52946
53118
  recordCount(METRIC.agentInstallHintShown, 1);
53119
+ disableSetupPrompt(setupProjectRoot);
52947
53120
  }
52948
53121
  }
52949
53122
  } catch (error) {
@@ -53951,4 +54124,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
53951
54124
  export {};
53952
54125
 
53953
54126
  //# sourceMappingURL=cli.js.map
53954
- //# debugId=54905d6a-f06f-5f80-9fb6-7d4ab857d553
54127
+ //# debugId=ad091b20-c3e2-5c96-93ac-9a910745a035
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]="b4f6f7e7-99db-553a-aa15-14b646162f39")}catch(e){}}();
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";
@@ -36472,15 +36472,10 @@ const buildCapabilities = (project) => {
36472
36472
  }
36473
36473
  if (project.tailwindVersion !== null) {
36474
36474
  capabilities.add("tailwind");
36475
- const tailwind = parseTailwindMajorMinor(project.tailwindVersion);
36476
- if (isTailwindAtLeast(tailwind, {
36475
+ if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
36477
36476
  major: 3,
36478
36477
  minor: 4
36479
36478
  })) capabilities.add("tailwind:3.4");
36480
- if (tailwind !== null && isTailwindAtLeast(tailwind, {
36481
- major: 4,
36482
- minor: 0
36483
- })) capabilities.add("tailwind:4");
36484
36479
  }
36485
36480
  if (project.zodVersion !== null) {
36486
36481
  capabilities.add("zod");
@@ -38089,8 +38084,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38089
38084
  }
38090
38085
  return enabled;
38091
38086
  };
38092
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
38093
- 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);
38094
38089
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38095
38090
  const jsPlugins = [];
38096
38091
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -38787,9 +38782,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38787
38782
  try {
38788
38783
  parsed = JSON.parse(sanitizedStdout);
38789
38784
  } catch {
38790
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38785
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38791
38786
  }
38792
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38787
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38793
38788
  const minifiedFileCache = /* @__PURE__ */ new Map();
38794
38789
  const isMinifiedDiagnosticFile = (filename) => {
38795
38790
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -39080,6 +39075,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39080
39075
  NFS.closeSync(fileHandle);
39081
39076
  }
39082
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
+ };
39083
39100
  /**
39084
39101
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
39085
39102
  *
@@ -39107,15 +39124,16 @@ const runOxlint = async (options) => {
39107
39124
  const pluginPath = resolvePluginPath();
39108
39125
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
39109
39126
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
39110
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
39127
+ const buildConfig = (overrides) => createOxlintConfig({
39111
39128
  pluginPath,
39112
39129
  project,
39113
39130
  customRulesOnly,
39114
- extendsPaths: extendsForThisAttempt,
39131
+ extendsPaths: overrides.extendsPaths,
39115
39132
  ignoredTags,
39116
39133
  serverAuthFunctionNames,
39117
39134
  severityControls,
39118
- userPlugins
39135
+ userPlugins,
39136
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39119
39137
  });
39120
39138
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39121
39139
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -39151,12 +39169,22 @@ const runOxlint = async (options) => {
39151
39169
  outputMaxBytes,
39152
39170
  concurrency: options.concurrency
39153
39171
  });
39154
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
39172
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39155
39173
  try {
39156
39174
  return await runBatches();
39157
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
+ }
39158
39186
  if (extendsPaths.length === 0) throw error;
39159
- writeOxlintConfig(configPath, buildConfig([]));
39187
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
39160
39188
  return await runBatches();
39161
39189
  }
39162
39190
  } finally {
@@ -40576,4 +40604,4 @@ const toJsonReport = (result, options) => buildJsonReport({
40576
40604
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
40577
40605
 
40578
40606
  //# sourceMappingURL=index.js.map
40579
- //# debugId=b4f6f7e7-99db-553a-aa15-14b646162f39
40607
+ //# debugId=4e83db09-9255-525f-b6fd-405784826e7d
package/dist/lsp.js CHANGED
@@ -36458,15 +36458,10 @@ const buildCapabilities = (project) => {
36458
36458
  }
36459
36459
  if (project.tailwindVersion !== null) {
36460
36460
  capabilities.add("tailwind");
36461
- const tailwind = parseTailwindMajorMinor(project.tailwindVersion);
36462
- if (isTailwindAtLeast(tailwind, {
36461
+ if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
36463
36462
  major: 3,
36464
36463
  minor: 4
36465
36464
  })) capabilities.add("tailwind:3.4");
36466
- if (tailwind !== null && isTailwindAtLeast(tailwind, {
36467
- major: 4,
36468
- minor: 0
36469
- })) capabilities.add("tailwind:4");
36470
36465
  }
36471
36466
  if (project.zodVersion !== null) {
36472
36467
  capabilities.add("zod");
@@ -38075,8 +38070,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38075
38070
  }
38076
38071
  return enabled;
38077
38072
  };
38078
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
38079
- 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);
38080
38075
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38081
38076
  const jsPlugins = [];
38082
38077
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -38773,9 +38768,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38773
38768
  try {
38774
38769
  parsed = JSON.parse(sanitizedStdout);
38775
38770
  } catch {
38776
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38771
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38777
38772
  }
38778
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38773
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38779
38774
  const minifiedFileCache = /* @__PURE__ */ new Map();
38780
38775
  const isMinifiedDiagnosticFile = (filename) => {
38781
38776
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -39066,6 +39061,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39066
39061
  NFS.closeSync(fileHandle);
39067
39062
  }
39068
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
+ };
39069
39086
  /**
39070
39087
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
39071
39088
  *
@@ -39093,15 +39110,16 @@ const runOxlint = async (options) => {
39093
39110
  const pluginPath = resolvePluginPath();
39094
39111
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
39095
39112
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
39096
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
39113
+ const buildConfig = (overrides) => createOxlintConfig({
39097
39114
  pluginPath,
39098
39115
  project,
39099
39116
  customRulesOnly,
39100
- extendsPaths: extendsForThisAttempt,
39117
+ extendsPaths: overrides.extendsPaths,
39101
39118
  ignoredTags,
39102
39119
  serverAuthFunctionNames,
39103
39120
  severityControls,
39104
- userPlugins
39121
+ userPlugins,
39122
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39105
39123
  });
39106
39124
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39107
39125
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -39137,12 +39155,22 @@ const runOxlint = async (options) => {
39137
39155
  outputMaxBytes,
39138
39156
  concurrency: options.concurrency
39139
39157
  });
39140
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
39158
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39141
39159
  try {
39142
39160
  return await runBatches();
39143
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
+ }
39144
39172
  if (extendsPaths.length === 0) throw error;
39145
- writeOxlintConfig(configPath, buildConfig([]));
39173
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
39146
39174
  return await runBatches();
39147
39175
  }
39148
39176
  } finally {
@@ -42361,5 +42389,5 @@ const startLanguageServer = () => {
42361
42389
  };
42362
42390
  //#endregion
42363
42391
  export { startLanguageServer };
42364
- !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]="9421149c-0ed9-5e00-81da-4f7dd8faf332")}catch(e){}}();
42365
- //# debugId=9421149c-0ed9-5e00-81da-4f7dd8faf332
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.81bbfcc",
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.81bbfcc"
67
+ "oxlint-plugin-react-doctor": "0.5.6-dev.937a7ca"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/babel__code-frame": "^7.27.0",
@@ -72,9 +72,9 @@
72
72
  "@xterm/headless": "^6.0.0",
73
73
  "commander": "^14.0.3",
74
74
  "ora": "^9.4.0",
75
- "@react-doctor/core": "0.5.6",
75
+ "@react-doctor/api": "0.5.6",
76
76
  "@react-doctor/language-server": "0.5.6",
77
- "@react-doctor/api": "0.5.6"
77
+ "@react-doctor/core": "0.5.6"
78
78
  },
79
79
  "engines": {
80
80
  "node": "^20.19.0 || >=22.13.0"