react-doctor 0.7.6-dev.e141d90 → 0.7.6-dev.e877ca7

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]="9a5c1a3c-7706-5a23-bca4-ddddd2f085af")}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]="63b8a227-543e-5501-a998-58c03a1fb10e")}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";
@@ -36341,12 +36341,10 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
36341
36341
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
36342
36342
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
36343
36343
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
36344
- const UTF8_BOM_CHARACTER = "";
36345
- const stripUtf8Bom = (content) => content.startsWith(UTF8_BOM_CHARACTER) ? content.slice(1) : content;
36346
36344
  const cachedPackageJsons = /* @__PURE__ */ new Map();
36347
36345
  const readPackageJsonUncached = (packageJsonPath) => {
36348
36346
  try {
36349
- return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
36347
+ return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
36350
36348
  } catch (error) {
36351
36349
  if (error instanceof SyntaxError) return {};
36352
36350
  if (isErrnoException(error)) {
@@ -40019,6 +40017,7 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
40019
40017
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
40020
40018
  const PACKAGE_JSON_FILE = "package.json";
40021
40019
  const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
40020
+ const UTF8_BOM_CHAR = "";
40022
40021
  const HARDENING_SETTING_KEYS = new Set([
40023
40022
  "minimumReleaseAge",
40024
40023
  "blockExoticSubdeps",
@@ -40043,11 +40042,12 @@ const stripInlineComment = (rawValue) => {
40043
40042
  return rawValue;
40044
40043
  };
40045
40044
  const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
40045
+ const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
40046
40046
  const parseHardeningSettings = (content) => {
40047
40047
  let minimumReleaseAge = null;
40048
40048
  let blockExoticSubdeps = null;
40049
40049
  let trustPolicy = null;
40050
- const lines = stripUtf8Bom(content).split(/\r?\n/);
40050
+ const lines = stripBom(content).split(/\r?\n/);
40051
40051
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
40052
40052
  const lineText = lines[lineIndex];
40053
40053
  if (lineText === void 0) continue;
@@ -40081,7 +40081,7 @@ const isPnpmManagedProject = (rootDirectory) => {
40081
40081
  const packageJsonPath = Path.join(rootDirectory, PACKAGE_JSON_FILE);
40082
40082
  if (!isFile(packageJsonPath)) return false;
40083
40083
  try {
40084
- const packageJsonRaw = stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8"));
40084
+ const packageJsonRaw = NFS.readFileSync(packageJsonPath, "utf-8");
40085
40085
  const packageJson = JSON.parse(packageJsonRaw);
40086
40086
  if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
40087
40087
  } catch {
@@ -40762,7 +40762,9 @@ const setColorEnabled = (enabled) => {
40762
40762
  highlighter.gray = colors.gray;
40763
40763
  highlighter.bold = colors.bold;
40764
40764
  };
40765
- const computeExplicitLintIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter(isLintableSourceFile) : void 0;
40765
+ const NEXT_ENTRY_FILE_PATTERN = /^(?:\.\/)?(?:src\/)?(?:middleware|proxy)\.(?:tsx?|jsx?|mts|mjs)$/;
40766
+ const shouldIncludeExplicitLintPath = (filePath, project) => JSX_FILE_PATTERN.test(filePath) || project?.framework === "nextjs" && NEXT_ENTRY_FILE_PATTERN.test(filePath);
40767
+ const computeExplicitLintIncludePaths = (includePaths, project) => includePaths.length > 0 ? includePaths.filter((filePath) => shouldIncludeExplicitLintPath(filePath, project)) : void 0;
40766
40768
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
40767
40769
  const FALSY_VALUES = new Set([
40768
40770
  "false",
@@ -41601,11 +41603,11 @@ const resolveDeadCodeTimeout = (input) => {
41601
41603
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
41602
41604
  };
41603
41605
  };
41604
- const resolveLintIncludePaths = (rootDirectory, userConfig) => {
41606
+ const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
41605
41607
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
41606
41608
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
41607
41609
  return listSourceFiles(rootDirectory).filter((filePath) => {
41608
- if (!isLintableSourceFile(filePath)) return false;
41610
+ if (!shouldIncludeExplicitLintPath(filePath, project)) return false;
41609
41611
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
41610
41612
  });
41611
41613
  };
@@ -42886,12 +42888,6 @@ const mapWithConcurrency = async (items, concurrency, task) => {
42886
42888
  if (errors.length > 0) throw errors[0];
42887
42889
  return results;
42888
42890
  };
42889
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
42890
- let lineNumber = 1;
42891
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
42892
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === 10) lineNumber++;
42893
- return lineNumber;
42894
- };
42895
42891
  const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
42896
42892
  const ACCESSORS_MIN_VERSION = "3.15.0";
42897
42893
  const supportsAccessors = (reanimatedVersion) => {
@@ -43463,6 +43459,13 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
43463
43459
  visit(sourceFile);
43464
43460
  return foundRange;
43465
43461
  };
43462
+ const NEWLINE_BYTE = 10;
43463
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
43464
+ let lineNumber = 1;
43465
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
43466
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
43467
+ return lineNumber;
43468
+ };
43466
43469
  /**
43467
43470
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
43468
43471
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -43667,33 +43670,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43667
43670
  }
43668
43671
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
43669
43672
  const minifiedFileCache = /* @__PURE__ */ new Map();
43670
- const sourceBufferCache = /* @__PURE__ */ new Map();
43671
- const sourceLinesCache = /* @__PURE__ */ new Map();
43672
- const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
43673
- const readSourceBuffer = (filename) => {
43674
- const absolutePath = resolveAbsolutePath(filename);
43675
- const cached = sourceBufferCache.get(absolutePath);
43676
- if (cached !== void 0) return cached;
43677
- let sourceBuffer;
43678
- try {
43679
- sourceBuffer = NFS.readFileSync(absolutePath);
43680
- } catch {
43681
- sourceBuffer = null;
43682
- }
43683
- sourceBufferCache.set(absolutePath, sourceBuffer);
43684
- return sourceBuffer;
43685
- };
43686
- const readSourceLines = (filename) => {
43687
- const absolutePath = resolveAbsolutePath(filename);
43688
- const cached = sourceLinesCache.get(absolutePath);
43689
- if (cached !== void 0) return cached;
43690
- const sourceBuffer = readSourceBuffer(filename);
43691
- const sourceLines = sourceBuffer ? sourceBuffer.toString("utf8").split("\n") : null;
43692
- sourceLinesCache.set(absolutePath, sourceLines);
43693
- return sourceLines;
43694
- };
43695
43673
  const isMinifiedDiagnosticFile = (filename) => {
43696
- const absolutePath = resolveAbsolutePath(filename);
43674
+ const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
43697
43675
  const cached = minifiedFileCache.get(absolutePath);
43698
43676
  if (cached !== void 0) return cached;
43699
43677
  const minified = isMinifiedSource(absolutePath);
@@ -43706,17 +43684,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43706
43684
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
43707
43685
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
43708
43686
  const primarySpan = primaryLabel?.span;
43709
- const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
43710
43687
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
43711
43688
  const category = resolveDiagnosticCategory(plugin, rule);
43712
- const matchByOccurrence = resolveMatchByOccurrence(rule, category);
43713
- const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
43714
- const primaryLineIndex = primarySpan ? primarySpan.line - 1 : -1;
43715
- const primaryLine = sourceLines?.[primaryLineIndex];
43716
- const primaryColumnIndex = primarySpan ? primarySpan.column - 1 : -1;
43717
- const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
43718
- const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
43719
- const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
43720
43689
  return {
43721
43690
  filePath: normalizedFilePath,
43722
43691
  plugin,
@@ -43730,11 +43699,10 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43730
43699
  column: primarySpan?.column ?? 0,
43731
43700
  ...primarySpan ? {
43732
43701
  offset: primarySpan.offset,
43733
- length: primarySpan.length,
43734
- ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
43702
+ length: primarySpan.length
43735
43703
  } : {},
43736
43704
  category,
43737
- ...matchByOccurrence ? { matchByOccurrence: true } : {},
43705
+ ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
43738
43706
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
43739
43707
  };
43740
43708
  }), rootDirectory);
@@ -45521,7 +45489,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
45521
45489
  directory: scanDirectory,
45522
45490
  repo
45523
45491
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
45524
- const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
45492
+ const lintIncludePaths = (input.skipJsxIncludeFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths], project)) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, project);
45525
45493
  const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
45526
45494
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
45527
45495
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
@@ -46699,7 +46667,7 @@ const makeNoopConsole = () => ({
46699
46667
  });
46700
46668
  //#endregion
46701
46669
  //#region src/cli/utils/version.ts
46702
- const VERSION = "0.7.6-dev.e141d90";
46670
+ const VERSION = "0.7.6-dev.e877ca7";
46703
46671
  //#endregion
46704
46672
  //#region src/cli/utils/json-mode.ts
46705
46673
  let context = null;
@@ -47068,13 +47036,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
47068
47036
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
47069
47037
  * standard `SENTRY_RELEASE` override.
47070
47038
  */
47071
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.6-dev.e141d90`;
47039
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.6-dev.e877ca7`;
47072
47040
  /**
47073
47041
  * Deployment environment shown in Sentry's environment filter. Defaults to
47074
47042
  * `production` for tagged releases and `development` for dev/unbuilt versions,
47075
47043
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
47076
47044
  */
47077
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.6-dev.e141d90") ? "development" : "production");
47045
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.6-dev.e877ca7") ? "development" : "production");
47078
47046
  /**
47079
47047
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
47080
47048
  * (set to `0` to disable tracing) and falls back to
@@ -50839,7 +50807,7 @@ const BLOCKING_SUMMARY = {
50839
50807
  const SCOPE_SUMMARY = {
50840
50808
  full: "Scan the whole project on every run",
50841
50809
  files: "Report every issue in changed files",
50842
- lines: "Report issues whose source spans touch changed lines",
50810
+ lines: "Report issues on changed lines",
50843
50811
  changed: "Report only the issues a change introduces"
50844
50812
  };
50845
50813
  const gatesEqual = (left, right) => left.blocking === right.blocking && left.scope === right.scope && left.comment === right.comment && left.reviewComments === right.reviewComments && left.commitStatus === right.commitStatus;
@@ -59234,7 +59202,7 @@ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
59234
59202
  return null;
59235
59203
  }
59236
59204
  if (!stats.isFile()) return null;
59237
- if (stats.size > 10485760) return null;
59205
+ if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
59238
59206
  return hashFileContents(absolutePath);
59239
59207
  };
59240
59208
  const buildWorktreeFingerprint = (projectDirectory) => {
@@ -59708,9 +59676,7 @@ const buildScanAttributes = (input) => {
59708
59676
  hasCustomConfig: input.hasCustomConfig,
59709
59677
  rulesConfigured: ruleKeys.length,
59710
59678
  rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
59711
- fileCount: input.result?.scannedFileCount ?? null,
59712
- nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length ?? null,
59713
- multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length ?? null
59679
+ fileCount: input.result?.scannedFileCount ?? null
59714
59680
  });
59715
59681
  };
59716
59682
  /**
@@ -59779,12 +59745,6 @@ const countDeadlineSkippedFiles = (lintPartialFailures) => sumFileCountsMatching
59779
59745
  const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
59780
59746
  const countDroppedLintFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DROPPED_FILES_MESSAGE_PATTERN);
59781
59747
  //#endregion
59782
- //#region src/cli/utils/diagnostic-intersects-line-ranges.ts
59783
- const diagnosticIntersectsLineRanges = (diagnostic, lineRanges) => {
59784
- const diagnosticEndLine = Math.max(diagnostic.line, diagnostic.endLine ?? diagnostic.line);
59785
- return lineRanges.some(([rangeStartLine, rangeEndLine]) => diagnostic.line <= rangeEndLine && diagnosticEndLine >= rangeStartLine);
59786
- };
59787
- //#endregion
59788
59748
  //#region src/cli/utils/materialize-baseline-files.ts
59789
59749
  /**
59790
59750
  * Materializes the `ref` version of `files` into `tempDirectory` (via
@@ -60970,7 +60930,7 @@ const buildChangedLineMatcher = (directory, changedLineRanges) => {
60970
60930
  const relativePath = toForwardSlashes(path.isAbsolute(diagnostic.filePath) ? path.relative(directory, diagnostic.filePath) : diagnostic.filePath);
60971
60931
  const ranges = rangesByFile.get(relativePath);
60972
60932
  if (ranges === void 0) return false;
60973
- return diagnosticIntersectsLineRanges(diagnostic, ranges);
60933
+ return ranges.some(([start, end]) => diagnostic.line >= start && diagnostic.line <= end);
60974
60934
  };
60975
60935
  };
60976
60936
  const buildIgnoredTags = (userConfig) => {
@@ -65221,7 +65181,7 @@ ${highlighter.dim("Learn more:")}
65221
65181
  ${highlighter.info(CI_URL)}
65222
65182
  `;
65223
65183
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
65224
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much supported JS/TS source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
65184
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
65225
65185
  program.action(inspectAction);
65226
65186
  program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
65227
65187
  program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
@@ -65271,4 +65231,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
65271
65231
  export {};
65272
65232
 
65273
65233
  //# sourceMappingURL=cli.js.map
65274
- //# debugId=9a5c1a3c-7706-5a23-bca4-ddddd2f085af
65234
+ //# debugId=63b8a227-543e-5501-a998-58c03a1fb10e
package/dist/index.d.ts CHANGED
@@ -9654,7 +9654,6 @@ interface DiagnoseOptions {
9654
9654
  /** See `ReactDoctorConfig.deadCode`. Ignored in diff mode. */
9655
9655
  deadCode?: boolean;
9656
9656
  verbose?: boolean;
9657
- /** Restrict linting to these supported JS/TS source files. */
9658
9657
  includePaths?: string[];
9659
9658
  /**
9660
9659
  * Per-call override for `ReactDoctorConfig.respectInlineDisables`.
@@ -9916,9 +9915,9 @@ interface JsonReportProjectEntry {
9916
9915
  skippedCheckReasons?: Record<string, string>;
9917
9916
  /**
9918
9917
  * Number of source files this scan's linter examined. In diff / changed
9919
- * mode it's the count of changed supported JS/TS source files; in a full
9920
- * scan it's the whole source tree. `0` in a diff scan means the changed
9921
- * files held nothing React Doctor lints —
9918
+ * mode it's the count of changed React-eligible files (`.tsx`/`.jsx` plus
9919
+ * framework entry files); in a full scan it's the whole source tree. `0`
9920
+ * in a diff scan means the changed files held nothing React Doctor lints —
9922
9921
  * the GitHub Action reads that as "nothing to report" (skips the PR comment;
9923
9922
  * the commit status says "skipped"). Optional: absent on reports from
9924
9923
  * constructors that don't track it (e.g. `toJsonReport`).
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]="11364a34-6c8b-5bd3-b9e8-aaa7c0ea264c")}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]="7a10d23c-04fd-53e1-a623-a3dc5d6d0830")}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 reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
@@ -32930,6 +32930,7 @@ const IGNORED_DIRECTORIES = new Set([
32930
32930
  "out",
32931
32931
  "storybook-static"
32932
32932
  ]);
32933
+ const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
32933
32934
  const MILLISECONDS_PER_SECOND = 1e3;
32934
32935
  const SCORE_API_URL = "https://www.react.doctor/api/score";
32935
32936
  const FETCH_TIMEOUT_MS = 1e4;
@@ -33142,15 +33143,13 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33142
33143
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33143
33144
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33144
33145
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33145
- const UTF8_BOM_CHARACTER = "";
33146
- const stripUtf8Bom = (content) => content.startsWith(UTF8_BOM_CHARACTER) ? content.slice(1) : content;
33147
33146
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33148
33147
  const clearPackageJsonCache = () => {
33149
33148
  cachedPackageJsons.clear();
33150
33149
  };
33151
33150
  const readPackageJsonUncached = (packageJsonPath) => {
33152
33151
  try {
33153
- return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
33152
+ return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
33154
33153
  } catch (error) {
33155
33154
  if (error instanceof SyntaxError) return {};
33156
33155
  if (isErrnoException(error)) {
@@ -36791,6 +36790,7 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
36791
36790
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
36792
36791
  const PACKAGE_JSON_FILE = "package.json";
36793
36792
  const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
36793
+ const UTF8_BOM_CHAR = "";
36794
36794
  const HARDENING_SETTING_KEYS = new Set([
36795
36795
  "minimumReleaseAge",
36796
36796
  "blockExoticSubdeps",
@@ -36815,11 +36815,12 @@ const stripInlineComment = (rawValue) => {
36815
36815
  return rawValue;
36816
36816
  };
36817
36817
  const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
36818
+ const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
36818
36819
  const parseHardeningSettings = (content) => {
36819
36820
  let minimumReleaseAge = null;
36820
36821
  let blockExoticSubdeps = null;
36821
36822
  let trustPolicy = null;
36822
- const lines = stripUtf8Bom(content).split(/\r?\n/);
36823
+ const lines = stripBom(content).split(/\r?\n/);
36823
36824
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
36824
36825
  const lineText = lines[lineIndex];
36825
36826
  if (lineText === void 0) continue;
@@ -36853,7 +36854,7 @@ const isPnpmManagedProject = (rootDirectory) => {
36853
36854
  const packageJsonPath = Path.join(rootDirectory, PACKAGE_JSON_FILE);
36854
36855
  if (!isFile(packageJsonPath)) return false;
36855
36856
  try {
36856
- const packageJsonRaw = stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8"));
36857
+ const packageJsonRaw = NFS.readFileSync(packageJsonPath, "utf-8");
36857
36858
  const packageJson = JSON.parse(packageJsonRaw);
36858
36859
  if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
36859
36860
  } catch {
@@ -37515,7 +37516,9 @@ const highlighter = {
37515
37516
  gray: import_picocolors.default.gray,
37516
37517
  bold: import_picocolors.default.bold
37517
37518
  };
37518
- const computeExplicitLintIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter(isLintableSourceFile) : void 0;
37519
+ const NEXT_ENTRY_FILE_PATTERN = /^(?:\.\/)?(?:src\/)?(?:middleware|proxy)\.(?:tsx?|jsx?|mts|mjs)$/;
37520
+ const shouldIncludeExplicitLintPath = (filePath, project) => JSX_FILE_PATTERN.test(filePath) || project?.framework === "nextjs" && NEXT_ENTRY_FILE_PATTERN.test(filePath);
37521
+ const computeExplicitLintIncludePaths = (includePaths, project) => includePaths.length > 0 ? includePaths.filter((filePath) => shouldIncludeExplicitLintPath(filePath, project)) : void 0;
37519
37522
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
37520
37523
  const FALSY_VALUES = new Set([
37521
37524
  "false",
@@ -38357,11 +38360,11 @@ const resolveDeadCodeTimeout = (input) => {
38357
38360
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
38358
38361
  };
38359
38362
  };
38360
- const resolveLintIncludePaths = (rootDirectory, userConfig) => {
38363
+ const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
38361
38364
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
38362
38365
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
38363
38366
  return listSourceFiles(rootDirectory).filter((filePath) => {
38364
- if (!isLintableSourceFile(filePath)) return false;
38367
+ if (!shouldIncludeExplicitLintPath(filePath, project)) return false;
38365
38368
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
38366
38369
  });
38367
38370
  };
@@ -39642,12 +39645,6 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39642
39645
  if (errors.length > 0) throw errors[0];
39643
39646
  return results;
39644
39647
  };
39645
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
39646
- let lineNumber = 1;
39647
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
39648
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === 10) lineNumber++;
39649
- return lineNumber;
39650
- };
39651
39648
  const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
39652
39649
  const ACCESSORS_MIN_VERSION = "3.15.0";
39653
39650
  const supportsAccessors = (reanimatedVersion) => {
@@ -40219,6 +40216,13 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40219
40216
  visit(sourceFile);
40220
40217
  return foundRange;
40221
40218
  };
40219
+ const NEWLINE_BYTE = 10;
40220
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40221
+ let lineNumber = 1;
40222
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40223
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40224
+ return lineNumber;
40225
+ };
40222
40226
  /**
40223
40227
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40224
40228
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -40423,33 +40427,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40423
40427
  }
40424
40428
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40425
40429
  const minifiedFileCache = /* @__PURE__ */ new Map();
40426
- const sourceBufferCache = /* @__PURE__ */ new Map();
40427
- const sourceLinesCache = /* @__PURE__ */ new Map();
40428
- const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40429
- const readSourceBuffer = (filename) => {
40430
- const absolutePath = resolveAbsolutePath(filename);
40431
- const cached = sourceBufferCache.get(absolutePath);
40432
- if (cached !== void 0) return cached;
40433
- let sourceBuffer;
40434
- try {
40435
- sourceBuffer = NFS.readFileSync(absolutePath);
40436
- } catch {
40437
- sourceBuffer = null;
40438
- }
40439
- sourceBufferCache.set(absolutePath, sourceBuffer);
40440
- return sourceBuffer;
40441
- };
40442
- const readSourceLines = (filename) => {
40443
- const absolutePath = resolveAbsolutePath(filename);
40444
- const cached = sourceLinesCache.get(absolutePath);
40445
- if (cached !== void 0) return cached;
40446
- const sourceBuffer = readSourceBuffer(filename);
40447
- const sourceLines = sourceBuffer ? sourceBuffer.toString("utf8").split("\n") : null;
40448
- sourceLinesCache.set(absolutePath, sourceLines);
40449
- return sourceLines;
40450
- };
40451
40430
  const isMinifiedDiagnosticFile = (filename) => {
40452
- const absolutePath = resolveAbsolutePath(filename);
40431
+ const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40453
40432
  const cached = minifiedFileCache.get(absolutePath);
40454
40433
  if (cached !== void 0) return cached;
40455
40434
  const minified = isMinifiedSource(absolutePath);
@@ -40462,17 +40441,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40462
40441
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
40463
40442
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
40464
40443
  const primarySpan = primaryLabel?.span;
40465
- const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
40466
40444
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
40467
40445
  const category = resolveDiagnosticCategory(plugin, rule);
40468
- const matchByOccurrence = resolveMatchByOccurrence(rule, category);
40469
- const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
40470
- const primaryLineIndex = primarySpan ? primarySpan.line - 1 : -1;
40471
- const primaryLine = sourceLines?.[primaryLineIndex];
40472
- const primaryColumnIndex = primarySpan ? primarySpan.column - 1 : -1;
40473
- const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
40474
- const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
40475
- const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
40476
40446
  return {
40477
40447
  filePath: normalizedFilePath,
40478
40448
  plugin,
@@ -40486,11 +40456,10 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40486
40456
  column: primarySpan?.column ?? 0,
40487
40457
  ...primarySpan ? {
40488
40458
  offset: primarySpan.offset,
40489
- length: primarySpan.length,
40490
- ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
40459
+ length: primarySpan.length
40491
40460
  } : {},
40492
40461
  category,
40493
- ...matchByOccurrence ? { matchByOccurrence: true } : {},
40462
+ ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
40494
40463
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
40495
40464
  };
40496
40465
  }), rootDirectory);
@@ -42277,7 +42246,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42277
42246
  directory: scanDirectory,
42278
42247
  repo
42279
42248
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
42280
- const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
42249
+ const lintIncludePaths = (input.skipJsxIncludeFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths], project)) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, project);
42281
42250
  const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
42282
42251
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
42283
42252
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
@@ -43241,4 +43210,4 @@ const toJsonReport = (result, options) => buildJsonReport({
43241
43210
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
43242
43211
 
43243
43212
  //# sourceMappingURL=index.js.map
43244
- //# debugId=11364a34-6c8b-5bd3-b9e8-aaa7c0ea264c
43213
+ //# debugId=7a10d23c-04fd-53e1-a623-a3dc5d6d0830
package/dist/lsp.js CHANGED
@@ -32959,6 +32959,7 @@ const IGNORED_DIRECTORIES = new Set([
32959
32959
  "out",
32960
32960
  "storybook-static"
32961
32961
  ]);
32962
+ const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
32962
32963
  const MILLISECONDS_PER_SECOND = 1e3;
32963
32964
  const SCORE_API_URL = "https://www.react.doctor/api/score";
32964
32965
  const FETCH_TIMEOUT_MS = 1e4;
@@ -33194,15 +33195,13 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33194
33195
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33195
33196
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33196
33197
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33197
- const UTF8_BOM_CHARACTER = "";
33198
- const stripUtf8Bom = (content) => content.startsWith(UTF8_BOM_CHARACTER) ? content.slice(1) : content;
33199
33198
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33200
33199
  const clearPackageJsonCache = () => {
33201
33200
  cachedPackageJsons.clear();
33202
33201
  };
33203
33202
  const readPackageJsonUncached = (packageJsonPath) => {
33204
33203
  try {
33205
- return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
33204
+ return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
33206
33205
  } catch (error) {
33207
33206
  if (error instanceof SyntaxError) return {};
33208
33207
  if (isErrnoException(error)) {
@@ -36769,6 +36768,7 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
36769
36768
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
36770
36769
  const PACKAGE_JSON_FILE = "package.json";
36771
36770
  const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
36771
+ const UTF8_BOM_CHAR = "";
36772
36772
  const HARDENING_SETTING_KEYS = new Set([
36773
36773
  "minimumReleaseAge",
36774
36774
  "blockExoticSubdeps",
@@ -36793,11 +36793,12 @@ const stripInlineComment = (rawValue) => {
36793
36793
  return rawValue;
36794
36794
  };
36795
36795
  const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
36796
+ const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
36796
36797
  const parseHardeningSettings = (content) => {
36797
36798
  let minimumReleaseAge = null;
36798
36799
  let blockExoticSubdeps = null;
36799
36800
  let trustPolicy = null;
36800
- const lines = stripUtf8Bom(content).split(/\r?\n/);
36801
+ const lines = stripBom(content).split(/\r?\n/);
36801
36802
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
36802
36803
  const lineText = lines[lineIndex];
36803
36804
  if (lineText === void 0) continue;
@@ -36831,7 +36832,7 @@ const isPnpmManagedProject = (rootDirectory) => {
36831
36832
  const packageJsonPath = Path.join(rootDirectory, PACKAGE_JSON_FILE);
36832
36833
  if (!isFile(packageJsonPath)) return false;
36833
36834
  try {
36834
- const packageJsonRaw = stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8"));
36835
+ const packageJsonRaw = NFS.readFileSync(packageJsonPath, "utf-8");
36835
36836
  const packageJson = JSON.parse(packageJsonRaw);
36836
36837
  if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
36837
36838
  } catch {
@@ -37493,7 +37494,9 @@ const highlighter = {
37493
37494
  gray: import_picocolors.default.gray,
37494
37495
  bold: import_picocolors.default.bold
37495
37496
  };
37496
- const computeExplicitLintIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter(isLintableSourceFile) : void 0;
37497
+ const NEXT_ENTRY_FILE_PATTERN = /^(?:\.\/)?(?:src\/)?(?:middleware|proxy)\.(?:tsx?|jsx?|mts|mjs)$/;
37498
+ const shouldIncludeExplicitLintPath = (filePath, project) => JSX_FILE_PATTERN.test(filePath) || project?.framework === "nextjs" && NEXT_ENTRY_FILE_PATTERN.test(filePath);
37499
+ const computeExplicitLintIncludePaths = (includePaths, project) => includePaths.length > 0 ? includePaths.filter((filePath) => shouldIncludeExplicitLintPath(filePath, project)) : void 0;
37497
37500
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
37498
37501
  const FALSY_VALUES = new Set([
37499
37502
  "false",
@@ -38335,11 +38338,11 @@ const resolveDeadCodeTimeout = (input) => {
38335
38338
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
38336
38339
  };
38337
38340
  };
38338
- const resolveLintIncludePaths = (rootDirectory, userConfig) => {
38341
+ const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
38339
38342
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
38340
38343
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
38341
38344
  return listSourceFiles(rootDirectory).filter((filePath) => {
38342
- if (!isLintableSourceFile(filePath)) return false;
38345
+ if (!shouldIncludeExplicitLintPath(filePath, project)) return false;
38343
38346
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
38344
38347
  });
38345
38348
  };
@@ -39620,12 +39623,6 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39620
39623
  if (errors.length > 0) throw errors[0];
39621
39624
  return results;
39622
39625
  };
39623
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
39624
- let lineNumber = 1;
39625
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
39626
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === 10) lineNumber++;
39627
- return lineNumber;
39628
- };
39629
39626
  const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
39630
39627
  const ACCESSORS_MIN_VERSION = "3.15.0";
39631
39628
  const supportsAccessors = (reanimatedVersion) => {
@@ -40197,6 +40194,13 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40197
40194
  visit(sourceFile);
40198
40195
  return foundRange;
40199
40196
  };
40197
+ const NEWLINE_BYTE = 10;
40198
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40199
+ let lineNumber = 1;
40200
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40201
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40202
+ return lineNumber;
40203
+ };
40200
40204
  /**
40201
40205
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40202
40206
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -40401,33 +40405,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40401
40405
  }
40402
40406
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40403
40407
  const minifiedFileCache = /* @__PURE__ */ new Map();
40404
- const sourceBufferCache = /* @__PURE__ */ new Map();
40405
- const sourceLinesCache = /* @__PURE__ */ new Map();
40406
- const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40407
- const readSourceBuffer = (filename) => {
40408
- const absolutePath = resolveAbsolutePath(filename);
40409
- const cached = sourceBufferCache.get(absolutePath);
40410
- if (cached !== void 0) return cached;
40411
- let sourceBuffer;
40412
- try {
40413
- sourceBuffer = NFS.readFileSync(absolutePath);
40414
- } catch {
40415
- sourceBuffer = null;
40416
- }
40417
- sourceBufferCache.set(absolutePath, sourceBuffer);
40418
- return sourceBuffer;
40419
- };
40420
- const readSourceLines = (filename) => {
40421
- const absolutePath = resolveAbsolutePath(filename);
40422
- const cached = sourceLinesCache.get(absolutePath);
40423
- if (cached !== void 0) return cached;
40424
- const sourceBuffer = readSourceBuffer(filename);
40425
- const sourceLines = sourceBuffer ? sourceBuffer.toString("utf8").split("\n") : null;
40426
- sourceLinesCache.set(absolutePath, sourceLines);
40427
- return sourceLines;
40428
- };
40429
40408
  const isMinifiedDiagnosticFile = (filename) => {
40430
- const absolutePath = resolveAbsolutePath(filename);
40409
+ const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40431
40410
  const cached = minifiedFileCache.get(absolutePath);
40432
40411
  if (cached !== void 0) return cached;
40433
40412
  const minified = isMinifiedSource(absolutePath);
@@ -40440,17 +40419,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40440
40419
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
40441
40420
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
40442
40421
  const primarySpan = primaryLabel?.span;
40443
- const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
40444
40422
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
40445
40423
  const category = resolveDiagnosticCategory(plugin, rule);
40446
- const matchByOccurrence = resolveMatchByOccurrence(rule, category);
40447
- const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
40448
- const primaryLineIndex = primarySpan ? primarySpan.line - 1 : -1;
40449
- const primaryLine = sourceLines?.[primaryLineIndex];
40450
- const primaryColumnIndex = primarySpan ? primarySpan.column - 1 : -1;
40451
- const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
40452
- const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
40453
- const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
40454
40424
  return {
40455
40425
  filePath: normalizedFilePath,
40456
40426
  plugin,
@@ -40464,11 +40434,10 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40464
40434
  column: primarySpan?.column ?? 0,
40465
40435
  ...primarySpan ? {
40466
40436
  offset: primarySpan.offset,
40467
- length: primarySpan.length,
40468
- ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
40437
+ length: primarySpan.length
40469
40438
  } : {},
40470
40439
  category,
40471
- ...matchByOccurrence ? { matchByOccurrence: true } : {},
40440
+ ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
40472
40441
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
40473
40442
  };
40474
40443
  }), rootDirectory);
@@ -42255,7 +42224,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42255
42224
  directory: scanDirectory,
42256
42225
  repo
42257
42226
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
42258
- const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
42227
+ const lintIncludePaths = (input.skipJsxIncludeFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths], project)) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, project);
42259
42228
  const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
42260
42229
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
42261
42230
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
@@ -42750,7 +42719,7 @@ const runEditorScan = async (input) => {
42750
42719
  warnings,
42751
42720
  isCi: false,
42752
42721
  resolveLocalGithubViewerPermission: false,
42753
- skipExplicitIncludePathFilter: true
42722
+ skipJsxIncludeFilter: true
42754
42723
  }).pipe(withSpan("runEditorScan", { attributes: {
42755
42724
  "editor.lint": lint,
42756
42725
  "editor.runDeadCode": runDeadCode
@@ -43513,7 +43482,7 @@ const createLintCache = (input) => {
43513
43482
  let persistTimer = null;
43514
43483
  try {
43515
43484
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
43516
- if (parsed.version === 2 && parsed.fingerprint === input.fingerprint) for (const [fsPath, entry] of Object.entries(parsed.entries)) entries.set(fsPath, entry);
43485
+ if (parsed.version === 1 && parsed.fingerprint === input.fingerprint) for (const [fsPath, entry] of Object.entries(parsed.entries)) entries.set(fsPath, entry);
43517
43486
  } catch {}
43518
43487
  const persist = () => {
43519
43488
  if (persistTimer) {
@@ -43524,7 +43493,7 @@ const createLintCache = (input) => {
43524
43493
  dirty = false;
43525
43494
  try {
43526
43495
  const payload = {
43527
- version: 2,
43496
+ version: 1,
43528
43497
  fingerprint: input.fingerprint,
43529
43498
  entries: Object.fromEntries(entries)
43530
43499
  };
@@ -43537,14 +43506,14 @@ const createLintCache = (input) => {
43537
43506
  }
43538
43507
  };
43539
43508
  return {
43540
- lookup: (fsPath, identity) => {
43509
+ lookup: (fsPath, stat) => {
43541
43510
  const entry = entries.get(fsPath);
43542
- if (entry !== void 0 && entry.contentHash === identity.contentHash) return entry.diagnostics;
43511
+ if (entry !== void 0 && entry.mtimeMs === stat.mtimeMs && entry.size === stat.size) return entry.diagnostics;
43543
43512
  return null;
43544
43513
  },
43545
- store: (fsPath, identity, diagnostics) => {
43514
+ store: (fsPath, stat, diagnostics) => {
43546
43515
  entries.set(fsPath, {
43547
- ...identity,
43516
+ ...stat,
43548
43517
  diagnostics
43549
43518
  });
43550
43519
  dirty = true;
@@ -43633,9 +43602,16 @@ const resolveDiagnosticFsPath = (rawFilePath, scanDirectory, projectDirectory, o
43633
43602
  }
43634
43603
  return normalizeFsPath(absolute);
43635
43604
  };
43636
- const readFileIdentity = (fsPath) => {
43637
- const contentHash = hashFileContents(fsPath);
43638
- return contentHash === null ? null : { contentHash };
43605
+ const statSafe = (fsPath) => {
43606
+ try {
43607
+ const stat = fs.statSync(fsPath);
43608
+ return {
43609
+ mtimeMs: stat.mtimeMs,
43610
+ size: stat.size
43611
+ };
43612
+ } catch {
43613
+ return null;
43614
+ }
43639
43615
  };
43640
43616
  /**
43641
43617
  * Outcome for a per-file request served without running oxlint (every
@@ -43661,7 +43637,7 @@ const outcomeWithoutScan = (request, byFile, requestedPaths) => ({
43661
43637
  * overlay tree (unsaved buffers) or disk, groups diagnostics by canonical
43662
43638
  * absolute path, and reports stale-detection metadata.
43663
43639
  *
43664
- * A persistent per-file lint cache (keyed by content, namespaced by a
43640
+ * A persistent per-file lint cache (keyed by mtime + size, namespaced by a
43665
43641
  * config fingerprint) short-circuits unchanged files so a re-opened editor
43666
43642
  * or repeated workspace scan skips the oxlint subprocess for everything it
43667
43643
  * hasn't edited. The cache applies only to disk-based, lint-only, per-file
@@ -43691,15 +43667,15 @@ const createScanRunner = (options) => {
43691
43667
  const requestedPaths = request.priority === "background" && !request.useOverlay && options.isOpen ? allRequested.filter((fsPath) => !options.isOpen?.(fsPath)) : allRequested;
43692
43668
  const cache = cacheEnabled && !isWholeProject && !request.useOverlay && !request.runDeadCode ? getCache(projectDirectory) : null;
43693
43669
  const byFile = /* @__PURE__ */ new Map();
43694
- const identityByPath = /* @__PURE__ */ new Map();
43670
+ const statByPath = /* @__PURE__ */ new Map();
43695
43671
  let filesToScan = requestedPaths;
43696
43672
  if (cache) {
43697
43673
  const uncached = [];
43698
43674
  for (const fsPath of requestedPaths) {
43699
- const identity = readFileIdentity(fsPath);
43700
- if (identity) {
43701
- identityByPath.set(fsPath, identity);
43702
- const hit = cache.lookup(fsPath, identity);
43675
+ const stat = statSafe(fsPath);
43676
+ if (stat) {
43677
+ statByPath.set(fsPath, stat);
43678
+ const hit = cache.lookup(fsPath, stat);
43703
43679
  if (hit !== null) {
43704
43680
  if (hit.length > 0) byFile.set(fsPath, hit);
43705
43681
  continue;
@@ -43742,9 +43718,9 @@ const createScanRunner = (options) => {
43742
43718
  }
43743
43719
  if (cache && result.ok && !result.skipped && !result.didLintFail && result.lintPartialFailures.length === 0) {
43744
43720
  for (const fsPath of filesToScan) {
43745
- const identity = identityByPath.get(fsPath);
43746
- if (!identity) continue;
43747
- cache.store(fsPath, identity, byFile.get(fsPath) ?? []);
43721
+ const stat = statByPath.get(fsPath);
43722
+ if (!stat) continue;
43723
+ cache.store(fsPath, stat, byFile.get(fsPath) ?? []);
43748
43724
  }
43749
43725
  cache.schedulePersist();
43750
43726
  }
@@ -44878,5 +44854,5 @@ const startLanguageServer = () => {
44878
44854
  };
44879
44855
  //#endregion
44880
44856
  export { startLanguageServer };
44881
- !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]="f9337714-b38f-5f28-ac00-c9d7d79ebaea")}catch(e){}}();
44882
- //# debugId=f9337714-b38f-5f28-ac00-c9d7d79ebaea
44857
+ !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]="b77898db-d8e8-531a-a32b-9de2d1a63635")}catch(e){}}();
44858
+ //# debugId=b77898db-d8e8-531a-a32b-9de2d1a63635
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.7.6-dev.e141d90",
3
+ "version": "0.7.6-dev.e877ca7",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -66,7 +66,7 @@
66
66
  "vscode-uri": "^3.1.0",
67
67
  "yaml": "^2.9.0",
68
68
  "deslop-js": "0.7.6",
69
- "oxlint-plugin-react-doctor": "0.7.6-dev.e141d90"
69
+ "oxlint-plugin-react-doctor": "0.7.6-dev.e877ca7"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/babel__code-frame": "^7.27.0",
@@ -74,8 +74,8 @@
74
74
  "@xterm/headless": "^6.0.0",
75
75
  "commander": "^14.0.3",
76
76
  "ora": "^9.4.0",
77
- "@react-doctor/core": "0.7.6",
78
77
  "@react-doctor/api": "0.7.6",
78
+ "@react-doctor/core": "0.7.6",
79
79
  "@react-doctor/language-server": "0.7.6"
80
80
  },
81
81
  "engines": {