react-doctor 0.7.2 → 0.7.3-dev.6b70b32

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]="7d3c9a4a-cf80-5529-97be-47427cc1e986")}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]="d6c8e327-6382-5aed-99c8-f3e537c25f51")}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";
@@ -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,6 +35985,18 @@ 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;
35971
36002
  const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
@@ -36100,6 +36131,8 @@ const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
36100
36131
  const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
36101
36132
  const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
36102
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;
36103
36136
  const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
36104
36137
  const LINT_PHASE_TIMEOUT_MS = 3e5;
36105
36138
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
@@ -36146,7 +36179,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36146
36179
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36147
36180
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36148
36181
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36149
- const CORE_PACKAGE_VERSION = "0.7.2";
36182
+ const CORE_PACKAGE_VERSION = "0.7.3";
36150
36183
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36151
36184
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36152
36185
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -36192,25 +36225,6 @@ const isLargeMinifiedFile = (absolutePath, knownSizeBytes) => {
36192
36225
  cachedIsLargeMinifiedByPath.set(absolutePath, result);
36193
36226
  return result;
36194
36227
  };
36195
- const isErrnoException = (error) => error instanceof Error && "code" in error;
36196
- const IGNORABLE_READDIR_ERROR_CODES = new Set([
36197
- "EACCES",
36198
- "EPERM",
36199
- "ENOENT",
36200
- "ENOTDIR",
36201
- "EINVAL",
36202
- "ELOOP",
36203
- "ENAMETOOLONG"
36204
- ]);
36205
- const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
36206
- const readDirectoryEntries = (directoryPath) => {
36207
- try {
36208
- return NFS.readdirSync(directoryPath, { withFileTypes: true });
36209
- } catch (error) {
36210
- if (isIgnorableReaddirError(error)) return [];
36211
- throw error;
36212
- }
36213
- };
36214
36228
  function* walkSourceTreeFiles(rootDirectory) {
36215
36229
  const stack = [rootDirectory];
36216
36230
  while (stack.length > 0) {
@@ -36265,23 +36279,6 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
36265
36279
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
36266
36280
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
36267
36281
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
36268
- /**
36269
- * The chain of ancestor directories of `startDirectory`, nearest first, up to
36270
- * and including the filesystem root. With `includeStart`, the chain begins at
36271
- * `startDirectory` itself; otherwise at its parent. The one place the "walk up
36272
- * the tree" traversal is expressed, so each caller adds only its own match /
36273
- * stop policy instead of re-deriving the `path.dirname` loop.
36274
- */
36275
- const ancestorDirectories = (startDirectory, { includeStart }) => {
36276
- const directories = [];
36277
- let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
36278
- while (true) {
36279
- directories.push(currentDirectory);
36280
- const parentDirectory = Path.dirname(currentDirectory);
36281
- if (parentDirectory === currentDirectory) return directories;
36282
- currentDirectory = parentDirectory;
36283
- }
36284
- };
36285
36282
  const cachedPackageJsons = /* @__PURE__ */ new Map();
36286
36283
  const readPackageJsonUncached = (packageJsonPath) => {
36287
36284
  try {
@@ -36312,7 +36309,11 @@ const isMonorepoRoot = (directory) => {
36312
36309
  return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
36313
36310
  };
36314
36311
  const findMonorepoRoot = (startDirectory) => {
36315
- 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
+ }
36316
36317
  return null;
36317
36318
  };
36318
36319
  /**
@@ -36326,6 +36327,146 @@ const findMonorepoRoot = (startDirectory) => {
36326
36327
  * byte-equivalent copy.
36327
36328
  */
36328
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
+ };
36329
36470
  const REACT_COMPILER_PACKAGES = new Set([
36330
36471
  "babel-plugin-react-compiler",
36331
36472
  "react-compiler-runtime",
@@ -36362,6 +36503,7 @@ const EXPO_APP_CONFIG_FILENAMES = [
36362
36503
  ];
36363
36504
  const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
36364
36505
  const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
36506
+ const STATIC_EXPORT_OUTPUT_PATTERN = /(?:^|[^.\w])["']?output["']?\s*:\s*["']export["']/m;
36365
36507
  const hasCompilerPackage = (packageJson) => {
36366
36508
  const allDependencies = {
36367
36509
  ...packageJson.peerDependencies,
@@ -36394,34 +36536,10 @@ const detectReactCompiler = (directory, packageJson) => {
36394
36536
  }
36395
36537
  return false;
36396
36538
  };
36397
- const FRAMEWORK_PACKAGES = {
36398
- next: "nextjs",
36399
- "@tanstack/react-start": "tanstack-start",
36400
- vite: "vite",
36401
- "react-scripts": "cra",
36402
- "@remix-run/react": "remix",
36403
- gatsby: "gatsby",
36404
- expo: "expo",
36405
- "react-native": "react-native"
36406
- };
36407
- const FRAMEWORK_DISPLAY_NAMES = {
36408
- nextjs: "Next.js",
36409
- "tanstack-start": "TanStack Start",
36410
- vite: "Vite",
36411
- cra: "Create React App",
36412
- remix: "Remix",
36413
- gatsby: "Gatsby",
36414
- expo: "Expo",
36415
- "react-native": "React Native",
36416
- preact: "Preact",
36417
- unknown: "React"
36418
- };
36419
- const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
36420
- const detectFramework = (dependencies) => {
36421
- for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
36422
- if (dependencies.preact && !dependencies.react) return "preact";
36423
- return "unknown";
36424
- };
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
+ });
36425
36543
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
36426
36544
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
36427
36545
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
@@ -36559,10 +36677,76 @@ const isConcreteDependencyVersion = (version) => {
36559
36677
  const normalizedVersion = normalizeDependencyVersion(version);
36560
36678
  return normalizedVersion !== null && /\d/.test(normalizedVersion);
36561
36679
  };
36562
- const isPlainObject = (value) => {
36563
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
36564
- const prototype = Object.getPrototypeOf(value);
36565
- 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;
36566
36750
  };
36567
36751
  const isCatalogReference = (version) => version.startsWith("catalog:");
36568
36752
  const extractCatalogName = (version) => {
@@ -36704,6 +36888,16 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
36704
36888
  }
36705
36889
  return null;
36706
36890
  };
36891
+ const REACT_SECTIONS = [
36892
+ "dependencies",
36893
+ "peerDependencies",
36894
+ "devDependencies"
36895
+ ];
36896
+ const TAILWIND_ZOD_SECTIONS = [
36897
+ "dependencies",
36898
+ "devDependencies",
36899
+ "peerDependencies"
36900
+ ];
36707
36901
  const EMPTY_DEPENDENCY_INFO = {
36708
36902
  reactVersion: null,
36709
36903
  tailwindVersion: null,
@@ -36744,6 +36938,10 @@ const extractDependencyInfo = (packageJson) => {
36744
36938
  framework: detectFramework(allDependencies)
36745
36939
  };
36746
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
+ };
36747
36945
  const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
36748
36946
  for (const section of sections) {
36749
36947
  const version = packageJson[section]?.[packageName];
@@ -36760,31 +36958,141 @@ const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
36760
36958
  version: null
36761
36959
  };
36762
36960
  };
36763
- const isDirectory = (directoryPath) => {
36764
- try {
36765
- return NFS.statSync(directoryPath).isDirectory();
36766
- } catch {
36767
- return false;
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));
36974
+ };
36975
+ const getPreactVersion = (packageJson) => {
36976
+ return {
36977
+ ...packageJson.peerDependencies,
36978
+ ...packageJson.dependencies,
36979
+ ...packageJson.devDependencies
36980
+ }.preact ?? null;
36981
+ };
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));
36994
+ };
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;
37005
+ };
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;
36768
37021
  }
36769
37022
  };
36770
- const NX_PROJECT_DISCOVERY_DIRS = [
36771
- "apps",
36772
- "libs",
36773
- "packages"
36774
- ];
36775
- const getNxWorkspaceDirectories = (rootDirectory) => {
36776
- if (!isFile(Path.join(rootDirectory, "nx.json"))) return [];
36777
- const collected = [];
36778
- for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
36779
- const candidatePath = Path.join(rootDirectory, candidate);
36780
- if (!isDirectory(candidatePath)) continue;
36781
- for (const entry of readDirectoryEntries(candidatePath)) {
36782
- if (!entry.isDirectory()) continue;
36783
- const projectDirectory = Path.join(candidatePath, entry.name);
36784
- if (isFile(Path.join(projectDirectory, "project.json")) || isFile(Path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
36785
- }
37023
+ /**
37024
+ * Walk up from `startDirectory` to the nearest ancestor that owns a
37025
+ * `package.json`, stopping at (and including) the enclosing project boundary
37026
+ * — the working tree's git root or a monorepo root. Lets a scan of a package
37027
+ * subfolder that has no `package.json` of its own adopt the nearest enclosing
37028
+ * package as its project root, so it inherits that package's dependency +
37029
+ * framework detection instead of synthesizing an empty (React-blind) project.
37030
+ *
37031
+ * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
37032
+ * most specific owning package (a leaf workspace, not just the repo root), so
37033
+ * a subfolder of one workspace no longer borrows a sibling's capabilities.
37034
+ *
37035
+ * Returns `null` when no ancestor `package.json` exists inside the project
37036
+ * boundary (a loose tree of files, or a subfolder of a package-less repo).
37037
+ */
37038
+ const findNearestAncestorPackageJson = (startDirectory) => {
37039
+ if (isProjectBoundary(startDirectory)) return null;
37040
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
37041
+ if (isFile(Path.join(directory, "package.json"))) return directory;
37042
+ if (isProjectBoundary(directory)) return null;
36786
37043
  }
36787
- return collected;
37044
+ return null;
37045
+ };
37046
+ const NAMES = new Set([
37047
+ "react-native",
37048
+ "react-native-tvos",
37049
+ ...new Set([
37050
+ "expo",
37051
+ "expo-router",
37052
+ "@expo/cli",
37053
+ "@expo/metro-config",
37054
+ "@expo/metro-runtime"
37055
+ ]),
37056
+ "react-native-windows",
37057
+ "react-native-macos"
37058
+ ]);
37059
+ const PREFIXES = ["@react-native/", "@react-native-"];
37060
+ const isReactNativeDependencyName = (dependencyName) => {
37061
+ if (NAMES.has(dependencyName)) return true;
37062
+ for (const prefix of PREFIXES) if (dependencyName.startsWith(prefix)) return true;
37063
+ return false;
37064
+ };
37065
+ const containsAnyReactNativeDependency = (section) => {
37066
+ if (!section) return false;
37067
+ for (const dependencyName of Object.keys(section)) if (isReactNativeDependencyName(dependencyName)) return true;
37068
+ return false;
37069
+ };
37070
+ const isPackageJsonReactNativeAware = (packageJson) => {
37071
+ if (typeof packageJson["react-native"] === "string") return true;
37072
+ if (containsAnyReactNativeDependency(packageJson.dependencies)) return true;
37073
+ if (containsAnyReactNativeDependency(packageJson.devDependencies)) return true;
37074
+ if (containsAnyReactNativeDependency(packageJson.peerDependencies)) return true;
37075
+ if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
37076
+ return false;
37077
+ };
37078
+ const REANIMATED_DEPENDENCY_NAME$1 = "react-native-reanimated";
37079
+ const isPackageJsonReanimatedAware = (packageJson) => {
37080
+ const allDependencies = {
37081
+ ...packageJson.peerDependencies,
37082
+ ...packageJson.dependencies,
37083
+ ...packageJson.devDependencies,
37084
+ ...packageJson.optionalDependencies
37085
+ };
37086
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME$1);
37087
+ };
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 [];
36788
37096
  };
36789
37097
  const parsePnpmWorkspacePatterns = (rootDirectory) => {
36790
37098
  const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
@@ -36803,18 +37111,24 @@ const parsePnpmWorkspacePatterns = (rootDirectory) => {
36803
37111
  }
36804
37112
  return patterns;
36805
37113
  };
36806
- const getWorkspacePatterns = (rootDirectory, packageJson) => {
36807
- const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
36808
- if (pnpmPatterns.length > 0) return pnpmPatterns;
36809
- if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
36810
- if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
36811
- const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
36812
- if (nxPatterns.length > 0) return nxPatterns;
36813
- return [];
36814
- };
36815
- const parseReactMajor = (reactVersion) => {
36816
- if (typeof reactVersion !== "string") return null;
36817
- return getLowestDependencyMajor(reactVersion);
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
+ }
37130
+ }
37131
+ return collected;
36818
37132
  };
36819
37133
  const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
36820
37134
  const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
@@ -36834,6 +37148,38 @@ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
36834
37148
  }
36835
37149
  return resolved;
36836
37150
  };
37151
+ const listWorkspacePackages = (rootDirectory) => {
37152
+ const packageJsonPath = Path.join(rootDirectory, "package.json");
37153
+ if (!isFile(packageJsonPath)) return [];
37154
+ const packageJson = readPackageJson$1(packageJsonPath);
37155
+ const patterns = getWorkspacePatterns(rootDirectory, packageJson);
37156
+ if (patterns.length === 0) return [];
37157
+ const packages = [];
37158
+ const seenDirectories = /* @__PURE__ */ new Set();
37159
+ const pushIfNew = (workspacePackage) => {
37160
+ if (seenDirectories.has(workspacePackage.directory)) return;
37161
+ seenDirectories.add(workspacePackage.directory);
37162
+ packages.push(workspacePackage);
37163
+ };
37164
+ if (hasReactDependency(packageJson)) pushIfNew({
37165
+ name: packageJson.name ?? Path.basename(rootDirectory),
37166
+ directory: rootDirectory
37167
+ });
37168
+ for (const pattern of patterns) {
37169
+ const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
37170
+ for (const workspaceDirectory of directories) {
37171
+ const workspacePackageJson = readPackageJson$1(Path.join(workspaceDirectory, "package.json"));
37172
+ if (!hasReactDependency(workspacePackageJson)) continue;
37173
+ pushIfNew({
37174
+ name: workspacePackageJson.name ?? Path.basename(workspaceDirectory),
37175
+ directory: workspaceDirectory
37176
+ });
37177
+ }
37178
+ }
37179
+ return packages;
37180
+ };
37181
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
37182
+ const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
36837
37183
  const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
36838
37184
  const dependencyDeclaration = getDependencyDeclaration({
36839
37185
  packageJson: workspacePackageJson,
@@ -36851,24 +37197,76 @@ const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
36851
37197
  if (nextMajor === null) return false;
36852
37198
  return nextMajor < currentMajor;
36853
37199
  };
36854
- const findReactInWorkspaces = (rootDirectory, packageJson) => {
36855
- const patterns = getWorkspacePatterns(rootDirectory, packageJson);
36856
- const result = { ...EMPTY_DEPENDENCY_INFO };
36857
- for (const pattern of patterns) {
36858
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
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();
36859
37256
  for (const workspaceDirectory of directories) {
37257
+ if (visitedDirectories.has(workspaceDirectory)) continue;
37258
+ visitedDirectories.add(workspaceDirectory);
36860
37259
  const workspacePackageJson = readPackageJson$1(Path.join(workspaceDirectory, "package.json"));
37260
+ evaluateManifestFacts(facts, workspacePackageJson, workspaceDirectory);
36861
37261
  const info = extractDependencyInfo(workspacePackageJson);
37262
+ if (info.framework !== "unknown" && frameworkMergeRank(info.framework) < frameworkMergeRank(facts.framework)) facts.framework = info.framework;
37263
+ if (isReactGroupSettled) continue;
36862
37264
  const reactVersion = resolveWorkspaceDependencyVersion({
36863
37265
  concreteVersion: info.reactVersion,
36864
37266
  packageName: "react",
36865
37267
  rootDirectory,
36866
- rootPackageJson: packageJson,
36867
- sections: [
36868
- "dependencies",
36869
- "peerDependencies",
36870
- "devDependencies"
36871
- ],
37268
+ rootPackageJson,
37269
+ sections: REACT_SECTIONS,
36872
37270
  workspaceDirectory,
36873
37271
  workspacePackageJson
36874
37272
  });
@@ -36876,12 +37274,8 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
36876
37274
  concreteVersion: info.tailwindVersion,
36877
37275
  packageName: "tailwindcss",
36878
37276
  rootDirectory,
36879
- rootPackageJson: packageJson,
36880
- sections: [
36881
- "dependencies",
36882
- "devDependencies",
36883
- "peerDependencies"
36884
- ],
37277
+ rootPackageJson,
37278
+ sections: TAILWIND_ZOD_SECTIONS,
36885
37279
  workspaceDirectory,
36886
37280
  workspacePackageJson
36887
37281
  });
@@ -36889,24 +37283,19 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
36889
37283
  concreteVersion: info.zodVersion,
36890
37284
  packageName: "zod",
36891
37285
  rootDirectory,
36892
- rootPackageJson: packageJson,
36893
- sections: [
36894
- "dependencies",
36895
- "devDependencies",
36896
- "peerDependencies"
36897
- ],
37286
+ rootPackageJson,
37287
+ sections: TAILWIND_ZOD_SECTIONS,
36898
37288
  workspaceDirectory,
36899
37289
  workspacePackageJson
36900
37290
  });
36901
- if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
36902
- if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
36903
- if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
36904
- if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
36905
- const resultReactMajor = parseReactMajor(result.reactVersion);
36906
- if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
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);
36907
37296
  }
36908
37297
  }
36909
- return result;
37298
+ return facts;
36910
37299
  };
36911
37300
  const findDependencyInfoFromMonorepoRoot = (directory) => {
36912
37301
  const monorepoRoot = findMonorepoRoot(directory);
@@ -36920,29 +37309,17 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
36920
37309
  const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
36921
37310
  packageJson: leafPackageJson,
36922
37311
  packageName: "react",
36923
- sections: [
36924
- "dependencies",
36925
- "peerDependencies",
36926
- "devDependencies"
36927
- ]
37312
+ sections: REACT_SECTIONS
36928
37313
  }) : null;
36929
37314
  const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
36930
37315
  packageJson: leafPackageJson,
36931
37316
  packageName: "tailwindcss",
36932
- sections: [
36933
- "dependencies",
36934
- "devDependencies",
36935
- "peerDependencies"
36936
- ]
37317
+ sections: TAILWIND_ZOD_SECTIONS
36937
37318
  }) : null;
36938
37319
  const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
36939
37320
  packageJson: leafPackageJson,
36940
37321
  packageName: "zod",
36941
- sections: [
36942
- "dependencies",
36943
- "devDependencies",
36944
- "peerDependencies"
36945
- ]
37322
+ sections: TAILWIND_ZOD_SECTIONS
36946
37323
  }) : null;
36947
37324
  const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
36948
37325
  const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
@@ -36950,38 +37327,15 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
36950
37327
  const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
36951
37328
  const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
36952
37329
  const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
36953
- const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
37330
+ const workspaceFacts = collectWorkspaceFacts(monorepoRoot, rootPackageJson, { collectReactGroup: true });
36954
37331
  return {
36955
- reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
36956
- tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
36957
- zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
36958
- framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
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
36959
37336
  };
36960
37337
  };
36961
37338
  /**
36962
- * Walk up from `startDirectory` to the nearest ancestor that owns a
36963
- * `package.json`, stopping at (and including) the enclosing project boundary
36964
- * — the working tree's git root or a monorepo root. Lets a scan of a package
36965
- * subfolder that has no `package.json` of its own adopt the nearest enclosing
36966
- * package as its project root, so it inherits that package's dependency +
36967
- * framework detection instead of synthesizing an empty (React-blind) project.
36968
- *
36969
- * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
36970
- * most specific owning package (a leaf workspace, not just the repo root), so
36971
- * a subfolder of one workspace no longer borrows a sibling's capabilities.
36972
- *
36973
- * Returns `null` when no ancestor `package.json` exists inside the project
36974
- * boundary (a loose tree of files, or a subfolder of a package-less repo).
36975
- */
36976
- const findNearestAncestorPackageJson = (startDirectory) => {
36977
- if (isProjectBoundary(startDirectory)) return null;
36978
- for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
36979
- if (isFile(Path.join(directory, "package.json"))) return directory;
36980
- if (isProjectBoundary(directory)) return null;
36981
- }
36982
- return null;
36983
- };
36984
- /**
36985
37339
  * Last-resort React detection: locate React the way Node's `node_modules`
36986
37340
  * resolution would from `directory` — check each `node_modules/react` up the
36987
37341
  * tree — but bounded to the enclosing repo so a globally installed React can't
@@ -37026,295 +37380,6 @@ const resolveInstalledReactVersion = (directory) => {
37026
37380
  }
37027
37381
  return null;
37028
37382
  };
37029
- const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
37030
- const rootValue = select(rootPackageJson);
37031
- if (rootValue !== null) return rootValue;
37032
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
37033
- if (patterns.length === 0) return null;
37034
- const visitedDirectories = /* @__PURE__ */ new Set();
37035
- for (const pattern of patterns) {
37036
- const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
37037
- for (const workspaceDirectory of directories) {
37038
- if (visitedDirectories.has(workspaceDirectory)) continue;
37039
- visitedDirectories.add(workspaceDirectory);
37040
- const value = select(readPackageJson$1(Path.join(workspaceDirectory, "package.json")));
37041
- if (value !== null) return value;
37042
- }
37043
- }
37044
- return null;
37045
- };
37046
- const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => predicate(packageJson) ? true : null) !== null;
37047
- const NAMES = new Set([
37048
- "react-native",
37049
- "react-native-tvos",
37050
- ...new Set([
37051
- "expo",
37052
- "expo-router",
37053
- "@expo/cli",
37054
- "@expo/metro-config",
37055
- "@expo/metro-runtime"
37056
- ]),
37057
- "react-native-windows",
37058
- "react-native-macos"
37059
- ]);
37060
- const PREFIXES = ["@react-native/", "@react-native-"];
37061
- const isReactNativeDependencyName = (dependencyName) => {
37062
- if (NAMES.has(dependencyName)) return true;
37063
- for (const prefix of PREFIXES) if (dependencyName.startsWith(prefix)) return true;
37064
- return false;
37065
- };
37066
- const containsAnyReactNativeDependency = (section) => {
37067
- if (!section) return false;
37068
- for (const dependencyName of Object.keys(section)) if (isReactNativeDependencyName(dependencyName)) return true;
37069
- return false;
37070
- };
37071
- const isPackageJsonReactNativeAware = (packageJson) => {
37072
- if (typeof packageJson["react-native"] === "string") return true;
37073
- if (containsAnyReactNativeDependency(packageJson.dependencies)) return true;
37074
- if (containsAnyReactNativeDependency(packageJson.devDependencies)) return true;
37075
- if (containsAnyReactNativeDependency(packageJson.peerDependencies)) return true;
37076
- if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
37077
- return false;
37078
- };
37079
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
37080
- const getDependencySpec = (packageJson, packageName) => {
37081
- const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
37082
- return typeof spec === "string" ? spec : null;
37083
- };
37084
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
37085
- const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
37086
- const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
37087
- const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
37088
- if (version === null || !isCatalogReference(version)) return version;
37089
- const catalogName = extractCatalogName(version);
37090
- const resolvedLocalVersion = resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, catalogName);
37091
- if (resolvedLocalVersion) return resolvedLocalVersion;
37092
- const monorepoRoot = findMonorepoRoot(rootDirectory);
37093
- if (!monorepoRoot) return version;
37094
- const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
37095
- if (!isFile(monorepoPackageJsonPath)) return version;
37096
- return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
37097
- };
37098
- const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
37099
- const getPreactVersion = (packageJson) => {
37100
- return {
37101
- ...packageJson.peerDependencies,
37102
- ...packageJson.dependencies,
37103
- ...packageJson.devDependencies
37104
- }.preact ?? null;
37105
- };
37106
- const TANSTACK_QUERY_PACKAGES = new Set([
37107
- "@tanstack/react-query",
37108
- "@tanstack/query-core",
37109
- "react-query"
37110
- ]);
37111
- const hasTanStackQuery = (packageJson) => {
37112
- const allDependencies = {
37113
- ...packageJson.peerDependencies,
37114
- ...packageJson.dependencies,
37115
- ...packageJson.devDependencies
37116
- };
37117
- return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
37118
- };
37119
- const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
37120
- const isPackageJsonReanimatedAware = (packageJson) => {
37121
- const allDependencies = {
37122
- ...packageJson.peerDependencies,
37123
- ...packageJson.dependencies,
37124
- ...packageJson.devDependencies,
37125
- ...packageJson.optionalDependencies
37126
- };
37127
- return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
37128
- };
37129
- const parseZodMajor = (zodVersion) => {
37130
- if (typeof zodVersion !== "string") return null;
37131
- return getLowestDependencyMajor(zodVersion);
37132
- };
37133
- const hasUpperBoundOnlyPeerRange = (range) => {
37134
- if (typeof range !== "string") return false;
37135
- const normalizedRange = normalizeDependencyVersion(range);
37136
- if (normalizedRange === null) return false;
37137
- return splitDependencyVersionBranches(normalizedRange).some((branch) => {
37138
- const normalizedBranch = normalizeDependencyVersion(branch);
37139
- return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
37140
- });
37141
- };
37142
- const peerRangeMinMajor = (range) => {
37143
- if (typeof range !== "string") return null;
37144
- return getLowestDependencyMajor(range);
37145
- };
37146
- const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
37147
- const installedReactMajor = parseReactMajor(reactVersion);
37148
- const peerReactRange = packageJson.peerDependencies?.react;
37149
- if (typeof peerReactRange !== "string") return installedReactMajor;
37150
- const peerFloor = peerRangeMinMajor(peerReactRange);
37151
- if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
37152
- return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
37153
- };
37154
- const TSCONFIG_FILENAME = "tsconfig.json";
37155
- const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
37156
- const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
37157
- const resolvePackageExtendsPath = (extendsValue, fromConfigDirectory) => {
37158
- const requireFromConfig = createRequire(Path.join(fromConfigDirectory, "tsconfig.json"));
37159
- const candidates = [
37160
- extendsValue,
37161
- ensureJsonExtension(extendsValue),
37162
- `${extendsValue.replace(/\/$/, "")}/tsconfig.json`
37163
- ];
37164
- for (const candidate of candidates) try {
37165
- return requireFromConfig.resolve(candidate);
37166
- } catch {
37167
- continue;
37168
- }
37169
- return null;
37170
- };
37171
- const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
37172
- if (isRelativeExtendsValue(extendsValue)) return ensureJsonExtension(Path.resolve(fromConfigDirectory, extendsValue));
37173
- return resolvePackageExtendsPath(extendsValue, fromConfigDirectory);
37174
- };
37175
- const normalizeCompilerOptions = (compilerOptions) => {
37176
- if (!isPlainObject(compilerOptions)) return { hasExplicitLib: false };
37177
- const target = typeof compilerOptions.target === "string" ? compilerOptions.target : void 0;
37178
- const hasExplicitLib = Object.hasOwn(compilerOptions, "lib");
37179
- return {
37180
- target,
37181
- lib: Array.isArray(compilerOptions.lib) ? compilerOptions.lib.filter((entry) => typeof entry === "string") : void 0,
37182
- hasExplicitLib
37183
- };
37184
- };
37185
- const normalizeReferencePaths = (references) => {
37186
- if (!Array.isArray(references)) return [];
37187
- return references.map((reference) => isPlainObject(reference) && typeof reference.path === "string" ? reference.path : null).filter((referencePath) => referencePath !== null);
37188
- };
37189
- const readTsConfig = (filePath) => {
37190
- let content;
37191
- try {
37192
- content = NFS.readFileSync(filePath, "utf-8");
37193
- } catch {
37194
- return null;
37195
- }
37196
- const parsed = ts.parseConfigFileTextToJson(filePath, content);
37197
- if (!isPlainObject(parsed.config)) return null;
37198
- return {
37199
- extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
37200
- referencePaths: normalizeReferencePaths(parsed.config.references),
37201
- compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
37202
- };
37203
- };
37204
- const mergeCompilerOptions = (inherited, current) => {
37205
- const target = current.target ?? inherited?.target;
37206
- const hasExplicitLib = current.hasExplicitLib || Boolean(inherited?.hasExplicitLib);
37207
- return {
37208
- target,
37209
- lib: current.hasExplicitLib ? current.lib : inherited?.lib,
37210
- hasExplicitLib
37211
- };
37212
- };
37213
- const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
37214
- const realPath = NFS.realpathSync.native(tsConfigPath);
37215
- if (visitedPaths.has(realPath)) return null;
37216
- const tsConfig = readTsConfig(realPath);
37217
- if (!tsConfig) return null;
37218
- const nextVisitedPaths = new Set(visitedPaths);
37219
- nextVisitedPaths.add(realPath);
37220
- if (tsConfig.extends && extendsDepth < 8) {
37221
- const parentPath = resolveExtendsPath(tsConfig.extends, Path.dirname(realPath));
37222
- if (parentPath && isFile(parentPath)) return mergeCompilerOptions(readResolvedCompilerOptions(parentPath, extendsDepth + 1, nextVisitedPaths), tsConfig.compilerOptions);
37223
- }
37224
- return tsConfig.compilerOptions;
37225
- };
37226
- const targetYearIsPreES2023 = (target) => {
37227
- const year = ES_TARGET_YEAR_BY_NAME[target.toLowerCase()];
37228
- return year !== void 0 && year < 2023;
37229
- };
37230
- const libEntryIncludesES2023Array = (entry) => {
37231
- const normalizedEntry = entry.toLowerCase();
37232
- if (normalizedEntry === "esnext" || normalizedEntry === "esnext.array") return true;
37233
- const esYearMatch = /^es(\d{4})(?:\.(.+))?$/.exec(normalizedEntry);
37234
- if (!esYearMatch) return false;
37235
- if (Number(esYearMatch[1]) < 2023) return false;
37236
- const component = esYearMatch[2];
37237
- return component === void 0 || component === "array";
37238
- };
37239
- const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
37240
- const compilerOptionsArePreES2023 = (compilerOptions) => {
37241
- if (compilerOptions.target && targetYearIsPreES2023(compilerOptions.target)) return true;
37242
- if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
37243
- return false;
37244
- };
37245
- const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
37246
- const detectPreES2023FromConfig = (tsConfigPath, visitedConfigPaths = /* @__PURE__ */ new Set()) => {
37247
- if (visitedConfigPaths.has(tsConfigPath)) return false;
37248
- const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
37249
- if (!compilerOptions) return false;
37250
- if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) {
37251
- const tsConfig = readTsConfig(tsConfigPath);
37252
- if (!tsConfig) return false;
37253
- const nextVisitedConfigPaths = new Set(visitedConfigPaths);
37254
- nextVisitedConfigPaths.add(tsConfigPath);
37255
- const configDirectory = Path.dirname(tsConfigPath);
37256
- return tsConfig.referencePaths.some((referencePath) => {
37257
- const resolvedReferencePath = Path.resolve(configDirectory, referencePath);
37258
- const referencedConfigPath = isFile(resolvedReferencePath) ? resolvedReferencePath : Path.join(resolvedReferencePath, TSCONFIG_FILENAME);
37259
- return isFile(referencedConfigPath) && detectPreES2023FromConfig(referencedConfigPath, nextVisitedConfigPaths);
37260
- });
37261
- }
37262
- return compilerOptionsArePreES2023(compilerOptions);
37263
- };
37264
- const FALLBACK_TSCONFIG_FILENAMES = ["tsconfig.app.json", "tsconfig.build.json"];
37265
- const detectPreES2023Target = (directory) => {
37266
- const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
37267
- if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
37268
- for (const fallbackFilename of FALLBACK_TSCONFIG_FILENAMES) {
37269
- const fallbackPath = Path.join(directory, fallbackFilename);
37270
- if (isFile(fallbackPath)) return detectPreES2023FromConfig(fallbackPath);
37271
- }
37272
- return false;
37273
- };
37274
- const REACT_DEPENDENCY_NAMES = new Set([
37275
- "react",
37276
- "react-native",
37277
- "next",
37278
- "preact"
37279
- ]);
37280
- const hasReactDependency = (packageJson) => {
37281
- const allDependencies = {
37282
- ...packageJson.peerDependencies,
37283
- ...packageJson.dependencies,
37284
- ...packageJson.devDependencies
37285
- };
37286
- return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
37287
- };
37288
- const listWorkspacePackages = (rootDirectory) => {
37289
- const packageJsonPath = Path.join(rootDirectory, "package.json");
37290
- if (!isFile(packageJsonPath)) return [];
37291
- const packageJson = readPackageJson$1(packageJsonPath);
37292
- const patterns = getWorkspacePatterns(rootDirectory, packageJson);
37293
- if (patterns.length === 0) return [];
37294
- const packages = [];
37295
- const seenDirectories = /* @__PURE__ */ new Set();
37296
- const pushIfNew = (workspacePackage) => {
37297
- if (seenDirectories.has(workspacePackage.directory)) return;
37298
- seenDirectories.add(workspacePackage.directory);
37299
- packages.push(workspacePackage);
37300
- };
37301
- if (hasReactDependency(packageJson)) pushIfNew({
37302
- name: packageJson.name ?? Path.basename(rootDirectory),
37303
- directory: rootDirectory
37304
- });
37305
- for (const pattern of patterns) {
37306
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
37307
- for (const workspaceDirectory of directories) {
37308
- const workspacePackageJson = readPackageJson$1(Path.join(workspaceDirectory, "package.json"));
37309
- if (!hasReactDependency(workspacePackageJson)) continue;
37310
- pushIfNew({
37311
- name: workspacePackageJson.name ?? Path.basename(workspaceDirectory),
37312
- directory: workspaceDirectory
37313
- });
37314
- }
37315
- }
37316
- return packages;
37317
- };
37318
37383
  const toReactWorkspacePackages = (directories) => {
37319
37384
  const packages = [];
37320
37385
  for (const directory of directories) {
@@ -37384,14 +37449,13 @@ const discoverReactSubprojects = (rootDirectory) => {
37384
37449
  const cachedProjectInfos = /* @__PURE__ */ new Map();
37385
37450
  /**
37386
37451
  * Build a `ProjectInfo` for a directory that has no `package.json` of
37387
- * its own — a package subfolder like `repo/packages` or `app/src/features`,
37388
- * or any loose tree of TypeScript/JavaScript files. Dependency + framework
37389
- * detection is inherited from the nearest enclosing package (a leaf
37390
- * workspace, a plain app root, or a monorepo root — whichever is closest,
37391
- * bounded by the git root), so scanning a subdirectory of a React project
37392
- * still gets the React capabilities; a standalone non-React directory simply
37393
- * scans with the framework-agnostic rules. Throws only when the directory has
37394
- * 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).
37395
37459
  */
37396
37460
  const discoverProjectWithoutPackageJson = (directory) => {
37397
37461
  const sourceFileCount = countSourceFiles(directory);
@@ -37429,6 +37493,7 @@ const discoverProjectWithoutPackageJson = (directory) => {
37429
37493
  hasReanimated: false,
37430
37494
  reanimatedVersion: null,
37431
37495
  isPreES2023Target: hasOwnTsConfig && detectPreES2023Target(directory),
37496
+ isStaticExport: false,
37432
37497
  sourceFileCount
37433
37498
  };
37434
37499
  };
@@ -37442,90 +37507,85 @@ const discoverProject = (directory) => {
37442
37507
  return synthesized;
37443
37508
  }
37444
37509
  const packageJson = readPackageJson$1(packageJsonPath);
37445
- let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
37446
- const reactDeclaration = getDependencyDeclaration({
37447
- packageJson,
37448
- packageName: "react",
37449
- sections: [
37450
- "dependencies",
37451
- "peerDependencies",
37452
- "devDependencies"
37453
- ]
37454
- });
37455
- const tailwindDeclaration = getDependencyDeclaration({
37456
- packageJson,
37457
- packageName: "tailwindcss",
37458
- sections: [
37459
- "dependencies",
37460
- "devDependencies",
37461
- "peerDependencies"
37462
- ]
37463
- });
37464
- 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({
37465
37527
  packageJson,
37466
- packageName: "zod",
37467
- sections: [
37468
- "dependencies",
37469
- "devDependencies",
37470
- "peerDependencies"
37471
- ]
37472
- });
37473
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
37474
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
37475
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
37476
- 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) {
37477
37536
  const monorepoRoot = findMonorepoRoot(directory);
37478
37537
  if (monorepoRoot) {
37479
37538
  const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
37480
- if (isFile(monorepoPackageJsonPath)) {
37481
- const rootPackageJson = readPackageJson$1(monorepoPackageJsonPath);
37482
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
37483
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
37484
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
37485
- }
37539
+ if (isFile(monorepoPackageJsonPath)) fillFromCatalogs(readPackageJson$1(monorepoPackageJsonPath), monorepoRoot);
37486
37540
  }
37487
37541
  }
37488
- if (!reactVersion || framework === "unknown") {
37489
- const workspaceInfo = findReactInWorkspaces(directory, packageJson);
37490
- if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
37491
- if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
37492
- if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
37493
- 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;
37494
37549
  }
37495
- if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
37550
+ if ((!tracked.react.version || framework === "unknown") && !isMonorepoRoot(directory)) {
37496
37551
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
37497
- if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
37498
- if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
37499
- if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
37552
+ tracked.react.version ||= monorepoInfo.reactVersion;
37553
+ tracked.tailwindcss.version ||= monorepoInfo.tailwindVersion;
37554
+ tracked.zod.version ||= monorepoInfo.zodVersion;
37500
37555
  if (framework === "unknown") framework = monorepoInfo.framework;
37501
37556
  }
37502
- if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
37503
- if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
37504
- 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;
37505
37563
  if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
37564
+ const tailwindVersion = tailwindcss.version;
37565
+ const zodVersion = zod.version;
37506
37566
  const projectName = packageJson.name ?? Path.basename(directory);
37507
37567
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
37508
37568
  const sourceFileCount = countSourceFiles(directory);
37509
- const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
37569
+ const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
37510
37570
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
37511
37571
  rootDirectory: directory,
37512
37572
  rootPackageJson: packageJson,
37513
37573
  packageName: "expo",
37514
- version: findExpoVersion(directory, packageJson)
37574
+ version: workspaceFacts.expo.version
37515
37575
  }) : null;
37516
37576
  const shopifyFlashListVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
37517
37577
  rootDirectory: directory,
37518
37578
  rootPackageJson: packageJson,
37519
37579
  packageName: SHOPIFY_FLASH_LIST_PACKAGE_NAME,
37520
- version: findShopifyFlashListVersion(directory, packageJson)
37580
+ version: workspaceFacts.shopifyFlashList.version
37521
37581
  }) : null;
37522
- const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
37523
- const reanimatedVersion = hasReanimated ? findInWorkspacePackageJsons(directory, packageJson, (workspacePackageJson) => getDependencySpec(workspacePackageJson, REANIMATED_DEPENDENCY_NAME)) : null;
37582
+ const hasReanimated = hasReactNativeWorkspace && workspaceFacts.hasReanimatedAwarePackage;
37583
+ const reanimatedVersion = hasReanimated ? workspaceFacts.reanimatedVersion : null;
37524
37584
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
37525
37585
  rootDirectory: directory,
37526
37586
  rootPackageJson: packageJson,
37527
37587
  packageName: "next",
37528
- version: findNextjsVersion(directory, packageJson)
37588
+ version: workspaceFacts.next.version
37529
37589
  }) : null;
37530
37590
  const preactVersion = getPreactVersion(packageJson);
37531
37591
  const isPreES2023Target = hasTypeScript && detectPreES2023Target(directory);
@@ -37536,7 +37596,7 @@ const discoverProject = (directory) => {
37536
37596
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
37537
37597
  tailwindVersion,
37538
37598
  zodVersion,
37539
- zodMajorVersion: parseZodMajor(zodVersion),
37599
+ zodMajorVersion: zodVersion === null ? null : getLowestDependencyMajor(zodVersion),
37540
37600
  framework,
37541
37601
  hasTypeScript,
37542
37602
  hasReactCompiler: detectReactCompiler(directory, packageJson),
@@ -37552,6 +37612,7 @@ const discoverProject = (directory) => {
37552
37612
  hasReanimated,
37553
37613
  reanimatedVersion,
37554
37614
  isPreES2023Target,
37615
+ isStaticExport: framework === "nextjs" && detectNextjsStaticExport(workspaceFacts.next.sourceDirectory ?? directory),
37555
37616
  sourceFileCount
37556
37617
  };
37557
37618
  cachedProjectInfos.set(directory, projectInfo);
@@ -37567,57 +37628,6 @@ const discoverProject = (directory) => {
37567
37628
  * single source file has nothing to analyze.
37568
37629
  */
37569
37630
  const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null || project.sourceFileCount > 0;
37570
- const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
37571
- const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
37572
- const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
37573
- const parseReactMajorMinor = (reactVersion) => {
37574
- if (typeof reactVersion !== "string") return null;
37575
- const trimmed = reactVersion.trim();
37576
- if (trimmed.length === 0) return null;
37577
- const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
37578
- if (lowerBoundsOnly.length === 0) return null;
37579
- const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
37580
- if (majorMinorMatch) {
37581
- const major = Number.parseInt(majorMinorMatch[1], 10);
37582
- const minor = Number.parseInt(majorMinorMatch[2], 10);
37583
- if (!Number.isFinite(major) || major <= 0) return null;
37584
- if (!Number.isFinite(minor) || minor < 0) return null;
37585
- return {
37586
- major,
37587
- minor
37588
- };
37589
- }
37590
- const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
37591
- if (!majorOnlyMatch) return null;
37592
- const major = Number.parseInt(majorOnlyMatch[1], 10);
37593
- if (!Number.isFinite(major) || major <= 0) return null;
37594
- return {
37595
- major,
37596
- minor: 0
37597
- };
37598
- };
37599
- const isReactAtLeast = (detected, required) => {
37600
- if (detected === null) return true;
37601
- if (detected.major !== required.major) return detected.major > required.major;
37602
- return detected.minor >= required.minor;
37603
- };
37604
- const parseLowerBoundVersion = (versionSpec) => import_semver.validRange(versionSpec) !== null ? import_semver.minVersion(versionSpec) : import_semver.coerce(versionSpec);
37605
- const parseTailwindMajorMinor = (tailwindVersion) => {
37606
- if (typeof tailwindVersion !== "string") return null;
37607
- const trimmed = tailwindVersion.trim();
37608
- if (trimmed.length === 0) return null;
37609
- const lowerBound = parseLowerBoundVersion(trimmed);
37610
- if (lowerBound === null || lowerBound.major <= 0) return null;
37611
- return {
37612
- major: lowerBound.major,
37613
- minor: lowerBound.minor
37614
- };
37615
- };
37616
- const isTailwindAtLeast = (detected, required) => {
37617
- if (detected === null) return true;
37618
- if (detected.major !== required.major) return detected.major > required.major;
37619
- return detected.minor >= required.minor;
37620
- };
37621
37631
  const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
37622
37632
  var InvalidGlobPatternError = class extends Error {
37623
37633
  pattern;
@@ -40428,64 +40438,64 @@ function* collectSecurityScanFiles(rootDirectory) {
40428
40438
  }
40429
40439
  }
40430
40440
  }
40431
- /**
40432
- * Whether project discovery resolved a React-compatible runtime (React or
40433
- * Preact, which ships the same hooks + JSX model). This is the same
40434
- * predicate that grants the `react` capability, so `false` means every
40435
- * React-runtime rule family was gated off for the project.
40436
- */
40437
- 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
+ };
40438
40453
  const buildCapabilities = (project) => {
40439
40454
  const capabilities = /* @__PURE__ */ new Set();
40440
40455
  capabilities.add(project.framework);
40441
- if (hasReactRuntime(project)) capabilities.add("react");
40456
+ if (project.reactVersion !== null || project.preactVersion !== null) capabilities.add("react");
40442
40457
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
40443
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");
40444
40462
  if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
40445
- const reactMajor = project.reactMajorVersion;
40446
- if (reactMajor !== null) {
40447
- const cappedReactMajor = Math.min(reactMajor, 30);
40448
- for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
40449
- if (reactMajor >= 19) {
40450
- if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
40451
- major: 19,
40452
- minor: 2
40453
- })) capabilities.add("react:19.2");
40454
- }
40455
- }
40456
- if (project.tailwindVersion !== null) {
40457
- capabilities.add("tailwind");
40458
- if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
40459
- major: 3,
40460
- minor: 4
40461
- })) capabilities.add("tailwind:3.4");
40462
- }
40463
- if (project.zodVersion !== null) {
40464
- capabilities.add("zod");
40465
- if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
40466
- }
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");
40467
40475
  if (project.isPreES2023Target) capabilities.add("pre-es2023");
40468
40476
  if (project.hasReactCompiler) capabilities.add("react-compiler");
40469
40477
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
40470
40478
  if (project.hasTypeScript) capabilities.add("typescript");
40471
- if (project.preactVersion !== null) {
40472
- capabilities.add("preact");
40473
- const preactMajor = project.preactMajorVersion;
40474
- if (preactMajor !== null) {
40475
- const cappedPreactMajor = Math.min(preactMajor, 20);
40476
- for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
40477
- }
40478
- if (project.reactVersion === null) capabilities.add("pure-preact");
40479
- }
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");
40480
40482
  return capabilities;
40481
40483
  };
40482
- const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
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);
40490
+ return capabilities;
40491
+ };
40492
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhen) => {
40483
40493
  if (requires) {
40484
40494
  for (const capability of requires) if (!capabilities.has(capability)) return false;
40485
40495
  }
40486
40496
  if (tags?.includes("react-jsx-only") && !capabilities.has("react")) return false;
40487
- if (disabledBy) {
40488
- 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;
40489
40499
  }
40490
40500
  if (tags) {
40491
40501
  for (const tag of tags) if (ignoredTags.has(tag)) return false;
@@ -40496,14 +40506,14 @@ const yieldToEventLoop = () => new Promise((resolve) => {
40496
40506
  setImmediate(resolve);
40497
40507
  });
40498
40508
  const createSecurityScanSession = (rootDirectory, options) => {
40499
- const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
40509
+ const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
40500
40510
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
40501
40511
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
40502
40512
  const rule = entry.rule;
40503
40513
  const scan = rule.scan;
40504
40514
  if (typeof scan !== "function") return [];
40505
40515
  if (rule.defaultEnabled === false) return [];
40506
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) return [];
40516
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) return [];
40507
40517
  return [{
40508
40518
  entry,
40509
40519
  scan,
@@ -40523,6 +40533,7 @@ const createSecurityScanSession = (rootDirectory, options) => {
40523
40533
  return status === true;
40524
40534
  };
40525
40535
  const scanFileByRule = function* (file) {
40536
+ if (file === null) return;
40526
40537
  for (const { entry, scan, committedFilesOnly } of enabledScanRules) {
40527
40538
  for (const finding of scan(file)) {
40528
40539
  if (committedFilesOnly && isFileGitIgnored(file)) continue;
@@ -42419,7 +42430,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42419
42430
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
42420
42431
  const jsPlugins = [];
42421
42432
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
42422
- const capabilities = buildCapabilities(project);
42433
+ const capabilities = getCapabilities(project);
42423
42434
  const enabledReactDoctorRules = {};
42424
42435
  for (const registryEntry of REACT_DOCTOR_RULES) {
42425
42436
  const rule = reactDoctorPlugin.rules[registryEntry.id];
@@ -42430,7 +42441,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42430
42441
  if (rule.scan !== void 0) continue;
42431
42442
  if (customRulesOnly && registryEntry.originallyExternal) continue;
42432
42443
  if (rule.framework !== "global" && !rule.requires) continue;
42433
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
42444
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) continue;
42434
42445
  const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
42435
42446
  if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
42436
42447
  const severity = resolveRuleSeverityOverride({
@@ -42461,6 +42472,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42461
42472
  settings: { "react-doctor": {
42462
42473
  framework: project.framework,
42463
42474
  rootDirectory: resolveSettingsRootDirectory(project.rootDirectory),
42475
+ capabilities: [...capabilities].sort(),
42464
42476
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
42465
42477
  ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
42466
42478
  } },
@@ -42751,21 +42763,6 @@ const mapWithConcurrency = async (items, concurrency, task) => {
42751
42763
  if (errors.length > 0) throw errors[0];
42752
42764
  return results;
42753
42765
  };
42754
- const getPublicEnvPrefix = (framework) => {
42755
- switch (framework) {
42756
- case "nextjs": return "NEXT_PUBLIC_*";
42757
- case "vite":
42758
- case "tanstack-start": return "VITE_*";
42759
- case "cra": return "REACT_APP_*";
42760
- case "gatsby": return "GATSBY_*";
42761
- default: return null;
42762
- }
42763
- };
42764
- const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
42765
- const publicEnvPrefix = getPublicEnvPrefix(project.framework);
42766
- if (!publicEnvPrefix) return fallbackRecommendation;
42767
- 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`;
42768
- };
42769
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";
42770
42767
  const ACCESSORS_MIN_VERSION = "3.15.0";
42771
42768
  const supportsAccessors = (reanimatedVersion) => {
@@ -43446,8 +43443,14 @@ const PLUGIN_CATEGORY_MAP = {
43446
43443
  };
43447
43444
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
43448
43445
  const getRuleRecommendation = (ruleName, project) => {
43449
- if (ruleName === "no-secrets-in-client-code") return buildNoSecretsRecommendation(project, reactDoctorPlugin.rules["no-secrets-in-client-code"]?.recommendation ?? "Move secrets to server-only code");
43450
- 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;
43451
43454
  };
43452
43455
  const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
43453
43456
  const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
@@ -43601,7 +43604,9 @@ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
43601
43604
  * - `OxlintBatchExceeded { kind: "output-too-large" }` — stdout+stderr
43602
43605
  * crossed `OXLINT_OUTPUT_MAX_BYTES`.
43603
43606
  * - `OxlintBatchExceeded { kind: "oom" | "killed" }` — child exited
43604
- * 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).
43605
43610
  * - `OxlintSpawnFailed { cause }` — `spawn` itself errored, or the
43606
43611
  * child exited successfully but printed only stderr.
43607
43612
  *
@@ -43667,7 +43672,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
43667
43672
  clearAbortListener();
43668
43673
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
43669
43674
  });
43670
- child.on("close", (_code, signal) => {
43675
+ child.on("close", (code, signal) => {
43671
43676
  clearTimeout(timeoutHandle);
43672
43677
  clearAbortListener();
43673
43678
  if (didKillForSize) {
@@ -43677,10 +43682,11 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
43677
43682
  }) }));
43678
43683
  return;
43679
43684
  }
43680
- if (signal) {
43685
+ const isAbortExitCode = code !== null && ABORT_EXIT_CODES.has(code);
43686
+ if (signal || isAbortExitCode) {
43681
43687
  const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
43682
- const isOom = signal === "SIGABRT";
43683
- const detailParts = [`killed by ${signal}`];
43688
+ const isOom = signal === "SIGABRT" || isAbortExitCode;
43689
+ const detailParts = [signal ? `killed by ${signal}` : `aborted with exit code ${code}`];
43684
43690
  if (isOom) detailParts.push("try scanning fewer files with --diff");
43685
43691
  if (stderrOutput) detailParts.push(stderrOutput);
43686
43692
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
@@ -43723,12 +43729,19 @@ const isParallelismRelatedSpawnError = (error) => {
43723
43729
  * (surfaced via `onPartialFailure`) so JSON-mode consumers see WHICH
43724
43730
  * files were skipped instead of silently losing them.
43725
43731
  *
43726
- * Parallel runs (concurrency > 1) get one extra safety net: if the pass
43727
- * fails with a resource-exhaustion error that's exclusive to running
43728
- * many oxlint subprocesses at once (EAGAIN / EMFILE / ENFILE / ENOMEM —
43729
- * see `isParallelismRelatedSpawnError`), the whole pass replays once
43730
- * with a single worker. That's the only failure a serial replay can
43731
- * 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.
43732
43745
  *
43733
43746
  * Errors that aren't splittable and aren't parallelism-related (oxlint
43734
43747
  * config crash, JS plugin resolution failure, etc.) propagate to the
@@ -43738,13 +43751,16 @@ const isParallelismRelatedSpawnError = (error) => {
43738
43751
  const spawnLintBatches = async (input) => {
43739
43752
  const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
43740
43753
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
43741
- const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
43742
- 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);
43743
43758
  const allDiagnostics = [];
43744
43759
  const droppedFiles = [];
43760
+ const oomDroppedFiles = [];
43745
43761
  const deadlineSkippedFiles = [];
43746
- const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0;
43747
43762
  let firstDropReason = null;
43763
+ let firstNonOomDropReason = null;
43748
43764
  const spawnLintBatch = async (batch, depth, batchState) => {
43749
43765
  if (isPastDeadline()) {
43750
43766
  deadlineSkippedFiles.push(...batch);
@@ -43766,12 +43782,14 @@ const spawnLintBatches = async (input) => {
43766
43782
  const isDepthCapReached = depth >= splitMaxDepth;
43767
43783
  if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
43768
43784
  droppedFiles.push(...batch);
43769
- if (firstDropReason === null) {
43770
- let limitHint = "";
43771
- if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
43772
- else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
43773
- firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
43774
- }
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;
43775
43793
  return [];
43776
43794
  }
43777
43795
  const splitIndex = Math.ceil(batch.length / 2);
@@ -43781,16 +43799,16 @@ const spawnLintBatches = async (input) => {
43781
43799
  let startedFileCount = 0;
43782
43800
  let scannedFileCount = 0;
43783
43801
  let displayedFileCount = 0;
43784
- const progressTimer = onFileProgress && totalFileCount > 1 ? setInterval(() => {
43802
+ const progressTimer = passOnFileProgress && totalFileCount > 1 ? setInterval(() => {
43785
43803
  const ceiling = Math.min(startedFileCount, totalFileCount - 1);
43786
43804
  if (displayedFileCount < ceiling) {
43787
43805
  displayedFileCount += 1;
43788
- onFileProgress(displayedFileCount, totalFileCount);
43806
+ passOnFileProgress(displayedFileCount, totalFileCount);
43789
43807
  }
43790
43808
  }, 50) : null;
43791
43809
  progressTimer?.unref?.();
43792
43810
  try {
43793
- const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
43811
+ const batchResults = await mapWithConcurrency(passBatches, concurrency, async (batch) => {
43794
43812
  if (isPastDeadline()) {
43795
43813
  deadlineSkippedFiles.push(...batch);
43796
43814
  return [];
@@ -43802,9 +43820,9 @@ const spawnLintBatches = async (input) => {
43802
43820
  };
43803
43821
  const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
43804
43822
  scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
43805
- if (onFileProgress) {
43823
+ if (passOnFileProgress) {
43806
43824
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
43807
- onFileProgress(displayedFileCount, totalFileCount);
43825
+ passOnFileProgress(displayedFileCount, totalFileCount);
43808
43826
  }
43809
43827
  return batchDiagnostics;
43810
43828
  });
@@ -43812,22 +43830,53 @@ const spawnLintBatches = async (input) => {
43812
43830
  } finally {
43813
43831
  if (progressTimer !== null) clearInterval(progressTimer);
43814
43832
  }
43815
- const reportSkippedFiles = (skippedFiles, buildMessage) => {
43816
- if (skippedFiles.length === 0 || !onPartialFailure) return;
43817
- 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
43818
43840
  };
43819
- const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
43820
- reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
43821
- reportSkippedFiles(deadlineSkippedFiles, (fileListPreview) => `${deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
43822
- return allDiagnostics;
43823
43841
  };
43824
- let diagnostics;
43842
+ let outcome;
43843
+ let effectiveConcurrency = requestedConcurrency;
43825
43844
  try {
43826
- diagnostics = await runBatchPass(requestedConcurrency);
43845
+ outcome = await runBatchPass(requestedConcurrency, fileBatches, onFileProgress);
43827
43846
  } catch (error) {
43828
43847
  if (requestedConcurrency <= 1 || !isParallelismRelatedSpawnError(error)) throw error;
43829
- 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;
43830
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})`);
43831
43880
  return dedupeDiagnostics(diagnostics);
43832
43881
  };
43833
43882
  let didValidate = false;
@@ -45796,6 +45845,13 @@ const summarizeDiagnostics = (diagnostics, worstScore = null, worstScoreLabel =
45796
45845
  scoreLabel: worstScoreLabel
45797
45846
  };
45798
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;
45799
45855
  const toJsonDiff = (diff) => {
45800
45856
  if (!diff) return null;
45801
45857
  return {
@@ -46378,7 +46434,7 @@ const makeNoopConsole = () => ({
46378
46434
  });
46379
46435
  //#endregion
46380
46436
  //#region src/cli/utils/version.ts
46381
- const VERSION = "0.7.2";
46437
+ const VERSION = "0.7.3-dev.6b70b32";
46382
46438
  //#endregion
46383
46439
  //#region src/cli/utils/json-mode.ts
46384
46440
  let context = null;
@@ -46747,13 +46803,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
46747
46803
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
46748
46804
  * standard `SENTRY_RELEASE` override.
46749
46805
  */
46750
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.2`;
46806
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.3-dev.6b70b32`;
46751
46807
  /**
46752
46808
  * Deployment environment shown in Sentry's environment filter. Defaults to
46753
46809
  * `production` for tagged releases and `development` for dev/unbuilt versions,
46754
46810
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
46755
46811
  */
46756
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.2") ? "development" : "production");
46812
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.3-dev.6b70b32") ? "development" : "production");
46757
46813
  /**
46758
46814
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
46759
46815
  * (set to `0` to disable tracing) and falls back to
@@ -64928,4 +64984,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
64928
64984
  export {};
64929
64985
 
64930
64986
  //# sourceMappingURL=cli.js.map
64931
- //# debugId=7d3c9a4a-cf80-5529-97be-47427cc1e986
64987
+ //# debugId=d6c8e327-6382-5aed-99c8-f3e537c25f51