react-doctor 0.7.1-dev.d353dad → 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]="f00947fe-3794-5d73-a491-232b535dc962")}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 {
@@ -33064,23 +33081,6 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33064
33081
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33065
33082
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33066
33083
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33067
- /**
33068
- * The chain of ancestor directories of `startDirectory`, nearest first, up to
33069
- * and including the filesystem root. With `includeStart`, the chain begins at
33070
- * `startDirectory` itself; otherwise at its parent. The one place the "walk up
33071
- * the tree" traversal is expressed, so each caller adds only its own match /
33072
- * stop policy instead of re-deriving the `path.dirname` loop.
33073
- */
33074
- const ancestorDirectories = (startDirectory, { includeStart }) => {
33075
- const directories = [];
33076
- let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
33077
- while (true) {
33078
- directories.push(currentDirectory);
33079
- const parentDirectory = Path.dirname(currentDirectory);
33080
- if (parentDirectory === currentDirectory) return directories;
33081
- currentDirectory = parentDirectory;
33082
- }
33083
- };
33084
33084
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33085
33085
  const clearPackageJsonCache = () => {
33086
33086
  cachedPackageJsons.clear();
@@ -33114,7 +33114,11 @@ const isMonorepoRoot = (directory) => {
33114
33114
  return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
33115
33115
  };
33116
33116
  const findMonorepoRoot = (startDirectory) => {
33117
- for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
33117
+ let currentDirectory = Path.dirname(startDirectory);
33118
+ while (currentDirectory !== Path.dirname(currentDirectory)) {
33119
+ if (isMonorepoRoot(currentDirectory)) return currentDirectory;
33120
+ currentDirectory = Path.dirname(currentDirectory);
33121
+ }
33118
33122
  return null;
33119
33123
  };
33120
33124
  /**
@@ -33128,6 +33132,146 @@ const findMonorepoRoot = (startDirectory) => {
33128
33132
  * byte-equivalent copy.
33129
33133
  */
33130
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
+ };
33131
33275
  const REACT_COMPILER_PACKAGES = new Set([
33132
33276
  "babel-plugin-react-compiler",
33133
33277
  "react-compiler-runtime",
@@ -33164,6 +33308,7 @@ const EXPO_APP_CONFIG_FILENAMES = [
33164
33308
  ];
33165
33309
  const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
33166
33310
  const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
33311
+ const STATIC_EXPORT_OUTPUT_PATTERN = /(?:^|[^.\w])["']?output["']?\s*:\s*["']export["']/m;
33167
33312
  const hasCompilerPackage = (packageJson) => {
33168
33313
  const allDependencies = {
33169
33314
  ...packageJson.peerDependencies,
@@ -33196,34 +33341,10 @@ const detectReactCompiler = (directory, packageJson) => {
33196
33341
  }
33197
33342
  return false;
33198
33343
  };
33199
- const FRAMEWORK_PACKAGES = {
33200
- next: "nextjs",
33201
- "@tanstack/react-start": "tanstack-start",
33202
- vite: "vite",
33203
- "react-scripts": "cra",
33204
- "@remix-run/react": "remix",
33205
- gatsby: "gatsby",
33206
- expo: "expo",
33207
- "react-native": "react-native"
33208
- };
33209
- const FRAMEWORK_DISPLAY_NAMES = {
33210
- nextjs: "Next.js",
33211
- "tanstack-start": "TanStack Start",
33212
- vite: "Vite",
33213
- cra: "Create React App",
33214
- remix: "Remix",
33215
- gatsby: "Gatsby",
33216
- expo: "Expo",
33217
- "react-native": "React Native",
33218
- preact: "Preact",
33219
- unknown: "React"
33220
- };
33221
- const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
33222
- const detectFramework = (dependencies) => {
33223
- for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
33224
- if (dependencies.preact && !dependencies.react) return "preact";
33225
- return "unknown";
33226
- };
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
+ });
33227
33348
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
33228
33349
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
33229
33350
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
@@ -33361,10 +33482,76 @@ const isConcreteDependencyVersion = (version) => {
33361
33482
  const normalizedVersion = normalizeDependencyVersion(version);
33362
33483
  return normalizedVersion !== null && /\d/.test(normalizedVersion);
33363
33484
  };
33364
- const isPlainObject = (value) => {
33365
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
33366
- const prototype = Object.getPrototypeOf(value);
33367
- 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;
33368
33555
  };
33369
33556
  const isCatalogReference = (version) => version.startsWith("catalog:");
33370
33557
  const extractCatalogName = (version) => {
@@ -33506,6 +33693,16 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
33506
33693
  }
33507
33694
  return null;
33508
33695
  };
33696
+ const REACT_SECTIONS = [
33697
+ "dependencies",
33698
+ "peerDependencies",
33699
+ "devDependencies"
33700
+ ];
33701
+ const TAILWIND_ZOD_SECTIONS = [
33702
+ "dependencies",
33703
+ "devDependencies",
33704
+ "peerDependencies"
33705
+ ];
33509
33706
  const EMPTY_DEPENDENCY_INFO = {
33510
33707
  reactVersion: null,
33511
33708
  tailwindVersion: null,
@@ -33546,6 +33743,10 @@ const extractDependencyInfo = (packageJson) => {
33546
33743
  framework: detectFramework(allDependencies)
33547
33744
  };
33548
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
+ };
33549
33750
  const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
33550
33751
  for (const section of sections) {
33551
33752
  const version = packageJson[section]?.[packageName];
@@ -33562,31 +33763,141 @@ const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
33562
33763
  version: null
33563
33764
  };
33564
33765
  };
33565
- const isDirectory = (directoryPath) => {
33566
- try {
33567
- return NFS.statSync(directoryPath).isDirectory();
33568
- } catch {
33569
- return false;
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));
33779
+ };
33780
+ const getPreactVersion = (packageJson) => {
33781
+ return {
33782
+ ...packageJson.peerDependencies,
33783
+ ...packageJson.dependencies,
33784
+ ...packageJson.devDependencies
33785
+ }.preact ?? null;
33786
+ };
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));
33799
+ };
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;
33810
+ };
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;
33570
33826
  }
33571
33827
  };
33572
- const NX_PROJECT_DISCOVERY_DIRS = [
33573
- "apps",
33574
- "libs",
33575
- "packages"
33576
- ];
33577
- const getNxWorkspaceDirectories = (rootDirectory) => {
33578
- if (!isFile(Path.join(rootDirectory, "nx.json"))) return [];
33579
- const collected = [];
33580
- for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
33581
- const candidatePath = Path.join(rootDirectory, candidate);
33582
- if (!isDirectory(candidatePath)) continue;
33583
- for (const entry of readDirectoryEntries(candidatePath)) {
33584
- if (!entry.isDirectory()) continue;
33585
- const projectDirectory = Path.join(candidatePath, entry.name);
33586
- if (isFile(Path.join(projectDirectory, "project.json")) || isFile(Path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
33587
- }
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;
33588
33848
  }
33589
- return collected;
33849
+ return null;
33850
+ };
33851
+ const NAMES = new Set([
33852
+ "react-native",
33853
+ "react-native-tvos",
33854
+ ...new Set([
33855
+ "expo",
33856
+ "expo-router",
33857
+ "@expo/cli",
33858
+ "@expo/metro-config",
33859
+ "@expo/metro-runtime"
33860
+ ]),
33861
+ "react-native-windows",
33862
+ "react-native-macos"
33863
+ ]);
33864
+ const PREFIXES = ["@react-native/", "@react-native-"];
33865
+ const isReactNativeDependencyName = (dependencyName) => {
33866
+ if (NAMES.has(dependencyName)) return true;
33867
+ for (const prefix of PREFIXES) if (dependencyName.startsWith(prefix)) return true;
33868
+ return false;
33869
+ };
33870
+ const containsAnyReactNativeDependency = (section) => {
33871
+ if (!section) return false;
33872
+ for (const dependencyName of Object.keys(section)) if (isReactNativeDependencyName(dependencyName)) return true;
33873
+ return false;
33874
+ };
33875
+ const isPackageJsonReactNativeAware = (packageJson) => {
33876
+ if (typeof packageJson["react-native"] === "string") return true;
33877
+ if (containsAnyReactNativeDependency(packageJson.dependencies)) return true;
33878
+ if (containsAnyReactNativeDependency(packageJson.devDependencies)) return true;
33879
+ if (containsAnyReactNativeDependency(packageJson.peerDependencies)) return true;
33880
+ if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
33881
+ return false;
33882
+ };
33883
+ const REANIMATED_DEPENDENCY_NAME$1 = "react-native-reanimated";
33884
+ const isPackageJsonReanimatedAware = (packageJson) => {
33885
+ const allDependencies = {
33886
+ ...packageJson.peerDependencies,
33887
+ ...packageJson.dependencies,
33888
+ ...packageJson.devDependencies,
33889
+ ...packageJson.optionalDependencies
33890
+ };
33891
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME$1);
33892
+ };
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 [];
33590
33901
  };
33591
33902
  const parsePnpmWorkspacePatterns = (rootDirectory) => {
33592
33903
  const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
@@ -33605,18 +33916,24 @@ const parsePnpmWorkspacePatterns = (rootDirectory) => {
33605
33916
  }
33606
33917
  return patterns;
33607
33918
  };
33608
- const getWorkspacePatterns = (rootDirectory, packageJson) => {
33609
- const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
33610
- if (pnpmPatterns.length > 0) return pnpmPatterns;
33611
- if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
33612
- if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
33613
- const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
33614
- if (nxPatterns.length > 0) return nxPatterns;
33615
- return [];
33616
- };
33617
- const parseReactMajor = (reactVersion) => {
33618
- if (typeof reactVersion !== "string") return null;
33619
- return getLowestDependencyMajor(reactVersion);
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
+ }
33935
+ }
33936
+ return collected;
33620
33937
  };
33621
33938
  const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
33622
33939
  const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
@@ -33636,6 +33953,38 @@ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
33636
33953
  }
33637
33954
  return resolved;
33638
33955
  };
33956
+ const listWorkspacePackages = (rootDirectory) => {
33957
+ const packageJsonPath = Path.join(rootDirectory, "package.json");
33958
+ if (!isFile(packageJsonPath)) return [];
33959
+ const packageJson = readPackageJson(packageJsonPath);
33960
+ const patterns = getWorkspacePatterns(rootDirectory, packageJson);
33961
+ if (patterns.length === 0) return [];
33962
+ const packages = [];
33963
+ const seenDirectories = /* @__PURE__ */ new Set();
33964
+ const pushIfNew = (workspacePackage) => {
33965
+ if (seenDirectories.has(workspacePackage.directory)) return;
33966
+ seenDirectories.add(workspacePackage.directory);
33967
+ packages.push(workspacePackage);
33968
+ };
33969
+ if (hasReactDependency(packageJson)) pushIfNew({
33970
+ name: packageJson.name ?? Path.basename(rootDirectory),
33971
+ directory: rootDirectory
33972
+ });
33973
+ for (const pattern of patterns) {
33974
+ const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
33975
+ for (const workspaceDirectory of directories) {
33976
+ const workspacePackageJson = readPackageJson(Path.join(workspaceDirectory, "package.json"));
33977
+ if (!hasReactDependency(workspacePackageJson)) continue;
33978
+ pushIfNew({
33979
+ name: workspacePackageJson.name ?? Path.basename(workspaceDirectory),
33980
+ directory: workspaceDirectory
33981
+ });
33982
+ }
33983
+ }
33984
+ return packages;
33985
+ };
33986
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
33987
+ const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
33639
33988
  const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
33640
33989
  const dependencyDeclaration = getDependencyDeclaration({
33641
33990
  packageJson: workspacePackageJson,
@@ -33653,24 +34002,76 @@ const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
33653
34002
  if (nextMajor === null) return false;
33654
34003
  return nextMajor < currentMajor;
33655
34004
  };
33656
- const findReactInWorkspaces = (rootDirectory, packageJson) => {
33657
- const patterns = getWorkspacePatterns(rootDirectory, packageJson);
33658
- const result = { ...EMPTY_DEPENDENCY_INFO };
33659
- for (const pattern of patterns) {
33660
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
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();
33661
34061
  for (const workspaceDirectory of directories) {
34062
+ if (visitedDirectories.has(workspaceDirectory)) continue;
34063
+ visitedDirectories.add(workspaceDirectory);
33662
34064
  const workspacePackageJson = readPackageJson(Path.join(workspaceDirectory, "package.json"));
34065
+ evaluateManifestFacts(facts, workspacePackageJson, workspaceDirectory);
33663
34066
  const info = extractDependencyInfo(workspacePackageJson);
34067
+ if (info.framework !== "unknown" && frameworkMergeRank(info.framework) < frameworkMergeRank(facts.framework)) facts.framework = info.framework;
34068
+ if (isReactGroupSettled) continue;
33664
34069
  const reactVersion = resolveWorkspaceDependencyVersion({
33665
34070
  concreteVersion: info.reactVersion,
33666
34071
  packageName: "react",
33667
34072
  rootDirectory,
33668
- rootPackageJson: packageJson,
33669
- sections: [
33670
- "dependencies",
33671
- "peerDependencies",
33672
- "devDependencies"
33673
- ],
34073
+ rootPackageJson,
34074
+ sections: REACT_SECTIONS,
33674
34075
  workspaceDirectory,
33675
34076
  workspacePackageJson
33676
34077
  });
@@ -33678,12 +34079,8 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
33678
34079
  concreteVersion: info.tailwindVersion,
33679
34080
  packageName: "tailwindcss",
33680
34081
  rootDirectory,
33681
- rootPackageJson: packageJson,
33682
- sections: [
33683
- "dependencies",
33684
- "devDependencies",
33685
- "peerDependencies"
33686
- ],
34082
+ rootPackageJson,
34083
+ sections: TAILWIND_ZOD_SECTIONS,
33687
34084
  workspaceDirectory,
33688
34085
  workspacePackageJson
33689
34086
  });
@@ -33691,24 +34088,19 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
33691
34088
  concreteVersion: info.zodVersion,
33692
34089
  packageName: "zod",
33693
34090
  rootDirectory,
33694
- rootPackageJson: packageJson,
33695
- sections: [
33696
- "dependencies",
33697
- "devDependencies",
33698
- "peerDependencies"
33699
- ],
34091
+ rootPackageJson,
34092
+ sections: TAILWIND_ZOD_SECTIONS,
33700
34093
  workspaceDirectory,
33701
34094
  workspacePackageJson
33702
34095
  });
33703
- if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
33704
- if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
33705
- if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
33706
- if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
33707
- const resultReactMajor = parseReactMajor(result.reactVersion);
33708
- if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
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);
33709
34101
  }
33710
34102
  }
33711
- return result;
34103
+ return facts;
33712
34104
  };
33713
34105
  const findDependencyInfoFromMonorepoRoot = (directory) => {
33714
34106
  const monorepoRoot = findMonorepoRoot(directory);
@@ -33722,29 +34114,17 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
33722
34114
  const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
33723
34115
  packageJson: leafPackageJson,
33724
34116
  packageName: "react",
33725
- sections: [
33726
- "dependencies",
33727
- "peerDependencies",
33728
- "devDependencies"
33729
- ]
34117
+ sections: REACT_SECTIONS
33730
34118
  }) : null;
33731
34119
  const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
33732
34120
  packageJson: leafPackageJson,
33733
34121
  packageName: "tailwindcss",
33734
- sections: [
33735
- "dependencies",
33736
- "devDependencies",
33737
- "peerDependencies"
33738
- ]
34122
+ sections: TAILWIND_ZOD_SECTIONS
33739
34123
  }) : null;
33740
34124
  const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
33741
34125
  packageJson: leafPackageJson,
33742
34126
  packageName: "zod",
33743
- sections: [
33744
- "dependencies",
33745
- "devDependencies",
33746
- "peerDependencies"
33747
- ]
34127
+ sections: TAILWIND_ZOD_SECTIONS
33748
34128
  }) : null;
33749
34129
  const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
33750
34130
  const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
@@ -33752,38 +34132,15 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
33752
34132
  const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
33753
34133
  const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
33754
34134
  const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
33755
- const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
34135
+ const workspaceFacts = collectWorkspaceFacts(monorepoRoot, rootPackageJson, { collectReactGroup: true });
33756
34136
  return {
33757
- reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
33758
- tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
33759
- zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
33760
- framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
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
33761
34141
  };
33762
34142
  };
33763
34143
  /**
33764
- * Walk up from `startDirectory` to the nearest ancestor that owns a
33765
- * `package.json`, stopping at (and including) the enclosing project boundary
33766
- * — the working tree's git root or a monorepo root. Lets a scan of a package
33767
- * subfolder that has no `package.json` of its own adopt the nearest enclosing
33768
- * package as its project root, so it inherits that package's dependency +
33769
- * framework detection instead of synthesizing an empty (React-blind) project.
33770
- *
33771
- * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
33772
- * most specific owning package (a leaf workspace, not just the repo root), so
33773
- * a subfolder of one workspace no longer borrows a sibling's capabilities.
33774
- *
33775
- * Returns `null` when no ancestor `package.json` exists inside the project
33776
- * boundary (a loose tree of files, or a subfolder of a package-less repo).
33777
- */
33778
- const findNearestAncestorPackageJson = (startDirectory) => {
33779
- if (isProjectBoundary(startDirectory)) return null;
33780
- for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
33781
- if (isFile(Path.join(directory, "package.json"))) return directory;
33782
- if (isProjectBoundary(directory)) return null;
33783
- }
33784
- return null;
33785
- };
33786
- /**
33787
34144
  * Last-resort React detection: locate React the way Node's `node_modules`
33788
34145
  * resolution would from `directory` — check each `node_modules/react` up the
33789
34146
  * tree — but bounded to the enclosing repo so a globally installed React can't
@@ -33828,273 +34185,6 @@ const resolveInstalledReactVersion = (directory) => {
33828
34185
  }
33829
34186
  return null;
33830
34187
  };
33831
- const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
33832
- const rootValue = select(rootPackageJson);
33833
- if (rootValue !== null) return rootValue;
33834
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
33835
- if (patterns.length === 0) return null;
33836
- const visitedDirectories = /* @__PURE__ */ new Set();
33837
- for (const pattern of patterns) {
33838
- const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
33839
- for (const workspaceDirectory of directories) {
33840
- if (visitedDirectories.has(workspaceDirectory)) continue;
33841
- visitedDirectories.add(workspaceDirectory);
33842
- const value = select(readPackageJson(Path.join(workspaceDirectory, "package.json")));
33843
- if (value !== null) return value;
33844
- }
33845
- }
33846
- return null;
33847
- };
33848
- const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => predicate(packageJson) ? true : null) !== null;
33849
- const NAMES = new Set([
33850
- "react-native",
33851
- "react-native-tvos",
33852
- ...new Set([
33853
- "expo",
33854
- "expo-router",
33855
- "@expo/cli",
33856
- "@expo/metro-config",
33857
- "@expo/metro-runtime"
33858
- ]),
33859
- "react-native-windows",
33860
- "react-native-macos"
33861
- ]);
33862
- const PREFIXES = ["@react-native/", "@react-native-"];
33863
- const isReactNativeDependencyName = (dependencyName) => {
33864
- if (NAMES.has(dependencyName)) return true;
33865
- for (const prefix of PREFIXES) if (dependencyName.startsWith(prefix)) return true;
33866
- return false;
33867
- };
33868
- const containsAnyReactNativeDependency = (section) => {
33869
- if (!section) return false;
33870
- for (const dependencyName of Object.keys(section)) if (isReactNativeDependencyName(dependencyName)) return true;
33871
- return false;
33872
- };
33873
- const isPackageJsonReactNativeAware = (packageJson) => {
33874
- if (typeof packageJson["react-native"] === "string") return true;
33875
- if (containsAnyReactNativeDependency(packageJson.dependencies)) return true;
33876
- if (containsAnyReactNativeDependency(packageJson.devDependencies)) return true;
33877
- if (containsAnyReactNativeDependency(packageJson.peerDependencies)) return true;
33878
- if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
33879
- return false;
33880
- };
33881
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
33882
- const getDependencySpec = (packageJson, packageName) => {
33883
- const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
33884
- return typeof spec === "string" ? spec : null;
33885
- };
33886
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
33887
- const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
33888
- const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
33889
- const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
33890
- if (version === null || !isCatalogReference(version)) return version;
33891
- const catalogName = extractCatalogName(version);
33892
- const resolvedLocalVersion = resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, catalogName);
33893
- if (resolvedLocalVersion) return resolvedLocalVersion;
33894
- const monorepoRoot = findMonorepoRoot(rootDirectory);
33895
- if (!monorepoRoot) return version;
33896
- const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
33897
- if (!isFile(monorepoPackageJsonPath)) return version;
33898
- return resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
33899
- };
33900
- const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
33901
- const getPreactVersion = (packageJson) => {
33902
- return {
33903
- ...packageJson.peerDependencies,
33904
- ...packageJson.dependencies,
33905
- ...packageJson.devDependencies
33906
- }.preact ?? null;
33907
- };
33908
- const TANSTACK_QUERY_PACKAGES = new Set([
33909
- "@tanstack/react-query",
33910
- "@tanstack/query-core",
33911
- "react-query"
33912
- ]);
33913
- const hasTanStackQuery = (packageJson) => {
33914
- const allDependencies = {
33915
- ...packageJson.peerDependencies,
33916
- ...packageJson.dependencies,
33917
- ...packageJson.devDependencies
33918
- };
33919
- return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
33920
- };
33921
- const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
33922
- const isPackageJsonReanimatedAware = (packageJson) => {
33923
- const allDependencies = {
33924
- ...packageJson.peerDependencies,
33925
- ...packageJson.dependencies,
33926
- ...packageJson.devDependencies,
33927
- ...packageJson.optionalDependencies
33928
- };
33929
- return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
33930
- };
33931
- const parseZodMajor = (zodVersion) => {
33932
- if (typeof zodVersion !== "string") return null;
33933
- return getLowestDependencyMajor(zodVersion);
33934
- };
33935
- const hasUpperBoundOnlyPeerRange = (range) => {
33936
- if (typeof range !== "string") return false;
33937
- const normalizedRange = normalizeDependencyVersion(range);
33938
- if (normalizedRange === null) return false;
33939
- return splitDependencyVersionBranches(normalizedRange).some((branch) => {
33940
- const normalizedBranch = normalizeDependencyVersion(branch);
33941
- return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
33942
- });
33943
- };
33944
- const peerRangeMinMajor = (range) => {
33945
- if (typeof range !== "string") return null;
33946
- return getLowestDependencyMajor(range);
33947
- };
33948
- const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
33949
- const installedReactMajor = parseReactMajor(reactVersion);
33950
- const peerReactRange = packageJson.peerDependencies?.react;
33951
- if (typeof peerReactRange !== "string") return installedReactMajor;
33952
- const peerFloor = peerRangeMinMajor(peerReactRange);
33953
- if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
33954
- return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
33955
- };
33956
- const TSCONFIG_FILENAME = "tsconfig.json";
33957
- const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
33958
- const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
33959
- const resolvePackageExtendsPath = (extendsValue, fromConfigDirectory) => {
33960
- const requireFromConfig = createRequire(Path.join(fromConfigDirectory, "tsconfig.json"));
33961
- const candidates = [
33962
- extendsValue,
33963
- ensureJsonExtension(extendsValue),
33964
- `${extendsValue.replace(/\/$/, "")}/tsconfig.json`
33965
- ];
33966
- for (const candidate of candidates) try {
33967
- return requireFromConfig.resolve(candidate);
33968
- } catch {
33969
- continue;
33970
- }
33971
- return null;
33972
- };
33973
- const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
33974
- if (isRelativeExtendsValue(extendsValue)) return ensureJsonExtension(Path.resolve(fromConfigDirectory, extendsValue));
33975
- return resolvePackageExtendsPath(extendsValue, fromConfigDirectory);
33976
- };
33977
- const normalizeCompilerOptions = (compilerOptions) => {
33978
- if (!isPlainObject(compilerOptions)) return { hasExplicitLib: false };
33979
- const target = typeof compilerOptions.target === "string" ? compilerOptions.target : void 0;
33980
- const hasExplicitLib = Object.hasOwn(compilerOptions, "lib");
33981
- return {
33982
- target,
33983
- lib: Array.isArray(compilerOptions.lib) ? compilerOptions.lib.filter((entry) => typeof entry === "string") : void 0,
33984
- hasExplicitLib
33985
- };
33986
- };
33987
- const readTsConfig = (filePath) => {
33988
- let content;
33989
- try {
33990
- content = NFS.readFileSync(filePath, "utf-8");
33991
- } catch {
33992
- return null;
33993
- }
33994
- const parsed = ts.parseConfigFileTextToJson(filePath, content);
33995
- if (!isPlainObject(parsed.config)) return null;
33996
- return {
33997
- extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
33998
- compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
33999
- };
34000
- };
34001
- const mergeCompilerOptions = (inherited, current) => {
34002
- const target = current.target ?? inherited?.target;
34003
- const hasExplicitLib = current.hasExplicitLib || Boolean(inherited?.hasExplicitLib);
34004
- return {
34005
- target,
34006
- lib: current.hasExplicitLib ? current.lib : inherited?.lib,
34007
- hasExplicitLib
34008
- };
34009
- };
34010
- const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
34011
- const realPath = NFS.realpathSync.native(tsConfigPath);
34012
- if (visitedPaths.has(realPath)) return null;
34013
- const tsConfig = readTsConfig(realPath);
34014
- if (!tsConfig) return null;
34015
- const nextVisitedPaths = new Set(visitedPaths);
34016
- nextVisitedPaths.add(realPath);
34017
- if (tsConfig.extends && extendsDepth < 8) {
34018
- const parentPath = resolveExtendsPath(tsConfig.extends, Path.dirname(realPath));
34019
- if (parentPath && isFile(parentPath)) return mergeCompilerOptions(readResolvedCompilerOptions(parentPath, extendsDepth + 1, nextVisitedPaths), tsConfig.compilerOptions);
34020
- }
34021
- return tsConfig.compilerOptions;
34022
- };
34023
- const targetYearIsPreES2023 = (target) => {
34024
- const year = ES_TARGET_YEAR_BY_NAME[target.toLowerCase()];
34025
- return year !== void 0 && year < 2023;
34026
- };
34027
- const libEntryIncludesES2023Array = (entry) => {
34028
- const normalizedEntry = entry.toLowerCase();
34029
- if (normalizedEntry === "esnext" || normalizedEntry === "esnext.array") return true;
34030
- const esYearMatch = /^es(\d{4})(?:\.(.+))?$/.exec(normalizedEntry);
34031
- if (!esYearMatch) return false;
34032
- if (Number(esYearMatch[1]) < 2023) return false;
34033
- const component = esYearMatch[2];
34034
- return component === void 0 || component === "array";
34035
- };
34036
- const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
34037
- const compilerOptionsArePreES2023 = (compilerOptions) => {
34038
- if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
34039
- if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
34040
- return false;
34041
- };
34042
- const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
34043
- const detectPreES2023FromConfig = (tsConfigPath) => {
34044
- const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
34045
- if (!compilerOptions) return false;
34046
- if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) return false;
34047
- return compilerOptionsArePreES2023(compilerOptions);
34048
- };
34049
- const detectPreES2023Target = (directory) => {
34050
- const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
34051
- if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
34052
- return false;
34053
- };
34054
- const REACT_DEPENDENCY_NAMES = new Set([
34055
- "react",
34056
- "react-native",
34057
- "next",
34058
- "preact"
34059
- ]);
34060
- const hasReactDependency = (packageJson) => {
34061
- const allDependencies = {
34062
- ...packageJson.peerDependencies,
34063
- ...packageJson.dependencies,
34064
- ...packageJson.devDependencies
34065
- };
34066
- return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
34067
- };
34068
- const listWorkspacePackages = (rootDirectory) => {
34069
- const packageJsonPath = Path.join(rootDirectory, "package.json");
34070
- if (!isFile(packageJsonPath)) return [];
34071
- const packageJson = readPackageJson(packageJsonPath);
34072
- const patterns = getWorkspacePatterns(rootDirectory, packageJson);
34073
- if (patterns.length === 0) return [];
34074
- const packages = [];
34075
- const seenDirectories = /* @__PURE__ */ new Set();
34076
- const pushIfNew = (workspacePackage) => {
34077
- if (seenDirectories.has(workspacePackage.directory)) return;
34078
- seenDirectories.add(workspacePackage.directory);
34079
- packages.push(workspacePackage);
34080
- };
34081
- if (hasReactDependency(packageJson)) pushIfNew({
34082
- name: packageJson.name ?? Path.basename(rootDirectory),
34083
- directory: rootDirectory
34084
- });
34085
- for (const pattern of patterns) {
34086
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
34087
- for (const workspaceDirectory of directories) {
34088
- const workspacePackageJson = readPackageJson(Path.join(workspaceDirectory, "package.json"));
34089
- if (!hasReactDependency(workspacePackageJson)) continue;
34090
- pushIfNew({
34091
- name: workspacePackageJson.name ?? Path.basename(workspaceDirectory),
34092
- directory: workspaceDirectory
34093
- });
34094
- }
34095
- }
34096
- return packages;
34097
- };
34098
34188
  const toReactWorkspacePackages = (directories) => {
34099
34189
  const packages = [];
34100
34190
  for (const directory of directories) {
@@ -34167,14 +34257,13 @@ const clearProjectCache = () => {
34167
34257
  };
34168
34258
  /**
34169
34259
  * Build a `ProjectInfo` for a directory that has no `package.json` of
34170
- * its own — a package subfolder like `repo/packages` or `app/src/features`,
34171
- * or any loose tree of TypeScript/JavaScript files. Dependency + framework
34172
- * detection is inherited from the nearest enclosing package (a leaf
34173
- * workspace, a plain app root, or a monorepo root — whichever is closest,
34174
- * bounded by the git root), so scanning a subdirectory of a React project
34175
- * still gets the React capabilities; a standalone non-React directory simply
34176
- * scans with the framework-agnostic rules. Throws only when the directory has
34177
- * nothing to scan (no enclosing project and no source files of its own).
34260
+ * its own — a monorepo subfolder like `repo/packages`, or any loose tree
34261
+ * of TypeScript/JavaScript files. Dependency + framework detection is
34262
+ * inherited from the enclosing workspace root when there is one, so
34263
+ * scanning a subdirectory of a React monorepo still gets the React
34264
+ * capabilities; a standalone non-React directory simply scans with the
34265
+ * framework-agnostic rules. Throws only when the directory has nothing
34266
+ * to scan (no enclosing project and no source files of its own).
34178
34267
  */
34179
34268
  const discoverProjectWithoutPackageJson = (directory) => {
34180
34269
  const sourceFileCount = countSourceFiles(directory);
@@ -34210,7 +34299,9 @@ const discoverProjectWithoutPackageJson = (directory) => {
34210
34299
  shopifyFlashListVersion: null,
34211
34300
  shopifyFlashListMajorVersion: null,
34212
34301
  hasReanimated: false,
34302
+ reanimatedVersion: null,
34213
34303
  isPreES2023Target: hasOwnTsConfig && detectPreES2023Target(directory),
34304
+ isStaticExport: false,
34214
34305
  sourceFileCount
34215
34306
  };
34216
34307
  };
@@ -34224,89 +34315,85 @@ const discoverProject = (directory) => {
34224
34315
  return synthesized;
34225
34316
  }
34226
34317
  const packageJson = readPackageJson(packageJsonPath);
34227
- let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
34228
- const reactDeclaration = getDependencyDeclaration({
34229
- packageJson,
34230
- packageName: "react",
34231
- sections: [
34232
- "dependencies",
34233
- "peerDependencies",
34234
- "devDependencies"
34235
- ]
34236
- });
34237
- const tailwindDeclaration = getDependencyDeclaration({
34238
- packageJson,
34239
- packageName: "tailwindcss",
34240
- sections: [
34241
- "dependencies",
34242
- "devDependencies",
34243
- "peerDependencies"
34244
- ]
34245
- });
34246
- 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({
34247
34335
  packageJson,
34248
- packageName: "zod",
34249
- sections: [
34250
- "dependencies",
34251
- "devDependencies",
34252
- "peerDependencies"
34253
- ]
34254
- });
34255
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
34256
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
34257
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
34258
- 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) {
34259
34344
  const monorepoRoot = findMonorepoRoot(directory);
34260
34345
  if (monorepoRoot) {
34261
34346
  const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
34262
- if (isFile(monorepoPackageJsonPath)) {
34263
- const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
34264
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
34265
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
34266
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
34267
- }
34347
+ if (isFile(monorepoPackageJsonPath)) fillFromCatalogs(readPackageJson(monorepoPackageJsonPath), monorepoRoot);
34268
34348
  }
34269
34349
  }
34270
- if (!reactVersion || framework === "unknown") {
34271
- const workspaceInfo = findReactInWorkspaces(directory, packageJson);
34272
- if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
34273
- if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
34274
- if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
34275
- 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;
34276
34357
  }
34277
- if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
34358
+ if ((!tracked.react.version || framework === "unknown") && !isMonorepoRoot(directory)) {
34278
34359
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
34279
- if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
34280
- if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
34281
- if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
34360
+ tracked.react.version ||= monorepoInfo.reactVersion;
34361
+ tracked.tailwindcss.version ||= monorepoInfo.tailwindVersion;
34362
+ tracked.zod.version ||= monorepoInfo.zodVersion;
34282
34363
  if (framework === "unknown") framework = monorepoInfo.framework;
34283
34364
  }
34284
- if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
34285
- if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
34286
- 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;
34287
34371
  if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
34372
+ const tailwindVersion = tailwindcss.version;
34373
+ const zodVersion = zod.version;
34288
34374
  const projectName = packageJson.name ?? Path.basename(directory);
34289
34375
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
34290
34376
  const sourceFileCount = countSourceFiles(directory);
34291
- const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
34377
+ const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
34292
34378
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
34293
34379
  rootDirectory: directory,
34294
34380
  rootPackageJson: packageJson,
34295
34381
  packageName: "expo",
34296
- version: findExpoVersion(directory, packageJson)
34382
+ version: workspaceFacts.expo.version
34297
34383
  }) : null;
34298
34384
  const shopifyFlashListVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
34299
34385
  rootDirectory: directory,
34300
34386
  rootPackageJson: packageJson,
34301
34387
  packageName: SHOPIFY_FLASH_LIST_PACKAGE_NAME,
34302
- version: findShopifyFlashListVersion(directory, packageJson)
34388
+ version: workspaceFacts.shopifyFlashList.version
34303
34389
  }) : null;
34304
- const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
34390
+ const hasReanimated = hasReactNativeWorkspace && workspaceFacts.hasReanimatedAwarePackage;
34391
+ const reanimatedVersion = hasReanimated ? workspaceFacts.reanimatedVersion : null;
34305
34392
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
34306
34393
  rootDirectory: directory,
34307
34394
  rootPackageJson: packageJson,
34308
34395
  packageName: "next",
34309
- version: findNextjsVersion(directory, packageJson)
34396
+ version: workspaceFacts.next.version
34310
34397
  }) : null;
34311
34398
  const preactVersion = getPreactVersion(packageJson);
34312
34399
  const isPreES2023Target = hasTypeScript && detectPreES2023Target(directory);
@@ -34317,7 +34404,7 @@ const discoverProject = (directory) => {
34317
34404
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
34318
34405
  tailwindVersion,
34319
34406
  zodVersion,
34320
- zodMajorVersion: parseZodMajor(zodVersion),
34407
+ zodMajorVersion: zodVersion === null ? null : getLowestDependencyMajor(zodVersion),
34321
34408
  framework,
34322
34409
  hasTypeScript,
34323
34410
  hasReactCompiler: detectReactCompiler(directory, packageJson),
@@ -34331,7 +34418,9 @@ const discoverProject = (directory) => {
34331
34418
  shopifyFlashListVersion,
34332
34419
  shopifyFlashListMajorVersion: shopifyFlashListVersion === null ? null : getLowestDependencyMajor(shopifyFlashListVersion),
34333
34420
  hasReanimated,
34421
+ reanimatedVersion,
34334
34422
  isPreES2023Target,
34423
+ isStaticExport: framework === "nextjs" && detectNextjsStaticExport(workspaceFacts.next.sourceDirectory ?? directory),
34335
34424
  sourceFileCount
34336
34425
  };
34337
34426
  cachedProjectInfos.set(directory, projectInfo);
@@ -34347,57 +34436,6 @@ const discoverProject = (directory) => {
34347
34436
  * single source file has nothing to analyze.
34348
34437
  */
34349
34438
  const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null || project.sourceFileCount > 0;
34350
- const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
34351
- const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
34352
- const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
34353
- const parseReactMajorMinor = (reactVersion) => {
34354
- if (typeof reactVersion !== "string") return null;
34355
- const trimmed = reactVersion.trim();
34356
- if (trimmed.length === 0) return null;
34357
- const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
34358
- if (lowerBoundsOnly.length === 0) return null;
34359
- const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
34360
- if (majorMinorMatch) {
34361
- const major = Number.parseInt(majorMinorMatch[1], 10);
34362
- const minor = Number.parseInt(majorMinorMatch[2], 10);
34363
- if (!Number.isFinite(major) || major <= 0) return null;
34364
- if (!Number.isFinite(minor) || minor < 0) return null;
34365
- return {
34366
- major,
34367
- minor
34368
- };
34369
- }
34370
- const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
34371
- if (!majorOnlyMatch) return null;
34372
- const major = Number.parseInt(majorOnlyMatch[1], 10);
34373
- if (!Number.isFinite(major) || major <= 0) return null;
34374
- return {
34375
- major,
34376
- minor: 0
34377
- };
34378
- };
34379
- const isReactAtLeast = (detected, required) => {
34380
- if (detected === null) return true;
34381
- if (detected.major !== required.major) return detected.major > required.major;
34382
- return detected.minor >= required.minor;
34383
- };
34384
- const parseLowerBoundVersion = (versionSpec) => import_semver.validRange(versionSpec) !== null ? import_semver.minVersion(versionSpec) : import_semver.coerce(versionSpec);
34385
- const parseTailwindMajorMinor = (tailwindVersion) => {
34386
- if (typeof tailwindVersion !== "string") return null;
34387
- const trimmed = tailwindVersion.trim();
34388
- if (trimmed.length === 0) return null;
34389
- const lowerBound = parseLowerBoundVersion(trimmed);
34390
- if (lowerBound === null || lowerBound.major <= 0) return null;
34391
- return {
34392
- major: lowerBound.major,
34393
- minor: lowerBound.minor
34394
- };
34395
- };
34396
- const isTailwindAtLeast = (detected, required) => {
34397
- if (detected === null) return true;
34398
- if (detected.major !== required.major) return detected.major > required.major;
34399
- return detected.minor >= required.minor;
34400
- };
34401
34439
  const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
34402
34440
  var InvalidGlobPatternError = class extends Error {
34403
34441
  pattern;
@@ -34523,6 +34561,9 @@ const buildRuleSeverityControls = (config) => {
34523
34561
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
34524
34562
  };
34525
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]*)/;
34526
34567
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34527
34568
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
34528
34569
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -34600,6 +34641,7 @@ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34600
34641
  "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
34601
34642
  "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
34602
34643
  "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
34644
+ "react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
34603
34645
  "react/jsx-no-undef": "react-doctor/jsx-no-undef",
34604
34646
  "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
34605
34647
  "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
@@ -34673,9 +34715,6 @@ const tokenizeRuleList = (ruleList) => {
34673
34715
  if (!ruleSection) return [];
34674
34716
  return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
34675
34717
  };
34676
- const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34677
- const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34678
- const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34679
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}\`).`;
34680
34719
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34681
34720
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
@@ -34729,6 +34768,53 @@ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34729
34768
  if (!ruleId.startsWith("react-doctor/")) return null;
34730
34769
  return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
34731
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
+ };
34732
34818
  const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34733
34819
  const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34734
34820
  const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
@@ -34872,6 +34958,11 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
34872
34958
  isSuppressed: true,
34873
34959
  nearMissHint: null
34874
34960
  };
34961
+ if (isCompilerRuleForeignDisabled(lines, diagnosticLineIndex, ruleId)) return {
34962
+ isSuppressed: true,
34963
+ nearMissHint: null,
34964
+ isForeignDirective: true
34965
+ };
34875
34966
  const directComments = findStackedDisableCommentsAbove(lines, diagnosticLineIndex);
34876
34967
  if (hasChainSuppressor(directComments, ruleId)) return {
34877
34968
  isSuppressed: true,
@@ -35157,12 +35248,18 @@ const collectStringSet = (values) => {
35157
35248
  return new Set(values.filter((value) => typeof value === "string"));
35158
35249
  };
35159
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));
35160
35257
  /**
35161
35258
  * Pre-compiles every stateful filter and returns a single
35162
35259
  * `apply(diagnostic)` closure that runs (in order):
35163
35260
  *
35164
35261
  * 1. auto-suppress (test-noise rules in test files; `migration-hint`
35165
- * wins over `test-noise`)
35262
+ * wins over `test-noise`; `no-console` on Node CLI scripts)
35166
35263
  * 2. severity overrides (top-level `rules` / `categories`, with
35167
35264
  * `"off"` dropping)
35168
35265
  * 3. warning suppression (only when `showWarnings` is false: drops every
@@ -35260,6 +35357,12 @@ const buildDiagnosticPipeline = (input) => {
35260
35357
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35261
35358
  return false;
35262
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
+ };
35263
35366
  const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35264
35367
  if (!hasRuntimeGlobals) return false;
35265
35368
  if (diagnostic.rule !== "jsx-no-undef") return false;
@@ -35269,6 +35372,7 @@ const buildDiagnosticPipeline = (input) => {
35269
35372
  return {
35270
35373
  apply: (diagnostic) => {
35271
35374
  if (shouldAutoSuppress(diagnostic)) return null;
35375
+ if (isNoConsoleOnNodeCliScript(diagnostic)) return null;
35272
35376
  let current = diagnostic;
35273
35377
  let explicitSeverityOverride;
35274
35378
  let explicitRuleOverride;
@@ -35298,7 +35402,7 @@ const buildDiagnosticPipeline = (input) => {
35298
35402
  if (lines) {
35299
35403
  const ruleIdentifier = `${current.plugin}/${current.rule}`;
35300
35404
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35301
- if (evaluation.isSuppressed) return suppress(current, "inline");
35405
+ if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
35302
35406
  if (evaluation.nearMissHint) current = {
35303
35407
  ...current,
35304
35408
  suppressionHint: evaluation.nearMissHint
@@ -37107,64 +37211,64 @@ function* collectSecurityScanFiles(rootDirectory) {
37107
37211
  }
37108
37212
  }
37109
37213
  }
37110
- /**
37111
- * Whether project discovery resolved a React-compatible runtime (React or
37112
- * Preact, which ships the same hooks + JSX model). This is the same
37113
- * predicate that grants the `react` capability, so `false` means every
37114
- * React-runtime rule family was gated off for the project.
37115
- */
37116
- 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
+ };
37117
37226
  const buildCapabilities = (project) => {
37118
37227
  const capabilities = /* @__PURE__ */ new Set();
37119
37228
  capabilities.add(project.framework);
37120
- if (hasReactRuntime(project)) capabilities.add("react");
37229
+ if (project.reactVersion !== null || project.preactVersion !== null) capabilities.add("react");
37121
37230
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
37122
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");
37123
37235
  if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
37124
- const reactMajor = project.reactMajorVersion;
37125
- if (reactMajor !== null) {
37126
- const cappedReactMajor = Math.min(reactMajor, 30);
37127
- for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
37128
- if (reactMajor >= 19) {
37129
- if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
37130
- major: 19,
37131
- minor: 2
37132
- })) capabilities.add("react:19.2");
37133
- }
37134
- }
37135
- if (project.tailwindVersion !== null) {
37136
- capabilities.add("tailwind");
37137
- if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
37138
- major: 3,
37139
- minor: 4
37140
- })) capabilities.add("tailwind:3.4");
37141
- }
37142
- if (project.zodVersion !== null) {
37143
- capabilities.add("zod");
37144
- if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
37145
- }
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");
37146
37248
  if (project.isPreES2023Target) capabilities.add("pre-es2023");
37147
37249
  if (project.hasReactCompiler) capabilities.add("react-compiler");
37148
37250
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
37149
37251
  if (project.hasTypeScript) capabilities.add("typescript");
37150
- if (project.preactVersion !== null) {
37151
- capabilities.add("preact");
37152
- const preactMajor = project.preactMajorVersion;
37153
- if (preactMajor !== null) {
37154
- const cappedPreactMajor = Math.min(preactMajor, 20);
37155
- for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
37156
- }
37157
- if (project.reactVersion === null) capabilities.add("pure-preact");
37158
- }
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");
37159
37255
  return capabilities;
37160
37256
  };
37161
- const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
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);
37263
+ return capabilities;
37264
+ };
37265
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhen) => {
37162
37266
  if (requires) {
37163
37267
  for (const capability of requires) if (!capabilities.has(capability)) return false;
37164
37268
  }
37165
37269
  if (tags?.includes("react-jsx-only") && !capabilities.has("react")) return false;
37166
- if (disabledBy) {
37167
- 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;
37168
37272
  }
37169
37273
  if (tags) {
37170
37274
  for (const tag of tags) if (ignoredTags.has(tag)) return false;
@@ -37175,14 +37279,14 @@ const yieldToEventLoop = () => new Promise((resolve) => {
37175
37279
  setImmediate(resolve);
37176
37280
  });
37177
37281
  const createSecurityScanSession = (rootDirectory, options) => {
37178
- const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
37282
+ const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
37179
37283
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
37180
37284
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
37181
37285
  const rule = entry.rule;
37182
37286
  const scan = rule.scan;
37183
37287
  if (typeof scan !== "function") return [];
37184
37288
  if (rule.defaultEnabled === false) return [];
37185
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) return [];
37289
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) return [];
37186
37290
  return [{
37187
37291
  entry,
37188
37292
  scan,
@@ -37202,6 +37306,7 @@ const createSecurityScanSession = (rootDirectory, options) => {
37202
37306
  return status === true;
37203
37307
  };
37204
37308
  const scanFileByRule = function* (file) {
37309
+ if (file === null) return;
37205
37310
  for (const { entry, scan, committedFilesOnly } of enabledScanRules) {
37206
37311
  for (const finding of scan(file)) {
37207
37312
  if (committedFilesOnly && isFileGitIgnored(file)) continue;
@@ -39082,7 +39187,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
39082
39187
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
39083
39188
  const jsPlugins = [];
39084
39189
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
39085
- const capabilities = buildCapabilities(project);
39190
+ const capabilities = getCapabilities(project);
39086
39191
  const enabledReactDoctorRules = {};
39087
39192
  for (const registryEntry of REACT_DOCTOR_RULES) {
39088
39193
  const rule = reactDoctorPlugin.rules[registryEntry.id];
@@ -39093,7 +39198,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
39093
39198
  if (rule.scan !== void 0) continue;
39094
39199
  if (customRulesOnly && registryEntry.originallyExternal) continue;
39095
39200
  if (rule.framework !== "global" && !rule.requires) continue;
39096
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
39201
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) continue;
39097
39202
  const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
39098
39203
  if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
39099
39204
  const severity = resolveRuleSeverityOverride({
@@ -39124,6 +39229,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
39124
39229
  settings: { "react-doctor": {
39125
39230
  framework: project.framework,
39126
39231
  rootDirectory: resolveSettingsRootDirectory(project.rootDirectory),
39232
+ capabilities: [...capabilities].sort(),
39127
39233
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
39128
39234
  ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
39129
39235
  } },
@@ -39414,25 +39520,18 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39414
39520
  if (errors.length > 0) throw errors[0];
39415
39521
  return results;
39416
39522
  };
39417
- const getPublicEnvPrefix = (framework) => {
39418
- switch (framework) {
39419
- case "nextjs": return "NEXT_PUBLIC_*";
39420
- case "vite":
39421
- case "tanstack-start": return "VITE_*";
39422
- case "cra": return "REACT_APP_*";
39423
- case "gatsby": return "GATSBY_*";
39424
- default: return null;
39425
- }
39426
- };
39427
- const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
39428
- const publicEnvPrefix = getPublicEnvPrefix(project.framework);
39429
- if (!publicEnvPrefix) return fallbackRecommendation;
39430
- 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`;
39431
- };
39432
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
+ };
39433
39531
  const appendReanimatedSharedValueHint = (help, rule, project) => {
39434
39532
  if (rule !== "immutability") return help;
39435
39533
  if (!project.hasReanimated) return help;
39534
+ if (!supportsAccessors(project.reanimatedVersion)) return help;
39436
39535
  if (!help) return REANIMATED_SHARED_VALUE_HINT;
39437
39536
  return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
39438
39537
  };
@@ -39594,13 +39693,13 @@ const REACT_USE_BINDING_RESOLUTION = {
39594
39693
  isReactUseBinding: true,
39595
39694
  isReactNamespaceBinding: false
39596
39695
  };
39597
- const getScriptKind = (filename) => {
39696
+ const getScriptKind$2 = (filename) => {
39598
39697
  if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
39599
39698
  if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
39600
39699
  if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
39601
39700
  return ts.ScriptKind.JS;
39602
39701
  };
39603
- 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;
39604
39703
  const unwrapExpression = (expression) => {
39605
39704
  let currentExpression = expression;
39606
39705
  while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
@@ -39849,8 +39948,8 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
39849
39948
  return matchedIdentifier;
39850
39949
  };
39851
39950
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
39852
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filename));
39853
- 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));
39854
39953
  if (!useIdentifier) return null;
39855
39954
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
39856
39955
  };
@@ -39871,6 +39970,201 @@ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
39871
39970
  const bindingResolution = resolveUseCallBinding(sourceText, absolutePath, primaryLabel.span.offset);
39872
39971
  return bindingResolution !== null && !bindingResolution.isReactUseBinding;
39873
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
+ };
39874
40168
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
39875
40169
  const LEADING_SEVERITY_LABEL_PATTERN = /^(?:Error|Warning):\s*/;
39876
40170
  const TRAILING_PERIOD_PATTERN = /\.$/;
@@ -39906,8 +40200,14 @@ const PLUGIN_CATEGORY_MAP = {
39906
40200
  };
39907
40201
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
39908
40202
  const getRuleRecommendation = (ruleName, project) => {
39909
- if (ruleName === "no-secrets-in-client-code") return buildNoSecretsRecommendation(project, reactDoctorPlugin.rules["no-secrets-in-client-code"]?.recommendation ?? "Move secrets to server-only code");
39910
- 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;
39911
40211
  };
39912
40212
  const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
39913
40213
  const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
@@ -40010,7 +40310,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40010
40310
  minifiedFileCache.set(absolutePath, minified);
40011
40311
  return minified;
40012
40312
  };
40013
- 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) => {
40014
40314
  const { plugin, rule } = parseRuleCode(diagnostic.code);
40015
40315
  const primaryLabel = diagnostic.labels[0];
40016
40316
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -40037,7 +40337,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40037
40337
  ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
40038
40338
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
40039
40339
  };
40040
- });
40340
+ }), rootDirectory);
40041
40341
  };
40042
40342
  const buildOxlintChildEnv = (sourceEnv) => {
40043
40343
  const childEnv = {};
@@ -40061,7 +40361,9 @@ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
40061
40361
  * - `OxlintBatchExceeded { kind: "output-too-large" }` — stdout+stderr
40062
40362
  * crossed `OXLINT_OUTPUT_MAX_BYTES`.
40063
40363
  * - `OxlintBatchExceeded { kind: "oom" | "killed" }` — child exited
40064
- * 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).
40065
40367
  * - `OxlintSpawnFailed { cause }` — `spawn` itself errored, or the
40066
40368
  * child exited successfully but printed only stderr.
40067
40369
  *
@@ -40127,7 +40429,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
40127
40429
  clearAbortListener();
40128
40430
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
40129
40431
  });
40130
- child.on("close", (_code, signal) => {
40432
+ child.on("close", (code, signal) => {
40131
40433
  clearTimeout(timeoutHandle);
40132
40434
  clearAbortListener();
40133
40435
  if (didKillForSize) {
@@ -40137,10 +40439,11 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
40137
40439
  }) }));
40138
40440
  return;
40139
40441
  }
40140
- if (signal) {
40442
+ const isAbortExitCode = code !== null && ABORT_EXIT_CODES.has(code);
40443
+ if (signal || isAbortExitCode) {
40141
40444
  const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
40142
- const isOom = signal === "SIGABRT";
40143
- const detailParts = [`killed by ${signal}`];
40445
+ const isOom = signal === "SIGABRT" || isAbortExitCode;
40446
+ const detailParts = [signal ? `killed by ${signal}` : `aborted with exit code ${code}`];
40144
40447
  if (isOom) detailParts.push("try scanning fewer files with --diff");
40145
40448
  if (stderrOutput) detailParts.push(stderrOutput);
40146
40449
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
@@ -40183,12 +40486,19 @@ const isParallelismRelatedSpawnError = (error) => {
40183
40486
  * (surfaced via `onPartialFailure`) so JSON-mode consumers see WHICH
40184
40487
  * files were skipped instead of silently losing them.
40185
40488
  *
40186
- * Parallel runs (concurrency > 1) get one extra safety net: if the pass
40187
- * fails with a resource-exhaustion error that's exclusive to running
40188
- * many oxlint subprocesses at once (EAGAIN / EMFILE / ENFILE / ENOMEM —
40189
- * see `isParallelismRelatedSpawnError`), the whole pass replays once
40190
- * with a single worker. That's the only failure a serial replay can
40191
- * 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.
40192
40502
  *
40193
40503
  * Errors that aren't splittable and aren't parallelism-related (oxlint
40194
40504
  * config crash, JS plugin resolution failure, etc.) propagate to the
@@ -40198,13 +40508,16 @@ const isParallelismRelatedSpawnError = (error) => {
40198
40508
  const spawnLintBatches = async (input) => {
40199
40509
  const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
40200
40510
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
40201
- const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
40202
- 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);
40203
40515
  const allDiagnostics = [];
40204
40516
  const droppedFiles = [];
40517
+ const oomDroppedFiles = [];
40205
40518
  const deadlineSkippedFiles = [];
40206
- const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0;
40207
40519
  let firstDropReason = null;
40520
+ let firstNonOomDropReason = null;
40208
40521
  const spawnLintBatch = async (batch, depth, batchState) => {
40209
40522
  if (isPastDeadline()) {
40210
40523
  deadlineSkippedFiles.push(...batch);
@@ -40226,12 +40539,14 @@ const spawnLintBatches = async (input) => {
40226
40539
  const isDepthCapReached = depth >= splitMaxDepth;
40227
40540
  if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
40228
40541
  droppedFiles.push(...batch);
40229
- if (firstDropReason === null) {
40230
- let limitHint = "";
40231
- if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
40232
- else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
40233
- firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
40234
- }
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;
40235
40550
  return [];
40236
40551
  }
40237
40552
  const splitIndex = Math.ceil(batch.length / 2);
@@ -40241,16 +40556,16 @@ const spawnLintBatches = async (input) => {
40241
40556
  let startedFileCount = 0;
40242
40557
  let scannedFileCount = 0;
40243
40558
  let displayedFileCount = 0;
40244
- const progressTimer = onFileProgress && totalFileCount > 1 ? setInterval(() => {
40559
+ const progressTimer = passOnFileProgress && totalFileCount > 1 ? setInterval(() => {
40245
40560
  const ceiling = Math.min(startedFileCount, totalFileCount - 1);
40246
40561
  if (displayedFileCount < ceiling) {
40247
40562
  displayedFileCount += 1;
40248
- onFileProgress(displayedFileCount, totalFileCount);
40563
+ passOnFileProgress(displayedFileCount, totalFileCount);
40249
40564
  }
40250
40565
  }, 50) : null;
40251
40566
  progressTimer?.unref?.();
40252
40567
  try {
40253
- const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
40568
+ const batchResults = await mapWithConcurrency(passBatches, concurrency, async (batch) => {
40254
40569
  if (isPastDeadline()) {
40255
40570
  deadlineSkippedFiles.push(...batch);
40256
40571
  return [];
@@ -40262,9 +40577,9 @@ const spawnLintBatches = async (input) => {
40262
40577
  };
40263
40578
  const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
40264
40579
  scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
40265
- if (onFileProgress) {
40580
+ if (passOnFileProgress) {
40266
40581
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
40267
- onFileProgress(displayedFileCount, totalFileCount);
40582
+ passOnFileProgress(displayedFileCount, totalFileCount);
40268
40583
  }
40269
40584
  return batchDiagnostics;
40270
40585
  });
@@ -40272,22 +40587,53 @@ const spawnLintBatches = async (input) => {
40272
40587
  } finally {
40273
40588
  if (progressTimer !== null) clearInterval(progressTimer);
40274
40589
  }
40275
- const reportSkippedFiles = (skippedFiles, buildMessage) => {
40276
- if (skippedFiles.length === 0 || !onPartialFailure) return;
40277
- 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
40278
40597
  };
40279
- const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
40280
- reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
40281
- reportSkippedFiles(deadlineSkippedFiles, (fileListPreview) => `${deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
40282
- return allDiagnostics;
40283
40598
  };
40284
- let diagnostics;
40599
+ let outcome;
40600
+ let effectiveConcurrency = requestedConcurrency;
40285
40601
  try {
40286
- diagnostics = await runBatchPass(requestedConcurrency);
40602
+ outcome = await runBatchPass(requestedConcurrency, fileBatches, onFileProgress);
40287
40603
  } catch (error) {
40288
40604
  if (requestedConcurrency <= 1 || !isParallelismRelatedSpawnError(error)) throw error;
40289
- 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;
40290
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})`);
40291
40637
  return dedupeDiagnostics(diagnostics);
40292
40638
  };
40293
40639
  let didValidate = false;
@@ -40548,6 +40894,7 @@ const runOxlint = async (options) => {
40548
40894
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
40549
40895
  const severityControls = buildRuleSeverityControls(userConfig);
40550
40896
  validateRuleRegistration();
40897
+ resetManifestCaches();
40551
40898
  if (includePaths !== void 0 && includePaths.length === 0) return [];
40552
40899
  const pluginPath = resolvePluginPath();
40553
40900
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
@@ -41210,9 +41557,10 @@ const CONTEXT_AXES = [
41210
41557
  }
41211
41558
  ];
41212
41559
  const SCORE_AXES = [...GATED_AXES, ...CONTEXT_AXES];
41213
- const worstGatedAxis = (score) => {
41214
- let worst = GATED_AXES[0];
41215
- 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;
41216
41564
  return worst;
41217
41565
  };
41218
41566
  const clampScore = (value) => {
@@ -41236,6 +41584,69 @@ const resolveConcreteVersion = (spec) => {
41236
41584
  if (range === null || range === "*") return null;
41237
41585
  return import_semver.minVersion(trimmed)?.version ?? null;
41238
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
+ };
41239
41650
  const locateDependencyKey = (packageJsonText, section, name) => {
41240
41651
  const needle = `"${name}"`;
41241
41652
  const sectionHeader = new RegExp(`"${section}"\\s*:\\s*\\{`);
@@ -41262,7 +41673,7 @@ const locateDependencyKey = (packageJsonText, section, name) => {
41262
41673
  }
41263
41674
  return null;
41264
41675
  };
41265
- const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies) => {
41676
+ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies, lockedVersionLookup) => {
41266
41677
  const sectionByName = /* @__PURE__ */ new Map();
41267
41678
  for (const name of Object.keys(packageJson.dependencies ?? {})) sectionByName.set(name, "dependencies");
41268
41679
  if (includeDevDependencies) {
@@ -41272,13 +41683,18 @@ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDepe
41272
41683
  for (const [name, section] of sectionByName) {
41273
41684
  if (SUPPLY_CHAIN_IGNORED_PACKAGES.has(name)) continue;
41274
41685
  const spec = (packageJson[section] ?? {})[name] ?? "";
41275
- const version = resolveConcreteVersion(spec);
41276
- 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;
41277
41691
  const location = locateDependencyKey(packageJsonText, section, name);
41278
41692
  dependencies.push({
41279
41693
  name,
41280
- version,
41694
+ version: lockSatisfiesSpec ? lockedVersion : floorVersion,
41281
41695
  spec,
41696
+ section,
41697
+ resolution: isExactPin ? "pin" : lockSatisfiesSpec ? "locked" : "floor",
41282
41698
  line: location?.line ?? 0,
41283
41699
  column: location?.column ?? 0
41284
41700
  });
@@ -41429,7 +41845,11 @@ const formatAlertReason = (topAlerts, totalCount) => {
41429
41845
  }
41430
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)}.`;
41431
41847
  };
41432
- 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
+ };
41433
41853
  const buildSupplyChainHelp = (dependency, failingAxis, topAlerts, packagePageUrl, options) => {
41434
41854
  const hasCriticalAlert = topAlerts.some((alert) => alert.severity.toLowerCase() === "critical");
41435
41855
  const entry = `\`"${dependency.name}": "${dependency.spec}"\``;
@@ -41472,7 +41892,7 @@ const buildLowScoreDiagnostic = (dependency, artifact, failingAxis, options) =>
41472
41892
  const checkSupplyChain = (input) => gen(function* () {
41473
41893
  const options = resolveOptions(input.userConfig);
41474
41894
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
41475
- const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
41895
+ const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies, buildLockedVersionLookup(input.rootDirectory));
41476
41896
  if (dependencies.length === 0) return [];
41477
41897
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
41478
41898
  if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
@@ -41481,7 +41901,7 @@ const checkSupplyChain = (input) => gen(function* () {
41481
41901
  for (let index = 0; index < dependencies.length; index += 1) {
41482
41902
  const artifact = artifacts[index];
41483
41903
  if (!artifact) continue;
41484
- const worstAxis = worstGatedAxis(artifact.score);
41904
+ const worstAxis = worstGatedAxis(artifact.score, gatedAxesForSection(dependencies[index].section));
41485
41905
  if (toHundred(artifact.score[worstAxis.key]) >= options.minScore) continue;
41486
41906
  diagnostics.push(buildLowScoreDiagnostic(dependencies[index], artifact, worstAxis, options));
41487
41907
  }
@@ -42158,6 +42578,13 @@ const summarizeDiagnostics = (diagnostics, worstScore = null, worstScoreLabel =
42158
42578
  scoreLabel: worstScoreLabel
42159
42579
  };
42160
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;
42161
42588
  const toJsonDiff = (diff) => {
42162
42589
  if (!diff) return null;
42163
42590
  return {
@@ -42392,17 +42819,20 @@ const warnIfAiTrainingEnvironment = () => {
42392
42819
  didWarnAiTraining = true;
42393
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.");
42394
42821
  };
42395
- const buildDiagnoseLayer = (config, configOverrideTarget) => {
42396
- const configLayer = configOverrideTarget === void 0 ? Config.layerNode : Config.layerOf({
42397
- config,
42398
- resolvedDirectory: configOverrideTarget.resolvedDirectory,
42399
- 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
42400
42829
  });
42401
- 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([]));
42402
42831
  };
42403
42832
  const buildInspectProgram = (scanTarget, options, configOverride) => {
42404
42833
  const effectiveConfig = configOverride ?? scanTarget.userConfig;
42405
42834
  const includePaths = options.includePaths ?? [];
42835
+ const shouldRunDeadCode = resolveShouldRunDeadCode(options, effectiveConfig);
42406
42836
  return runInspect({
42407
42837
  directory: scanTarget.resolvedDirectory,
42408
42838
  includePaths,
@@ -42411,7 +42841,7 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
42411
42841
  warnings: options.warnings ?? effectiveConfig?.warnings ?? true,
42412
42842
  adoptExistingLintConfig: effectiveConfig?.adoptExistingLintConfig ?? true,
42413
42843
  ignoredTags: new Set(effectiveConfig?.ignore?.tags ?? []),
42414
- runDeadCode: options.deadCode ?? effectiveConfig?.deadCode ?? true,
42844
+ runDeadCode: shouldRunDeadCode,
42415
42845
  isCi: false,
42416
42846
  resolveLocalGithubViewerPermission: true
42417
42847
  });
@@ -42433,7 +42863,14 @@ const diagnoseDirectory = async (directory, options) => {
42433
42863
  warnIfAiTrainingEnvironment();
42434
42864
  const startTime = globalThis.performance.now();
42435
42865
  const scanTarget = await resolveScanTarget(directory);
42436
- 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);
42437
42874
  };
42438
42875
  const findWorstScore = (projectResults) => {
42439
42876
  let worstResult = null;
@@ -42454,15 +42891,27 @@ const diagnoseProject = async (projectDefinition, baseOptions, batchConfig) => {
42454
42891
  const { directory: _, config: projectConfig, ...perProjectOptions } = projectDefinition;
42455
42892
  const didOverrideConfig = batchConfig !== void 0 || projectConfig !== void 0;
42456
42893
  const effectiveConfig = mergeReactDoctorConfigs(mergeReactDoctorConfigs(scanTarget.userConfig, batchConfig), projectConfig);
42457
- const program = buildInspectProgram(scanTarget, {
42894
+ const mergedOptions = {
42458
42895
  ...baseOptions,
42459
42896
  ...perProjectOptions
42460
- }, 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);
42461
42901
  const didOverridePlugins = batchConfig?.plugins !== void 0 || projectConfig?.plugins !== void 0;
42462
- const layer = buildDiagnoseLayer(effectiveConfig, didOverrideConfig ? {
42463
- resolvedDirectory: scanTarget.resolvedDirectory,
42464
- configSourceDirectory: didOverridePlugins ? null : scanTarget.configSourceDirectory
42465
- } : 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
+ });
42466
42915
  return {
42467
42916
  ok: true,
42468
42917
  ...outputToDiagnoseResult(await runPromise(restoreLegacyThrow(program.pipe(provide(layer), provide(layerOtlp)))), globalThis.performance.now() - startTime),
@@ -42524,4 +42973,4 @@ const toJsonReport = (result, options) => buildJsonReport({
42524
42973
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
42525
42974
 
42526
42975
  //# sourceMappingURL=index.js.map
42527
- //# debugId=f00947fe-3794-5d73-a491-232b535dc962
42976
+ //# debugId=3dc4a1ac-42dd-51e8-ba38-0260edf45659