react-doctor 0.7.1-dev.ce9dabf → 0.7.1-dev.d353dad

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]="bbc083b3-ec5f-541e-8e68-a28e5b06f6eb")}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]="47338856-c658-5c78-a5e2-e871c2dafaa7")}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";
@@ -36262,6 +36262,23 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
36262
36262
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
36263
36263
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
36264
36264
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
36265
+ /**
36266
+ * The chain of ancestor directories of `startDirectory`, nearest first, up to
36267
+ * and including the filesystem root. With `includeStart`, the chain begins at
36268
+ * `startDirectory` itself; otherwise at its parent. The one place the "walk up
36269
+ * the tree" traversal is expressed, so each caller adds only its own match /
36270
+ * stop policy instead of re-deriving the `path.dirname` loop.
36271
+ */
36272
+ const ancestorDirectories = (startDirectory, { includeStart }) => {
36273
+ const directories = [];
36274
+ let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
36275
+ while (true) {
36276
+ directories.push(currentDirectory);
36277
+ const parentDirectory = Path.dirname(currentDirectory);
36278
+ if (parentDirectory === currentDirectory) return directories;
36279
+ currentDirectory = parentDirectory;
36280
+ }
36281
+ };
36265
36282
  const cachedPackageJsons = /* @__PURE__ */ new Map();
36266
36283
  const readPackageJsonUncached = (packageJsonPath) => {
36267
36284
  try {
@@ -36292,11 +36309,7 @@ const isMonorepoRoot = (directory) => {
36292
36309
  return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
36293
36310
  };
36294
36311
  const findMonorepoRoot = (startDirectory) => {
36295
- let currentDirectory = Path.dirname(startDirectory);
36296
- while (currentDirectory !== Path.dirname(currentDirectory)) {
36297
- if (isMonorepoRoot(currentDirectory)) return currentDirectory;
36298
- currentDirectory = Path.dirname(currentDirectory);
36299
- }
36312
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
36300
36313
  return null;
36301
36314
  };
36302
36315
  /**
@@ -36942,6 +36955,74 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
36942
36955
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
36943
36956
  };
36944
36957
  };
36958
+ /**
36959
+ * Walk up from `startDirectory` to the nearest ancestor that owns a
36960
+ * `package.json`, stopping at (and including) the enclosing project boundary
36961
+ * — the working tree's git root or a monorepo root. Lets a scan of a package
36962
+ * subfolder that has no `package.json` of its own adopt the nearest enclosing
36963
+ * package as its project root, so it inherits that package's dependency +
36964
+ * framework detection instead of synthesizing an empty (React-blind) project.
36965
+ *
36966
+ * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
36967
+ * most specific owning package (a leaf workspace, not just the repo root), so
36968
+ * a subfolder of one workspace no longer borrows a sibling's capabilities.
36969
+ *
36970
+ * Returns `null` when no ancestor `package.json` exists inside the project
36971
+ * boundary (a loose tree of files, or a subfolder of a package-less repo).
36972
+ */
36973
+ const findNearestAncestorPackageJson = (startDirectory) => {
36974
+ if (isProjectBoundary(startDirectory)) return null;
36975
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
36976
+ if (isFile(Path.join(directory, "package.json"))) return directory;
36977
+ if (isProjectBoundary(directory)) return null;
36978
+ }
36979
+ return null;
36980
+ };
36981
+ /**
36982
+ * Last-resort React detection: locate React the way Node's `node_modules`
36983
+ * resolution would from `directory` — check each `node_modules/react` up the
36984
+ * tree — but bounded to the enclosing repo so a globally installed React can't
36985
+ * masquerade as the project's version. Stops at (and including) the nearest
36986
+ * project boundary — the git root or a monorepo root — and returns the first
36987
+ * installed `version`.
36988
+ *
36989
+ * Makes "React is installed and importable" ⇒ "React is detected" an invariant
36990
+ * for packages whose only React declaration is a version-less spec
36991
+ * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
36992
+ * `node_modules` the declaration walks never reach — the profile of a component
36993
+ * package inside a monorepo. Matching the `node_modules/react` entry rather than
36994
+ * its realpath means a project whose `node_modules` is symlinked elsewhere (a
36995
+ * Docker volume, a shared store) still counts, while a global install outside
36996
+ * the repo does not.
36997
+ *
36998
+ * Returns `null` when React isn't installed within the repo or its package.json
36999
+ * carries no version string. A tree with no boundary marker at all bounds the
37000
+ * search to `directory`, so a dependency hoisted above the scanned package there
37001
+ * is conservatively not adopted.
37002
+ */
37003
+ /**
37004
+ * The nearest enclosing project boundary (git root or monorepo root), or
37005
+ * `directory` itself when the scan target sits outside any repo. Bounds the
37006
+ * node_modules walk so a React hoisted anywhere inside the repo counts, but a
37007
+ * React in an ancestor above the repo — a home-directory or global
37008
+ * `node_modules` — can't leak in.
37009
+ */
37010
+ const findContainmentRoot = (directory) => {
37011
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
37012
+ return directory;
37013
+ };
37014
+ const resolveInstalledReactVersion = (directory) => {
37015
+ const containmentRoot = findContainmentRoot(directory);
37016
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
37017
+ const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
37018
+ if (isFile(reactPackageJsonPath)) {
37019
+ const installedVersion = readPackageJson$1(reactPackageJsonPath).version;
37020
+ return typeof installedVersion === "string" ? installedVersion : null;
37021
+ }
37022
+ if (ancestorDirectory === containmentRoot) return null;
37023
+ }
37024
+ return null;
37025
+ };
36945
37026
  const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
36946
37027
  const rootValue = select(rootPackageJson);
36947
37028
  if (rootValue !== null) return rootValue;
@@ -37278,19 +37359,20 @@ const discoverReactSubprojects = (rootDirectory) => {
37278
37359
  const cachedProjectInfos = /* @__PURE__ */ new Map();
37279
37360
  /**
37280
37361
  * Build a `ProjectInfo` for a directory that has no `package.json` of
37281
- * its own — a monorepo subfolder like `repo/packages`, or any loose tree
37282
- * of TypeScript/JavaScript files. Dependency + framework detection is
37283
- * inherited from the enclosing workspace root when there is one, so
37284
- * scanning a subdirectory of a React monorepo still gets the React
37285
- * capabilities; a standalone non-React directory simply scans with the
37286
- * framework-agnostic rules. Throws only when the directory has nothing
37287
- * to scan (no enclosing project and no source files of its own).
37362
+ * its own — a package subfolder like `repo/packages` or `app/src/features`,
37363
+ * or any loose tree of TypeScript/JavaScript files. Dependency + framework
37364
+ * detection is inherited from the nearest enclosing package (a leaf
37365
+ * workspace, a plain app root, or a monorepo root — whichever is closest,
37366
+ * bounded by the git root), so scanning a subdirectory of a React project
37367
+ * still gets the React capabilities; a standalone non-React directory simply
37368
+ * scans with the framework-agnostic rules. Throws only when the directory has
37369
+ * nothing to scan (no enclosing project and no source files of its own).
37288
37370
  */
37289
37371
  const discoverProjectWithoutPackageJson = (directory) => {
37290
37372
  const sourceFileCount = countSourceFiles(directory);
37291
37373
  const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
37292
- const monorepoRoot = findMonorepoRoot(directory);
37293
- const enclosingProject = monorepoRoot !== null && isFile(Path.join(monorepoRoot, "package.json")) ? discoverProject(monorepoRoot) : null;
37374
+ const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
37375
+ const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
37294
37376
  if (enclosingProject !== null) return {
37295
37377
  ...enclosingProject,
37296
37378
  rootDirectory: directory,
@@ -37394,6 +37476,7 @@ const discoverProject = (directory) => {
37394
37476
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
37395
37477
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
37396
37478
  if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
37479
+ if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
37397
37480
  const projectName = packageJson.name ?? Path.basename(directory);
37398
37481
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
37399
37482
  const sourceFileCount = countSourceFiles(directory);
@@ -45924,7 +46007,7 @@ const makeNoopConsole = () => ({
45924
46007
  });
45925
46008
  //#endregion
45926
46009
  //#region src/cli/utils/version.ts
45927
- const VERSION = "0.7.1-dev.ce9dabf";
46010
+ const VERSION = "0.7.1-dev.d353dad";
45928
46011
  //#endregion
45929
46012
  //#region src/cli/utils/json-mode.ts
45930
46013
  let context = null;
@@ -46293,13 +46376,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
46293
46376
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
46294
46377
  * standard `SENTRY_RELEASE` override.
46295
46378
  */
46296
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.1-dev.ce9dabf`;
46379
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.1-dev.d353dad`;
46297
46380
  /**
46298
46381
  * Deployment environment shown in Sentry's environment filter. Defaults to
46299
46382
  * `production` for tagged releases and `development` for dev/unbuilt versions,
46300
46383
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
46301
46384
  */
46302
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.1-dev.ce9dabf") ? "development" : "production");
46385
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.1-dev.d353dad") ? "development" : "production");
46303
46386
  /**
46304
46387
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
46305
46388
  * (set to `0` to disable tracing) and falls back to
@@ -58143,7 +58226,7 @@ const buildRuntimeLayers = (input) => {
58143
58226
  const linterLayer = input.shouldSkipLint ? Linter.layerOf([]) : Linter.layerOxlint;
58144
58227
  const deadCodeLayer = input.shouldRunDeadCode ? DeadCode.layerNode : DeadCode.layerOf([]);
58145
58228
  const scoreLayer = input.shouldComputeScore ? Score.layerHttp : Score.layerOf(null);
58146
- const supplyChainLayer = input.userConfig?.supplyChain?.enabled === false ? SupplyChain.layerOf([]) : SupplyChain.layerNode;
58229
+ const supplyChainLayer = input.shouldRunSupplyChain ? SupplyChain.layerNode : SupplyChain.layerOf([]);
58147
58230
  const projectLayer = input.projectInfoOverride === void 0 ? Project.layerNode : Project.layerOf(input.projectInfoOverride);
58148
58231
  const progressLayer = input.shouldShowProgressSpinners ? Progress.layerOra(buildSpinnerProgressHandle) : Progress.layerNoop;
58149
58232
  const baseLayers = mergeAll$1(projectLayer, input.hasConfigOverride ? Config.layerOf({
@@ -58499,21 +58582,22 @@ const resolveDotenvFingerprint = (projectDirectory) => {
58499
58582
  return [];
58500
58583
  }
58501
58584
  };
58502
- const hasHiddenTrackedFileState = (projectDirectory) => {
58585
+ const isGitIdentityTrustworthy = (projectDirectory) => {
58503
58586
  const output = runGit(projectDirectory, ["ls-files", "-v"]);
58504
- if (output === null) return true;
58505
- return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
58587
+ if (output === null) return false;
58588
+ const entryLines = output.split("\n").filter((line) => line.length > 0);
58589
+ return entryLines.length > 0 && entryLines.every((line) => line[0] === "H");
58506
58590
  };
58507
58591
  const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
58508
58592
  const readPersistedCache = (cacheFilePath) => {
58509
58593
  try {
58510
58594
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
58511
- if (!isRecord(parsed) || parsed.version !== 3) return {
58512
- version: 3,
58595
+ if (!isRecord(parsed) || parsed.version !== 4) return {
58596
+ version: 4,
58513
58597
  entries: []
58514
58598
  };
58515
58599
  if (!Array.isArray(parsed.entries)) return {
58516
- version: 3,
58600
+ version: 4,
58517
58601
  entries: []
58518
58602
  };
58519
58603
  const entries = [];
@@ -58523,12 +58607,12 @@ const readPersistedCache = (cacheFilePath) => {
58523
58607
  entries.push(entry);
58524
58608
  }
58525
58609
  return {
58526
- version: 3,
58610
+ version: 4,
58527
58611
  entries
58528
58612
  };
58529
58613
  } catch {
58530
58614
  return {
58531
- version: 3,
58615
+ version: 4,
58532
58616
  entries: []
58533
58617
  };
58534
58618
  }
@@ -58560,6 +58644,7 @@ const resolveProjectIdentity = (projectDirectory) => {
58560
58644
  return path.resolve(projectDirectory);
58561
58645
  }
58562
58646
  };
58647
+ const resolveManifestContentHash = (projectDirectory) => hashFileContents(getPackageJsonPath(projectDirectory));
58563
58648
  const fileFingerprint = (filePath) => {
58564
58649
  try {
58565
58650
  const stat = fs.statSync(filePath);
@@ -58582,14 +58667,14 @@ const resolveToolchainFingerprint = (nodeBinaryPath) => {
58582
58667
  };
58583
58668
  const buildScanResultCacheKey = (input) => {
58584
58669
  if (isCacheGloballyDisabled()) return null;
58585
- if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
58670
+ if (!isGitIdentityTrustworthy(input.projectDirectory)) return null;
58586
58671
  const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
58587
58672
  if (worktreeFingerprint === null) return null;
58588
58673
  const headSha = readHeadSha(input.projectDirectory);
58589
58674
  if (headSha === null) return null;
58590
58675
  if (stringifyStableJson(input.userConfig) === null) return null;
58591
58676
  const cacheKeyJson = stringifyStableJson({
58592
- schemaVersion: 3,
58677
+ schemaVersion: 4,
58593
58678
  projectIdentity: resolveProjectIdentity(input.projectDirectory),
58594
58679
  headSha,
58595
58680
  worktreeFingerprint,
@@ -58604,6 +58689,7 @@ const buildScanResultCacheKey = (input) => {
58604
58689
  engineOptions: {
58605
58690
  lint: input.options.lint,
58606
58691
  deadCode: input.options.deadCode,
58692
+ supplyChain: input.options.supplyChain,
58607
58693
  includePaths: [...input.options.includePaths].sort(),
58608
58694
  customRulesOnly: input.options.customRulesOnly,
58609
58695
  respectInlineDisables: input.options.respectInlineDisables,
@@ -58629,17 +58715,24 @@ const createScanResultCache = (projectDirectory) => {
58629
58715
  for (const entry of persistedCache.entries) entries.set(entry.key, entry);
58630
58716
  const persist = () => {
58631
58717
  writePersistedCache(cacheFilePath, {
58632
- version: 3,
58718
+ version: 4,
58633
58719
  entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
58634
58720
  });
58635
58721
  };
58636
58722
  return {
58637
- lookup: (key) => entries.get(key)?.payload ?? null,
58723
+ lookup: (key) => {
58724
+ const payload = entries.get(key)?.payload;
58725
+ if (payload === void 0) return null;
58726
+ return resolveProjectIdentity(payload.directory) === resolveProjectIdentity(projectDirectory) && (payload.manifestContentHash ?? null) === resolveManifestContentHash(projectDirectory) ? payload : null;
58727
+ },
58638
58728
  store: (key, payload) => {
58639
58729
  entries.set(key, {
58640
58730
  key,
58641
58731
  createdAtMs: Date.now(),
58642
- payload
58732
+ payload: {
58733
+ ...payload,
58734
+ manifestContentHash: resolveManifestContentHash(projectDirectory)
58735
+ }
58643
58736
  });
58644
58737
  persist();
58645
58738
  }
@@ -58904,6 +58997,7 @@ const buildScanAttributes = (input) => {
58904
58997
  maxDurationMs: input.maxDurationMs,
58905
58998
  lint: input.lint,
58906
58999
  deadCode: input.deadCode,
59000
+ supplyChain: input.supplyChain,
58907
59001
  scoreOnly: input.scoreOnly,
58908
59002
  noScore: input.noScore,
58909
59003
  respectInlineDisables: input.respectInlineDisables,
@@ -60178,6 +60272,7 @@ const buildIgnoredTags = (userConfig) => {
60178
60272
  const mergeInspectOptions = (inputOptions, userConfig) => ({
60179
60273
  lint: inputOptions.lint ?? userConfig?.lint ?? true,
60180
60274
  deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
60275
+ supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
60181
60276
  verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
60182
60277
  outputDirectory: inputOptions.outputDirectory || null,
60183
60278
  scoreOnly: inputOptions.scoreOnly ?? false,
@@ -60217,6 +60312,7 @@ const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorke
60217
60312
  maxDurationMs: options.maxDurationMs,
60218
60313
  lint: options.lint,
60219
60314
  deadCode: options.deadCode,
60315
+ supplyChain: options.supplyChain,
60220
60316
  scoreOnly: options.scoreOnly,
60221
60317
  noScore: options.noScore,
60222
60318
  respectInlineDisables: options.respectInlineDisables,
@@ -60300,6 +60396,7 @@ const runBaselineComparison = async (params) => {
60300
60396
  projectInfoOverride: params.headProjectInfo,
60301
60397
  shouldSkipLint: !params.options.lint || !params.resolvedNodeBinaryPath,
60302
60398
  shouldRunDeadCode: false,
60399
+ shouldRunSupplyChain: params.options.supplyChain,
60303
60400
  shouldComputeScore: false,
60304
60401
  shouldShowProgressSpinners: false,
60305
60402
  oxlintConcurrency: params.options.concurrency
@@ -60388,6 +60485,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
60388
60485
  configSourceDirectory,
60389
60486
  shouldSkipLint: !options.lint || lintBindingMissing,
60390
60487
  shouldRunDeadCode: options.deadCode,
60488
+ shouldRunSupplyChain: options.supplyChain,
60391
60489
  shouldComputeScore: !options.noScore,
60392
60490
  shouldShowProgressSpinners,
60393
60491
  oxlintConcurrency: options.concurrency
@@ -62673,8 +62771,8 @@ const resolveParallelFlag = (parallel) => parallel === false ? 1 : void 0;
62673
62771
  * accepts. Flag-derived fields only (`scoreOnly`, `noScore`, `silent`,
62674
62772
  * `isCi`): every `userConfig` fallback — including `noScore` — lives in
62675
62773
  * `inspect()`'s merge layer, which sees each project's module-merged
62676
- * config. The plain boolean knobs (`lint`, `deadCode`, `verbose`) pass
62677
- * through unchanged for the same reason. This resolver reads
62774
+ * config. The plain boolean knobs (`lint`, `deadCode`, `supplyChain`,
62775
+ * `verbose`) pass through unchanged for the same reason. This resolver reads
62678
62776
  * `userConfig` only to decide the `--blocking warning` gate.
62679
62777
  */
62680
62778
  const resolveCliInspectOptions = (flags, userConfig) => {
@@ -62682,6 +62780,7 @@ const resolveCliInspectOptions = (flags, userConfig) => {
62682
62780
  return {
62683
62781
  lint: flags.lint,
62684
62782
  deadCode: flags.deadCode,
62783
+ supplyChain: flags.supplyChain,
62685
62784
  verbose: flags.verbose,
62686
62785
  outputDirectory: flags.outputDir,
62687
62786
  respectInlineDisables: flags.respectInlineDisables === false ? false : void 0,
@@ -63232,7 +63331,7 @@ const inspectAction = async (directory, flags) => {
63232
63331
  const scanDirectory = projectScanTarget.resolvedDirectory;
63233
63332
  const projectConfig = projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0);
63234
63333
  const projectConfigSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
63235
- const supplyChainEnabled = projectConfig?.supplyChain?.enabled !== false;
63334
+ const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
63236
63335
  let includePaths;
63237
63336
  let supplyChainManifestChanged = false;
63238
63337
  if (isDiffMode) {
@@ -64152,10 +64251,12 @@ const ROOT_FLAG_SPEC = {
64152
64251
  "--no-parallel",
64153
64252
  "--no-respect-inline-disables",
64154
64253
  "--no-score",
64254
+ "--no-supply-chain",
64155
64255
  "--no-telemetry",
64156
64256
  "--no-warnings",
64157
64257
  "--score",
64158
64258
  "--staged",
64259
+ "--supply-chain",
64159
64260
  "--verbose",
64160
64261
  "--version",
64161
64262
  "--warnings",
@@ -64404,7 +64505,7 @@ ${highlighter.dim("Learn more:")}
64404
64505
  ${highlighter.info(CI_URL)}
64405
64506
  `;
64406
64507
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
64407
- 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("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
64508
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
64408
64509
  program.action(inspectAction);
64409
64510
  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));
64410
64511
  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);
@@ -64454,4 +64555,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
64454
64555
  export {};
64455
64556
 
64456
64557
  //# sourceMappingURL=cli.js.map
64457
- //# debugId=bbc083b3-ec5f-541e-8e68-a28e5b06f6eb
64558
+ //# debugId=47338856-c658-5c78-a5e2-e871c2dafaa7
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]="f1919957-a013-5ded-b0ed-3d2e8469d0e2")}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]="f00947fe-3794-5d73-a491-232b535dc962")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import * as NFS from "node:fs";
@@ -33064,6 +33064,23 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33064
33064
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33065
33065
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33066
33066
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33067
+ /**
33068
+ * The chain of ancestor directories of `startDirectory`, nearest first, up to
33069
+ * and including the filesystem root. With `includeStart`, the chain begins at
33070
+ * `startDirectory` itself; otherwise at its parent. The one place the "walk up
33071
+ * the tree" traversal is expressed, so each caller adds only its own match /
33072
+ * stop policy instead of re-deriving the `path.dirname` loop.
33073
+ */
33074
+ const ancestorDirectories = (startDirectory, { includeStart }) => {
33075
+ const directories = [];
33076
+ let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
33077
+ while (true) {
33078
+ directories.push(currentDirectory);
33079
+ const parentDirectory = Path.dirname(currentDirectory);
33080
+ if (parentDirectory === currentDirectory) return directories;
33081
+ currentDirectory = parentDirectory;
33082
+ }
33083
+ };
33067
33084
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33068
33085
  const clearPackageJsonCache = () => {
33069
33086
  cachedPackageJsons.clear();
@@ -33097,11 +33114,7 @@ const isMonorepoRoot = (directory) => {
33097
33114
  return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
33098
33115
  };
33099
33116
  const findMonorepoRoot = (startDirectory) => {
33100
- let currentDirectory = Path.dirname(startDirectory);
33101
- while (currentDirectory !== Path.dirname(currentDirectory)) {
33102
- if (isMonorepoRoot(currentDirectory)) return currentDirectory;
33103
- currentDirectory = Path.dirname(currentDirectory);
33104
- }
33117
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
33105
33118
  return null;
33106
33119
  };
33107
33120
  /**
@@ -33747,6 +33760,74 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
33747
33760
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
33748
33761
  };
33749
33762
  };
33763
+ /**
33764
+ * Walk up from `startDirectory` to the nearest ancestor that owns a
33765
+ * `package.json`, stopping at (and including) the enclosing project boundary
33766
+ * — the working tree's git root or a monorepo root. Lets a scan of a package
33767
+ * subfolder that has no `package.json` of its own adopt the nearest enclosing
33768
+ * package as its project root, so it inherits that package's dependency +
33769
+ * framework detection instead of synthesizing an empty (React-blind) project.
33770
+ *
33771
+ * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
33772
+ * most specific owning package (a leaf workspace, not just the repo root), so
33773
+ * a subfolder of one workspace no longer borrows a sibling's capabilities.
33774
+ *
33775
+ * Returns `null` when no ancestor `package.json` exists inside the project
33776
+ * boundary (a loose tree of files, or a subfolder of a package-less repo).
33777
+ */
33778
+ const findNearestAncestorPackageJson = (startDirectory) => {
33779
+ if (isProjectBoundary(startDirectory)) return null;
33780
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
33781
+ if (isFile(Path.join(directory, "package.json"))) return directory;
33782
+ if (isProjectBoundary(directory)) return null;
33783
+ }
33784
+ return null;
33785
+ };
33786
+ /**
33787
+ * Last-resort React detection: locate React the way Node's `node_modules`
33788
+ * resolution would from `directory` — check each `node_modules/react` up the
33789
+ * tree — but bounded to the enclosing repo so a globally installed React can't
33790
+ * masquerade as the project's version. Stops at (and including) the nearest
33791
+ * project boundary — the git root or a monorepo root — and returns the first
33792
+ * installed `version`.
33793
+ *
33794
+ * Makes "React is installed and importable" ⇒ "React is detected" an invariant
33795
+ * for packages whose only React declaration is a version-less spec
33796
+ * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
33797
+ * `node_modules` the declaration walks never reach — the profile of a component
33798
+ * package inside a monorepo. Matching the `node_modules/react` entry rather than
33799
+ * its realpath means a project whose `node_modules` is symlinked elsewhere (a
33800
+ * Docker volume, a shared store) still counts, while a global install outside
33801
+ * the repo does not.
33802
+ *
33803
+ * Returns `null` when React isn't installed within the repo or its package.json
33804
+ * carries no version string. A tree with no boundary marker at all bounds the
33805
+ * search to `directory`, so a dependency hoisted above the scanned package there
33806
+ * is conservatively not adopted.
33807
+ */
33808
+ /**
33809
+ * The nearest enclosing project boundary (git root or monorepo root), or
33810
+ * `directory` itself when the scan target sits outside any repo. Bounds the
33811
+ * node_modules walk so a React hoisted anywhere inside the repo counts, but a
33812
+ * React in an ancestor above the repo — a home-directory or global
33813
+ * `node_modules` — can't leak in.
33814
+ */
33815
+ const findContainmentRoot = (directory) => {
33816
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
33817
+ return directory;
33818
+ };
33819
+ const resolveInstalledReactVersion = (directory) => {
33820
+ const containmentRoot = findContainmentRoot(directory);
33821
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
33822
+ const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
33823
+ if (isFile(reactPackageJsonPath)) {
33824
+ const installedVersion = readPackageJson(reactPackageJsonPath).version;
33825
+ return typeof installedVersion === "string" ? installedVersion : null;
33826
+ }
33827
+ if (ancestorDirectory === containmentRoot) return null;
33828
+ }
33829
+ return null;
33830
+ };
33750
33831
  const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
33751
33832
  const rootValue = select(rootPackageJson);
33752
33833
  if (rootValue !== null) return rootValue;
@@ -34086,19 +34167,20 @@ const clearProjectCache = () => {
34086
34167
  };
34087
34168
  /**
34088
34169
  * Build a `ProjectInfo` for a directory that has no `package.json` of
34089
- * its own — a monorepo subfolder like `repo/packages`, or any loose tree
34090
- * of TypeScript/JavaScript files. Dependency + framework detection is
34091
- * inherited from the enclosing workspace root when there is one, so
34092
- * scanning a subdirectory of a React monorepo still gets the React
34093
- * capabilities; a standalone non-React directory simply scans with the
34094
- * framework-agnostic rules. Throws only when the directory has nothing
34095
- * to scan (no enclosing project and no source files of its own).
34170
+ * its own — a package subfolder like `repo/packages` or `app/src/features`,
34171
+ * or any loose tree of TypeScript/JavaScript files. Dependency + framework
34172
+ * detection is inherited from the nearest enclosing package (a leaf
34173
+ * workspace, a plain app root, or a monorepo root — whichever is closest,
34174
+ * bounded by the git root), so scanning a subdirectory of a React project
34175
+ * still gets the React capabilities; a standalone non-React directory simply
34176
+ * scans with the framework-agnostic rules. Throws only when the directory has
34177
+ * nothing to scan (no enclosing project and no source files of its own).
34096
34178
  */
34097
34179
  const discoverProjectWithoutPackageJson = (directory) => {
34098
34180
  const sourceFileCount = countSourceFiles(directory);
34099
34181
  const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
34100
- const monorepoRoot = findMonorepoRoot(directory);
34101
- const enclosingProject = monorepoRoot !== null && isFile(Path.join(monorepoRoot, "package.json")) ? discoverProject(monorepoRoot) : null;
34182
+ const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
34183
+ const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
34102
34184
  if (enclosingProject !== null) return {
34103
34185
  ...enclosingProject,
34104
34186
  rootDirectory: directory,
@@ -34202,6 +34284,7 @@ const discoverProject = (directory) => {
34202
34284
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
34203
34285
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
34204
34286
  if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
34287
+ if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
34205
34288
  const projectName = packageJson.name ?? Path.basename(directory);
34206
34289
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
34207
34290
  const sourceFileCount = countSourceFiles(directory);
@@ -42441,4 +42524,4 @@ const toJsonReport = (result, options) => buildJsonReport({
42441
42524
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
42442
42525
 
42443
42526
  //# sourceMappingURL=index.js.map
42444
- //# debugId=f1919957-a013-5ded-b0ed-3d2e8469d0e2
42527
+ //# debugId=f00947fe-3794-5d73-a491-232b535dc962
package/dist/lsp.js CHANGED
@@ -33123,6 +33123,23 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33123
33123
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33124
33124
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33125
33125
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33126
+ /**
33127
+ * The chain of ancestor directories of `startDirectory`, nearest first, up to
33128
+ * and including the filesystem root. With `includeStart`, the chain begins at
33129
+ * `startDirectory` itself; otherwise at its parent. The one place the "walk up
33130
+ * the tree" traversal is expressed, so each caller adds only its own match /
33131
+ * stop policy instead of re-deriving the `path.dirname` loop.
33132
+ */
33133
+ const ancestorDirectories = (startDirectory, { includeStart }) => {
33134
+ const directories = [];
33135
+ let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
33136
+ while (true) {
33137
+ directories.push(currentDirectory);
33138
+ const parentDirectory = Path.dirname(currentDirectory);
33139
+ if (parentDirectory === currentDirectory) return directories;
33140
+ currentDirectory = parentDirectory;
33141
+ }
33142
+ };
33126
33143
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33127
33144
  const clearPackageJsonCache = () => {
33128
33145
  cachedPackageJsons.clear();
@@ -33156,11 +33173,7 @@ const isMonorepoRoot = (directory) => {
33156
33173
  return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
33157
33174
  };
33158
33175
  const findMonorepoRoot = (startDirectory) => {
33159
- let currentDirectory = Path.dirname(startDirectory);
33160
- while (currentDirectory !== Path.dirname(currentDirectory)) {
33161
- if (isMonorepoRoot(currentDirectory)) return currentDirectory;
33162
- currentDirectory = Path.dirname(currentDirectory);
33163
- }
33176
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
33164
33177
  return null;
33165
33178
  };
33166
33179
  /**
@@ -33806,6 +33819,74 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
33806
33819
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
33807
33820
  };
33808
33821
  };
33822
+ /**
33823
+ * Walk up from `startDirectory` to the nearest ancestor that owns a
33824
+ * `package.json`, stopping at (and including) the enclosing project boundary
33825
+ * — the working tree's git root or a monorepo root. Lets a scan of a package
33826
+ * subfolder that has no `package.json` of its own adopt the nearest enclosing
33827
+ * package as its project root, so it inherits that package's dependency +
33828
+ * framework detection instead of synthesizing an empty (React-blind) project.
33829
+ *
33830
+ * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
33831
+ * most specific owning package (a leaf workspace, not just the repo root), so
33832
+ * a subfolder of one workspace no longer borrows a sibling's capabilities.
33833
+ *
33834
+ * Returns `null` when no ancestor `package.json` exists inside the project
33835
+ * boundary (a loose tree of files, or a subfolder of a package-less repo).
33836
+ */
33837
+ const findNearestAncestorPackageJson = (startDirectory) => {
33838
+ if (isProjectBoundary(startDirectory)) return null;
33839
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
33840
+ if (isFile(Path.join(directory, "package.json"))) return directory;
33841
+ if (isProjectBoundary(directory)) return null;
33842
+ }
33843
+ return null;
33844
+ };
33845
+ /**
33846
+ * Last-resort React detection: locate React the way Node's `node_modules`
33847
+ * resolution would from `directory` — check each `node_modules/react` up the
33848
+ * tree — but bounded to the enclosing repo so a globally installed React can't
33849
+ * masquerade as the project's version. Stops at (and including) the nearest
33850
+ * project boundary — the git root or a monorepo root — and returns the first
33851
+ * installed `version`.
33852
+ *
33853
+ * Makes "React is installed and importable" ⇒ "React is detected" an invariant
33854
+ * for packages whose only React declaration is a version-less spec
33855
+ * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
33856
+ * `node_modules` the declaration walks never reach — the profile of a component
33857
+ * package inside a monorepo. Matching the `node_modules/react` entry rather than
33858
+ * its realpath means a project whose `node_modules` is symlinked elsewhere (a
33859
+ * Docker volume, a shared store) still counts, while a global install outside
33860
+ * the repo does not.
33861
+ *
33862
+ * Returns `null` when React isn't installed within the repo or its package.json
33863
+ * carries no version string. A tree with no boundary marker at all bounds the
33864
+ * search to `directory`, so a dependency hoisted above the scanned package there
33865
+ * is conservatively not adopted.
33866
+ */
33867
+ /**
33868
+ * The nearest enclosing project boundary (git root or monorepo root), or
33869
+ * `directory` itself when the scan target sits outside any repo. Bounds the
33870
+ * node_modules walk so a React hoisted anywhere inside the repo counts, but a
33871
+ * React in an ancestor above the repo — a home-directory or global
33872
+ * `node_modules` — can't leak in.
33873
+ */
33874
+ const findContainmentRoot = (directory) => {
33875
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
33876
+ return directory;
33877
+ };
33878
+ const resolveInstalledReactVersion = (directory) => {
33879
+ const containmentRoot = findContainmentRoot(directory);
33880
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
33881
+ const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
33882
+ if (isFile(reactPackageJsonPath)) {
33883
+ const installedVersion = readPackageJson(reactPackageJsonPath).version;
33884
+ return typeof installedVersion === "string" ? installedVersion : null;
33885
+ }
33886
+ if (ancestorDirectory === containmentRoot) return null;
33887
+ }
33888
+ return null;
33889
+ };
33809
33890
  const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
33810
33891
  const rootValue = select(rootPackageJson);
33811
33892
  if (rootValue !== null) return rootValue;
@@ -34145,19 +34226,20 @@ const clearProjectCache = () => {
34145
34226
  };
34146
34227
  /**
34147
34228
  * Build a `ProjectInfo` for a directory that has no `package.json` of
34148
- * its own — a monorepo subfolder like `repo/packages`, or any loose tree
34149
- * of TypeScript/JavaScript files. Dependency + framework detection is
34150
- * inherited from the enclosing workspace root when there is one, so
34151
- * scanning a subdirectory of a React monorepo still gets the React
34152
- * capabilities; a standalone non-React directory simply scans with the
34153
- * framework-agnostic rules. Throws only when the directory has nothing
34154
- * to scan (no enclosing project and no source files of its own).
34229
+ * its own — a package subfolder like `repo/packages` or `app/src/features`,
34230
+ * or any loose tree of TypeScript/JavaScript files. Dependency + framework
34231
+ * detection is inherited from the nearest enclosing package (a leaf
34232
+ * workspace, a plain app root, or a monorepo root — whichever is closest,
34233
+ * bounded by the git root), so scanning a subdirectory of a React project
34234
+ * still gets the React capabilities; a standalone non-React directory simply
34235
+ * scans with the framework-agnostic rules. Throws only when the directory has
34236
+ * nothing to scan (no enclosing project and no source files of its own).
34155
34237
  */
34156
34238
  const discoverProjectWithoutPackageJson = (directory) => {
34157
34239
  const sourceFileCount = countSourceFiles(directory);
34158
34240
  const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
34159
- const monorepoRoot = findMonorepoRoot(directory);
34160
- const enclosingProject = monorepoRoot !== null && isFile(Path.join(monorepoRoot, "package.json")) ? discoverProject(monorepoRoot) : null;
34241
+ const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
34242
+ const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
34161
34243
  if (enclosingProject !== null) return {
34162
34244
  ...enclosingProject,
34163
34245
  rootDirectory: directory,
@@ -34261,6 +34343,7 @@ const discoverProject = (directory) => {
34261
34343
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
34262
34344
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
34263
34345
  if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
34346
+ if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
34264
34347
  const projectName = packageJson.name ?? Path.basename(directory);
34265
34348
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
34266
34349
  const sourceFileCount = countSourceFiles(directory);
@@ -44160,5 +44243,5 @@ const startLanguageServer = () => {
44160
44243
  };
44161
44244
  //#endregion
44162
44245
  export { startLanguageServer };
44163
- !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]="0cab34bf-65ac-5994-9a93-41f1b5670387")}catch(e){}}();
44164
- //# debugId=0cab34bf-65ac-5994-9a93-41f1b5670387
44246
+ !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]="8bc40ab2-982a-5d5d-a65c-9733ec980391")}catch(e){}}();
44247
+ //# debugId=8bc40ab2-982a-5d5d-a65c-9733ec980391
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.7.1-dev.ce9dabf",
3
+ "version": "0.7.1-dev.d353dad",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -66,7 +66,7 @@
66
66
  "vscode-uri": "^3.1.0",
67
67
  "yaml": "^2.9.0",
68
68
  "deslop-js": "0.7.1",
69
- "oxlint-plugin-react-doctor": "0.7.1-dev.ce9dabf"
69
+ "oxlint-plugin-react-doctor": "0.7.1-dev.d353dad"
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/language-server": "0.7.1",
77
+ "@react-doctor/core": "0.7.1",
78
78
  "@react-doctor/api": "0.7.1",
79
- "@react-doctor/core": "0.7.1"
79
+ "@react-doctor/language-server": "0.7.1"
80
80
  },
81
81
  "engines": {
82
82
  "node": "^20.19.0 || >=22.13.0"