react-doctor 0.7.6-dev.4fb3694 → 0.7.6-dev.543ebfe

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]="bc56f5c4-5b95-5727-8e86-d43a534abbbc")}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]="5dcb5b0b-ca1e-5655-8f52-3c22b421256d")}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,10 +36341,12 @@ 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;
36344
36346
  const cachedPackageJsons = /* @__PURE__ */ new Map();
36345
36347
  const readPackageJsonUncached = (packageJsonPath) => {
36346
36348
  try {
36347
- return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
36349
+ return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
36348
36350
  } catch (error) {
36349
36351
  if (error instanceof SyntaxError) return {};
36350
36352
  if (isErrnoException(error)) {
@@ -40017,7 +40019,6 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
40017
40019
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
40018
40020
  const PACKAGE_JSON_FILE = "package.json";
40019
40021
  const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
40020
- const UTF8_BOM_CHAR = "";
40021
40022
  const HARDENING_SETTING_KEYS = new Set([
40022
40023
  "minimumReleaseAge",
40023
40024
  "blockExoticSubdeps",
@@ -40042,12 +40043,11 @@ const stripInlineComment = (rawValue) => {
40042
40043
  return rawValue;
40043
40044
  };
40044
40045
  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 = stripBom(content).split(/\r?\n/);
40050
+ const lines = stripUtf8Bom(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 = NFS.readFileSync(packageJsonPath, "utf-8");
40084
+ const packageJsonRaw = stripUtf8Bom(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 {
@@ -41823,6 +41823,7 @@ const parseGithubViewerPermission = (stdout) => {
41823
41823
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
41824
41824
  };
41825
41825
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
41826
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
41826
41827
  /**
41827
41828
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
41828
41829
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -41893,6 +41894,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
41893
41894
  args,
41894
41895
  directory
41895
41896
  });
41897
+ const listUntrackedFilePaths = (directory, includePaths = []) => runGit(directory, [
41898
+ "ls-files",
41899
+ "-z",
41900
+ "--others",
41901
+ "--exclude-standard",
41902
+ ...includePaths.length > 0 ? ["--", ...includePaths] : []
41903
+ ]).pipe(map$3((result) => result.status === 0 ? splitNullSeparated(result.stdout) : null));
41904
+ const mergeUntracked = (directory, trackedFilePaths, includeUntracked) => includeUntracked ? listUntrackedFilePaths(directory).pipe(map$3((untracked) => untracked === null ? trackedFilePaths : [...new Set([...trackedFilePaths, ...untracked])])) : succeed$2(trackedFilePaths);
41896
41905
  const currentBranch = (directory) => runGit(directory, [
41897
41906
  "rev-parse",
41898
41907
  "--abbrev-ref",
@@ -42015,7 +42024,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42015
42024
  githubViewerPermission,
42016
42025
  branchExists,
42017
42026
  mergeBase,
42018
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
42027
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
42019
42028
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
42020
42029
  if (explicitBaseBranch !== void 0) {
42021
42030
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -42044,7 +42053,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42044
42053
  "HEAD"
42045
42054
  ]);
42046
42055
  if (uncommitted.status !== 0) return null;
42047
- const files = splitNullSeparated(uncommitted.stdout);
42056
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
42048
42057
  if (files.length === 0) return null;
42049
42058
  return {
42050
42059
  currentBranch: resolvedCurrentBranch,
@@ -42074,7 +42083,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42074
42083
  currentBranch: resolvedCurrentBranch,
42075
42084
  baseBranch,
42076
42085
  diffBaseRef: mergeBaseRef,
42077
- changedFiles: splitNullSeparated(diff.stdout),
42086
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
42078
42087
  isCurrentChanges: false
42079
42088
  };
42080
42089
  }).pipe(withSpan("Git.diffSelection")),
@@ -42120,7 +42129,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42120
42129
  stdout: result.stdout
42121
42130
  };
42122
42131
  }).pipe(withSpan("Git.grep")),
42123
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
42132
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
42124
42133
  if (files.length === 0) return [];
42125
42134
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
42126
42135
  const result = yield* runGit(directory, [
@@ -42134,7 +42143,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
42134
42143
  ...files
42135
42144
  ]);
42136
42145
  if (result.status !== 0) return null;
42137
- return parseChangedLineRanges(result.stdout);
42146
+ const changedLineRanges = parseChangedLineRanges(result.stdout);
42147
+ if (cached || !includeUntracked) return changedLineRanges;
42148
+ const untrackedFilePaths = yield* listUntrackedFilePaths(directory, files);
42149
+ if (untrackedFilePaths === null) return changedLineRanges;
42150
+ return [...changedLineRanges, ...untrackedFilePaths.map((file) => ({
42151
+ file,
42152
+ ranges: [[1, UNTRACKED_FILE_LAST_LINE]]
42153
+ }))];
42138
42154
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
42139
42155
  });
42140
42156
  })).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
@@ -42886,6 +42902,12 @@ const mapWithConcurrency = async (items, concurrency, task) => {
42886
42902
  if (errors.length > 0) throw errors[0];
42887
42903
  return results;
42888
42904
  };
42905
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
42906
+ let lineNumber = 1;
42907
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
42908
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === 10) lineNumber++;
42909
+ return lineNumber;
42910
+ };
42889
42911
  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";
42890
42912
  const ACCESSORS_MIN_VERSION = "3.15.0";
42891
42913
  const supportsAccessors = (reanimatedVersion) => {
@@ -43457,13 +43479,6 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
43457
43479
  visit(sourceFile);
43458
43480
  return foundRange;
43459
43481
  };
43460
- const NEWLINE_BYTE = 10;
43461
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
43462
- let lineNumber = 1;
43463
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
43464
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
43465
- return lineNumber;
43466
- };
43467
43482
  /**
43468
43483
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
43469
43484
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -43668,8 +43683,33 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43668
43683
  }
43669
43684
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
43670
43685
  const minifiedFileCache = /* @__PURE__ */ new Map();
43686
+ const sourceBufferCache = /* @__PURE__ */ new Map();
43687
+ const sourceLinesCache = /* @__PURE__ */ new Map();
43688
+ const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
43689
+ const readSourceBuffer = (filename) => {
43690
+ const absolutePath = resolveAbsolutePath(filename);
43691
+ const cached = sourceBufferCache.get(absolutePath);
43692
+ if (cached !== void 0) return cached;
43693
+ let sourceBuffer;
43694
+ try {
43695
+ sourceBuffer = NFS.readFileSync(absolutePath);
43696
+ } catch {
43697
+ sourceBuffer = null;
43698
+ }
43699
+ sourceBufferCache.set(absolutePath, sourceBuffer);
43700
+ return sourceBuffer;
43701
+ };
43702
+ const readSourceLines = (filename) => {
43703
+ const absolutePath = resolveAbsolutePath(filename);
43704
+ const cached = sourceLinesCache.get(absolutePath);
43705
+ if (cached !== void 0) return cached;
43706
+ const sourceBuffer = readSourceBuffer(filename);
43707
+ const sourceLines = sourceBuffer ? sourceBuffer.toString("utf8").split("\n") : null;
43708
+ sourceLinesCache.set(absolutePath, sourceLines);
43709
+ return sourceLines;
43710
+ };
43671
43711
  const isMinifiedDiagnosticFile = (filename) => {
43672
- const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
43712
+ const absolutePath = resolveAbsolutePath(filename);
43673
43713
  const cached = minifiedFileCache.get(absolutePath);
43674
43714
  if (cached !== void 0) return cached;
43675
43715
  const minified = isMinifiedSource(absolutePath);
@@ -43682,8 +43722,17 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43682
43722
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
43683
43723
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
43684
43724
  const primarySpan = primaryLabel?.span;
43725
+ const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
43685
43726
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
43686
43727
  const category = resolveDiagnosticCategory(plugin, rule);
43728
+ const matchByOccurrence = resolveMatchByOccurrence(rule, category);
43729
+ const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
43730
+ const primaryLineIndex = primarySpan ? primarySpan.line - 1 : -1;
43731
+ const primaryLine = sourceLines?.[primaryLineIndex];
43732
+ const primaryColumnIndex = primarySpan ? primarySpan.column - 1 : -1;
43733
+ const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
43734
+ const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
43735
+ const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
43687
43736
  return {
43688
43737
  filePath: normalizedFilePath,
43689
43738
  plugin,
@@ -43697,10 +43746,11 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43697
43746
  column: primarySpan?.column ?? 0,
43698
43747
  ...primarySpan ? {
43699
43748
  offset: primarySpan.offset,
43700
- length: primarySpan.length
43749
+ length: primarySpan.length,
43750
+ ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
43701
43751
  } : {},
43702
43752
  category,
43703
- ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
43753
+ ...matchByOccurrence ? { matchByOccurrence: true } : {},
43704
43754
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
43705
43755
  };
43706
43756
  }), rootDirectory);
@@ -46232,10 +46282,11 @@ const computeDiagnosticDelta = (input) => {
46232
46282
  * render diff-base mistakes (`GitBaseBranchInvalid` /
46233
46283
  * `GitBaseBranchMissing`) as clean user errors instead of crashes.
46234
46284
  */
46235
- const getDiffInfo = (directory, explicitBaseBranch) => runPromise(gen(function* () {
46285
+ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runPromise(gen(function* () {
46236
46286
  const selection = yield* (yield* Git).diffSelection({
46237
46287
  directory,
46238
- explicitBaseBranch
46288
+ explicitBaseBranch,
46289
+ includeUntracked
46239
46290
  });
46240
46291
  if (selection === null) return null;
46241
46292
  return {
@@ -46665,7 +46716,7 @@ const makeNoopConsole = () => ({
46665
46716
  });
46666
46717
  //#endregion
46667
46718
  //#region src/cli/utils/version.ts
46668
- const VERSION = "0.7.6-dev.4fb3694";
46719
+ const VERSION = "0.7.6-dev.543ebfe";
46669
46720
  //#endregion
46670
46721
  //#region src/cli/utils/json-mode.ts
46671
46722
  let context = null;
@@ -47034,13 +47085,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
47034
47085
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
47035
47086
  * standard `SENTRY_RELEASE` override.
47036
47087
  */
47037
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.6-dev.4fb3694`;
47088
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.6-dev.543ebfe`;
47038
47089
  /**
47039
47090
  * Deployment environment shown in Sentry's environment filter. Defaults to
47040
47091
  * `production` for tagged releases and `development` for dev/unbuilt versions,
47041
47092
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
47042
47093
  */
47043
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.6-dev.4fb3694") ? "development" : "production");
47094
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.6-dev.543ebfe") ? "development" : "production");
47044
47095
  /**
47045
47096
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
47046
47097
  * (set to `0` to disable tracing) and falls back to
@@ -50805,7 +50856,7 @@ const BLOCKING_SUMMARY = {
50805
50856
  const SCOPE_SUMMARY = {
50806
50857
  full: "Scan the whole project on every run",
50807
50858
  files: "Report every issue in changed files",
50808
- lines: "Report issues on changed lines",
50859
+ lines: "Report issues whose source spans touch changed lines",
50809
50860
  changed: "Report only the issues a change introduces"
50810
50861
  };
50811
50862
  const gatesEqual = (left, right) => left.blocking === right.blocking && left.scope === right.scope && left.comment === right.comment && left.reviewComments === right.reviewComments && left.commitStatus === right.commitStatus;
@@ -59675,7 +59726,8 @@ const buildScanAttributes = (input) => {
59675
59726
  rulesConfigured: ruleKeys.length,
59676
59727
  rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
59677
59728
  fileCount: input.result?.scannedFileCount ?? null,
59678
- nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length ?? null
59729
+ nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length ?? null,
59730
+ multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length ?? null
59679
59731
  });
59680
59732
  };
59681
59733
  /**
@@ -59744,6 +59796,12 @@ const countDeadlineSkippedFiles = (lintPartialFailures) => sumFileCountsMatching
59744
59796
  const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
59745
59797
  const countDroppedLintFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DROPPED_FILES_MESSAGE_PATTERN);
59746
59798
  //#endregion
59799
+ //#region src/cli/utils/diagnostic-intersects-line-ranges.ts
59800
+ const diagnosticIntersectsLineRanges = (diagnostic, lineRanges) => {
59801
+ const diagnosticEndLine = Math.max(diagnostic.line, diagnostic.endLine ?? diagnostic.line);
59802
+ return lineRanges.some(([rangeStartLine, rangeEndLine]) => diagnostic.line <= rangeEndLine && diagnosticEndLine >= rangeStartLine);
59803
+ };
59804
+ //#endregion
59747
59805
  //#region src/cli/utils/materialize-baseline-files.ts
59748
59806
  /**
59749
59807
  * Materializes the `ref` version of `files` into `tempDirectory` (via
@@ -60929,7 +60987,7 @@ const buildChangedLineMatcher = (directory, changedLineRanges) => {
60929
60987
  const relativePath = toForwardSlashes(path.isAbsolute(diagnostic.filePath) ? path.relative(directory, diagnostic.filePath) : diagnostic.filePath);
60930
60988
  const ranges = rangesByFile.get(relativePath);
60931
60989
  if (ranges === void 0) return false;
60932
- return ranges.some(([start, end]) => diagnostic.line >= start && diagnostic.line <= end);
60990
+ return diagnosticIntersectsLineRanges(diagnostic, ranges);
60933
60991
  };
60934
60992
  };
60935
60993
  const buildIgnoredTags = (userConfig) => {
@@ -63751,10 +63809,15 @@ const warnIfAiTrainingEnvironment = () => {
63751
63809
  //#region src/cli/utils/validate-mode-flags.ts
63752
63810
  const usedDiffAlias = (flags) => flags.diff !== void 0 && flags.diff !== false && flags.diff !== "false" && flags.diff !== "";
63753
63811
  const usedScope = (flags) => typeof flags.scope === "string" && flags.scope.length > 0;
63812
+ const validateIncludeUntrackedScope = (includeUntracked, scope) => {
63813
+ if (!includeUntracked || scope !== void 0 && scope !== "full") return;
63814
+ throw new CliInputError("--include-untracked requires the files, changed, or lines scope (via --scope, --diff, or config).");
63815
+ };
63754
63816
  const validateModeFlags = (flags) => {
63755
63817
  if (usedScope(flags) && usedDiffAlias(flags)) throw new CliInputError("Cannot combine --scope and --diff; --diff is the deprecated alias.");
63756
63818
  if (flags.staged && usedDiffAlias(flags)) throw new CliInputError("Cannot combine --staged and --diff; pick one mode.");
63757
63819
  if (flags.staged && (flags.scope === "full" || flags.scope === "changed")) throw new CliInputError(`Cannot combine --staged with --scope ${flags.scope}; use --scope files or --scope lines, or drop --scope.`);
63820
+ if (flags.includeUntracked && flags.staged) throw new CliInputError("Cannot combine --include-untracked with --staged; the git index never holds untracked files.");
63758
63821
  if (flags.score && flags.json) throw new CliInputError("Cannot combine --score and --json; pick one output mode.");
63759
63822
  if (flags.score && flags.telemetry === false) throw new CliInputError("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
63760
63823
  if (flags.debug && (flags.score === false || flags.telemetry === false)) {
@@ -63964,13 +64027,15 @@ const inspectAction = async (directory, flags) => {
63964
64027
  const projectDirectories = await selectProjects(resolvedDirectory, flags.project, skipPrompts, userConfig?.projects);
63965
64028
  const changedFilesDiffInfo = flags.changedFilesFrom ? buildChangedFilesDiffInfo(readChangedFilesFrom(Path.resolve(flags.changedFilesFrom))) : null;
63966
64029
  const requestedScope = resolveScope(flags, userConfig);
64030
+ const includeUntracked = flags.includeUntracked ?? false;
63967
64031
  const scopeRequest = requestedScope.scope === void 0 && changedFilesDiffInfo !== null ? {
63968
64032
  ...requestedScope,
63969
64033
  scope: "changed"
63970
64034
  } : requestedScope;
64035
+ validateIncludeUntrackedScope(includeUntracked, scopeRequest.scope);
63971
64036
  const wantsDiffMode = scopeRequest.scope !== void 0 && scopeRequest.scope !== "full";
63972
64037
  const shouldDetectDiff = changedFilesDiffInfo === null && (wantsDiffMode || scopeRequest.scope === void 0 && !skipPrompts && !isQuiet);
63973
- const diffInfo = changedFilesDiffInfo ?? (shouldDetectDiff ? await getDiffInfo(resolvedDirectory, scopeRequest.base) : null);
64038
+ const diffInfo = changedFilesDiffInfo ?? (shouldDetectDiff ? await getDiffInfo(resolvedDirectory, scopeRequest.base, includeUntracked) : null);
63974
64039
  const scope = await finalizeScope({
63975
64040
  requested: scopeRequest,
63976
64041
  diffInfo,
@@ -63984,7 +64049,8 @@ const inspectAction = async (directory, flags) => {
63984
64049
  const changedLineRanges = scope === "lines" && diffInfo !== null && (diffInfo.isCurrentChanges || linesBaseRef !== null) && diffInfo !== null ? await getChangedLineRanges({
63985
64050
  directory: resolvedDirectory,
63986
64051
  baseRef: linesBaseRef ?? void 0,
63987
- files: [...diffInfo.changedFiles]
64052
+ files: [...diffInfo.changedFiles],
64053
+ includeUntracked
63988
64054
  }) : null;
63989
64055
  if (scope === "lines" && changedLineRanges === null && !isQuiet) {
63990
64056
  cliLogger.warn("Could not determine changed lines (no base ref or git diff failed); reporting all issues in changed files.");
@@ -64917,6 +64983,7 @@ const ROOT_FLAG_SPEC = {
64917
64983
  "--dead-code",
64918
64984
  "--debug",
64919
64985
  "--help",
64986
+ "--include-untracked",
64920
64987
  "--json",
64921
64988
  "--json-compact",
64922
64989
  "--lint",
@@ -65180,7 +65247,7 @@ ${highlighter.dim("Learn more:")}
65180
65247
  ${highlighter.info(CI_URL)}
65181
65248
  `;
65182
65249
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
65183
- 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 (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);
65250
+ 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)").option("--include-untracked", "with --scope files/changed/lines, also scan ordinary untracked files (respects .gitignore)").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
65251
  program.action(inspectAction);
65185
65252
  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));
65186
65253
  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);
@@ -65230,4 +65297,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
65230
65297
  export {};
65231
65298
 
65232
65299
  //# sourceMappingURL=cli.js.map
65233
- //# debugId=bc56f5c4-5b95-5727-8e86-d43a534abbbc
65300
+ //# debugId=5dcb5b0b-ca1e-5655-8f52-3c22b421256d
package/dist/index.d.ts CHANGED
@@ -10256,7 +10256,7 @@ declare const buildJsonReport: (input: BuildJsonReportInput) => JsonReportV3$1;
10256
10256
  * render diff-base mistakes (`GitBaseBranchInvalid` /
10257
10257
  * `GitBaseBranchMissing`) as clean user errors instead of crashes.
10258
10258
  */
10259
- declare const getDiffInfo: (directory: string, explicitBaseBranch?: string) => Promise<DiffInfo | null>;
10259
+ declare const getDiffInfo: (directory: string, explicitBaseBranch?: string, includeUntracked?: boolean) => Promise<DiffInfo | null>;
10260
10260
  declare const filterSourceFiles: (filePaths: string[]) => string[];
10261
10261
  /**
10262
10262
  * Programmatic façade over `Git.changedLineRanges` (the `lines` scope). Diffs
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]="3e7262c1-34a6-5fd7-a4a2-5e7a15ca735d")}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]="80f981d4-ebf0-5bc0-9643-8a68d27626a5")}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";
@@ -33142,13 +33142,15 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33142
33142
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33143
33143
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33144
33144
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33145
+ const UTF8_BOM_CHARACTER = "";
33146
+ const stripUtf8Bom = (content) => content.startsWith(UTF8_BOM_CHARACTER) ? content.slice(1) : content;
33145
33147
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33146
33148
  const clearPackageJsonCache = () => {
33147
33149
  cachedPackageJsons.clear();
33148
33150
  };
33149
33151
  const readPackageJsonUncached = (packageJsonPath) => {
33150
33152
  try {
33151
- return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
33153
+ return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
33152
33154
  } catch (error) {
33153
33155
  if (error instanceof SyntaxError) return {};
33154
33156
  if (isErrnoException(error)) {
@@ -36789,7 +36791,6 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
36789
36791
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
36790
36792
  const PACKAGE_JSON_FILE = "package.json";
36791
36793
  const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
36792
- const UTF8_BOM_CHAR = "";
36793
36794
  const HARDENING_SETTING_KEYS = new Set([
36794
36795
  "minimumReleaseAge",
36795
36796
  "blockExoticSubdeps",
@@ -36814,12 +36815,11 @@ const stripInlineComment = (rawValue) => {
36814
36815
  return rawValue;
36815
36816
  };
36816
36817
  const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
36817
- const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
36818
36818
  const parseHardeningSettings = (content) => {
36819
36819
  let minimumReleaseAge = null;
36820
36820
  let blockExoticSubdeps = null;
36821
36821
  let trustPolicy = null;
36822
- const lines = stripBom(content).split(/\r?\n/);
36822
+ const lines = stripUtf8Bom(content).split(/\r?\n/);
36823
36823
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
36824
36824
  const lineText = lines[lineIndex];
36825
36825
  if (lineText === void 0) continue;
@@ -36853,7 +36853,7 @@ const isPnpmManagedProject = (rootDirectory) => {
36853
36853
  const packageJsonPath = Path.join(rootDirectory, PACKAGE_JSON_FILE);
36854
36854
  if (!isFile(packageJsonPath)) return false;
36855
36855
  try {
36856
- const packageJsonRaw = NFS.readFileSync(packageJsonPath, "utf-8");
36856
+ const packageJsonRaw = stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8"));
36857
36857
  const packageJson = JSON.parse(packageJsonRaw);
36858
36858
  if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
36859
36859
  } catch {
@@ -38579,6 +38579,7 @@ const parseGithubViewerPermission = (stdout) => {
38579
38579
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
38580
38580
  };
38581
38581
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
38582
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
38582
38583
  /**
38583
38584
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
38584
38585
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -38649,6 +38650,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38649
38650
  args,
38650
38651
  directory
38651
38652
  });
38653
+ const listUntrackedFilePaths = (directory, includePaths = []) => runGit(directory, [
38654
+ "ls-files",
38655
+ "-z",
38656
+ "--others",
38657
+ "--exclude-standard",
38658
+ ...includePaths.length > 0 ? ["--", ...includePaths] : []
38659
+ ]).pipe(map$3((result) => result.status === 0 ? splitNullSeparated(result.stdout) : null));
38660
+ const mergeUntracked = (directory, trackedFilePaths, includeUntracked) => includeUntracked ? listUntrackedFilePaths(directory).pipe(map$3((untracked) => untracked === null ? trackedFilePaths : [...new Set([...trackedFilePaths, ...untracked])])) : succeed$2(trackedFilePaths);
38652
38661
  const currentBranch = (directory) => runGit(directory, [
38653
38662
  "rev-parse",
38654
38663
  "--abbrev-ref",
@@ -38771,7 +38780,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38771
38780
  githubViewerPermission,
38772
38781
  branchExists,
38773
38782
  mergeBase,
38774
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
38783
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
38775
38784
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
38776
38785
  if (explicitBaseBranch !== void 0) {
38777
38786
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -38800,7 +38809,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38800
38809
  "HEAD"
38801
38810
  ]);
38802
38811
  if (uncommitted.status !== 0) return null;
38803
- const files = splitNullSeparated(uncommitted.stdout);
38812
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
38804
38813
  if (files.length === 0) return null;
38805
38814
  return {
38806
38815
  currentBranch: resolvedCurrentBranch,
@@ -38830,7 +38839,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38830
38839
  currentBranch: resolvedCurrentBranch,
38831
38840
  baseBranch,
38832
38841
  diffBaseRef: mergeBaseRef,
38833
- changedFiles: splitNullSeparated(diff.stdout),
38842
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
38834
38843
  isCurrentChanges: false
38835
38844
  };
38836
38845
  }).pipe(withSpan("Git.diffSelection")),
@@ -38876,7 +38885,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38876
38885
  stdout: result.stdout
38877
38886
  };
38878
38887
  }).pipe(withSpan("Git.grep")),
38879
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
38888
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
38880
38889
  if (files.length === 0) return [];
38881
38890
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
38882
38891
  const result = yield* runGit(directory, [
@@ -38890,7 +38899,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38890
38899
  ...files
38891
38900
  ]);
38892
38901
  if (result.status !== 0) return null;
38893
- return parseChangedLineRanges(result.stdout);
38902
+ const changedLineRanges = parseChangedLineRanges(result.stdout);
38903
+ if (cached || !includeUntracked) return changedLineRanges;
38904
+ const untrackedFilePaths = yield* listUntrackedFilePaths(directory, files);
38905
+ if (untrackedFilePaths === null) return changedLineRanges;
38906
+ return [...changedLineRanges, ...untrackedFilePaths.map((file) => ({
38907
+ file,
38908
+ ranges: [[1, UNTRACKED_FILE_LAST_LINE]]
38909
+ }))];
38894
38910
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38895
38911
  });
38896
38912
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
@@ -39642,6 +39658,12 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39642
39658
  if (errors.length > 0) throw errors[0];
39643
39659
  return results;
39644
39660
  };
39661
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
39662
+ let lineNumber = 1;
39663
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
39664
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === 10) lineNumber++;
39665
+ return lineNumber;
39666
+ };
39645
39667
  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";
39646
39668
  const ACCESSORS_MIN_VERSION = "3.15.0";
39647
39669
  const supportsAccessors = (reanimatedVersion) => {
@@ -40213,13 +40235,6 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40213
40235
  visit(sourceFile);
40214
40236
  return foundRange;
40215
40237
  };
40216
- const NEWLINE_BYTE = 10;
40217
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40218
- let lineNumber = 1;
40219
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40220
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40221
- return lineNumber;
40222
- };
40223
40238
  /**
40224
40239
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40225
40240
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -40424,8 +40439,33 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40424
40439
  }
40425
40440
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40426
40441
  const minifiedFileCache = /* @__PURE__ */ new Map();
40442
+ const sourceBufferCache = /* @__PURE__ */ new Map();
40443
+ const sourceLinesCache = /* @__PURE__ */ new Map();
40444
+ const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40445
+ const readSourceBuffer = (filename) => {
40446
+ const absolutePath = resolveAbsolutePath(filename);
40447
+ const cached = sourceBufferCache.get(absolutePath);
40448
+ if (cached !== void 0) return cached;
40449
+ let sourceBuffer;
40450
+ try {
40451
+ sourceBuffer = NFS.readFileSync(absolutePath);
40452
+ } catch {
40453
+ sourceBuffer = null;
40454
+ }
40455
+ sourceBufferCache.set(absolutePath, sourceBuffer);
40456
+ return sourceBuffer;
40457
+ };
40458
+ const readSourceLines = (filename) => {
40459
+ const absolutePath = resolveAbsolutePath(filename);
40460
+ const cached = sourceLinesCache.get(absolutePath);
40461
+ if (cached !== void 0) return cached;
40462
+ const sourceBuffer = readSourceBuffer(filename);
40463
+ const sourceLines = sourceBuffer ? sourceBuffer.toString("utf8").split("\n") : null;
40464
+ sourceLinesCache.set(absolutePath, sourceLines);
40465
+ return sourceLines;
40466
+ };
40427
40467
  const isMinifiedDiagnosticFile = (filename) => {
40428
- const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40468
+ const absolutePath = resolveAbsolutePath(filename);
40429
40469
  const cached = minifiedFileCache.get(absolutePath);
40430
40470
  if (cached !== void 0) return cached;
40431
40471
  const minified = isMinifiedSource(absolutePath);
@@ -40438,8 +40478,17 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40438
40478
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
40439
40479
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
40440
40480
  const primarySpan = primaryLabel?.span;
40481
+ const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
40441
40482
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
40442
40483
  const category = resolveDiagnosticCategory(plugin, rule);
40484
+ const matchByOccurrence = resolveMatchByOccurrence(rule, category);
40485
+ const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
40486
+ const primaryLineIndex = primarySpan ? primarySpan.line - 1 : -1;
40487
+ const primaryLine = sourceLines?.[primaryLineIndex];
40488
+ const primaryColumnIndex = primarySpan ? primarySpan.column - 1 : -1;
40489
+ const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
40490
+ const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
40491
+ const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
40443
40492
  return {
40444
40493
  filePath: normalizedFilePath,
40445
40494
  plugin,
@@ -40453,10 +40502,11 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40453
40502
  column: primarySpan?.column ?? 0,
40454
40503
  ...primarySpan ? {
40455
40504
  offset: primarySpan.offset,
40456
- length: primarySpan.length
40505
+ length: primarySpan.length,
40506
+ ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
40457
40507
  } : {},
40458
40508
  category,
40459
- ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
40509
+ ...matchByOccurrence ? { matchByOccurrence: true } : {},
40460
40510
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
40461
40511
  };
40462
40512
  }), rootDirectory);
@@ -42914,10 +42964,11 @@ const buildSkippedChecks = (input) => {
42914
42964
  * render diff-base mistakes (`GitBaseBranchInvalid` /
42915
42965
  * `GitBaseBranchMissing`) as clean user errors instead of crashes.
42916
42966
  */
42917
- const getDiffInfo = (directory, explicitBaseBranch) => runPromise(gen(function* () {
42967
+ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runPromise(gen(function* () {
42918
42968
  const selection = yield* (yield* Git).diffSelection({
42919
42969
  directory,
42920
- explicitBaseBranch
42970
+ explicitBaseBranch,
42971
+ includeUntracked
42921
42972
  });
42922
42973
  if (selection === null) return null;
42923
42974
  return {
@@ -43207,4 +43258,4 @@ const toJsonReport = (result, options) => buildJsonReport({
43207
43258
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
43208
43259
 
43209
43260
  //# sourceMappingURL=index.js.map
43210
- //# debugId=3e7262c1-34a6-5fd7-a4a2-5e7a15ca735d
43261
+ //# debugId=80f981d4-ebf0-5bc0-9643-8a68d27626a5
package/dist/lsp.js CHANGED
@@ -33194,13 +33194,15 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33194
33194
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33195
33195
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33196
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;
33197
33199
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33198
33200
  const clearPackageJsonCache = () => {
33199
33201
  cachedPackageJsons.clear();
33200
33202
  };
33201
33203
  const readPackageJsonUncached = (packageJsonPath) => {
33202
33204
  try {
33203
- return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
33205
+ return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
33204
33206
  } catch (error) {
33205
33207
  if (error instanceof SyntaxError) return {};
33206
33208
  if (isErrnoException(error)) {
@@ -36767,7 +36769,6 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
36767
36769
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
36768
36770
  const PACKAGE_JSON_FILE = "package.json";
36769
36771
  const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
36770
- const UTF8_BOM_CHAR = "";
36771
36772
  const HARDENING_SETTING_KEYS = new Set([
36772
36773
  "minimumReleaseAge",
36773
36774
  "blockExoticSubdeps",
@@ -36792,12 +36793,11 @@ const stripInlineComment = (rawValue) => {
36792
36793
  return rawValue;
36793
36794
  };
36794
36795
  const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
36795
- const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
36796
36796
  const parseHardeningSettings = (content) => {
36797
36797
  let minimumReleaseAge = null;
36798
36798
  let blockExoticSubdeps = null;
36799
36799
  let trustPolicy = null;
36800
- const lines = stripBom(content).split(/\r?\n/);
36800
+ const lines = stripUtf8Bom(content).split(/\r?\n/);
36801
36801
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
36802
36802
  const lineText = lines[lineIndex];
36803
36803
  if (lineText === void 0) continue;
@@ -36831,7 +36831,7 @@ const isPnpmManagedProject = (rootDirectory) => {
36831
36831
  const packageJsonPath = Path.join(rootDirectory, PACKAGE_JSON_FILE);
36832
36832
  if (!isFile(packageJsonPath)) return false;
36833
36833
  try {
36834
- const packageJsonRaw = NFS.readFileSync(packageJsonPath, "utf-8");
36834
+ const packageJsonRaw = stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8"));
36835
36835
  const packageJson = JSON.parse(packageJsonRaw);
36836
36836
  if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
36837
36837
  } catch {
@@ -38557,6 +38557,7 @@ const parseGithubViewerPermission = (stdout) => {
38557
38557
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
38558
38558
  };
38559
38559
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
38560
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
38560
38561
  /**
38561
38562
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
38562
38563
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -38627,6 +38628,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38627
38628
  args,
38628
38629
  directory
38629
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);
38630
38639
  const currentBranch = (directory) => runGit(directory, [
38631
38640
  "rev-parse",
38632
38641
  "--abbrev-ref",
@@ -38749,7 +38758,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38749
38758
  githubViewerPermission,
38750
38759
  branchExists,
38751
38760
  mergeBase,
38752
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
38761
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
38753
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." }) }));
38754
38763
  if (explicitBaseBranch !== void 0) {
38755
38764
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -38778,7 +38787,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38778
38787
  "HEAD"
38779
38788
  ]);
38780
38789
  if (uncommitted.status !== 0) return null;
38781
- const files = splitNullSeparated(uncommitted.stdout);
38790
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
38782
38791
  if (files.length === 0) return null;
38783
38792
  return {
38784
38793
  currentBranch: resolvedCurrentBranch,
@@ -38808,7 +38817,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38808
38817
  currentBranch: resolvedCurrentBranch,
38809
38818
  baseBranch,
38810
38819
  diffBaseRef: mergeBaseRef,
38811
- changedFiles: splitNullSeparated(diff.stdout),
38820
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
38812
38821
  isCurrentChanges: false
38813
38822
  };
38814
38823
  }).pipe(withSpan("Git.diffSelection")),
@@ -38854,7 +38863,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38854
38863
  stdout: result.stdout
38855
38864
  };
38856
38865
  }).pipe(withSpan("Git.grep")),
38857
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
38866
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
38858
38867
  if (files.length === 0) return [];
38859
38868
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
38860
38869
  const result = yield* runGit(directory, [
@@ -38868,7 +38877,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38868
38877
  ...files
38869
38878
  ]);
38870
38879
  if (result.status !== 0) return null;
38871
- 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
+ }))];
38872
38888
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38873
38889
  });
38874
38890
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
@@ -39620,6 +39636,12 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39620
39636
  if (errors.length > 0) throw errors[0];
39621
39637
  return results;
39622
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
+ };
39623
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";
39624
39646
  const ACCESSORS_MIN_VERSION = "3.15.0";
39625
39647
  const supportsAccessors = (reanimatedVersion) => {
@@ -40191,13 +40213,6 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40191
40213
  visit(sourceFile);
40192
40214
  return foundRange;
40193
40215
  };
40194
- const NEWLINE_BYTE = 10;
40195
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40196
- let lineNumber = 1;
40197
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40198
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40199
- return lineNumber;
40200
- };
40201
40216
  /**
40202
40217
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40203
40218
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -40402,8 +40417,33 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40402
40417
  }
40403
40418
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40404
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
+ };
40405
40445
  const isMinifiedDiagnosticFile = (filename) => {
40406
- const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40446
+ const absolutePath = resolveAbsolutePath(filename);
40407
40447
  const cached = minifiedFileCache.get(absolutePath);
40408
40448
  if (cached !== void 0) return cached;
40409
40449
  const minified = isMinifiedSource(absolutePath);
@@ -40416,8 +40456,17 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40416
40456
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
40417
40457
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
40418
40458
  const primarySpan = primaryLabel?.span;
40459
+ const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
40419
40460
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
40420
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;
40421
40470
  return {
40422
40471
  filePath: normalizedFilePath,
40423
40472
  plugin,
@@ -40431,10 +40480,11 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40431
40480
  column: primarySpan?.column ?? 0,
40432
40481
  ...primarySpan ? {
40433
40482
  offset: primarySpan.offset,
40434
- length: primarySpan.length
40483
+ length: primarySpan.length,
40484
+ ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
40435
40485
  } : {},
40436
40486
  category,
40437
- ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
40487
+ ...matchByOccurrence ? { matchByOccurrence: true } : {},
40438
40488
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
40439
40489
  };
40440
40490
  }), rootDirectory);
@@ -44844,5 +44894,5 @@ const startLanguageServer = () => {
44844
44894
  };
44845
44895
  //#endregion
44846
44896
  export { startLanguageServer };
44847
- !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]="a3135dfe-cc85-5d34-adea-93f5c29d9bcb")}catch(e){}}();
44848
- //# debugId=a3135dfe-cc85-5d34-adea-93f5c29d9bcb
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.6-dev.4fb3694",
3
+ "version": "0.7.6-dev.543ebfe",
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
- "oxlint-plugin-react-doctor": "0.7.6-dev.4fb3694",
69
- "deslop-js": "0.7.6"
68
+ "deslop-js": "0.7.6",
69
+ "oxlint-plugin-react-doctor": "0.7.6-dev.543ebfe"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/babel__code-frame": "^7.27.0",