react-doctor 0.7.1 → 0.7.2-dev.aa519e5

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/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]="7cb857ba-6f0a-57d9-91f3-3c286472121d")}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]="10e3e1b0-f291-5306-b5fd-79b158b00159")}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";
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import * as NodeChildProcess from "node:child_process";
10
10
  import { execFileSync, spawn, spawnSync } from "node:child_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 from "node:os";
15
15
  import { parseJSON5 } from "confbox";
@@ -32800,8 +32800,10 @@ const isFile = (filePath) => {
32800
32800
  };
32801
32801
  const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?|mts|mjs)$/;
32802
32802
  const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
32803
+ const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
32803
32804
  const MINIFIED_SNIFF_BYTES = 65536;
32804
32805
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
32806
+ const SCANNED_DOT_DIRECTORIES = new Set([".dumi", ".storybook"]);
32805
32807
  const IGNORED_DIRECTORIES = new Set([
32806
32808
  ".angular",
32807
32809
  ".astro",
@@ -32943,7 +32945,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32943
32945
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32944
32946
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32945
32947
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
32946
- const CORE_PACKAGE_VERSION = "0.7.1";
32948
+ const CORE_PACKAGE_VERSION = "0.7.2";
32947
32949
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32948
32950
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
32949
32951
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -32953,8 +32955,9 @@ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
32953
32955
  const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
32954
32956
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
32955
32957
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
32956
- const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
32957
- const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath);
32958
+ const isIgnoredDirectoryName = (directoryName) => IGNORED_DIRECTORIES.has(directoryName) || directoryName.startsWith(".") && !SCANNED_DOT_DIRECTORIES.has(directoryName);
32959
+ const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => isIgnoredDirectoryName(segment));
32960
+ const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath) && !GENERATED_SOURCE_DIRECTORY_PATTERN.test(filePath);
32958
32961
  const isMinifiedSource = (absolutePath) => {
32959
32962
  let fileDescriptor;
32960
32963
  try {
@@ -33018,7 +33021,7 @@ function* walkSourceTreeFiles(rootDirectory) {
33018
33021
  for (const entry of readDirectoryEntries(currentDirectory)) {
33019
33022
  const absolutePath = Path.join(currentDirectory, entry.name);
33020
33023
  if (entry.isDirectory()) {
33021
- if (!IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
33024
+ if (!isIgnoredDirectoryName(entry.name)) stack.push(absolutePath);
33022
33025
  continue;
33023
33026
  }
33024
33027
  if (entry.isFile()) yield {
@@ -33064,6 +33067,23 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33064
33067
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33065
33068
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33066
33069
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33070
+ /**
33071
+ * The chain of ancestor directories of `startDirectory`, nearest first, up to
33072
+ * and including the filesystem root. With `includeStart`, the chain begins at
33073
+ * `startDirectory` itself; otherwise at its parent. The one place the "walk up
33074
+ * the tree" traversal is expressed, so each caller adds only its own match /
33075
+ * stop policy instead of re-deriving the `path.dirname` loop.
33076
+ */
33077
+ const ancestorDirectories = (startDirectory, { includeStart }) => {
33078
+ const directories = [];
33079
+ let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
33080
+ while (true) {
33081
+ directories.push(currentDirectory);
33082
+ const parentDirectory = Path.dirname(currentDirectory);
33083
+ if (parentDirectory === currentDirectory) return directories;
33084
+ currentDirectory = parentDirectory;
33085
+ }
33086
+ };
33067
33087
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33068
33088
  const clearPackageJsonCache = () => {
33069
33089
  cachedPackageJsons.clear();
@@ -33097,11 +33117,7 @@ const isMonorepoRoot = (directory) => {
33097
33117
  return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
33098
33118
  };
33099
33119
  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
- }
33120
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
33105
33121
  return null;
33106
33122
  };
33107
33123
  /**
@@ -33747,6 +33763,74 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
33747
33763
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
33748
33764
  };
33749
33765
  };
33766
+ /**
33767
+ * Walk up from `startDirectory` to the nearest ancestor that owns a
33768
+ * `package.json`, stopping at (and including) the enclosing project boundary
33769
+ * — the working tree's git root or a monorepo root. Lets a scan of a package
33770
+ * subfolder that has no `package.json` of its own adopt the nearest enclosing
33771
+ * package as its project root, so it inherits that package's dependency +
33772
+ * framework detection instead of synthesizing an empty (React-blind) project.
33773
+ *
33774
+ * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
33775
+ * most specific owning package (a leaf workspace, not just the repo root), so
33776
+ * a subfolder of one workspace no longer borrows a sibling's capabilities.
33777
+ *
33778
+ * Returns `null` when no ancestor `package.json` exists inside the project
33779
+ * boundary (a loose tree of files, or a subfolder of a package-less repo).
33780
+ */
33781
+ const findNearestAncestorPackageJson = (startDirectory) => {
33782
+ if (isProjectBoundary(startDirectory)) return null;
33783
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
33784
+ if (isFile(Path.join(directory, "package.json"))) return directory;
33785
+ if (isProjectBoundary(directory)) return null;
33786
+ }
33787
+ return null;
33788
+ };
33789
+ /**
33790
+ * Last-resort React detection: locate React the way Node's `node_modules`
33791
+ * resolution would from `directory` — check each `node_modules/react` up the
33792
+ * tree — but bounded to the enclosing repo so a globally installed React can't
33793
+ * masquerade as the project's version. Stops at (and including) the nearest
33794
+ * project boundary — the git root or a monorepo root — and returns the first
33795
+ * installed `version`.
33796
+ *
33797
+ * Makes "React is installed and importable" ⇒ "React is detected" an invariant
33798
+ * for packages whose only React declaration is a version-less spec
33799
+ * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
33800
+ * `node_modules` the declaration walks never reach — the profile of a component
33801
+ * package inside a monorepo. Matching the `node_modules/react` entry rather than
33802
+ * its realpath means a project whose `node_modules` is symlinked elsewhere (a
33803
+ * Docker volume, a shared store) still counts, while a global install outside
33804
+ * the repo does not.
33805
+ *
33806
+ * Returns `null` when React isn't installed within the repo or its package.json
33807
+ * carries no version string. A tree with no boundary marker at all bounds the
33808
+ * search to `directory`, so a dependency hoisted above the scanned package there
33809
+ * is conservatively not adopted.
33810
+ */
33811
+ /**
33812
+ * The nearest enclosing project boundary (git root or monorepo root), or
33813
+ * `directory` itself when the scan target sits outside any repo. Bounds the
33814
+ * node_modules walk so a React hoisted anywhere inside the repo counts, but a
33815
+ * React in an ancestor above the repo — a home-directory or global
33816
+ * `node_modules` — can't leak in.
33817
+ */
33818
+ const findContainmentRoot = (directory) => {
33819
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
33820
+ return directory;
33821
+ };
33822
+ const resolveInstalledReactVersion = (directory) => {
33823
+ const containmentRoot = findContainmentRoot(directory);
33824
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
33825
+ const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
33826
+ if (isFile(reactPackageJsonPath)) {
33827
+ const installedVersion = readPackageJson(reactPackageJsonPath).version;
33828
+ return typeof installedVersion === "string" ? installedVersion : null;
33829
+ }
33830
+ if (ancestorDirectory === containmentRoot) return null;
33831
+ }
33832
+ return null;
33833
+ };
33750
33834
  const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
33751
33835
  const rootValue = select(rootPackageJson);
33752
33836
  if (rootValue !== null) return rootValue;
@@ -33903,6 +33987,10 @@ const normalizeCompilerOptions = (compilerOptions) => {
33903
33987
  hasExplicitLib
33904
33988
  };
33905
33989
  };
33990
+ const normalizeReferencePaths = (references) => {
33991
+ if (!Array.isArray(references)) return [];
33992
+ return references.map((reference) => isPlainObject(reference) && typeof reference.path === "string" ? reference.path : null).filter((referencePath) => referencePath !== null);
33993
+ };
33906
33994
  const readTsConfig = (filePath) => {
33907
33995
  let content;
33908
33996
  try {
@@ -33914,6 +34002,7 @@ const readTsConfig = (filePath) => {
33914
34002
  if (!isPlainObject(parsed.config)) return null;
33915
34003
  return {
33916
34004
  extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
34005
+ referencePaths: normalizeReferencePaths(parsed.config.references),
33917
34006
  compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
33918
34007
  };
33919
34008
  };
@@ -33954,20 +34043,37 @@ const libEntryIncludesES2023Array = (entry) => {
33954
34043
  };
33955
34044
  const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
33956
34045
  const compilerOptionsArePreES2023 = (compilerOptions) => {
34046
+ if (compilerOptions.target && targetYearIsPreES2023(compilerOptions.target)) return true;
33957
34047
  if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
33958
- if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
33959
34048
  return false;
33960
34049
  };
33961
34050
  const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
33962
- const detectPreES2023FromConfig = (tsConfigPath) => {
34051
+ const detectPreES2023FromConfig = (tsConfigPath, visitedConfigPaths = /* @__PURE__ */ new Set()) => {
34052
+ if (visitedConfigPaths.has(tsConfigPath)) return false;
33963
34053
  const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
33964
34054
  if (!compilerOptions) return false;
33965
- if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) return false;
34055
+ if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) {
34056
+ const tsConfig = readTsConfig(tsConfigPath);
34057
+ if (!tsConfig) return false;
34058
+ const nextVisitedConfigPaths = new Set(visitedConfigPaths);
34059
+ nextVisitedConfigPaths.add(tsConfigPath);
34060
+ const configDirectory = Path.dirname(tsConfigPath);
34061
+ return tsConfig.referencePaths.some((referencePath) => {
34062
+ const resolvedReferencePath = Path.resolve(configDirectory, referencePath);
34063
+ const referencedConfigPath = isFile(resolvedReferencePath) ? resolvedReferencePath : Path.join(resolvedReferencePath, TSCONFIG_FILENAME);
34064
+ return isFile(referencedConfigPath) && detectPreES2023FromConfig(referencedConfigPath, nextVisitedConfigPaths);
34065
+ });
34066
+ }
33966
34067
  return compilerOptionsArePreES2023(compilerOptions);
33967
34068
  };
34069
+ const FALLBACK_TSCONFIG_FILENAMES = ["tsconfig.app.json", "tsconfig.build.json"];
33968
34070
  const detectPreES2023Target = (directory) => {
33969
34071
  const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
33970
34072
  if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
34073
+ for (const fallbackFilename of FALLBACK_TSCONFIG_FILENAMES) {
34074
+ const fallbackPath = Path.join(directory, fallbackFilename);
34075
+ if (isFile(fallbackPath)) return detectPreES2023FromConfig(fallbackPath);
34076
+ }
33971
34077
  return false;
33972
34078
  };
33973
34079
  const REACT_DEPENDENCY_NAMES = new Set([
@@ -34086,19 +34192,20 @@ const clearProjectCache = () => {
34086
34192
  };
34087
34193
  /**
34088
34194
  * 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).
34195
+ * its own — a package subfolder like `repo/packages` or `app/src/features`,
34196
+ * or any loose tree of TypeScript/JavaScript files. Dependency + framework
34197
+ * detection is inherited from the nearest enclosing package (a leaf
34198
+ * workspace, a plain app root, or a monorepo root — whichever is closest,
34199
+ * bounded by the git root), so scanning a subdirectory of a React project
34200
+ * still gets the React capabilities; a standalone non-React directory simply
34201
+ * scans with the framework-agnostic rules. Throws only when the directory has
34202
+ * nothing to scan (no enclosing project and no source files of its own).
34096
34203
  */
34097
34204
  const discoverProjectWithoutPackageJson = (directory) => {
34098
34205
  const sourceFileCount = countSourceFiles(directory);
34099
34206
  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;
34207
+ const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
34208
+ const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
34102
34209
  if (enclosingProject !== null) return {
34103
34210
  ...enclosingProject,
34104
34211
  rootDirectory: directory,
@@ -34128,6 +34235,7 @@ const discoverProjectWithoutPackageJson = (directory) => {
34128
34235
  shopifyFlashListVersion: null,
34129
34236
  shopifyFlashListMajorVersion: null,
34130
34237
  hasReanimated: false,
34238
+ reanimatedVersion: null,
34131
34239
  isPreES2023Target: hasOwnTsConfig && detectPreES2023Target(directory),
34132
34240
  sourceFileCount
34133
34241
  };
@@ -34202,6 +34310,7 @@ const discoverProject = (directory) => {
34202
34310
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
34203
34311
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
34204
34312
  if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
34313
+ if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
34205
34314
  const projectName = packageJson.name ?? Path.basename(directory);
34206
34315
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
34207
34316
  const sourceFileCount = countSourceFiles(directory);
@@ -34219,6 +34328,7 @@ const discoverProject = (directory) => {
34219
34328
  version: findShopifyFlashListVersion(directory, packageJson)
34220
34329
  }) : null;
34221
34330
  const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
34331
+ const reanimatedVersion = hasReanimated ? findInWorkspacePackageJsons(directory, packageJson, (workspacePackageJson) => getDependencySpec(workspacePackageJson, REANIMATED_DEPENDENCY_NAME)) : null;
34222
34332
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
34223
34333
  rootDirectory: directory,
34224
34334
  rootPackageJson: packageJson,
@@ -34248,6 +34358,7 @@ const discoverProject = (directory) => {
34248
34358
  shopifyFlashListVersion,
34249
34359
  shopifyFlashListMajorVersion: shopifyFlashListVersion === null ? null : getLowestDependencyMajor(shopifyFlashListVersion),
34250
34360
  hasReanimated,
34361
+ reanimatedVersion,
34251
34362
  isPreES2023Target,
34252
34363
  sourceFileCount
34253
34364
  };
@@ -34440,6 +34551,9 @@ const buildRuleSeverityControls = (config) => {
34440
34551
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
34441
34552
  };
34442
34553
  };
34554
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34555
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34556
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34443
34557
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34444
34558
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
34445
34559
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -34517,6 +34631,7 @@ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34517
34631
  "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
34518
34632
  "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
34519
34633
  "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
34634
+ "react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
34520
34635
  "react/jsx-no-undef": "react-doctor/jsx-no-undef",
34521
34636
  "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
34522
34637
  "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
@@ -34590,9 +34705,6 @@ const tokenizeRuleList = (ruleList) => {
34590
34705
  if (!ruleSection) return [];
34591
34706
  return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
34592
34707
  };
34593
- const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34594
- const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34595
- const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34596
34708
  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}\`).`;
34597
34709
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34598
34710
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
@@ -34646,6 +34758,53 @@ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34646
34758
  if (!ruleId.startsWith("react-doctor/")) return null;
34647
34759
  return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
34648
34760
  };
34761
+ const ESLINT_REACT_HOOKS_PLUGIN_PREFIX = "react-hooks/";
34762
+ const buildAcceptedTokens = (ruleId) => {
34763
+ const shortRuleId = ruleId.slice(15);
34764
+ return new Set([ruleId, `${ESLINT_REACT_HOOKS_PLUGIN_PREFIX}${shortRuleId}`]);
34765
+ };
34766
+ const listNamesRule = (ruleList, acceptedTokens) => tokenizeRuleList(ruleList).some((token) => acceptedTokens.has(token));
34767
+ const isInlineDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
34768
+ const candidates = [{
34769
+ line: lines[diagnosticLineIndex],
34770
+ requiredScope: "line"
34771
+ }, {
34772
+ line: lines[diagnosticLineIndex - 1],
34773
+ requiredScope: "next-line"
34774
+ }];
34775
+ for (const { line, requiredScope } of candidates) {
34776
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
34777
+ if (!match) continue;
34778
+ const [, , scope, ruleList] = match;
34779
+ if (scope !== requiredScope) continue;
34780
+ if (listNamesRule(ruleList, acceptedTokens)) return true;
34781
+ }
34782
+ return false;
34783
+ };
34784
+ const isBlockDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
34785
+ let isRangeOpen = false;
34786
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
34787
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
34788
+ const line = lines[lineIndex];
34789
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
34790
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
34791
+ if (disableMatch && listNamesRule(disableMatch[2], acceptedTokens)) {
34792
+ isRangeOpen = true;
34793
+ continue;
34794
+ }
34795
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
34796
+ if (enableMatch) {
34797
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
34798
+ if (enabledRules.length === 0 || enabledRules.some((rule) => acceptedTokens.has(rule))) isRangeOpen = false;
34799
+ }
34800
+ }
34801
+ return isRangeOpen;
34802
+ };
34803
+ const isCompilerRuleForeignDisabled = (lines, diagnosticLineIndex, ruleId) => {
34804
+ if (!ruleId.startsWith("react-hooks-js/")) return false;
34805
+ const acceptedTokens = buildAcceptedTokens(ruleId);
34806
+ return isInlineDisabled(lines, diagnosticLineIndex, acceptedTokens) || isBlockDisabled(lines, diagnosticLineIndex, acceptedTokens);
34807
+ };
34649
34808
  const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34650
34809
  const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34651
34810
  const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
@@ -34789,6 +34948,11 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
34789
34948
  isSuppressed: true,
34790
34949
  nearMissHint: null
34791
34950
  };
34951
+ if (isCompilerRuleForeignDisabled(lines, diagnosticLineIndex, ruleId)) return {
34952
+ isSuppressed: true,
34953
+ nearMissHint: null,
34954
+ isForeignDirective: true
34955
+ };
34792
34956
  const directComments = findStackedDisableCommentsAbove(lines, diagnosticLineIndex);
34793
34957
  if (hasChainSuppressor(directComments, ruleId)) return {
34794
34958
  isSuppressed: true,
@@ -35074,12 +35238,18 @@ const collectStringSet = (values) => {
35074
35238
  return new Set(values.filter((value) => typeof value === "string"));
35075
35239
  };
35076
35240
  const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
35241
+ const NODE_CLI_PATH_SEGMENTS = new Set([
35242
+ "scripts",
35243
+ "bin",
35244
+ "tools"
35245
+ ]);
35246
+ const hasNodeCliPathSegment = (filePath) => filePath.split(/[\\/]/).slice(0, -1).some((segment) => NODE_CLI_PATH_SEGMENTS.has(segment));
35077
35247
  /**
35078
35248
  * Pre-compiles every stateful filter and returns a single
35079
35249
  * `apply(diagnostic)` closure that runs (in order):
35080
35250
  *
35081
35251
  * 1. auto-suppress (test-noise rules in test files; `migration-hint`
35082
- * wins over `test-noise`)
35252
+ * wins over `test-noise`; `no-console` on Node CLI scripts)
35083
35253
  * 2. severity overrides (top-level `rules` / `categories`, with
35084
35254
  * `"off"` dropping)
35085
35255
  * 3. warning suppression (only when `showWarnings` is false: drops every
@@ -35177,6 +35347,12 @@ const buildDiagnosticPipeline = (input) => {
35177
35347
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35178
35348
  return false;
35179
35349
  };
35350
+ const isNoConsoleOnNodeCliScript = (diagnostic) => {
35351
+ if (diagnostic.rule !== "no-console") return false;
35352
+ if (hasNodeCliPathSegment(diagnostic.filePath)) return true;
35353
+ const lines = getFileLines(diagnostic.filePath);
35354
+ return lines !== null && Boolean(lines[0]?.startsWith("#!"));
35355
+ };
35180
35356
  const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35181
35357
  if (!hasRuntimeGlobals) return false;
35182
35358
  if (diagnostic.rule !== "jsx-no-undef") return false;
@@ -35186,6 +35362,7 @@ const buildDiagnosticPipeline = (input) => {
35186
35362
  return {
35187
35363
  apply: (diagnostic) => {
35188
35364
  if (shouldAutoSuppress(diagnostic)) return null;
35365
+ if (isNoConsoleOnNodeCliScript(diagnostic)) return null;
35189
35366
  let current = diagnostic;
35190
35367
  let explicitSeverityOverride;
35191
35368
  let explicitRuleOverride;
@@ -35215,7 +35392,7 @@ const buildDiagnosticPipeline = (input) => {
35215
35392
  if (lines) {
35216
35393
  const ruleIdentifier = `${current.plugin}/${current.rule}`;
35217
35394
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35218
- if (evaluation.isSuppressed) return suppress(current, "inline");
35395
+ if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
35219
35396
  if (evaluation.nearMissHint) current = {
35220
35397
  ...current,
35221
35398
  suppressionHint: evaluation.nearMissHint
@@ -38564,7 +38741,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38564
38741
  })),
38565
38742
  showStagedContent: (directory, relativePath, options) => runCommand({
38566
38743
  command: "git",
38567
- args: ["show", `:${relativePath}`],
38744
+ args: ["show", `:./${relativePath}`],
38568
38745
  directory,
38569
38746
  maxStdoutBytes: options?.maxBufferBytes
38570
38747
  }).pipe(map$3((result) => result.status === 0 ? result.stdout : null)),
@@ -39347,9 +39524,17 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
39347
39524
  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`;
39348
39525
  };
39349
39526
  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";
39527
+ const ACCESSORS_MIN_VERSION = "3.15.0";
39528
+ const supportsAccessors = (reanimatedVersion) => {
39529
+ if (reanimatedVersion === null) return true;
39530
+ const lowerBound = import_semver.validRange(reanimatedVersion) !== null ? import_semver.minVersion(reanimatedVersion) : import_semver.coerce(reanimatedVersion);
39531
+ if (lowerBound === null) return true;
39532
+ return import_semver.gte(lowerBound, ACCESSORS_MIN_VERSION);
39533
+ };
39350
39534
  const appendReanimatedSharedValueHint = (help, rule, project) => {
39351
39535
  if (rule !== "immutability") return help;
39352
39536
  if (!project.hasReanimated) return help;
39537
+ if (!supportsAccessors(project.reanimatedVersion)) return help;
39353
39538
  if (!help) return REANIMATED_SHARED_VALUE_HINT;
39354
39539
  return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
39355
39540
  };
@@ -39511,13 +39696,13 @@ const REACT_USE_BINDING_RESOLUTION = {
39511
39696
  isReactUseBinding: true,
39512
39697
  isReactNamespaceBinding: false
39513
39698
  };
39514
- const getScriptKind = (filename) => {
39699
+ const getScriptKind$2 = (filename) => {
39515
39700
  if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
39516
39701
  if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
39517
39702
  if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
39518
39703
  return ts.ScriptKind.JS;
39519
39704
  };
39520
- const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39705
+ const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39521
39706
  const unwrapExpression = (expression) => {
39522
39707
  let currentExpression = expression;
39523
39708
  while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
@@ -39766,8 +39951,8 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
39766
39951
  return matchedIdentifier;
39767
39952
  };
39768
39953
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
39769
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filename));
39770
- const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset(sourceText, utf8Offset));
39954
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
39955
+ const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
39771
39956
  if (!useIdentifier) return null;
39772
39957
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
39773
39958
  };
@@ -39788,6 +39973,201 @@ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
39788
39973
  const bindingResolution = resolveUseCallBinding(sourceText, absolutePath, primaryLabel.span.offset);
39789
39974
  return bindingResolution !== null && !bindingResolution.isReactUseBinding;
39790
39975
  };
39976
+ const SUPPRESSED_COMPILER_CODES = new Set(["react-hooks-js(immutability)", "react-hooks-js(refs)"]);
39977
+ const WORKLET_DIRECTIVE = "worklet";
39978
+ const WORKLET_ACCEPTING_CALLEES = new Set([
39979
+ "useAnimatedStyle",
39980
+ "useAnimatedProps",
39981
+ "useDerivedValue",
39982
+ "useAnimatedReaction",
39983
+ "useAnimatedScrollHandler",
39984
+ "useAnimatedGestureHandler",
39985
+ "useFrameCallback",
39986
+ "runOnUI",
39987
+ "executeOnUIRuntimeSync"
39988
+ ]);
39989
+ const GESTURE_ROOT_IDENTIFIER = "Gesture";
39990
+ const getScriptKind$1 = (filename) => {
39991
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
39992
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
39993
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
39994
+ return ts.ScriptKind.JS;
39995
+ };
39996
+ const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39997
+ const hasWorkletDirective = (node) => {
39998
+ if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
39999
+ const body = node.body;
40000
+ if (!body || !ts.isBlock(body)) return false;
40001
+ for (const statement of body.statements) {
40002
+ if (!ts.isExpressionStatement(statement)) return false;
40003
+ if (!ts.isStringLiteral(statement.expression)) return false;
40004
+ if (statement.expression.text === WORKLET_DIRECTIVE) return true;
40005
+ }
40006
+ return false;
40007
+ };
40008
+ const rootIdentifierOfCallChain = (expression) => {
40009
+ let currentExpression = expression;
40010
+ while (true) {
40011
+ if (ts.isIdentifier(currentExpression)) return currentExpression.text;
40012
+ if (ts.isPropertyAccessExpression(currentExpression)) {
40013
+ currentExpression = currentExpression.expression;
40014
+ continue;
40015
+ }
40016
+ if (ts.isCallExpression(currentExpression)) {
40017
+ currentExpression = currentExpression.expression;
40018
+ continue;
40019
+ }
40020
+ return null;
40021
+ }
40022
+ };
40023
+ const isWorkletAcceptingCall = (callExpression) => {
40024
+ const callee = callExpression.expression;
40025
+ if (ts.isIdentifier(callee)) return WORKLET_ACCEPTING_CALLEES.has(callee.text);
40026
+ if (ts.isPropertyAccessExpression(callee)) {
40027
+ if (WORKLET_ACCEPTING_CALLEES.has(callee.name.text)) return true;
40028
+ return rootIdentifierOfCallChain(callee) === GESTURE_ROOT_IDENTIFIER;
40029
+ }
40030
+ return false;
40031
+ };
40032
+ const isFunctionWorklet = (functionNode) => {
40033
+ if (hasWorkletDirective(functionNode)) return true;
40034
+ const parent = functionNode.parent;
40035
+ return ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === functionNode) && isWorkletAcceptingCall(parent);
40036
+ };
40037
+ const findNodeAtOffset = (sourceFile, targetOffset) => {
40038
+ let matchedNode = null;
40039
+ const visit = (node) => {
40040
+ if (node.getStart(sourceFile) > targetOffset || node.getEnd() <= targetOffset) return;
40041
+ matchedNode = node;
40042
+ ts.forEachChild(node, visit);
40043
+ };
40044
+ visit(sourceFile);
40045
+ return matchedNode;
40046
+ };
40047
+ const isOffsetInsideWorklet = (sourceFile, targetOffset) => {
40048
+ let currentNode = findNodeAtOffset(sourceFile, targetOffset);
40049
+ while (currentNode) {
40050
+ if (ts.isFunctionLike(currentNode) && isFunctionWorklet(currentNode)) return true;
40051
+ currentNode = currentNode.parent;
40052
+ }
40053
+ return false;
40054
+ };
40055
+ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirectory) => {
40056
+ if (!SUPPRESSED_COMPILER_CODES.has(diagnostic.code)) return false;
40057
+ if (!project.hasReanimated) return false;
40058
+ const primaryLabel = diagnostic.labels[0];
40059
+ if (!primaryLabel) return false;
40060
+ const absolutePath = Path.isAbsolute(diagnostic.filename) ? diagnostic.filename : Path.join(rootDirectory, diagnostic.filename);
40061
+ let sourceText;
40062
+ try {
40063
+ sourceText = NFS.readFileSync(absolutePath, "utf-8");
40064
+ } catch {
40065
+ return false;
40066
+ }
40067
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
40068
+ };
40069
+ const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
40070
+ const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
40071
+ const REACT_COMPILER_PLUGIN = "react-hooks-js";
40072
+ const getScriptKind = (filename) => {
40073
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
40074
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
40075
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
40076
+ return ts.ScriptKind.JS;
40077
+ };
40078
+ const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
40079
+ const nodeLineRange = (sourceFile, node) => ({
40080
+ startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
40081
+ endLine: sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1
40082
+ });
40083
+ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40084
+ let foundRange = null;
40085
+ const visit = (node) => {
40086
+ if (foundRange) return;
40087
+ if (ts.isFunctionLike(node)) {
40088
+ const range = nodeLineRange(sourceFile, node);
40089
+ if (range.startLine <= targetLine && targetLine <= range.endLine) foundRange = range;
40090
+ return;
40091
+ }
40092
+ ts.forEachChild(node, visit);
40093
+ };
40094
+ visit(sourceFile);
40095
+ return foundRange;
40096
+ };
40097
+ const NEWLINE_BYTE = 10;
40098
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40099
+ let lineNumber = 1;
40100
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40101
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40102
+ return lineNumber;
40103
+ };
40104
+ /**
40105
+ * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40106
+ * whose enclosing function also carries a `react-hooks-js` (React
40107
+ * Compiler) diagnostic. Every react-hooks-js finding marks a function the
40108
+ * compiler could NOT compile (a bail-out / skip / preserve-manual-
40109
+ * memoization failure), so "React Compiler already caches every value
40110
+ * here" is false for that function and the manual memoization is
40111
+ * load-bearing, not dead weight.
40112
+ *
40113
+ * Matching is per enclosing function, not per file: a bailed-out
40114
+ * component must not silence redundant-memo findings in a sibling
40115
+ * component the compiler did optimize.
40116
+ *
40117
+ * Module-scope `memo(() => …)` wrappers have no enclosing function; for
40118
+ * those the memo call's own span is the containment range, so a bail-out
40119
+ * inside the inline-wrapped component still suppresses. Every failure
40120
+ * path (unreadable file, missing span) fails open and keeps the
40121
+ * diagnostic.
40122
+ */
40123
+ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) => {
40124
+ const compilerDiagnosticLinesByFile = /* @__PURE__ */ new Map();
40125
+ let hasManualMemoizationDiagnostic = false;
40126
+ for (const diagnostic of diagnostics) {
40127
+ if (isManualMemoizationDiagnostic(diagnostic)) {
40128
+ hasManualMemoizationDiagnostic = true;
40129
+ continue;
40130
+ }
40131
+ if (diagnostic.plugin !== REACT_COMPILER_PLUGIN || diagnostic.line <= 0) continue;
40132
+ const lines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
40133
+ if (lines) lines.push(diagnostic.line);
40134
+ else compilerDiagnosticLinesByFile.set(diagnostic.filePath, [diagnostic.line]);
40135
+ }
40136
+ if (!hasManualMemoizationDiagnostic || compilerDiagnosticLinesByFile.size === 0) return diagnostics;
40137
+ const parsedFileCache = /* @__PURE__ */ new Map();
40138
+ const readAndParse = (filePath) => {
40139
+ const cached = parsedFileCache.get(filePath);
40140
+ if (cached !== void 0) return cached;
40141
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
40142
+ let parsed;
40143
+ try {
40144
+ const buffer = NFS.readFileSync(absolutePath);
40145
+ parsed = {
40146
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
40147
+ buffer
40148
+ };
40149
+ } catch {
40150
+ parsed = null;
40151
+ }
40152
+ parsedFileCache.set(filePath, parsed);
40153
+ return parsed;
40154
+ };
40155
+ const isInsideBailedOutFunction = (diagnostic) => {
40156
+ const compilerLines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
40157
+ if (!compilerLines || diagnostic.line <= 0) return false;
40158
+ const parsed = readAndParse(diagnostic.filePath);
40159
+ if (!parsed) return false;
40160
+ let containmentRange = findOutermostEnclosingFunctionRange(parsed.sourceFile, diagnostic.line);
40161
+ if (!containmentRange && diagnostic.offset !== void 0 && diagnostic.length !== void 0) containmentRange = {
40162
+ startLine: diagnostic.line,
40163
+ endLine: lineOfUtf8Offset(parsed.buffer, diagnostic.offset + diagnostic.length)
40164
+ };
40165
+ if (!containmentRange) return false;
40166
+ const { startLine, endLine } = containmentRange;
40167
+ return compilerLines.some((compilerLine) => startLine <= compilerLine && compilerLine <= endLine);
40168
+ };
40169
+ return diagnostics.filter((diagnostic) => !isManualMemoizationDiagnostic(diagnostic) || !isInsideBailedOutFunction(diagnostic));
40170
+ };
39791
40171
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
39792
40172
  const LEADING_SEVERITY_LABEL_PATTERN = /^(?:Error|Warning):\s*/;
39793
40173
  const TRAILING_PERIOD_PATTERN = /\.$/;
@@ -39927,7 +40307,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
39927
40307
  minifiedFileCache.set(absolutePath, minified);
39928
40308
  return minified;
39929
40309
  };
39930
- return parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
40310
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
39931
40311
  const { plugin, rule } = parseRuleCode(diagnostic.code);
39932
40312
  const primaryLabel = diagnostic.labels[0];
39933
40313
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -39954,7 +40334,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
39954
40334
  ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
39955
40335
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
39956
40336
  };
39957
- });
40337
+ }), rootDirectory);
39958
40338
  };
39959
40339
  const buildOxlintChildEnv = (sourceEnv) => {
39960
40340
  const childEnv = {};
@@ -40465,6 +40845,7 @@ const runOxlint = async (options) => {
40465
40845
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
40466
40846
  const severityControls = buildRuleSeverityControls(userConfig);
40467
40847
  validateRuleRegistration();
40848
+ resetManifestCaches();
40468
40849
  if (includePaths !== void 0 && includePaths.length === 0) return [];
40469
40850
  const pluginPath = resolvePluginPath();
40470
40851
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
@@ -41127,9 +41508,10 @@ const CONTEXT_AXES = [
41127
41508
  }
41128
41509
  ];
41129
41510
  const SCORE_AXES = [...GATED_AXES, ...CONTEXT_AXES];
41130
- const worstGatedAxis = (score) => {
41131
- let worst = GATED_AXES[0];
41132
- for (const axis of GATED_AXES) if (score[axis.key] < score[worst.key]) worst = axis;
41511
+ const gatedAxesForSection = (section) => section === "devDependencies" ? GATED_AXES.filter((axis) => axis.key !== "vulnerability") : GATED_AXES;
41512
+ const worstGatedAxis = (score, gatedAxes) => {
41513
+ let worst = gatedAxes[0];
41514
+ for (const axis of gatedAxes) if (score[axis.key] < score[worst.key]) worst = axis;
41133
41515
  return worst;
41134
41516
  };
41135
41517
  const clampScore = (value) => {
@@ -41153,6 +41535,69 @@ const resolveConcreteVersion = (spec) => {
41153
41535
  if (range === null || range === "*") return null;
41154
41536
  return import_semver.minVersion(trimmed)?.version ?? null;
41155
41537
  };
41538
+ const readTextFileOrNull = (filePath) => {
41539
+ try {
41540
+ return NFS.readFileSync(filePath, "utf-8");
41541
+ } catch {
41542
+ return null;
41543
+ }
41544
+ };
41545
+ const buildNpmLockLookup = (lockText) => {
41546
+ let parsed;
41547
+ try {
41548
+ parsed = JSON.parse(lockText);
41549
+ } catch {
41550
+ return null;
41551
+ }
41552
+ if (typeof parsed !== "object" || parsed === null) return null;
41553
+ const lock = parsed;
41554
+ return (name) => {
41555
+ const entry = lock.packages?.[`node_modules/${name}`] ?? lock.dependencies?.[name];
41556
+ return typeof entry?.version === "string" ? entry.version : null;
41557
+ };
41558
+ };
41559
+ const buildYarnLockLookup = (lockText) => {
41560
+ const lines = lockText.split(/\r?\n/);
41561
+ return (name, spec) => {
41562
+ const descriptor = `${name}@${spec}`;
41563
+ const berryDescriptor = `${name}@npm:${spec}`;
41564
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
41565
+ const line = lines[lineIndex];
41566
+ if (line.startsWith(" ") || line.startsWith("#") || !line.includes(":")) continue;
41567
+ const descriptors = line.replace(/:\s*$/, "").split(",").map((part) => part.trim().replace(/^"|"$/g, ""));
41568
+ if (!descriptors.includes(descriptor) && !descriptors.includes(berryDescriptor)) continue;
41569
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && (lines[bodyIndex].startsWith(" ") || lines[bodyIndex] === ""); bodyIndex += 1) {
41570
+ const versionMatch = /^\s+version:?\s+"?([^"\s]+)"?/.exec(lines[bodyIndex]);
41571
+ if (versionMatch) return versionMatch[1];
41572
+ }
41573
+ return null;
41574
+ }
41575
+ return null;
41576
+ };
41577
+ };
41578
+ const buildPnpmLockLookup = (lockText) => {
41579
+ return (name) => {
41580
+ const entryPattern = new RegExp(`^ {4,8}['"]?${name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}['"]?:\\s*$`);
41581
+ const lines = lockText.split(/\r?\n/);
41582
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
41583
+ if (!entryPattern.test(lines[lineIndex])) continue;
41584
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && /^ {5,}\S/.test(lines[bodyIndex]); bodyIndex += 1) {
41585
+ const versionMatch = /^\s+version:\s+['"]?([^'"\s(]+)/.exec(lines[bodyIndex]);
41586
+ if (versionMatch) return versionMatch[1];
41587
+ }
41588
+ }
41589
+ return null;
41590
+ };
41591
+ };
41592
+ const buildLockedVersionLookup = (rootDirectory) => {
41593
+ const npmLockText = readTextFileOrNull(Path.join(rootDirectory, "package-lock.json"));
41594
+ if (npmLockText !== null) return buildNpmLockLookup(npmLockText);
41595
+ const yarnLockText = readTextFileOrNull(Path.join(rootDirectory, "yarn.lock"));
41596
+ if (yarnLockText !== null) return buildYarnLockLookup(yarnLockText);
41597
+ const pnpmLockText = readTextFileOrNull(Path.join(rootDirectory, "pnpm-lock.yaml"));
41598
+ if (pnpmLockText !== null) return buildPnpmLockLookup(pnpmLockText);
41599
+ return null;
41600
+ };
41156
41601
  const locateDependencyKey = (packageJsonText, section, name) => {
41157
41602
  const needle = `"${name}"`;
41158
41603
  const sectionHeader = new RegExp(`"${section}"\\s*:\\s*\\{`);
@@ -41179,7 +41624,7 @@ const locateDependencyKey = (packageJsonText, section, name) => {
41179
41624
  }
41180
41625
  return null;
41181
41626
  };
41182
- const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies) => {
41627
+ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies, lockedVersionLookup) => {
41183
41628
  const sectionByName = /* @__PURE__ */ new Map();
41184
41629
  for (const name of Object.keys(packageJson.dependencies ?? {})) sectionByName.set(name, "dependencies");
41185
41630
  if (includeDevDependencies) {
@@ -41189,13 +41634,18 @@ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDepe
41189
41634
  for (const [name, section] of sectionByName) {
41190
41635
  if (SUPPLY_CHAIN_IGNORED_PACKAGES.has(name)) continue;
41191
41636
  const spec = (packageJson[section] ?? {})[name] ?? "";
41192
- const version = resolveConcreteVersion(spec);
41193
- if (version === null) continue;
41637
+ const floorVersion = resolveConcreteVersion(spec);
41638
+ if (floorVersion === null) continue;
41639
+ const lockedVersion = lockedVersionLookup?.(name, spec) ?? null;
41640
+ const lockSatisfiesSpec = lockedVersion !== null && import_semver.valid(lockedVersion) !== null && import_semver.satisfies(lockedVersion, spec);
41641
+ const isExactPin = import_semver.valid(spec.trim()) !== null;
41194
41642
  const location = locateDependencyKey(packageJsonText, section, name);
41195
41643
  dependencies.push({
41196
41644
  name,
41197
- version,
41645
+ version: lockSatisfiesSpec ? lockedVersion : floorVersion,
41198
41646
  spec,
41647
+ section,
41648
+ resolution: isExactPin ? "pin" : lockSatisfiesSpec ? "locked" : "floor",
41199
41649
  line: location?.line ?? 0,
41200
41650
  column: location?.column ?? 0
41201
41651
  });
@@ -41346,7 +41796,11 @@ const formatAlertReason = (topAlerts, totalCount) => {
41346
41796
  }
41347
41797
  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)}.`;
41348
41798
  };
41349
- const formatDependencyIdentity = (dependency) => import_semver.valid(dependency.spec) !== null ? `${dependency.name}@${dependency.version}` : `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
41799
+ const formatDependencyIdentity = (dependency) => {
41800
+ if (dependency.resolution === "pin") return `${dependency.name}@${dependency.version}`;
41801
+ if (dependency.resolution === "locked") return `${dependency.name}@${dependency.version} (lockfile resolution of "${dependency.spec}")`;
41802
+ return `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
41803
+ };
41350
41804
  const buildSupplyChainHelp = (dependency, failingAxis, topAlerts, packagePageUrl, options) => {
41351
41805
  const hasCriticalAlert = topAlerts.some((alert) => alert.severity.toLowerCase() === "critical");
41352
41806
  const entry = `\`"${dependency.name}": "${dependency.spec}"\``;
@@ -41389,7 +41843,7 @@ const buildLowScoreDiagnostic = (dependency, artifact, failingAxis, options) =>
41389
41843
  const checkSupplyChain = (input) => gen(function* () {
41390
41844
  const options = resolveOptions(input.userConfig);
41391
41845
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
41392
- const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
41846
+ const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies, buildLockedVersionLookup(input.rootDirectory));
41393
41847
  if (dependencies.length === 0) return [];
41394
41848
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
41395
41849
  if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
@@ -41398,7 +41852,7 @@ const checkSupplyChain = (input) => gen(function* () {
41398
41852
  for (let index = 0; index < dependencies.length; index += 1) {
41399
41853
  const artifact = artifacts[index];
41400
41854
  if (!artifact) continue;
41401
- const worstAxis = worstGatedAxis(artifact.score);
41855
+ const worstAxis = worstGatedAxis(artifact.score, gatedAxesForSection(dependencies[index].section));
41402
41856
  if (toHundred(artifact.score[worstAxis.key]) >= options.minScore) continue;
41403
41857
  diagnostics.push(buildLowScoreDiagnostic(dependencies[index], artifact, worstAxis, options));
41404
41858
  }
@@ -42441,4 +42895,4 @@ const toJsonReport = (result, options) => buildJsonReport({
42441
42895
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
42442
42896
 
42443
42897
  //# sourceMappingURL=index.js.map
42444
- //# debugId=7cb857ba-6f0a-57d9-91f3-3c286472121d
42898
+ //# debugId=10e3e1b0-f291-5306-b5fd-79b158b00159