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/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="47338856-c658-5c78-a5e2-e871c2dafaa7")}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]="9aed4e70-dd94-51e8-9810-f8e8945d80a3")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -9,7 +9,7 @@ import * as NFS from "node:fs";
9
9
  import fs, { mkdtempSync, rmSync } from "node:fs";
10
10
  import process$1 from "node:process";
11
11
  import * as ts from "typescript";
12
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
+ import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
13
13
  import * as OS from "node:os";
14
14
  import os, { tmpdir } from "node:os";
15
15
  import { parseJSON5 } from "confbox";
@@ -35959,6 +35959,25 @@ var AmbiguousProjectError = class extends Error {
35959
35959
  }
35960
35960
  };
35961
35961
  const isProjectDiscoveryError = (value) => value instanceof ProjectNotFoundError || value instanceof NoReactDependencyError || value instanceof PackageJsonNotFoundError || value instanceof NotADirectoryError || value instanceof AmbiguousProjectError;
35962
+ const isErrnoException = (error) => error instanceof Error && "code" in error;
35963
+ const IGNORABLE_READDIR_ERROR_CODES = new Set([
35964
+ "EACCES",
35965
+ "EPERM",
35966
+ "ENOENT",
35967
+ "ENOTDIR",
35968
+ "EINVAL",
35969
+ "ELOOP",
35970
+ "ENAMETOOLONG"
35971
+ ]);
35972
+ const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
35973
+ const readDirectoryEntries = (directoryPath) => {
35974
+ try {
35975
+ return NFS.readdirSync(directoryPath, { withFileTypes: true });
35976
+ } catch (error) {
35977
+ if (isIgnorableReaddirError(error)) return [];
35978
+ throw error;
35979
+ }
35980
+ };
35962
35981
  const isFile = (filePath) => {
35963
35982
  try {
35964
35983
  return NFS.statSync(filePath).isFile();
@@ -35966,10 +35985,24 @@ const isFile = (filePath) => {
35966
35985
  return false;
35967
35986
  }
35968
35987
  };
35988
+ const isDirectory = (directoryPath) => {
35989
+ try {
35990
+ return NFS.statSync(directoryPath).isDirectory();
35991
+ } catch {
35992
+ return false;
35993
+ }
35994
+ };
35995
+ const isPlainObject = (value) => {
35996
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
35997
+ const prototype = Object.getPrototypeOf(value);
35998
+ return prototype === null || prototype === Object.prototype;
35999
+ };
35969
36000
  const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?|mts|mjs)$/;
35970
36001
  const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
36002
+ const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
35971
36003
  const MINIFIED_SNIFF_BYTES = 65536;
35972
36004
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
36005
+ const SCANNED_DOT_DIRECTORIES = new Set([".dumi", ".storybook"]);
35973
36006
  const IGNORED_DIRECTORIES = new Set([
35974
36007
  ".angular",
35975
36008
  ".astro",
@@ -36098,6 +36131,8 @@ const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
36098
36131
  const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
36099
36132
  const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
36100
36133
  const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
36134
+ const ABORT_EXIT_CODES = new Set([134, 3221226505]);
36135
+ const OXLINT_OOM_RESCUE_BUDGET_MS = 6e4;
36101
36136
  const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
36102
36137
  const LINT_PHASE_TIMEOUT_MS = 3e5;
36103
36138
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
@@ -36144,7 +36179,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36144
36179
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36145
36180
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36146
36181
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36147
- const CORE_PACKAGE_VERSION = "0.7.1";
36182
+ const CORE_PACKAGE_VERSION = "0.7.2";
36148
36183
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36149
36184
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36150
36185
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -36154,8 +36189,9 @@ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
36154
36189
  const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
36155
36190
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
36156
36191
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
36157
- const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
36158
- const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath);
36192
+ const isIgnoredDirectoryName = (directoryName) => IGNORED_DIRECTORIES.has(directoryName) || directoryName.startsWith(".") && !SCANNED_DOT_DIRECTORIES.has(directoryName);
36193
+ const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => isIgnoredDirectoryName(segment));
36194
+ const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath) && !GENERATED_SOURCE_DIRECTORY_PATTERN.test(filePath);
36159
36195
  const isMinifiedSource = (absolutePath) => {
36160
36196
  let fileDescriptor;
36161
36197
  try {
@@ -36189,25 +36225,6 @@ const isLargeMinifiedFile = (absolutePath, knownSizeBytes) => {
36189
36225
  cachedIsLargeMinifiedByPath.set(absolutePath, result);
36190
36226
  return result;
36191
36227
  };
36192
- const isErrnoException = (error) => error instanceof Error && "code" in error;
36193
- const IGNORABLE_READDIR_ERROR_CODES = new Set([
36194
- "EACCES",
36195
- "EPERM",
36196
- "ENOENT",
36197
- "ENOTDIR",
36198
- "EINVAL",
36199
- "ELOOP",
36200
- "ENAMETOOLONG"
36201
- ]);
36202
- const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
36203
- const readDirectoryEntries = (directoryPath) => {
36204
- try {
36205
- return NFS.readdirSync(directoryPath, { withFileTypes: true });
36206
- } catch (error) {
36207
- if (isIgnorableReaddirError(error)) return [];
36208
- throw error;
36209
- }
36210
- };
36211
36228
  function* walkSourceTreeFiles(rootDirectory) {
36212
36229
  const stack = [rootDirectory];
36213
36230
  while (stack.length > 0) {
@@ -36216,7 +36233,7 @@ function* walkSourceTreeFiles(rootDirectory) {
36216
36233
  for (const entry of readDirectoryEntries(currentDirectory)) {
36217
36234
  const absolutePath = Path.join(currentDirectory, entry.name);
36218
36235
  if (entry.isDirectory()) {
36219
- if (!IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
36236
+ if (!isIgnoredDirectoryName(entry.name)) stack.push(absolutePath);
36220
36237
  continue;
36221
36238
  }
36222
36239
  if (entry.isFile()) yield {
@@ -36262,23 +36279,6 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
36262
36279
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
36263
36280
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
36264
36281
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
36265
- /**
36266
- * The chain of ancestor directories of `startDirectory`, nearest first, up to
36267
- * and including the filesystem root. With `includeStart`, the chain begins at
36268
- * `startDirectory` itself; otherwise at its parent. The one place the "walk up
36269
- * the tree" traversal is expressed, so each caller adds only its own match /
36270
- * stop policy instead of re-deriving the `path.dirname` loop.
36271
- */
36272
- const ancestorDirectories = (startDirectory, { includeStart }) => {
36273
- const directories = [];
36274
- let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
36275
- while (true) {
36276
- directories.push(currentDirectory);
36277
- const parentDirectory = Path.dirname(currentDirectory);
36278
- if (parentDirectory === currentDirectory) return directories;
36279
- currentDirectory = parentDirectory;
36280
- }
36281
- };
36282
36282
  const cachedPackageJsons = /* @__PURE__ */ new Map();
36283
36283
  const readPackageJsonUncached = (packageJsonPath) => {
36284
36284
  try {
@@ -36309,7 +36309,11 @@ const isMonorepoRoot = (directory) => {
36309
36309
  return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
36310
36310
  };
36311
36311
  const findMonorepoRoot = (startDirectory) => {
36312
- for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
36312
+ let currentDirectory = Path.dirname(startDirectory);
36313
+ while (currentDirectory !== Path.dirname(currentDirectory)) {
36314
+ if (isMonorepoRoot(currentDirectory)) return currentDirectory;
36315
+ currentDirectory = Path.dirname(currentDirectory);
36316
+ }
36313
36317
  return null;
36314
36318
  };
36315
36319
  /**
@@ -36323,6 +36327,146 @@ const findMonorepoRoot = (startDirectory) => {
36323
36327
  * byte-equivalent copy.
36324
36328
  */
36325
36329
  const isProjectBoundary = (directory) => NFS.existsSync(Path.join(directory, ".git")) || isMonorepoRoot(directory);
36330
+ const TSCONFIG_FILENAME = "tsconfig.json";
36331
+ const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
36332
+ const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
36333
+ const resolvePackageExtendsPath = (extendsValue, fromConfigDirectory) => {
36334
+ const requireFromConfig = createRequire(Path.join(fromConfigDirectory, "tsconfig.json"));
36335
+ const candidates = [
36336
+ extendsValue,
36337
+ ensureJsonExtension(extendsValue),
36338
+ `${extendsValue.replace(/\/$/, "")}/tsconfig.json`
36339
+ ];
36340
+ for (const candidate of candidates) try {
36341
+ return requireFromConfig.resolve(candidate);
36342
+ } catch {
36343
+ continue;
36344
+ }
36345
+ return null;
36346
+ };
36347
+ const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
36348
+ if (isRelativeExtendsValue(extendsValue)) return ensureJsonExtension(Path.resolve(fromConfigDirectory, extendsValue));
36349
+ return resolvePackageExtendsPath(extendsValue, fromConfigDirectory);
36350
+ };
36351
+ const normalizeCompilerOptions = (compilerOptions) => {
36352
+ if (!isPlainObject(compilerOptions)) return { hasExplicitLib: false };
36353
+ const target = typeof compilerOptions.target === "string" ? compilerOptions.target : void 0;
36354
+ const hasExplicitLib = Object.hasOwn(compilerOptions, "lib");
36355
+ return {
36356
+ target,
36357
+ lib: Array.isArray(compilerOptions.lib) ? compilerOptions.lib.filter((entry) => typeof entry === "string") : void 0,
36358
+ hasExplicitLib
36359
+ };
36360
+ };
36361
+ const readTsConfig = (filePath) => {
36362
+ let content;
36363
+ try {
36364
+ content = NFS.readFileSync(filePath, "utf-8");
36365
+ } catch {
36366
+ return null;
36367
+ }
36368
+ const parsed = ts.parseConfigFileTextToJson(filePath, content);
36369
+ if (!isPlainObject(parsed.config)) return null;
36370
+ return {
36371
+ extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
36372
+ referencePaths: normalizeReferencePaths(parsed.config.references),
36373
+ compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
36374
+ };
36375
+ };
36376
+ const normalizeReferencePaths = (references) => {
36377
+ if (!Array.isArray(references)) return [];
36378
+ return references.map((reference) => isPlainObject(reference) && typeof reference.path === "string" ? reference.path : null).filter((referencePath) => referencePath !== null);
36379
+ };
36380
+ const mergeCompilerOptions = (inherited, current) => {
36381
+ const target = current.target ?? inherited?.target;
36382
+ const hasExplicitLib = current.hasExplicitLib || Boolean(inherited?.hasExplicitLib);
36383
+ return {
36384
+ target,
36385
+ lib: current.hasExplicitLib ? current.lib : inherited?.lib,
36386
+ hasExplicitLib
36387
+ };
36388
+ };
36389
+ const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
36390
+ const realPath = NFS.realpathSync.native(tsConfigPath);
36391
+ if (visitedPaths.has(realPath)) return null;
36392
+ const tsConfig = readTsConfig(realPath);
36393
+ if (!tsConfig) return null;
36394
+ const nextVisitedPaths = new Set(visitedPaths);
36395
+ nextVisitedPaths.add(realPath);
36396
+ if (tsConfig.extends && extendsDepth < 8) {
36397
+ const parentPath = resolveExtendsPath(tsConfig.extends, Path.dirname(realPath));
36398
+ if (parentPath && isFile(parentPath)) return mergeCompilerOptions(readResolvedCompilerOptions(parentPath, extendsDepth + 1, nextVisitedPaths), tsConfig.compilerOptions);
36399
+ }
36400
+ return tsConfig.compilerOptions;
36401
+ };
36402
+ const targetYearIsPreES2023 = (target) => {
36403
+ const year = ES_TARGET_YEAR_BY_NAME[target.toLowerCase()];
36404
+ return year !== void 0 && year < 2023;
36405
+ };
36406
+ const libEntryIncludesES2023Array = (entry) => {
36407
+ const normalizedEntry = entry.toLowerCase();
36408
+ if (normalizedEntry === "esnext" || normalizedEntry === "esnext.array") return true;
36409
+ const esYearMatch = /^es(\d{4})(?:\.(.+))?$/.exec(normalizedEntry);
36410
+ if (!esYearMatch) return false;
36411
+ if (Number(esYearMatch[1]) < 2023) return false;
36412
+ const component = esYearMatch[2];
36413
+ return component === void 0 || component === "array";
36414
+ };
36415
+ const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
36416
+ const compilerOptionsArePreES2023 = (compilerOptions) => {
36417
+ if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
36418
+ if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
36419
+ return false;
36420
+ };
36421
+ const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
36422
+ const detectPreES2023FromConfig = (tsConfigPath, visitedConfigPaths = /* @__PURE__ */ new Set()) => {
36423
+ if (visitedConfigPaths.has(tsConfigPath)) return false;
36424
+ const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
36425
+ if (!compilerOptions) return false;
36426
+ if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) {
36427
+ const tsConfig = readTsConfig(tsConfigPath);
36428
+ if (!tsConfig) return false;
36429
+ const nextVisitedConfigPaths = new Set(visitedConfigPaths);
36430
+ nextVisitedConfigPaths.add(tsConfigPath);
36431
+ const configDirectory = Path.dirname(tsConfigPath);
36432
+ return tsConfig.referencePaths.some((referencePath) => {
36433
+ const resolvedReferencePath = Path.resolve(configDirectory, referencePath);
36434
+ const referencedConfigPath = isFile(resolvedReferencePath) ? resolvedReferencePath : Path.join(resolvedReferencePath, TSCONFIG_FILENAME);
36435
+ return isFile(referencedConfigPath) && detectPreES2023FromConfig(referencedConfigPath, nextVisitedConfigPaths);
36436
+ });
36437
+ }
36438
+ return compilerOptionsArePreES2023(compilerOptions);
36439
+ };
36440
+ const detectPreES2023Target = (directory) => {
36441
+ const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
36442
+ if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
36443
+ for (const fallbackFilename of FALLBACK_TSCONFIG_FILENAMES) {
36444
+ const fallbackPath = Path.join(directory, fallbackFilename);
36445
+ if (isFile(fallbackPath)) return detectPreES2023FromConfig(fallbackPath);
36446
+ }
36447
+ return false;
36448
+ };
36449
+ const FALLBACK_TSCONFIG_FILENAMES = ["tsconfig.app.json", "tsconfig.build.json"];
36450
+ const FRAMEWORK_PACKAGES = {
36451
+ next: "nextjs",
36452
+ "@tanstack/react-start": "tanstack-start",
36453
+ vite: "vite",
36454
+ "react-scripts": "cra",
36455
+ "@remix-run/react": "remix",
36456
+ gatsby: "gatsby",
36457
+ expo: "expo",
36458
+ "react-native": "react-native"
36459
+ };
36460
+ const detectFramework = (dependencies) => {
36461
+ for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
36462
+ if (dependencies.preact && !dependencies.react) return "preact";
36463
+ return "unknown";
36464
+ };
36465
+ const MOBILE_FRAMEWORKS = new Set(["expo", "react-native"]);
36466
+ const frameworkMergeRank = (framework) => {
36467
+ if (framework === "unknown") return 3;
36468
+ return MOBILE_FRAMEWORKS.has(framework) ? 2 : 1;
36469
+ };
36326
36470
  const REACT_COMPILER_PACKAGES = new Set([
36327
36471
  "babel-plugin-react-compiler",
36328
36472
  "react-compiler-runtime",
@@ -36359,6 +36503,7 @@ const EXPO_APP_CONFIG_FILENAMES = [
36359
36503
  ];
36360
36504
  const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
36361
36505
  const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
36506
+ const STATIC_EXPORT_OUTPUT_PATTERN = /(?:^|[^.\w])["']?output["']?\s*:\s*["']export["']/m;
36362
36507
  const hasCompilerPackage = (packageJson) => {
36363
36508
  const allDependencies = {
36364
36509
  ...packageJson.peerDependencies,
@@ -36391,34 +36536,10 @@ const detectReactCompiler = (directory, packageJson) => {
36391
36536
  }
36392
36537
  return false;
36393
36538
  };
36394
- const FRAMEWORK_PACKAGES = {
36395
- next: "nextjs",
36396
- "@tanstack/react-start": "tanstack-start",
36397
- vite: "vite",
36398
- "react-scripts": "cra",
36399
- "@remix-run/react": "remix",
36400
- gatsby: "gatsby",
36401
- expo: "expo",
36402
- "react-native": "react-native"
36403
- };
36404
- const FRAMEWORK_DISPLAY_NAMES = {
36405
- nextjs: "Next.js",
36406
- "tanstack-start": "TanStack Start",
36407
- vite: "Vite",
36408
- cra: "Create React App",
36409
- remix: "Remix",
36410
- gatsby: "Gatsby",
36411
- expo: "Expo",
36412
- "react-native": "React Native",
36413
- preact: "Preact",
36414
- unknown: "React"
36415
- };
36416
- const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
36417
- const detectFramework = (dependencies) => {
36418
- for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
36419
- if (dependencies.preact && !dependencies.react) return "preact";
36420
- return "unknown";
36421
- };
36539
+ const detectNextjsStaticExport = (directory) => NEXT_CONFIG_FILENAMES.some((filename) => {
36540
+ const filePath = Path.join(directory, filename);
36541
+ return isFile(filePath) && STATIC_EXPORT_OUTPUT_PATTERN.test(NFS.readFileSync(filePath, "utf-8"));
36542
+ });
36422
36543
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
36423
36544
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
36424
36545
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
@@ -36556,10 +36677,76 @@ const isConcreteDependencyVersion = (version) => {
36556
36677
  const normalizedVersion = normalizeDependencyVersion(version);
36557
36678
  return normalizedVersion !== null && /\d/.test(normalizedVersion);
36558
36679
  };
36559
- const isPlainObject = (value) => {
36560
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
36561
- const prototype = Object.getPrototypeOf(value);
36562
- return prototype === null || prototype === Object.prototype;
36680
+ const isMajorMinorAtLeast = (detected, required) => {
36681
+ if (detected === null) return true;
36682
+ if (detected.major !== required.major) return detected.major > required.major;
36683
+ return detected.minor >= required.minor;
36684
+ };
36685
+ const parseReactMajor = (reactVersion) => {
36686
+ if (typeof reactVersion !== "string") return null;
36687
+ return getLowestDependencyMajor(reactVersion);
36688
+ };
36689
+ const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
36690
+ const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
36691
+ const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
36692
+ const parseReactMajorMinor = (reactVersion) => {
36693
+ if (typeof reactVersion !== "string") return null;
36694
+ const trimmed = reactVersion.trim();
36695
+ if (trimmed.length === 0) return null;
36696
+ const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
36697
+ if (lowerBoundsOnly.length === 0) return null;
36698
+ const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
36699
+ if (majorMinorMatch) {
36700
+ const major = Number.parseInt(majorMinorMatch[1], 10);
36701
+ const minor = Number.parseInt(majorMinorMatch[2], 10);
36702
+ if (!Number.isFinite(major) || major <= 0) return null;
36703
+ if (!Number.isFinite(minor) || minor < 0) return null;
36704
+ return {
36705
+ major,
36706
+ minor
36707
+ };
36708
+ }
36709
+ const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
36710
+ if (!majorOnlyMatch) return null;
36711
+ const major = Number.parseInt(majorOnlyMatch[1], 10);
36712
+ if (!Number.isFinite(major) || major <= 0) return null;
36713
+ return {
36714
+ major,
36715
+ minor: 0
36716
+ };
36717
+ };
36718
+ const parseLowerBoundVersion = (versionSpec) => import_semver.validRange(versionSpec) !== null ? import_semver.minVersion(versionSpec) : import_semver.coerce(versionSpec);
36719
+ const parseTailwindMajorMinor = (tailwindVersion) => {
36720
+ if (typeof tailwindVersion !== "string") return null;
36721
+ const trimmed = tailwindVersion.trim();
36722
+ if (trimmed.length === 0) return null;
36723
+ const lowerBound = parseLowerBoundVersion(trimmed);
36724
+ if (lowerBound === null || lowerBound.major <= 0) return null;
36725
+ return {
36726
+ major: lowerBound.major,
36727
+ minor: lowerBound.minor
36728
+ };
36729
+ };
36730
+ const hasUpperBoundOnlyPeerRange = (range) => {
36731
+ if (typeof range !== "string") return false;
36732
+ const normalizedRange = normalizeDependencyVersion(range);
36733
+ if (normalizedRange === null) return false;
36734
+ return splitDependencyVersionBranches(normalizedRange).some((branch) => {
36735
+ const normalizedBranch = normalizeDependencyVersion(branch);
36736
+ return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
36737
+ });
36738
+ };
36739
+ const peerRangeMinMajor = (range) => {
36740
+ if (typeof range !== "string") return null;
36741
+ return getLowestDependencyMajor(range);
36742
+ };
36743
+ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
36744
+ const installedReactMajor = parseReactMajor(reactVersion);
36745
+ const peerReactRange = packageJson.peerDependencies?.react;
36746
+ if (typeof peerReactRange !== "string") return installedReactMajor;
36747
+ const peerFloor = peerRangeMinMajor(peerReactRange);
36748
+ if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
36749
+ return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
36563
36750
  };
36564
36751
  const isCatalogReference = (version) => version.startsWith("catalog:");
36565
36752
  const extractCatalogName = (version) => {
@@ -36701,6 +36888,16 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
36701
36888
  }
36702
36889
  return null;
36703
36890
  };
36891
+ const REACT_SECTIONS = [
36892
+ "dependencies",
36893
+ "peerDependencies",
36894
+ "devDependencies"
36895
+ ];
36896
+ const TAILWIND_ZOD_SECTIONS = [
36897
+ "dependencies",
36898
+ "devDependencies",
36899
+ "peerDependencies"
36900
+ ];
36704
36901
  const EMPTY_DEPENDENCY_INFO = {
36705
36902
  reactVersion: null,
36706
36903
  tailwindVersion: null,
@@ -36741,6 +36938,10 @@ const extractDependencyInfo = (packageJson) => {
36741
36938
  framework: detectFramework(allDependencies)
36742
36939
  };
36743
36940
  };
36941
+ const getDependencySpec = (packageJson, packageName) => {
36942
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
36943
+ return typeof spec === "string" ? spec : null;
36944
+ };
36744
36945
  const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
36745
36946
  for (const section of sections) {
36746
36947
  const version = packageJson[section]?.[packageName];
@@ -36757,203 +36958,67 @@ const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
36757
36958
  version: null
36758
36959
  };
36759
36960
  };
36760
- const isDirectory = (directoryPath) => {
36761
- try {
36762
- return NFS.statSync(directoryPath).isDirectory();
36763
- } catch {
36764
- return false;
36765
- }
36766
- };
36767
- const NX_PROJECT_DISCOVERY_DIRS = [
36768
- "apps",
36769
- "libs",
36770
- "packages"
36771
- ];
36772
- const getNxWorkspaceDirectories = (rootDirectory) => {
36773
- if (!isFile(Path.join(rootDirectory, "nx.json"))) return [];
36774
- const collected = [];
36775
- for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
36776
- const candidatePath = Path.join(rootDirectory, candidate);
36777
- if (!isDirectory(candidatePath)) continue;
36778
- for (const entry of readDirectoryEntries(candidatePath)) {
36779
- if (!entry.isDirectory()) continue;
36780
- const projectDirectory = Path.join(candidatePath, entry.name);
36781
- if (isFile(Path.join(projectDirectory, "project.json")) || isFile(Path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
36782
- }
36783
- }
36784
- return collected;
36785
- };
36786
- const parsePnpmWorkspacePatterns = (rootDirectory) => {
36787
- const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
36788
- if (!isFile(workspacePath)) return [];
36789
- const content = NFS.readFileSync(workspacePath, "utf-8");
36790
- const patterns = [];
36791
- let isInsidePackagesBlock = false;
36792
- for (const line of content.split("\n")) {
36793
- const trimmed = line.trim();
36794
- if (trimmed === "packages:") {
36795
- isInsidePackagesBlock = true;
36796
- continue;
36797
- }
36798
- if (isInsidePackagesBlock && trimmed.startsWith("-")) patterns.push(trimmed.replace(/^-\s*/, "").replace(/["']/g, ""));
36799
- else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith("#")) isInsidePackagesBlock = false;
36800
- }
36801
- return patterns;
36802
- };
36803
- const getWorkspacePatterns = (rootDirectory, packageJson) => {
36804
- const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
36805
- if (pnpmPatterns.length > 0) return pnpmPatterns;
36806
- if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
36807
- if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
36808
- const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
36809
- if (nxPatterns.length > 0) return nxPatterns;
36810
- return [];
36811
- };
36812
- const parseReactMajor = (reactVersion) => {
36813
- if (typeof reactVersion !== "string") return null;
36814
- return getLowestDependencyMajor(reactVersion);
36961
+ const REACT_DEPENDENCY_NAMES = new Set([
36962
+ "react",
36963
+ "react-native",
36964
+ "next",
36965
+ "preact"
36966
+ ]);
36967
+ const hasReactDependency = (packageJson) => {
36968
+ const allDependencies = {
36969
+ ...packageJson.peerDependencies,
36970
+ ...packageJson.dependencies,
36971
+ ...packageJson.devDependencies
36972
+ };
36973
+ return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
36815
36974
  };
36816
- const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
36817
- const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
36818
- if (!cleanPattern.includes("*")) {
36819
- const directoryPath = Path.join(rootDirectory, cleanPattern);
36820
- if (isDirectory(directoryPath) && isFile(Path.join(directoryPath, "package.json"))) return [directoryPath];
36821
- return [];
36822
- }
36823
- const wildcardIndex = cleanPattern.indexOf("*");
36824
- const baseDirectory = Path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
36825
- const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
36826
- if (!isDirectory(baseDirectory)) return [];
36827
- const resolved = [];
36828
- for (const entry of readDirectoryEntries(baseDirectory)) {
36829
- const entryPath = Path.join(baseDirectory, entry.name, suffixAfterWildcard);
36830
- if (isDirectory(entryPath) && isFile(Path.join(entryPath, "package.json"))) resolved.push(entryPath);
36831
- }
36832
- return resolved;
36975
+ const getPreactVersion = (packageJson) => {
36976
+ return {
36977
+ ...packageJson.peerDependencies,
36978
+ ...packageJson.dependencies,
36979
+ ...packageJson.devDependencies
36980
+ }.preact ?? null;
36833
36981
  };
36834
- const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
36835
- const dependencyDeclaration = getDependencyDeclaration({
36836
- packageJson: workspacePackageJson,
36837
- packageName,
36838
- sections
36839
- });
36840
- if (!dependencyDeclaration.hasDeclaration) return null;
36841
- return concreteVersion ?? resolveCatalogVersion(workspacePackageJson, packageName, workspaceDirectory, dependencyDeclaration.catalogReference) ?? resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, dependencyDeclaration.catalogReference);
36982
+ const TANSTACK_QUERY_PACKAGES = new Set([
36983
+ "@tanstack/react-query",
36984
+ "@tanstack/query-core",
36985
+ "react-query"
36986
+ ]);
36987
+ const hasTanStackQuery = (packageJson) => {
36988
+ const allDependencies = {
36989
+ ...packageJson.peerDependencies,
36990
+ ...packageJson.dependencies,
36991
+ ...packageJson.devDependencies
36992
+ };
36993
+ return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
36842
36994
  };
36843
- const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
36844
- if (!currentVersion) return true;
36845
- const currentMajor = parseReactMajor(currentVersion);
36846
- const nextMajor = parseReactMajor(nextVersion);
36847
- if (currentMajor === null) return nextMajor !== null;
36848
- if (nextMajor === null) return false;
36849
- return nextMajor < currentMajor;
36995
+ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
36996
+ if (version === null || !isCatalogReference(version)) return version;
36997
+ const catalogName = extractCatalogName(version);
36998
+ const resolvedLocalVersion = resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, catalogName);
36999
+ if (resolvedLocalVersion) return resolvedLocalVersion;
37000
+ const monorepoRoot = findMonorepoRoot(rootDirectory);
37001
+ if (!monorepoRoot) return version;
37002
+ const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
37003
+ if (!isFile(monorepoPackageJsonPath)) return version;
37004
+ return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
36850
37005
  };
36851
- const findReactInWorkspaces = (rootDirectory, packageJson) => {
36852
- const patterns = getWorkspacePatterns(rootDirectory, packageJson);
36853
- const result = { ...EMPTY_DEPENDENCY_INFO };
36854
- for (const pattern of patterns) {
36855
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
36856
- for (const workspaceDirectory of directories) {
36857
- const workspacePackageJson = readPackageJson$1(Path.join(workspaceDirectory, "package.json"));
36858
- const info = extractDependencyInfo(workspacePackageJson);
36859
- const reactVersion = resolveWorkspaceDependencyVersion({
36860
- concreteVersion: info.reactVersion,
36861
- packageName: "react",
36862
- rootDirectory,
36863
- rootPackageJson: packageJson,
36864
- sections: [
36865
- "dependencies",
36866
- "peerDependencies",
36867
- "devDependencies"
36868
- ],
36869
- workspaceDirectory,
36870
- workspacePackageJson
36871
- });
36872
- const tailwindVersion = resolveWorkspaceDependencyVersion({
36873
- concreteVersion: info.tailwindVersion,
36874
- packageName: "tailwindcss",
36875
- rootDirectory,
36876
- rootPackageJson: packageJson,
36877
- sections: [
36878
- "dependencies",
36879
- "devDependencies",
36880
- "peerDependencies"
36881
- ],
36882
- workspaceDirectory,
36883
- workspacePackageJson
36884
- });
36885
- const zodVersion = resolveWorkspaceDependencyVersion({
36886
- concreteVersion: info.zodVersion,
36887
- packageName: "zod",
36888
- rootDirectory,
36889
- rootPackageJson: packageJson,
36890
- sections: [
36891
- "dependencies",
36892
- "devDependencies",
36893
- "peerDependencies"
36894
- ],
36895
- workspaceDirectory,
36896
- workspacePackageJson
36897
- });
36898
- if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
36899
- if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
36900
- if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
36901
- if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
36902
- const resultReactMajor = parseReactMajor(result.reactVersion);
36903
- if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
36904
- }
37006
+ /**
37007
+ * The chain of ancestor directories of `startDirectory`, nearest first, up to
37008
+ * and including the filesystem root. With `includeStart`, the chain begins at
37009
+ * `startDirectory` itself; otherwise at its parent. The one place the "walk up
37010
+ * the tree" traversal is expressed, so each caller adds only its own match /
37011
+ * stop policy instead of re-deriving the `path.dirname` loop.
37012
+ */
37013
+ const ancestorDirectories = (startDirectory, { includeStart }) => {
37014
+ const directories = [];
37015
+ let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
37016
+ while (true) {
37017
+ directories.push(currentDirectory);
37018
+ const parentDirectory = Path.dirname(currentDirectory);
37019
+ if (parentDirectory === currentDirectory) return directories;
37020
+ currentDirectory = parentDirectory;
36905
37021
  }
36906
- return result;
36907
- };
36908
- const findDependencyInfoFromMonorepoRoot = (directory) => {
36909
- const monorepoRoot = findMonorepoRoot(directory);
36910
- if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
36911
- const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
36912
- if (!isFile(monorepoPackageJsonPath)) return EMPTY_DEPENDENCY_INFO;
36913
- const rootPackageJson = readPackageJson$1(monorepoPackageJsonPath);
36914
- const rootInfo = extractDependencyInfo(rootPackageJson);
36915
- const leafPackageJsonPath = Path.join(directory, "package.json");
36916
- const leafPackageJson = isFile(leafPackageJsonPath) ? readPackageJson$1(leafPackageJsonPath) : null;
36917
- const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
36918
- packageJson: leafPackageJson,
36919
- packageName: "react",
36920
- sections: [
36921
- "dependencies",
36922
- "peerDependencies",
36923
- "devDependencies"
36924
- ]
36925
- }) : null;
36926
- const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
36927
- packageJson: leafPackageJson,
36928
- packageName: "tailwindcss",
36929
- sections: [
36930
- "dependencies",
36931
- "devDependencies",
36932
- "peerDependencies"
36933
- ]
36934
- }) : null;
36935
- const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
36936
- packageJson: leafPackageJson,
36937
- packageName: "zod",
36938
- sections: [
36939
- "dependencies",
36940
- "devDependencies",
36941
- "peerDependencies"
36942
- ]
36943
- }) : null;
36944
- const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
36945
- const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
36946
- const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
36947
- const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
36948
- const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
36949
- const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
36950
- const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
36951
- return {
36952
- reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
36953
- tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
36954
- zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
36955
- framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
36956
- };
36957
37022
  };
36958
37023
  /**
36959
37024
  * Walk up from `startDirectory` to the nearest ancestor that owns a
@@ -36978,69 +37043,6 @@ const findNearestAncestorPackageJson = (startDirectory) => {
36978
37043
  }
36979
37044
  return null;
36980
37045
  };
36981
- /**
36982
- * Last-resort React detection: locate React the way Node's `node_modules`
36983
- * resolution would from `directory` — check each `node_modules/react` up the
36984
- * tree — but bounded to the enclosing repo so a globally installed React can't
36985
- * masquerade as the project's version. Stops at (and including) the nearest
36986
- * project boundary — the git root or a monorepo root — and returns the first
36987
- * installed `version`.
36988
- *
36989
- * Makes "React is installed and importable" ⇒ "React is detected" an invariant
36990
- * for packages whose only React declaration is a version-less spec
36991
- * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
36992
- * `node_modules` the declaration walks never reach — the profile of a component
36993
- * package inside a monorepo. Matching the `node_modules/react` entry rather than
36994
- * its realpath means a project whose `node_modules` is symlinked elsewhere (a
36995
- * Docker volume, a shared store) still counts, while a global install outside
36996
- * the repo does not.
36997
- *
36998
- * Returns `null` when React isn't installed within the repo or its package.json
36999
- * carries no version string. A tree with no boundary marker at all bounds the
37000
- * search to `directory`, so a dependency hoisted above the scanned package there
37001
- * is conservatively not adopted.
37002
- */
37003
- /**
37004
- * The nearest enclosing project boundary (git root or monorepo root), or
37005
- * `directory` itself when the scan target sits outside any repo. Bounds the
37006
- * node_modules walk so a React hoisted anywhere inside the repo counts, but a
37007
- * React in an ancestor above the repo — a home-directory or global
37008
- * `node_modules` — can't leak in.
37009
- */
37010
- const findContainmentRoot = (directory) => {
37011
- for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
37012
- return directory;
37013
- };
37014
- const resolveInstalledReactVersion = (directory) => {
37015
- const containmentRoot = findContainmentRoot(directory);
37016
- for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
37017
- const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
37018
- if (isFile(reactPackageJsonPath)) {
37019
- const installedVersion = readPackageJson$1(reactPackageJsonPath).version;
37020
- return typeof installedVersion === "string" ? installedVersion : null;
37021
- }
37022
- if (ancestorDirectory === containmentRoot) return null;
37023
- }
37024
- return null;
37025
- };
37026
- const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
37027
- const rootValue = select(rootPackageJson);
37028
- if (rootValue !== null) return rootValue;
37029
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
37030
- if (patterns.length === 0) return null;
37031
- const visitedDirectories = /* @__PURE__ */ new Set();
37032
- for (const pattern of patterns) {
37033
- const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
37034
- for (const workspaceDirectory of directories) {
37035
- if (visitedDirectories.has(workspaceDirectory)) continue;
37036
- visitedDirectories.add(workspaceDirectory);
37037
- const value = select(readPackageJson$1(Path.join(workspaceDirectory, "package.json")));
37038
- if (value !== null) return value;
37039
- }
37040
- }
37041
- return null;
37042
- };
37043
- const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => predicate(packageJson) ? true : null) !== null;
37044
37046
  const NAMES = new Set([
37045
37047
  "react-native",
37046
37048
  "react-native-tvos",
@@ -37073,47 +37075,7 @@ const isPackageJsonReactNativeAware = (packageJson) => {
37073
37075
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
37074
37076
  return false;
37075
37077
  };
37076
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
37077
- const getDependencySpec = (packageJson, packageName) => {
37078
- const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
37079
- return typeof spec === "string" ? spec : null;
37080
- };
37081
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
37082
- const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
37083
- const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
37084
- const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
37085
- if (version === null || !isCatalogReference(version)) return version;
37086
- const catalogName = extractCatalogName(version);
37087
- const resolvedLocalVersion = resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, catalogName);
37088
- if (resolvedLocalVersion) return resolvedLocalVersion;
37089
- const monorepoRoot = findMonorepoRoot(rootDirectory);
37090
- if (!monorepoRoot) return version;
37091
- const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
37092
- if (!isFile(monorepoPackageJsonPath)) return version;
37093
- return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
37094
- };
37095
- const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
37096
- const getPreactVersion = (packageJson) => {
37097
- return {
37098
- ...packageJson.peerDependencies,
37099
- ...packageJson.dependencies,
37100
- ...packageJson.devDependencies
37101
- }.preact ?? null;
37102
- };
37103
- const TANSTACK_QUERY_PACKAGES = new Set([
37104
- "@tanstack/react-query",
37105
- "@tanstack/query-core",
37106
- "react-query"
37107
- ]);
37108
- const hasTanStackQuery = (packageJson) => {
37109
- const allDependencies = {
37110
- ...packageJson.peerDependencies,
37111
- ...packageJson.dependencies,
37112
- ...packageJson.devDependencies
37113
- };
37114
- return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
37115
- };
37116
- const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
37078
+ const REANIMATED_DEPENDENCY_NAME$1 = "react-native-reanimated";
37117
37079
  const isPackageJsonReanimatedAware = (packageJson) => {
37118
37080
  const allDependencies = {
37119
37081
  ...packageJson.peerDependencies,
@@ -37121,144 +37083,70 @@ const isPackageJsonReanimatedAware = (packageJson) => {
37121
37083
  ...packageJson.devDependencies,
37122
37084
  ...packageJson.optionalDependencies
37123
37085
  };
37124
- return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
37125
- };
37126
- const parseZodMajor = (zodVersion) => {
37127
- if (typeof zodVersion !== "string") return null;
37128
- return getLowestDependencyMajor(zodVersion);
37129
- };
37130
- const hasUpperBoundOnlyPeerRange = (range) => {
37131
- if (typeof range !== "string") return false;
37132
- const normalizedRange = normalizeDependencyVersion(range);
37133
- if (normalizedRange === null) return false;
37134
- return splitDependencyVersionBranches(normalizedRange).some((branch) => {
37135
- const normalizedBranch = normalizeDependencyVersion(branch);
37136
- return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
37137
- });
37086
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME$1);
37138
37087
  };
37139
- const peerRangeMinMajor = (range) => {
37140
- if (typeof range !== "string") return null;
37141
- return getLowestDependencyMajor(range);
37142
- };
37143
- const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
37144
- const installedReactMajor = parseReactMajor(reactVersion);
37145
- const peerReactRange = packageJson.peerDependencies?.react;
37146
- if (typeof peerReactRange !== "string") return installedReactMajor;
37147
- const peerFloor = peerRangeMinMajor(peerReactRange);
37148
- if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
37149
- return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
37088
+ const getWorkspacePatterns = (rootDirectory, packageJson) => {
37089
+ const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
37090
+ if (pnpmPatterns.length > 0) return pnpmPatterns;
37091
+ if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
37092
+ if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
37093
+ const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
37094
+ if (nxPatterns.length > 0) return nxPatterns;
37095
+ return [];
37150
37096
  };
37151
- const TSCONFIG_FILENAME = "tsconfig.json";
37152
- const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
37153
- const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
37154
- const resolvePackageExtendsPath = (extendsValue, fromConfigDirectory) => {
37155
- const requireFromConfig = createRequire(Path.join(fromConfigDirectory, "tsconfig.json"));
37156
- const candidates = [
37157
- extendsValue,
37158
- ensureJsonExtension(extendsValue),
37159
- `${extendsValue.replace(/\/$/, "")}/tsconfig.json`
37160
- ];
37161
- for (const candidate of candidates) try {
37162
- return requireFromConfig.resolve(candidate);
37163
- } catch {
37164
- continue;
37097
+ const parsePnpmWorkspacePatterns = (rootDirectory) => {
37098
+ const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
37099
+ if (!isFile(workspacePath)) return [];
37100
+ const content = NFS.readFileSync(workspacePath, "utf-8");
37101
+ const patterns = [];
37102
+ let isInsidePackagesBlock = false;
37103
+ for (const line of content.split("\n")) {
37104
+ const trimmed = line.trim();
37105
+ if (trimmed === "packages:") {
37106
+ isInsidePackagesBlock = true;
37107
+ continue;
37108
+ }
37109
+ if (isInsidePackagesBlock && trimmed.startsWith("-")) patterns.push(trimmed.replace(/^-\s*/, "").replace(/["']/g, ""));
37110
+ else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith("#")) isInsidePackagesBlock = false;
37165
37111
  }
37166
- return null;
37167
- };
37168
- const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
37169
- if (isRelativeExtendsValue(extendsValue)) return ensureJsonExtension(Path.resolve(fromConfigDirectory, extendsValue));
37170
- return resolvePackageExtendsPath(extendsValue, fromConfigDirectory);
37171
- };
37172
- const normalizeCompilerOptions = (compilerOptions) => {
37173
- if (!isPlainObject(compilerOptions)) return { hasExplicitLib: false };
37174
- const target = typeof compilerOptions.target === "string" ? compilerOptions.target : void 0;
37175
- const hasExplicitLib = Object.hasOwn(compilerOptions, "lib");
37176
- return {
37177
- target,
37178
- lib: Array.isArray(compilerOptions.lib) ? compilerOptions.lib.filter((entry) => typeof entry === "string") : void 0,
37179
- hasExplicitLib
37180
- };
37112
+ return patterns;
37181
37113
  };
37182
- const readTsConfig = (filePath) => {
37183
- let content;
37184
- try {
37185
- content = NFS.readFileSync(filePath, "utf-8");
37186
- } catch {
37187
- return null;
37114
+ const NX_PROJECT_DISCOVERY_DIRS = [
37115
+ "apps",
37116
+ "libs",
37117
+ "packages"
37118
+ ];
37119
+ const getNxWorkspaceDirectories = (rootDirectory) => {
37120
+ if (!isFile(Path.join(rootDirectory, "nx.json"))) return [];
37121
+ const collected = [];
37122
+ for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
37123
+ const candidatePath = Path.join(rootDirectory, candidate);
37124
+ if (!isDirectory(candidatePath)) continue;
37125
+ for (const entry of readDirectoryEntries(candidatePath)) {
37126
+ if (!entry.isDirectory()) continue;
37127
+ const projectDirectory = Path.join(candidatePath, entry.name);
37128
+ if (isFile(Path.join(projectDirectory, "project.json")) || isFile(Path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
37129
+ }
37188
37130
  }
37189
- const parsed = ts.parseConfigFileTextToJson(filePath, content);
37190
- if (!isPlainObject(parsed.config)) return null;
37191
- return {
37192
- extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
37193
- compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
37194
- };
37195
- };
37196
- const mergeCompilerOptions = (inherited, current) => {
37197
- const target = current.target ?? inherited?.target;
37198
- const hasExplicitLib = current.hasExplicitLib || Boolean(inherited?.hasExplicitLib);
37199
- return {
37200
- target,
37201
- lib: current.hasExplicitLib ? current.lib : inherited?.lib,
37202
- hasExplicitLib
37203
- };
37131
+ return collected;
37204
37132
  };
37205
- const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
37206
- const realPath = NFS.realpathSync.native(tsConfigPath);
37207
- if (visitedPaths.has(realPath)) return null;
37208
- const tsConfig = readTsConfig(realPath);
37209
- if (!tsConfig) return null;
37210
- const nextVisitedPaths = new Set(visitedPaths);
37211
- nextVisitedPaths.add(realPath);
37212
- if (tsConfig.extends && extendsDepth < 8) {
37213
- const parentPath = resolveExtendsPath(tsConfig.extends, Path.dirname(realPath));
37214
- if (parentPath && isFile(parentPath)) return mergeCompilerOptions(readResolvedCompilerOptions(parentPath, extendsDepth + 1, nextVisitedPaths), tsConfig.compilerOptions);
37133
+ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
37134
+ const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
37135
+ if (!cleanPattern.includes("*")) {
37136
+ const directoryPath = Path.join(rootDirectory, cleanPattern);
37137
+ if (isDirectory(directoryPath) && isFile(Path.join(directoryPath, "package.json"))) return [directoryPath];
37138
+ return [];
37215
37139
  }
37216
- return tsConfig.compilerOptions;
37217
- };
37218
- const targetYearIsPreES2023 = (target) => {
37219
- const year = ES_TARGET_YEAR_BY_NAME[target.toLowerCase()];
37220
- return year !== void 0 && year < 2023;
37221
- };
37222
- const libEntryIncludesES2023Array = (entry) => {
37223
- const normalizedEntry = entry.toLowerCase();
37224
- if (normalizedEntry === "esnext" || normalizedEntry === "esnext.array") return true;
37225
- const esYearMatch = /^es(\d{4})(?:\.(.+))?$/.exec(normalizedEntry);
37226
- if (!esYearMatch) return false;
37227
- if (Number(esYearMatch[1]) < 2023) return false;
37228
- const component = esYearMatch[2];
37229
- return component === void 0 || component === "array";
37230
- };
37231
- const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
37232
- const compilerOptionsArePreES2023 = (compilerOptions) => {
37233
- if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
37234
- if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
37235
- return false;
37236
- };
37237
- const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
37238
- const detectPreES2023FromConfig = (tsConfigPath) => {
37239
- const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
37240
- if (!compilerOptions) return false;
37241
- if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) return false;
37242
- return compilerOptionsArePreES2023(compilerOptions);
37243
- };
37244
- const detectPreES2023Target = (directory) => {
37245
- const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
37246
- if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
37247
- return false;
37248
- };
37249
- const REACT_DEPENDENCY_NAMES = new Set([
37250
- "react",
37251
- "react-native",
37252
- "next",
37253
- "preact"
37254
- ]);
37255
- const hasReactDependency = (packageJson) => {
37256
- const allDependencies = {
37257
- ...packageJson.peerDependencies,
37258
- ...packageJson.dependencies,
37259
- ...packageJson.devDependencies
37260
- };
37261
- return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
37140
+ const wildcardIndex = cleanPattern.indexOf("*");
37141
+ const baseDirectory = Path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
37142
+ const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
37143
+ if (!isDirectory(baseDirectory)) return [];
37144
+ const resolved = [];
37145
+ for (const entry of readDirectoryEntries(baseDirectory)) {
37146
+ const entryPath = Path.join(baseDirectory, entry.name, suffixAfterWildcard);
37147
+ if (isDirectory(entryPath) && isFile(Path.join(entryPath, "package.json"))) resolved.push(entryPath);
37148
+ }
37149
+ return resolved;
37262
37150
  };
37263
37151
  const listWorkspacePackages = (rootDirectory) => {
37264
37152
  const packageJsonPath = Path.join(rootDirectory, "package.json");
@@ -37290,6 +37178,208 @@ const listWorkspacePackages = (rootDirectory) => {
37290
37178
  }
37291
37179
  return packages;
37292
37180
  };
37181
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
37182
+ const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
37183
+ const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
37184
+ const dependencyDeclaration = getDependencyDeclaration({
37185
+ packageJson: workspacePackageJson,
37186
+ packageName,
37187
+ sections
37188
+ });
37189
+ if (!dependencyDeclaration.hasDeclaration) return null;
37190
+ return concreteVersion ?? resolveCatalogVersion(workspacePackageJson, packageName, workspaceDirectory, dependencyDeclaration.catalogReference) ?? resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, dependencyDeclaration.catalogReference);
37191
+ };
37192
+ const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
37193
+ if (!currentVersion) return true;
37194
+ const currentMajor = parseReactMajor(currentVersion);
37195
+ const nextMajor = parseReactMajor(nextVersion);
37196
+ if (currentMajor === null) return nextMajor !== null;
37197
+ if (nextMajor === null) return false;
37198
+ return nextMajor < currentMajor;
37199
+ };
37200
+ const evaluateManifestFacts = (facts, packageJson, directory) => {
37201
+ if (facts.expo.version === null) {
37202
+ const spec = getDependencySpec(packageJson, "expo");
37203
+ if (spec !== null) facts.expo = {
37204
+ version: spec,
37205
+ sourceDirectory: directory
37206
+ };
37207
+ }
37208
+ if (facts.next.version === null) {
37209
+ const spec = getDependencySpec(packageJson, "next");
37210
+ if (spec !== null) facts.next = {
37211
+ version: spec,
37212
+ sourceDirectory: directory
37213
+ };
37214
+ }
37215
+ if (facts.shopifyFlashList.version === null) {
37216
+ const spec = getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME);
37217
+ if (spec !== null) facts.shopifyFlashList = {
37218
+ version: spec,
37219
+ sourceDirectory: directory
37220
+ };
37221
+ }
37222
+ if (facts.reanimatedVersion === null) {
37223
+ const spec = getDependencySpec(packageJson, REANIMATED_DEPENDENCY_NAME);
37224
+ if (spec !== null) facts.reanimatedVersion = spec;
37225
+ }
37226
+ facts.hasReactNativeAwarePackage = facts.hasReactNativeAwarePackage || isPackageJsonReactNativeAware(packageJson);
37227
+ facts.hasReanimatedAwarePackage = facts.hasReanimatedAwarePackage || isPackageJsonReanimatedAware(packageJson);
37228
+ };
37229
+ const collectWorkspaceFacts = (rootDirectory, rootPackageJson, { collectReactGroup }) => {
37230
+ const facts = {
37231
+ reactVersion: null,
37232
+ tailwindVersion: null,
37233
+ zodVersion: null,
37234
+ framework: "unknown",
37235
+ expo: {
37236
+ version: null,
37237
+ sourceDirectory: null
37238
+ },
37239
+ next: {
37240
+ version: null,
37241
+ sourceDirectory: null
37242
+ },
37243
+ shopifyFlashList: {
37244
+ version: null,
37245
+ sourceDirectory: null
37246
+ },
37247
+ hasReactNativeAwarePackage: false,
37248
+ hasReanimatedAwarePackage: false,
37249
+ reanimatedVersion: null
37250
+ };
37251
+ evaluateManifestFacts(facts, rootPackageJson, rootDirectory);
37252
+ let isReactGroupSettled = !collectReactGroup;
37253
+ const visitedDirectories = /* @__PURE__ */ new Set();
37254
+ for (const pattern of getWorkspacePatterns(rootDirectory, rootPackageJson)) {
37255
+ const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
37256
+ for (const workspaceDirectory of directories) {
37257
+ if (visitedDirectories.has(workspaceDirectory)) continue;
37258
+ visitedDirectories.add(workspaceDirectory);
37259
+ const workspacePackageJson = readPackageJson$1(Path.join(workspaceDirectory, "package.json"));
37260
+ evaluateManifestFacts(facts, workspacePackageJson, workspaceDirectory);
37261
+ const info = extractDependencyInfo(workspacePackageJson);
37262
+ if (info.framework !== "unknown" && frameworkMergeRank(info.framework) < frameworkMergeRank(facts.framework)) facts.framework = info.framework;
37263
+ if (isReactGroupSettled) continue;
37264
+ const reactVersion = resolveWorkspaceDependencyVersion({
37265
+ concreteVersion: info.reactVersion,
37266
+ packageName: "react",
37267
+ rootDirectory,
37268
+ rootPackageJson,
37269
+ sections: REACT_SECTIONS,
37270
+ workspaceDirectory,
37271
+ workspacePackageJson
37272
+ });
37273
+ const tailwindVersion = resolveWorkspaceDependencyVersion({
37274
+ concreteVersion: info.tailwindVersion,
37275
+ packageName: "tailwindcss",
37276
+ rootDirectory,
37277
+ rootPackageJson,
37278
+ sections: TAILWIND_ZOD_SECTIONS,
37279
+ workspaceDirectory,
37280
+ workspacePackageJson
37281
+ });
37282
+ const zodVersion = resolveWorkspaceDependencyVersion({
37283
+ concreteVersion: info.zodVersion,
37284
+ packageName: "zod",
37285
+ rootDirectory,
37286
+ rootPackageJson,
37287
+ sections: TAILWIND_ZOD_SECTIONS,
37288
+ workspaceDirectory,
37289
+ workspacePackageJson
37290
+ });
37291
+ if (reactVersion && shouldReplaceReactVersion(facts.reactVersion, reactVersion)) facts.reactVersion = reactVersion;
37292
+ if (tailwindVersion && !facts.tailwindVersion) facts.tailwindVersion = tailwindVersion;
37293
+ if (zodVersion && !facts.zodVersion) facts.zodVersion = zodVersion;
37294
+ const settledReactMajor = parseReactMajor(facts.reactVersion);
37295
+ isReactGroupSettled = Boolean(facts.reactVersion && facts.tailwindVersion && facts.framework !== "unknown" && settledReactMajor !== null && settledReactMajor <= 17);
37296
+ }
37297
+ }
37298
+ return facts;
37299
+ };
37300
+ const findDependencyInfoFromMonorepoRoot = (directory) => {
37301
+ const monorepoRoot = findMonorepoRoot(directory);
37302
+ if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
37303
+ const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
37304
+ if (!isFile(monorepoPackageJsonPath)) return EMPTY_DEPENDENCY_INFO;
37305
+ const rootPackageJson = readPackageJson$1(monorepoPackageJsonPath);
37306
+ const rootInfo = extractDependencyInfo(rootPackageJson);
37307
+ const leafPackageJsonPath = Path.join(directory, "package.json");
37308
+ const leafPackageJson = isFile(leafPackageJsonPath) ? readPackageJson$1(leafPackageJsonPath) : null;
37309
+ const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
37310
+ packageJson: leafPackageJson,
37311
+ packageName: "react",
37312
+ sections: REACT_SECTIONS
37313
+ }) : null;
37314
+ const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
37315
+ packageJson: leafPackageJson,
37316
+ packageName: "tailwindcss",
37317
+ sections: TAILWIND_ZOD_SECTIONS
37318
+ }) : null;
37319
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
37320
+ packageJson: leafPackageJson,
37321
+ packageName: "zod",
37322
+ sections: TAILWIND_ZOD_SECTIONS
37323
+ }) : null;
37324
+ const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
37325
+ const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
37326
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
37327
+ const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
37328
+ const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
37329
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
37330
+ const workspaceFacts = collectWorkspaceFacts(monorepoRoot, rootPackageJson, { collectReactGroup: true });
37331
+ return {
37332
+ reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceFacts.reactVersion : rootInfo.reactVersion ?? workspaceFacts.reactVersion,
37333
+ tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceFacts.tailwindVersion : null,
37334
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceFacts.zodVersion : null,
37335
+ framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceFacts.framework
37336
+ };
37337
+ };
37338
+ /**
37339
+ * Last-resort React detection: locate React the way Node's `node_modules`
37340
+ * resolution would from `directory` — check each `node_modules/react` up the
37341
+ * tree — but bounded to the enclosing repo so a globally installed React can't
37342
+ * masquerade as the project's version. Stops at (and including) the nearest
37343
+ * project boundary — the git root or a monorepo root — and returns the first
37344
+ * installed `version`.
37345
+ *
37346
+ * Makes "React is installed and importable" ⇒ "React is detected" an invariant
37347
+ * for packages whose only React declaration is a version-less spec
37348
+ * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
37349
+ * `node_modules` the declaration walks never reach — the profile of a component
37350
+ * package inside a monorepo. Matching the `node_modules/react` entry rather than
37351
+ * its realpath means a project whose `node_modules` is symlinked elsewhere (a
37352
+ * Docker volume, a shared store) still counts, while a global install outside
37353
+ * the repo does not.
37354
+ *
37355
+ * Returns `null` when React isn't installed within the repo or its package.json
37356
+ * carries no version string. A tree with no boundary marker at all bounds the
37357
+ * search to `directory`, so a dependency hoisted above the scanned package there
37358
+ * is conservatively not adopted.
37359
+ */
37360
+ /**
37361
+ * The nearest enclosing project boundary (git root or monorepo root), or
37362
+ * `directory` itself when the scan target sits outside any repo. Bounds the
37363
+ * node_modules walk so a React hoisted anywhere inside the repo counts, but a
37364
+ * React in an ancestor above the repo — a home-directory or global
37365
+ * `node_modules` — can't leak in.
37366
+ */
37367
+ const findContainmentRoot = (directory) => {
37368
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
37369
+ return directory;
37370
+ };
37371
+ const resolveInstalledReactVersion = (directory) => {
37372
+ const containmentRoot = findContainmentRoot(directory);
37373
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
37374
+ const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
37375
+ if (isFile(reactPackageJsonPath)) {
37376
+ const installedVersion = readPackageJson$1(reactPackageJsonPath).version;
37377
+ return typeof installedVersion === "string" ? installedVersion : null;
37378
+ }
37379
+ if (ancestorDirectory === containmentRoot) return null;
37380
+ }
37381
+ return null;
37382
+ };
37293
37383
  const toReactWorkspacePackages = (directories) => {
37294
37384
  const packages = [];
37295
37385
  for (const directory of directories) {
@@ -37359,14 +37449,13 @@ const discoverReactSubprojects = (rootDirectory) => {
37359
37449
  const cachedProjectInfos = /* @__PURE__ */ new Map();
37360
37450
  /**
37361
37451
  * Build a `ProjectInfo` for a directory that has no `package.json` of
37362
- * its own — a package subfolder like `repo/packages` or `app/src/features`,
37363
- * or any loose tree of TypeScript/JavaScript files. Dependency + framework
37364
- * detection is inherited from the nearest enclosing package (a leaf
37365
- * workspace, a plain app root, or a monorepo root — whichever is closest,
37366
- * bounded by the git root), so scanning a subdirectory of a React project
37367
- * still gets the React capabilities; a standalone non-React directory simply
37368
- * scans with the framework-agnostic rules. Throws only when the directory has
37369
- * nothing to scan (no enclosing project and no source files of its own).
37452
+ * its own — a monorepo subfolder like `repo/packages`, or any loose tree
37453
+ * of TypeScript/JavaScript files. Dependency + framework detection is
37454
+ * inherited from the enclosing workspace root when there is one, so
37455
+ * scanning a subdirectory of a React monorepo still gets the React
37456
+ * capabilities; a standalone non-React directory simply scans with the
37457
+ * framework-agnostic rules. Throws only when the directory has nothing
37458
+ * to scan (no enclosing project and no source files of its own).
37370
37459
  */
37371
37460
  const discoverProjectWithoutPackageJson = (directory) => {
37372
37461
  const sourceFileCount = countSourceFiles(directory);
@@ -37402,7 +37491,9 @@ const discoverProjectWithoutPackageJson = (directory) => {
37402
37491
  shopifyFlashListVersion: null,
37403
37492
  shopifyFlashListMajorVersion: null,
37404
37493
  hasReanimated: false,
37494
+ reanimatedVersion: null,
37405
37495
  isPreES2023Target: hasOwnTsConfig && detectPreES2023Target(directory),
37496
+ isStaticExport: false,
37406
37497
  sourceFileCount
37407
37498
  };
37408
37499
  };
@@ -37416,89 +37507,85 @@ const discoverProject = (directory) => {
37416
37507
  return synthesized;
37417
37508
  }
37418
37509
  const packageJson = readPackageJson$1(packageJsonPath);
37419
- let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
37420
- const reactDeclaration = getDependencyDeclaration({
37421
- packageJson,
37422
- packageName: "react",
37423
- sections: [
37424
- "dependencies",
37425
- "peerDependencies",
37426
- "devDependencies"
37427
- ]
37428
- });
37429
- const tailwindDeclaration = getDependencyDeclaration({
37430
- packageJson,
37431
- packageName: "tailwindcss",
37432
- sections: [
37433
- "dependencies",
37434
- "devDependencies",
37435
- "peerDependencies"
37436
- ]
37437
- });
37438
- const zodDeclaration = getDependencyDeclaration({
37510
+ const rootInfo = extractDependencyInfo(packageJson);
37511
+ let framework = rootInfo.framework;
37512
+ const tracked = {
37513
+ react: {
37514
+ version: rootInfo.reactVersion,
37515
+ sections: REACT_SECTIONS
37516
+ },
37517
+ tailwindcss: {
37518
+ version: rootInfo.tailwindVersion,
37519
+ sections: TAILWIND_ZOD_SECTIONS
37520
+ },
37521
+ zod: {
37522
+ version: rootInfo.zodVersion,
37523
+ sections: TAILWIND_ZOD_SECTIONS
37524
+ }
37525
+ };
37526
+ const declarations = Object.fromEntries(Object.entries(tracked).map(([packageName, entry]) => [packageName, getDependencyDeclaration({
37439
37527
  packageJson,
37440
- packageName: "zod",
37441
- sections: [
37442
- "dependencies",
37443
- "devDependencies",
37444
- "peerDependencies"
37445
- ]
37446
- });
37447
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
37448
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
37449
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
37450
- if (!reactVersion || !tailwindVersion || !zodVersion) {
37528
+ packageName,
37529
+ sections: entry.sections
37530
+ })]));
37531
+ const fillFromCatalogs = (source, sourceDirectory) => {
37532
+ for (const [packageName, entry] of Object.entries(tracked)) if (!entry.version && declarations[packageName].hasDeclaration) entry.version = resolveCatalogVersion(source, packageName, sourceDirectory, declarations[packageName].catalogReference);
37533
+ };
37534
+ fillFromCatalogs(packageJson, directory);
37535
+ if (!tracked.react.version || !tracked.tailwindcss.version || !tracked.zod.version) {
37451
37536
  const monorepoRoot = findMonorepoRoot(directory);
37452
37537
  if (monorepoRoot) {
37453
37538
  const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
37454
- if (isFile(monorepoPackageJsonPath)) {
37455
- const rootPackageJson = readPackageJson$1(monorepoPackageJsonPath);
37456
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
37457
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
37458
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
37459
- }
37539
+ if (isFile(monorepoPackageJsonPath)) fillFromCatalogs(readPackageJson$1(monorepoPackageJsonPath), monorepoRoot);
37460
37540
  }
37461
37541
  }
37462
- if (!reactVersion || framework === "unknown") {
37463
- const workspaceInfo = findReactInWorkspaces(directory, packageJson);
37464
- if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
37465
- if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
37466
- if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
37467
- if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
37542
+ const shouldCollectReactGroup = !tracked.react.version || framework === "unknown";
37543
+ const workspaceFacts = collectWorkspaceFacts(directory, packageJson, { collectReactGroup: shouldCollectReactGroup });
37544
+ if (shouldCollectReactGroup) {
37545
+ tracked.react.version ||= workspaceFacts.reactVersion;
37546
+ tracked.tailwindcss.version ||= workspaceFacts.tailwindVersion;
37547
+ tracked.zod.version ||= workspaceFacts.zodVersion;
37548
+ if (framework === "unknown" && workspaceFacts.framework !== "unknown") framework = workspaceFacts.framework;
37468
37549
  }
37469
- if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
37550
+ if ((!tracked.react.version || framework === "unknown") && !isMonorepoRoot(directory)) {
37470
37551
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
37471
- if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
37472
- if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
37473
- if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
37552
+ tracked.react.version ||= monorepoInfo.reactVersion;
37553
+ tracked.tailwindcss.version ||= monorepoInfo.tailwindVersion;
37554
+ tracked.zod.version ||= monorepoInfo.zodVersion;
37474
37555
  if (framework === "unknown") framework = monorepoInfo.framework;
37475
37556
  }
37476
- if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
37477
- if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
37478
- if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
37557
+ for (const [packageName, entry] of Object.entries(tracked)) {
37558
+ const declaredVersion = declarations[packageName].version;
37559
+ if (!entry.version && declaredVersion && !isCatalogReference(declaredVersion)) entry.version = declaredVersion;
37560
+ }
37561
+ const { react, tailwindcss, zod } = tracked;
37562
+ let reactVersion = react.version;
37479
37563
  if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
37564
+ const tailwindVersion = tailwindcss.version;
37565
+ const zodVersion = zod.version;
37480
37566
  const projectName = packageJson.name ?? Path.basename(directory);
37481
37567
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
37482
37568
  const sourceFileCount = countSourceFiles(directory);
37483
- const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
37569
+ const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
37484
37570
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
37485
37571
  rootDirectory: directory,
37486
37572
  rootPackageJson: packageJson,
37487
37573
  packageName: "expo",
37488
- version: findExpoVersion(directory, packageJson)
37574
+ version: workspaceFacts.expo.version
37489
37575
  }) : null;
37490
37576
  const shopifyFlashListVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
37491
37577
  rootDirectory: directory,
37492
37578
  rootPackageJson: packageJson,
37493
37579
  packageName: SHOPIFY_FLASH_LIST_PACKAGE_NAME,
37494
- version: findShopifyFlashListVersion(directory, packageJson)
37580
+ version: workspaceFacts.shopifyFlashList.version
37495
37581
  }) : null;
37496
- const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
37582
+ const hasReanimated = hasReactNativeWorkspace && workspaceFacts.hasReanimatedAwarePackage;
37583
+ const reanimatedVersion = hasReanimated ? workspaceFacts.reanimatedVersion : null;
37497
37584
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
37498
37585
  rootDirectory: directory,
37499
37586
  rootPackageJson: packageJson,
37500
37587
  packageName: "next",
37501
- version: findNextjsVersion(directory, packageJson)
37588
+ version: workspaceFacts.next.version
37502
37589
  }) : null;
37503
37590
  const preactVersion = getPreactVersion(packageJson);
37504
37591
  const isPreES2023Target = hasTypeScript && detectPreES2023Target(directory);
@@ -37509,7 +37596,7 @@ const discoverProject = (directory) => {
37509
37596
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
37510
37597
  tailwindVersion,
37511
37598
  zodVersion,
37512
- zodMajorVersion: parseZodMajor(zodVersion),
37599
+ zodMajorVersion: zodVersion === null ? null : getLowestDependencyMajor(zodVersion),
37513
37600
  framework,
37514
37601
  hasTypeScript,
37515
37602
  hasReactCompiler: detectReactCompiler(directory, packageJson),
@@ -37523,7 +37610,9 @@ const discoverProject = (directory) => {
37523
37610
  shopifyFlashListVersion,
37524
37611
  shopifyFlashListMajorVersion: shopifyFlashListVersion === null ? null : getLowestDependencyMajor(shopifyFlashListVersion),
37525
37612
  hasReanimated,
37613
+ reanimatedVersion,
37526
37614
  isPreES2023Target,
37615
+ isStaticExport: framework === "nextjs" && detectNextjsStaticExport(workspaceFacts.next.sourceDirectory ?? directory),
37527
37616
  sourceFileCount
37528
37617
  };
37529
37618
  cachedProjectInfos.set(directory, projectInfo);
@@ -37539,57 +37628,6 @@ const discoverProject = (directory) => {
37539
37628
  * single source file has nothing to analyze.
37540
37629
  */
37541
37630
  const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null || project.sourceFileCount > 0;
37542
- const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
37543
- const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
37544
- const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
37545
- const parseReactMajorMinor = (reactVersion) => {
37546
- if (typeof reactVersion !== "string") return null;
37547
- const trimmed = reactVersion.trim();
37548
- if (trimmed.length === 0) return null;
37549
- const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
37550
- if (lowerBoundsOnly.length === 0) return null;
37551
- const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
37552
- if (majorMinorMatch) {
37553
- const major = Number.parseInt(majorMinorMatch[1], 10);
37554
- const minor = Number.parseInt(majorMinorMatch[2], 10);
37555
- if (!Number.isFinite(major) || major <= 0) return null;
37556
- if (!Number.isFinite(minor) || minor < 0) return null;
37557
- return {
37558
- major,
37559
- minor
37560
- };
37561
- }
37562
- const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
37563
- if (!majorOnlyMatch) return null;
37564
- const major = Number.parseInt(majorOnlyMatch[1], 10);
37565
- if (!Number.isFinite(major) || major <= 0) return null;
37566
- return {
37567
- major,
37568
- minor: 0
37569
- };
37570
- };
37571
- const isReactAtLeast = (detected, required) => {
37572
- if (detected === null) return true;
37573
- if (detected.major !== required.major) return detected.major > required.major;
37574
- return detected.minor >= required.minor;
37575
- };
37576
- const parseLowerBoundVersion = (versionSpec) => import_semver.validRange(versionSpec) !== null ? import_semver.minVersion(versionSpec) : import_semver.coerce(versionSpec);
37577
- const parseTailwindMajorMinor = (tailwindVersion) => {
37578
- if (typeof tailwindVersion !== "string") return null;
37579
- const trimmed = tailwindVersion.trim();
37580
- if (trimmed.length === 0) return null;
37581
- const lowerBound = parseLowerBoundVersion(trimmed);
37582
- if (lowerBound === null || lowerBound.major <= 0) return null;
37583
- return {
37584
- major: lowerBound.major,
37585
- minor: lowerBound.minor
37586
- };
37587
- };
37588
- const isTailwindAtLeast = (detected, required) => {
37589
- if (detected === null) return true;
37590
- if (detected.major !== required.major) return detected.major > required.major;
37591
- return detected.minor >= required.minor;
37592
- };
37593
37631
  const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
37594
37632
  var InvalidGlobPatternError = class extends Error {
37595
37633
  pattern;
@@ -37715,6 +37753,9 @@ const buildRuleSeverityControls = (config) => {
37715
37753
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
37716
37754
  };
37717
37755
  };
37756
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
37757
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
37758
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
37718
37759
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
37719
37760
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
37720
37761
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -37792,6 +37833,7 @@ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
37792
37833
  "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
37793
37834
  "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
37794
37835
  "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
37836
+ "react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
37795
37837
  "react/jsx-no-undef": "react-doctor/jsx-no-undef",
37796
37838
  "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
37797
37839
  "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
@@ -37876,9 +37918,6 @@ const tokenizeRuleList = (ruleList) => {
37876
37918
  if (!ruleSection) return [];
37877
37919
  return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
37878
37920
  };
37879
- const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
37880
- const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
37881
- const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
37882
37921
  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}\`).`;
37883
37922
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
37884
37923
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
@@ -37932,6 +37971,53 @@ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
37932
37971
  if (!ruleId.startsWith("react-doctor/")) return null;
37933
37972
  return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
37934
37973
  };
37974
+ const ESLINT_REACT_HOOKS_PLUGIN_PREFIX = "react-hooks/";
37975
+ const buildAcceptedTokens = (ruleId) => {
37976
+ const shortRuleId = ruleId.slice(15);
37977
+ return new Set([ruleId, `${ESLINT_REACT_HOOKS_PLUGIN_PREFIX}${shortRuleId}`]);
37978
+ };
37979
+ const listNamesRule = (ruleList, acceptedTokens) => tokenizeRuleList(ruleList).some((token) => acceptedTokens.has(token));
37980
+ const isInlineDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
37981
+ const candidates = [{
37982
+ line: lines[diagnosticLineIndex],
37983
+ requiredScope: "line"
37984
+ }, {
37985
+ line: lines[diagnosticLineIndex - 1],
37986
+ requiredScope: "next-line"
37987
+ }];
37988
+ for (const { line, requiredScope } of candidates) {
37989
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
37990
+ if (!match) continue;
37991
+ const [, , scope, ruleList] = match;
37992
+ if (scope !== requiredScope) continue;
37993
+ if (listNamesRule(ruleList, acceptedTokens)) return true;
37994
+ }
37995
+ return false;
37996
+ };
37997
+ const isBlockDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
37998
+ let isRangeOpen = false;
37999
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
38000
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
38001
+ const line = lines[lineIndex];
38002
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
38003
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
38004
+ if (disableMatch && listNamesRule(disableMatch[2], acceptedTokens)) {
38005
+ isRangeOpen = true;
38006
+ continue;
38007
+ }
38008
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
38009
+ if (enableMatch) {
38010
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
38011
+ if (enabledRules.length === 0 || enabledRules.some((rule) => acceptedTokens.has(rule))) isRangeOpen = false;
38012
+ }
38013
+ }
38014
+ return isRangeOpen;
38015
+ };
38016
+ const isCompilerRuleForeignDisabled = (lines, diagnosticLineIndex, ruleId) => {
38017
+ if (!ruleId.startsWith("react-hooks-js/")) return false;
38018
+ const acceptedTokens = buildAcceptedTokens(ruleId);
38019
+ return isInlineDisabled(lines, diagnosticLineIndex, acceptedTokens) || isBlockDisabled(lines, diagnosticLineIndex, acceptedTokens);
38020
+ };
37935
38021
  const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
37936
38022
  const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
37937
38023
  const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
@@ -38075,6 +38161,11 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
38075
38161
  isSuppressed: true,
38076
38162
  nearMissHint: null
38077
38163
  };
38164
+ if (isCompilerRuleForeignDisabled(lines, diagnosticLineIndex, ruleId)) return {
38165
+ isSuppressed: true,
38166
+ nearMissHint: null,
38167
+ isForeignDirective: true
38168
+ };
38078
38169
  const directComments = findStackedDisableCommentsAbove(lines, diagnosticLineIndex);
38079
38170
  if (hasChainSuppressor(directComments, ruleId)) return {
38080
38171
  isSuppressed: true,
@@ -38356,12 +38447,18 @@ const collectStringSet = (values) => {
38356
38447
  return new Set(values.filter((value) => typeof value === "string"));
38357
38448
  };
38358
38449
  const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
38450
+ const NODE_CLI_PATH_SEGMENTS = new Set([
38451
+ "scripts",
38452
+ "bin",
38453
+ "tools"
38454
+ ]);
38455
+ const hasNodeCliPathSegment = (filePath) => filePath.split(/[\\/]/).slice(0, -1).some((segment) => NODE_CLI_PATH_SEGMENTS.has(segment));
38359
38456
  /**
38360
38457
  * Pre-compiles every stateful filter and returns a single
38361
38458
  * `apply(diagnostic)` closure that runs (in order):
38362
38459
  *
38363
38460
  * 1. auto-suppress (test-noise rules in test files; `migration-hint`
38364
- * wins over `test-noise`)
38461
+ * wins over `test-noise`; `no-console` on Node CLI scripts)
38365
38462
  * 2. severity overrides (top-level `rules` / `categories`, with
38366
38463
  * `"off"` dropping)
38367
38464
  * 3. warning suppression (only when `showWarnings` is false: drops every
@@ -38459,6 +38556,12 @@ const buildDiagnosticPipeline = (input) => {
38459
38556
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
38460
38557
  return false;
38461
38558
  };
38559
+ const isNoConsoleOnNodeCliScript = (diagnostic) => {
38560
+ if (diagnostic.rule !== "no-console") return false;
38561
+ if (hasNodeCliPathSegment(diagnostic.filePath)) return true;
38562
+ const lines = getFileLines(diagnostic.filePath);
38563
+ return lines !== null && Boolean(lines[0]?.startsWith("#!"));
38564
+ };
38462
38565
  const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
38463
38566
  if (!hasRuntimeGlobals) return false;
38464
38567
  if (diagnostic.rule !== "jsx-no-undef") return false;
@@ -38468,6 +38571,7 @@ const buildDiagnosticPipeline = (input) => {
38468
38571
  return {
38469
38572
  apply: (diagnostic) => {
38470
38573
  if (shouldAutoSuppress(diagnostic)) return null;
38574
+ if (isNoConsoleOnNodeCliScript(diagnostic)) return null;
38471
38575
  let current = diagnostic;
38472
38576
  let explicitSeverityOverride;
38473
38577
  let explicitRuleOverride;
@@ -38497,7 +38601,7 @@ const buildDiagnosticPipeline = (input) => {
38497
38601
  if (lines) {
38498
38602
  const ruleIdentifier = `${current.plugin}/${current.rule}`;
38499
38603
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
38500
- if (evaluation.isSuppressed) return suppress(current, "inline");
38604
+ if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
38501
38605
  if (evaluation.nearMissHint) current = {
38502
38606
  ...current,
38503
38607
  suppressionHint: evaluation.nearMissHint
@@ -40334,64 +40438,64 @@ function* collectSecurityScanFiles(rootDirectory) {
40334
40438
  }
40335
40439
  }
40336
40440
  }
40337
- /**
40338
- * Whether project discovery resolved a React-compatible runtime (React or
40339
- * Preact, which ships the same hooks + JSX model). This is the same
40340
- * predicate that grants the `react` capability, so `false` means every
40341
- * React-runtime rule family was gated off for the project.
40342
- */
40343
- const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
40441
+ const CLIENT_ONLY_FRAMEWORKS = new Set([
40442
+ "vite",
40443
+ "cra",
40444
+ "gatsby",
40445
+ "react-native",
40446
+ "expo"
40447
+ ]);
40448
+ const addMajorLadder = (capabilities, name, major, earliest, latest) => {
40449
+ if (major === null) return;
40450
+ const cappedMajor = Math.min(major, latest);
40451
+ for (let candidate = earliest; candidate <= cappedMajor; candidate += 1) capabilities.add(`${name}:${candidate}`);
40452
+ };
40344
40453
  const buildCapabilities = (project) => {
40345
40454
  const capabilities = /* @__PURE__ */ new Set();
40346
40455
  capabilities.add(project.framework);
40347
- if (hasReactRuntime(project)) capabilities.add("react");
40456
+ if (project.reactVersion !== null || project.preactVersion !== null) capabilities.add("react");
40348
40457
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
40349
40458
  if (project.expoVersion !== null) capabilities.add("expo");
40459
+ if ((project.framework === "nextjs" || project.framework === "tanstack-start" || project.framework === "remix") && !project.isStaticExport) capabilities.add("server-actions");
40460
+ if (CLIENT_ONLY_FRAMEWORKS.has(project.framework)) capabilities.add("client-only");
40461
+ if (project.isStaticExport) capabilities.add("nextjs:static-export");
40350
40462
  if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
40351
- const reactMajor = project.reactMajorVersion;
40352
- if (reactMajor !== null) {
40353
- const cappedReactMajor = Math.min(reactMajor, 30);
40354
- for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
40355
- if (reactMajor >= 19) {
40356
- if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
40357
- major: 19,
40358
- minor: 2
40359
- })) capabilities.add("react:19.2");
40360
- }
40361
- }
40362
- if (project.tailwindVersion !== null) {
40363
- capabilities.add("tailwind");
40364
- if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
40365
- major: 3,
40366
- minor: 4
40367
- })) capabilities.add("tailwind:3.4");
40368
- }
40369
- if (project.zodVersion !== null) {
40370
- capabilities.add("zod");
40371
- if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
40372
- }
40463
+ addMajorLadder(capabilities, "react", project.reactMajorVersion, 17, 30);
40464
+ if (project.reactMajorVersion !== null && project.reactMajorVersion >= 19 && isMajorMinorAtLeast(parseReactMajorMinor(project.reactVersion), {
40465
+ major: 19,
40466
+ minor: 2
40467
+ })) capabilities.add("react:19.2");
40468
+ if (project.tailwindVersion !== null) capabilities.add("tailwind");
40469
+ if (project.tailwindVersion !== null && isMajorMinorAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
40470
+ major: 3,
40471
+ minor: 4
40472
+ })) capabilities.add("tailwind:3.4");
40473
+ if (project.zodVersion !== null) capabilities.add("zod");
40474
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
40373
40475
  if (project.isPreES2023Target) capabilities.add("pre-es2023");
40374
40476
  if (project.hasReactCompiler) capabilities.add("react-compiler");
40375
40477
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
40376
40478
  if (project.hasTypeScript) capabilities.add("typescript");
40377
- if (project.preactVersion !== null) {
40378
- capabilities.add("preact");
40379
- const preactMajor = project.preactMajorVersion;
40380
- if (preactMajor !== null) {
40381
- const cappedPreactMajor = Math.min(preactMajor, 20);
40382
- for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
40383
- }
40384
- if (project.reactVersion === null) capabilities.add("pure-preact");
40385
- }
40479
+ if (project.preactVersion !== null) capabilities.add("preact");
40480
+ addMajorLadder(capabilities, "preact", project.preactMajorVersion, 10, 20);
40481
+ if (project.preactVersion !== null && project.reactVersion === null) capabilities.add("pure-preact");
40482
+ return capabilities;
40483
+ };
40484
+ const capabilitiesByProject = /* @__PURE__ */ new WeakMap();
40485
+ const getCapabilities = (project) => {
40486
+ const cached = capabilitiesByProject.get(project);
40487
+ if (cached !== void 0) return cached;
40488
+ const capabilities = buildCapabilities(project);
40489
+ capabilitiesByProject.set(project, capabilities);
40386
40490
  return capabilities;
40387
40491
  };
40388
- const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
40492
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhen) => {
40389
40493
  if (requires) {
40390
40494
  for (const capability of requires) if (!capabilities.has(capability)) return false;
40391
40495
  }
40392
40496
  if (tags?.includes("react-jsx-only") && !capabilities.has("react")) return false;
40393
- if (disabledBy) {
40394
- for (const capability of disabledBy) if (capabilities.has(capability)) return false;
40497
+ if (disabledWhen) {
40498
+ for (const capability of disabledWhen) if (capabilities.has(capability)) return false;
40395
40499
  }
40396
40500
  if (tags) {
40397
40501
  for (const tag of tags) if (ignoredTags.has(tag)) return false;
@@ -40402,14 +40506,14 @@ const yieldToEventLoop = () => new Promise((resolve) => {
40402
40506
  setImmediate(resolve);
40403
40507
  });
40404
40508
  const createSecurityScanSession = (rootDirectory, options) => {
40405
- const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
40509
+ const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
40406
40510
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
40407
40511
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
40408
40512
  const rule = entry.rule;
40409
40513
  const scan = rule.scan;
40410
40514
  if (typeof scan !== "function") return [];
40411
40515
  if (rule.defaultEnabled === false) return [];
40412
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) return [];
40516
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) return [];
40413
40517
  return [{
40414
40518
  entry,
40415
40519
  scan,
@@ -40429,6 +40533,7 @@ const createSecurityScanSession = (rootDirectory, options) => {
40429
40533
  return status === true;
40430
40534
  };
40431
40535
  const scanFileByRule = function* (file) {
40536
+ if (file === null) return;
40432
40537
  for (const { entry, scan, committedFilesOnly } of enabledScanRules) {
40433
40538
  for (const finding of scan(file)) {
40434
40539
  if (committedFilesOnly && isFileGitIgnored(file)) continue;
@@ -42325,7 +42430,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42325
42430
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
42326
42431
  const jsPlugins = [];
42327
42432
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
42328
- const capabilities = buildCapabilities(project);
42433
+ const capabilities = getCapabilities(project);
42329
42434
  const enabledReactDoctorRules = {};
42330
42435
  for (const registryEntry of REACT_DOCTOR_RULES) {
42331
42436
  const rule = reactDoctorPlugin.rules[registryEntry.id];
@@ -42336,7 +42441,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42336
42441
  if (rule.scan !== void 0) continue;
42337
42442
  if (customRulesOnly && registryEntry.originallyExternal) continue;
42338
42443
  if (rule.framework !== "global" && !rule.requires) continue;
42339
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
42444
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) continue;
42340
42445
  const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
42341
42446
  if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
42342
42447
  const severity = resolveRuleSeverityOverride({
@@ -42367,6 +42472,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42367
42472
  settings: { "react-doctor": {
42368
42473
  framework: project.framework,
42369
42474
  rootDirectory: resolveSettingsRootDirectory(project.rootDirectory),
42475
+ capabilities: [...capabilities].sort(),
42370
42476
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
42371
42477
  ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
42372
42478
  } },
@@ -42657,25 +42763,18 @@ const mapWithConcurrency = async (items, concurrency, task) => {
42657
42763
  if (errors.length > 0) throw errors[0];
42658
42764
  return results;
42659
42765
  };
42660
- const getPublicEnvPrefix = (framework) => {
42661
- switch (framework) {
42662
- case "nextjs": return "NEXT_PUBLIC_*";
42663
- case "vite":
42664
- case "tanstack-start": return "VITE_*";
42665
- case "cra": return "REACT_APP_*";
42666
- case "gatsby": return "GATSBY_*";
42667
- default: return null;
42668
- }
42669
- };
42670
- const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
42671
- const publicEnvPrefix = getPublicEnvPrefix(project.framework);
42672
- if (!publicEnvPrefix) return fallbackRecommendation;
42673
- 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`;
42674
- };
42675
42766
  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";
42767
+ const ACCESSORS_MIN_VERSION = "3.15.0";
42768
+ const supportsAccessors = (reanimatedVersion) => {
42769
+ if (reanimatedVersion === null) return true;
42770
+ const lowerBound = import_semver.validRange(reanimatedVersion) !== null ? import_semver.minVersion(reanimatedVersion) : import_semver.coerce(reanimatedVersion);
42771
+ if (lowerBound === null) return true;
42772
+ return import_semver.gte(lowerBound, ACCESSORS_MIN_VERSION);
42773
+ };
42676
42774
  const appendReanimatedSharedValueHint = (help, rule, project) => {
42677
42775
  if (rule !== "immutability") return help;
42678
42776
  if (!project.hasReanimated) return help;
42777
+ if (!supportsAccessors(project.reanimatedVersion)) return help;
42679
42778
  if (!help) return REANIMATED_SHARED_VALUE_HINT;
42680
42779
  return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
42681
42780
  };
@@ -42837,13 +42936,13 @@ const REACT_USE_BINDING_RESOLUTION = {
42837
42936
  isReactUseBinding: true,
42838
42937
  isReactNamespaceBinding: false
42839
42938
  };
42840
- const getScriptKind = (filename) => {
42939
+ const getScriptKind$2 = (filename) => {
42841
42940
  if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
42842
42941
  if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
42843
42942
  if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
42844
42943
  return ts.ScriptKind.JS;
42845
42944
  };
42846
- const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
42945
+ const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
42847
42946
  const unwrapExpression = (expression) => {
42848
42947
  let currentExpression = expression;
42849
42948
  while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
@@ -43092,8 +43191,8 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
43092
43191
  return matchedIdentifier;
43093
43192
  };
43094
43193
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
43095
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filename));
43096
- const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset(sourceText, utf8Offset));
43194
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
43195
+ const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
43097
43196
  if (!useIdentifier) return null;
43098
43197
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
43099
43198
  };
@@ -43114,6 +43213,201 @@ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
43114
43213
  const bindingResolution = resolveUseCallBinding(sourceText, absolutePath, primaryLabel.span.offset);
43115
43214
  return bindingResolution !== null && !bindingResolution.isReactUseBinding;
43116
43215
  };
43216
+ const SUPPRESSED_COMPILER_CODES = new Set(["react-hooks-js(immutability)", "react-hooks-js(refs)"]);
43217
+ const WORKLET_DIRECTIVE = "worklet";
43218
+ const WORKLET_ACCEPTING_CALLEES = new Set([
43219
+ "useAnimatedStyle",
43220
+ "useAnimatedProps",
43221
+ "useDerivedValue",
43222
+ "useAnimatedReaction",
43223
+ "useAnimatedScrollHandler",
43224
+ "useAnimatedGestureHandler",
43225
+ "useFrameCallback",
43226
+ "runOnUI",
43227
+ "executeOnUIRuntimeSync"
43228
+ ]);
43229
+ const GESTURE_ROOT_IDENTIFIER = "Gesture";
43230
+ const getScriptKind$1 = (filename) => {
43231
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43232
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43233
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43234
+ return ts.ScriptKind.JS;
43235
+ };
43236
+ const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
43237
+ const hasWorkletDirective = (node) => {
43238
+ if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
43239
+ const body = node.body;
43240
+ if (!body || !ts.isBlock(body)) return false;
43241
+ for (const statement of body.statements) {
43242
+ if (!ts.isExpressionStatement(statement)) return false;
43243
+ if (!ts.isStringLiteral(statement.expression)) return false;
43244
+ if (statement.expression.text === WORKLET_DIRECTIVE) return true;
43245
+ }
43246
+ return false;
43247
+ };
43248
+ const rootIdentifierOfCallChain = (expression) => {
43249
+ let currentExpression = expression;
43250
+ while (true) {
43251
+ if (ts.isIdentifier(currentExpression)) return currentExpression.text;
43252
+ if (ts.isPropertyAccessExpression(currentExpression)) {
43253
+ currentExpression = currentExpression.expression;
43254
+ continue;
43255
+ }
43256
+ if (ts.isCallExpression(currentExpression)) {
43257
+ currentExpression = currentExpression.expression;
43258
+ continue;
43259
+ }
43260
+ return null;
43261
+ }
43262
+ };
43263
+ const isWorkletAcceptingCall = (callExpression) => {
43264
+ const callee = callExpression.expression;
43265
+ if (ts.isIdentifier(callee)) return WORKLET_ACCEPTING_CALLEES.has(callee.text);
43266
+ if (ts.isPropertyAccessExpression(callee)) {
43267
+ if (WORKLET_ACCEPTING_CALLEES.has(callee.name.text)) return true;
43268
+ return rootIdentifierOfCallChain(callee) === GESTURE_ROOT_IDENTIFIER;
43269
+ }
43270
+ return false;
43271
+ };
43272
+ const isFunctionWorklet = (functionNode) => {
43273
+ if (hasWorkletDirective(functionNode)) return true;
43274
+ const parent = functionNode.parent;
43275
+ return ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === functionNode) && isWorkletAcceptingCall(parent);
43276
+ };
43277
+ const findNodeAtOffset = (sourceFile, targetOffset) => {
43278
+ let matchedNode = null;
43279
+ const visit = (node) => {
43280
+ if (node.getStart(sourceFile) > targetOffset || node.getEnd() <= targetOffset) return;
43281
+ matchedNode = node;
43282
+ ts.forEachChild(node, visit);
43283
+ };
43284
+ visit(sourceFile);
43285
+ return matchedNode;
43286
+ };
43287
+ const isOffsetInsideWorklet = (sourceFile, targetOffset) => {
43288
+ let currentNode = findNodeAtOffset(sourceFile, targetOffset);
43289
+ while (currentNode) {
43290
+ if (ts.isFunctionLike(currentNode) && isFunctionWorklet(currentNode)) return true;
43291
+ currentNode = currentNode.parent;
43292
+ }
43293
+ return false;
43294
+ };
43295
+ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirectory) => {
43296
+ if (!SUPPRESSED_COMPILER_CODES.has(diagnostic.code)) return false;
43297
+ if (!project.hasReanimated) return false;
43298
+ const primaryLabel = diagnostic.labels[0];
43299
+ if (!primaryLabel) return false;
43300
+ const absolutePath = Path.isAbsolute(diagnostic.filename) ? diagnostic.filename : Path.join(rootDirectory, diagnostic.filename);
43301
+ let sourceText;
43302
+ try {
43303
+ sourceText = NFS.readFileSync(absolutePath, "utf-8");
43304
+ } catch {
43305
+ return false;
43306
+ }
43307
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
43308
+ };
43309
+ const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
43310
+ const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
43311
+ const REACT_COMPILER_PLUGIN = "react-hooks-js";
43312
+ const getScriptKind = (filename) => {
43313
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43314
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43315
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43316
+ return ts.ScriptKind.JS;
43317
+ };
43318
+ const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
43319
+ const nodeLineRange = (sourceFile, node) => ({
43320
+ startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
43321
+ endLine: sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1
43322
+ });
43323
+ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
43324
+ let foundRange = null;
43325
+ const visit = (node) => {
43326
+ if (foundRange) return;
43327
+ if (ts.isFunctionLike(node)) {
43328
+ const range = nodeLineRange(sourceFile, node);
43329
+ if (range.startLine <= targetLine && targetLine <= range.endLine) foundRange = range;
43330
+ return;
43331
+ }
43332
+ ts.forEachChild(node, visit);
43333
+ };
43334
+ visit(sourceFile);
43335
+ return foundRange;
43336
+ };
43337
+ const NEWLINE_BYTE = 10;
43338
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
43339
+ let lineNumber = 1;
43340
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
43341
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
43342
+ return lineNumber;
43343
+ };
43344
+ /**
43345
+ * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
43346
+ * whose enclosing function also carries a `react-hooks-js` (React
43347
+ * Compiler) diagnostic. Every react-hooks-js finding marks a function the
43348
+ * compiler could NOT compile (a bail-out / skip / preserve-manual-
43349
+ * memoization failure), so "React Compiler already caches every value
43350
+ * here" is false for that function and the manual memoization is
43351
+ * load-bearing, not dead weight.
43352
+ *
43353
+ * Matching is per enclosing function, not per file: a bailed-out
43354
+ * component must not silence redundant-memo findings in a sibling
43355
+ * component the compiler did optimize.
43356
+ *
43357
+ * Module-scope `memo(() => …)` wrappers have no enclosing function; for
43358
+ * those the memo call's own span is the containment range, so a bail-out
43359
+ * inside the inline-wrapped component still suppresses. Every failure
43360
+ * path (unreadable file, missing span) fails open and keeps the
43361
+ * diagnostic.
43362
+ */
43363
+ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) => {
43364
+ const compilerDiagnosticLinesByFile = /* @__PURE__ */ new Map();
43365
+ let hasManualMemoizationDiagnostic = false;
43366
+ for (const diagnostic of diagnostics) {
43367
+ if (isManualMemoizationDiagnostic(diagnostic)) {
43368
+ hasManualMemoizationDiagnostic = true;
43369
+ continue;
43370
+ }
43371
+ if (diagnostic.plugin !== REACT_COMPILER_PLUGIN || diagnostic.line <= 0) continue;
43372
+ const lines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
43373
+ if (lines) lines.push(diagnostic.line);
43374
+ else compilerDiagnosticLinesByFile.set(diagnostic.filePath, [diagnostic.line]);
43375
+ }
43376
+ if (!hasManualMemoizationDiagnostic || compilerDiagnosticLinesByFile.size === 0) return diagnostics;
43377
+ const parsedFileCache = /* @__PURE__ */ new Map();
43378
+ const readAndParse = (filePath) => {
43379
+ const cached = parsedFileCache.get(filePath);
43380
+ if (cached !== void 0) return cached;
43381
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
43382
+ let parsed;
43383
+ try {
43384
+ const buffer = NFS.readFileSync(absolutePath);
43385
+ parsed = {
43386
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
43387
+ buffer
43388
+ };
43389
+ } catch {
43390
+ parsed = null;
43391
+ }
43392
+ parsedFileCache.set(filePath, parsed);
43393
+ return parsed;
43394
+ };
43395
+ const isInsideBailedOutFunction = (diagnostic) => {
43396
+ const compilerLines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
43397
+ if (!compilerLines || diagnostic.line <= 0) return false;
43398
+ const parsed = readAndParse(diagnostic.filePath);
43399
+ if (!parsed) return false;
43400
+ let containmentRange = findOutermostEnclosingFunctionRange(parsed.sourceFile, diagnostic.line);
43401
+ if (!containmentRange && diagnostic.offset !== void 0 && diagnostic.length !== void 0) containmentRange = {
43402
+ startLine: diagnostic.line,
43403
+ endLine: lineOfUtf8Offset(parsed.buffer, diagnostic.offset + diagnostic.length)
43404
+ };
43405
+ if (!containmentRange) return false;
43406
+ const { startLine, endLine } = containmentRange;
43407
+ return compilerLines.some((compilerLine) => startLine <= compilerLine && compilerLine <= endLine);
43408
+ };
43409
+ return diagnostics.filter((diagnostic) => !isManualMemoizationDiagnostic(diagnostic) || !isInsideBailedOutFunction(diagnostic));
43410
+ };
43117
43411
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
43118
43412
  const LEADING_SEVERITY_LABEL_PATTERN = /^(?:Error|Warning):\s*/;
43119
43413
  const TRAILING_PERIOD_PATTERN = /\.$/;
@@ -43149,8 +43443,14 @@ const PLUGIN_CATEGORY_MAP = {
43149
43443
  };
43150
43444
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
43151
43445
  const getRuleRecommendation = (ruleName, project) => {
43152
- if (ruleName === "no-secrets-in-client-code") return buildNoSecretsRecommendation(project, reactDoctorPlugin.rules["no-secrets-in-client-code"]?.recommendation ?? "Move secrets to server-only code");
43153
- return reactDoctorPlugin.rules[ruleName]?.recommendation;
43446
+ const rule = reactDoctorPlugin.rules[ruleName];
43447
+ if (!rule) return void 0;
43448
+ if (rule.recommendationFor) {
43449
+ const capabilities = getCapabilities(project);
43450
+ const conditionalRecommendation = rule.recommendationFor((capability) => capabilities.has(capability));
43451
+ if (conditionalRecommendation !== void 0) return conditionalRecommendation;
43452
+ }
43453
+ return rule.recommendation;
43154
43454
  };
43155
43455
  const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
43156
43456
  const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
@@ -43253,7 +43553,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43253
43553
  minifiedFileCache.set(absolutePath, minified);
43254
43554
  return minified;
43255
43555
  };
43256
- return parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
43556
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
43257
43557
  const { plugin, rule } = parseRuleCode(diagnostic.code);
43258
43558
  const primaryLabel = diagnostic.labels[0];
43259
43559
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -43280,7 +43580,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43280
43580
  ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
43281
43581
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
43282
43582
  };
43283
- });
43583
+ }), rootDirectory);
43284
43584
  };
43285
43585
  const buildOxlintChildEnv = (sourceEnv) => {
43286
43586
  const childEnv = {};
@@ -43304,7 +43604,9 @@ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
43304
43604
  * - `OxlintBatchExceeded { kind: "output-too-large" }` — stdout+stderr
43305
43605
  * crossed `OXLINT_OUTPUT_MAX_BYTES`.
43306
43606
  * - `OxlintBatchExceeded { kind: "oom" | "killed" }` — child exited
43307
- * on a signal (SIGABRT → OOM, others → generic kill).
43607
+ * on a signal (SIGABRT → OOM, others → generic kill), or with one
43608
+ * of the abort exit codes Windows reports instead of a signal
43609
+ * (`ABORT_EXIT_CODES` → OOM).
43308
43610
  * - `OxlintSpawnFailed { cause }` — `spawn` itself errored, or the
43309
43611
  * child exited successfully but printed only stderr.
43310
43612
  *
@@ -43370,7 +43672,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
43370
43672
  clearAbortListener();
43371
43673
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
43372
43674
  });
43373
- child.on("close", (_code, signal) => {
43675
+ child.on("close", (code, signal) => {
43374
43676
  clearTimeout(timeoutHandle);
43375
43677
  clearAbortListener();
43376
43678
  if (didKillForSize) {
@@ -43380,10 +43682,11 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
43380
43682
  }) }));
43381
43683
  return;
43382
43684
  }
43383
- if (signal) {
43685
+ const isAbortExitCode = code !== null && ABORT_EXIT_CODES.has(code);
43686
+ if (signal || isAbortExitCode) {
43384
43687
  const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
43385
- const isOom = signal === "SIGABRT";
43386
- const detailParts = [`killed by ${signal}`];
43688
+ const isOom = signal === "SIGABRT" || isAbortExitCode;
43689
+ const detailParts = [signal ? `killed by ${signal}` : `aborted with exit code ${code}`];
43387
43690
  if (isOom) detailParts.push("try scanning fewer files with --diff");
43388
43691
  if (stderrOutput) detailParts.push(stderrOutput);
43389
43692
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
@@ -43426,12 +43729,19 @@ const isParallelismRelatedSpawnError = (error) => {
43426
43729
  * (surfaced via `onPartialFailure`) so JSON-mode consumers see WHICH
43427
43730
  * files were skipped instead of silently losing them.
43428
43731
  *
43429
- * Parallel runs (concurrency > 1) get one extra safety net: if the pass
43430
- * fails with a resource-exhaustion error that's exclusive to running
43431
- * many oxlint subprocesses at once (EAGAIN / EMFILE / ENFILE / ENOMEM —
43432
- * see `isParallelismRelatedSpawnError`), the whole pass replays once
43433
- * with a single worker. That's the only failure a serial replay can
43434
- * clear, so every other error class is left to propagate.
43732
+ * Parallel runs (concurrency > 1) get two extra safety nets:
43733
+ * - If the pass fails with a resource-exhaustion error that's exclusive
43734
+ * to running many oxlint subprocesses at once (EAGAIN / EMFILE /
43735
+ * ENFILE / ENOMEM — see `isParallelismRelatedSpawnError`), the whole
43736
+ * pass replays once with a single worker. That's the only failure a
43737
+ * serial replay can clear, so every other error class is left to
43738
+ * propagate.
43739
+ * - Files dropped because oxlint's native binding SIGABRT'd under
43740
+ * memory pressure (`OxlintBatchExceeded { kind: "oom" }`) get one
43741
+ * serial single-file-batch rescue pass: the OOM is usually a function
43742
+ * of N concurrent oxlint allocator arenas, not the file itself, so a
43743
+ * lone process with the machine to itself typically clears it —
43744
+ * turning a partial scan into a complete (if slower) one.
43435
43745
  *
43436
43746
  * Errors that aren't splittable and aren't parallelism-related (oxlint
43437
43747
  * config crash, JS plugin resolution failure, etc.) propagate to the
@@ -43441,13 +43751,16 @@ const isParallelismRelatedSpawnError = (error) => {
43441
43751
  const spawnLintBatches = async (input) => {
43442
43752
  const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
43443
43753
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
43444
- const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
43445
- const runBatchPass = async (concurrency) => {
43754
+ let rescueDeadlineEpochMs;
43755
+ const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0 || rescueDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(rescueDeadlineEpochMs) === 0;
43756
+ const runBatchPass = async (concurrency, passBatches, passOnFileProgress) => {
43757
+ const totalFileCount = passBatches.reduce((sum, batch) => sum + batch.length, 0);
43446
43758
  const allDiagnostics = [];
43447
43759
  const droppedFiles = [];
43760
+ const oomDroppedFiles = [];
43448
43761
  const deadlineSkippedFiles = [];
43449
- const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0;
43450
43762
  let firstDropReason = null;
43763
+ let firstNonOomDropReason = null;
43451
43764
  const spawnLintBatch = async (batch, depth, batchState) => {
43452
43765
  if (isPastDeadline()) {
43453
43766
  deadlineSkippedFiles.push(...batch);
@@ -43469,12 +43782,14 @@ const spawnLintBatches = async (input) => {
43469
43782
  const isDepthCapReached = depth >= splitMaxDepth;
43470
43783
  if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
43471
43784
  droppedFiles.push(...batch);
43472
- if (firstDropReason === null) {
43473
- let limitHint = "";
43474
- if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
43475
- else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
43476
- firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
43477
- }
43785
+ const isOomDrop = error.reason._tag === "OxlintBatchExceeded" && error.reason.kind === "oom";
43786
+ if (isOomDrop) oomDroppedFiles.push(...batch);
43787
+ let limitHint = "";
43788
+ if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
43789
+ else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
43790
+ const dropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
43791
+ firstDropReason ??= dropReason;
43792
+ if (!isOomDrop) firstNonOomDropReason ??= dropReason;
43478
43793
  return [];
43479
43794
  }
43480
43795
  const splitIndex = Math.ceil(batch.length / 2);
@@ -43484,16 +43799,16 @@ const spawnLintBatches = async (input) => {
43484
43799
  let startedFileCount = 0;
43485
43800
  let scannedFileCount = 0;
43486
43801
  let displayedFileCount = 0;
43487
- const progressTimer = onFileProgress && totalFileCount > 1 ? setInterval(() => {
43802
+ const progressTimer = passOnFileProgress && totalFileCount > 1 ? setInterval(() => {
43488
43803
  const ceiling = Math.min(startedFileCount, totalFileCount - 1);
43489
43804
  if (displayedFileCount < ceiling) {
43490
43805
  displayedFileCount += 1;
43491
- onFileProgress(displayedFileCount, totalFileCount);
43806
+ passOnFileProgress(displayedFileCount, totalFileCount);
43492
43807
  }
43493
43808
  }, 50) : null;
43494
43809
  progressTimer?.unref?.();
43495
43810
  try {
43496
- const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
43811
+ const batchResults = await mapWithConcurrency(passBatches, concurrency, async (batch) => {
43497
43812
  if (isPastDeadline()) {
43498
43813
  deadlineSkippedFiles.push(...batch);
43499
43814
  return [];
@@ -43505,9 +43820,9 @@ const spawnLintBatches = async (input) => {
43505
43820
  };
43506
43821
  const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
43507
43822
  scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
43508
- if (onFileProgress) {
43823
+ if (passOnFileProgress) {
43509
43824
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
43510
- onFileProgress(displayedFileCount, totalFileCount);
43825
+ passOnFileProgress(displayedFileCount, totalFileCount);
43511
43826
  }
43512
43827
  return batchDiagnostics;
43513
43828
  });
@@ -43515,22 +43830,53 @@ const spawnLintBatches = async (input) => {
43515
43830
  } finally {
43516
43831
  if (progressTimer !== null) clearInterval(progressTimer);
43517
43832
  }
43518
- const reportSkippedFiles = (skippedFiles, buildMessage) => {
43519
- if (skippedFiles.length === 0 || !onPartialFailure) return;
43520
- onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
43833
+ return {
43834
+ diagnostics: allDiagnostics,
43835
+ droppedFiles,
43836
+ oomDroppedFiles,
43837
+ deadlineSkippedFiles,
43838
+ firstDropReason,
43839
+ firstNonOomDropReason
43521
43840
  };
43522
- const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
43523
- reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
43524
- reportSkippedFiles(deadlineSkippedFiles, (fileListPreview) => `${deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
43525
- return allDiagnostics;
43526
43841
  };
43527
- let diagnostics;
43842
+ let outcome;
43843
+ let effectiveConcurrency = requestedConcurrency;
43528
43844
  try {
43529
- diagnostics = await runBatchPass(requestedConcurrency);
43845
+ outcome = await runBatchPass(requestedConcurrency, fileBatches, onFileProgress);
43530
43846
  } catch (error) {
43531
43847
  if (requestedConcurrency <= 1 || !isParallelismRelatedSpawnError(error)) throw error;
43532
- diagnostics = await runBatchPass(1);
43848
+ effectiveConcurrency = 1;
43849
+ outcome = await runBatchPass(1, fileBatches, onFileProgress);
43850
+ }
43851
+ let { droppedFiles, firstDropReason } = outcome;
43852
+ const diagnostics = outcome.diagnostics;
43853
+ if (outcome.oomDroppedFiles.length > 0 && effectiveConcurrency > 1 && !isPastDeadline()) {
43854
+ rescueDeadlineEpochMs = Date.now() + OXLINT_OOM_RESCUE_BUDGET_MS;
43855
+ const rescuedFiles = /* @__PURE__ */ new Set();
43856
+ let firstRescueFailureReason = null;
43857
+ for (const oomDroppedFile of outcome.oomDroppedFiles) {
43858
+ if (isPastDeadline()) break;
43859
+ try {
43860
+ const rescueOutcome = await runBatchPass(1, [[oomDroppedFile]], void 0);
43861
+ diagnostics.push(...rescueOutcome.diagnostics);
43862
+ if (rescueOutcome.droppedFiles.length > 0 || rescueOutcome.deadlineSkippedFiles.length > 0) firstRescueFailureReason ??= rescueOutcome.firstDropReason;
43863
+ else rescuedFiles.add(oomDroppedFile);
43864
+ } catch (error) {
43865
+ firstRescueFailureReason ??= error instanceof Error ? error.message : String(error);
43866
+ }
43867
+ }
43868
+ rescueDeadlineEpochMs = void 0;
43869
+ droppedFiles = droppedFiles.filter((filePath) => !rescuedFiles.has(filePath));
43870
+ if (droppedFiles.length === 0) firstDropReason = null;
43871
+ else firstDropReason = firstRescueFailureReason ?? outcome.firstNonOomDropReason ?? firstDropReason;
43533
43872
  }
43873
+ const reportSkippedFiles = (skippedFiles, buildMessage) => {
43874
+ if (skippedFiles.length === 0 || !onPartialFailure) return;
43875
+ onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
43876
+ };
43877
+ const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
43878
+ reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
43879
+ reportSkippedFiles(outcome.deadlineSkippedFiles, (fileListPreview) => `${outcome.deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
43534
43880
  return dedupeDiagnostics(diagnostics);
43535
43881
  };
43536
43882
  let didValidate = false;
@@ -43791,6 +44137,7 @@ const runOxlint = async (options) => {
43791
44137
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
43792
44138
  const severityControls = buildRuleSeverityControls(userConfig);
43793
44139
  validateRuleRegistration();
44140
+ resetManifestCaches();
43794
44141
  if (includePaths !== void 0 && includePaths.length === 0) return [];
43795
44142
  const pluginPath = resolvePluginPath();
43796
44143
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
@@ -44453,9 +44800,10 @@ const CONTEXT_AXES = [
44453
44800
  }
44454
44801
  ];
44455
44802
  const SCORE_AXES = [...GATED_AXES, ...CONTEXT_AXES];
44456
- const worstGatedAxis = (score) => {
44457
- let worst = GATED_AXES[0];
44458
- for (const axis of GATED_AXES) if (score[axis.key] < score[worst.key]) worst = axis;
44803
+ const gatedAxesForSection = (section) => section === "devDependencies" ? GATED_AXES.filter((axis) => axis.key !== "vulnerability") : GATED_AXES;
44804
+ const worstGatedAxis = (score, gatedAxes) => {
44805
+ let worst = gatedAxes[0];
44806
+ for (const axis of gatedAxes) if (score[axis.key] < score[worst.key]) worst = axis;
44459
44807
  return worst;
44460
44808
  };
44461
44809
  const clampScore = (value) => {
@@ -44479,6 +44827,69 @@ const resolveConcreteVersion = (spec) => {
44479
44827
  if (range === null || range === "*") return null;
44480
44828
  return import_semver.minVersion(trimmed)?.version ?? null;
44481
44829
  };
44830
+ const readTextFileOrNull = (filePath) => {
44831
+ try {
44832
+ return NFS.readFileSync(filePath, "utf-8");
44833
+ } catch {
44834
+ return null;
44835
+ }
44836
+ };
44837
+ const buildNpmLockLookup = (lockText) => {
44838
+ let parsed;
44839
+ try {
44840
+ parsed = JSON.parse(lockText);
44841
+ } catch {
44842
+ return null;
44843
+ }
44844
+ if (typeof parsed !== "object" || parsed === null) return null;
44845
+ const lock = parsed;
44846
+ return (name) => {
44847
+ const entry = lock.packages?.[`node_modules/${name}`] ?? lock.dependencies?.[name];
44848
+ return typeof entry?.version === "string" ? entry.version : null;
44849
+ };
44850
+ };
44851
+ const buildYarnLockLookup = (lockText) => {
44852
+ const lines = lockText.split(/\r?\n/);
44853
+ return (name, spec) => {
44854
+ const descriptor = `${name}@${spec}`;
44855
+ const berryDescriptor = `${name}@npm:${spec}`;
44856
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
44857
+ const line = lines[lineIndex];
44858
+ if (line.startsWith(" ") || line.startsWith("#") || !line.includes(":")) continue;
44859
+ const descriptors = line.replace(/:\s*$/, "").split(",").map((part) => part.trim().replace(/^"|"$/g, ""));
44860
+ if (!descriptors.includes(descriptor) && !descriptors.includes(berryDescriptor)) continue;
44861
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && (lines[bodyIndex].startsWith(" ") || lines[bodyIndex] === ""); bodyIndex += 1) {
44862
+ const versionMatch = /^\s+version:?\s+"?([^"\s]+)"?/.exec(lines[bodyIndex]);
44863
+ if (versionMatch) return versionMatch[1];
44864
+ }
44865
+ return null;
44866
+ }
44867
+ return null;
44868
+ };
44869
+ };
44870
+ const buildPnpmLockLookup = (lockText) => {
44871
+ return (name) => {
44872
+ const entryPattern = new RegExp(`^ {4,8}['"]?${name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}['"]?:\\s*$`);
44873
+ const lines = lockText.split(/\r?\n/);
44874
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
44875
+ if (!entryPattern.test(lines[lineIndex])) continue;
44876
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && /^ {5,}\S/.test(lines[bodyIndex]); bodyIndex += 1) {
44877
+ const versionMatch = /^\s+version:\s+['"]?([^'"\s(]+)/.exec(lines[bodyIndex]);
44878
+ if (versionMatch) return versionMatch[1];
44879
+ }
44880
+ }
44881
+ return null;
44882
+ };
44883
+ };
44884
+ const buildLockedVersionLookup = (rootDirectory) => {
44885
+ const npmLockText = readTextFileOrNull(Path.join(rootDirectory, "package-lock.json"));
44886
+ if (npmLockText !== null) return buildNpmLockLookup(npmLockText);
44887
+ const yarnLockText = readTextFileOrNull(Path.join(rootDirectory, "yarn.lock"));
44888
+ if (yarnLockText !== null) return buildYarnLockLookup(yarnLockText);
44889
+ const pnpmLockText = readTextFileOrNull(Path.join(rootDirectory, "pnpm-lock.yaml"));
44890
+ if (pnpmLockText !== null) return buildPnpmLockLookup(pnpmLockText);
44891
+ return null;
44892
+ };
44482
44893
  const locateDependencyKey = (packageJsonText, section, name) => {
44483
44894
  const needle = `"${name}"`;
44484
44895
  const sectionHeader = new RegExp(`"${section}"\\s*:\\s*\\{`);
@@ -44505,7 +44916,7 @@ const locateDependencyKey = (packageJsonText, section, name) => {
44505
44916
  }
44506
44917
  return null;
44507
44918
  };
44508
- const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies) => {
44919
+ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies, lockedVersionLookup) => {
44509
44920
  const sectionByName = /* @__PURE__ */ new Map();
44510
44921
  for (const name of Object.keys(packageJson.dependencies ?? {})) sectionByName.set(name, "dependencies");
44511
44922
  if (includeDevDependencies) {
@@ -44515,13 +44926,18 @@ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDepe
44515
44926
  for (const [name, section] of sectionByName) {
44516
44927
  if (SUPPLY_CHAIN_IGNORED_PACKAGES.has(name)) continue;
44517
44928
  const spec = (packageJson[section] ?? {})[name] ?? "";
44518
- const version = resolveConcreteVersion(spec);
44519
- if (version === null) continue;
44929
+ const floorVersion = resolveConcreteVersion(spec);
44930
+ if (floorVersion === null) continue;
44931
+ const lockedVersion = lockedVersionLookup?.(name, spec) ?? null;
44932
+ const lockSatisfiesSpec = lockedVersion !== null && import_semver.valid(lockedVersion) !== null && import_semver.satisfies(lockedVersion, spec);
44933
+ const isExactPin = import_semver.valid(spec.trim()) !== null;
44520
44934
  const location = locateDependencyKey(packageJsonText, section, name);
44521
44935
  dependencies.push({
44522
44936
  name,
44523
- version,
44937
+ version: lockSatisfiesSpec ? lockedVersion : floorVersion,
44524
44938
  spec,
44939
+ section,
44940
+ resolution: isExactPin ? "pin" : lockSatisfiesSpec ? "locked" : "floor",
44525
44941
  line: location?.line ?? 0,
44526
44942
  column: location?.column ?? 0
44527
44943
  });
@@ -44672,7 +45088,11 @@ const formatAlertReason = (topAlerts, totalCount) => {
44672
45088
  }
44673
45089
  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)}.`;
44674
45090
  };
44675
- const formatDependencyIdentity = (dependency) => import_semver.valid(dependency.spec) !== null ? `${dependency.name}@${dependency.version}` : `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
45091
+ const formatDependencyIdentity = (dependency) => {
45092
+ if (dependency.resolution === "pin") return `${dependency.name}@${dependency.version}`;
45093
+ if (dependency.resolution === "locked") return `${dependency.name}@${dependency.version} (lockfile resolution of "${dependency.spec}")`;
45094
+ return `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
45095
+ };
44676
45096
  const buildSupplyChainHelp = (dependency, failingAxis, topAlerts, packagePageUrl, options) => {
44677
45097
  const hasCriticalAlert = topAlerts.some((alert) => alert.severity.toLowerCase() === "critical");
44678
45098
  const entry = `\`"${dependency.name}": "${dependency.spec}"\``;
@@ -44715,7 +45135,7 @@ const buildLowScoreDiagnostic = (dependency, artifact, failingAxis, options) =>
44715
45135
  const checkSupplyChain = (input) => gen(function* () {
44716
45136
  const options = resolveOptions(input.userConfig);
44717
45137
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
44718
- const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
45138
+ const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies, buildLockedVersionLookup(input.rootDirectory));
44719
45139
  if (dependencies.length === 0) return [];
44720
45140
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
44721
45141
  if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
@@ -44724,7 +45144,7 @@ const checkSupplyChain = (input) => gen(function* () {
44724
45144
  for (let index = 0; index < dependencies.length; index += 1) {
44725
45145
  const artifact = artifacts[index];
44726
45146
  if (!artifact) continue;
44727
- const worstAxis = worstGatedAxis(artifact.score);
45147
+ const worstAxis = worstGatedAxis(artifact.score, gatedAxesForSection(dependencies[index].section));
44728
45148
  if (toHundred(artifact.score[worstAxis.key]) >= options.minScore) continue;
44729
45149
  diagnostics.push(buildLowScoreDiagnostic(dependencies[index], artifact, worstAxis, options));
44730
45150
  }
@@ -45425,6 +45845,13 @@ const summarizeDiagnostics = (diagnostics, worstScore = null, worstScoreLabel =
45425
45845
  scoreLabel: worstScoreLabel
45426
45846
  };
45427
45847
  };
45848
+ /**
45849
+ * Whether project discovery resolved a React-compatible runtime (React or
45850
+ * Preact, which ships the same hooks + JSX model). This is the same
45851
+ * predicate that grants the `react` capability, so `false` means every
45852
+ * React-runtime rule family was gated off for the project.
45853
+ */
45854
+ const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
45428
45855
  const toJsonDiff = (diff) => {
45429
45856
  if (!diff) return null;
45430
45857
  return {
@@ -46007,7 +46434,7 @@ const makeNoopConsole = () => ({
46007
46434
  });
46008
46435
  //#endregion
46009
46436
  //#region src/cli/utils/version.ts
46010
- const VERSION = "0.7.1-dev.d353dad";
46437
+ const VERSION = "0.7.2-dev.08b768b";
46011
46438
  //#endregion
46012
46439
  //#region src/cli/utils/json-mode.ts
46013
46440
  let context = null;
@@ -46376,13 +46803,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
46376
46803
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
46377
46804
  * standard `SENTRY_RELEASE` override.
46378
46805
  */
46379
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.1-dev.d353dad`;
46806
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.2-dev.08b768b`;
46380
46807
  /**
46381
46808
  * Deployment environment shown in Sentry's environment filter. Defaults to
46382
46809
  * `production` for tagged releases and `development` for dev/unbuilt versions,
46383
46810
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
46384
46811
  */
46385
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.1-dev.d353dad") ? "development" : "production");
46812
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.2-dev.08b768b") ? "development" : "production");
46386
46813
  /**
46387
46814
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
46388
46815
  * (set to `0` to disable tracing) and falls back to
@@ -58899,13 +59326,15 @@ const buildOutcomeAttributes = (input) => {
58899
59326
  const countBySource = {
58900
59327
  config: 0,
58901
59328
  override: 0,
58902
- inline: 0
59329
+ inline: 0,
59330
+ "foreign-inline": 0
58903
59331
  };
58904
59332
  for (const suppression of input.suppressedRuleCounts) countBySource[suppression.source] += suppression.count;
58905
- suppressionRollup.suppressed = countBySource.config + countBySource.override + countBySource.inline;
59333
+ suppressionRollup.suppressed = countBySource.config + countBySource.override + countBySource.inline + countBySource["foreign-inline"];
58906
59334
  suppressionRollup.suppressedConfig = countBySource.config;
58907
59335
  suppressionRollup.suppressedOverride = countBySource.override;
58908
59336
  suppressionRollup.suppressedInline = countBySource.inline;
59337
+ suppressionRollup.suppressedForeignInline = countBySource["foreign-inline"];
58909
59338
  }
58910
59339
  const attributes = {
58911
59340
  ...withNamespace("outcome", {
@@ -64555,4 +64984,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
64555
64984
  export {};
64556
64985
 
64557
64986
  //# sourceMappingURL=cli.js.map
64558
- //# debugId=47338856-c658-5c78-a5e2-e871c2dafaa7
64987
+ //# debugId=9aed4e70-dd94-51e8-9810-f8e8945d80a3