react-doctor 0.7.5 → 0.7.6-dev.0a8d30b

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/lsp.js CHANGED
@@ -32959,7 +32959,6 @@ const IGNORED_DIRECTORIES = new Set([
32959
32959
  "out",
32960
32960
  "storybook-static"
32961
32961
  ]);
32962
- const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
32963
32962
  const MILLISECONDS_PER_SECOND = 1e3;
32964
32963
  const SCORE_API_URL = "https://www.react.doctor/api/score";
32965
32964
  const FETCH_TIMEOUT_MS = 1e4;
@@ -33092,7 +33091,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33092
33091
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33093
33092
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33094
33093
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
33095
- const CORE_PACKAGE_VERSION = "0.7.5";
33094
+ const CORE_PACKAGE_VERSION = "0.7.6";
33096
33095
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
33097
33096
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
33098
33097
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -33195,13 +33194,15 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33195
33194
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33196
33195
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33197
33196
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33197
+ const UTF8_BOM_CHARACTER = "";
33198
+ const stripUtf8Bom = (content) => content.startsWith(UTF8_BOM_CHARACTER) ? content.slice(1) : content;
33198
33199
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33199
33200
  const clearPackageJsonCache = () => {
33200
33201
  cachedPackageJsons.clear();
33201
33202
  };
33202
33203
  const readPackageJsonUncached = (packageJsonPath) => {
33203
33204
  try {
33204
- return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
33205
+ return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
33205
33206
  } catch (error) {
33206
33207
  if (error instanceof SyntaxError) return {};
33207
33208
  if (isErrnoException(error)) {
@@ -36768,7 +36769,6 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
36768
36769
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
36769
36770
  const PACKAGE_JSON_FILE = "package.json";
36770
36771
  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,12 +36793,11 @@ 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;
36797
36796
  const parseHardeningSettings = (content) => {
36798
36797
  let minimumReleaseAge = null;
36799
36798
  let blockExoticSubdeps = null;
36800
36799
  let trustPolicy = null;
36801
- const lines = stripBom(content).split(/\r?\n/);
36800
+ const lines = stripUtf8Bom(content).split(/\r?\n/);
36802
36801
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
36803
36802
  const lineText = lines[lineIndex];
36804
36803
  if (lineText === void 0) continue;
@@ -36832,7 +36831,7 @@ const isPnpmManagedProject = (rootDirectory) => {
36832
36831
  const packageJsonPath = Path.join(rootDirectory, PACKAGE_JSON_FILE);
36833
36832
  if (!isFile(packageJsonPath)) return false;
36834
36833
  try {
36835
- const packageJsonRaw = NFS.readFileSync(packageJsonPath, "utf-8");
36834
+ const packageJsonRaw = stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8"));
36836
36835
  const packageJson = JSON.parse(packageJsonRaw);
36837
36836
  if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
36838
36837
  } catch {
@@ -37494,9 +37493,7 @@ const highlighter = {
37494
37493
  gray: import_picocolors.default.gray,
37495
37494
  bold: import_picocolors.default.bold
37496
37495
  };
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;
37496
+ const computeExplicitLintIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter(isLintableSourceFile) : void 0;
37500
37497
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
37501
37498
  const FALSY_VALUES = new Set([
37502
37499
  "false",
@@ -38338,11 +38335,11 @@ const resolveDeadCodeTimeout = (input) => {
38338
38335
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
38339
38336
  };
38340
38337
  };
38341
- const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
38338
+ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
38342
38339
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
38343
38340
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
38344
38341
  return listSourceFiles(rootDirectory).filter((filePath) => {
38345
- if (!shouldIncludeExplicitLintPath(filePath, project)) return false;
38342
+ if (!isLintableSourceFile(filePath)) return false;
38346
38343
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
38347
38344
  });
38348
38345
  };
@@ -38560,6 +38557,7 @@ const parseGithubViewerPermission = (stdout) => {
38560
38557
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
38561
38558
  };
38562
38559
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
38560
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
38563
38561
  /**
38564
38562
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
38565
38563
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -38630,6 +38628,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38630
38628
  args,
38631
38629
  directory
38632
38630
  });
38631
+ const listUntrackedFilePaths = (directory, includePaths = []) => runGit(directory, [
38632
+ "ls-files",
38633
+ "-z",
38634
+ "--others",
38635
+ "--exclude-standard",
38636
+ ...includePaths.length > 0 ? ["--", ...includePaths] : []
38637
+ ]).pipe(map$3((result) => result.status === 0 ? splitNullSeparated(result.stdout) : null));
38638
+ const mergeUntracked = (directory, trackedFilePaths, includeUntracked) => includeUntracked ? listUntrackedFilePaths(directory).pipe(map$3((untracked) => untracked === null ? trackedFilePaths : [...new Set([...trackedFilePaths, ...untracked])])) : succeed$2(trackedFilePaths);
38633
38639
  const currentBranch = (directory) => runGit(directory, [
38634
38640
  "rev-parse",
38635
38641
  "--abbrev-ref",
@@ -38752,7 +38758,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38752
38758
  githubViewerPermission,
38753
38759
  branchExists,
38754
38760
  mergeBase,
38755
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
38761
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
38756
38762
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
38757
38763
  if (explicitBaseBranch !== void 0) {
38758
38764
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -38781,7 +38787,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38781
38787
  "HEAD"
38782
38788
  ]);
38783
38789
  if (uncommitted.status !== 0) return null;
38784
- const files = splitNullSeparated(uncommitted.stdout);
38790
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
38785
38791
  if (files.length === 0) return null;
38786
38792
  return {
38787
38793
  currentBranch: resolvedCurrentBranch,
@@ -38811,7 +38817,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38811
38817
  currentBranch: resolvedCurrentBranch,
38812
38818
  baseBranch,
38813
38819
  diffBaseRef: mergeBaseRef,
38814
- changedFiles: splitNullSeparated(diff.stdout),
38820
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
38815
38821
  isCurrentChanges: false
38816
38822
  };
38817
38823
  }).pipe(withSpan("Git.diffSelection")),
@@ -38857,7 +38863,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38857
38863
  stdout: result.stdout
38858
38864
  };
38859
38865
  }).pipe(withSpan("Git.grep")),
38860
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
38866
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
38861
38867
  if (files.length === 0) return [];
38862
38868
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
38863
38869
  const result = yield* runGit(directory, [
@@ -38871,7 +38877,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38871
38877
  ...files
38872
38878
  ]);
38873
38879
  if (result.status !== 0) return null;
38874
- return parseChangedLineRanges(result.stdout);
38880
+ const changedLineRanges = parseChangedLineRanges(result.stdout);
38881
+ if (cached || !includeUntracked) return changedLineRanges;
38882
+ const untrackedFilePaths = yield* listUntrackedFilePaths(directory, files);
38883
+ if (untrackedFilePaths === null) return changedLineRanges;
38884
+ return [...changedLineRanges, ...untrackedFilePaths.map((file) => ({
38885
+ file,
38886
+ ranges: [[1, UNTRACKED_FILE_LAST_LINE]]
38887
+ }))];
38875
38888
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38876
38889
  });
38877
38890
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
@@ -39103,7 +39116,7 @@ const normalizeConfigForHash = (config) => {
39103
39116
  if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
39104
39117
  return clone;
39105
39118
  };
39106
- const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update("\0").update(input.tsconfigContent ?? "").digest("hex");
39119
+ const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update("\0").update(input.tsconfigContent ?? "").update("\0").update(input.respectInlineDisables ? "respect-inline-disables" : "audit-mode").digest("hex");
39107
39120
  /**
39108
39121
  * Loads a plugin module via the local require resolver and extracts
39109
39122
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -39139,7 +39152,7 @@ const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
39139
39152
  } catch {
39140
39153
  return null;
39141
39154
  }
39142
- const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire$1(spec));
39155
+ const { ruleNames } = readPluginShape(pluginSpecifier, (specifier) => bundledRequire$1(specifier));
39143
39156
  return {
39144
39157
  entry: {
39145
39158
  name: "react-hooks-js",
@@ -39547,6 +39560,23 @@ const resolveTsConfigRelativePath = (rootDirectory) => {
39547
39560
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
39548
39561
  return null;
39549
39562
  };
39563
+ const DERIVED_STATE_RULE_PRIORITY = new Map([
39564
+ ["no-initialize-state", 0],
39565
+ ["no-adjust-state-on-prop-change", 1],
39566
+ ["no-derived-state", 2],
39567
+ ["no-derived-state-effect", 3]
39568
+ ]);
39569
+ const doDiagnosticSpansOverlap = (first, second) => {
39570
+ if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
39571
+ if (first.offset !== void 0 && second.offset !== void 0) {
39572
+ const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
39573
+ const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
39574
+ return first.offset < secondEnd && second.offset < firstEnd;
39575
+ }
39576
+ const firstEndLine = first.endLine ?? first.line;
39577
+ const secondEndLine = second.endLine ?? second.line;
39578
+ return first.line <= secondEndLine && second.line <= firstEndLine;
39579
+ };
39550
39580
  const dedupeDiagnostics = (diagnostics) => {
39551
39581
  const seenKeys = /* @__PURE__ */ new Set();
39552
39582
  const uniqueDiagnostics = [];
@@ -39554,6 +39584,16 @@ const dedupeDiagnostics = (diagnostics) => {
39554
39584
  const key = `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}\u0000${diagnostic.plugin}\u0000${diagnostic.rule}\u0000${diagnostic.severity}\u0000${diagnostic.message}`;
39555
39585
  if (seenKeys.has(key)) continue;
39556
39586
  seenKeys.add(key);
39587
+ const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
39588
+ if (priority !== void 0) {
39589
+ const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && candidate.severity === diagnostic.severity && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
39590
+ if (overlappingDiagnosticIndex >= 0) {
39591
+ const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
39592
+ const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
39593
+ if (existingPriority !== void 0 && priority < existingPriority) uniqueDiagnostics[overlappingDiagnosticIndex] = diagnostic;
39594
+ continue;
39595
+ }
39596
+ }
39557
39597
  uniqueDiagnostics.push(diagnostic);
39558
39598
  }
39559
39599
  return uniqueDiagnostics;
@@ -39596,6 +39636,12 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39596
39636
  if (errors.length > 0) throw errors[0];
39597
39637
  return results;
39598
39638
  };
39639
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
39640
+ let lineNumber = 1;
39641
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
39642
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === 10) lineNumber++;
39643
+ return lineNumber;
39644
+ };
39599
39645
  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";
39600
39646
  const ACCESSORS_MIN_VERSION = "3.15.0";
39601
39647
  const supportsAccessors = (reanimatedVersion) => {
@@ -40167,13 +40213,6 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40167
40213
  visit(sourceFile);
40168
40214
  return foundRange;
40169
40215
  };
40170
- const NEWLINE_BYTE = 10;
40171
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40172
- let lineNumber = 1;
40173
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40174
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40175
- return lineNumber;
40176
- };
40177
40216
  /**
40178
40217
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40179
40218
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -40378,8 +40417,33 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40378
40417
  }
40379
40418
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40380
40419
  const minifiedFileCache = /* @__PURE__ */ new Map();
40420
+ const sourceBufferCache = /* @__PURE__ */ new Map();
40421
+ const sourceLinesCache = /* @__PURE__ */ new Map();
40422
+ const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40423
+ const readSourceBuffer = (filename) => {
40424
+ const absolutePath = resolveAbsolutePath(filename);
40425
+ const cached = sourceBufferCache.get(absolutePath);
40426
+ if (cached !== void 0) return cached;
40427
+ let sourceBuffer;
40428
+ try {
40429
+ sourceBuffer = NFS.readFileSync(absolutePath);
40430
+ } catch {
40431
+ sourceBuffer = null;
40432
+ }
40433
+ sourceBufferCache.set(absolutePath, sourceBuffer);
40434
+ return sourceBuffer;
40435
+ };
40436
+ const readSourceLines = (filename) => {
40437
+ const absolutePath = resolveAbsolutePath(filename);
40438
+ const cached = sourceLinesCache.get(absolutePath);
40439
+ if (cached !== void 0) return cached;
40440
+ const sourceBuffer = readSourceBuffer(filename);
40441
+ const sourceLines = sourceBuffer ? sourceBuffer.toString("utf8").split("\n") : null;
40442
+ sourceLinesCache.set(absolutePath, sourceLines);
40443
+ return sourceLines;
40444
+ };
40381
40445
  const isMinifiedDiagnosticFile = (filename) => {
40382
- const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40446
+ const absolutePath = resolveAbsolutePath(filename);
40383
40447
  const cached = minifiedFileCache.get(absolutePath);
40384
40448
  if (cached !== void 0) return cached;
40385
40449
  const minified = isMinifiedSource(absolutePath);
@@ -40392,8 +40456,17 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40392
40456
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
40393
40457
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
40394
40458
  const primarySpan = primaryLabel?.span;
40459
+ const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
40395
40460
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
40396
40461
  const category = resolveDiagnosticCategory(plugin, rule);
40462
+ const matchByOccurrence = resolveMatchByOccurrence(rule, category);
40463
+ const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
40464
+ const primaryLineIndex = primarySpan ? primarySpan.line - 1 : -1;
40465
+ const primaryLine = sourceLines?.[primaryLineIndex];
40466
+ const primaryColumnIndex = primarySpan ? primarySpan.column - 1 : -1;
40467
+ const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
40468
+ const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
40469
+ const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
40397
40470
  return {
40398
40471
  filePath: normalizedFilePath,
40399
40472
  plugin,
@@ -40407,10 +40480,11 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40407
40480
  column: primarySpan?.column ?? 0,
40408
40481
  ...primarySpan ? {
40409
40482
  offset: primarySpan.offset,
40410
- length: primarySpan.length
40483
+ length: primarySpan.length,
40484
+ ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
40411
40485
  } : {},
40412
40486
  category,
40413
- ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
40487
+ ...matchByOccurrence ? { matchByOccurrence: true } : {},
40414
40488
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
40415
40489
  };
40416
40490
  }), rootDirectory);
@@ -40427,6 +40501,11 @@ const buildOxlintChildEnv = (sourceEnv) => {
40427
40501
  if (!isCompileCacheDisabled && !isCompileCacheAlreadySet) childEnv.NODE_COMPILE_CACHE = Path.join(os.tmpdir(), NODE_COMPILE_CACHE_DIR_NAME);
40428
40502
  return childEnv;
40429
40503
  };
40504
+ const buildProfiledNodeArguments = (input) => [
40505
+ ...input.cpuProfileDirectory ? ["--cpu-prof", `--cpu-prof-dir=${input.cpuProfileDirectory}`] : [],
40506
+ ...input.heapProfileDirectory ? ["--heap-prof", `--heap-prof-dir=${input.heapProfileDirectory}`] : [],
40507
+ ...input.argumentsList
40508
+ ];
40430
40509
  const SANITIZED_ENV = buildOxlintChildEnv(process.env);
40431
40510
  /**
40432
40511
  * Spawn one oxlint subprocess with hard ceilings on wall time and
@@ -40451,7 +40530,11 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
40451
40530
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
40452
40531
  return;
40453
40532
  }
40454
- const child = spawn(nodeBinaryPath, args, {
40533
+ const child = spawn(nodeBinaryPath, buildProfiledNodeArguments({
40534
+ argumentsList: args,
40535
+ cpuProfileDirectory: SANITIZED_ENV.REACT_DOCTOR_CPU_PROFILE_DIR,
40536
+ heapProfileDirectory: SANITIZED_ENV.REACT_DOCTOR_HEAP_PROFILE_DIR
40537
+ }), {
40455
40538
  cwd: rootDirectory,
40456
40539
  env: SANITIZED_ENV,
40457
40540
  stdio: [
@@ -41095,7 +41178,7 @@ const runOxlint = async (options) => {
41095
41178
  };
41096
41179
  }
41097
41180
  };
41098
- if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
41181
+ if (perFileLintCacheEnabled && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
41099
41182
  const rulesetHash = computeRulesetHash({
41100
41183
  config: buildConfig({
41101
41184
  extendsPaths: [],
@@ -41103,7 +41186,8 @@ const runOxlint = async (options) => {
41103
41186
  }),
41104
41187
  toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
41105
41188
  ignorePatterns: combinedPatterns,
41106
- tsconfigContent
41189
+ tsconfigContent,
41190
+ respectInlineDisables
41107
41191
  });
41108
41192
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
41109
41193
  const cacheKeyByFile = /* @__PURE__ */ new Map();
@@ -41146,7 +41230,8 @@ const runOxlint = async (options) => {
41146
41230
  }),
41147
41231
  toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
41148
41232
  ignorePatterns: combinedPatterns,
41149
- tsconfigContent
41233
+ tsconfigContent,
41234
+ respectInlineDisables
41150
41235
  })) : null;
41151
41236
  const probeAnswers = createSidecarProbeAnswerResolver({
41152
41237
  rootDirectory,
@@ -42186,7 +42271,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42186
42271
  directory: scanDirectory,
42187
42272
  repo
42188
42273
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
42189
- const lintIncludePaths = (input.skipJsxIncludeFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths], project)) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, project);
42274
+ const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
42190
42275
  const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
42191
42276
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
42192
42277
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
@@ -42681,7 +42766,7 @@ const runEditorScan = async (input) => {
42681
42766
  warnings,
42682
42767
  isCi: false,
42683
42768
  resolveLocalGithubViewerPermission: false,
42684
- skipJsxIncludeFilter: true
42769
+ skipExplicitIncludePathFilter: true
42685
42770
  }).pipe(withSpan("runEditorScan", { attributes: {
42686
42771
  "editor.lint": lint,
42687
42772
  "editor.runDeadCode": runDeadCode
@@ -42765,7 +42850,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
42765
42850
  /** Display name used in client-facing messages and progress titles. */
42766
42851
  const SERVER_DISPLAY_NAME = "React Doctor";
42767
42852
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
42768
- const SERVER_VERSION = "0.7.5";
42853
+ const SERVER_VERSION = "0.7.6";
42769
42854
  /** `Diagnostic.source` shown next to every published diagnostic. */
42770
42855
  const DIAGNOSTIC_SOURCE = "react-doctor";
42771
42856
  /**
@@ -43444,7 +43529,7 @@ const createLintCache = (input) => {
43444
43529
  let persistTimer = null;
43445
43530
  try {
43446
43531
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
43447
- if (parsed.version === 1 && parsed.fingerprint === input.fingerprint) for (const [fsPath, entry] of Object.entries(parsed.entries)) entries.set(fsPath, entry);
43532
+ if (parsed.version === 2 && parsed.fingerprint === input.fingerprint) for (const [fsPath, entry] of Object.entries(parsed.entries)) entries.set(fsPath, entry);
43448
43533
  } catch {}
43449
43534
  const persist = () => {
43450
43535
  if (persistTimer) {
@@ -43455,7 +43540,7 @@ const createLintCache = (input) => {
43455
43540
  dirty = false;
43456
43541
  try {
43457
43542
  const payload = {
43458
- version: 1,
43543
+ version: 2,
43459
43544
  fingerprint: input.fingerprint,
43460
43545
  entries: Object.fromEntries(entries)
43461
43546
  };
@@ -43468,14 +43553,14 @@ const createLintCache = (input) => {
43468
43553
  }
43469
43554
  };
43470
43555
  return {
43471
- lookup: (fsPath, stat) => {
43556
+ lookup: (fsPath, identity) => {
43472
43557
  const entry = entries.get(fsPath);
43473
- if (entry !== void 0 && entry.mtimeMs === stat.mtimeMs && entry.size === stat.size) return entry.diagnostics;
43558
+ if (entry !== void 0 && entry.contentHash === identity.contentHash) return entry.diagnostics;
43474
43559
  return null;
43475
43560
  },
43476
- store: (fsPath, stat, diagnostics) => {
43561
+ store: (fsPath, identity, diagnostics) => {
43477
43562
  entries.set(fsPath, {
43478
- ...stat,
43563
+ ...identity,
43479
43564
  diagnostics
43480
43565
  });
43481
43566
  dirty = true;
@@ -43564,16 +43649,9 @@ const resolveDiagnosticFsPath = (rawFilePath, scanDirectory, projectDirectory, o
43564
43649
  }
43565
43650
  return normalizeFsPath(absolute);
43566
43651
  };
43567
- const statSafe = (fsPath) => {
43568
- try {
43569
- const stat = fs.statSync(fsPath);
43570
- return {
43571
- mtimeMs: stat.mtimeMs,
43572
- size: stat.size
43573
- };
43574
- } catch {
43575
- return null;
43576
- }
43652
+ const readFileIdentity = (fsPath) => {
43653
+ const contentHash = hashFileContents(fsPath);
43654
+ return contentHash === null ? null : { contentHash };
43577
43655
  };
43578
43656
  /**
43579
43657
  * Outcome for a per-file request served without running oxlint (every
@@ -43599,7 +43677,7 @@ const outcomeWithoutScan = (request, byFile, requestedPaths) => ({
43599
43677
  * overlay tree (unsaved buffers) or disk, groups diagnostics by canonical
43600
43678
  * absolute path, and reports stale-detection metadata.
43601
43679
  *
43602
- * A persistent per-file lint cache (keyed by mtime + size, namespaced by a
43680
+ * A persistent per-file lint cache (keyed by content, namespaced by a
43603
43681
  * config fingerprint) short-circuits unchanged files so a re-opened editor
43604
43682
  * or repeated workspace scan skips the oxlint subprocess for everything it
43605
43683
  * hasn't edited. The cache applies only to disk-based, lint-only, per-file
@@ -43629,15 +43707,15 @@ const createScanRunner = (options) => {
43629
43707
  const requestedPaths = request.priority === "background" && !request.useOverlay && options.isOpen ? allRequested.filter((fsPath) => !options.isOpen?.(fsPath)) : allRequested;
43630
43708
  const cache = cacheEnabled && !isWholeProject && !request.useOverlay && !request.runDeadCode ? getCache(projectDirectory) : null;
43631
43709
  const byFile = /* @__PURE__ */ new Map();
43632
- const statByPath = /* @__PURE__ */ new Map();
43710
+ const identityByPath = /* @__PURE__ */ new Map();
43633
43711
  let filesToScan = requestedPaths;
43634
43712
  if (cache) {
43635
43713
  const uncached = [];
43636
43714
  for (const fsPath of requestedPaths) {
43637
- const stat = statSafe(fsPath);
43638
- if (stat) {
43639
- statByPath.set(fsPath, stat);
43640
- const hit = cache.lookup(fsPath, stat);
43715
+ const identity = readFileIdentity(fsPath);
43716
+ if (identity) {
43717
+ identityByPath.set(fsPath, identity);
43718
+ const hit = cache.lookup(fsPath, identity);
43641
43719
  if (hit !== null) {
43642
43720
  if (hit.length > 0) byFile.set(fsPath, hit);
43643
43721
  continue;
@@ -43680,9 +43758,9 @@ const createScanRunner = (options) => {
43680
43758
  }
43681
43759
  if (cache && result.ok && !result.skipped && !result.didLintFail && result.lintPartialFailures.length === 0) {
43682
43760
  for (const fsPath of filesToScan) {
43683
- const stat = statByPath.get(fsPath);
43684
- if (!stat) continue;
43685
- cache.store(fsPath, stat, byFile.get(fsPath) ?? []);
43761
+ const identity = identityByPath.get(fsPath);
43762
+ if (!identity) continue;
43763
+ cache.store(fsPath, identity, byFile.get(fsPath) ?? []);
43686
43764
  }
43687
43765
  cache.schedulePersist();
43688
43766
  }
@@ -44816,5 +44894,5 @@ const startLanguageServer = () => {
44816
44894
  };
44817
44895
  //#endregion
44818
44896
  export { startLanguageServer };
44819
- !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]="c7260006-9441-53b6-807e-078a5823b211")}catch(e){}}();
44820
- //# debugId=c7260006-9441-53b6-807e-078a5823b211
44897
+ !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]="288f7240-2ee1-5bcb-a56e-bca2b972818c")}catch(e){}}();
44898
+ //# debugId=288f7240-2ee1-5bcb-a56e-bca2b972818c
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.7.5",
3
+ "version": "0.7.6-dev.0a8d30b",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -65,8 +65,8 @@
65
65
  "vscode-languageserver-textdocument": "^1.0.12",
66
66
  "vscode-uri": "^3.1.0",
67
67
  "yaml": "^2.9.0",
68
- "deslop-js": "0.7.5",
69
- "oxlint-plugin-react-doctor": "0.7.5"
68
+ "deslop-js": "0.7.6",
69
+ "oxlint-plugin-react-doctor": "0.7.6-dev.0a8d30b"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/babel__code-frame": "^7.27.0",
@@ -74,9 +74,9 @@
74
74
  "@xterm/headless": "^6.0.0",
75
75
  "commander": "^14.0.3",
76
76
  "ora": "^9.4.0",
77
- "@react-doctor/api": "0.7.5",
78
- "@react-doctor/core": "0.7.5",
79
- "@react-doctor/language-server": "0.7.5"
77
+ "@react-doctor/api": "0.7.6",
78
+ "@react-doctor/language-server": "0.7.6",
79
+ "@react-doctor/core": "0.7.6"
80
80
  },
81
81
  "engines": {
82
82
  "node": "^20.19.0 || >=22.13.0"