react-doctor 0.7.1 → 0.7.2-dev.08b768b

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]="3dc4a1ac-42dd-51e8-ba38-0260edf45659")}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";
@@ -32791,6 +32791,25 @@ var AmbiguousProjectError = class extends Error {
32791
32791
  }
32792
32792
  };
32793
32793
  const isProjectDiscoveryError = (value) => value instanceof ProjectNotFoundError || value instanceof NoReactDependencyError || value instanceof PackageJsonNotFoundError || value instanceof NotADirectoryError || value instanceof AmbiguousProjectError;
32794
+ const isErrnoException = (error) => error instanceof Error && "code" in error;
32795
+ const IGNORABLE_READDIR_ERROR_CODES = new Set([
32796
+ "EACCES",
32797
+ "EPERM",
32798
+ "ENOENT",
32799
+ "ENOTDIR",
32800
+ "EINVAL",
32801
+ "ELOOP",
32802
+ "ENAMETOOLONG"
32803
+ ]);
32804
+ const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
32805
+ const readDirectoryEntries = (directoryPath) => {
32806
+ try {
32807
+ return NFS.readdirSync(directoryPath, { withFileTypes: true });
32808
+ } catch (error) {
32809
+ if (isIgnorableReaddirError(error)) return [];
32810
+ throw error;
32811
+ }
32812
+ };
32794
32813
  const isFile = (filePath) => {
32795
32814
  try {
32796
32815
  return NFS.statSync(filePath).isFile();
@@ -32798,10 +32817,24 @@ const isFile = (filePath) => {
32798
32817
  return false;
32799
32818
  }
32800
32819
  };
32820
+ const isDirectory = (directoryPath) => {
32821
+ try {
32822
+ return NFS.statSync(directoryPath).isDirectory();
32823
+ } catch {
32824
+ return false;
32825
+ }
32826
+ };
32827
+ const isPlainObject = (value) => {
32828
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
32829
+ const prototype = Object.getPrototypeOf(value);
32830
+ return prototype === null || prototype === Object.prototype;
32831
+ };
32801
32832
  const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?|mts|mjs)$/;
32802
32833
  const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
32834
+ const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
32803
32835
  const MINIFIED_SNIFF_BYTES = 65536;
32804
32836
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
32837
+ const SCANNED_DOT_DIRECTORIES = new Set([".dumi", ".storybook"]);
32805
32838
  const IGNORED_DIRECTORIES = new Set([
32806
32839
  ".angular",
32807
32840
  ".astro",
@@ -32897,6 +32930,8 @@ const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
32897
32930
  const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
32898
32931
  const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
32899
32932
  const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
32933
+ const ABORT_EXIT_CODES = new Set([134, 3221226505]);
32934
+ const OXLINT_OOM_RESCUE_BUDGET_MS = 6e4;
32900
32935
  const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
32901
32936
  const LINT_PHASE_TIMEOUT_MS = 3e5;
32902
32937
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
@@ -32943,7 +32978,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32943
32978
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32944
32979
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32945
32980
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
32946
- const CORE_PACKAGE_VERSION = "0.7.1";
32981
+ const CORE_PACKAGE_VERSION = "0.7.2";
32947
32982
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32948
32983
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
32949
32984
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -32953,8 +32988,9 @@ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
32953
32988
  const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
32954
32989
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
32955
32990
  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);
32991
+ const isIgnoredDirectoryName = (directoryName) => IGNORED_DIRECTORIES.has(directoryName) || directoryName.startsWith(".") && !SCANNED_DOT_DIRECTORIES.has(directoryName);
32992
+ const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => isIgnoredDirectoryName(segment));
32993
+ const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath) && !GENERATED_SOURCE_DIRECTORY_PATTERN.test(filePath);
32958
32994
  const isMinifiedSource = (absolutePath) => {
32959
32995
  let fileDescriptor;
32960
32996
  try {
@@ -32991,25 +33027,6 @@ const isLargeMinifiedFile = (absolutePath, knownSizeBytes) => {
32991
33027
  cachedIsLargeMinifiedByPath.set(absolutePath, result);
32992
33028
  return result;
32993
33029
  };
32994
- const isErrnoException = (error) => error instanceof Error && "code" in error;
32995
- const IGNORABLE_READDIR_ERROR_CODES = new Set([
32996
- "EACCES",
32997
- "EPERM",
32998
- "ENOENT",
32999
- "ENOTDIR",
33000
- "EINVAL",
33001
- "ELOOP",
33002
- "ENAMETOOLONG"
33003
- ]);
33004
- const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
33005
- const readDirectoryEntries = (directoryPath) => {
33006
- try {
33007
- return NFS.readdirSync(directoryPath, { withFileTypes: true });
33008
- } catch (error) {
33009
- if (isIgnorableReaddirError(error)) return [];
33010
- throw error;
33011
- }
33012
- };
33013
33030
  function* walkSourceTreeFiles(rootDirectory) {
33014
33031
  const stack = [rootDirectory];
33015
33032
  while (stack.length > 0) {
@@ -33018,7 +33035,7 @@ function* walkSourceTreeFiles(rootDirectory) {
33018
33035
  for (const entry of readDirectoryEntries(currentDirectory)) {
33019
33036
  const absolutePath = Path.join(currentDirectory, entry.name);
33020
33037
  if (entry.isDirectory()) {
33021
- if (!IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
33038
+ if (!isIgnoredDirectoryName(entry.name)) stack.push(absolutePath);
33022
33039
  continue;
33023
33040
  }
33024
33041
  if (entry.isFile()) yield {
@@ -33115,6 +33132,146 @@ const findMonorepoRoot = (startDirectory) => {
33115
33132
  * byte-equivalent copy.
33116
33133
  */
33117
33134
  const isProjectBoundary = (directory) => NFS.existsSync(Path.join(directory, ".git")) || isMonorepoRoot(directory);
33135
+ const TSCONFIG_FILENAME = "tsconfig.json";
33136
+ const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
33137
+ const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
33138
+ const resolvePackageExtendsPath = (extendsValue, fromConfigDirectory) => {
33139
+ const requireFromConfig = createRequire(Path.join(fromConfigDirectory, "tsconfig.json"));
33140
+ const candidates = [
33141
+ extendsValue,
33142
+ ensureJsonExtension(extendsValue),
33143
+ `${extendsValue.replace(/\/$/, "")}/tsconfig.json`
33144
+ ];
33145
+ for (const candidate of candidates) try {
33146
+ return requireFromConfig.resolve(candidate);
33147
+ } catch {
33148
+ continue;
33149
+ }
33150
+ return null;
33151
+ };
33152
+ const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
33153
+ if (isRelativeExtendsValue(extendsValue)) return ensureJsonExtension(Path.resolve(fromConfigDirectory, extendsValue));
33154
+ return resolvePackageExtendsPath(extendsValue, fromConfigDirectory);
33155
+ };
33156
+ const normalizeCompilerOptions = (compilerOptions) => {
33157
+ if (!isPlainObject(compilerOptions)) return { hasExplicitLib: false };
33158
+ const target = typeof compilerOptions.target === "string" ? compilerOptions.target : void 0;
33159
+ const hasExplicitLib = Object.hasOwn(compilerOptions, "lib");
33160
+ return {
33161
+ target,
33162
+ lib: Array.isArray(compilerOptions.lib) ? compilerOptions.lib.filter((entry) => typeof entry === "string") : void 0,
33163
+ hasExplicitLib
33164
+ };
33165
+ };
33166
+ const readTsConfig = (filePath) => {
33167
+ let content;
33168
+ try {
33169
+ content = NFS.readFileSync(filePath, "utf-8");
33170
+ } catch {
33171
+ return null;
33172
+ }
33173
+ const parsed = ts.parseConfigFileTextToJson(filePath, content);
33174
+ if (!isPlainObject(parsed.config)) return null;
33175
+ return {
33176
+ extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
33177
+ referencePaths: normalizeReferencePaths(parsed.config.references),
33178
+ compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
33179
+ };
33180
+ };
33181
+ const normalizeReferencePaths = (references) => {
33182
+ if (!Array.isArray(references)) return [];
33183
+ return references.map((reference) => isPlainObject(reference) && typeof reference.path === "string" ? reference.path : null).filter((referencePath) => referencePath !== null);
33184
+ };
33185
+ const mergeCompilerOptions = (inherited, current) => {
33186
+ const target = current.target ?? inherited?.target;
33187
+ const hasExplicitLib = current.hasExplicitLib || Boolean(inherited?.hasExplicitLib);
33188
+ return {
33189
+ target,
33190
+ lib: current.hasExplicitLib ? current.lib : inherited?.lib,
33191
+ hasExplicitLib
33192
+ };
33193
+ };
33194
+ const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
33195
+ const realPath = NFS.realpathSync.native(tsConfigPath);
33196
+ if (visitedPaths.has(realPath)) return null;
33197
+ const tsConfig = readTsConfig(realPath);
33198
+ if (!tsConfig) return null;
33199
+ const nextVisitedPaths = new Set(visitedPaths);
33200
+ nextVisitedPaths.add(realPath);
33201
+ if (tsConfig.extends && extendsDepth < 8) {
33202
+ const parentPath = resolveExtendsPath(tsConfig.extends, Path.dirname(realPath));
33203
+ if (parentPath && isFile(parentPath)) return mergeCompilerOptions(readResolvedCompilerOptions(parentPath, extendsDepth + 1, nextVisitedPaths), tsConfig.compilerOptions);
33204
+ }
33205
+ return tsConfig.compilerOptions;
33206
+ };
33207
+ const targetYearIsPreES2023 = (target) => {
33208
+ const year = ES_TARGET_YEAR_BY_NAME[target.toLowerCase()];
33209
+ return year !== void 0 && year < 2023;
33210
+ };
33211
+ const libEntryIncludesES2023Array = (entry) => {
33212
+ const normalizedEntry = entry.toLowerCase();
33213
+ if (normalizedEntry === "esnext" || normalizedEntry === "esnext.array") return true;
33214
+ const esYearMatch = /^es(\d{4})(?:\.(.+))?$/.exec(normalizedEntry);
33215
+ if (!esYearMatch) return false;
33216
+ if (Number(esYearMatch[1]) < 2023) return false;
33217
+ const component = esYearMatch[2];
33218
+ return component === void 0 || component === "array";
33219
+ };
33220
+ const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
33221
+ const compilerOptionsArePreES2023 = (compilerOptions) => {
33222
+ if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
33223
+ if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
33224
+ return false;
33225
+ };
33226
+ const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
33227
+ const detectPreES2023FromConfig = (tsConfigPath, visitedConfigPaths = /* @__PURE__ */ new Set()) => {
33228
+ if (visitedConfigPaths.has(tsConfigPath)) return false;
33229
+ const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
33230
+ if (!compilerOptions) return false;
33231
+ if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) {
33232
+ const tsConfig = readTsConfig(tsConfigPath);
33233
+ if (!tsConfig) return false;
33234
+ const nextVisitedConfigPaths = new Set(visitedConfigPaths);
33235
+ nextVisitedConfigPaths.add(tsConfigPath);
33236
+ const configDirectory = Path.dirname(tsConfigPath);
33237
+ return tsConfig.referencePaths.some((referencePath) => {
33238
+ const resolvedReferencePath = Path.resolve(configDirectory, referencePath);
33239
+ const referencedConfigPath = isFile(resolvedReferencePath) ? resolvedReferencePath : Path.join(resolvedReferencePath, TSCONFIG_FILENAME);
33240
+ return isFile(referencedConfigPath) && detectPreES2023FromConfig(referencedConfigPath, nextVisitedConfigPaths);
33241
+ });
33242
+ }
33243
+ return compilerOptionsArePreES2023(compilerOptions);
33244
+ };
33245
+ const detectPreES2023Target = (directory) => {
33246
+ const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
33247
+ if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
33248
+ for (const fallbackFilename of FALLBACK_TSCONFIG_FILENAMES) {
33249
+ const fallbackPath = Path.join(directory, fallbackFilename);
33250
+ if (isFile(fallbackPath)) return detectPreES2023FromConfig(fallbackPath);
33251
+ }
33252
+ return false;
33253
+ };
33254
+ const FALLBACK_TSCONFIG_FILENAMES = ["tsconfig.app.json", "tsconfig.build.json"];
33255
+ const FRAMEWORK_PACKAGES = {
33256
+ next: "nextjs",
33257
+ "@tanstack/react-start": "tanstack-start",
33258
+ vite: "vite",
33259
+ "react-scripts": "cra",
33260
+ "@remix-run/react": "remix",
33261
+ gatsby: "gatsby",
33262
+ expo: "expo",
33263
+ "react-native": "react-native"
33264
+ };
33265
+ const detectFramework = (dependencies) => {
33266
+ for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
33267
+ if (dependencies.preact && !dependencies.react) return "preact";
33268
+ return "unknown";
33269
+ };
33270
+ const MOBILE_FRAMEWORKS = new Set(["expo", "react-native"]);
33271
+ const frameworkMergeRank = (framework) => {
33272
+ if (framework === "unknown") return 3;
33273
+ return MOBILE_FRAMEWORKS.has(framework) ? 2 : 1;
33274
+ };
33118
33275
  const REACT_COMPILER_PACKAGES = new Set([
33119
33276
  "babel-plugin-react-compiler",
33120
33277
  "react-compiler-runtime",
@@ -33151,6 +33308,7 @@ const EXPO_APP_CONFIG_FILENAMES = [
33151
33308
  ];
33152
33309
  const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
33153
33310
  const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
33311
+ const STATIC_EXPORT_OUTPUT_PATTERN = /(?:^|[^.\w])["']?output["']?\s*:\s*["']export["']/m;
33154
33312
  const hasCompilerPackage = (packageJson) => {
33155
33313
  const allDependencies = {
33156
33314
  ...packageJson.peerDependencies,
@@ -33183,34 +33341,10 @@ const detectReactCompiler = (directory, packageJson) => {
33183
33341
  }
33184
33342
  return false;
33185
33343
  };
33186
- const FRAMEWORK_PACKAGES = {
33187
- next: "nextjs",
33188
- "@tanstack/react-start": "tanstack-start",
33189
- vite: "vite",
33190
- "react-scripts": "cra",
33191
- "@remix-run/react": "remix",
33192
- gatsby: "gatsby",
33193
- expo: "expo",
33194
- "react-native": "react-native"
33195
- };
33196
- const FRAMEWORK_DISPLAY_NAMES = {
33197
- nextjs: "Next.js",
33198
- "tanstack-start": "TanStack Start",
33199
- vite: "Vite",
33200
- cra: "Create React App",
33201
- remix: "Remix",
33202
- gatsby: "Gatsby",
33203
- expo: "Expo",
33204
- "react-native": "React Native",
33205
- preact: "Preact",
33206
- unknown: "React"
33207
- };
33208
- const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
33209
- const detectFramework = (dependencies) => {
33210
- for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
33211
- if (dependencies.preact && !dependencies.react) return "preact";
33212
- return "unknown";
33213
- };
33344
+ const detectNextjsStaticExport = (directory) => NEXT_CONFIG_FILENAMES.some((filename) => {
33345
+ const filePath = Path.join(directory, filename);
33346
+ return isFile(filePath) && STATIC_EXPORT_OUTPUT_PATTERN.test(NFS.readFileSync(filePath, "utf-8"));
33347
+ });
33214
33348
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
33215
33349
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
33216
33350
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
@@ -33348,10 +33482,76 @@ const isConcreteDependencyVersion = (version) => {
33348
33482
  const normalizedVersion = normalizeDependencyVersion(version);
33349
33483
  return normalizedVersion !== null && /\d/.test(normalizedVersion);
33350
33484
  };
33351
- const isPlainObject = (value) => {
33352
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
33353
- const prototype = Object.getPrototypeOf(value);
33354
- return prototype === null || prototype === Object.prototype;
33485
+ const isMajorMinorAtLeast = (detected, required) => {
33486
+ if (detected === null) return true;
33487
+ if (detected.major !== required.major) return detected.major > required.major;
33488
+ return detected.minor >= required.minor;
33489
+ };
33490
+ const parseReactMajor = (reactVersion) => {
33491
+ if (typeof reactVersion !== "string") return null;
33492
+ return getLowestDependencyMajor(reactVersion);
33493
+ };
33494
+ const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
33495
+ const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
33496
+ const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
33497
+ const parseReactMajorMinor = (reactVersion) => {
33498
+ if (typeof reactVersion !== "string") return null;
33499
+ const trimmed = reactVersion.trim();
33500
+ if (trimmed.length === 0) return null;
33501
+ const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
33502
+ if (lowerBoundsOnly.length === 0) return null;
33503
+ const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
33504
+ if (majorMinorMatch) {
33505
+ const major = Number.parseInt(majorMinorMatch[1], 10);
33506
+ const minor = Number.parseInt(majorMinorMatch[2], 10);
33507
+ if (!Number.isFinite(major) || major <= 0) return null;
33508
+ if (!Number.isFinite(minor) || minor < 0) return null;
33509
+ return {
33510
+ major,
33511
+ minor
33512
+ };
33513
+ }
33514
+ const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
33515
+ if (!majorOnlyMatch) return null;
33516
+ const major = Number.parseInt(majorOnlyMatch[1], 10);
33517
+ if (!Number.isFinite(major) || major <= 0) return null;
33518
+ return {
33519
+ major,
33520
+ minor: 0
33521
+ };
33522
+ };
33523
+ const parseLowerBoundVersion = (versionSpec) => import_semver.validRange(versionSpec) !== null ? import_semver.minVersion(versionSpec) : import_semver.coerce(versionSpec);
33524
+ const parseTailwindMajorMinor = (tailwindVersion) => {
33525
+ if (typeof tailwindVersion !== "string") return null;
33526
+ const trimmed = tailwindVersion.trim();
33527
+ if (trimmed.length === 0) return null;
33528
+ const lowerBound = parseLowerBoundVersion(trimmed);
33529
+ if (lowerBound === null || lowerBound.major <= 0) return null;
33530
+ return {
33531
+ major: lowerBound.major,
33532
+ minor: lowerBound.minor
33533
+ };
33534
+ };
33535
+ const hasUpperBoundOnlyPeerRange = (range) => {
33536
+ if (typeof range !== "string") return false;
33537
+ const normalizedRange = normalizeDependencyVersion(range);
33538
+ if (normalizedRange === null) return false;
33539
+ return splitDependencyVersionBranches(normalizedRange).some((branch) => {
33540
+ const normalizedBranch = normalizeDependencyVersion(branch);
33541
+ return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
33542
+ });
33543
+ };
33544
+ const peerRangeMinMajor = (range) => {
33545
+ if (typeof range !== "string") return null;
33546
+ return getLowestDependencyMajor(range);
33547
+ };
33548
+ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
33549
+ const installedReactMajor = parseReactMajor(reactVersion);
33550
+ const peerReactRange = packageJson.peerDependencies?.react;
33551
+ if (typeof peerReactRange !== "string") return installedReactMajor;
33552
+ const peerFloor = peerRangeMinMajor(peerReactRange);
33553
+ if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
33554
+ return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
33355
33555
  };
33356
33556
  const isCatalogReference = (version) => version.startsWith("catalog:");
33357
33557
  const extractCatalogName = (version) => {
@@ -33493,6 +33693,16 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
33493
33693
  }
33494
33694
  return null;
33495
33695
  };
33696
+ const REACT_SECTIONS = [
33697
+ "dependencies",
33698
+ "peerDependencies",
33699
+ "devDependencies"
33700
+ ];
33701
+ const TAILWIND_ZOD_SECTIONS = [
33702
+ "dependencies",
33703
+ "devDependencies",
33704
+ "peerDependencies"
33705
+ ];
33496
33706
  const EMPTY_DEPENDENCY_INFO = {
33497
33707
  reactVersion: null,
33498
33708
  tailwindVersion: null,
@@ -33533,6 +33743,10 @@ const extractDependencyInfo = (packageJson) => {
33533
33743
  framework: detectFramework(allDependencies)
33534
33744
  };
33535
33745
  };
33746
+ const getDependencySpec = (packageJson, packageName) => {
33747
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
33748
+ return typeof spec === "string" ? spec : null;
33749
+ };
33536
33750
  const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
33537
33751
  for (const section of sections) {
33538
33752
  const version = packageJson[section]?.[packageName];
@@ -33549,222 +33763,91 @@ const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
33549
33763
  version: null
33550
33764
  };
33551
33765
  };
33552
- const isDirectory = (directoryPath) => {
33553
- try {
33554
- return NFS.statSync(directoryPath).isDirectory();
33555
- } catch {
33556
- return false;
33557
- }
33558
- };
33559
- const NX_PROJECT_DISCOVERY_DIRS = [
33560
- "apps",
33561
- "libs",
33562
- "packages"
33563
- ];
33564
- const getNxWorkspaceDirectories = (rootDirectory) => {
33565
- if (!isFile(Path.join(rootDirectory, "nx.json"))) return [];
33566
- const collected = [];
33567
- for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
33568
- const candidatePath = Path.join(rootDirectory, candidate);
33569
- if (!isDirectory(candidatePath)) continue;
33570
- for (const entry of readDirectoryEntries(candidatePath)) {
33571
- if (!entry.isDirectory()) continue;
33572
- const projectDirectory = Path.join(candidatePath, entry.name);
33573
- if (isFile(Path.join(projectDirectory, "project.json")) || isFile(Path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
33574
- }
33575
- }
33576
- return collected;
33577
- };
33578
- const parsePnpmWorkspacePatterns = (rootDirectory) => {
33579
- const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
33580
- if (!isFile(workspacePath)) return [];
33581
- const content = NFS.readFileSync(workspacePath, "utf-8");
33582
- const patterns = [];
33583
- let isInsidePackagesBlock = false;
33584
- for (const line of content.split("\n")) {
33585
- const trimmed = line.trim();
33586
- if (trimmed === "packages:") {
33587
- isInsidePackagesBlock = true;
33588
- continue;
33589
- }
33590
- if (isInsidePackagesBlock && trimmed.startsWith("-")) patterns.push(trimmed.replace(/^-\s*/, "").replace(/["']/g, ""));
33591
- else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith("#")) isInsidePackagesBlock = false;
33592
- }
33593
- return patterns;
33594
- };
33595
- const getWorkspacePatterns = (rootDirectory, packageJson) => {
33596
- const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
33597
- if (pnpmPatterns.length > 0) return pnpmPatterns;
33598
- if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
33599
- if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
33600
- const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
33601
- if (nxPatterns.length > 0) return nxPatterns;
33602
- return [];
33603
- };
33604
- const parseReactMajor = (reactVersion) => {
33605
- if (typeof reactVersion !== "string") return null;
33606
- return getLowestDependencyMajor(reactVersion);
33766
+ const REACT_DEPENDENCY_NAMES = new Set([
33767
+ "react",
33768
+ "react-native",
33769
+ "next",
33770
+ "preact"
33771
+ ]);
33772
+ const hasReactDependency = (packageJson) => {
33773
+ const allDependencies = {
33774
+ ...packageJson.peerDependencies,
33775
+ ...packageJson.dependencies,
33776
+ ...packageJson.devDependencies
33777
+ };
33778
+ return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
33607
33779
  };
33608
- const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
33609
- const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
33610
- if (!cleanPattern.includes("*")) {
33611
- const directoryPath = Path.join(rootDirectory, cleanPattern);
33612
- if (isDirectory(directoryPath) && isFile(Path.join(directoryPath, "package.json"))) return [directoryPath];
33613
- return [];
33614
- }
33615
- const wildcardIndex = cleanPattern.indexOf("*");
33616
- const baseDirectory = Path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
33617
- const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
33618
- if (!isDirectory(baseDirectory)) return [];
33619
- const resolved = [];
33620
- for (const entry of readDirectoryEntries(baseDirectory)) {
33621
- const entryPath = Path.join(baseDirectory, entry.name, suffixAfterWildcard);
33622
- if (isDirectory(entryPath) && isFile(Path.join(entryPath, "package.json"))) resolved.push(entryPath);
33623
- }
33624
- return resolved;
33780
+ const getPreactVersion = (packageJson) => {
33781
+ return {
33782
+ ...packageJson.peerDependencies,
33783
+ ...packageJson.dependencies,
33784
+ ...packageJson.devDependencies
33785
+ }.preact ?? null;
33625
33786
  };
33626
- const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
33627
- const dependencyDeclaration = getDependencyDeclaration({
33628
- packageJson: workspacePackageJson,
33629
- packageName,
33630
- sections
33631
- });
33632
- if (!dependencyDeclaration.hasDeclaration) return null;
33633
- return concreteVersion ?? resolveCatalogVersion(workspacePackageJson, packageName, workspaceDirectory, dependencyDeclaration.catalogReference) ?? resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, dependencyDeclaration.catalogReference);
33787
+ const TANSTACK_QUERY_PACKAGES = new Set([
33788
+ "@tanstack/react-query",
33789
+ "@tanstack/query-core",
33790
+ "react-query"
33791
+ ]);
33792
+ const hasTanStackQuery = (packageJson) => {
33793
+ const allDependencies = {
33794
+ ...packageJson.peerDependencies,
33795
+ ...packageJson.dependencies,
33796
+ ...packageJson.devDependencies
33797
+ };
33798
+ return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
33634
33799
  };
33635
- const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
33636
- if (!currentVersion) return true;
33637
- const currentMajor = parseReactMajor(currentVersion);
33638
- const nextMajor = parseReactMajor(nextVersion);
33639
- if (currentMajor === null) return nextMajor !== null;
33640
- if (nextMajor === null) return false;
33641
- return nextMajor < currentMajor;
33800
+ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
33801
+ if (version === null || !isCatalogReference(version)) return version;
33802
+ const catalogName = extractCatalogName(version);
33803
+ const resolvedLocalVersion = resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, catalogName);
33804
+ if (resolvedLocalVersion) return resolvedLocalVersion;
33805
+ const monorepoRoot = findMonorepoRoot(rootDirectory);
33806
+ if (!monorepoRoot) return version;
33807
+ const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
33808
+ if (!isFile(monorepoPackageJsonPath)) return version;
33809
+ return resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
33642
33810
  };
33643
- const findReactInWorkspaces = (rootDirectory, packageJson) => {
33644
- const patterns = getWorkspacePatterns(rootDirectory, packageJson);
33645
- const result = { ...EMPTY_DEPENDENCY_INFO };
33646
- for (const pattern of patterns) {
33647
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
33648
- for (const workspaceDirectory of directories) {
33649
- const workspacePackageJson = readPackageJson(Path.join(workspaceDirectory, "package.json"));
33650
- const info = extractDependencyInfo(workspacePackageJson);
33651
- const reactVersion = resolveWorkspaceDependencyVersion({
33652
- concreteVersion: info.reactVersion,
33653
- packageName: "react",
33654
- rootDirectory,
33655
- rootPackageJson: packageJson,
33656
- sections: [
33657
- "dependencies",
33658
- "peerDependencies",
33659
- "devDependencies"
33660
- ],
33661
- workspaceDirectory,
33662
- workspacePackageJson
33663
- });
33664
- const tailwindVersion = resolveWorkspaceDependencyVersion({
33665
- concreteVersion: info.tailwindVersion,
33666
- packageName: "tailwindcss",
33667
- rootDirectory,
33668
- rootPackageJson: packageJson,
33669
- sections: [
33670
- "dependencies",
33671
- "devDependencies",
33672
- "peerDependencies"
33673
- ],
33674
- workspaceDirectory,
33675
- workspacePackageJson
33676
- });
33677
- const zodVersion = resolveWorkspaceDependencyVersion({
33678
- concreteVersion: info.zodVersion,
33679
- packageName: "zod",
33680
- rootDirectory,
33681
- rootPackageJson: packageJson,
33682
- sections: [
33683
- "dependencies",
33684
- "devDependencies",
33685
- "peerDependencies"
33686
- ],
33687
- workspaceDirectory,
33688
- workspacePackageJson
33689
- });
33690
- if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
33691
- if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
33692
- if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
33693
- if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
33694
- const resultReactMajor = parseReactMajor(result.reactVersion);
33695
- if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
33696
- }
33811
+ /**
33812
+ * The chain of ancestor directories of `startDirectory`, nearest first, up to
33813
+ * and including the filesystem root. With `includeStart`, the chain begins at
33814
+ * `startDirectory` itself; otherwise at its parent. The one place the "walk up
33815
+ * the tree" traversal is expressed, so each caller adds only its own match /
33816
+ * stop policy instead of re-deriving the `path.dirname` loop.
33817
+ */
33818
+ const ancestorDirectories = (startDirectory, { includeStart }) => {
33819
+ const directories = [];
33820
+ let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
33821
+ while (true) {
33822
+ directories.push(currentDirectory);
33823
+ const parentDirectory = Path.dirname(currentDirectory);
33824
+ if (parentDirectory === currentDirectory) return directories;
33825
+ currentDirectory = parentDirectory;
33697
33826
  }
33698
- return result;
33699
33827
  };
33700
- const findDependencyInfoFromMonorepoRoot = (directory) => {
33701
- const monorepoRoot = findMonorepoRoot(directory);
33702
- if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
33703
- const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
33704
- if (!isFile(monorepoPackageJsonPath)) return EMPTY_DEPENDENCY_INFO;
33705
- const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
33706
- const rootInfo = extractDependencyInfo(rootPackageJson);
33707
- const leafPackageJsonPath = Path.join(directory, "package.json");
33708
- const leafPackageJson = isFile(leafPackageJsonPath) ? readPackageJson(leafPackageJsonPath) : null;
33709
- const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
33710
- packageJson: leafPackageJson,
33711
- packageName: "react",
33712
- sections: [
33713
- "dependencies",
33714
- "peerDependencies",
33715
- "devDependencies"
33716
- ]
33717
- }) : null;
33718
- const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
33719
- packageJson: leafPackageJson,
33720
- packageName: "tailwindcss",
33721
- sections: [
33722
- "dependencies",
33723
- "devDependencies",
33724
- "peerDependencies"
33725
- ]
33726
- }) : null;
33727
- const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
33728
- packageJson: leafPackageJson,
33729
- packageName: "zod",
33730
- sections: [
33731
- "dependencies",
33732
- "devDependencies",
33733
- "peerDependencies"
33734
- ]
33735
- }) : null;
33736
- const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
33737
- const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
33738
- const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
33739
- const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
33740
- const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
33741
- const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
33742
- const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
33743
- return {
33744
- reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
33745
- tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
33746
- zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
33747
- framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
33748
- };
33749
- };
33750
- const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
33751
- const rootValue = select(rootPackageJson);
33752
- if (rootValue !== null) return rootValue;
33753
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
33754
- if (patterns.length === 0) return null;
33755
- const visitedDirectories = /* @__PURE__ */ new Set();
33756
- for (const pattern of patterns) {
33757
- const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
33758
- for (const workspaceDirectory of directories) {
33759
- if (visitedDirectories.has(workspaceDirectory)) continue;
33760
- visitedDirectories.add(workspaceDirectory);
33761
- const value = select(readPackageJson(Path.join(workspaceDirectory, "package.json")));
33762
- if (value !== null) return value;
33763
- }
33828
+ /**
33829
+ * Walk up from `startDirectory` to the nearest ancestor that owns a
33830
+ * `package.json`, stopping at (and including) the enclosing project boundary
33831
+ * — the working tree's git root or a monorepo root. Lets a scan of a package
33832
+ * subfolder that has no `package.json` of its own adopt the nearest enclosing
33833
+ * package as its project root, so it inherits that package's dependency +
33834
+ * framework detection instead of synthesizing an empty (React-blind) project.
33835
+ *
33836
+ * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
33837
+ * most specific owning package (a leaf workspace, not just the repo root), so
33838
+ * a subfolder of one workspace no longer borrows a sibling's capabilities.
33839
+ *
33840
+ * Returns `null` when no ancestor `package.json` exists inside the project
33841
+ * boundary (a loose tree of files, or a subfolder of a package-less repo).
33842
+ */
33843
+ const findNearestAncestorPackageJson = (startDirectory) => {
33844
+ if (isProjectBoundary(startDirectory)) return null;
33845
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
33846
+ if (isFile(Path.join(directory, "package.json"))) return directory;
33847
+ if (isProjectBoundary(directory)) return null;
33764
33848
  }
33765
33849
  return null;
33766
33850
  };
33767
- const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => predicate(packageJson) ? true : null) !== null;
33768
33851
  const NAMES = new Set([
33769
33852
  "react-native",
33770
33853
  "react-native-tvos",
@@ -33797,47 +33880,7 @@ const isPackageJsonReactNativeAware = (packageJson) => {
33797
33880
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
33798
33881
  return false;
33799
33882
  };
33800
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
33801
- const getDependencySpec = (packageJson, packageName) => {
33802
- const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
33803
- return typeof spec === "string" ? spec : null;
33804
- };
33805
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
33806
- const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
33807
- const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
33808
- const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
33809
- if (version === null || !isCatalogReference(version)) return version;
33810
- const catalogName = extractCatalogName(version);
33811
- const resolvedLocalVersion = resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, catalogName);
33812
- if (resolvedLocalVersion) return resolvedLocalVersion;
33813
- const monorepoRoot = findMonorepoRoot(rootDirectory);
33814
- if (!monorepoRoot) return version;
33815
- const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
33816
- if (!isFile(monorepoPackageJsonPath)) return version;
33817
- return resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
33818
- };
33819
- const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
33820
- const getPreactVersion = (packageJson) => {
33821
- return {
33822
- ...packageJson.peerDependencies,
33823
- ...packageJson.dependencies,
33824
- ...packageJson.devDependencies
33825
- }.preact ?? null;
33826
- };
33827
- const TANSTACK_QUERY_PACKAGES = new Set([
33828
- "@tanstack/react-query",
33829
- "@tanstack/query-core",
33830
- "react-query"
33831
- ]);
33832
- const hasTanStackQuery = (packageJson) => {
33833
- const allDependencies = {
33834
- ...packageJson.peerDependencies,
33835
- ...packageJson.dependencies,
33836
- ...packageJson.devDependencies
33837
- };
33838
- return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
33839
- };
33840
- const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
33883
+ const REANIMATED_DEPENDENCY_NAME$1 = "react-native-reanimated";
33841
33884
  const isPackageJsonReanimatedAware = (packageJson) => {
33842
33885
  const allDependencies = {
33843
33886
  ...packageJson.peerDependencies,
@@ -33845,144 +33888,70 @@ const isPackageJsonReanimatedAware = (packageJson) => {
33845
33888
  ...packageJson.devDependencies,
33846
33889
  ...packageJson.optionalDependencies
33847
33890
  };
33848
- return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
33849
- };
33850
- const parseZodMajor = (zodVersion) => {
33851
- if (typeof zodVersion !== "string") return null;
33852
- return getLowestDependencyMajor(zodVersion);
33891
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME$1);
33853
33892
  };
33854
- const hasUpperBoundOnlyPeerRange = (range) => {
33855
- if (typeof range !== "string") return false;
33856
- const normalizedRange = normalizeDependencyVersion(range);
33857
- if (normalizedRange === null) return false;
33858
- return splitDependencyVersionBranches(normalizedRange).some((branch) => {
33859
- const normalizedBranch = normalizeDependencyVersion(branch);
33860
- return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
33861
- });
33862
- };
33863
- const peerRangeMinMajor = (range) => {
33864
- if (typeof range !== "string") return null;
33865
- return getLowestDependencyMajor(range);
33866
- };
33867
- const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
33868
- const installedReactMajor = parseReactMajor(reactVersion);
33869
- const peerReactRange = packageJson.peerDependencies?.react;
33870
- if (typeof peerReactRange !== "string") return installedReactMajor;
33871
- const peerFloor = peerRangeMinMajor(peerReactRange);
33872
- if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
33873
- return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
33893
+ const getWorkspacePatterns = (rootDirectory, packageJson) => {
33894
+ const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
33895
+ if (pnpmPatterns.length > 0) return pnpmPatterns;
33896
+ if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
33897
+ if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
33898
+ const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
33899
+ if (nxPatterns.length > 0) return nxPatterns;
33900
+ return [];
33874
33901
  };
33875
- const TSCONFIG_FILENAME = "tsconfig.json";
33876
- const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
33877
- const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
33878
- const resolvePackageExtendsPath = (extendsValue, fromConfigDirectory) => {
33879
- const requireFromConfig = createRequire(Path.join(fromConfigDirectory, "tsconfig.json"));
33880
- const candidates = [
33881
- extendsValue,
33882
- ensureJsonExtension(extendsValue),
33883
- `${extendsValue.replace(/\/$/, "")}/tsconfig.json`
33884
- ];
33885
- for (const candidate of candidates) try {
33886
- return requireFromConfig.resolve(candidate);
33887
- } catch {
33888
- continue;
33902
+ const parsePnpmWorkspacePatterns = (rootDirectory) => {
33903
+ const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
33904
+ if (!isFile(workspacePath)) return [];
33905
+ const content = NFS.readFileSync(workspacePath, "utf-8");
33906
+ const patterns = [];
33907
+ let isInsidePackagesBlock = false;
33908
+ for (const line of content.split("\n")) {
33909
+ const trimmed = line.trim();
33910
+ if (trimmed === "packages:") {
33911
+ isInsidePackagesBlock = true;
33912
+ continue;
33913
+ }
33914
+ if (isInsidePackagesBlock && trimmed.startsWith("-")) patterns.push(trimmed.replace(/^-\s*/, "").replace(/["']/g, ""));
33915
+ else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith("#")) isInsidePackagesBlock = false;
33889
33916
  }
33890
- return null;
33891
- };
33892
- const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
33893
- if (isRelativeExtendsValue(extendsValue)) return ensureJsonExtension(Path.resolve(fromConfigDirectory, extendsValue));
33894
- return resolvePackageExtendsPath(extendsValue, fromConfigDirectory);
33895
- };
33896
- const normalizeCompilerOptions = (compilerOptions) => {
33897
- if (!isPlainObject(compilerOptions)) return { hasExplicitLib: false };
33898
- const target = typeof compilerOptions.target === "string" ? compilerOptions.target : void 0;
33899
- const hasExplicitLib = Object.hasOwn(compilerOptions, "lib");
33900
- return {
33901
- target,
33902
- lib: Array.isArray(compilerOptions.lib) ? compilerOptions.lib.filter((entry) => typeof entry === "string") : void 0,
33903
- hasExplicitLib
33904
- };
33917
+ return patterns;
33905
33918
  };
33906
- const readTsConfig = (filePath) => {
33907
- let content;
33908
- try {
33909
- content = NFS.readFileSync(filePath, "utf-8");
33910
- } catch {
33911
- return null;
33919
+ const NX_PROJECT_DISCOVERY_DIRS = [
33920
+ "apps",
33921
+ "libs",
33922
+ "packages"
33923
+ ];
33924
+ const getNxWorkspaceDirectories = (rootDirectory) => {
33925
+ if (!isFile(Path.join(rootDirectory, "nx.json"))) return [];
33926
+ const collected = [];
33927
+ for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
33928
+ const candidatePath = Path.join(rootDirectory, candidate);
33929
+ if (!isDirectory(candidatePath)) continue;
33930
+ for (const entry of readDirectoryEntries(candidatePath)) {
33931
+ if (!entry.isDirectory()) continue;
33932
+ const projectDirectory = Path.join(candidatePath, entry.name);
33933
+ if (isFile(Path.join(projectDirectory, "project.json")) || isFile(Path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
33934
+ }
33912
33935
  }
33913
- const parsed = ts.parseConfigFileTextToJson(filePath, content);
33914
- if (!isPlainObject(parsed.config)) return null;
33915
- return {
33916
- extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
33917
- compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
33918
- };
33919
- };
33920
- const mergeCompilerOptions = (inherited, current) => {
33921
- const target = current.target ?? inherited?.target;
33922
- const hasExplicitLib = current.hasExplicitLib || Boolean(inherited?.hasExplicitLib);
33923
- return {
33924
- target,
33925
- lib: current.hasExplicitLib ? current.lib : inherited?.lib,
33926
- hasExplicitLib
33927
- };
33936
+ return collected;
33928
33937
  };
33929
- const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
33930
- const realPath = NFS.realpathSync.native(tsConfigPath);
33931
- if (visitedPaths.has(realPath)) return null;
33932
- const tsConfig = readTsConfig(realPath);
33933
- if (!tsConfig) return null;
33934
- const nextVisitedPaths = new Set(visitedPaths);
33935
- nextVisitedPaths.add(realPath);
33936
- if (tsConfig.extends && extendsDepth < 8) {
33937
- const parentPath = resolveExtendsPath(tsConfig.extends, Path.dirname(realPath));
33938
- if (parentPath && isFile(parentPath)) return mergeCompilerOptions(readResolvedCompilerOptions(parentPath, extendsDepth + 1, nextVisitedPaths), tsConfig.compilerOptions);
33938
+ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
33939
+ const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
33940
+ if (!cleanPattern.includes("*")) {
33941
+ const directoryPath = Path.join(rootDirectory, cleanPattern);
33942
+ if (isDirectory(directoryPath) && isFile(Path.join(directoryPath, "package.json"))) return [directoryPath];
33943
+ return [];
33939
33944
  }
33940
- return tsConfig.compilerOptions;
33941
- };
33942
- const targetYearIsPreES2023 = (target) => {
33943
- const year = ES_TARGET_YEAR_BY_NAME[target.toLowerCase()];
33944
- return year !== void 0 && year < 2023;
33945
- };
33946
- const libEntryIncludesES2023Array = (entry) => {
33947
- const normalizedEntry = entry.toLowerCase();
33948
- if (normalizedEntry === "esnext" || normalizedEntry === "esnext.array") return true;
33949
- const esYearMatch = /^es(\d{4})(?:\.(.+))?$/.exec(normalizedEntry);
33950
- if (!esYearMatch) return false;
33951
- if (Number(esYearMatch[1]) < 2023) return false;
33952
- const component = esYearMatch[2];
33953
- return component === void 0 || component === "array";
33954
- };
33955
- const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
33956
- const compilerOptionsArePreES2023 = (compilerOptions) => {
33957
- if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
33958
- if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
33959
- return false;
33960
- };
33961
- const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
33962
- const detectPreES2023FromConfig = (tsConfigPath) => {
33963
- const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
33964
- if (!compilerOptions) return false;
33965
- if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) return false;
33966
- return compilerOptionsArePreES2023(compilerOptions);
33967
- };
33968
- const detectPreES2023Target = (directory) => {
33969
- const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
33970
- if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
33971
- return false;
33972
- };
33973
- const REACT_DEPENDENCY_NAMES = new Set([
33974
- "react",
33975
- "react-native",
33976
- "next",
33977
- "preact"
33978
- ]);
33979
- const hasReactDependency = (packageJson) => {
33980
- const allDependencies = {
33981
- ...packageJson.peerDependencies,
33982
- ...packageJson.dependencies,
33983
- ...packageJson.devDependencies
33984
- };
33985
- return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
33945
+ const wildcardIndex = cleanPattern.indexOf("*");
33946
+ const baseDirectory = Path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
33947
+ const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
33948
+ if (!isDirectory(baseDirectory)) return [];
33949
+ const resolved = [];
33950
+ for (const entry of readDirectoryEntries(baseDirectory)) {
33951
+ const entryPath = Path.join(baseDirectory, entry.name, suffixAfterWildcard);
33952
+ if (isDirectory(entryPath) && isFile(Path.join(entryPath, "package.json"))) resolved.push(entryPath);
33953
+ }
33954
+ return resolved;
33986
33955
  };
33987
33956
  const listWorkspacePackages = (rootDirectory) => {
33988
33957
  const packageJsonPath = Path.join(rootDirectory, "package.json");
@@ -34014,6 +33983,208 @@ const listWorkspacePackages = (rootDirectory) => {
34014
33983
  }
34015
33984
  return packages;
34016
33985
  };
33986
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
33987
+ const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
33988
+ const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
33989
+ const dependencyDeclaration = getDependencyDeclaration({
33990
+ packageJson: workspacePackageJson,
33991
+ packageName,
33992
+ sections
33993
+ });
33994
+ if (!dependencyDeclaration.hasDeclaration) return null;
33995
+ return concreteVersion ?? resolveCatalogVersion(workspacePackageJson, packageName, workspaceDirectory, dependencyDeclaration.catalogReference) ?? resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, dependencyDeclaration.catalogReference);
33996
+ };
33997
+ const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
33998
+ if (!currentVersion) return true;
33999
+ const currentMajor = parseReactMajor(currentVersion);
34000
+ const nextMajor = parseReactMajor(nextVersion);
34001
+ if (currentMajor === null) return nextMajor !== null;
34002
+ if (nextMajor === null) return false;
34003
+ return nextMajor < currentMajor;
34004
+ };
34005
+ const evaluateManifestFacts = (facts, packageJson, directory) => {
34006
+ if (facts.expo.version === null) {
34007
+ const spec = getDependencySpec(packageJson, "expo");
34008
+ if (spec !== null) facts.expo = {
34009
+ version: spec,
34010
+ sourceDirectory: directory
34011
+ };
34012
+ }
34013
+ if (facts.next.version === null) {
34014
+ const spec = getDependencySpec(packageJson, "next");
34015
+ if (spec !== null) facts.next = {
34016
+ version: spec,
34017
+ sourceDirectory: directory
34018
+ };
34019
+ }
34020
+ if (facts.shopifyFlashList.version === null) {
34021
+ const spec = getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME);
34022
+ if (spec !== null) facts.shopifyFlashList = {
34023
+ version: spec,
34024
+ sourceDirectory: directory
34025
+ };
34026
+ }
34027
+ if (facts.reanimatedVersion === null) {
34028
+ const spec = getDependencySpec(packageJson, REANIMATED_DEPENDENCY_NAME);
34029
+ if (spec !== null) facts.reanimatedVersion = spec;
34030
+ }
34031
+ facts.hasReactNativeAwarePackage = facts.hasReactNativeAwarePackage || isPackageJsonReactNativeAware(packageJson);
34032
+ facts.hasReanimatedAwarePackage = facts.hasReanimatedAwarePackage || isPackageJsonReanimatedAware(packageJson);
34033
+ };
34034
+ const collectWorkspaceFacts = (rootDirectory, rootPackageJson, { collectReactGroup }) => {
34035
+ const facts = {
34036
+ reactVersion: null,
34037
+ tailwindVersion: null,
34038
+ zodVersion: null,
34039
+ framework: "unknown",
34040
+ expo: {
34041
+ version: null,
34042
+ sourceDirectory: null
34043
+ },
34044
+ next: {
34045
+ version: null,
34046
+ sourceDirectory: null
34047
+ },
34048
+ shopifyFlashList: {
34049
+ version: null,
34050
+ sourceDirectory: null
34051
+ },
34052
+ hasReactNativeAwarePackage: false,
34053
+ hasReanimatedAwarePackage: false,
34054
+ reanimatedVersion: null
34055
+ };
34056
+ evaluateManifestFacts(facts, rootPackageJson, rootDirectory);
34057
+ let isReactGroupSettled = !collectReactGroup;
34058
+ const visitedDirectories = /* @__PURE__ */ new Set();
34059
+ for (const pattern of getWorkspacePatterns(rootDirectory, rootPackageJson)) {
34060
+ const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
34061
+ for (const workspaceDirectory of directories) {
34062
+ if (visitedDirectories.has(workspaceDirectory)) continue;
34063
+ visitedDirectories.add(workspaceDirectory);
34064
+ const workspacePackageJson = readPackageJson(Path.join(workspaceDirectory, "package.json"));
34065
+ evaluateManifestFacts(facts, workspacePackageJson, workspaceDirectory);
34066
+ const info = extractDependencyInfo(workspacePackageJson);
34067
+ if (info.framework !== "unknown" && frameworkMergeRank(info.framework) < frameworkMergeRank(facts.framework)) facts.framework = info.framework;
34068
+ if (isReactGroupSettled) continue;
34069
+ const reactVersion = resolveWorkspaceDependencyVersion({
34070
+ concreteVersion: info.reactVersion,
34071
+ packageName: "react",
34072
+ rootDirectory,
34073
+ rootPackageJson,
34074
+ sections: REACT_SECTIONS,
34075
+ workspaceDirectory,
34076
+ workspacePackageJson
34077
+ });
34078
+ const tailwindVersion = resolveWorkspaceDependencyVersion({
34079
+ concreteVersion: info.tailwindVersion,
34080
+ packageName: "tailwindcss",
34081
+ rootDirectory,
34082
+ rootPackageJson,
34083
+ sections: TAILWIND_ZOD_SECTIONS,
34084
+ workspaceDirectory,
34085
+ workspacePackageJson
34086
+ });
34087
+ const zodVersion = resolveWorkspaceDependencyVersion({
34088
+ concreteVersion: info.zodVersion,
34089
+ packageName: "zod",
34090
+ rootDirectory,
34091
+ rootPackageJson,
34092
+ sections: TAILWIND_ZOD_SECTIONS,
34093
+ workspaceDirectory,
34094
+ workspacePackageJson
34095
+ });
34096
+ if (reactVersion && shouldReplaceReactVersion(facts.reactVersion, reactVersion)) facts.reactVersion = reactVersion;
34097
+ if (tailwindVersion && !facts.tailwindVersion) facts.tailwindVersion = tailwindVersion;
34098
+ if (zodVersion && !facts.zodVersion) facts.zodVersion = zodVersion;
34099
+ const settledReactMajor = parseReactMajor(facts.reactVersion);
34100
+ isReactGroupSettled = Boolean(facts.reactVersion && facts.tailwindVersion && facts.framework !== "unknown" && settledReactMajor !== null && settledReactMajor <= 17);
34101
+ }
34102
+ }
34103
+ return facts;
34104
+ };
34105
+ const findDependencyInfoFromMonorepoRoot = (directory) => {
34106
+ const monorepoRoot = findMonorepoRoot(directory);
34107
+ if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
34108
+ const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
34109
+ if (!isFile(monorepoPackageJsonPath)) return EMPTY_DEPENDENCY_INFO;
34110
+ const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
34111
+ const rootInfo = extractDependencyInfo(rootPackageJson);
34112
+ const leafPackageJsonPath = Path.join(directory, "package.json");
34113
+ const leafPackageJson = isFile(leafPackageJsonPath) ? readPackageJson(leafPackageJsonPath) : null;
34114
+ const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
34115
+ packageJson: leafPackageJson,
34116
+ packageName: "react",
34117
+ sections: REACT_SECTIONS
34118
+ }) : null;
34119
+ const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
34120
+ packageJson: leafPackageJson,
34121
+ packageName: "tailwindcss",
34122
+ sections: TAILWIND_ZOD_SECTIONS
34123
+ }) : null;
34124
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
34125
+ packageJson: leafPackageJson,
34126
+ packageName: "zod",
34127
+ sections: TAILWIND_ZOD_SECTIONS
34128
+ }) : null;
34129
+ const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
34130
+ const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
34131
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
34132
+ const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
34133
+ const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
34134
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
34135
+ const workspaceFacts = collectWorkspaceFacts(monorepoRoot, rootPackageJson, { collectReactGroup: true });
34136
+ return {
34137
+ reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceFacts.reactVersion : rootInfo.reactVersion ?? workspaceFacts.reactVersion,
34138
+ tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceFacts.tailwindVersion : null,
34139
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceFacts.zodVersion : null,
34140
+ framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceFacts.framework
34141
+ };
34142
+ };
34143
+ /**
34144
+ * Last-resort React detection: locate React the way Node's `node_modules`
34145
+ * resolution would from `directory` — check each `node_modules/react` up the
34146
+ * tree — but bounded to the enclosing repo so a globally installed React can't
34147
+ * masquerade as the project's version. Stops at (and including) the nearest
34148
+ * project boundary — the git root or a monorepo root — and returns the first
34149
+ * installed `version`.
34150
+ *
34151
+ * Makes "React is installed and importable" ⇒ "React is detected" an invariant
34152
+ * for packages whose only React declaration is a version-less spec
34153
+ * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
34154
+ * `node_modules` the declaration walks never reach — the profile of a component
34155
+ * package inside a monorepo. Matching the `node_modules/react` entry rather than
34156
+ * its realpath means a project whose `node_modules` is symlinked elsewhere (a
34157
+ * Docker volume, a shared store) still counts, while a global install outside
34158
+ * the repo does not.
34159
+ *
34160
+ * Returns `null` when React isn't installed within the repo or its package.json
34161
+ * carries no version string. A tree with no boundary marker at all bounds the
34162
+ * search to `directory`, so a dependency hoisted above the scanned package there
34163
+ * is conservatively not adopted.
34164
+ */
34165
+ /**
34166
+ * The nearest enclosing project boundary (git root or monorepo root), or
34167
+ * `directory` itself when the scan target sits outside any repo. Bounds the
34168
+ * node_modules walk so a React hoisted anywhere inside the repo counts, but a
34169
+ * React in an ancestor above the repo — a home-directory or global
34170
+ * `node_modules` — can't leak in.
34171
+ */
34172
+ const findContainmentRoot = (directory) => {
34173
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
34174
+ return directory;
34175
+ };
34176
+ const resolveInstalledReactVersion = (directory) => {
34177
+ const containmentRoot = findContainmentRoot(directory);
34178
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
34179
+ const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
34180
+ if (isFile(reactPackageJsonPath)) {
34181
+ const installedVersion = readPackageJson(reactPackageJsonPath).version;
34182
+ return typeof installedVersion === "string" ? installedVersion : null;
34183
+ }
34184
+ if (ancestorDirectory === containmentRoot) return null;
34185
+ }
34186
+ return null;
34187
+ };
34017
34188
  const toReactWorkspacePackages = (directories) => {
34018
34189
  const packages = [];
34019
34190
  for (const directory of directories) {
@@ -34097,8 +34268,8 @@ const clearProjectCache = () => {
34097
34268
  const discoverProjectWithoutPackageJson = (directory) => {
34098
34269
  const sourceFileCount = countSourceFiles(directory);
34099
34270
  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;
34271
+ const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
34272
+ const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
34102
34273
  if (enclosingProject !== null) return {
34103
34274
  ...enclosingProject,
34104
34275
  rootDirectory: directory,
@@ -34128,7 +34299,9 @@ const discoverProjectWithoutPackageJson = (directory) => {
34128
34299
  shopifyFlashListVersion: null,
34129
34300
  shopifyFlashListMajorVersion: null,
34130
34301
  hasReanimated: false,
34302
+ reanimatedVersion: null,
34131
34303
  isPreES2023Target: hasOwnTsConfig && detectPreES2023Target(directory),
34304
+ isStaticExport: false,
34132
34305
  sourceFileCount
34133
34306
  };
34134
34307
  };
@@ -34142,88 +34315,85 @@ const discoverProject = (directory) => {
34142
34315
  return synthesized;
34143
34316
  }
34144
34317
  const packageJson = readPackageJson(packageJsonPath);
34145
- let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
34146
- const reactDeclaration = getDependencyDeclaration({
34147
- packageJson,
34148
- packageName: "react",
34149
- sections: [
34150
- "dependencies",
34151
- "peerDependencies",
34152
- "devDependencies"
34153
- ]
34154
- });
34155
- const tailwindDeclaration = getDependencyDeclaration({
34156
- packageJson,
34157
- packageName: "tailwindcss",
34158
- sections: [
34159
- "dependencies",
34160
- "devDependencies",
34161
- "peerDependencies"
34162
- ]
34163
- });
34164
- const zodDeclaration = getDependencyDeclaration({
34318
+ const rootInfo = extractDependencyInfo(packageJson);
34319
+ let framework = rootInfo.framework;
34320
+ const tracked = {
34321
+ react: {
34322
+ version: rootInfo.reactVersion,
34323
+ sections: REACT_SECTIONS
34324
+ },
34325
+ tailwindcss: {
34326
+ version: rootInfo.tailwindVersion,
34327
+ sections: TAILWIND_ZOD_SECTIONS
34328
+ },
34329
+ zod: {
34330
+ version: rootInfo.zodVersion,
34331
+ sections: TAILWIND_ZOD_SECTIONS
34332
+ }
34333
+ };
34334
+ const declarations = Object.fromEntries(Object.entries(tracked).map(([packageName, entry]) => [packageName, getDependencyDeclaration({
34165
34335
  packageJson,
34166
- packageName: "zod",
34167
- sections: [
34168
- "dependencies",
34169
- "devDependencies",
34170
- "peerDependencies"
34171
- ]
34172
- });
34173
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
34174
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
34175
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
34176
- if (!reactVersion || !tailwindVersion || !zodVersion) {
34336
+ packageName,
34337
+ sections: entry.sections
34338
+ })]));
34339
+ const fillFromCatalogs = (source, sourceDirectory) => {
34340
+ for (const [packageName, entry] of Object.entries(tracked)) if (!entry.version && declarations[packageName].hasDeclaration) entry.version = resolveCatalogVersion(source, packageName, sourceDirectory, declarations[packageName].catalogReference);
34341
+ };
34342
+ fillFromCatalogs(packageJson, directory);
34343
+ if (!tracked.react.version || !tracked.tailwindcss.version || !tracked.zod.version) {
34177
34344
  const monorepoRoot = findMonorepoRoot(directory);
34178
34345
  if (monorepoRoot) {
34179
34346
  const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
34180
- if (isFile(monorepoPackageJsonPath)) {
34181
- const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
34182
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
34183
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
34184
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
34185
- }
34347
+ if (isFile(monorepoPackageJsonPath)) fillFromCatalogs(readPackageJson(monorepoPackageJsonPath), monorepoRoot);
34186
34348
  }
34187
34349
  }
34188
- if (!reactVersion || framework === "unknown") {
34189
- const workspaceInfo = findReactInWorkspaces(directory, packageJson);
34190
- if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
34191
- if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
34192
- if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
34193
- if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
34350
+ const shouldCollectReactGroup = !tracked.react.version || framework === "unknown";
34351
+ const workspaceFacts = collectWorkspaceFacts(directory, packageJson, { collectReactGroup: shouldCollectReactGroup });
34352
+ if (shouldCollectReactGroup) {
34353
+ tracked.react.version ||= workspaceFacts.reactVersion;
34354
+ tracked.tailwindcss.version ||= workspaceFacts.tailwindVersion;
34355
+ tracked.zod.version ||= workspaceFacts.zodVersion;
34356
+ if (framework === "unknown" && workspaceFacts.framework !== "unknown") framework = workspaceFacts.framework;
34194
34357
  }
34195
- if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
34358
+ if ((!tracked.react.version || framework === "unknown") && !isMonorepoRoot(directory)) {
34196
34359
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
34197
- if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
34198
- if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
34199
- if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
34360
+ tracked.react.version ||= monorepoInfo.reactVersion;
34361
+ tracked.tailwindcss.version ||= monorepoInfo.tailwindVersion;
34362
+ tracked.zod.version ||= monorepoInfo.zodVersion;
34200
34363
  if (framework === "unknown") framework = monorepoInfo.framework;
34201
34364
  }
34202
- if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
34203
- if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
34204
- if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
34365
+ for (const [packageName, entry] of Object.entries(tracked)) {
34366
+ const declaredVersion = declarations[packageName].version;
34367
+ if (!entry.version && declaredVersion && !isCatalogReference(declaredVersion)) entry.version = declaredVersion;
34368
+ }
34369
+ const { react, tailwindcss, zod } = tracked;
34370
+ let reactVersion = react.version;
34371
+ if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
34372
+ const tailwindVersion = tailwindcss.version;
34373
+ const zodVersion = zod.version;
34205
34374
  const projectName = packageJson.name ?? Path.basename(directory);
34206
34375
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
34207
34376
  const sourceFileCount = countSourceFiles(directory);
34208
- const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
34377
+ const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
34209
34378
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
34210
34379
  rootDirectory: directory,
34211
34380
  rootPackageJson: packageJson,
34212
34381
  packageName: "expo",
34213
- version: findExpoVersion(directory, packageJson)
34382
+ version: workspaceFacts.expo.version
34214
34383
  }) : null;
34215
34384
  const shopifyFlashListVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
34216
34385
  rootDirectory: directory,
34217
34386
  rootPackageJson: packageJson,
34218
34387
  packageName: SHOPIFY_FLASH_LIST_PACKAGE_NAME,
34219
- version: findShopifyFlashListVersion(directory, packageJson)
34388
+ version: workspaceFacts.shopifyFlashList.version
34220
34389
  }) : null;
34221
- const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
34390
+ const hasReanimated = hasReactNativeWorkspace && workspaceFacts.hasReanimatedAwarePackage;
34391
+ const reanimatedVersion = hasReanimated ? workspaceFacts.reanimatedVersion : null;
34222
34392
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
34223
34393
  rootDirectory: directory,
34224
34394
  rootPackageJson: packageJson,
34225
34395
  packageName: "next",
34226
- version: findNextjsVersion(directory, packageJson)
34396
+ version: workspaceFacts.next.version
34227
34397
  }) : null;
34228
34398
  const preactVersion = getPreactVersion(packageJson);
34229
34399
  const isPreES2023Target = hasTypeScript && detectPreES2023Target(directory);
@@ -34234,7 +34404,7 @@ const discoverProject = (directory) => {
34234
34404
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
34235
34405
  tailwindVersion,
34236
34406
  zodVersion,
34237
- zodMajorVersion: parseZodMajor(zodVersion),
34407
+ zodMajorVersion: zodVersion === null ? null : getLowestDependencyMajor(zodVersion),
34238
34408
  framework,
34239
34409
  hasTypeScript,
34240
34410
  hasReactCompiler: detectReactCompiler(directory, packageJson),
@@ -34248,7 +34418,9 @@ const discoverProject = (directory) => {
34248
34418
  shopifyFlashListVersion,
34249
34419
  shopifyFlashListMajorVersion: shopifyFlashListVersion === null ? null : getLowestDependencyMajor(shopifyFlashListVersion),
34250
34420
  hasReanimated,
34421
+ reanimatedVersion,
34251
34422
  isPreES2023Target,
34423
+ isStaticExport: framework === "nextjs" && detectNextjsStaticExport(workspaceFacts.next.sourceDirectory ?? directory),
34252
34424
  sourceFileCount
34253
34425
  };
34254
34426
  cachedProjectInfos.set(directory, projectInfo);
@@ -34264,57 +34436,6 @@ const discoverProject = (directory) => {
34264
34436
  * single source file has nothing to analyze.
34265
34437
  */
34266
34438
  const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null || project.sourceFileCount > 0;
34267
- const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
34268
- const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
34269
- const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
34270
- const parseReactMajorMinor = (reactVersion) => {
34271
- if (typeof reactVersion !== "string") return null;
34272
- const trimmed = reactVersion.trim();
34273
- if (trimmed.length === 0) return null;
34274
- const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
34275
- if (lowerBoundsOnly.length === 0) return null;
34276
- const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
34277
- if (majorMinorMatch) {
34278
- const major = Number.parseInt(majorMinorMatch[1], 10);
34279
- const minor = Number.parseInt(majorMinorMatch[2], 10);
34280
- if (!Number.isFinite(major) || major <= 0) return null;
34281
- if (!Number.isFinite(minor) || minor < 0) return null;
34282
- return {
34283
- major,
34284
- minor
34285
- };
34286
- }
34287
- const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
34288
- if (!majorOnlyMatch) return null;
34289
- const major = Number.parseInt(majorOnlyMatch[1], 10);
34290
- if (!Number.isFinite(major) || major <= 0) return null;
34291
- return {
34292
- major,
34293
- minor: 0
34294
- };
34295
- };
34296
- const isReactAtLeast = (detected, required) => {
34297
- if (detected === null) return true;
34298
- if (detected.major !== required.major) return detected.major > required.major;
34299
- return detected.minor >= required.minor;
34300
- };
34301
- const parseLowerBoundVersion = (versionSpec) => import_semver.validRange(versionSpec) !== null ? import_semver.minVersion(versionSpec) : import_semver.coerce(versionSpec);
34302
- const parseTailwindMajorMinor = (tailwindVersion) => {
34303
- if (typeof tailwindVersion !== "string") return null;
34304
- const trimmed = tailwindVersion.trim();
34305
- if (trimmed.length === 0) return null;
34306
- const lowerBound = parseLowerBoundVersion(trimmed);
34307
- if (lowerBound === null || lowerBound.major <= 0) return null;
34308
- return {
34309
- major: lowerBound.major,
34310
- minor: lowerBound.minor
34311
- };
34312
- };
34313
- const isTailwindAtLeast = (detected, required) => {
34314
- if (detected === null) return true;
34315
- if (detected.major !== required.major) return detected.major > required.major;
34316
- return detected.minor >= required.minor;
34317
- };
34318
34439
  const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
34319
34440
  var InvalidGlobPatternError = class extends Error {
34320
34441
  pattern;
@@ -34440,6 +34561,9 @@ const buildRuleSeverityControls = (config) => {
34440
34561
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
34441
34562
  };
34442
34563
  };
34564
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34565
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34566
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34443
34567
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34444
34568
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
34445
34569
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -34517,6 +34641,7 @@ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34517
34641
  "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
34518
34642
  "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
34519
34643
  "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
34644
+ "react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
34520
34645
  "react/jsx-no-undef": "react-doctor/jsx-no-undef",
34521
34646
  "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
34522
34647
  "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
@@ -34590,9 +34715,6 @@ const tokenizeRuleList = (ruleList) => {
34590
34715
  if (!ruleSection) return [];
34591
34716
  return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
34592
34717
  };
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
34718
  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
34719
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34598
34720
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
@@ -34646,6 +34768,53 @@ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34646
34768
  if (!ruleId.startsWith("react-doctor/")) return null;
34647
34769
  return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
34648
34770
  };
34771
+ const ESLINT_REACT_HOOKS_PLUGIN_PREFIX = "react-hooks/";
34772
+ const buildAcceptedTokens = (ruleId) => {
34773
+ const shortRuleId = ruleId.slice(15);
34774
+ return new Set([ruleId, `${ESLINT_REACT_HOOKS_PLUGIN_PREFIX}${shortRuleId}`]);
34775
+ };
34776
+ const listNamesRule = (ruleList, acceptedTokens) => tokenizeRuleList(ruleList).some((token) => acceptedTokens.has(token));
34777
+ const isInlineDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
34778
+ const candidates = [{
34779
+ line: lines[diagnosticLineIndex],
34780
+ requiredScope: "line"
34781
+ }, {
34782
+ line: lines[diagnosticLineIndex - 1],
34783
+ requiredScope: "next-line"
34784
+ }];
34785
+ for (const { line, requiredScope } of candidates) {
34786
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
34787
+ if (!match) continue;
34788
+ const [, , scope, ruleList] = match;
34789
+ if (scope !== requiredScope) continue;
34790
+ if (listNamesRule(ruleList, acceptedTokens)) return true;
34791
+ }
34792
+ return false;
34793
+ };
34794
+ const isBlockDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
34795
+ let isRangeOpen = false;
34796
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
34797
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
34798
+ const line = lines[lineIndex];
34799
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
34800
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
34801
+ if (disableMatch && listNamesRule(disableMatch[2], acceptedTokens)) {
34802
+ isRangeOpen = true;
34803
+ continue;
34804
+ }
34805
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
34806
+ if (enableMatch) {
34807
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
34808
+ if (enabledRules.length === 0 || enabledRules.some((rule) => acceptedTokens.has(rule))) isRangeOpen = false;
34809
+ }
34810
+ }
34811
+ return isRangeOpen;
34812
+ };
34813
+ const isCompilerRuleForeignDisabled = (lines, diagnosticLineIndex, ruleId) => {
34814
+ if (!ruleId.startsWith("react-hooks-js/")) return false;
34815
+ const acceptedTokens = buildAcceptedTokens(ruleId);
34816
+ return isInlineDisabled(lines, diagnosticLineIndex, acceptedTokens) || isBlockDisabled(lines, diagnosticLineIndex, acceptedTokens);
34817
+ };
34649
34818
  const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34650
34819
  const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34651
34820
  const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
@@ -34789,6 +34958,11 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
34789
34958
  isSuppressed: true,
34790
34959
  nearMissHint: null
34791
34960
  };
34961
+ if (isCompilerRuleForeignDisabled(lines, diagnosticLineIndex, ruleId)) return {
34962
+ isSuppressed: true,
34963
+ nearMissHint: null,
34964
+ isForeignDirective: true
34965
+ };
34792
34966
  const directComments = findStackedDisableCommentsAbove(lines, diagnosticLineIndex);
34793
34967
  if (hasChainSuppressor(directComments, ruleId)) return {
34794
34968
  isSuppressed: true,
@@ -35074,12 +35248,18 @@ const collectStringSet = (values) => {
35074
35248
  return new Set(values.filter((value) => typeof value === "string"));
35075
35249
  };
35076
35250
  const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
35251
+ const NODE_CLI_PATH_SEGMENTS = new Set([
35252
+ "scripts",
35253
+ "bin",
35254
+ "tools"
35255
+ ]);
35256
+ const hasNodeCliPathSegment = (filePath) => filePath.split(/[\\/]/).slice(0, -1).some((segment) => NODE_CLI_PATH_SEGMENTS.has(segment));
35077
35257
  /**
35078
35258
  * Pre-compiles every stateful filter and returns a single
35079
35259
  * `apply(diagnostic)` closure that runs (in order):
35080
35260
  *
35081
35261
  * 1. auto-suppress (test-noise rules in test files; `migration-hint`
35082
- * wins over `test-noise`)
35262
+ * wins over `test-noise`; `no-console` on Node CLI scripts)
35083
35263
  * 2. severity overrides (top-level `rules` / `categories`, with
35084
35264
  * `"off"` dropping)
35085
35265
  * 3. warning suppression (only when `showWarnings` is false: drops every
@@ -35177,6 +35357,12 @@ const buildDiagnosticPipeline = (input) => {
35177
35357
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35178
35358
  return false;
35179
35359
  };
35360
+ const isNoConsoleOnNodeCliScript = (diagnostic) => {
35361
+ if (diagnostic.rule !== "no-console") return false;
35362
+ if (hasNodeCliPathSegment(diagnostic.filePath)) return true;
35363
+ const lines = getFileLines(diagnostic.filePath);
35364
+ return lines !== null && Boolean(lines[0]?.startsWith("#!"));
35365
+ };
35180
35366
  const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35181
35367
  if (!hasRuntimeGlobals) return false;
35182
35368
  if (diagnostic.rule !== "jsx-no-undef") return false;
@@ -35186,6 +35372,7 @@ const buildDiagnosticPipeline = (input) => {
35186
35372
  return {
35187
35373
  apply: (diagnostic) => {
35188
35374
  if (shouldAutoSuppress(diagnostic)) return null;
35375
+ if (isNoConsoleOnNodeCliScript(diagnostic)) return null;
35189
35376
  let current = diagnostic;
35190
35377
  let explicitSeverityOverride;
35191
35378
  let explicitRuleOverride;
@@ -35215,7 +35402,7 @@ const buildDiagnosticPipeline = (input) => {
35215
35402
  if (lines) {
35216
35403
  const ruleIdentifier = `${current.plugin}/${current.rule}`;
35217
35404
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35218
- if (evaluation.isSuppressed) return suppress(current, "inline");
35405
+ if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
35219
35406
  if (evaluation.nearMissHint) current = {
35220
35407
  ...current,
35221
35408
  suppressionHint: evaluation.nearMissHint
@@ -37024,64 +37211,64 @@ function* collectSecurityScanFiles(rootDirectory) {
37024
37211
  }
37025
37212
  }
37026
37213
  }
37027
- /**
37028
- * Whether project discovery resolved a React-compatible runtime (React or
37029
- * Preact, which ships the same hooks + JSX model). This is the same
37030
- * predicate that grants the `react` capability, so `false` means every
37031
- * React-runtime rule family was gated off for the project.
37032
- */
37033
- const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
37214
+ const CLIENT_ONLY_FRAMEWORKS = new Set([
37215
+ "vite",
37216
+ "cra",
37217
+ "gatsby",
37218
+ "react-native",
37219
+ "expo"
37220
+ ]);
37221
+ const addMajorLadder = (capabilities, name, major, earliest, latest) => {
37222
+ if (major === null) return;
37223
+ const cappedMajor = Math.min(major, latest);
37224
+ for (let candidate = earliest; candidate <= cappedMajor; candidate += 1) capabilities.add(`${name}:${candidate}`);
37225
+ };
37034
37226
  const buildCapabilities = (project) => {
37035
37227
  const capabilities = /* @__PURE__ */ new Set();
37036
37228
  capabilities.add(project.framework);
37037
- if (hasReactRuntime(project)) capabilities.add("react");
37229
+ if (project.reactVersion !== null || project.preactVersion !== null) capabilities.add("react");
37038
37230
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
37039
37231
  if (project.expoVersion !== null) capabilities.add("expo");
37232
+ if ((project.framework === "nextjs" || project.framework === "tanstack-start" || project.framework === "remix") && !project.isStaticExport) capabilities.add("server-actions");
37233
+ if (CLIENT_ONLY_FRAMEWORKS.has(project.framework)) capabilities.add("client-only");
37234
+ if (project.isStaticExport) capabilities.add("nextjs:static-export");
37040
37235
  if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
37041
- const reactMajor = project.reactMajorVersion;
37042
- if (reactMajor !== null) {
37043
- const cappedReactMajor = Math.min(reactMajor, 30);
37044
- for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
37045
- if (reactMajor >= 19) {
37046
- if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
37047
- major: 19,
37048
- minor: 2
37049
- })) capabilities.add("react:19.2");
37050
- }
37051
- }
37052
- if (project.tailwindVersion !== null) {
37053
- capabilities.add("tailwind");
37054
- if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
37055
- major: 3,
37056
- minor: 4
37057
- })) capabilities.add("tailwind:3.4");
37058
- }
37059
- if (project.zodVersion !== null) {
37060
- capabilities.add("zod");
37061
- if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
37062
- }
37236
+ addMajorLadder(capabilities, "react", project.reactMajorVersion, 17, 30);
37237
+ if (project.reactMajorVersion !== null && project.reactMajorVersion >= 19 && isMajorMinorAtLeast(parseReactMajorMinor(project.reactVersion), {
37238
+ major: 19,
37239
+ minor: 2
37240
+ })) capabilities.add("react:19.2");
37241
+ if (project.tailwindVersion !== null) capabilities.add("tailwind");
37242
+ if (project.tailwindVersion !== null && isMajorMinorAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
37243
+ major: 3,
37244
+ minor: 4
37245
+ })) capabilities.add("tailwind:3.4");
37246
+ if (project.zodVersion !== null) capabilities.add("zod");
37247
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
37063
37248
  if (project.isPreES2023Target) capabilities.add("pre-es2023");
37064
37249
  if (project.hasReactCompiler) capabilities.add("react-compiler");
37065
37250
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
37066
37251
  if (project.hasTypeScript) capabilities.add("typescript");
37067
- if (project.preactVersion !== null) {
37068
- capabilities.add("preact");
37069
- const preactMajor = project.preactMajorVersion;
37070
- if (preactMajor !== null) {
37071
- const cappedPreactMajor = Math.min(preactMajor, 20);
37072
- for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
37073
- }
37074
- if (project.reactVersion === null) capabilities.add("pure-preact");
37075
- }
37252
+ if (project.preactVersion !== null) capabilities.add("preact");
37253
+ addMajorLadder(capabilities, "preact", project.preactMajorVersion, 10, 20);
37254
+ if (project.preactVersion !== null && project.reactVersion === null) capabilities.add("pure-preact");
37255
+ return capabilities;
37256
+ };
37257
+ const capabilitiesByProject = /* @__PURE__ */ new WeakMap();
37258
+ const getCapabilities = (project) => {
37259
+ const cached = capabilitiesByProject.get(project);
37260
+ if (cached !== void 0) return cached;
37261
+ const capabilities = buildCapabilities(project);
37262
+ capabilitiesByProject.set(project, capabilities);
37076
37263
  return capabilities;
37077
37264
  };
37078
- const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
37265
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhen) => {
37079
37266
  if (requires) {
37080
37267
  for (const capability of requires) if (!capabilities.has(capability)) return false;
37081
37268
  }
37082
37269
  if (tags?.includes("react-jsx-only") && !capabilities.has("react")) return false;
37083
- if (disabledBy) {
37084
- for (const capability of disabledBy) if (capabilities.has(capability)) return false;
37270
+ if (disabledWhen) {
37271
+ for (const capability of disabledWhen) if (capabilities.has(capability)) return false;
37085
37272
  }
37086
37273
  if (tags) {
37087
37274
  for (const tag of tags) if (ignoredTags.has(tag)) return false;
@@ -37092,14 +37279,14 @@ const yieldToEventLoop = () => new Promise((resolve) => {
37092
37279
  setImmediate(resolve);
37093
37280
  });
37094
37281
  const createSecurityScanSession = (rootDirectory, options) => {
37095
- const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
37282
+ const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
37096
37283
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
37097
37284
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
37098
37285
  const rule = entry.rule;
37099
37286
  const scan = rule.scan;
37100
37287
  if (typeof scan !== "function") return [];
37101
37288
  if (rule.defaultEnabled === false) return [];
37102
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) return [];
37289
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) return [];
37103
37290
  return [{
37104
37291
  entry,
37105
37292
  scan,
@@ -37119,6 +37306,7 @@ const createSecurityScanSession = (rootDirectory, options) => {
37119
37306
  return status === true;
37120
37307
  };
37121
37308
  const scanFileByRule = function* (file) {
37309
+ if (file === null) return;
37122
37310
  for (const { entry, scan, committedFilesOnly } of enabledScanRules) {
37123
37311
  for (const finding of scan(file)) {
37124
37312
  if (committedFilesOnly && isFileGitIgnored(file)) continue;
@@ -38564,7 +38752,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38564
38752
  })),
38565
38753
  showStagedContent: (directory, relativePath, options) => runCommand({
38566
38754
  command: "git",
38567
- args: ["show", `:${relativePath}`],
38755
+ args: ["show", `:./${relativePath}`],
38568
38756
  directory,
38569
38757
  maxStdoutBytes: options?.maxBufferBytes
38570
38758
  }).pipe(map$3((result) => result.status === 0 ? result.stdout : null)),
@@ -38999,7 +39187,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38999
39187
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
39000
39188
  const jsPlugins = [];
39001
39189
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
39002
- const capabilities = buildCapabilities(project);
39190
+ const capabilities = getCapabilities(project);
39003
39191
  const enabledReactDoctorRules = {};
39004
39192
  for (const registryEntry of REACT_DOCTOR_RULES) {
39005
39193
  const rule = reactDoctorPlugin.rules[registryEntry.id];
@@ -39010,7 +39198,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
39010
39198
  if (rule.scan !== void 0) continue;
39011
39199
  if (customRulesOnly && registryEntry.originallyExternal) continue;
39012
39200
  if (rule.framework !== "global" && !rule.requires) continue;
39013
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
39201
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) continue;
39014
39202
  const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
39015
39203
  if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
39016
39204
  const severity = resolveRuleSeverityOverride({
@@ -39041,6 +39229,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
39041
39229
  settings: { "react-doctor": {
39042
39230
  framework: project.framework,
39043
39231
  rootDirectory: resolveSettingsRootDirectory(project.rootDirectory),
39232
+ capabilities: [...capabilities].sort(),
39044
39233
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
39045
39234
  ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
39046
39235
  } },
@@ -39331,25 +39520,18 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39331
39520
  if (errors.length > 0) throw errors[0];
39332
39521
  return results;
39333
39522
  };
39334
- const getPublicEnvPrefix = (framework) => {
39335
- switch (framework) {
39336
- case "nextjs": return "NEXT_PUBLIC_*";
39337
- case "vite":
39338
- case "tanstack-start": return "VITE_*";
39339
- case "cra": return "REACT_APP_*";
39340
- case "gatsby": return "GATSBY_*";
39341
- default: return null;
39342
- }
39343
- };
39344
- const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
39345
- const publicEnvPrefix = getPublicEnvPrefix(project.framework);
39346
- if (!publicEnvPrefix) return fallbackRecommendation;
39347
- 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
- };
39349
39523
  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";
39524
+ const ACCESSORS_MIN_VERSION = "3.15.0";
39525
+ const supportsAccessors = (reanimatedVersion) => {
39526
+ if (reanimatedVersion === null) return true;
39527
+ const lowerBound = import_semver.validRange(reanimatedVersion) !== null ? import_semver.minVersion(reanimatedVersion) : import_semver.coerce(reanimatedVersion);
39528
+ if (lowerBound === null) return true;
39529
+ return import_semver.gte(lowerBound, ACCESSORS_MIN_VERSION);
39530
+ };
39350
39531
  const appendReanimatedSharedValueHint = (help, rule, project) => {
39351
39532
  if (rule !== "immutability") return help;
39352
39533
  if (!project.hasReanimated) return help;
39534
+ if (!supportsAccessors(project.reanimatedVersion)) return help;
39353
39535
  if (!help) return REANIMATED_SHARED_VALUE_HINT;
39354
39536
  return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
39355
39537
  };
@@ -39511,13 +39693,13 @@ const REACT_USE_BINDING_RESOLUTION = {
39511
39693
  isReactUseBinding: true,
39512
39694
  isReactNamespaceBinding: false
39513
39695
  };
39514
- const getScriptKind = (filename) => {
39696
+ const getScriptKind$2 = (filename) => {
39515
39697
  if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
39516
39698
  if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
39517
39699
  if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
39518
39700
  return ts.ScriptKind.JS;
39519
39701
  };
39520
- const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39702
+ const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39521
39703
  const unwrapExpression = (expression) => {
39522
39704
  let currentExpression = expression;
39523
39705
  while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
@@ -39766,8 +39948,8 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
39766
39948
  return matchedIdentifier;
39767
39949
  };
39768
39950
  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));
39951
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
39952
+ const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
39771
39953
  if (!useIdentifier) return null;
39772
39954
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
39773
39955
  };
@@ -39788,6 +39970,201 @@ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
39788
39970
  const bindingResolution = resolveUseCallBinding(sourceText, absolutePath, primaryLabel.span.offset);
39789
39971
  return bindingResolution !== null && !bindingResolution.isReactUseBinding;
39790
39972
  };
39973
+ const SUPPRESSED_COMPILER_CODES = new Set(["react-hooks-js(immutability)", "react-hooks-js(refs)"]);
39974
+ const WORKLET_DIRECTIVE = "worklet";
39975
+ const WORKLET_ACCEPTING_CALLEES = new Set([
39976
+ "useAnimatedStyle",
39977
+ "useAnimatedProps",
39978
+ "useDerivedValue",
39979
+ "useAnimatedReaction",
39980
+ "useAnimatedScrollHandler",
39981
+ "useAnimatedGestureHandler",
39982
+ "useFrameCallback",
39983
+ "runOnUI",
39984
+ "executeOnUIRuntimeSync"
39985
+ ]);
39986
+ const GESTURE_ROOT_IDENTIFIER = "Gesture";
39987
+ const getScriptKind$1 = (filename) => {
39988
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
39989
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
39990
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
39991
+ return ts.ScriptKind.JS;
39992
+ };
39993
+ const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39994
+ const hasWorkletDirective = (node) => {
39995
+ if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
39996
+ const body = node.body;
39997
+ if (!body || !ts.isBlock(body)) return false;
39998
+ for (const statement of body.statements) {
39999
+ if (!ts.isExpressionStatement(statement)) return false;
40000
+ if (!ts.isStringLiteral(statement.expression)) return false;
40001
+ if (statement.expression.text === WORKLET_DIRECTIVE) return true;
40002
+ }
40003
+ return false;
40004
+ };
40005
+ const rootIdentifierOfCallChain = (expression) => {
40006
+ let currentExpression = expression;
40007
+ while (true) {
40008
+ if (ts.isIdentifier(currentExpression)) return currentExpression.text;
40009
+ if (ts.isPropertyAccessExpression(currentExpression)) {
40010
+ currentExpression = currentExpression.expression;
40011
+ continue;
40012
+ }
40013
+ if (ts.isCallExpression(currentExpression)) {
40014
+ currentExpression = currentExpression.expression;
40015
+ continue;
40016
+ }
40017
+ return null;
40018
+ }
40019
+ };
40020
+ const isWorkletAcceptingCall = (callExpression) => {
40021
+ const callee = callExpression.expression;
40022
+ if (ts.isIdentifier(callee)) return WORKLET_ACCEPTING_CALLEES.has(callee.text);
40023
+ if (ts.isPropertyAccessExpression(callee)) {
40024
+ if (WORKLET_ACCEPTING_CALLEES.has(callee.name.text)) return true;
40025
+ return rootIdentifierOfCallChain(callee) === GESTURE_ROOT_IDENTIFIER;
40026
+ }
40027
+ return false;
40028
+ };
40029
+ const isFunctionWorklet = (functionNode) => {
40030
+ if (hasWorkletDirective(functionNode)) return true;
40031
+ const parent = functionNode.parent;
40032
+ return ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === functionNode) && isWorkletAcceptingCall(parent);
40033
+ };
40034
+ const findNodeAtOffset = (sourceFile, targetOffset) => {
40035
+ let matchedNode = null;
40036
+ const visit = (node) => {
40037
+ if (node.getStart(sourceFile) > targetOffset || node.getEnd() <= targetOffset) return;
40038
+ matchedNode = node;
40039
+ ts.forEachChild(node, visit);
40040
+ };
40041
+ visit(sourceFile);
40042
+ return matchedNode;
40043
+ };
40044
+ const isOffsetInsideWorklet = (sourceFile, targetOffset) => {
40045
+ let currentNode = findNodeAtOffset(sourceFile, targetOffset);
40046
+ while (currentNode) {
40047
+ if (ts.isFunctionLike(currentNode) && isFunctionWorklet(currentNode)) return true;
40048
+ currentNode = currentNode.parent;
40049
+ }
40050
+ return false;
40051
+ };
40052
+ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirectory) => {
40053
+ if (!SUPPRESSED_COMPILER_CODES.has(diagnostic.code)) return false;
40054
+ if (!project.hasReanimated) return false;
40055
+ const primaryLabel = diagnostic.labels[0];
40056
+ if (!primaryLabel) return false;
40057
+ const absolutePath = Path.isAbsolute(diagnostic.filename) ? diagnostic.filename : Path.join(rootDirectory, diagnostic.filename);
40058
+ let sourceText;
40059
+ try {
40060
+ sourceText = NFS.readFileSync(absolutePath, "utf-8");
40061
+ } catch {
40062
+ return false;
40063
+ }
40064
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
40065
+ };
40066
+ const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
40067
+ const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
40068
+ const REACT_COMPILER_PLUGIN = "react-hooks-js";
40069
+ const getScriptKind = (filename) => {
40070
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
40071
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
40072
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
40073
+ return ts.ScriptKind.JS;
40074
+ };
40075
+ const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
40076
+ const nodeLineRange = (sourceFile, node) => ({
40077
+ startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
40078
+ endLine: sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1
40079
+ });
40080
+ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40081
+ let foundRange = null;
40082
+ const visit = (node) => {
40083
+ if (foundRange) return;
40084
+ if (ts.isFunctionLike(node)) {
40085
+ const range = nodeLineRange(sourceFile, node);
40086
+ if (range.startLine <= targetLine && targetLine <= range.endLine) foundRange = range;
40087
+ return;
40088
+ }
40089
+ ts.forEachChild(node, visit);
40090
+ };
40091
+ visit(sourceFile);
40092
+ return foundRange;
40093
+ };
40094
+ const NEWLINE_BYTE = 10;
40095
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40096
+ let lineNumber = 1;
40097
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40098
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40099
+ return lineNumber;
40100
+ };
40101
+ /**
40102
+ * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40103
+ * whose enclosing function also carries a `react-hooks-js` (React
40104
+ * Compiler) diagnostic. Every react-hooks-js finding marks a function the
40105
+ * compiler could NOT compile (a bail-out / skip / preserve-manual-
40106
+ * memoization failure), so "React Compiler already caches every value
40107
+ * here" is false for that function and the manual memoization is
40108
+ * load-bearing, not dead weight.
40109
+ *
40110
+ * Matching is per enclosing function, not per file: a bailed-out
40111
+ * component must not silence redundant-memo findings in a sibling
40112
+ * component the compiler did optimize.
40113
+ *
40114
+ * Module-scope `memo(() => …)` wrappers have no enclosing function; for
40115
+ * those the memo call's own span is the containment range, so a bail-out
40116
+ * inside the inline-wrapped component still suppresses. Every failure
40117
+ * path (unreadable file, missing span) fails open and keeps the
40118
+ * diagnostic.
40119
+ */
40120
+ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) => {
40121
+ const compilerDiagnosticLinesByFile = /* @__PURE__ */ new Map();
40122
+ let hasManualMemoizationDiagnostic = false;
40123
+ for (const diagnostic of diagnostics) {
40124
+ if (isManualMemoizationDiagnostic(diagnostic)) {
40125
+ hasManualMemoizationDiagnostic = true;
40126
+ continue;
40127
+ }
40128
+ if (diagnostic.plugin !== REACT_COMPILER_PLUGIN || diagnostic.line <= 0) continue;
40129
+ const lines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
40130
+ if (lines) lines.push(diagnostic.line);
40131
+ else compilerDiagnosticLinesByFile.set(diagnostic.filePath, [diagnostic.line]);
40132
+ }
40133
+ if (!hasManualMemoizationDiagnostic || compilerDiagnosticLinesByFile.size === 0) return diagnostics;
40134
+ const parsedFileCache = /* @__PURE__ */ new Map();
40135
+ const readAndParse = (filePath) => {
40136
+ const cached = parsedFileCache.get(filePath);
40137
+ if (cached !== void 0) return cached;
40138
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
40139
+ let parsed;
40140
+ try {
40141
+ const buffer = NFS.readFileSync(absolutePath);
40142
+ parsed = {
40143
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
40144
+ buffer
40145
+ };
40146
+ } catch {
40147
+ parsed = null;
40148
+ }
40149
+ parsedFileCache.set(filePath, parsed);
40150
+ return parsed;
40151
+ };
40152
+ const isInsideBailedOutFunction = (diagnostic) => {
40153
+ const compilerLines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
40154
+ if (!compilerLines || diagnostic.line <= 0) return false;
40155
+ const parsed = readAndParse(diagnostic.filePath);
40156
+ if (!parsed) return false;
40157
+ let containmentRange = findOutermostEnclosingFunctionRange(parsed.sourceFile, diagnostic.line);
40158
+ if (!containmentRange && diagnostic.offset !== void 0 && diagnostic.length !== void 0) containmentRange = {
40159
+ startLine: diagnostic.line,
40160
+ endLine: lineOfUtf8Offset(parsed.buffer, diagnostic.offset + diagnostic.length)
40161
+ };
40162
+ if (!containmentRange) return false;
40163
+ const { startLine, endLine } = containmentRange;
40164
+ return compilerLines.some((compilerLine) => startLine <= compilerLine && compilerLine <= endLine);
40165
+ };
40166
+ return diagnostics.filter((diagnostic) => !isManualMemoizationDiagnostic(diagnostic) || !isInsideBailedOutFunction(diagnostic));
40167
+ };
39791
40168
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
39792
40169
  const LEADING_SEVERITY_LABEL_PATTERN = /^(?:Error|Warning):\s*/;
39793
40170
  const TRAILING_PERIOD_PATTERN = /\.$/;
@@ -39823,8 +40200,14 @@ const PLUGIN_CATEGORY_MAP = {
39823
40200
  };
39824
40201
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
39825
40202
  const getRuleRecommendation = (ruleName, project) => {
39826
- if (ruleName === "no-secrets-in-client-code") return buildNoSecretsRecommendation(project, reactDoctorPlugin.rules["no-secrets-in-client-code"]?.recommendation ?? "Move secrets to server-only code");
39827
- return reactDoctorPlugin.rules[ruleName]?.recommendation;
40203
+ const rule = reactDoctorPlugin.rules[ruleName];
40204
+ if (!rule) return void 0;
40205
+ if (rule.recommendationFor) {
40206
+ const capabilities = getCapabilities(project);
40207
+ const conditionalRecommendation = rule.recommendationFor((capability) => capabilities.has(capability));
40208
+ if (conditionalRecommendation !== void 0) return conditionalRecommendation;
40209
+ }
40210
+ return rule.recommendation;
39828
40211
  };
39829
40212
  const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
39830
40213
  const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
@@ -39927,7 +40310,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
39927
40310
  minifiedFileCache.set(absolutePath, minified);
39928
40311
  return minified;
39929
40312
  };
39930
- return parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
40313
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
39931
40314
  const { plugin, rule } = parseRuleCode(diagnostic.code);
39932
40315
  const primaryLabel = diagnostic.labels[0];
39933
40316
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -39954,7 +40337,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
39954
40337
  ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
39955
40338
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
39956
40339
  };
39957
- });
40340
+ }), rootDirectory);
39958
40341
  };
39959
40342
  const buildOxlintChildEnv = (sourceEnv) => {
39960
40343
  const childEnv = {};
@@ -39978,7 +40361,9 @@ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
39978
40361
  * - `OxlintBatchExceeded { kind: "output-too-large" }` — stdout+stderr
39979
40362
  * crossed `OXLINT_OUTPUT_MAX_BYTES`.
39980
40363
  * - `OxlintBatchExceeded { kind: "oom" | "killed" }` — child exited
39981
- * on a signal (SIGABRT → OOM, others → generic kill).
40364
+ * on a signal (SIGABRT → OOM, others → generic kill), or with one
40365
+ * of the abort exit codes Windows reports instead of a signal
40366
+ * (`ABORT_EXIT_CODES` → OOM).
39982
40367
  * - `OxlintSpawnFailed { cause }` — `spawn` itself errored, or the
39983
40368
  * child exited successfully but printed only stderr.
39984
40369
  *
@@ -40044,7 +40429,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
40044
40429
  clearAbortListener();
40045
40430
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
40046
40431
  });
40047
- child.on("close", (_code, signal) => {
40432
+ child.on("close", (code, signal) => {
40048
40433
  clearTimeout(timeoutHandle);
40049
40434
  clearAbortListener();
40050
40435
  if (didKillForSize) {
@@ -40054,10 +40439,11 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
40054
40439
  }) }));
40055
40440
  return;
40056
40441
  }
40057
- if (signal) {
40442
+ const isAbortExitCode = code !== null && ABORT_EXIT_CODES.has(code);
40443
+ if (signal || isAbortExitCode) {
40058
40444
  const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
40059
- const isOom = signal === "SIGABRT";
40060
- const detailParts = [`killed by ${signal}`];
40445
+ const isOom = signal === "SIGABRT" || isAbortExitCode;
40446
+ const detailParts = [signal ? `killed by ${signal}` : `aborted with exit code ${code}`];
40061
40447
  if (isOom) detailParts.push("try scanning fewer files with --diff");
40062
40448
  if (stderrOutput) detailParts.push(stderrOutput);
40063
40449
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
@@ -40100,12 +40486,19 @@ const isParallelismRelatedSpawnError = (error) => {
40100
40486
  * (surfaced via `onPartialFailure`) so JSON-mode consumers see WHICH
40101
40487
  * files were skipped instead of silently losing them.
40102
40488
  *
40103
- * Parallel runs (concurrency > 1) get one extra safety net: if the pass
40104
- * fails with a resource-exhaustion error that's exclusive to running
40105
- * many oxlint subprocesses at once (EAGAIN / EMFILE / ENFILE / ENOMEM —
40106
- * see `isParallelismRelatedSpawnError`), the whole pass replays once
40107
- * with a single worker. That's the only failure a serial replay can
40108
- * clear, so every other error class is left to propagate.
40489
+ * Parallel runs (concurrency > 1) get two extra safety nets:
40490
+ * - If the pass fails with a resource-exhaustion error that's exclusive
40491
+ * to running many oxlint subprocesses at once (EAGAIN / EMFILE /
40492
+ * ENFILE / ENOMEM — see `isParallelismRelatedSpawnError`), the whole
40493
+ * pass replays once with a single worker. That's the only failure a
40494
+ * serial replay can clear, so every other error class is left to
40495
+ * propagate.
40496
+ * - Files dropped because oxlint's native binding SIGABRT'd under
40497
+ * memory pressure (`OxlintBatchExceeded { kind: "oom" }`) get one
40498
+ * serial single-file-batch rescue pass: the OOM is usually a function
40499
+ * of N concurrent oxlint allocator arenas, not the file itself, so a
40500
+ * lone process with the machine to itself typically clears it —
40501
+ * turning a partial scan into a complete (if slower) one.
40109
40502
  *
40110
40503
  * Errors that aren't splittable and aren't parallelism-related (oxlint
40111
40504
  * config crash, JS plugin resolution failure, etc.) propagate to the
@@ -40115,13 +40508,16 @@ const isParallelismRelatedSpawnError = (error) => {
40115
40508
  const spawnLintBatches = async (input) => {
40116
40509
  const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
40117
40510
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
40118
- const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
40119
- const runBatchPass = async (concurrency) => {
40511
+ let rescueDeadlineEpochMs;
40512
+ const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0 || rescueDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(rescueDeadlineEpochMs) === 0;
40513
+ const runBatchPass = async (concurrency, passBatches, passOnFileProgress) => {
40514
+ const totalFileCount = passBatches.reduce((sum, batch) => sum + batch.length, 0);
40120
40515
  const allDiagnostics = [];
40121
40516
  const droppedFiles = [];
40517
+ const oomDroppedFiles = [];
40122
40518
  const deadlineSkippedFiles = [];
40123
- const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0;
40124
40519
  let firstDropReason = null;
40520
+ let firstNonOomDropReason = null;
40125
40521
  const spawnLintBatch = async (batch, depth, batchState) => {
40126
40522
  if (isPastDeadline()) {
40127
40523
  deadlineSkippedFiles.push(...batch);
@@ -40143,12 +40539,14 @@ const spawnLintBatches = async (input) => {
40143
40539
  const isDepthCapReached = depth >= splitMaxDepth;
40144
40540
  if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
40145
40541
  droppedFiles.push(...batch);
40146
- if (firstDropReason === null) {
40147
- let limitHint = "";
40148
- if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
40149
- else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
40150
- firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
40151
- }
40542
+ const isOomDrop = error.reason._tag === "OxlintBatchExceeded" && error.reason.kind === "oom";
40543
+ if (isOomDrop) oomDroppedFiles.push(...batch);
40544
+ let limitHint = "";
40545
+ if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
40546
+ else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
40547
+ const dropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
40548
+ firstDropReason ??= dropReason;
40549
+ if (!isOomDrop) firstNonOomDropReason ??= dropReason;
40152
40550
  return [];
40153
40551
  }
40154
40552
  const splitIndex = Math.ceil(batch.length / 2);
@@ -40158,16 +40556,16 @@ const spawnLintBatches = async (input) => {
40158
40556
  let startedFileCount = 0;
40159
40557
  let scannedFileCount = 0;
40160
40558
  let displayedFileCount = 0;
40161
- const progressTimer = onFileProgress && totalFileCount > 1 ? setInterval(() => {
40559
+ const progressTimer = passOnFileProgress && totalFileCount > 1 ? setInterval(() => {
40162
40560
  const ceiling = Math.min(startedFileCount, totalFileCount - 1);
40163
40561
  if (displayedFileCount < ceiling) {
40164
40562
  displayedFileCount += 1;
40165
- onFileProgress(displayedFileCount, totalFileCount);
40563
+ passOnFileProgress(displayedFileCount, totalFileCount);
40166
40564
  }
40167
40565
  }, 50) : null;
40168
40566
  progressTimer?.unref?.();
40169
40567
  try {
40170
- const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
40568
+ const batchResults = await mapWithConcurrency(passBatches, concurrency, async (batch) => {
40171
40569
  if (isPastDeadline()) {
40172
40570
  deadlineSkippedFiles.push(...batch);
40173
40571
  return [];
@@ -40179,9 +40577,9 @@ const spawnLintBatches = async (input) => {
40179
40577
  };
40180
40578
  const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
40181
40579
  scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
40182
- if (onFileProgress) {
40580
+ if (passOnFileProgress) {
40183
40581
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
40184
- onFileProgress(displayedFileCount, totalFileCount);
40582
+ passOnFileProgress(displayedFileCount, totalFileCount);
40185
40583
  }
40186
40584
  return batchDiagnostics;
40187
40585
  });
@@ -40189,22 +40587,53 @@ const spawnLintBatches = async (input) => {
40189
40587
  } finally {
40190
40588
  if (progressTimer !== null) clearInterval(progressTimer);
40191
40589
  }
40192
- const reportSkippedFiles = (skippedFiles, buildMessage) => {
40193
- if (skippedFiles.length === 0 || !onPartialFailure) return;
40194
- onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
40590
+ return {
40591
+ diagnostics: allDiagnostics,
40592
+ droppedFiles,
40593
+ oomDroppedFiles,
40594
+ deadlineSkippedFiles,
40595
+ firstDropReason,
40596
+ firstNonOomDropReason
40195
40597
  };
40196
- const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
40197
- reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
40198
- reportSkippedFiles(deadlineSkippedFiles, (fileListPreview) => `${deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
40199
- return allDiagnostics;
40200
40598
  };
40201
- let diagnostics;
40599
+ let outcome;
40600
+ let effectiveConcurrency = requestedConcurrency;
40202
40601
  try {
40203
- diagnostics = await runBatchPass(requestedConcurrency);
40602
+ outcome = await runBatchPass(requestedConcurrency, fileBatches, onFileProgress);
40204
40603
  } catch (error) {
40205
40604
  if (requestedConcurrency <= 1 || !isParallelismRelatedSpawnError(error)) throw error;
40206
- diagnostics = await runBatchPass(1);
40605
+ effectiveConcurrency = 1;
40606
+ outcome = await runBatchPass(1, fileBatches, onFileProgress);
40607
+ }
40608
+ let { droppedFiles, firstDropReason } = outcome;
40609
+ const diagnostics = outcome.diagnostics;
40610
+ if (outcome.oomDroppedFiles.length > 0 && effectiveConcurrency > 1 && !isPastDeadline()) {
40611
+ rescueDeadlineEpochMs = Date.now() + OXLINT_OOM_RESCUE_BUDGET_MS;
40612
+ const rescuedFiles = /* @__PURE__ */ new Set();
40613
+ let firstRescueFailureReason = null;
40614
+ for (const oomDroppedFile of outcome.oomDroppedFiles) {
40615
+ if (isPastDeadline()) break;
40616
+ try {
40617
+ const rescueOutcome = await runBatchPass(1, [[oomDroppedFile]], void 0);
40618
+ diagnostics.push(...rescueOutcome.diagnostics);
40619
+ if (rescueOutcome.droppedFiles.length > 0 || rescueOutcome.deadlineSkippedFiles.length > 0) firstRescueFailureReason ??= rescueOutcome.firstDropReason;
40620
+ else rescuedFiles.add(oomDroppedFile);
40621
+ } catch (error) {
40622
+ firstRescueFailureReason ??= error instanceof Error ? error.message : String(error);
40623
+ }
40624
+ }
40625
+ rescueDeadlineEpochMs = void 0;
40626
+ droppedFiles = droppedFiles.filter((filePath) => !rescuedFiles.has(filePath));
40627
+ if (droppedFiles.length === 0) firstDropReason = null;
40628
+ else firstDropReason = firstRescueFailureReason ?? outcome.firstNonOomDropReason ?? firstDropReason;
40207
40629
  }
40630
+ const reportSkippedFiles = (skippedFiles, buildMessage) => {
40631
+ if (skippedFiles.length === 0 || !onPartialFailure) return;
40632
+ onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
40633
+ };
40634
+ const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
40635
+ reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
40636
+ reportSkippedFiles(outcome.deadlineSkippedFiles, (fileListPreview) => `${outcome.deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
40208
40637
  return dedupeDiagnostics(diagnostics);
40209
40638
  };
40210
40639
  let didValidate = false;
@@ -40465,6 +40894,7 @@ const runOxlint = async (options) => {
40465
40894
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
40466
40895
  const severityControls = buildRuleSeverityControls(userConfig);
40467
40896
  validateRuleRegistration();
40897
+ resetManifestCaches();
40468
40898
  if (includePaths !== void 0 && includePaths.length === 0) return [];
40469
40899
  const pluginPath = resolvePluginPath();
40470
40900
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
@@ -41127,9 +41557,10 @@ const CONTEXT_AXES = [
41127
41557
  }
41128
41558
  ];
41129
41559
  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;
41560
+ const gatedAxesForSection = (section) => section === "devDependencies" ? GATED_AXES.filter((axis) => axis.key !== "vulnerability") : GATED_AXES;
41561
+ const worstGatedAxis = (score, gatedAxes) => {
41562
+ let worst = gatedAxes[0];
41563
+ for (const axis of gatedAxes) if (score[axis.key] < score[worst.key]) worst = axis;
41133
41564
  return worst;
41134
41565
  };
41135
41566
  const clampScore = (value) => {
@@ -41153,6 +41584,69 @@ const resolveConcreteVersion = (spec) => {
41153
41584
  if (range === null || range === "*") return null;
41154
41585
  return import_semver.minVersion(trimmed)?.version ?? null;
41155
41586
  };
41587
+ const readTextFileOrNull = (filePath) => {
41588
+ try {
41589
+ return NFS.readFileSync(filePath, "utf-8");
41590
+ } catch {
41591
+ return null;
41592
+ }
41593
+ };
41594
+ const buildNpmLockLookup = (lockText) => {
41595
+ let parsed;
41596
+ try {
41597
+ parsed = JSON.parse(lockText);
41598
+ } catch {
41599
+ return null;
41600
+ }
41601
+ if (typeof parsed !== "object" || parsed === null) return null;
41602
+ const lock = parsed;
41603
+ return (name) => {
41604
+ const entry = lock.packages?.[`node_modules/${name}`] ?? lock.dependencies?.[name];
41605
+ return typeof entry?.version === "string" ? entry.version : null;
41606
+ };
41607
+ };
41608
+ const buildYarnLockLookup = (lockText) => {
41609
+ const lines = lockText.split(/\r?\n/);
41610
+ return (name, spec) => {
41611
+ const descriptor = `${name}@${spec}`;
41612
+ const berryDescriptor = `${name}@npm:${spec}`;
41613
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
41614
+ const line = lines[lineIndex];
41615
+ if (line.startsWith(" ") || line.startsWith("#") || !line.includes(":")) continue;
41616
+ const descriptors = line.replace(/:\s*$/, "").split(",").map((part) => part.trim().replace(/^"|"$/g, ""));
41617
+ if (!descriptors.includes(descriptor) && !descriptors.includes(berryDescriptor)) continue;
41618
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && (lines[bodyIndex].startsWith(" ") || lines[bodyIndex] === ""); bodyIndex += 1) {
41619
+ const versionMatch = /^\s+version:?\s+"?([^"\s]+)"?/.exec(lines[bodyIndex]);
41620
+ if (versionMatch) return versionMatch[1];
41621
+ }
41622
+ return null;
41623
+ }
41624
+ return null;
41625
+ };
41626
+ };
41627
+ const buildPnpmLockLookup = (lockText) => {
41628
+ return (name) => {
41629
+ const entryPattern = new RegExp(`^ {4,8}['"]?${name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}['"]?:\\s*$`);
41630
+ const lines = lockText.split(/\r?\n/);
41631
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
41632
+ if (!entryPattern.test(lines[lineIndex])) continue;
41633
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && /^ {5,}\S/.test(lines[bodyIndex]); bodyIndex += 1) {
41634
+ const versionMatch = /^\s+version:\s+['"]?([^'"\s(]+)/.exec(lines[bodyIndex]);
41635
+ if (versionMatch) return versionMatch[1];
41636
+ }
41637
+ }
41638
+ return null;
41639
+ };
41640
+ };
41641
+ const buildLockedVersionLookup = (rootDirectory) => {
41642
+ const npmLockText = readTextFileOrNull(Path.join(rootDirectory, "package-lock.json"));
41643
+ if (npmLockText !== null) return buildNpmLockLookup(npmLockText);
41644
+ const yarnLockText = readTextFileOrNull(Path.join(rootDirectory, "yarn.lock"));
41645
+ if (yarnLockText !== null) return buildYarnLockLookup(yarnLockText);
41646
+ const pnpmLockText = readTextFileOrNull(Path.join(rootDirectory, "pnpm-lock.yaml"));
41647
+ if (pnpmLockText !== null) return buildPnpmLockLookup(pnpmLockText);
41648
+ return null;
41649
+ };
41156
41650
  const locateDependencyKey = (packageJsonText, section, name) => {
41157
41651
  const needle = `"${name}"`;
41158
41652
  const sectionHeader = new RegExp(`"${section}"\\s*:\\s*\\{`);
@@ -41179,7 +41673,7 @@ const locateDependencyKey = (packageJsonText, section, name) => {
41179
41673
  }
41180
41674
  return null;
41181
41675
  };
41182
- const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies) => {
41676
+ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies, lockedVersionLookup) => {
41183
41677
  const sectionByName = /* @__PURE__ */ new Map();
41184
41678
  for (const name of Object.keys(packageJson.dependencies ?? {})) sectionByName.set(name, "dependencies");
41185
41679
  if (includeDevDependencies) {
@@ -41189,13 +41683,18 @@ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDepe
41189
41683
  for (const [name, section] of sectionByName) {
41190
41684
  if (SUPPLY_CHAIN_IGNORED_PACKAGES.has(name)) continue;
41191
41685
  const spec = (packageJson[section] ?? {})[name] ?? "";
41192
- const version = resolveConcreteVersion(spec);
41193
- if (version === null) continue;
41686
+ const floorVersion = resolveConcreteVersion(spec);
41687
+ if (floorVersion === null) continue;
41688
+ const lockedVersion = lockedVersionLookup?.(name, spec) ?? null;
41689
+ const lockSatisfiesSpec = lockedVersion !== null && import_semver.valid(lockedVersion) !== null && import_semver.satisfies(lockedVersion, spec);
41690
+ const isExactPin = import_semver.valid(spec.trim()) !== null;
41194
41691
  const location = locateDependencyKey(packageJsonText, section, name);
41195
41692
  dependencies.push({
41196
41693
  name,
41197
- version,
41694
+ version: lockSatisfiesSpec ? lockedVersion : floorVersion,
41198
41695
  spec,
41696
+ section,
41697
+ resolution: isExactPin ? "pin" : lockSatisfiesSpec ? "locked" : "floor",
41199
41698
  line: location?.line ?? 0,
41200
41699
  column: location?.column ?? 0
41201
41700
  });
@@ -41346,7 +41845,11 @@ const formatAlertReason = (topAlerts, totalCount) => {
41346
41845
  }
41347
41846
  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
41847
  };
41349
- const formatDependencyIdentity = (dependency) => import_semver.valid(dependency.spec) !== null ? `${dependency.name}@${dependency.version}` : `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
41848
+ const formatDependencyIdentity = (dependency) => {
41849
+ if (dependency.resolution === "pin") return `${dependency.name}@${dependency.version}`;
41850
+ if (dependency.resolution === "locked") return `${dependency.name}@${dependency.version} (lockfile resolution of "${dependency.spec}")`;
41851
+ return `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
41852
+ };
41350
41853
  const buildSupplyChainHelp = (dependency, failingAxis, topAlerts, packagePageUrl, options) => {
41351
41854
  const hasCriticalAlert = topAlerts.some((alert) => alert.severity.toLowerCase() === "critical");
41352
41855
  const entry = `\`"${dependency.name}": "${dependency.spec}"\``;
@@ -41389,7 +41892,7 @@ const buildLowScoreDiagnostic = (dependency, artifact, failingAxis, options) =>
41389
41892
  const checkSupplyChain = (input) => gen(function* () {
41390
41893
  const options = resolveOptions(input.userConfig);
41391
41894
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
41392
- const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
41895
+ const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies, buildLockedVersionLookup(input.rootDirectory));
41393
41896
  if (dependencies.length === 0) return [];
41394
41897
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
41395
41898
  if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
@@ -41398,7 +41901,7 @@ const checkSupplyChain = (input) => gen(function* () {
41398
41901
  for (let index = 0; index < dependencies.length; index += 1) {
41399
41902
  const artifact = artifacts[index];
41400
41903
  if (!artifact) continue;
41401
- const worstAxis = worstGatedAxis(artifact.score);
41904
+ const worstAxis = worstGatedAxis(artifact.score, gatedAxesForSection(dependencies[index].section));
41402
41905
  if (toHundred(artifact.score[worstAxis.key]) >= options.minScore) continue;
41403
41906
  diagnostics.push(buildLowScoreDiagnostic(dependencies[index], artifact, worstAxis, options));
41404
41907
  }
@@ -42075,6 +42578,13 @@ const summarizeDiagnostics = (diagnostics, worstScore = null, worstScoreLabel =
42075
42578
  scoreLabel: worstScoreLabel
42076
42579
  };
42077
42580
  };
42581
+ /**
42582
+ * Whether project discovery resolved a React-compatible runtime (React or
42583
+ * Preact, which ships the same hooks + JSX model). This is the same
42584
+ * predicate that grants the `react` capability, so `false` means every
42585
+ * React-runtime rule family was gated off for the project.
42586
+ */
42587
+ const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
42078
42588
  const toJsonDiff = (diff) => {
42079
42589
  if (!diff) return null;
42080
42590
  return {
@@ -42309,17 +42819,20 @@ const warnIfAiTrainingEnvironment = () => {
42309
42819
  didWarnAiTraining = true;
42310
42820
  console.warn("[react-doctor] Use in an AI or ML pipeline requires written permission under the react-doctor license. Contact founders@million.dev to request access.");
42311
42821
  };
42312
- const buildDiagnoseLayer = (config, configOverrideTarget) => {
42313
- const configLayer = configOverrideTarget === void 0 ? Config.layerNode : Config.layerOf({
42314
- config,
42315
- resolvedDirectory: configOverrideTarget.resolvedDirectory,
42316
- configSourceDirectory: configOverrideTarget.configSourceDirectory
42822
+ const resolveShouldRunLint = (options, effectiveConfig) => options.lint ?? effectiveConfig?.lint ?? true;
42823
+ const resolveShouldRunDeadCode = (options, effectiveConfig) => options.deadCode ?? effectiveConfig?.deadCode ?? true;
42824
+ const buildDiagnoseLayer = (input) => {
42825
+ const configLayer = input.configOverrideTarget === void 0 ? Config.layerNode : Config.layerOf({
42826
+ config: input.config,
42827
+ resolvedDirectory: input.configOverrideTarget.resolvedDirectory,
42828
+ configSourceDirectory: input.configOverrideTarget.configSourceDirectory
42317
42829
  });
42318
- return mergeAll$1(Project.layerNode, configLayer, DeadCode.layerNode, Files.layerNode, Git.layerNode, Linter.layerOxlint, LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp, config?.supplyChain?.enabled !== false ? SupplyChain.layerNode : SupplyChain.layerOf([]));
42830
+ return mergeAll$1(Project.layerNode, configLayer, input.shouldRunDeadCode ? DeadCode.layerNode : DeadCode.layerOf([]), Files.layerNode, Git.layerNode, input.shouldRunLint ? Linter.layerOxlint : Linter.layerOf([]), LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp, input.config?.supplyChain?.enabled !== false ? SupplyChain.layerNode : SupplyChain.layerOf([]));
42319
42831
  };
42320
42832
  const buildInspectProgram = (scanTarget, options, configOverride) => {
42321
42833
  const effectiveConfig = configOverride ?? scanTarget.userConfig;
42322
42834
  const includePaths = options.includePaths ?? [];
42835
+ const shouldRunDeadCode = resolveShouldRunDeadCode(options, effectiveConfig);
42323
42836
  return runInspect({
42324
42837
  directory: scanTarget.resolvedDirectory,
42325
42838
  includePaths,
@@ -42328,7 +42841,7 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
42328
42841
  warnings: options.warnings ?? effectiveConfig?.warnings ?? true,
42329
42842
  adoptExistingLintConfig: effectiveConfig?.adoptExistingLintConfig ?? true,
42330
42843
  ignoredTags: new Set(effectiveConfig?.ignore?.tags ?? []),
42331
- runDeadCode: options.deadCode ?? effectiveConfig?.deadCode ?? true,
42844
+ runDeadCode: shouldRunDeadCode,
42332
42845
  isCi: false,
42333
42846
  resolveLocalGithubViewerPermission: true
42334
42847
  });
@@ -42350,7 +42863,14 @@ const diagnoseDirectory = async (directory, options) => {
42350
42863
  warnIfAiTrainingEnvironment();
42351
42864
  const startTime = globalThis.performance.now();
42352
42865
  const scanTarget = await resolveScanTarget(directory);
42353
- return outputToDiagnoseResult(await runPromise(restoreLegacyThrow(buildInspectProgram(scanTarget, options).pipe(provide(buildDiagnoseLayer(scanTarget.userConfig)), provide(layerOtlp)))), globalThis.performance.now() - startTime);
42866
+ const program = buildInspectProgram(scanTarget, options);
42867
+ const shouldRunLint = resolveShouldRunLint(options, scanTarget.userConfig);
42868
+ const shouldRunDeadCode = resolveShouldRunDeadCode(options, scanTarget.userConfig);
42869
+ return outputToDiagnoseResult(await runPromise(restoreLegacyThrow(program.pipe(provide(buildDiagnoseLayer({
42870
+ config: scanTarget.userConfig,
42871
+ shouldRunLint,
42872
+ shouldRunDeadCode
42873
+ })), provide(layerOtlp)))), globalThis.performance.now() - startTime);
42354
42874
  };
42355
42875
  const findWorstScore = (projectResults) => {
42356
42876
  let worstResult = null;
@@ -42371,15 +42891,27 @@ const diagnoseProject = async (projectDefinition, baseOptions, batchConfig) => {
42371
42891
  const { directory: _, config: projectConfig, ...perProjectOptions } = projectDefinition;
42372
42892
  const didOverrideConfig = batchConfig !== void 0 || projectConfig !== void 0;
42373
42893
  const effectiveConfig = mergeReactDoctorConfigs(mergeReactDoctorConfigs(scanTarget.userConfig, batchConfig), projectConfig);
42374
- const program = buildInspectProgram(scanTarget, {
42894
+ const mergedOptions = {
42375
42895
  ...baseOptions,
42376
42896
  ...perProjectOptions
42377
- }, effectiveConfig ?? void 0);
42897
+ };
42898
+ const shouldRunLint = resolveShouldRunLint(mergedOptions, effectiveConfig);
42899
+ const shouldRunDeadCode = resolveShouldRunDeadCode(mergedOptions, effectiveConfig);
42900
+ const program = buildInspectProgram(scanTarget, mergedOptions, effectiveConfig ?? void 0);
42378
42901
  const didOverridePlugins = batchConfig?.plugins !== void 0 || projectConfig?.plugins !== void 0;
42379
- const layer = buildDiagnoseLayer(effectiveConfig, didOverrideConfig ? {
42380
- resolvedDirectory: scanTarget.resolvedDirectory,
42381
- configSourceDirectory: didOverridePlugins ? null : scanTarget.configSourceDirectory
42382
- } : void 0);
42902
+ const layer = buildDiagnoseLayer(didOverrideConfig ? {
42903
+ config: effectiveConfig,
42904
+ shouldRunLint,
42905
+ shouldRunDeadCode,
42906
+ configOverrideTarget: {
42907
+ resolvedDirectory: scanTarget.resolvedDirectory,
42908
+ configSourceDirectory: didOverridePlugins ? null : scanTarget.configSourceDirectory
42909
+ }
42910
+ } : {
42911
+ config: effectiveConfig,
42912
+ shouldRunLint,
42913
+ shouldRunDeadCode
42914
+ });
42383
42915
  return {
42384
42916
  ok: true,
42385
42917
  ...outputToDiagnoseResult(await runPromise(restoreLegacyThrow(program.pipe(provide(layer), provide(layerOtlp)))), globalThis.performance.now() - startTime),
@@ -42441,4 +42973,4 @@ const toJsonReport = (result, options) => buildJsonReport({
42441
42973
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
42442
42974
 
42443
42975
  //# sourceMappingURL=index.js.map
42444
- //# debugId=7cb857ba-6f0a-57d9-91f3-3c286472121d
42976
+ //# debugId=3dc4a1ac-42dd-51e8-ba38-0260edf45659