react-doctor 0.7.1-dev.64452aa → 0.7.1-dev.7cf4d0b

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]="57f4381b-7060-58c6-88a4-95b4c97d3b74")}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]="7b7c23b7-250e-5339-af06-e3e3b5eb5f98")}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";
@@ -9,7 +9,7 @@ import * as NFS from "node:fs";
9
9
  import fs, { mkdtempSync, rmSync } from "node:fs";
10
10
  import process$1 from "node:process";
11
11
  import * as ts from "typescript";
12
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
+ import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
13
13
  import * as OS from "node:os";
14
14
  import os, { tmpdir } from "node:os";
15
15
  import { parseJSON5 } from "confbox";
@@ -35968,8 +35968,10 @@ const isFile = (filePath) => {
35968
35968
  };
35969
35969
  const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?|mts|mjs)$/;
35970
35970
  const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
35971
+ const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
35971
35972
  const MINIFIED_SNIFF_BYTES = 65536;
35972
35973
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
35974
+ const SCANNED_DOT_DIRECTORIES = new Set([".dumi", ".storybook"]);
35973
35975
  const IGNORED_DIRECTORIES = new Set([
35974
35976
  ".angular",
35975
35977
  ".astro",
@@ -36154,8 +36156,9 @@ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
36154
36156
  const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
36155
36157
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
36156
36158
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
36157
- const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
36158
- const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath);
36159
+ const isIgnoredDirectoryName = (directoryName) => IGNORED_DIRECTORIES.has(directoryName) || directoryName.startsWith(".") && !SCANNED_DOT_DIRECTORIES.has(directoryName);
36160
+ const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => isIgnoredDirectoryName(segment));
36161
+ const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath) && !GENERATED_SOURCE_DIRECTORY_PATTERN.test(filePath);
36159
36162
  const isMinifiedSource = (absolutePath) => {
36160
36163
  let fileDescriptor;
36161
36164
  try {
@@ -36216,7 +36219,7 @@ function* walkSourceTreeFiles(rootDirectory) {
36216
36219
  for (const entry of readDirectoryEntries(currentDirectory)) {
36217
36220
  const absolutePath = Path.join(currentDirectory, entry.name);
36218
36221
  if (entry.isDirectory()) {
36219
- if (!IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
36222
+ if (!isIgnoredDirectoryName(entry.name)) stack.push(absolutePath);
36220
36223
  continue;
36221
36224
  }
36222
36225
  if (entry.isFile()) yield {
@@ -36262,6 +36265,23 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
36262
36265
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
36263
36266
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
36264
36267
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
36268
+ /**
36269
+ * The chain of ancestor directories of `startDirectory`, nearest first, up to
36270
+ * and including the filesystem root. With `includeStart`, the chain begins at
36271
+ * `startDirectory` itself; otherwise at its parent. The one place the "walk up
36272
+ * the tree" traversal is expressed, so each caller adds only its own match /
36273
+ * stop policy instead of re-deriving the `path.dirname` loop.
36274
+ */
36275
+ const ancestorDirectories = (startDirectory, { includeStart }) => {
36276
+ const directories = [];
36277
+ let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
36278
+ while (true) {
36279
+ directories.push(currentDirectory);
36280
+ const parentDirectory = Path.dirname(currentDirectory);
36281
+ if (parentDirectory === currentDirectory) return directories;
36282
+ currentDirectory = parentDirectory;
36283
+ }
36284
+ };
36265
36285
  const cachedPackageJsons = /* @__PURE__ */ new Map();
36266
36286
  const readPackageJsonUncached = (packageJsonPath) => {
36267
36287
  try {
@@ -36292,11 +36312,7 @@ const isMonorepoRoot = (directory) => {
36292
36312
  return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
36293
36313
  };
36294
36314
  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
- }
36315
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
36300
36316
  return null;
36301
36317
  };
36302
36318
  /**
@@ -36942,6 +36958,74 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
36942
36958
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
36943
36959
  };
36944
36960
  };
36961
+ /**
36962
+ * Walk up from `startDirectory` to the nearest ancestor that owns a
36963
+ * `package.json`, stopping at (and including) the enclosing project boundary
36964
+ * — the working tree's git root or a monorepo root. Lets a scan of a package
36965
+ * subfolder that has no `package.json` of its own adopt the nearest enclosing
36966
+ * package as its project root, so it inherits that package's dependency +
36967
+ * framework detection instead of synthesizing an empty (React-blind) project.
36968
+ *
36969
+ * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
36970
+ * most specific owning package (a leaf workspace, not just the repo root), so
36971
+ * a subfolder of one workspace no longer borrows a sibling's capabilities.
36972
+ *
36973
+ * Returns `null` when no ancestor `package.json` exists inside the project
36974
+ * boundary (a loose tree of files, or a subfolder of a package-less repo).
36975
+ */
36976
+ const findNearestAncestorPackageJson = (startDirectory) => {
36977
+ if (isProjectBoundary(startDirectory)) return null;
36978
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
36979
+ if (isFile(Path.join(directory, "package.json"))) return directory;
36980
+ if (isProjectBoundary(directory)) return null;
36981
+ }
36982
+ return null;
36983
+ };
36984
+ /**
36985
+ * Last-resort React detection: locate React the way Node's `node_modules`
36986
+ * resolution would from `directory` — check each `node_modules/react` up the
36987
+ * tree — but bounded to the enclosing repo so a globally installed React can't
36988
+ * masquerade as the project's version. Stops at (and including) the nearest
36989
+ * project boundary — the git root or a monorepo root — and returns the first
36990
+ * installed `version`.
36991
+ *
36992
+ * Makes "React is installed and importable" ⇒ "React is detected" an invariant
36993
+ * for packages whose only React declaration is a version-less spec
36994
+ * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
36995
+ * `node_modules` the declaration walks never reach — the profile of a component
36996
+ * package inside a monorepo. Matching the `node_modules/react` entry rather than
36997
+ * its realpath means a project whose `node_modules` is symlinked elsewhere (a
36998
+ * Docker volume, a shared store) still counts, while a global install outside
36999
+ * the repo does not.
37000
+ *
37001
+ * Returns `null` when React isn't installed within the repo or its package.json
37002
+ * carries no version string. A tree with no boundary marker at all bounds the
37003
+ * search to `directory`, so a dependency hoisted above the scanned package there
37004
+ * is conservatively not adopted.
37005
+ */
37006
+ /**
37007
+ * The nearest enclosing project boundary (git root or monorepo root), or
37008
+ * `directory` itself when the scan target sits outside any repo. Bounds the
37009
+ * node_modules walk so a React hoisted anywhere inside the repo counts, but a
37010
+ * React in an ancestor above the repo — a home-directory or global
37011
+ * `node_modules` — can't leak in.
37012
+ */
37013
+ const findContainmentRoot = (directory) => {
37014
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
37015
+ return directory;
37016
+ };
37017
+ const resolveInstalledReactVersion = (directory) => {
37018
+ const containmentRoot = findContainmentRoot(directory);
37019
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
37020
+ const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
37021
+ if (isFile(reactPackageJsonPath)) {
37022
+ const installedVersion = readPackageJson$1(reactPackageJsonPath).version;
37023
+ return typeof installedVersion === "string" ? installedVersion : null;
37024
+ }
37025
+ if (ancestorDirectory === containmentRoot) return null;
37026
+ }
37027
+ return null;
37028
+ };
36945
37029
  const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
36946
37030
  const rootValue = select(rootPackageJson);
36947
37031
  if (rootValue !== null) return rootValue;
@@ -37098,6 +37182,10 @@ const normalizeCompilerOptions = (compilerOptions) => {
37098
37182
  hasExplicitLib
37099
37183
  };
37100
37184
  };
37185
+ const normalizeReferencePaths = (references) => {
37186
+ if (!Array.isArray(references)) return [];
37187
+ return references.map((reference) => isPlainObject(reference) && typeof reference.path === "string" ? reference.path : null).filter((referencePath) => referencePath !== null);
37188
+ };
37101
37189
  const readTsConfig = (filePath) => {
37102
37190
  let content;
37103
37191
  try {
@@ -37109,6 +37197,7 @@ const readTsConfig = (filePath) => {
37109
37197
  if (!isPlainObject(parsed.config)) return null;
37110
37198
  return {
37111
37199
  extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
37200
+ referencePaths: normalizeReferencePaths(parsed.config.references),
37112
37201
  compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
37113
37202
  };
37114
37203
  };
@@ -37149,20 +37238,37 @@ const libEntryIncludesES2023Array = (entry) => {
37149
37238
  };
37150
37239
  const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
37151
37240
  const compilerOptionsArePreES2023 = (compilerOptions) => {
37241
+ if (compilerOptions.target && targetYearIsPreES2023(compilerOptions.target)) return true;
37152
37242
  if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
37153
- if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
37154
37243
  return false;
37155
37244
  };
37156
37245
  const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
37157
- const detectPreES2023FromConfig = (tsConfigPath) => {
37246
+ const detectPreES2023FromConfig = (tsConfigPath, visitedConfigPaths = /* @__PURE__ */ new Set()) => {
37247
+ if (visitedConfigPaths.has(tsConfigPath)) return false;
37158
37248
  const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
37159
37249
  if (!compilerOptions) return false;
37160
- if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) return false;
37250
+ if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) {
37251
+ const tsConfig = readTsConfig(tsConfigPath);
37252
+ if (!tsConfig) return false;
37253
+ const nextVisitedConfigPaths = new Set(visitedConfigPaths);
37254
+ nextVisitedConfigPaths.add(tsConfigPath);
37255
+ const configDirectory = Path.dirname(tsConfigPath);
37256
+ return tsConfig.referencePaths.some((referencePath) => {
37257
+ const resolvedReferencePath = Path.resolve(configDirectory, referencePath);
37258
+ const referencedConfigPath = isFile(resolvedReferencePath) ? resolvedReferencePath : Path.join(resolvedReferencePath, TSCONFIG_FILENAME);
37259
+ return isFile(referencedConfigPath) && detectPreES2023FromConfig(referencedConfigPath, nextVisitedConfigPaths);
37260
+ });
37261
+ }
37161
37262
  return compilerOptionsArePreES2023(compilerOptions);
37162
37263
  };
37264
+ const FALLBACK_TSCONFIG_FILENAMES = ["tsconfig.app.json", "tsconfig.build.json"];
37163
37265
  const detectPreES2023Target = (directory) => {
37164
37266
  const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
37165
37267
  if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
37268
+ for (const fallbackFilename of FALLBACK_TSCONFIG_FILENAMES) {
37269
+ const fallbackPath = Path.join(directory, fallbackFilename);
37270
+ if (isFile(fallbackPath)) return detectPreES2023FromConfig(fallbackPath);
37271
+ }
37166
37272
  return false;
37167
37273
  };
37168
37274
  const REACT_DEPENDENCY_NAMES = new Set([
@@ -37278,19 +37384,20 @@ const discoverReactSubprojects = (rootDirectory) => {
37278
37384
  const cachedProjectInfos = /* @__PURE__ */ new Map();
37279
37385
  /**
37280
37386
  * 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).
37387
+ * its own — a package subfolder like `repo/packages` or `app/src/features`,
37388
+ * or any loose tree of TypeScript/JavaScript files. Dependency + framework
37389
+ * detection is inherited from the nearest enclosing package (a leaf
37390
+ * workspace, a plain app root, or a monorepo root — whichever is closest,
37391
+ * bounded by the git root), so scanning a subdirectory of a React project
37392
+ * still gets the React capabilities; a standalone non-React directory simply
37393
+ * scans with the framework-agnostic rules. Throws only when the directory has
37394
+ * nothing to scan (no enclosing project and no source files of its own).
37288
37395
  */
37289
37396
  const discoverProjectWithoutPackageJson = (directory) => {
37290
37397
  const sourceFileCount = countSourceFiles(directory);
37291
37398
  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;
37399
+ const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
37400
+ const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
37294
37401
  if (enclosingProject !== null) return {
37295
37402
  ...enclosingProject,
37296
37403
  rootDirectory: directory,
@@ -37320,6 +37427,7 @@ const discoverProjectWithoutPackageJson = (directory) => {
37320
37427
  shopifyFlashListVersion: null,
37321
37428
  shopifyFlashListMajorVersion: null,
37322
37429
  hasReanimated: false,
37430
+ reanimatedVersion: null,
37323
37431
  isPreES2023Target: hasOwnTsConfig && detectPreES2023Target(directory),
37324
37432
  sourceFileCount
37325
37433
  };
@@ -37394,6 +37502,7 @@ const discoverProject = (directory) => {
37394
37502
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
37395
37503
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
37396
37504
  if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
37505
+ if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
37397
37506
  const projectName = packageJson.name ?? Path.basename(directory);
37398
37507
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
37399
37508
  const sourceFileCount = countSourceFiles(directory);
@@ -37411,6 +37520,7 @@ const discoverProject = (directory) => {
37411
37520
  version: findShopifyFlashListVersion(directory, packageJson)
37412
37521
  }) : null;
37413
37522
  const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
37523
+ const reanimatedVersion = hasReanimated ? findInWorkspacePackageJsons(directory, packageJson, (workspacePackageJson) => getDependencySpec(workspacePackageJson, REANIMATED_DEPENDENCY_NAME)) : null;
37414
37524
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
37415
37525
  rootDirectory: directory,
37416
37526
  rootPackageJson: packageJson,
@@ -37440,6 +37550,7 @@ const discoverProject = (directory) => {
37440
37550
  shopifyFlashListVersion,
37441
37551
  shopifyFlashListMajorVersion: shopifyFlashListVersion === null ? null : getLowestDependencyMajor(shopifyFlashListVersion),
37442
37552
  hasReanimated,
37553
+ reanimatedVersion,
37443
37554
  isPreES2023Target,
37444
37555
  sourceFileCount
37445
37556
  };
@@ -37632,6 +37743,9 @@ const buildRuleSeverityControls = (config) => {
37632
37743
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
37633
37744
  };
37634
37745
  };
37746
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
37747
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
37748
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
37635
37749
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
37636
37750
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
37637
37751
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -37709,6 +37823,7 @@ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
37709
37823
  "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
37710
37824
  "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
37711
37825
  "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
37826
+ "react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
37712
37827
  "react/jsx-no-undef": "react-doctor/jsx-no-undef",
37713
37828
  "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
37714
37829
  "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
@@ -37793,9 +37908,6 @@ const tokenizeRuleList = (ruleList) => {
37793
37908
  if (!ruleSection) return [];
37794
37909
  return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
37795
37910
  };
37796
- const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
37797
- const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
37798
- const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
37799
37911
  const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — add \`${ruleId}\` to the comment (e.g. \`${token}, ${ruleId}\`).`;
37800
37912
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
37801
37913
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
@@ -37849,6 +37961,53 @@ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
37849
37961
  if (!ruleId.startsWith("react-doctor/")) return null;
37850
37962
  return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
37851
37963
  };
37964
+ const ESLINT_REACT_HOOKS_PLUGIN_PREFIX = "react-hooks/";
37965
+ const buildAcceptedTokens = (ruleId) => {
37966
+ const shortRuleId = ruleId.slice(15);
37967
+ return new Set([ruleId, `${ESLINT_REACT_HOOKS_PLUGIN_PREFIX}${shortRuleId}`]);
37968
+ };
37969
+ const listNamesRule = (ruleList, acceptedTokens) => tokenizeRuleList(ruleList).some((token) => acceptedTokens.has(token));
37970
+ const isInlineDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
37971
+ const candidates = [{
37972
+ line: lines[diagnosticLineIndex],
37973
+ requiredScope: "line"
37974
+ }, {
37975
+ line: lines[diagnosticLineIndex - 1],
37976
+ requiredScope: "next-line"
37977
+ }];
37978
+ for (const { line, requiredScope } of candidates) {
37979
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
37980
+ if (!match) continue;
37981
+ const [, , scope, ruleList] = match;
37982
+ if (scope !== requiredScope) continue;
37983
+ if (listNamesRule(ruleList, acceptedTokens)) return true;
37984
+ }
37985
+ return false;
37986
+ };
37987
+ const isBlockDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
37988
+ let isRangeOpen = false;
37989
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
37990
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
37991
+ const line = lines[lineIndex];
37992
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
37993
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
37994
+ if (disableMatch && listNamesRule(disableMatch[2], acceptedTokens)) {
37995
+ isRangeOpen = true;
37996
+ continue;
37997
+ }
37998
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
37999
+ if (enableMatch) {
38000
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
38001
+ if (enabledRules.length === 0 || enabledRules.some((rule) => acceptedTokens.has(rule))) isRangeOpen = false;
38002
+ }
38003
+ }
38004
+ return isRangeOpen;
38005
+ };
38006
+ const isCompilerRuleForeignDisabled = (lines, diagnosticLineIndex, ruleId) => {
38007
+ if (!ruleId.startsWith("react-hooks-js/")) return false;
38008
+ const acceptedTokens = buildAcceptedTokens(ruleId);
38009
+ return isInlineDisabled(lines, diagnosticLineIndex, acceptedTokens) || isBlockDisabled(lines, diagnosticLineIndex, acceptedTokens);
38010
+ };
37852
38011
  const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
37853
38012
  const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
37854
38013
  const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
@@ -37992,6 +38151,11 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
37992
38151
  isSuppressed: true,
37993
38152
  nearMissHint: null
37994
38153
  };
38154
+ if (isCompilerRuleForeignDisabled(lines, diagnosticLineIndex, ruleId)) return {
38155
+ isSuppressed: true,
38156
+ nearMissHint: null,
38157
+ isForeignDirective: true
38158
+ };
37995
38159
  const directComments = findStackedDisableCommentsAbove(lines, diagnosticLineIndex);
37996
38160
  if (hasChainSuppressor(directComments, ruleId)) return {
37997
38161
  isSuppressed: true,
@@ -38273,12 +38437,18 @@ const collectStringSet = (values) => {
38273
38437
  return new Set(values.filter((value) => typeof value === "string"));
38274
38438
  };
38275
38439
  const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
38440
+ const NODE_CLI_PATH_SEGMENTS = new Set([
38441
+ "scripts",
38442
+ "bin",
38443
+ "tools"
38444
+ ]);
38445
+ const hasNodeCliPathSegment = (filePath) => filePath.split(/[\\/]/).slice(0, -1).some((segment) => NODE_CLI_PATH_SEGMENTS.has(segment));
38276
38446
  /**
38277
38447
  * Pre-compiles every stateful filter and returns a single
38278
38448
  * `apply(diagnostic)` closure that runs (in order):
38279
38449
  *
38280
38450
  * 1. auto-suppress (test-noise rules in test files; `migration-hint`
38281
- * wins over `test-noise`)
38451
+ * wins over `test-noise`; `no-console` on Node CLI scripts)
38282
38452
  * 2. severity overrides (top-level `rules` / `categories`, with
38283
38453
  * `"off"` dropping)
38284
38454
  * 3. warning suppression (only when `showWarnings` is false: drops every
@@ -38376,6 +38546,12 @@ const buildDiagnosticPipeline = (input) => {
38376
38546
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
38377
38547
  return false;
38378
38548
  };
38549
+ const isNoConsoleOnNodeCliScript = (diagnostic) => {
38550
+ if (diagnostic.rule !== "no-console") return false;
38551
+ if (hasNodeCliPathSegment(diagnostic.filePath)) return true;
38552
+ const lines = getFileLines(diagnostic.filePath);
38553
+ return lines !== null && Boolean(lines[0]?.startsWith("#!"));
38554
+ };
38379
38555
  const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
38380
38556
  if (!hasRuntimeGlobals) return false;
38381
38557
  if (diagnostic.rule !== "jsx-no-undef") return false;
@@ -38385,6 +38561,7 @@ const buildDiagnosticPipeline = (input) => {
38385
38561
  return {
38386
38562
  apply: (diagnostic) => {
38387
38563
  if (shouldAutoSuppress(diagnostic)) return null;
38564
+ if (isNoConsoleOnNodeCliScript(diagnostic)) return null;
38388
38565
  let current = diagnostic;
38389
38566
  let explicitSeverityOverride;
38390
38567
  let explicitRuleOverride;
@@ -38414,7 +38591,7 @@ const buildDiagnosticPipeline = (input) => {
38414
38591
  if (lines) {
38415
38592
  const ruleIdentifier = `${current.plugin}/${current.rule}`;
38416
38593
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
38417
- if (evaluation.isSuppressed) return suppress(current, "inline");
38594
+ if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
38418
38595
  if (evaluation.nearMissHint) current = {
38419
38596
  ...current,
38420
38597
  suppressionHint: evaluation.nearMissHint
@@ -42590,9 +42767,17 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
42590
42767
  return `Move secrets to server-only code. In ${formatFrameworkName(project.framework)}, only \`${publicEnvPrefix}\` env vars are exposed to the browser, and they must not contain secrets`;
42591
42768
  };
42592
42769
  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";
42770
+ const ACCESSORS_MIN_VERSION = "3.15.0";
42771
+ const supportsAccessors = (reanimatedVersion) => {
42772
+ if (reanimatedVersion === null) return true;
42773
+ const lowerBound = import_semver.validRange(reanimatedVersion) !== null ? import_semver.minVersion(reanimatedVersion) : import_semver.coerce(reanimatedVersion);
42774
+ if (lowerBound === null) return true;
42775
+ return import_semver.gte(lowerBound, ACCESSORS_MIN_VERSION);
42776
+ };
42593
42777
  const appendReanimatedSharedValueHint = (help, rule, project) => {
42594
42778
  if (rule !== "immutability") return help;
42595
42779
  if (!project.hasReanimated) return help;
42780
+ if (!supportsAccessors(project.reanimatedVersion)) return help;
42596
42781
  if (!help) return REANIMATED_SHARED_VALUE_HINT;
42597
42782
  return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
42598
42783
  };
@@ -42754,13 +42939,13 @@ const REACT_USE_BINDING_RESOLUTION = {
42754
42939
  isReactUseBinding: true,
42755
42940
  isReactNamespaceBinding: false
42756
42941
  };
42757
- const getScriptKind = (filename) => {
42942
+ const getScriptKind$2 = (filename) => {
42758
42943
  if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
42759
42944
  if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
42760
42945
  if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
42761
42946
  return ts.ScriptKind.JS;
42762
42947
  };
42763
- const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
42948
+ const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
42764
42949
  const unwrapExpression = (expression) => {
42765
42950
  let currentExpression = expression;
42766
42951
  while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
@@ -43009,8 +43194,8 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
43009
43194
  return matchedIdentifier;
43010
43195
  };
43011
43196
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
43012
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filename));
43013
- const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset(sourceText, utf8Offset));
43197
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
43198
+ const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
43014
43199
  if (!useIdentifier) return null;
43015
43200
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
43016
43201
  };
@@ -43031,6 +43216,201 @@ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
43031
43216
  const bindingResolution = resolveUseCallBinding(sourceText, absolutePath, primaryLabel.span.offset);
43032
43217
  return bindingResolution !== null && !bindingResolution.isReactUseBinding;
43033
43218
  };
43219
+ const SUPPRESSED_COMPILER_CODES = new Set(["react-hooks-js(immutability)", "react-hooks-js(refs)"]);
43220
+ const WORKLET_DIRECTIVE = "worklet";
43221
+ const WORKLET_ACCEPTING_CALLEES = new Set([
43222
+ "useAnimatedStyle",
43223
+ "useAnimatedProps",
43224
+ "useDerivedValue",
43225
+ "useAnimatedReaction",
43226
+ "useAnimatedScrollHandler",
43227
+ "useAnimatedGestureHandler",
43228
+ "useFrameCallback",
43229
+ "runOnUI",
43230
+ "executeOnUIRuntimeSync"
43231
+ ]);
43232
+ const GESTURE_ROOT_IDENTIFIER = "Gesture";
43233
+ const getScriptKind$1 = (filename) => {
43234
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43235
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43236
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43237
+ return ts.ScriptKind.JS;
43238
+ };
43239
+ const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
43240
+ const hasWorkletDirective = (node) => {
43241
+ if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
43242
+ const body = node.body;
43243
+ if (!body || !ts.isBlock(body)) return false;
43244
+ for (const statement of body.statements) {
43245
+ if (!ts.isExpressionStatement(statement)) return false;
43246
+ if (!ts.isStringLiteral(statement.expression)) return false;
43247
+ if (statement.expression.text === WORKLET_DIRECTIVE) return true;
43248
+ }
43249
+ return false;
43250
+ };
43251
+ const rootIdentifierOfCallChain = (expression) => {
43252
+ let currentExpression = expression;
43253
+ while (true) {
43254
+ if (ts.isIdentifier(currentExpression)) return currentExpression.text;
43255
+ if (ts.isPropertyAccessExpression(currentExpression)) {
43256
+ currentExpression = currentExpression.expression;
43257
+ continue;
43258
+ }
43259
+ if (ts.isCallExpression(currentExpression)) {
43260
+ currentExpression = currentExpression.expression;
43261
+ continue;
43262
+ }
43263
+ return null;
43264
+ }
43265
+ };
43266
+ const isWorkletAcceptingCall = (callExpression) => {
43267
+ const callee = callExpression.expression;
43268
+ if (ts.isIdentifier(callee)) return WORKLET_ACCEPTING_CALLEES.has(callee.text);
43269
+ if (ts.isPropertyAccessExpression(callee)) {
43270
+ if (WORKLET_ACCEPTING_CALLEES.has(callee.name.text)) return true;
43271
+ return rootIdentifierOfCallChain(callee) === GESTURE_ROOT_IDENTIFIER;
43272
+ }
43273
+ return false;
43274
+ };
43275
+ const isFunctionWorklet = (functionNode) => {
43276
+ if (hasWorkletDirective(functionNode)) return true;
43277
+ const parent = functionNode.parent;
43278
+ return ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === functionNode) && isWorkletAcceptingCall(parent);
43279
+ };
43280
+ const findNodeAtOffset = (sourceFile, targetOffset) => {
43281
+ let matchedNode = null;
43282
+ const visit = (node) => {
43283
+ if (node.getStart(sourceFile) > targetOffset || node.getEnd() <= targetOffset) return;
43284
+ matchedNode = node;
43285
+ ts.forEachChild(node, visit);
43286
+ };
43287
+ visit(sourceFile);
43288
+ return matchedNode;
43289
+ };
43290
+ const isOffsetInsideWorklet = (sourceFile, targetOffset) => {
43291
+ let currentNode = findNodeAtOffset(sourceFile, targetOffset);
43292
+ while (currentNode) {
43293
+ if (ts.isFunctionLike(currentNode) && isFunctionWorklet(currentNode)) return true;
43294
+ currentNode = currentNode.parent;
43295
+ }
43296
+ return false;
43297
+ };
43298
+ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirectory) => {
43299
+ if (!SUPPRESSED_COMPILER_CODES.has(diagnostic.code)) return false;
43300
+ if (!project.hasReanimated) return false;
43301
+ const primaryLabel = diagnostic.labels[0];
43302
+ if (!primaryLabel) return false;
43303
+ const absolutePath = Path.isAbsolute(diagnostic.filename) ? diagnostic.filename : Path.join(rootDirectory, diagnostic.filename);
43304
+ let sourceText;
43305
+ try {
43306
+ sourceText = NFS.readFileSync(absolutePath, "utf-8");
43307
+ } catch {
43308
+ return false;
43309
+ }
43310
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
43311
+ };
43312
+ const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
43313
+ const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
43314
+ const REACT_COMPILER_PLUGIN = "react-hooks-js";
43315
+ const getScriptKind = (filename) => {
43316
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43317
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43318
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43319
+ return ts.ScriptKind.JS;
43320
+ };
43321
+ const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
43322
+ const nodeLineRange = (sourceFile, node) => ({
43323
+ startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
43324
+ endLine: sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1
43325
+ });
43326
+ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
43327
+ let foundRange = null;
43328
+ const visit = (node) => {
43329
+ if (foundRange) return;
43330
+ if (ts.isFunctionLike(node)) {
43331
+ const range = nodeLineRange(sourceFile, node);
43332
+ if (range.startLine <= targetLine && targetLine <= range.endLine) foundRange = range;
43333
+ return;
43334
+ }
43335
+ ts.forEachChild(node, visit);
43336
+ };
43337
+ visit(sourceFile);
43338
+ return foundRange;
43339
+ };
43340
+ const NEWLINE_BYTE = 10;
43341
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
43342
+ let lineNumber = 1;
43343
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
43344
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
43345
+ return lineNumber;
43346
+ };
43347
+ /**
43348
+ * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
43349
+ * whose enclosing function also carries a `react-hooks-js` (React
43350
+ * Compiler) diagnostic. Every react-hooks-js finding marks a function the
43351
+ * compiler could NOT compile (a bail-out / skip / preserve-manual-
43352
+ * memoization failure), so "React Compiler already caches every value
43353
+ * here" is false for that function and the manual memoization is
43354
+ * load-bearing, not dead weight.
43355
+ *
43356
+ * Matching is per enclosing function, not per file: a bailed-out
43357
+ * component must not silence redundant-memo findings in a sibling
43358
+ * component the compiler did optimize.
43359
+ *
43360
+ * Module-scope `memo(() => …)` wrappers have no enclosing function; for
43361
+ * those the memo call's own span is the containment range, so a bail-out
43362
+ * inside the inline-wrapped component still suppresses. Every failure
43363
+ * path (unreadable file, missing span) fails open and keeps the
43364
+ * diagnostic.
43365
+ */
43366
+ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) => {
43367
+ const compilerDiagnosticLinesByFile = /* @__PURE__ */ new Map();
43368
+ let hasManualMemoizationDiagnostic = false;
43369
+ for (const diagnostic of diagnostics) {
43370
+ if (isManualMemoizationDiagnostic(diagnostic)) {
43371
+ hasManualMemoizationDiagnostic = true;
43372
+ continue;
43373
+ }
43374
+ if (diagnostic.plugin !== REACT_COMPILER_PLUGIN || diagnostic.line <= 0) continue;
43375
+ const lines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
43376
+ if (lines) lines.push(diagnostic.line);
43377
+ else compilerDiagnosticLinesByFile.set(diagnostic.filePath, [diagnostic.line]);
43378
+ }
43379
+ if (!hasManualMemoizationDiagnostic || compilerDiagnosticLinesByFile.size === 0) return diagnostics;
43380
+ const parsedFileCache = /* @__PURE__ */ new Map();
43381
+ const readAndParse = (filePath) => {
43382
+ const cached = parsedFileCache.get(filePath);
43383
+ if (cached !== void 0) return cached;
43384
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
43385
+ let parsed;
43386
+ try {
43387
+ const buffer = NFS.readFileSync(absolutePath);
43388
+ parsed = {
43389
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
43390
+ buffer
43391
+ };
43392
+ } catch {
43393
+ parsed = null;
43394
+ }
43395
+ parsedFileCache.set(filePath, parsed);
43396
+ return parsed;
43397
+ };
43398
+ const isInsideBailedOutFunction = (diagnostic) => {
43399
+ const compilerLines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
43400
+ if (!compilerLines || diagnostic.line <= 0) return false;
43401
+ const parsed = readAndParse(diagnostic.filePath);
43402
+ if (!parsed) return false;
43403
+ let containmentRange = findOutermostEnclosingFunctionRange(parsed.sourceFile, diagnostic.line);
43404
+ if (!containmentRange && diagnostic.offset !== void 0 && diagnostic.length !== void 0) containmentRange = {
43405
+ startLine: diagnostic.line,
43406
+ endLine: lineOfUtf8Offset(parsed.buffer, diagnostic.offset + diagnostic.length)
43407
+ };
43408
+ if (!containmentRange) return false;
43409
+ const { startLine, endLine } = containmentRange;
43410
+ return compilerLines.some((compilerLine) => startLine <= compilerLine && compilerLine <= endLine);
43411
+ };
43412
+ return diagnostics.filter((diagnostic) => !isManualMemoizationDiagnostic(diagnostic) || !isInsideBailedOutFunction(diagnostic));
43413
+ };
43034
43414
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
43035
43415
  const LEADING_SEVERITY_LABEL_PATTERN = /^(?:Error|Warning):\s*/;
43036
43416
  const TRAILING_PERIOD_PATTERN = /\.$/;
@@ -43170,7 +43550,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43170
43550
  minifiedFileCache.set(absolutePath, minified);
43171
43551
  return minified;
43172
43552
  };
43173
- return parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
43553
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
43174
43554
  const { plugin, rule } = parseRuleCode(diagnostic.code);
43175
43555
  const primaryLabel = diagnostic.labels[0];
43176
43556
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -43197,7 +43577,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43197
43577
  ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
43198
43578
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
43199
43579
  };
43200
- });
43580
+ }), rootDirectory);
43201
43581
  };
43202
43582
  const buildOxlintChildEnv = (sourceEnv) => {
43203
43583
  const childEnv = {};
@@ -43708,6 +44088,7 @@ const runOxlint = async (options) => {
43708
44088
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
43709
44089
  const severityControls = buildRuleSeverityControls(userConfig);
43710
44090
  validateRuleRegistration();
44091
+ resetManifestCaches();
43711
44092
  if (includePaths !== void 0 && includePaths.length === 0) return [];
43712
44093
  const pluginPath = resolvePluginPath();
43713
44094
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
@@ -44370,9 +44751,10 @@ const CONTEXT_AXES = [
44370
44751
  }
44371
44752
  ];
44372
44753
  const SCORE_AXES = [...GATED_AXES, ...CONTEXT_AXES];
44373
- const worstGatedAxis = (score) => {
44374
- let worst = GATED_AXES[0];
44375
- for (const axis of GATED_AXES) if (score[axis.key] < score[worst.key]) worst = axis;
44754
+ const gatedAxesForSection = (section) => section === "devDependencies" ? GATED_AXES.filter((axis) => axis.key !== "vulnerability") : GATED_AXES;
44755
+ const worstGatedAxis = (score, gatedAxes) => {
44756
+ let worst = gatedAxes[0];
44757
+ for (const axis of gatedAxes) if (score[axis.key] < score[worst.key]) worst = axis;
44376
44758
  return worst;
44377
44759
  };
44378
44760
  const clampScore = (value) => {
@@ -44396,6 +44778,69 @@ const resolveConcreteVersion = (spec) => {
44396
44778
  if (range === null || range === "*") return null;
44397
44779
  return import_semver.minVersion(trimmed)?.version ?? null;
44398
44780
  };
44781
+ const readTextFileOrNull = (filePath) => {
44782
+ try {
44783
+ return NFS.readFileSync(filePath, "utf-8");
44784
+ } catch {
44785
+ return null;
44786
+ }
44787
+ };
44788
+ const buildNpmLockLookup = (lockText) => {
44789
+ let parsed;
44790
+ try {
44791
+ parsed = JSON.parse(lockText);
44792
+ } catch {
44793
+ return null;
44794
+ }
44795
+ if (typeof parsed !== "object" || parsed === null) return null;
44796
+ const lock = parsed;
44797
+ return (name) => {
44798
+ const entry = lock.packages?.[`node_modules/${name}`] ?? lock.dependencies?.[name];
44799
+ return typeof entry?.version === "string" ? entry.version : null;
44800
+ };
44801
+ };
44802
+ const buildYarnLockLookup = (lockText) => {
44803
+ const lines = lockText.split(/\r?\n/);
44804
+ return (name, spec) => {
44805
+ const descriptor = `${name}@${spec}`;
44806
+ const berryDescriptor = `${name}@npm:${spec}`;
44807
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
44808
+ const line = lines[lineIndex];
44809
+ if (line.startsWith(" ") || line.startsWith("#") || !line.includes(":")) continue;
44810
+ const descriptors = line.replace(/:\s*$/, "").split(",").map((part) => part.trim().replace(/^"|"$/g, ""));
44811
+ if (!descriptors.includes(descriptor) && !descriptors.includes(berryDescriptor)) continue;
44812
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && (lines[bodyIndex].startsWith(" ") || lines[bodyIndex] === ""); bodyIndex += 1) {
44813
+ const versionMatch = /^\s+version:?\s+"?([^"\s]+)"?/.exec(lines[bodyIndex]);
44814
+ if (versionMatch) return versionMatch[1];
44815
+ }
44816
+ return null;
44817
+ }
44818
+ return null;
44819
+ };
44820
+ };
44821
+ const buildPnpmLockLookup = (lockText) => {
44822
+ return (name) => {
44823
+ const entryPattern = new RegExp(`^ {4,8}['"]?${name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}['"]?:\\s*$`);
44824
+ const lines = lockText.split(/\r?\n/);
44825
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
44826
+ if (!entryPattern.test(lines[lineIndex])) continue;
44827
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && /^ {5,}\S/.test(lines[bodyIndex]); bodyIndex += 1) {
44828
+ const versionMatch = /^\s+version:\s+['"]?([^'"\s(]+)/.exec(lines[bodyIndex]);
44829
+ if (versionMatch) return versionMatch[1];
44830
+ }
44831
+ }
44832
+ return null;
44833
+ };
44834
+ };
44835
+ const buildLockedVersionLookup = (rootDirectory) => {
44836
+ const npmLockText = readTextFileOrNull(Path.join(rootDirectory, "package-lock.json"));
44837
+ if (npmLockText !== null) return buildNpmLockLookup(npmLockText);
44838
+ const yarnLockText = readTextFileOrNull(Path.join(rootDirectory, "yarn.lock"));
44839
+ if (yarnLockText !== null) return buildYarnLockLookup(yarnLockText);
44840
+ const pnpmLockText = readTextFileOrNull(Path.join(rootDirectory, "pnpm-lock.yaml"));
44841
+ if (pnpmLockText !== null) return buildPnpmLockLookup(pnpmLockText);
44842
+ return null;
44843
+ };
44399
44844
  const locateDependencyKey = (packageJsonText, section, name) => {
44400
44845
  const needle = `"${name}"`;
44401
44846
  const sectionHeader = new RegExp(`"${section}"\\s*:\\s*\\{`);
@@ -44422,7 +44867,7 @@ const locateDependencyKey = (packageJsonText, section, name) => {
44422
44867
  }
44423
44868
  return null;
44424
44869
  };
44425
- const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies) => {
44870
+ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies, lockedVersionLookup) => {
44426
44871
  const sectionByName = /* @__PURE__ */ new Map();
44427
44872
  for (const name of Object.keys(packageJson.dependencies ?? {})) sectionByName.set(name, "dependencies");
44428
44873
  if (includeDevDependencies) {
@@ -44432,13 +44877,18 @@ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDepe
44432
44877
  for (const [name, section] of sectionByName) {
44433
44878
  if (SUPPLY_CHAIN_IGNORED_PACKAGES.has(name)) continue;
44434
44879
  const spec = (packageJson[section] ?? {})[name] ?? "";
44435
- const version = resolveConcreteVersion(spec);
44436
- if (version === null) continue;
44880
+ const floorVersion = resolveConcreteVersion(spec);
44881
+ if (floorVersion === null) continue;
44882
+ const lockedVersion = lockedVersionLookup?.(name, spec) ?? null;
44883
+ const lockSatisfiesSpec = lockedVersion !== null && import_semver.valid(lockedVersion) !== null && import_semver.satisfies(lockedVersion, spec);
44884
+ const isExactPin = import_semver.valid(spec.trim()) !== null;
44437
44885
  const location = locateDependencyKey(packageJsonText, section, name);
44438
44886
  dependencies.push({
44439
44887
  name,
44440
- version,
44888
+ version: lockSatisfiesSpec ? lockedVersion : floorVersion,
44441
44889
  spec,
44890
+ section,
44891
+ resolution: isExactPin ? "pin" : lockSatisfiesSpec ? "locked" : "floor",
44442
44892
  line: location?.line ?? 0,
44443
44893
  column: location?.column ?? 0
44444
44894
  });
@@ -44589,7 +45039,11 @@ const formatAlertReason = (topAlerts, totalCount) => {
44589
45039
  }
44590
45040
  return `Socket flagged ${totalCount} alerts (${topAlerts.map((alert) => friendlyAlertType(alert.type)).join(", ")}${totalCount > topAlerts.length ? ` (+${totalCount - topAlerts.length} more)` : ""}); most severe: ${displaySeverity(topAlerts[0].severity)}.`;
44591
45041
  };
44592
- const formatDependencyIdentity = (dependency) => import_semver.valid(dependency.spec) !== null ? `${dependency.name}@${dependency.version}` : `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
45042
+ const formatDependencyIdentity = (dependency) => {
45043
+ if (dependency.resolution === "pin") return `${dependency.name}@${dependency.version}`;
45044
+ if (dependency.resolution === "locked") return `${dependency.name}@${dependency.version} (lockfile resolution of "${dependency.spec}")`;
45045
+ return `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
45046
+ };
44593
45047
  const buildSupplyChainHelp = (dependency, failingAxis, topAlerts, packagePageUrl, options) => {
44594
45048
  const hasCriticalAlert = topAlerts.some((alert) => alert.severity.toLowerCase() === "critical");
44595
45049
  const entry = `\`"${dependency.name}": "${dependency.spec}"\``;
@@ -44632,7 +45086,7 @@ const buildLowScoreDiagnostic = (dependency, artifact, failingAxis, options) =>
44632
45086
  const checkSupplyChain = (input) => gen(function* () {
44633
45087
  const options = resolveOptions(input.userConfig);
44634
45088
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
44635
- const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
45089
+ const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies, buildLockedVersionLookup(input.rootDirectory));
44636
45090
  if (dependencies.length === 0) return [];
44637
45091
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
44638
45092
  if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
@@ -44641,7 +45095,7 @@ const checkSupplyChain = (input) => gen(function* () {
44641
45095
  for (let index = 0; index < dependencies.length; index += 1) {
44642
45096
  const artifact = artifacts[index];
44643
45097
  if (!artifact) continue;
44644
- const worstAxis = worstGatedAxis(artifact.score);
45098
+ const worstAxis = worstGatedAxis(artifact.score, gatedAxesForSection(dependencies[index].section));
44645
45099
  if (toHundred(artifact.score[worstAxis.key]) >= options.minScore) continue;
44646
45100
  diagnostics.push(buildLowScoreDiagnostic(dependencies[index], artifact, worstAxis, options));
44647
45101
  }
@@ -45924,7 +46378,7 @@ const makeNoopConsole = () => ({
45924
46378
  });
45925
46379
  //#endregion
45926
46380
  //#region src/cli/utils/version.ts
45927
- const VERSION = "0.7.1-dev.64452aa";
46381
+ const VERSION = "0.7.1-dev.7cf4d0b";
45928
46382
  //#endregion
45929
46383
  //#region src/cli/utils/json-mode.ts
45930
46384
  let context = null;
@@ -46293,13 +46747,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
46293
46747
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
46294
46748
  * standard `SENTRY_RELEASE` override.
46295
46749
  */
46296
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.1-dev.64452aa`;
46750
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.1-dev.7cf4d0b`;
46297
46751
  /**
46298
46752
  * Deployment environment shown in Sentry's environment filter. Defaults to
46299
46753
  * `production` for tagged releases and `development` for dev/unbuilt versions,
46300
46754
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
46301
46755
  */
46302
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.1-dev.64452aa") ? "development" : "production");
46756
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.1-dev.7cf4d0b") ? "development" : "production");
46303
46757
  /**
46304
46758
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
46305
46759
  * (set to `0` to disable tracing) and falls back to
@@ -58143,7 +58597,7 @@ const buildRuntimeLayers = (input) => {
58143
58597
  const linterLayer = input.shouldSkipLint ? Linter.layerOf([]) : Linter.layerOxlint;
58144
58598
  const deadCodeLayer = input.shouldRunDeadCode ? DeadCode.layerNode : DeadCode.layerOf([]);
58145
58599
  const scoreLayer = input.shouldComputeScore ? Score.layerHttp : Score.layerOf(null);
58146
- const supplyChainLayer = input.userConfig?.supplyChain?.enabled === false ? SupplyChain.layerOf([]) : SupplyChain.layerNode;
58600
+ const supplyChainLayer = input.shouldRunSupplyChain ? SupplyChain.layerNode : SupplyChain.layerOf([]);
58147
58601
  const projectLayer = input.projectInfoOverride === void 0 ? Project.layerNode : Project.layerOf(input.projectInfoOverride);
58148
58602
  const progressLayer = input.shouldShowProgressSpinners ? Progress.layerOra(buildSpinnerProgressHandle) : Progress.layerNoop;
58149
58603
  const baseLayers = mergeAll$1(projectLayer, input.hasConfigOverride ? Config.layerOf({
@@ -58606,6 +59060,7 @@ const buildScanResultCacheKey = (input) => {
58606
59060
  engineOptions: {
58607
59061
  lint: input.options.lint,
58608
59062
  deadCode: input.options.deadCode,
59063
+ supplyChain: input.options.supplyChain,
58609
59064
  includePaths: [...input.options.includePaths].sort(),
58610
59065
  customRulesOnly: input.options.customRulesOnly,
58611
59066
  respectInlineDisables: input.options.respectInlineDisables,
@@ -58815,13 +59270,15 @@ const buildOutcomeAttributes = (input) => {
58815
59270
  const countBySource = {
58816
59271
  config: 0,
58817
59272
  override: 0,
58818
- inline: 0
59273
+ inline: 0,
59274
+ "foreign-inline": 0
58819
59275
  };
58820
59276
  for (const suppression of input.suppressedRuleCounts) countBySource[suppression.source] += suppression.count;
58821
- suppressionRollup.suppressed = countBySource.config + countBySource.override + countBySource.inline;
59277
+ suppressionRollup.suppressed = countBySource.config + countBySource.override + countBySource.inline + countBySource["foreign-inline"];
58822
59278
  suppressionRollup.suppressedConfig = countBySource.config;
58823
59279
  suppressionRollup.suppressedOverride = countBySource.override;
58824
59280
  suppressionRollup.suppressedInline = countBySource.inline;
59281
+ suppressionRollup.suppressedForeignInline = countBySource["foreign-inline"];
58825
59282
  }
58826
59283
  const attributes = {
58827
59284
  ...withNamespace("outcome", {
@@ -58913,6 +59370,7 @@ const buildScanAttributes = (input) => {
58913
59370
  maxDurationMs: input.maxDurationMs,
58914
59371
  lint: input.lint,
58915
59372
  deadCode: input.deadCode,
59373
+ supplyChain: input.supplyChain,
58916
59374
  scoreOnly: input.scoreOnly,
58917
59375
  noScore: input.noScore,
58918
59376
  respectInlineDisables: input.respectInlineDisables,
@@ -60187,6 +60645,7 @@ const buildIgnoredTags = (userConfig) => {
60187
60645
  const mergeInspectOptions = (inputOptions, userConfig) => ({
60188
60646
  lint: inputOptions.lint ?? userConfig?.lint ?? true,
60189
60647
  deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
60648
+ supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
60190
60649
  verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
60191
60650
  outputDirectory: inputOptions.outputDirectory || null,
60192
60651
  scoreOnly: inputOptions.scoreOnly ?? false,
@@ -60226,6 +60685,7 @@ const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorke
60226
60685
  maxDurationMs: options.maxDurationMs,
60227
60686
  lint: options.lint,
60228
60687
  deadCode: options.deadCode,
60688
+ supplyChain: options.supplyChain,
60229
60689
  scoreOnly: options.scoreOnly,
60230
60690
  noScore: options.noScore,
60231
60691
  respectInlineDisables: options.respectInlineDisables,
@@ -60309,6 +60769,7 @@ const runBaselineComparison = async (params) => {
60309
60769
  projectInfoOverride: params.headProjectInfo,
60310
60770
  shouldSkipLint: !params.options.lint || !params.resolvedNodeBinaryPath,
60311
60771
  shouldRunDeadCode: false,
60772
+ shouldRunSupplyChain: params.options.supplyChain,
60312
60773
  shouldComputeScore: false,
60313
60774
  shouldShowProgressSpinners: false,
60314
60775
  oxlintConcurrency: params.options.concurrency
@@ -60397,6 +60858,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
60397
60858
  configSourceDirectory,
60398
60859
  shouldSkipLint: !options.lint || lintBindingMissing,
60399
60860
  shouldRunDeadCode: options.deadCode,
60861
+ shouldRunSupplyChain: options.supplyChain,
60400
60862
  shouldComputeScore: !options.noScore,
60401
60863
  shouldShowProgressSpinners,
60402
60864
  oxlintConcurrency: options.concurrency
@@ -62682,8 +63144,8 @@ const resolveParallelFlag = (parallel) => parallel === false ? 1 : void 0;
62682
63144
  * accepts. Flag-derived fields only (`scoreOnly`, `noScore`, `silent`,
62683
63145
  * `isCi`): every `userConfig` fallback — including `noScore` — lives in
62684
63146
  * `inspect()`'s merge layer, which sees each project's module-merged
62685
- * config. The plain boolean knobs (`lint`, `deadCode`, `verbose`) pass
62686
- * through unchanged for the same reason. This resolver reads
63147
+ * config. The plain boolean knobs (`lint`, `deadCode`, `supplyChain`,
63148
+ * `verbose`) pass through unchanged for the same reason. This resolver reads
62687
63149
  * `userConfig` only to decide the `--blocking warning` gate.
62688
63150
  */
62689
63151
  const resolveCliInspectOptions = (flags, userConfig) => {
@@ -62691,6 +63153,7 @@ const resolveCliInspectOptions = (flags, userConfig) => {
62691
63153
  return {
62692
63154
  lint: flags.lint,
62693
63155
  deadCode: flags.deadCode,
63156
+ supplyChain: flags.supplyChain,
62694
63157
  verbose: flags.verbose,
62695
63158
  outputDirectory: flags.outputDir,
62696
63159
  respectInlineDisables: flags.respectInlineDisables === false ? false : void 0,
@@ -63241,7 +63704,7 @@ const inspectAction = async (directory, flags) => {
63241
63704
  const scanDirectory = projectScanTarget.resolvedDirectory;
63242
63705
  const projectConfig = projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0);
63243
63706
  const projectConfigSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
63244
- const supplyChainEnabled = projectConfig?.supplyChain?.enabled !== false;
63707
+ const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
63245
63708
  let includePaths;
63246
63709
  let supplyChainManifestChanged = false;
63247
63710
  if (isDiffMode) {
@@ -64161,10 +64624,12 @@ const ROOT_FLAG_SPEC = {
64161
64624
  "--no-parallel",
64162
64625
  "--no-respect-inline-disables",
64163
64626
  "--no-score",
64627
+ "--no-supply-chain",
64164
64628
  "--no-telemetry",
64165
64629
  "--no-warnings",
64166
64630
  "--score",
64167
64631
  "--staged",
64632
+ "--supply-chain",
64168
64633
  "--verbose",
64169
64634
  "--version",
64170
64635
  "--warnings",
@@ -64413,7 +64878,7 @@ ${highlighter.dim("Learn more:")}
64413
64878
  ${highlighter.info(CI_URL)}
64414
64879
  `;
64415
64880
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
64416
- 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);
64881
+ 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);
64417
64882
  program.action(inspectAction);
64418
64883
  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));
64419
64884
  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);
@@ -64463,4 +64928,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
64463
64928
  export {};
64464
64929
 
64465
64930
  //# sourceMappingURL=cli.js.map
64466
- //# debugId=57f4381b-7060-58c6-88a4-95b4c97d3b74
64931
+ //# debugId=7b7c23b7-250e-5339-af06-e3e3b5eb5f98