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/lsp.js CHANGED
@@ -8,7 +8,7 @@ import path from "node:path";
8
8
  import * as NodeChildProcess from "node:child_process";
9
9
  import { execFileSync, spawn, spawnSync } from "node:child_process";
10
10
  import * as ts from "typescript";
11
- 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";
11
+ 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";
12
12
  import { parseJSON5 } from "confbox";
13
13
  import * as NodeUrl from "node:url";
14
14
  import { fileURLToPath } from "node:url";
@@ -32836,8 +32836,10 @@ const isFile = (filePath) => {
32836
32836
  };
32837
32837
  const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?|mts|mjs)$/;
32838
32838
  const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
32839
+ const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
32839
32840
  const MINIFIED_SNIFF_BYTES = 65536;
32840
32841
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
32842
+ const SCANNED_DOT_DIRECTORIES = new Set([".dumi", ".storybook"]);
32841
32843
  const IGNORED_DIRECTORIES = new Set([
32842
32844
  ".angular",
32843
32845
  ".astro",
@@ -33002,7 +33004,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33002
33004
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33003
33005
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33004
33006
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
33005
- const CORE_PACKAGE_VERSION = "0.7.1";
33007
+ const CORE_PACKAGE_VERSION = "0.7.2";
33006
33008
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
33007
33009
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
33008
33010
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -33012,8 +33014,9 @@ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
33012
33014
  const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
33013
33015
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
33014
33016
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
33015
- const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
33016
- const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath);
33017
+ const isIgnoredDirectoryName = (directoryName) => IGNORED_DIRECTORIES.has(directoryName) || directoryName.startsWith(".") && !SCANNED_DOT_DIRECTORIES.has(directoryName);
33018
+ const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => isIgnoredDirectoryName(segment));
33019
+ const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath) && !GENERATED_SOURCE_DIRECTORY_PATTERN.test(filePath);
33017
33020
  const isMinifiedSource = (absolutePath) => {
33018
33021
  let fileDescriptor;
33019
33022
  try {
@@ -33077,7 +33080,7 @@ function* walkSourceTreeFiles(rootDirectory) {
33077
33080
  for (const entry of readDirectoryEntries(currentDirectory)) {
33078
33081
  const absolutePath = Path.join(currentDirectory, entry.name);
33079
33082
  if (entry.isDirectory()) {
33080
- if (!IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
33083
+ if (!isIgnoredDirectoryName(entry.name)) stack.push(absolutePath);
33081
33084
  continue;
33082
33085
  }
33083
33086
  if (entry.isFile()) yield {
@@ -33123,6 +33126,23 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33123
33126
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33124
33127
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33125
33128
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33129
+ /**
33130
+ * The chain of ancestor directories of `startDirectory`, nearest first, up to
33131
+ * and including the filesystem root. With `includeStart`, the chain begins at
33132
+ * `startDirectory` itself; otherwise at its parent. The one place the "walk up
33133
+ * the tree" traversal is expressed, so each caller adds only its own match /
33134
+ * stop policy instead of re-deriving the `path.dirname` loop.
33135
+ */
33136
+ const ancestorDirectories = (startDirectory, { includeStart }) => {
33137
+ const directories = [];
33138
+ let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
33139
+ while (true) {
33140
+ directories.push(currentDirectory);
33141
+ const parentDirectory = Path.dirname(currentDirectory);
33142
+ if (parentDirectory === currentDirectory) return directories;
33143
+ currentDirectory = parentDirectory;
33144
+ }
33145
+ };
33126
33146
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33127
33147
  const clearPackageJsonCache = () => {
33128
33148
  cachedPackageJsons.clear();
@@ -33156,11 +33176,7 @@ const isMonorepoRoot = (directory) => {
33156
33176
  return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
33157
33177
  };
33158
33178
  const findMonorepoRoot = (startDirectory) => {
33159
- let currentDirectory = Path.dirname(startDirectory);
33160
- while (currentDirectory !== Path.dirname(currentDirectory)) {
33161
- if (isMonorepoRoot(currentDirectory)) return currentDirectory;
33162
- currentDirectory = Path.dirname(currentDirectory);
33163
- }
33179
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
33164
33180
  return null;
33165
33181
  };
33166
33182
  /**
@@ -33806,6 +33822,74 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
33806
33822
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
33807
33823
  };
33808
33824
  };
33825
+ /**
33826
+ * Walk up from `startDirectory` to the nearest ancestor that owns a
33827
+ * `package.json`, stopping at (and including) the enclosing project boundary
33828
+ * — the working tree's git root or a monorepo root. Lets a scan of a package
33829
+ * subfolder that has no `package.json` of its own adopt the nearest enclosing
33830
+ * package as its project root, so it inherits that package's dependency +
33831
+ * framework detection instead of synthesizing an empty (React-blind) project.
33832
+ *
33833
+ * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
33834
+ * most specific owning package (a leaf workspace, not just the repo root), so
33835
+ * a subfolder of one workspace no longer borrows a sibling's capabilities.
33836
+ *
33837
+ * Returns `null` when no ancestor `package.json` exists inside the project
33838
+ * boundary (a loose tree of files, or a subfolder of a package-less repo).
33839
+ */
33840
+ const findNearestAncestorPackageJson = (startDirectory) => {
33841
+ if (isProjectBoundary(startDirectory)) return null;
33842
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
33843
+ if (isFile(Path.join(directory, "package.json"))) return directory;
33844
+ if (isProjectBoundary(directory)) return null;
33845
+ }
33846
+ return null;
33847
+ };
33848
+ /**
33849
+ * Last-resort React detection: locate React the way Node's `node_modules`
33850
+ * resolution would from `directory` — check each `node_modules/react` up the
33851
+ * tree — but bounded to the enclosing repo so a globally installed React can't
33852
+ * masquerade as the project's version. Stops at (and including) the nearest
33853
+ * project boundary — the git root or a monorepo root — and returns the first
33854
+ * installed `version`.
33855
+ *
33856
+ * Makes "React is installed and importable" ⇒ "React is detected" an invariant
33857
+ * for packages whose only React declaration is a version-less spec
33858
+ * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
33859
+ * `node_modules` the declaration walks never reach — the profile of a component
33860
+ * package inside a monorepo. Matching the `node_modules/react` entry rather than
33861
+ * its realpath means a project whose `node_modules` is symlinked elsewhere (a
33862
+ * Docker volume, a shared store) still counts, while a global install outside
33863
+ * the repo does not.
33864
+ *
33865
+ * Returns `null` when React isn't installed within the repo or its package.json
33866
+ * carries no version string. A tree with no boundary marker at all bounds the
33867
+ * search to `directory`, so a dependency hoisted above the scanned package there
33868
+ * is conservatively not adopted.
33869
+ */
33870
+ /**
33871
+ * The nearest enclosing project boundary (git root or monorepo root), or
33872
+ * `directory` itself when the scan target sits outside any repo. Bounds the
33873
+ * node_modules walk so a React hoisted anywhere inside the repo counts, but a
33874
+ * React in an ancestor above the repo — a home-directory or global
33875
+ * `node_modules` — can't leak in.
33876
+ */
33877
+ const findContainmentRoot = (directory) => {
33878
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
33879
+ return directory;
33880
+ };
33881
+ const resolveInstalledReactVersion = (directory) => {
33882
+ const containmentRoot = findContainmentRoot(directory);
33883
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
33884
+ const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
33885
+ if (isFile(reactPackageJsonPath)) {
33886
+ const installedVersion = readPackageJson(reactPackageJsonPath).version;
33887
+ return typeof installedVersion === "string" ? installedVersion : null;
33888
+ }
33889
+ if (ancestorDirectory === containmentRoot) return null;
33890
+ }
33891
+ return null;
33892
+ };
33809
33893
  const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
33810
33894
  const rootValue = select(rootPackageJson);
33811
33895
  if (rootValue !== null) return rootValue;
@@ -33962,6 +34046,10 @@ const normalizeCompilerOptions = (compilerOptions) => {
33962
34046
  hasExplicitLib
33963
34047
  };
33964
34048
  };
34049
+ const normalizeReferencePaths = (references) => {
34050
+ if (!Array.isArray(references)) return [];
34051
+ return references.map((reference) => isPlainObject(reference) && typeof reference.path === "string" ? reference.path : null).filter((referencePath) => referencePath !== null);
34052
+ };
33965
34053
  const readTsConfig = (filePath) => {
33966
34054
  let content;
33967
34055
  try {
@@ -33973,6 +34061,7 @@ const readTsConfig = (filePath) => {
33973
34061
  if (!isPlainObject(parsed.config)) return null;
33974
34062
  return {
33975
34063
  extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
34064
+ referencePaths: normalizeReferencePaths(parsed.config.references),
33976
34065
  compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
33977
34066
  };
33978
34067
  };
@@ -34013,20 +34102,37 @@ const libEntryIncludesES2023Array = (entry) => {
34013
34102
  };
34014
34103
  const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
34015
34104
  const compilerOptionsArePreES2023 = (compilerOptions) => {
34105
+ if (compilerOptions.target && targetYearIsPreES2023(compilerOptions.target)) return true;
34016
34106
  if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
34017
- if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
34018
34107
  return false;
34019
34108
  };
34020
34109
  const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
34021
- const detectPreES2023FromConfig = (tsConfigPath) => {
34110
+ const detectPreES2023FromConfig = (tsConfigPath, visitedConfigPaths = /* @__PURE__ */ new Set()) => {
34111
+ if (visitedConfigPaths.has(tsConfigPath)) return false;
34022
34112
  const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
34023
34113
  if (!compilerOptions) return false;
34024
- if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) return false;
34114
+ if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) {
34115
+ const tsConfig = readTsConfig(tsConfigPath);
34116
+ if (!tsConfig) return false;
34117
+ const nextVisitedConfigPaths = new Set(visitedConfigPaths);
34118
+ nextVisitedConfigPaths.add(tsConfigPath);
34119
+ const configDirectory = Path.dirname(tsConfigPath);
34120
+ return tsConfig.referencePaths.some((referencePath) => {
34121
+ const resolvedReferencePath = Path.resolve(configDirectory, referencePath);
34122
+ const referencedConfigPath = isFile(resolvedReferencePath) ? resolvedReferencePath : Path.join(resolvedReferencePath, TSCONFIG_FILENAME);
34123
+ return isFile(referencedConfigPath) && detectPreES2023FromConfig(referencedConfigPath, nextVisitedConfigPaths);
34124
+ });
34125
+ }
34025
34126
  return compilerOptionsArePreES2023(compilerOptions);
34026
34127
  };
34128
+ const FALLBACK_TSCONFIG_FILENAMES = ["tsconfig.app.json", "tsconfig.build.json"];
34027
34129
  const detectPreES2023Target = (directory) => {
34028
34130
  const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
34029
34131
  if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
34132
+ for (const fallbackFilename of FALLBACK_TSCONFIG_FILENAMES) {
34133
+ const fallbackPath = Path.join(directory, fallbackFilename);
34134
+ if (isFile(fallbackPath)) return detectPreES2023FromConfig(fallbackPath);
34135
+ }
34030
34136
  return false;
34031
34137
  };
34032
34138
  const REACT_DEPENDENCY_NAMES = new Set([
@@ -34145,19 +34251,20 @@ const clearProjectCache = () => {
34145
34251
  };
34146
34252
  /**
34147
34253
  * Build a `ProjectInfo` for a directory that has no `package.json` of
34148
- * its own — a monorepo subfolder like `repo/packages`, or any loose tree
34149
- * of TypeScript/JavaScript files. Dependency + framework detection is
34150
- * inherited from the enclosing workspace root when there is one, so
34151
- * scanning a subdirectory of a React monorepo still gets the React
34152
- * capabilities; a standalone non-React directory simply scans with the
34153
- * framework-agnostic rules. Throws only when the directory has nothing
34154
- * to scan (no enclosing project and no source files of its own).
34254
+ * its own — a package subfolder like `repo/packages` or `app/src/features`,
34255
+ * or any loose tree of TypeScript/JavaScript files. Dependency + framework
34256
+ * detection is inherited from the nearest enclosing package (a leaf
34257
+ * workspace, a plain app root, or a monorepo root — whichever is closest,
34258
+ * bounded by the git root), so scanning a subdirectory of a React project
34259
+ * still gets the React capabilities; a standalone non-React directory simply
34260
+ * scans with the framework-agnostic rules. Throws only when the directory has
34261
+ * nothing to scan (no enclosing project and no source files of its own).
34155
34262
  */
34156
34263
  const discoverProjectWithoutPackageJson = (directory) => {
34157
34264
  const sourceFileCount = countSourceFiles(directory);
34158
34265
  const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
34159
- const monorepoRoot = findMonorepoRoot(directory);
34160
- const enclosingProject = monorepoRoot !== null && isFile(Path.join(monorepoRoot, "package.json")) ? discoverProject(monorepoRoot) : null;
34266
+ const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
34267
+ const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
34161
34268
  if (enclosingProject !== null) return {
34162
34269
  ...enclosingProject,
34163
34270
  rootDirectory: directory,
@@ -34187,6 +34294,7 @@ const discoverProjectWithoutPackageJson = (directory) => {
34187
34294
  shopifyFlashListVersion: null,
34188
34295
  shopifyFlashListMajorVersion: null,
34189
34296
  hasReanimated: false,
34297
+ reanimatedVersion: null,
34190
34298
  isPreES2023Target: hasOwnTsConfig && detectPreES2023Target(directory),
34191
34299
  sourceFileCount
34192
34300
  };
@@ -34261,6 +34369,7 @@ const discoverProject = (directory) => {
34261
34369
  if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
34262
34370
  if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
34263
34371
  if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
34372
+ if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
34264
34373
  const projectName = packageJson.name ?? Path.basename(directory);
34265
34374
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
34266
34375
  const sourceFileCount = countSourceFiles(directory);
@@ -34278,6 +34387,7 @@ const discoverProject = (directory) => {
34278
34387
  version: findShopifyFlashListVersion(directory, packageJson)
34279
34388
  }) : null;
34280
34389
  const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
34390
+ const reanimatedVersion = hasReanimated ? findInWorkspacePackageJsons(directory, packageJson, (workspacePackageJson) => getDependencySpec(workspacePackageJson, REANIMATED_DEPENDENCY_NAME)) : null;
34281
34391
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
34282
34392
  rootDirectory: directory,
34283
34393
  rootPackageJson: packageJson,
@@ -34307,6 +34417,7 @@ const discoverProject = (directory) => {
34307
34417
  shopifyFlashListVersion,
34308
34418
  shopifyFlashListMajorVersion: shopifyFlashListVersion === null ? null : getLowestDependencyMajor(shopifyFlashListVersion),
34309
34419
  hasReanimated,
34420
+ reanimatedVersion,
34310
34421
  isPreES2023Target,
34311
34422
  sourceFileCount
34312
34423
  };
@@ -34499,6 +34610,9 @@ const buildRuleSeverityControls = (config) => {
34499
34610
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
34500
34611
  };
34501
34612
  };
34613
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34614
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34615
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34502
34616
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34503
34617
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
34504
34618
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -34576,6 +34690,7 @@ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34576
34690
  "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
34577
34691
  "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
34578
34692
  "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
34693
+ "react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
34579
34694
  "react/jsx-no-undef": "react-doctor/jsx-no-undef",
34580
34695
  "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
34581
34696
  "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
@@ -34649,9 +34764,6 @@ const tokenizeRuleList = (ruleList) => {
34649
34764
  if (!ruleSection) return [];
34650
34765
  return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
34651
34766
  };
34652
- const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34653
- const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34654
- const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34655
34767
  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}\`).`;
34656
34768
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34657
34769
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
@@ -34705,6 +34817,53 @@ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34705
34817
  if (!ruleId.startsWith("react-doctor/")) return null;
34706
34818
  return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
34707
34819
  };
34820
+ const ESLINT_REACT_HOOKS_PLUGIN_PREFIX = "react-hooks/";
34821
+ const buildAcceptedTokens = (ruleId) => {
34822
+ const shortRuleId = ruleId.slice(15);
34823
+ return new Set([ruleId, `${ESLINT_REACT_HOOKS_PLUGIN_PREFIX}${shortRuleId}`]);
34824
+ };
34825
+ const listNamesRule = (ruleList, acceptedTokens) => tokenizeRuleList(ruleList).some((token) => acceptedTokens.has(token));
34826
+ const isInlineDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
34827
+ const candidates = [{
34828
+ line: lines[diagnosticLineIndex],
34829
+ requiredScope: "line"
34830
+ }, {
34831
+ line: lines[diagnosticLineIndex - 1],
34832
+ requiredScope: "next-line"
34833
+ }];
34834
+ for (const { line, requiredScope } of candidates) {
34835
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
34836
+ if (!match) continue;
34837
+ const [, , scope, ruleList] = match;
34838
+ if (scope !== requiredScope) continue;
34839
+ if (listNamesRule(ruleList, acceptedTokens)) return true;
34840
+ }
34841
+ return false;
34842
+ };
34843
+ const isBlockDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
34844
+ let isRangeOpen = false;
34845
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
34846
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
34847
+ const line = lines[lineIndex];
34848
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
34849
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
34850
+ if (disableMatch && listNamesRule(disableMatch[2], acceptedTokens)) {
34851
+ isRangeOpen = true;
34852
+ continue;
34853
+ }
34854
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
34855
+ if (enableMatch) {
34856
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
34857
+ if (enabledRules.length === 0 || enabledRules.some((rule) => acceptedTokens.has(rule))) isRangeOpen = false;
34858
+ }
34859
+ }
34860
+ return isRangeOpen;
34861
+ };
34862
+ const isCompilerRuleForeignDisabled = (lines, diagnosticLineIndex, ruleId) => {
34863
+ if (!ruleId.startsWith("react-hooks-js/")) return false;
34864
+ const acceptedTokens = buildAcceptedTokens(ruleId);
34865
+ return isInlineDisabled(lines, diagnosticLineIndex, acceptedTokens) || isBlockDisabled(lines, diagnosticLineIndex, acceptedTokens);
34866
+ };
34708
34867
  const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34709
34868
  const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34710
34869
  const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
@@ -34848,6 +35007,11 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
34848
35007
  isSuppressed: true,
34849
35008
  nearMissHint: null
34850
35009
  };
35010
+ if (isCompilerRuleForeignDisabled(lines, diagnosticLineIndex, ruleId)) return {
35011
+ isSuppressed: true,
35012
+ nearMissHint: null,
35013
+ isForeignDirective: true
35014
+ };
34851
35015
  const directComments = findStackedDisableCommentsAbove(lines, diagnosticLineIndex);
34852
35016
  if (hasChainSuppressor(directComments, ruleId)) return {
34853
35017
  isSuppressed: true,
@@ -35129,12 +35293,18 @@ const collectStringSet = (values) => {
35129
35293
  return new Set(values.filter((value) => typeof value === "string"));
35130
35294
  };
35131
35295
  const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
35296
+ const NODE_CLI_PATH_SEGMENTS = new Set([
35297
+ "scripts",
35298
+ "bin",
35299
+ "tools"
35300
+ ]);
35301
+ const hasNodeCliPathSegment = (filePath) => filePath.split(/[\\/]/).slice(0, -1).some((segment) => NODE_CLI_PATH_SEGMENTS.has(segment));
35132
35302
  /**
35133
35303
  * Pre-compiles every stateful filter and returns a single
35134
35304
  * `apply(diagnostic)` closure that runs (in order):
35135
35305
  *
35136
35306
  * 1. auto-suppress (test-noise rules in test files; `migration-hint`
35137
- * wins over `test-noise`)
35307
+ * wins over `test-noise`; `no-console` on Node CLI scripts)
35138
35308
  * 2. severity overrides (top-level `rules` / `categories`, with
35139
35309
  * `"off"` dropping)
35140
35310
  * 3. warning suppression (only when `showWarnings` is false: drops every
@@ -35232,6 +35402,12 @@ const buildDiagnosticPipeline = (input) => {
35232
35402
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35233
35403
  return false;
35234
35404
  };
35405
+ const isNoConsoleOnNodeCliScript = (diagnostic) => {
35406
+ if (diagnostic.rule !== "no-console") return false;
35407
+ if (hasNodeCliPathSegment(diagnostic.filePath)) return true;
35408
+ const lines = getFileLines(diagnostic.filePath);
35409
+ return lines !== null && Boolean(lines[0]?.startsWith("#!"));
35410
+ };
35235
35411
  const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35236
35412
  if (!hasRuntimeGlobals) return false;
35237
35413
  if (diagnostic.rule !== "jsx-no-undef") return false;
@@ -35241,6 +35417,7 @@ const buildDiagnosticPipeline = (input) => {
35241
35417
  return {
35242
35418
  apply: (diagnostic) => {
35243
35419
  if (shouldAutoSuppress(diagnostic)) return null;
35420
+ if (isNoConsoleOnNodeCliScript(diagnostic)) return null;
35244
35421
  let current = diagnostic;
35245
35422
  let explicitSeverityOverride;
35246
35423
  let explicitRuleOverride;
@@ -35270,7 +35447,7 @@ const buildDiagnosticPipeline = (input) => {
35270
35447
  if (lines) {
35271
35448
  const ruleIdentifier = `${current.plugin}/${current.rule}`;
35272
35449
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35273
- if (evaluation.isSuppressed) return suppress(current, "inline");
35450
+ if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
35274
35451
  if (evaluation.nearMissHint) current = {
35275
35452
  ...current,
35276
35453
  suppressionHint: evaluation.nearMissHint
@@ -38549,7 +38726,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38549
38726
  })),
38550
38727
  showStagedContent: (directory, relativePath, options) => runCommand({
38551
38728
  command: "git",
38552
- args: ["show", `:${relativePath}`],
38729
+ args: ["show", `:./${relativePath}`],
38553
38730
  directory,
38554
38731
  maxStdoutBytes: options?.maxBufferBytes
38555
38732
  }).pipe(map$3((result) => result.status === 0 ? result.stdout : null)),
@@ -39332,9 +39509,17 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
39332
39509
  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`;
39333
39510
  };
39334
39511
  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";
39512
+ const ACCESSORS_MIN_VERSION = "3.15.0";
39513
+ const supportsAccessors = (reanimatedVersion) => {
39514
+ if (reanimatedVersion === null) return true;
39515
+ const lowerBound = import_semver.validRange(reanimatedVersion) !== null ? import_semver.minVersion(reanimatedVersion) : import_semver.coerce(reanimatedVersion);
39516
+ if (lowerBound === null) return true;
39517
+ return import_semver.gte(lowerBound, ACCESSORS_MIN_VERSION);
39518
+ };
39335
39519
  const appendReanimatedSharedValueHint = (help, rule, project) => {
39336
39520
  if (rule !== "immutability") return help;
39337
39521
  if (!project.hasReanimated) return help;
39522
+ if (!supportsAccessors(project.reanimatedVersion)) return help;
39338
39523
  if (!help) return REANIMATED_SHARED_VALUE_HINT;
39339
39524
  return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
39340
39525
  };
@@ -39496,13 +39681,13 @@ const REACT_USE_BINDING_RESOLUTION = {
39496
39681
  isReactUseBinding: true,
39497
39682
  isReactNamespaceBinding: false
39498
39683
  };
39499
- const getScriptKind = (filename) => {
39684
+ const getScriptKind$2 = (filename) => {
39500
39685
  if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
39501
39686
  if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
39502
39687
  if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
39503
39688
  return ts.ScriptKind.JS;
39504
39689
  };
39505
- const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39690
+ const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39506
39691
  const unwrapExpression = (expression) => {
39507
39692
  let currentExpression = expression;
39508
39693
  while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
@@ -39751,8 +39936,8 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
39751
39936
  return matchedIdentifier;
39752
39937
  };
39753
39938
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
39754
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filename));
39755
- const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset(sourceText, utf8Offset));
39939
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
39940
+ const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
39756
39941
  if (!useIdentifier) return null;
39757
39942
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
39758
39943
  };
@@ -39773,6 +39958,201 @@ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
39773
39958
  const bindingResolution = resolveUseCallBinding(sourceText, absolutePath, primaryLabel.span.offset);
39774
39959
  return bindingResolution !== null && !bindingResolution.isReactUseBinding;
39775
39960
  };
39961
+ const SUPPRESSED_COMPILER_CODES = new Set(["react-hooks-js(immutability)", "react-hooks-js(refs)"]);
39962
+ const WORKLET_DIRECTIVE = "worklet";
39963
+ const WORKLET_ACCEPTING_CALLEES = new Set([
39964
+ "useAnimatedStyle",
39965
+ "useAnimatedProps",
39966
+ "useDerivedValue",
39967
+ "useAnimatedReaction",
39968
+ "useAnimatedScrollHandler",
39969
+ "useAnimatedGestureHandler",
39970
+ "useFrameCallback",
39971
+ "runOnUI",
39972
+ "executeOnUIRuntimeSync"
39973
+ ]);
39974
+ const GESTURE_ROOT_IDENTIFIER = "Gesture";
39975
+ const getScriptKind$1 = (filename) => {
39976
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
39977
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
39978
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
39979
+ return ts.ScriptKind.JS;
39980
+ };
39981
+ const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39982
+ const hasWorkletDirective = (node) => {
39983
+ if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
39984
+ const body = node.body;
39985
+ if (!body || !ts.isBlock(body)) return false;
39986
+ for (const statement of body.statements) {
39987
+ if (!ts.isExpressionStatement(statement)) return false;
39988
+ if (!ts.isStringLiteral(statement.expression)) return false;
39989
+ if (statement.expression.text === WORKLET_DIRECTIVE) return true;
39990
+ }
39991
+ return false;
39992
+ };
39993
+ const rootIdentifierOfCallChain = (expression) => {
39994
+ let currentExpression = expression;
39995
+ while (true) {
39996
+ if (ts.isIdentifier(currentExpression)) return currentExpression.text;
39997
+ if (ts.isPropertyAccessExpression(currentExpression)) {
39998
+ currentExpression = currentExpression.expression;
39999
+ continue;
40000
+ }
40001
+ if (ts.isCallExpression(currentExpression)) {
40002
+ currentExpression = currentExpression.expression;
40003
+ continue;
40004
+ }
40005
+ return null;
40006
+ }
40007
+ };
40008
+ const isWorkletAcceptingCall = (callExpression) => {
40009
+ const callee = callExpression.expression;
40010
+ if (ts.isIdentifier(callee)) return WORKLET_ACCEPTING_CALLEES.has(callee.text);
40011
+ if (ts.isPropertyAccessExpression(callee)) {
40012
+ if (WORKLET_ACCEPTING_CALLEES.has(callee.name.text)) return true;
40013
+ return rootIdentifierOfCallChain(callee) === GESTURE_ROOT_IDENTIFIER;
40014
+ }
40015
+ return false;
40016
+ };
40017
+ const isFunctionWorklet = (functionNode) => {
40018
+ if (hasWorkletDirective(functionNode)) return true;
40019
+ const parent = functionNode.parent;
40020
+ return ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === functionNode) && isWorkletAcceptingCall(parent);
40021
+ };
40022
+ const findNodeAtOffset = (sourceFile, targetOffset) => {
40023
+ let matchedNode = null;
40024
+ const visit = (node) => {
40025
+ if (node.getStart(sourceFile) > targetOffset || node.getEnd() <= targetOffset) return;
40026
+ matchedNode = node;
40027
+ ts.forEachChild(node, visit);
40028
+ };
40029
+ visit(sourceFile);
40030
+ return matchedNode;
40031
+ };
40032
+ const isOffsetInsideWorklet = (sourceFile, targetOffset) => {
40033
+ let currentNode = findNodeAtOffset(sourceFile, targetOffset);
40034
+ while (currentNode) {
40035
+ if (ts.isFunctionLike(currentNode) && isFunctionWorklet(currentNode)) return true;
40036
+ currentNode = currentNode.parent;
40037
+ }
40038
+ return false;
40039
+ };
40040
+ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirectory) => {
40041
+ if (!SUPPRESSED_COMPILER_CODES.has(diagnostic.code)) return false;
40042
+ if (!project.hasReanimated) return false;
40043
+ const primaryLabel = diagnostic.labels[0];
40044
+ if (!primaryLabel) return false;
40045
+ const absolutePath = Path.isAbsolute(diagnostic.filename) ? diagnostic.filename : Path.join(rootDirectory, diagnostic.filename);
40046
+ let sourceText;
40047
+ try {
40048
+ sourceText = NFS.readFileSync(absolutePath, "utf-8");
40049
+ } catch {
40050
+ return false;
40051
+ }
40052
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
40053
+ };
40054
+ const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
40055
+ const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
40056
+ const REACT_COMPILER_PLUGIN = "react-hooks-js";
40057
+ const getScriptKind = (filename) => {
40058
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
40059
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
40060
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
40061
+ return ts.ScriptKind.JS;
40062
+ };
40063
+ const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
40064
+ const nodeLineRange = (sourceFile, node) => ({
40065
+ startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
40066
+ endLine: sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1
40067
+ });
40068
+ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40069
+ let foundRange = null;
40070
+ const visit = (node) => {
40071
+ if (foundRange) return;
40072
+ if (ts.isFunctionLike(node)) {
40073
+ const range = nodeLineRange(sourceFile, node);
40074
+ if (range.startLine <= targetLine && targetLine <= range.endLine) foundRange = range;
40075
+ return;
40076
+ }
40077
+ ts.forEachChild(node, visit);
40078
+ };
40079
+ visit(sourceFile);
40080
+ return foundRange;
40081
+ };
40082
+ const NEWLINE_BYTE = 10;
40083
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40084
+ let lineNumber = 1;
40085
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40086
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40087
+ return lineNumber;
40088
+ };
40089
+ /**
40090
+ * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40091
+ * whose enclosing function also carries a `react-hooks-js` (React
40092
+ * Compiler) diagnostic. Every react-hooks-js finding marks a function the
40093
+ * compiler could NOT compile (a bail-out / skip / preserve-manual-
40094
+ * memoization failure), so "React Compiler already caches every value
40095
+ * here" is false for that function and the manual memoization is
40096
+ * load-bearing, not dead weight.
40097
+ *
40098
+ * Matching is per enclosing function, not per file: a bailed-out
40099
+ * component must not silence redundant-memo findings in a sibling
40100
+ * component the compiler did optimize.
40101
+ *
40102
+ * Module-scope `memo(() => …)` wrappers have no enclosing function; for
40103
+ * those the memo call's own span is the containment range, so a bail-out
40104
+ * inside the inline-wrapped component still suppresses. Every failure
40105
+ * path (unreadable file, missing span) fails open and keeps the
40106
+ * diagnostic.
40107
+ */
40108
+ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) => {
40109
+ const compilerDiagnosticLinesByFile = /* @__PURE__ */ new Map();
40110
+ let hasManualMemoizationDiagnostic = false;
40111
+ for (const diagnostic of diagnostics) {
40112
+ if (isManualMemoizationDiagnostic(diagnostic)) {
40113
+ hasManualMemoizationDiagnostic = true;
40114
+ continue;
40115
+ }
40116
+ if (diagnostic.plugin !== REACT_COMPILER_PLUGIN || diagnostic.line <= 0) continue;
40117
+ const lines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
40118
+ if (lines) lines.push(diagnostic.line);
40119
+ else compilerDiagnosticLinesByFile.set(diagnostic.filePath, [diagnostic.line]);
40120
+ }
40121
+ if (!hasManualMemoizationDiagnostic || compilerDiagnosticLinesByFile.size === 0) return diagnostics;
40122
+ const parsedFileCache = /* @__PURE__ */ new Map();
40123
+ const readAndParse = (filePath) => {
40124
+ const cached = parsedFileCache.get(filePath);
40125
+ if (cached !== void 0) return cached;
40126
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
40127
+ let parsed;
40128
+ try {
40129
+ const buffer = NFS.readFileSync(absolutePath);
40130
+ parsed = {
40131
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
40132
+ buffer
40133
+ };
40134
+ } catch {
40135
+ parsed = null;
40136
+ }
40137
+ parsedFileCache.set(filePath, parsed);
40138
+ return parsed;
40139
+ };
40140
+ const isInsideBailedOutFunction = (diagnostic) => {
40141
+ const compilerLines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
40142
+ if (!compilerLines || diagnostic.line <= 0) return false;
40143
+ const parsed = readAndParse(diagnostic.filePath);
40144
+ if (!parsed) return false;
40145
+ let containmentRange = findOutermostEnclosingFunctionRange(parsed.sourceFile, diagnostic.line);
40146
+ if (!containmentRange && diagnostic.offset !== void 0 && diagnostic.length !== void 0) containmentRange = {
40147
+ startLine: diagnostic.line,
40148
+ endLine: lineOfUtf8Offset(parsed.buffer, diagnostic.offset + diagnostic.length)
40149
+ };
40150
+ if (!containmentRange) return false;
40151
+ const { startLine, endLine } = containmentRange;
40152
+ return compilerLines.some((compilerLine) => startLine <= compilerLine && compilerLine <= endLine);
40153
+ };
40154
+ return diagnostics.filter((diagnostic) => !isManualMemoizationDiagnostic(diagnostic) || !isInsideBailedOutFunction(diagnostic));
40155
+ };
39776
40156
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
39777
40157
  const LEADING_SEVERITY_LABEL_PATTERN = /^(?:Error|Warning):\s*/;
39778
40158
  const TRAILING_PERIOD_PATTERN = /\.$/;
@@ -39912,7 +40292,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
39912
40292
  minifiedFileCache.set(absolutePath, minified);
39913
40293
  return minified;
39914
40294
  };
39915
- return parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
40295
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
39916
40296
  const { plugin, rule } = parseRuleCode(diagnostic.code);
39917
40297
  const primaryLabel = diagnostic.labels[0];
39918
40298
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -39939,7 +40319,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
39939
40319
  ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
39940
40320
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
39941
40321
  };
39942
- });
40322
+ }), rootDirectory);
39943
40323
  };
39944
40324
  const buildOxlintChildEnv = (sourceEnv) => {
39945
40325
  const childEnv = {};
@@ -40450,6 +40830,7 @@ const runOxlint = async (options) => {
40450
40830
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
40451
40831
  const severityControls = buildRuleSeverityControls(userConfig);
40452
40832
  validateRuleRegistration();
40833
+ resetManifestCaches();
40453
40834
  if (includePaths !== void 0 && includePaths.length === 0) return [];
40454
40835
  const pluginPath = resolvePluginPath();
40455
40836
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
@@ -41112,9 +41493,10 @@ const CONTEXT_AXES = [
41112
41493
  }
41113
41494
  ];
41114
41495
  const SCORE_AXES = [...GATED_AXES, ...CONTEXT_AXES];
41115
- const worstGatedAxis = (score) => {
41116
- let worst = GATED_AXES[0];
41117
- for (const axis of GATED_AXES) if (score[axis.key] < score[worst.key]) worst = axis;
41496
+ const gatedAxesForSection = (section) => section === "devDependencies" ? GATED_AXES.filter((axis) => axis.key !== "vulnerability") : GATED_AXES;
41497
+ const worstGatedAxis = (score, gatedAxes) => {
41498
+ let worst = gatedAxes[0];
41499
+ for (const axis of gatedAxes) if (score[axis.key] < score[worst.key]) worst = axis;
41118
41500
  return worst;
41119
41501
  };
41120
41502
  const clampScore = (value) => {
@@ -41138,6 +41520,69 @@ const resolveConcreteVersion = (spec) => {
41138
41520
  if (range === null || range === "*") return null;
41139
41521
  return import_semver.minVersion(trimmed)?.version ?? null;
41140
41522
  };
41523
+ const readTextFileOrNull = (filePath) => {
41524
+ try {
41525
+ return NFS.readFileSync(filePath, "utf-8");
41526
+ } catch {
41527
+ return null;
41528
+ }
41529
+ };
41530
+ const buildNpmLockLookup = (lockText) => {
41531
+ let parsed;
41532
+ try {
41533
+ parsed = JSON.parse(lockText);
41534
+ } catch {
41535
+ return null;
41536
+ }
41537
+ if (typeof parsed !== "object" || parsed === null) return null;
41538
+ const lock = parsed;
41539
+ return (name) => {
41540
+ const entry = lock.packages?.[`node_modules/${name}`] ?? lock.dependencies?.[name];
41541
+ return typeof entry?.version === "string" ? entry.version : null;
41542
+ };
41543
+ };
41544
+ const buildYarnLockLookup = (lockText) => {
41545
+ const lines = lockText.split(/\r?\n/);
41546
+ return (name, spec) => {
41547
+ const descriptor = `${name}@${spec}`;
41548
+ const berryDescriptor = `${name}@npm:${spec}`;
41549
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
41550
+ const line = lines[lineIndex];
41551
+ if (line.startsWith(" ") || line.startsWith("#") || !line.includes(":")) continue;
41552
+ const descriptors = line.replace(/:\s*$/, "").split(",").map((part) => part.trim().replace(/^"|"$/g, ""));
41553
+ if (!descriptors.includes(descriptor) && !descriptors.includes(berryDescriptor)) continue;
41554
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && (lines[bodyIndex].startsWith(" ") || lines[bodyIndex] === ""); bodyIndex += 1) {
41555
+ const versionMatch = /^\s+version:?\s+"?([^"\s]+)"?/.exec(lines[bodyIndex]);
41556
+ if (versionMatch) return versionMatch[1];
41557
+ }
41558
+ return null;
41559
+ }
41560
+ return null;
41561
+ };
41562
+ };
41563
+ const buildPnpmLockLookup = (lockText) => {
41564
+ return (name) => {
41565
+ const entryPattern = new RegExp(`^ {4,8}['"]?${name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}['"]?:\\s*$`);
41566
+ const lines = lockText.split(/\r?\n/);
41567
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
41568
+ if (!entryPattern.test(lines[lineIndex])) continue;
41569
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && /^ {5,}\S/.test(lines[bodyIndex]); bodyIndex += 1) {
41570
+ const versionMatch = /^\s+version:\s+['"]?([^'"\s(]+)/.exec(lines[bodyIndex]);
41571
+ if (versionMatch) return versionMatch[1];
41572
+ }
41573
+ }
41574
+ return null;
41575
+ };
41576
+ };
41577
+ const buildLockedVersionLookup = (rootDirectory) => {
41578
+ const npmLockText = readTextFileOrNull(Path.join(rootDirectory, "package-lock.json"));
41579
+ if (npmLockText !== null) return buildNpmLockLookup(npmLockText);
41580
+ const yarnLockText = readTextFileOrNull(Path.join(rootDirectory, "yarn.lock"));
41581
+ if (yarnLockText !== null) return buildYarnLockLookup(yarnLockText);
41582
+ const pnpmLockText = readTextFileOrNull(Path.join(rootDirectory, "pnpm-lock.yaml"));
41583
+ if (pnpmLockText !== null) return buildPnpmLockLookup(pnpmLockText);
41584
+ return null;
41585
+ };
41141
41586
  const locateDependencyKey = (packageJsonText, section, name) => {
41142
41587
  const needle = `"${name}"`;
41143
41588
  const sectionHeader = new RegExp(`"${section}"\\s*:\\s*\\{`);
@@ -41164,7 +41609,7 @@ const locateDependencyKey = (packageJsonText, section, name) => {
41164
41609
  }
41165
41610
  return null;
41166
41611
  };
41167
- const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies) => {
41612
+ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies, lockedVersionLookup) => {
41168
41613
  const sectionByName = /* @__PURE__ */ new Map();
41169
41614
  for (const name of Object.keys(packageJson.dependencies ?? {})) sectionByName.set(name, "dependencies");
41170
41615
  if (includeDevDependencies) {
@@ -41174,13 +41619,18 @@ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDepe
41174
41619
  for (const [name, section] of sectionByName) {
41175
41620
  if (SUPPLY_CHAIN_IGNORED_PACKAGES.has(name)) continue;
41176
41621
  const spec = (packageJson[section] ?? {})[name] ?? "";
41177
- const version = resolveConcreteVersion(spec);
41178
- if (version === null) continue;
41622
+ const floorVersion = resolveConcreteVersion(spec);
41623
+ if (floorVersion === null) continue;
41624
+ const lockedVersion = lockedVersionLookup?.(name, spec) ?? null;
41625
+ const lockSatisfiesSpec = lockedVersion !== null && import_semver.valid(lockedVersion) !== null && import_semver.satisfies(lockedVersion, spec);
41626
+ const isExactPin = import_semver.valid(spec.trim()) !== null;
41179
41627
  const location = locateDependencyKey(packageJsonText, section, name);
41180
41628
  dependencies.push({
41181
41629
  name,
41182
- version,
41630
+ version: lockSatisfiesSpec ? lockedVersion : floorVersion,
41183
41631
  spec,
41632
+ section,
41633
+ resolution: isExactPin ? "pin" : lockSatisfiesSpec ? "locked" : "floor",
41184
41634
  line: location?.line ?? 0,
41185
41635
  column: location?.column ?? 0
41186
41636
  });
@@ -41331,7 +41781,11 @@ const formatAlertReason = (topAlerts, totalCount) => {
41331
41781
  }
41332
41782
  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)}.`;
41333
41783
  };
41334
- const formatDependencyIdentity = (dependency) => import_semver.valid(dependency.spec) !== null ? `${dependency.name}@${dependency.version}` : `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
41784
+ const formatDependencyIdentity = (dependency) => {
41785
+ if (dependency.resolution === "pin") return `${dependency.name}@${dependency.version}`;
41786
+ if (dependency.resolution === "locked") return `${dependency.name}@${dependency.version} (lockfile resolution of "${dependency.spec}")`;
41787
+ return `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
41788
+ };
41335
41789
  const buildSupplyChainHelp = (dependency, failingAxis, topAlerts, packagePageUrl, options) => {
41336
41790
  const hasCriticalAlert = topAlerts.some((alert) => alert.severity.toLowerCase() === "critical");
41337
41791
  const entry = `\`"${dependency.name}": "${dependency.spec}"\``;
@@ -41374,7 +41828,7 @@ const buildLowScoreDiagnostic = (dependency, artifact, failingAxis, options) =>
41374
41828
  const checkSupplyChain = (input) => gen(function* () {
41375
41829
  const options = resolveOptions(input.userConfig);
41376
41830
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
41377
- const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
41831
+ const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies, buildLockedVersionLookup(input.rootDirectory));
41378
41832
  if (dependencies.length === 0) return [];
41379
41833
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
41380
41834
  if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
@@ -41383,7 +41837,7 @@ const checkSupplyChain = (input) => gen(function* () {
41383
41837
  for (let index = 0; index < dependencies.length; index += 1) {
41384
41838
  const artifact = artifacts[index];
41385
41839
  if (!artifact) continue;
41386
- const worstAxis = worstGatedAxis(artifact.score);
41840
+ const worstAxis = worstGatedAxis(artifact.score, gatedAxesForSection(dependencies[index].section));
41387
41841
  if (toHundred(artifact.score[worstAxis.key]) >= options.minScore) continue;
41388
41842
  diagnostics.push(buildLowScoreDiagnostic(dependencies[index], artifact, worstAxis, options));
41389
41843
  }
@@ -42109,7 +42563,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
42109
42563
  /** Display name used in client-facing messages and progress titles. */
42110
42564
  const SERVER_DISPLAY_NAME = "React Doctor";
42111
42565
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
42112
- const SERVER_VERSION = "0.7.1";
42566
+ const SERVER_VERSION = "0.7.2";
42113
42567
  /** `Diagnostic.source` shown next to every published diagnostic. */
42114
42568
  const DIAGNOSTIC_SOURCE = "react-doctor";
42115
42569
  /**
@@ -44160,5 +44614,5 @@ const startLanguageServer = () => {
44160
44614
  };
44161
44615
  //#endregion
44162
44616
  export { startLanguageServer };
44163
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="027c66d6-b30a-59de-ad83-58f5ad304c94")}catch(e){}}();
44164
- //# debugId=027c66d6-b30a-59de-ad83-58f5ad304c94
44617
+ !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]="30e226f8-fb2c-5784-b48c-95e8d218d035")}catch(e){}}();
44618
+ //# debugId=30e226f8-fb2c-5784-b48c-95e8d218d035