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/lsp.js CHANGED
@@ -32827,6 +32827,25 @@ var AmbiguousProjectError = class extends Error {
32827
32827
  }
32828
32828
  };
32829
32829
  const isProjectDiscoveryError = (value) => value instanceof ProjectNotFoundError || value instanceof NoReactDependencyError || value instanceof PackageJsonNotFoundError || value instanceof NotADirectoryError || value instanceof AmbiguousProjectError;
32830
+ const isErrnoException = (error) => error instanceof Error && "code" in error;
32831
+ const IGNORABLE_READDIR_ERROR_CODES = new Set([
32832
+ "EACCES",
32833
+ "EPERM",
32834
+ "ENOENT",
32835
+ "ENOTDIR",
32836
+ "EINVAL",
32837
+ "ELOOP",
32838
+ "ENAMETOOLONG"
32839
+ ]);
32840
+ const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
32841
+ const readDirectoryEntries = (directoryPath) => {
32842
+ try {
32843
+ return NFS.readdirSync(directoryPath, { withFileTypes: true });
32844
+ } catch (error) {
32845
+ if (isIgnorableReaddirError(error)) return [];
32846
+ throw error;
32847
+ }
32848
+ };
32830
32849
  const isFile = (filePath) => {
32831
32850
  try {
32832
32851
  return NFS.statSync(filePath).isFile();
@@ -32834,6 +32853,18 @@ const isFile = (filePath) => {
32834
32853
  return false;
32835
32854
  }
32836
32855
  };
32856
+ const isDirectory = (directoryPath) => {
32857
+ try {
32858
+ return NFS.statSync(directoryPath).isDirectory();
32859
+ } catch {
32860
+ return false;
32861
+ }
32862
+ };
32863
+ const isPlainObject = (value) => {
32864
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
32865
+ const prototype = Object.getPrototypeOf(value);
32866
+ return prototype === null || prototype === Object.prototype;
32867
+ };
32837
32868
  const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?|mts|mjs)$/;
32838
32869
  const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
32839
32870
  const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
@@ -32958,6 +32989,8 @@ const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
32958
32989
  const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
32959
32990
  const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
32960
32991
  const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
32992
+ const ABORT_EXIT_CODES = new Set([134, 3221226505]);
32993
+ const OXLINT_OOM_RESCUE_BUDGET_MS = 6e4;
32961
32994
  const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
32962
32995
  const LINT_PHASE_TIMEOUT_MS = 3e5;
32963
32996
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
@@ -33004,7 +33037,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33004
33037
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33005
33038
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33006
33039
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
33007
- const CORE_PACKAGE_VERSION = "0.7.2";
33040
+ const CORE_PACKAGE_VERSION = "0.7.3";
33008
33041
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
33009
33042
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
33010
33043
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -33053,25 +33086,6 @@ const isLargeMinifiedFile = (absolutePath, knownSizeBytes) => {
33053
33086
  cachedIsLargeMinifiedByPath.set(absolutePath, result);
33054
33087
  return result;
33055
33088
  };
33056
- const isErrnoException = (error) => error instanceof Error && "code" in error;
33057
- const IGNORABLE_READDIR_ERROR_CODES = new Set([
33058
- "EACCES",
33059
- "EPERM",
33060
- "ENOENT",
33061
- "ENOTDIR",
33062
- "EINVAL",
33063
- "ELOOP",
33064
- "ENAMETOOLONG"
33065
- ]);
33066
- const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
33067
- const readDirectoryEntries = (directoryPath) => {
33068
- try {
33069
- return NFS.readdirSync(directoryPath, { withFileTypes: true });
33070
- } catch (error) {
33071
- if (isIgnorableReaddirError(error)) return [];
33072
- throw error;
33073
- }
33074
- };
33075
33089
  function* walkSourceTreeFiles(rootDirectory) {
33076
33090
  const stack = [rootDirectory];
33077
33091
  while (stack.length > 0) {
@@ -33126,23 +33140,6 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33126
33140
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33127
33141
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33128
33142
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33129
- /**
33130
- * The chain of ancestor directories of `startDirectory`, nearest first, up to
33131
- * and including the filesystem root. With `includeStart`, the chain begins at
33132
- * `startDirectory` itself; otherwise at its parent. The one place the "walk up
33133
- * the tree" traversal is expressed, so each caller adds only its own match /
33134
- * stop policy instead of re-deriving the `path.dirname` loop.
33135
- */
33136
- const ancestorDirectories = (startDirectory, { includeStart }) => {
33137
- const directories = [];
33138
- let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
33139
- while (true) {
33140
- directories.push(currentDirectory);
33141
- const parentDirectory = Path.dirname(currentDirectory);
33142
- if (parentDirectory === currentDirectory) return directories;
33143
- currentDirectory = parentDirectory;
33144
- }
33145
- };
33146
33143
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33147
33144
  const clearPackageJsonCache = () => {
33148
33145
  cachedPackageJsons.clear();
@@ -33176,7 +33173,11 @@ const isMonorepoRoot = (directory) => {
33176
33173
  return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
33177
33174
  };
33178
33175
  const findMonorepoRoot = (startDirectory) => {
33179
- for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
33176
+ let currentDirectory = Path.dirname(startDirectory);
33177
+ while (currentDirectory !== Path.dirname(currentDirectory)) {
33178
+ if (isMonorepoRoot(currentDirectory)) return currentDirectory;
33179
+ currentDirectory = Path.dirname(currentDirectory);
33180
+ }
33180
33181
  return null;
33181
33182
  };
33182
33183
  /**
@@ -33190,6 +33191,146 @@ const findMonorepoRoot = (startDirectory) => {
33190
33191
  * byte-equivalent copy.
33191
33192
  */
33192
33193
  const isProjectBoundary = (directory) => NFS.existsSync(Path.join(directory, ".git")) || isMonorepoRoot(directory);
33194
+ const TSCONFIG_FILENAME = "tsconfig.json";
33195
+ const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
33196
+ const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
33197
+ const resolvePackageExtendsPath = (extendsValue, fromConfigDirectory) => {
33198
+ const requireFromConfig = createRequire(Path.join(fromConfigDirectory, "tsconfig.json"));
33199
+ const candidates = [
33200
+ extendsValue,
33201
+ ensureJsonExtension(extendsValue),
33202
+ `${extendsValue.replace(/\/$/, "")}/tsconfig.json`
33203
+ ];
33204
+ for (const candidate of candidates) try {
33205
+ return requireFromConfig.resolve(candidate);
33206
+ } catch {
33207
+ continue;
33208
+ }
33209
+ return null;
33210
+ };
33211
+ const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
33212
+ if (isRelativeExtendsValue(extendsValue)) return ensureJsonExtension(Path.resolve(fromConfigDirectory, extendsValue));
33213
+ return resolvePackageExtendsPath(extendsValue, fromConfigDirectory);
33214
+ };
33215
+ const normalizeCompilerOptions = (compilerOptions) => {
33216
+ if (!isPlainObject(compilerOptions)) return { hasExplicitLib: false };
33217
+ const target = typeof compilerOptions.target === "string" ? compilerOptions.target : void 0;
33218
+ const hasExplicitLib = Object.hasOwn(compilerOptions, "lib");
33219
+ return {
33220
+ target,
33221
+ lib: Array.isArray(compilerOptions.lib) ? compilerOptions.lib.filter((entry) => typeof entry === "string") : void 0,
33222
+ hasExplicitLib
33223
+ };
33224
+ };
33225
+ const readTsConfig = (filePath) => {
33226
+ let content;
33227
+ try {
33228
+ content = NFS.readFileSync(filePath, "utf-8");
33229
+ } catch {
33230
+ return null;
33231
+ }
33232
+ const parsed = ts.parseConfigFileTextToJson(filePath, content);
33233
+ if (!isPlainObject(parsed.config)) return null;
33234
+ return {
33235
+ extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
33236
+ referencePaths: normalizeReferencePaths(parsed.config.references),
33237
+ compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
33238
+ };
33239
+ };
33240
+ const normalizeReferencePaths = (references) => {
33241
+ if (!Array.isArray(references)) return [];
33242
+ return references.map((reference) => isPlainObject(reference) && typeof reference.path === "string" ? reference.path : null).filter((referencePath) => referencePath !== null);
33243
+ };
33244
+ const mergeCompilerOptions = (inherited, current) => {
33245
+ const target = current.target ?? inherited?.target;
33246
+ const hasExplicitLib = current.hasExplicitLib || Boolean(inherited?.hasExplicitLib);
33247
+ return {
33248
+ target,
33249
+ lib: current.hasExplicitLib ? current.lib : inherited?.lib,
33250
+ hasExplicitLib
33251
+ };
33252
+ };
33253
+ const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
33254
+ const realPath = NFS.realpathSync.native(tsConfigPath);
33255
+ if (visitedPaths.has(realPath)) return null;
33256
+ const tsConfig = readTsConfig(realPath);
33257
+ if (!tsConfig) return null;
33258
+ const nextVisitedPaths = new Set(visitedPaths);
33259
+ nextVisitedPaths.add(realPath);
33260
+ if (tsConfig.extends && extendsDepth < 8) {
33261
+ const parentPath = resolveExtendsPath(tsConfig.extends, Path.dirname(realPath));
33262
+ if (parentPath && isFile(parentPath)) return mergeCompilerOptions(readResolvedCompilerOptions(parentPath, extendsDepth + 1, nextVisitedPaths), tsConfig.compilerOptions);
33263
+ }
33264
+ return tsConfig.compilerOptions;
33265
+ };
33266
+ const targetYearIsPreES2023 = (target) => {
33267
+ const year = ES_TARGET_YEAR_BY_NAME[target.toLowerCase()];
33268
+ return year !== void 0 && year < 2023;
33269
+ };
33270
+ const libEntryIncludesES2023Array = (entry) => {
33271
+ const normalizedEntry = entry.toLowerCase();
33272
+ if (normalizedEntry === "esnext" || normalizedEntry === "esnext.array") return true;
33273
+ const esYearMatch = /^es(\d{4})(?:\.(.+))?$/.exec(normalizedEntry);
33274
+ if (!esYearMatch) return false;
33275
+ if (Number(esYearMatch[1]) < 2023) return false;
33276
+ const component = esYearMatch[2];
33277
+ return component === void 0 || component === "array";
33278
+ };
33279
+ const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
33280
+ const compilerOptionsArePreES2023 = (compilerOptions) => {
33281
+ if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
33282
+ if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
33283
+ return false;
33284
+ };
33285
+ const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
33286
+ const detectPreES2023FromConfig = (tsConfigPath, visitedConfigPaths = /* @__PURE__ */ new Set()) => {
33287
+ if (visitedConfigPaths.has(tsConfigPath)) return false;
33288
+ const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
33289
+ if (!compilerOptions) return false;
33290
+ if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) {
33291
+ const tsConfig = readTsConfig(tsConfigPath);
33292
+ if (!tsConfig) return false;
33293
+ const nextVisitedConfigPaths = new Set(visitedConfigPaths);
33294
+ nextVisitedConfigPaths.add(tsConfigPath);
33295
+ const configDirectory = Path.dirname(tsConfigPath);
33296
+ return tsConfig.referencePaths.some((referencePath) => {
33297
+ const resolvedReferencePath = Path.resolve(configDirectory, referencePath);
33298
+ const referencedConfigPath = isFile(resolvedReferencePath) ? resolvedReferencePath : Path.join(resolvedReferencePath, TSCONFIG_FILENAME);
33299
+ return isFile(referencedConfigPath) && detectPreES2023FromConfig(referencedConfigPath, nextVisitedConfigPaths);
33300
+ });
33301
+ }
33302
+ return compilerOptionsArePreES2023(compilerOptions);
33303
+ };
33304
+ const detectPreES2023Target = (directory) => {
33305
+ const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
33306
+ if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
33307
+ for (const fallbackFilename of FALLBACK_TSCONFIG_FILENAMES) {
33308
+ const fallbackPath = Path.join(directory, fallbackFilename);
33309
+ if (isFile(fallbackPath)) return detectPreES2023FromConfig(fallbackPath);
33310
+ }
33311
+ return false;
33312
+ };
33313
+ const FALLBACK_TSCONFIG_FILENAMES = ["tsconfig.app.json", "tsconfig.build.json"];
33314
+ const FRAMEWORK_PACKAGES = {
33315
+ next: "nextjs",
33316
+ "@tanstack/react-start": "tanstack-start",
33317
+ vite: "vite",
33318
+ "react-scripts": "cra",
33319
+ "@remix-run/react": "remix",
33320
+ gatsby: "gatsby",
33321
+ expo: "expo",
33322
+ "react-native": "react-native"
33323
+ };
33324
+ const detectFramework = (dependencies) => {
33325
+ for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
33326
+ if (dependencies.preact && !dependencies.react) return "preact";
33327
+ return "unknown";
33328
+ };
33329
+ const MOBILE_FRAMEWORKS = new Set(["expo", "react-native"]);
33330
+ const frameworkMergeRank = (framework) => {
33331
+ if (framework === "unknown") return 3;
33332
+ return MOBILE_FRAMEWORKS.has(framework) ? 2 : 1;
33333
+ };
33193
33334
  const REACT_COMPILER_PACKAGES = new Set([
33194
33335
  "babel-plugin-react-compiler",
33195
33336
  "react-compiler-runtime",
@@ -33226,6 +33367,7 @@ const EXPO_APP_CONFIG_FILENAMES = [
33226
33367
  ];
33227
33368
  const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
33228
33369
  const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
33370
+ const STATIC_EXPORT_OUTPUT_PATTERN = /(?:^|[^.\w])["']?output["']?\s*:\s*["']export["']/m;
33229
33371
  const hasCompilerPackage = (packageJson) => {
33230
33372
  const allDependencies = {
33231
33373
  ...packageJson.peerDependencies,
@@ -33258,34 +33400,10 @@ const detectReactCompiler = (directory, packageJson) => {
33258
33400
  }
33259
33401
  return false;
33260
33402
  };
33261
- const FRAMEWORK_PACKAGES = {
33262
- next: "nextjs",
33263
- "@tanstack/react-start": "tanstack-start",
33264
- vite: "vite",
33265
- "react-scripts": "cra",
33266
- "@remix-run/react": "remix",
33267
- gatsby: "gatsby",
33268
- expo: "expo",
33269
- "react-native": "react-native"
33270
- };
33271
- const FRAMEWORK_DISPLAY_NAMES = {
33272
- nextjs: "Next.js",
33273
- "tanstack-start": "TanStack Start",
33274
- vite: "Vite",
33275
- cra: "Create React App",
33276
- remix: "Remix",
33277
- gatsby: "Gatsby",
33278
- expo: "Expo",
33279
- "react-native": "React Native",
33280
- preact: "Preact",
33281
- unknown: "React"
33282
- };
33283
- const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
33284
- const detectFramework = (dependencies) => {
33285
- for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
33286
- if (dependencies.preact && !dependencies.react) return "preact";
33287
- return "unknown";
33288
- };
33403
+ const detectNextjsStaticExport = (directory) => NEXT_CONFIG_FILENAMES.some((filename) => {
33404
+ const filePath = Path.join(directory, filename);
33405
+ return isFile(filePath) && STATIC_EXPORT_OUTPUT_PATTERN.test(NFS.readFileSync(filePath, "utf-8"));
33406
+ });
33289
33407
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
33290
33408
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
33291
33409
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
@@ -33423,39 +33541,105 @@ const isConcreteDependencyVersion = (version) => {
33423
33541
  const normalizedVersion = normalizeDependencyVersion(version);
33424
33542
  return normalizedVersion !== null && /\d/.test(normalizedVersion);
33425
33543
  };
33426
- const isPlainObject = (value) => {
33427
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
33428
- const prototype = Object.getPrototypeOf(value);
33429
- return prototype === null || prototype === Object.prototype;
33430
- };
33431
- const isCatalogReference = (version) => version.startsWith("catalog:");
33432
- const extractCatalogName = (version) => {
33433
- if (!isCatalogReference(version)) return null;
33434
- const name = version.slice(8).trim();
33435
- return name.length > 0 ? name : null;
33544
+ const isMajorMinorAtLeast = (detected, required) => {
33545
+ if (detected === null) return true;
33546
+ if (detected.major !== required.major) return detected.major > required.major;
33547
+ return detected.minor >= required.minor;
33436
33548
  };
33437
- const resolveVersionFromCatalog = (catalog, packageName) => {
33438
- const version = catalog[packageName];
33439
- if (typeof version === "string" && !isCatalogReference(version)) return version;
33440
- return null;
33549
+ const parseReactMajor = (reactVersion) => {
33550
+ if (typeof reactVersion !== "string") return null;
33551
+ return getLowestDependencyMajor(reactVersion);
33441
33552
  };
33442
- const parsePnpmWorkspaceCatalogs = (rootDirectory) => {
33443
- const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
33444
- if (!isFile(workspacePath)) return {
33445
- defaultCatalog: {},
33446
- namedCatalogs: {}
33447
- };
33448
- const content = NFS.readFileSync(workspacePath, "utf-8");
33449
- const defaultCatalog = {};
33450
- const namedCatalogs = {};
33451
- let currentSection = "none";
33452
- let currentCatalogName = "";
33453
- for (const line of content.split("\n")) {
33454
- const trimmed = line.trim();
33455
- if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
33456
- const indentLevel = line.search(/\S/);
33457
- if (indentLevel === 0 && trimmed === "catalog:") {
33458
- currentSection = "catalog";
33553
+ const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
33554
+ const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
33555
+ const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
33556
+ const parseReactMajorMinor = (reactVersion) => {
33557
+ if (typeof reactVersion !== "string") return null;
33558
+ const trimmed = reactVersion.trim();
33559
+ if (trimmed.length === 0) return null;
33560
+ const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
33561
+ if (lowerBoundsOnly.length === 0) return null;
33562
+ const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
33563
+ if (majorMinorMatch) {
33564
+ const major = Number.parseInt(majorMinorMatch[1], 10);
33565
+ const minor = Number.parseInt(majorMinorMatch[2], 10);
33566
+ if (!Number.isFinite(major) || major <= 0) return null;
33567
+ if (!Number.isFinite(minor) || minor < 0) return null;
33568
+ return {
33569
+ major,
33570
+ minor
33571
+ };
33572
+ }
33573
+ const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
33574
+ if (!majorOnlyMatch) return null;
33575
+ const major = Number.parseInt(majorOnlyMatch[1], 10);
33576
+ if (!Number.isFinite(major) || major <= 0) return null;
33577
+ return {
33578
+ major,
33579
+ minor: 0
33580
+ };
33581
+ };
33582
+ const parseLowerBoundVersion = (versionSpec) => import_semver.validRange(versionSpec) !== null ? import_semver.minVersion(versionSpec) : import_semver.coerce(versionSpec);
33583
+ const parseTailwindMajorMinor = (tailwindVersion) => {
33584
+ if (typeof tailwindVersion !== "string") return null;
33585
+ const trimmed = tailwindVersion.trim();
33586
+ if (trimmed.length === 0) return null;
33587
+ const lowerBound = parseLowerBoundVersion(trimmed);
33588
+ if (lowerBound === null || lowerBound.major <= 0) return null;
33589
+ return {
33590
+ major: lowerBound.major,
33591
+ minor: lowerBound.minor
33592
+ };
33593
+ };
33594
+ const hasUpperBoundOnlyPeerRange = (range) => {
33595
+ if (typeof range !== "string") return false;
33596
+ const normalizedRange = normalizeDependencyVersion(range);
33597
+ if (normalizedRange === null) return false;
33598
+ return splitDependencyVersionBranches(normalizedRange).some((branch) => {
33599
+ const normalizedBranch = normalizeDependencyVersion(branch);
33600
+ return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
33601
+ });
33602
+ };
33603
+ const peerRangeMinMajor = (range) => {
33604
+ if (typeof range !== "string") return null;
33605
+ return getLowestDependencyMajor(range);
33606
+ };
33607
+ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
33608
+ const installedReactMajor = parseReactMajor(reactVersion);
33609
+ const peerReactRange = packageJson.peerDependencies?.react;
33610
+ if (typeof peerReactRange !== "string") return installedReactMajor;
33611
+ const peerFloor = peerRangeMinMajor(peerReactRange);
33612
+ if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
33613
+ return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
33614
+ };
33615
+ const isCatalogReference = (version) => version.startsWith("catalog:");
33616
+ const extractCatalogName = (version) => {
33617
+ if (!isCatalogReference(version)) return null;
33618
+ const name = version.slice(8).trim();
33619
+ return name.length > 0 ? name : null;
33620
+ };
33621
+ const resolveVersionFromCatalog = (catalog, packageName) => {
33622
+ const version = catalog[packageName];
33623
+ if (typeof version === "string" && !isCatalogReference(version)) return version;
33624
+ return null;
33625
+ };
33626
+ const parsePnpmWorkspaceCatalogs = (rootDirectory) => {
33627
+ const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
33628
+ if (!isFile(workspacePath)) return {
33629
+ defaultCatalog: {},
33630
+ namedCatalogs: {}
33631
+ };
33632
+ const content = NFS.readFileSync(workspacePath, "utf-8");
33633
+ const defaultCatalog = {};
33634
+ const namedCatalogs = {};
33635
+ let currentSection = "none";
33636
+ let currentCatalogName = "";
33637
+ for (const line of content.split("\n")) {
33638
+ const trimmed = line.trim();
33639
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
33640
+ const indentLevel = line.search(/\S/);
33641
+ if (indentLevel === 0 && trimmed === "catalog:") {
33642
+ currentSection = "catalog";
33459
33643
  continue;
33460
33644
  }
33461
33645
  if (indentLevel === 0 && trimmed === "catalogs:") {
@@ -33568,6 +33752,16 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
33568
33752
  }
33569
33753
  return null;
33570
33754
  };
33755
+ const REACT_SECTIONS = [
33756
+ "dependencies",
33757
+ "peerDependencies",
33758
+ "devDependencies"
33759
+ ];
33760
+ const TAILWIND_ZOD_SECTIONS = [
33761
+ "dependencies",
33762
+ "devDependencies",
33763
+ "peerDependencies"
33764
+ ];
33571
33765
  const EMPTY_DEPENDENCY_INFO = {
33572
33766
  reactVersion: null,
33573
33767
  tailwindVersion: null,
@@ -33608,6 +33802,10 @@ const extractDependencyInfo = (packageJson) => {
33608
33802
  framework: detectFramework(allDependencies)
33609
33803
  };
33610
33804
  };
33805
+ const getDependencySpec = (packageJson, packageName) => {
33806
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
33807
+ return typeof spec === "string" ? spec : null;
33808
+ };
33611
33809
  const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
33612
33810
  for (const section of sections) {
33613
33811
  const version = packageJson[section]?.[packageName];
@@ -33624,31 +33822,141 @@ const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
33624
33822
  version: null
33625
33823
  };
33626
33824
  };
33627
- const isDirectory = (directoryPath) => {
33628
- try {
33629
- return NFS.statSync(directoryPath).isDirectory();
33630
- } catch {
33631
- return false;
33825
+ const REACT_DEPENDENCY_NAMES = new Set([
33826
+ "react",
33827
+ "react-native",
33828
+ "next",
33829
+ "preact"
33830
+ ]);
33831
+ const hasReactDependency = (packageJson) => {
33832
+ const allDependencies = {
33833
+ ...packageJson.peerDependencies,
33834
+ ...packageJson.dependencies,
33835
+ ...packageJson.devDependencies
33836
+ };
33837
+ return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
33838
+ };
33839
+ const getPreactVersion = (packageJson) => {
33840
+ return {
33841
+ ...packageJson.peerDependencies,
33842
+ ...packageJson.dependencies,
33843
+ ...packageJson.devDependencies
33844
+ }.preact ?? null;
33845
+ };
33846
+ const TANSTACK_QUERY_PACKAGES = new Set([
33847
+ "@tanstack/react-query",
33848
+ "@tanstack/query-core",
33849
+ "react-query"
33850
+ ]);
33851
+ const hasTanStackQuery = (packageJson) => {
33852
+ const allDependencies = {
33853
+ ...packageJson.peerDependencies,
33854
+ ...packageJson.dependencies,
33855
+ ...packageJson.devDependencies
33856
+ };
33857
+ return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
33858
+ };
33859
+ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
33860
+ if (version === null || !isCatalogReference(version)) return version;
33861
+ const catalogName = extractCatalogName(version);
33862
+ const resolvedLocalVersion = resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, catalogName);
33863
+ if (resolvedLocalVersion) return resolvedLocalVersion;
33864
+ const monorepoRoot = findMonorepoRoot(rootDirectory);
33865
+ if (!monorepoRoot) return version;
33866
+ const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
33867
+ if (!isFile(monorepoPackageJsonPath)) return version;
33868
+ return resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
33869
+ };
33870
+ /**
33871
+ * The chain of ancestor directories of `startDirectory`, nearest first, up to
33872
+ * and including the filesystem root. With `includeStart`, the chain begins at
33873
+ * `startDirectory` itself; otherwise at its parent. The one place the "walk up
33874
+ * the tree" traversal is expressed, so each caller adds only its own match /
33875
+ * stop policy instead of re-deriving the `path.dirname` loop.
33876
+ */
33877
+ const ancestorDirectories = (startDirectory, { includeStart }) => {
33878
+ const directories = [];
33879
+ let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
33880
+ while (true) {
33881
+ directories.push(currentDirectory);
33882
+ const parentDirectory = Path.dirname(currentDirectory);
33883
+ if (parentDirectory === currentDirectory) return directories;
33884
+ currentDirectory = parentDirectory;
33632
33885
  }
33633
33886
  };
33634
- const NX_PROJECT_DISCOVERY_DIRS = [
33635
- "apps",
33636
- "libs",
33637
- "packages"
33638
- ];
33639
- const getNxWorkspaceDirectories = (rootDirectory) => {
33640
- if (!isFile(Path.join(rootDirectory, "nx.json"))) return [];
33641
- const collected = [];
33642
- for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
33643
- const candidatePath = Path.join(rootDirectory, candidate);
33644
- if (!isDirectory(candidatePath)) continue;
33645
- for (const entry of readDirectoryEntries(candidatePath)) {
33646
- if (!entry.isDirectory()) continue;
33647
- const projectDirectory = Path.join(candidatePath, entry.name);
33648
- if (isFile(Path.join(projectDirectory, "project.json")) || isFile(Path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
33649
- }
33887
+ /**
33888
+ * Walk up from `startDirectory` to the nearest ancestor that owns a
33889
+ * `package.json`, stopping at (and including) the enclosing project boundary
33890
+ * — the working tree's git root or a monorepo root. Lets a scan of a package
33891
+ * subfolder that has no `package.json` of its own adopt the nearest enclosing
33892
+ * package as its project root, so it inherits that package's dependency +
33893
+ * framework detection instead of synthesizing an empty (React-blind) project.
33894
+ *
33895
+ * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
33896
+ * most specific owning package (a leaf workspace, not just the repo root), so
33897
+ * a subfolder of one workspace no longer borrows a sibling's capabilities.
33898
+ *
33899
+ * Returns `null` when no ancestor `package.json` exists inside the project
33900
+ * boundary (a loose tree of files, or a subfolder of a package-less repo).
33901
+ */
33902
+ const findNearestAncestorPackageJson = (startDirectory) => {
33903
+ if (isProjectBoundary(startDirectory)) return null;
33904
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
33905
+ if (isFile(Path.join(directory, "package.json"))) return directory;
33906
+ if (isProjectBoundary(directory)) return null;
33650
33907
  }
33651
- return collected;
33908
+ return null;
33909
+ };
33910
+ const NAMES = new Set([
33911
+ "react-native",
33912
+ "react-native-tvos",
33913
+ ...new Set([
33914
+ "expo",
33915
+ "expo-router",
33916
+ "@expo/cli",
33917
+ "@expo/metro-config",
33918
+ "@expo/metro-runtime"
33919
+ ]),
33920
+ "react-native-windows",
33921
+ "react-native-macos"
33922
+ ]);
33923
+ const PREFIXES = ["@react-native/", "@react-native-"];
33924
+ const isReactNativeDependencyName = (dependencyName) => {
33925
+ if (NAMES.has(dependencyName)) return true;
33926
+ for (const prefix of PREFIXES) if (dependencyName.startsWith(prefix)) return true;
33927
+ return false;
33928
+ };
33929
+ const containsAnyReactNativeDependency = (section) => {
33930
+ if (!section) return false;
33931
+ for (const dependencyName of Object.keys(section)) if (isReactNativeDependencyName(dependencyName)) return true;
33932
+ return false;
33933
+ };
33934
+ const isPackageJsonReactNativeAware = (packageJson) => {
33935
+ if (typeof packageJson["react-native"] === "string") return true;
33936
+ if (containsAnyReactNativeDependency(packageJson.dependencies)) return true;
33937
+ if (containsAnyReactNativeDependency(packageJson.devDependencies)) return true;
33938
+ if (containsAnyReactNativeDependency(packageJson.peerDependencies)) return true;
33939
+ if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
33940
+ return false;
33941
+ };
33942
+ const REANIMATED_DEPENDENCY_NAME$1 = "react-native-reanimated";
33943
+ const isPackageJsonReanimatedAware = (packageJson) => {
33944
+ const allDependencies = {
33945
+ ...packageJson.peerDependencies,
33946
+ ...packageJson.dependencies,
33947
+ ...packageJson.devDependencies,
33948
+ ...packageJson.optionalDependencies
33949
+ };
33950
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME$1);
33951
+ };
33952
+ const getWorkspacePatterns = (rootDirectory, packageJson) => {
33953
+ const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
33954
+ if (pnpmPatterns.length > 0) return pnpmPatterns;
33955
+ if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
33956
+ if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
33957
+ const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
33958
+ if (nxPatterns.length > 0) return nxPatterns;
33959
+ return [];
33652
33960
  };
33653
33961
  const parsePnpmWorkspacePatterns = (rootDirectory) => {
33654
33962
  const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
@@ -33667,487 +33975,42 @@ const parsePnpmWorkspacePatterns = (rootDirectory) => {
33667
33975
  }
33668
33976
  return patterns;
33669
33977
  };
33670
- const getWorkspacePatterns = (rootDirectory, packageJson) => {
33671
- const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
33672
- if (pnpmPatterns.length > 0) return pnpmPatterns;
33673
- if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
33674
- if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
33675
- const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
33676
- if (nxPatterns.length > 0) return nxPatterns;
33677
- return [];
33678
- };
33679
- const parseReactMajor = (reactVersion) => {
33680
- if (typeof reactVersion !== "string") return null;
33681
- return getLowestDependencyMajor(reactVersion);
33978
+ const NX_PROJECT_DISCOVERY_DIRS = [
33979
+ "apps",
33980
+ "libs",
33981
+ "packages"
33982
+ ];
33983
+ const getNxWorkspaceDirectories = (rootDirectory) => {
33984
+ if (!isFile(Path.join(rootDirectory, "nx.json"))) return [];
33985
+ const collected = [];
33986
+ for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
33987
+ const candidatePath = Path.join(rootDirectory, candidate);
33988
+ if (!isDirectory(candidatePath)) continue;
33989
+ for (const entry of readDirectoryEntries(candidatePath)) {
33990
+ if (!entry.isDirectory()) continue;
33991
+ const projectDirectory = Path.join(candidatePath, entry.name);
33992
+ if (isFile(Path.join(projectDirectory, "project.json")) || isFile(Path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
33993
+ }
33994
+ }
33995
+ return collected;
33682
33996
  };
33683
33997
  const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
33684
33998
  const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
33685
33999
  if (!cleanPattern.includes("*")) {
33686
- const directoryPath = Path.join(rootDirectory, cleanPattern);
33687
- if (isDirectory(directoryPath) && isFile(Path.join(directoryPath, "package.json"))) return [directoryPath];
33688
- return [];
33689
- }
33690
- const wildcardIndex = cleanPattern.indexOf("*");
33691
- const baseDirectory = Path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
33692
- const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
33693
- if (!isDirectory(baseDirectory)) return [];
33694
- const resolved = [];
33695
- for (const entry of readDirectoryEntries(baseDirectory)) {
33696
- const entryPath = Path.join(baseDirectory, entry.name, suffixAfterWildcard);
33697
- if (isDirectory(entryPath) && isFile(Path.join(entryPath, "package.json"))) resolved.push(entryPath);
33698
- }
33699
- return resolved;
33700
- };
33701
- const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
33702
- const dependencyDeclaration = getDependencyDeclaration({
33703
- packageJson: workspacePackageJson,
33704
- packageName,
33705
- sections
33706
- });
33707
- if (!dependencyDeclaration.hasDeclaration) return null;
33708
- return concreteVersion ?? resolveCatalogVersion(workspacePackageJson, packageName, workspaceDirectory, dependencyDeclaration.catalogReference) ?? resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, dependencyDeclaration.catalogReference);
33709
- };
33710
- const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
33711
- if (!currentVersion) return true;
33712
- const currentMajor = parseReactMajor(currentVersion);
33713
- const nextMajor = parseReactMajor(nextVersion);
33714
- if (currentMajor === null) return nextMajor !== null;
33715
- if (nextMajor === null) return false;
33716
- return nextMajor < currentMajor;
33717
- };
33718
- const findReactInWorkspaces = (rootDirectory, packageJson) => {
33719
- const patterns = getWorkspacePatterns(rootDirectory, packageJson);
33720
- const result = { ...EMPTY_DEPENDENCY_INFO };
33721
- for (const pattern of patterns) {
33722
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
33723
- for (const workspaceDirectory of directories) {
33724
- const workspacePackageJson = readPackageJson(Path.join(workspaceDirectory, "package.json"));
33725
- const info = extractDependencyInfo(workspacePackageJson);
33726
- const reactVersion = resolveWorkspaceDependencyVersion({
33727
- concreteVersion: info.reactVersion,
33728
- packageName: "react",
33729
- rootDirectory,
33730
- rootPackageJson: packageJson,
33731
- sections: [
33732
- "dependencies",
33733
- "peerDependencies",
33734
- "devDependencies"
33735
- ],
33736
- workspaceDirectory,
33737
- workspacePackageJson
33738
- });
33739
- const tailwindVersion = resolveWorkspaceDependencyVersion({
33740
- concreteVersion: info.tailwindVersion,
33741
- packageName: "tailwindcss",
33742
- rootDirectory,
33743
- rootPackageJson: packageJson,
33744
- sections: [
33745
- "dependencies",
33746
- "devDependencies",
33747
- "peerDependencies"
33748
- ],
33749
- workspaceDirectory,
33750
- workspacePackageJson
33751
- });
33752
- const zodVersion = resolveWorkspaceDependencyVersion({
33753
- concreteVersion: info.zodVersion,
33754
- packageName: "zod",
33755
- rootDirectory,
33756
- rootPackageJson: packageJson,
33757
- sections: [
33758
- "dependencies",
33759
- "devDependencies",
33760
- "peerDependencies"
33761
- ],
33762
- workspaceDirectory,
33763
- workspacePackageJson
33764
- });
33765
- if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
33766
- if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
33767
- if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
33768
- if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
33769
- const resultReactMajor = parseReactMajor(result.reactVersion);
33770
- if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
33771
- }
33772
- }
33773
- return result;
33774
- };
33775
- const findDependencyInfoFromMonorepoRoot = (directory) => {
33776
- const monorepoRoot = findMonorepoRoot(directory);
33777
- if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
33778
- const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
33779
- if (!isFile(monorepoPackageJsonPath)) return EMPTY_DEPENDENCY_INFO;
33780
- const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
33781
- const rootInfo = extractDependencyInfo(rootPackageJson);
33782
- const leafPackageJsonPath = Path.join(directory, "package.json");
33783
- const leafPackageJson = isFile(leafPackageJsonPath) ? readPackageJson(leafPackageJsonPath) : null;
33784
- const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
33785
- packageJson: leafPackageJson,
33786
- packageName: "react",
33787
- sections: [
33788
- "dependencies",
33789
- "peerDependencies",
33790
- "devDependencies"
33791
- ]
33792
- }) : null;
33793
- const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
33794
- packageJson: leafPackageJson,
33795
- packageName: "tailwindcss",
33796
- sections: [
33797
- "dependencies",
33798
- "devDependencies",
33799
- "peerDependencies"
33800
- ]
33801
- }) : null;
33802
- const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
33803
- packageJson: leafPackageJson,
33804
- packageName: "zod",
33805
- sections: [
33806
- "dependencies",
33807
- "devDependencies",
33808
- "peerDependencies"
33809
- ]
33810
- }) : null;
33811
- const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
33812
- const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
33813
- const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
33814
- const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
33815
- const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
33816
- const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
33817
- const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
33818
- return {
33819
- reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
33820
- tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
33821
- zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
33822
- framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
33823
- };
33824
- };
33825
- /**
33826
- * Walk up from `startDirectory` to the nearest ancestor that owns a
33827
- * `package.json`, stopping at (and including) the enclosing project boundary
33828
- * — the working tree's git root or a monorepo root. Lets a scan of a package
33829
- * subfolder that has no `package.json` of its own adopt the nearest enclosing
33830
- * package as its project root, so it inherits that package's dependency +
33831
- * framework detection instead of synthesizing an empty (React-blind) project.
33832
- *
33833
- * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
33834
- * most specific owning package (a leaf workspace, not just the repo root), so
33835
- * a subfolder of one workspace no longer borrows a sibling's capabilities.
33836
- *
33837
- * Returns `null` when no ancestor `package.json` exists inside the project
33838
- * boundary (a loose tree of files, or a subfolder of a package-less repo).
33839
- */
33840
- const findNearestAncestorPackageJson = (startDirectory) => {
33841
- if (isProjectBoundary(startDirectory)) return null;
33842
- for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
33843
- if (isFile(Path.join(directory, "package.json"))) return directory;
33844
- if (isProjectBoundary(directory)) return null;
33845
- }
33846
- return null;
33847
- };
33848
- /**
33849
- * Last-resort React detection: locate React the way Node's `node_modules`
33850
- * resolution would from `directory` — check each `node_modules/react` up the
33851
- * tree — but bounded to the enclosing repo so a globally installed React can't
33852
- * masquerade as the project's version. Stops at (and including) the nearest
33853
- * project boundary — the git root or a monorepo root — and returns the first
33854
- * installed `version`.
33855
- *
33856
- * Makes "React is installed and importable" ⇒ "React is detected" an invariant
33857
- * for packages whose only React declaration is a version-less spec
33858
- * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
33859
- * `node_modules` the declaration walks never reach — the profile of a component
33860
- * package inside a monorepo. Matching the `node_modules/react` entry rather than
33861
- * its realpath means a project whose `node_modules` is symlinked elsewhere (a
33862
- * Docker volume, a shared store) still counts, while a global install outside
33863
- * the repo does not.
33864
- *
33865
- * Returns `null` when React isn't installed within the repo or its package.json
33866
- * carries no version string. A tree with no boundary marker at all bounds the
33867
- * search to `directory`, so a dependency hoisted above the scanned package there
33868
- * is conservatively not adopted.
33869
- */
33870
- /**
33871
- * The nearest enclosing project boundary (git root or monorepo root), or
33872
- * `directory` itself when the scan target sits outside any repo. Bounds the
33873
- * node_modules walk so a React hoisted anywhere inside the repo counts, but a
33874
- * React in an ancestor above the repo — a home-directory or global
33875
- * `node_modules` — can't leak in.
33876
- */
33877
- const findContainmentRoot = (directory) => {
33878
- for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
33879
- return directory;
33880
- };
33881
- const resolveInstalledReactVersion = (directory) => {
33882
- const containmentRoot = findContainmentRoot(directory);
33883
- for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
33884
- const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
33885
- if (isFile(reactPackageJsonPath)) {
33886
- const installedVersion = readPackageJson(reactPackageJsonPath).version;
33887
- return typeof installedVersion === "string" ? installedVersion : null;
33888
- }
33889
- if (ancestorDirectory === containmentRoot) return null;
33890
- }
33891
- return null;
33892
- };
33893
- const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
33894
- const rootValue = select(rootPackageJson);
33895
- if (rootValue !== null) return rootValue;
33896
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
33897
- if (patterns.length === 0) return null;
33898
- const visitedDirectories = /* @__PURE__ */ new Set();
33899
- for (const pattern of patterns) {
33900
- const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
33901
- for (const workspaceDirectory of directories) {
33902
- if (visitedDirectories.has(workspaceDirectory)) continue;
33903
- visitedDirectories.add(workspaceDirectory);
33904
- const value = select(readPackageJson(Path.join(workspaceDirectory, "package.json")));
33905
- if (value !== null) return value;
33906
- }
33907
- }
33908
- return null;
33909
- };
33910
- const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => predicate(packageJson) ? true : null) !== null;
33911
- const NAMES = new Set([
33912
- "react-native",
33913
- "react-native-tvos",
33914
- ...new Set([
33915
- "expo",
33916
- "expo-router",
33917
- "@expo/cli",
33918
- "@expo/metro-config",
33919
- "@expo/metro-runtime"
33920
- ]),
33921
- "react-native-windows",
33922
- "react-native-macos"
33923
- ]);
33924
- const PREFIXES = ["@react-native/", "@react-native-"];
33925
- const isReactNativeDependencyName = (dependencyName) => {
33926
- if (NAMES.has(dependencyName)) return true;
33927
- for (const prefix of PREFIXES) if (dependencyName.startsWith(prefix)) return true;
33928
- return false;
33929
- };
33930
- const containsAnyReactNativeDependency = (section) => {
33931
- if (!section) return false;
33932
- for (const dependencyName of Object.keys(section)) if (isReactNativeDependencyName(dependencyName)) return true;
33933
- return false;
33934
- };
33935
- const isPackageJsonReactNativeAware = (packageJson) => {
33936
- if (typeof packageJson["react-native"] === "string") return true;
33937
- if (containsAnyReactNativeDependency(packageJson.dependencies)) return true;
33938
- if (containsAnyReactNativeDependency(packageJson.devDependencies)) return true;
33939
- if (containsAnyReactNativeDependency(packageJson.peerDependencies)) return true;
33940
- if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
33941
- return false;
33942
- };
33943
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
33944
- const getDependencySpec = (packageJson, packageName) => {
33945
- const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
33946
- return typeof spec === "string" ? spec : null;
33947
- };
33948
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
33949
- const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
33950
- const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
33951
- const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
33952
- if (version === null || !isCatalogReference(version)) return version;
33953
- const catalogName = extractCatalogName(version);
33954
- const resolvedLocalVersion = resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, catalogName);
33955
- if (resolvedLocalVersion) return resolvedLocalVersion;
33956
- const monorepoRoot = findMonorepoRoot(rootDirectory);
33957
- if (!monorepoRoot) return version;
33958
- const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
33959
- if (!isFile(monorepoPackageJsonPath)) return version;
33960
- return resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
33961
- };
33962
- const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
33963
- const getPreactVersion = (packageJson) => {
33964
- return {
33965
- ...packageJson.peerDependencies,
33966
- ...packageJson.dependencies,
33967
- ...packageJson.devDependencies
33968
- }.preact ?? null;
33969
- };
33970
- const TANSTACK_QUERY_PACKAGES = new Set([
33971
- "@tanstack/react-query",
33972
- "@tanstack/query-core",
33973
- "react-query"
33974
- ]);
33975
- const hasTanStackQuery = (packageJson) => {
33976
- const allDependencies = {
33977
- ...packageJson.peerDependencies,
33978
- ...packageJson.dependencies,
33979
- ...packageJson.devDependencies
33980
- };
33981
- return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
33982
- };
33983
- const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
33984
- const isPackageJsonReanimatedAware = (packageJson) => {
33985
- const allDependencies = {
33986
- ...packageJson.peerDependencies,
33987
- ...packageJson.dependencies,
33988
- ...packageJson.devDependencies,
33989
- ...packageJson.optionalDependencies
33990
- };
33991
- return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
33992
- };
33993
- const parseZodMajor = (zodVersion) => {
33994
- if (typeof zodVersion !== "string") return null;
33995
- return getLowestDependencyMajor(zodVersion);
33996
- };
33997
- const hasUpperBoundOnlyPeerRange = (range) => {
33998
- if (typeof range !== "string") return false;
33999
- const normalizedRange = normalizeDependencyVersion(range);
34000
- if (normalizedRange === null) return false;
34001
- return splitDependencyVersionBranches(normalizedRange).some((branch) => {
34002
- const normalizedBranch = normalizeDependencyVersion(branch);
34003
- return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
34004
- });
34005
- };
34006
- const peerRangeMinMajor = (range) => {
34007
- if (typeof range !== "string") return null;
34008
- return getLowestDependencyMajor(range);
34009
- };
34010
- const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
34011
- const installedReactMajor = parseReactMajor(reactVersion);
34012
- const peerReactRange = packageJson.peerDependencies?.react;
34013
- if (typeof peerReactRange !== "string") return installedReactMajor;
34014
- const peerFloor = peerRangeMinMajor(peerReactRange);
34015
- if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
34016
- return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
34017
- };
34018
- const TSCONFIG_FILENAME = "tsconfig.json";
34019
- const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
34020
- const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
34021
- const resolvePackageExtendsPath = (extendsValue, fromConfigDirectory) => {
34022
- const requireFromConfig = createRequire(Path.join(fromConfigDirectory, "tsconfig.json"));
34023
- const candidates = [
34024
- extendsValue,
34025
- ensureJsonExtension(extendsValue),
34026
- `${extendsValue.replace(/\/$/, "")}/tsconfig.json`
34027
- ];
34028
- for (const candidate of candidates) try {
34029
- return requireFromConfig.resolve(candidate);
34030
- } catch {
34031
- continue;
34032
- }
34033
- return null;
34034
- };
34035
- const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
34036
- if (isRelativeExtendsValue(extendsValue)) return ensureJsonExtension(Path.resolve(fromConfigDirectory, extendsValue));
34037
- return resolvePackageExtendsPath(extendsValue, fromConfigDirectory);
34038
- };
34039
- const normalizeCompilerOptions = (compilerOptions) => {
34040
- if (!isPlainObject(compilerOptions)) return { hasExplicitLib: false };
34041
- const target = typeof compilerOptions.target === "string" ? compilerOptions.target : void 0;
34042
- const hasExplicitLib = Object.hasOwn(compilerOptions, "lib");
34043
- return {
34044
- target,
34045
- lib: Array.isArray(compilerOptions.lib) ? compilerOptions.lib.filter((entry) => typeof entry === "string") : void 0,
34046
- hasExplicitLib
34047
- };
34048
- };
34049
- const normalizeReferencePaths = (references) => {
34050
- if (!Array.isArray(references)) return [];
34051
- return references.map((reference) => isPlainObject(reference) && typeof reference.path === "string" ? reference.path : null).filter((referencePath) => referencePath !== null);
34052
- };
34053
- const readTsConfig = (filePath) => {
34054
- let content;
34055
- try {
34056
- content = NFS.readFileSync(filePath, "utf-8");
34057
- } catch {
34058
- return null;
34059
- }
34060
- const parsed = ts.parseConfigFileTextToJson(filePath, content);
34061
- if (!isPlainObject(parsed.config)) return null;
34062
- return {
34063
- extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
34064
- referencePaths: normalizeReferencePaths(parsed.config.references),
34065
- compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
34066
- };
34067
- };
34068
- const mergeCompilerOptions = (inherited, current) => {
34069
- const target = current.target ?? inherited?.target;
34070
- const hasExplicitLib = current.hasExplicitLib || Boolean(inherited?.hasExplicitLib);
34071
- return {
34072
- target,
34073
- lib: current.hasExplicitLib ? current.lib : inherited?.lib,
34074
- hasExplicitLib
34075
- };
34076
- };
34077
- const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
34078
- const realPath = NFS.realpathSync.native(tsConfigPath);
34079
- if (visitedPaths.has(realPath)) return null;
34080
- const tsConfig = readTsConfig(realPath);
34081
- if (!tsConfig) return null;
34082
- const nextVisitedPaths = new Set(visitedPaths);
34083
- nextVisitedPaths.add(realPath);
34084
- if (tsConfig.extends && extendsDepth < 8) {
34085
- const parentPath = resolveExtendsPath(tsConfig.extends, Path.dirname(realPath));
34086
- if (parentPath && isFile(parentPath)) return mergeCompilerOptions(readResolvedCompilerOptions(parentPath, extendsDepth + 1, nextVisitedPaths), tsConfig.compilerOptions);
34087
- }
34088
- return tsConfig.compilerOptions;
34089
- };
34090
- const targetYearIsPreES2023 = (target) => {
34091
- const year = ES_TARGET_YEAR_BY_NAME[target.toLowerCase()];
34092
- return year !== void 0 && year < 2023;
34093
- };
34094
- const libEntryIncludesES2023Array = (entry) => {
34095
- const normalizedEntry = entry.toLowerCase();
34096
- if (normalizedEntry === "esnext" || normalizedEntry === "esnext.array") return true;
34097
- const esYearMatch = /^es(\d{4})(?:\.(.+))?$/.exec(normalizedEntry);
34098
- if (!esYearMatch) return false;
34099
- if (Number(esYearMatch[1]) < 2023) return false;
34100
- const component = esYearMatch[2];
34101
- return component === void 0 || component === "array";
34102
- };
34103
- const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
34104
- const compilerOptionsArePreES2023 = (compilerOptions) => {
34105
- if (compilerOptions.target && targetYearIsPreES2023(compilerOptions.target)) return true;
34106
- if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
34107
- return false;
34108
- };
34109
- const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
34110
- const detectPreES2023FromConfig = (tsConfigPath, visitedConfigPaths = /* @__PURE__ */ new Set()) => {
34111
- if (visitedConfigPaths.has(tsConfigPath)) return false;
34112
- const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
34113
- if (!compilerOptions) return false;
34114
- if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) {
34115
- const tsConfig = readTsConfig(tsConfigPath);
34116
- if (!tsConfig) return false;
34117
- const nextVisitedConfigPaths = new Set(visitedConfigPaths);
34118
- nextVisitedConfigPaths.add(tsConfigPath);
34119
- const configDirectory = Path.dirname(tsConfigPath);
34120
- return tsConfig.referencePaths.some((referencePath) => {
34121
- const resolvedReferencePath = Path.resolve(configDirectory, referencePath);
34122
- const referencedConfigPath = isFile(resolvedReferencePath) ? resolvedReferencePath : Path.join(resolvedReferencePath, TSCONFIG_FILENAME);
34123
- return isFile(referencedConfigPath) && detectPreES2023FromConfig(referencedConfigPath, nextVisitedConfigPaths);
34124
- });
34125
- }
34126
- return compilerOptionsArePreES2023(compilerOptions);
34127
- };
34128
- const FALLBACK_TSCONFIG_FILENAMES = ["tsconfig.app.json", "tsconfig.build.json"];
34129
- const detectPreES2023Target = (directory) => {
34130
- const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
34131
- if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
34132
- for (const fallbackFilename of FALLBACK_TSCONFIG_FILENAMES) {
34133
- const fallbackPath = Path.join(directory, fallbackFilename);
34134
- if (isFile(fallbackPath)) return detectPreES2023FromConfig(fallbackPath);
34135
- }
34136
- return false;
34137
- };
34138
- const REACT_DEPENDENCY_NAMES = new Set([
34139
- "react",
34140
- "react-native",
34141
- "next",
34142
- "preact"
34143
- ]);
34144
- const hasReactDependency = (packageJson) => {
34145
- const allDependencies = {
34146
- ...packageJson.peerDependencies,
34147
- ...packageJson.dependencies,
34148
- ...packageJson.devDependencies
34149
- };
34150
- return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
34000
+ const directoryPath = Path.join(rootDirectory, cleanPattern);
34001
+ if (isDirectory(directoryPath) && isFile(Path.join(directoryPath, "package.json"))) return [directoryPath];
34002
+ return [];
34003
+ }
34004
+ const wildcardIndex = cleanPattern.indexOf("*");
34005
+ const baseDirectory = Path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
34006
+ const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
34007
+ if (!isDirectory(baseDirectory)) return [];
34008
+ const resolved = [];
34009
+ for (const entry of readDirectoryEntries(baseDirectory)) {
34010
+ const entryPath = Path.join(baseDirectory, entry.name, suffixAfterWildcard);
34011
+ if (isDirectory(entryPath) && isFile(Path.join(entryPath, "package.json"))) resolved.push(entryPath);
34012
+ }
34013
+ return resolved;
34151
34014
  };
34152
34015
  const listWorkspacePackages = (rootDirectory) => {
34153
34016
  const packageJsonPath = Path.join(rootDirectory, "package.json");
@@ -34179,6 +34042,208 @@ const listWorkspacePackages = (rootDirectory) => {
34179
34042
  }
34180
34043
  return packages;
34181
34044
  };
34045
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
34046
+ const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
34047
+ const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
34048
+ const dependencyDeclaration = getDependencyDeclaration({
34049
+ packageJson: workspacePackageJson,
34050
+ packageName,
34051
+ sections
34052
+ });
34053
+ if (!dependencyDeclaration.hasDeclaration) return null;
34054
+ return concreteVersion ?? resolveCatalogVersion(workspacePackageJson, packageName, workspaceDirectory, dependencyDeclaration.catalogReference) ?? resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, dependencyDeclaration.catalogReference);
34055
+ };
34056
+ const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
34057
+ if (!currentVersion) return true;
34058
+ const currentMajor = parseReactMajor(currentVersion);
34059
+ const nextMajor = parseReactMajor(nextVersion);
34060
+ if (currentMajor === null) return nextMajor !== null;
34061
+ if (nextMajor === null) return false;
34062
+ return nextMajor < currentMajor;
34063
+ };
34064
+ const evaluateManifestFacts = (facts, packageJson, directory) => {
34065
+ if (facts.expo.version === null) {
34066
+ const spec = getDependencySpec(packageJson, "expo");
34067
+ if (spec !== null) facts.expo = {
34068
+ version: spec,
34069
+ sourceDirectory: directory
34070
+ };
34071
+ }
34072
+ if (facts.next.version === null) {
34073
+ const spec = getDependencySpec(packageJson, "next");
34074
+ if (spec !== null) facts.next = {
34075
+ version: spec,
34076
+ sourceDirectory: directory
34077
+ };
34078
+ }
34079
+ if (facts.shopifyFlashList.version === null) {
34080
+ const spec = getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME);
34081
+ if (spec !== null) facts.shopifyFlashList = {
34082
+ version: spec,
34083
+ sourceDirectory: directory
34084
+ };
34085
+ }
34086
+ if (facts.reanimatedVersion === null) {
34087
+ const spec = getDependencySpec(packageJson, REANIMATED_DEPENDENCY_NAME);
34088
+ if (spec !== null) facts.reanimatedVersion = spec;
34089
+ }
34090
+ facts.hasReactNativeAwarePackage = facts.hasReactNativeAwarePackage || isPackageJsonReactNativeAware(packageJson);
34091
+ facts.hasReanimatedAwarePackage = facts.hasReanimatedAwarePackage || isPackageJsonReanimatedAware(packageJson);
34092
+ };
34093
+ const collectWorkspaceFacts = (rootDirectory, rootPackageJson, { collectReactGroup }) => {
34094
+ const facts = {
34095
+ reactVersion: null,
34096
+ tailwindVersion: null,
34097
+ zodVersion: null,
34098
+ framework: "unknown",
34099
+ expo: {
34100
+ version: null,
34101
+ sourceDirectory: null
34102
+ },
34103
+ next: {
34104
+ version: null,
34105
+ sourceDirectory: null
34106
+ },
34107
+ shopifyFlashList: {
34108
+ version: null,
34109
+ sourceDirectory: null
34110
+ },
34111
+ hasReactNativeAwarePackage: false,
34112
+ hasReanimatedAwarePackage: false,
34113
+ reanimatedVersion: null
34114
+ };
34115
+ evaluateManifestFacts(facts, rootPackageJson, rootDirectory);
34116
+ let isReactGroupSettled = !collectReactGroup;
34117
+ const visitedDirectories = /* @__PURE__ */ new Set();
34118
+ for (const pattern of getWorkspacePatterns(rootDirectory, rootPackageJson)) {
34119
+ const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
34120
+ for (const workspaceDirectory of directories) {
34121
+ if (visitedDirectories.has(workspaceDirectory)) continue;
34122
+ visitedDirectories.add(workspaceDirectory);
34123
+ const workspacePackageJson = readPackageJson(Path.join(workspaceDirectory, "package.json"));
34124
+ evaluateManifestFacts(facts, workspacePackageJson, workspaceDirectory);
34125
+ const info = extractDependencyInfo(workspacePackageJson);
34126
+ if (info.framework !== "unknown" && frameworkMergeRank(info.framework) < frameworkMergeRank(facts.framework)) facts.framework = info.framework;
34127
+ if (isReactGroupSettled) continue;
34128
+ const reactVersion = resolveWorkspaceDependencyVersion({
34129
+ concreteVersion: info.reactVersion,
34130
+ packageName: "react",
34131
+ rootDirectory,
34132
+ rootPackageJson,
34133
+ sections: REACT_SECTIONS,
34134
+ workspaceDirectory,
34135
+ workspacePackageJson
34136
+ });
34137
+ const tailwindVersion = resolveWorkspaceDependencyVersion({
34138
+ concreteVersion: info.tailwindVersion,
34139
+ packageName: "tailwindcss",
34140
+ rootDirectory,
34141
+ rootPackageJson,
34142
+ sections: TAILWIND_ZOD_SECTIONS,
34143
+ workspaceDirectory,
34144
+ workspacePackageJson
34145
+ });
34146
+ const zodVersion = resolveWorkspaceDependencyVersion({
34147
+ concreteVersion: info.zodVersion,
34148
+ packageName: "zod",
34149
+ rootDirectory,
34150
+ rootPackageJson,
34151
+ sections: TAILWIND_ZOD_SECTIONS,
34152
+ workspaceDirectory,
34153
+ workspacePackageJson
34154
+ });
34155
+ if (reactVersion && shouldReplaceReactVersion(facts.reactVersion, reactVersion)) facts.reactVersion = reactVersion;
34156
+ if (tailwindVersion && !facts.tailwindVersion) facts.tailwindVersion = tailwindVersion;
34157
+ if (zodVersion && !facts.zodVersion) facts.zodVersion = zodVersion;
34158
+ const settledReactMajor = parseReactMajor(facts.reactVersion);
34159
+ isReactGroupSettled = Boolean(facts.reactVersion && facts.tailwindVersion && facts.framework !== "unknown" && settledReactMajor !== null && settledReactMajor <= 17);
34160
+ }
34161
+ }
34162
+ return facts;
34163
+ };
34164
+ const findDependencyInfoFromMonorepoRoot = (directory) => {
34165
+ const monorepoRoot = findMonorepoRoot(directory);
34166
+ if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
34167
+ const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
34168
+ if (!isFile(monorepoPackageJsonPath)) return EMPTY_DEPENDENCY_INFO;
34169
+ const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
34170
+ const rootInfo = extractDependencyInfo(rootPackageJson);
34171
+ const leafPackageJsonPath = Path.join(directory, "package.json");
34172
+ const leafPackageJson = isFile(leafPackageJsonPath) ? readPackageJson(leafPackageJsonPath) : null;
34173
+ const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
34174
+ packageJson: leafPackageJson,
34175
+ packageName: "react",
34176
+ sections: REACT_SECTIONS
34177
+ }) : null;
34178
+ const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
34179
+ packageJson: leafPackageJson,
34180
+ packageName: "tailwindcss",
34181
+ sections: TAILWIND_ZOD_SECTIONS
34182
+ }) : null;
34183
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
34184
+ packageJson: leafPackageJson,
34185
+ packageName: "zod",
34186
+ sections: TAILWIND_ZOD_SECTIONS
34187
+ }) : null;
34188
+ const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
34189
+ const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
34190
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
34191
+ const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
34192
+ const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
34193
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
34194
+ const workspaceFacts = collectWorkspaceFacts(monorepoRoot, rootPackageJson, { collectReactGroup: true });
34195
+ return {
34196
+ reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceFacts.reactVersion : rootInfo.reactVersion ?? workspaceFacts.reactVersion,
34197
+ tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceFacts.tailwindVersion : null,
34198
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceFacts.zodVersion : null,
34199
+ framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceFacts.framework
34200
+ };
34201
+ };
34202
+ /**
34203
+ * Last-resort React detection: locate React the way Node's `node_modules`
34204
+ * resolution would from `directory` — check each `node_modules/react` up the
34205
+ * tree — but bounded to the enclosing repo so a globally installed React can't
34206
+ * masquerade as the project's version. Stops at (and including) the nearest
34207
+ * project boundary — the git root or a monorepo root — and returns the first
34208
+ * installed `version`.
34209
+ *
34210
+ * Makes "React is installed and importable" ⇒ "React is detected" an invariant
34211
+ * for packages whose only React declaration is a version-less spec
34212
+ * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
34213
+ * `node_modules` the declaration walks never reach — the profile of a component
34214
+ * package inside a monorepo. Matching the `node_modules/react` entry rather than
34215
+ * its realpath means a project whose `node_modules` is symlinked elsewhere (a
34216
+ * Docker volume, a shared store) still counts, while a global install outside
34217
+ * the repo does not.
34218
+ *
34219
+ * Returns `null` when React isn't installed within the repo or its package.json
34220
+ * carries no version string. A tree with no boundary marker at all bounds the
34221
+ * search to `directory`, so a dependency hoisted above the scanned package there
34222
+ * is conservatively not adopted.
34223
+ */
34224
+ /**
34225
+ * The nearest enclosing project boundary (git root or monorepo root), or
34226
+ * `directory` itself when the scan target sits outside any repo. Bounds the
34227
+ * node_modules walk so a React hoisted anywhere inside the repo counts, but a
34228
+ * React in an ancestor above the repo — a home-directory or global
34229
+ * `node_modules` — can't leak in.
34230
+ */
34231
+ const findContainmentRoot = (directory) => {
34232
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
34233
+ return directory;
34234
+ };
34235
+ const resolveInstalledReactVersion = (directory) => {
34236
+ const containmentRoot = findContainmentRoot(directory);
34237
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
34238
+ const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
34239
+ if (isFile(reactPackageJsonPath)) {
34240
+ const installedVersion = readPackageJson(reactPackageJsonPath).version;
34241
+ return typeof installedVersion === "string" ? installedVersion : null;
34242
+ }
34243
+ if (ancestorDirectory === containmentRoot) return null;
34244
+ }
34245
+ return null;
34246
+ };
34182
34247
  const toReactWorkspacePackages = (directories) => {
34183
34248
  const packages = [];
34184
34249
  for (const directory of directories) {
@@ -34251,14 +34316,13 @@ const clearProjectCache = () => {
34251
34316
  };
34252
34317
  /**
34253
34318
  * Build a `ProjectInfo` for a directory that has no `package.json` of
34254
- * its own — a package subfolder like `repo/packages` or `app/src/features`,
34255
- * or any loose tree of TypeScript/JavaScript files. Dependency + framework
34256
- * detection is inherited from the nearest enclosing package (a leaf
34257
- * workspace, a plain app root, or a monorepo root — whichever is closest,
34258
- * bounded by the git root), so scanning a subdirectory of a React project
34259
- * still gets the React capabilities; a standalone non-React directory simply
34260
- * scans with the framework-agnostic rules. Throws only when the directory has
34261
- * nothing to scan (no enclosing project and no source files of its own).
34319
+ * its own — a monorepo subfolder like `repo/packages`, or any loose tree
34320
+ * of TypeScript/JavaScript files. Dependency + framework detection is
34321
+ * inherited from the enclosing workspace root when there is one, so
34322
+ * scanning a subdirectory of a React monorepo still gets the React
34323
+ * capabilities; a standalone non-React directory simply scans with the
34324
+ * framework-agnostic rules. Throws only when the directory has nothing
34325
+ * to scan (no enclosing project and no source files of its own).
34262
34326
  */
34263
34327
  const discoverProjectWithoutPackageJson = (directory) => {
34264
34328
  const sourceFileCount = countSourceFiles(directory);
@@ -34296,6 +34360,7 @@ const discoverProjectWithoutPackageJson = (directory) => {
34296
34360
  hasReanimated: false,
34297
34361
  reanimatedVersion: null,
34298
34362
  isPreES2023Target: hasOwnTsConfig && detectPreES2023Target(directory),
34363
+ isStaticExport: false,
34299
34364
  sourceFileCount
34300
34365
  };
34301
34366
  };
@@ -34309,90 +34374,85 @@ const discoverProject = (directory) => {
34309
34374
  return synthesized;
34310
34375
  }
34311
34376
  const packageJson = readPackageJson(packageJsonPath);
34312
- let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
34313
- const reactDeclaration = getDependencyDeclaration({
34314
- packageJson,
34315
- packageName: "react",
34316
- sections: [
34317
- "dependencies",
34318
- "peerDependencies",
34319
- "devDependencies"
34320
- ]
34321
- });
34322
- const tailwindDeclaration = getDependencyDeclaration({
34323
- packageJson,
34324
- packageName: "tailwindcss",
34325
- sections: [
34326
- "dependencies",
34327
- "devDependencies",
34328
- "peerDependencies"
34329
- ]
34330
- });
34331
- const zodDeclaration = getDependencyDeclaration({
34377
+ const rootInfo = extractDependencyInfo(packageJson);
34378
+ let framework = rootInfo.framework;
34379
+ const tracked = {
34380
+ react: {
34381
+ version: rootInfo.reactVersion,
34382
+ sections: REACT_SECTIONS
34383
+ },
34384
+ tailwindcss: {
34385
+ version: rootInfo.tailwindVersion,
34386
+ sections: TAILWIND_ZOD_SECTIONS
34387
+ },
34388
+ zod: {
34389
+ version: rootInfo.zodVersion,
34390
+ sections: TAILWIND_ZOD_SECTIONS
34391
+ }
34392
+ };
34393
+ const declarations = Object.fromEntries(Object.entries(tracked).map(([packageName, entry]) => [packageName, getDependencyDeclaration({
34332
34394
  packageJson,
34333
- packageName: "zod",
34334
- sections: [
34335
- "dependencies",
34336
- "devDependencies",
34337
- "peerDependencies"
34338
- ]
34339
- });
34340
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
34341
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
34342
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
34343
- if (!reactVersion || !tailwindVersion || !zodVersion) {
34395
+ packageName,
34396
+ sections: entry.sections
34397
+ })]));
34398
+ const fillFromCatalogs = (source, sourceDirectory) => {
34399
+ for (const [packageName, entry] of Object.entries(tracked)) if (!entry.version && declarations[packageName].hasDeclaration) entry.version = resolveCatalogVersion(source, packageName, sourceDirectory, declarations[packageName].catalogReference);
34400
+ };
34401
+ fillFromCatalogs(packageJson, directory);
34402
+ if (!tracked.react.version || !tracked.tailwindcss.version || !tracked.zod.version) {
34344
34403
  const monorepoRoot = findMonorepoRoot(directory);
34345
34404
  if (monorepoRoot) {
34346
34405
  const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
34347
- if (isFile(monorepoPackageJsonPath)) {
34348
- const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
34349
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
34350
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
34351
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
34352
- }
34406
+ if (isFile(monorepoPackageJsonPath)) fillFromCatalogs(readPackageJson(monorepoPackageJsonPath), monorepoRoot);
34353
34407
  }
34354
34408
  }
34355
- if (!reactVersion || framework === "unknown") {
34356
- const workspaceInfo = findReactInWorkspaces(directory, packageJson);
34357
- if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
34358
- if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
34359
- if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
34360
- if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
34409
+ const shouldCollectReactGroup = !tracked.react.version || framework === "unknown";
34410
+ const workspaceFacts = collectWorkspaceFacts(directory, packageJson, { collectReactGroup: shouldCollectReactGroup });
34411
+ if (shouldCollectReactGroup) {
34412
+ tracked.react.version ||= workspaceFacts.reactVersion;
34413
+ tracked.tailwindcss.version ||= workspaceFacts.tailwindVersion;
34414
+ tracked.zod.version ||= workspaceFacts.zodVersion;
34415
+ if (framework === "unknown" && workspaceFacts.framework !== "unknown") framework = workspaceFacts.framework;
34361
34416
  }
34362
- if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
34417
+ if ((!tracked.react.version || framework === "unknown") && !isMonorepoRoot(directory)) {
34363
34418
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
34364
- if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
34365
- if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
34366
- if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
34419
+ tracked.react.version ||= monorepoInfo.reactVersion;
34420
+ tracked.tailwindcss.version ||= monorepoInfo.tailwindVersion;
34421
+ tracked.zod.version ||= monorepoInfo.zodVersion;
34367
34422
  if (framework === "unknown") framework = monorepoInfo.framework;
34368
34423
  }
34369
- if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
34370
- if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
34371
- if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
34424
+ for (const [packageName, entry] of Object.entries(tracked)) {
34425
+ const declaredVersion = declarations[packageName].version;
34426
+ if (!entry.version && declaredVersion && !isCatalogReference(declaredVersion)) entry.version = declaredVersion;
34427
+ }
34428
+ const { react, tailwindcss, zod } = tracked;
34429
+ let reactVersion = react.version;
34372
34430
  if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
34431
+ const tailwindVersion = tailwindcss.version;
34432
+ const zodVersion = zod.version;
34373
34433
  const projectName = packageJson.name ?? Path.basename(directory);
34374
34434
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
34375
34435
  const sourceFileCount = countSourceFiles(directory);
34376
- const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
34436
+ const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
34377
34437
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
34378
34438
  rootDirectory: directory,
34379
34439
  rootPackageJson: packageJson,
34380
34440
  packageName: "expo",
34381
- version: findExpoVersion(directory, packageJson)
34441
+ version: workspaceFacts.expo.version
34382
34442
  }) : null;
34383
34443
  const shopifyFlashListVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
34384
34444
  rootDirectory: directory,
34385
34445
  rootPackageJson: packageJson,
34386
34446
  packageName: SHOPIFY_FLASH_LIST_PACKAGE_NAME,
34387
- version: findShopifyFlashListVersion(directory, packageJson)
34447
+ version: workspaceFacts.shopifyFlashList.version
34388
34448
  }) : null;
34389
- const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
34390
- const reanimatedVersion = hasReanimated ? findInWorkspacePackageJsons(directory, packageJson, (workspacePackageJson) => getDependencySpec(workspacePackageJson, REANIMATED_DEPENDENCY_NAME)) : null;
34449
+ const hasReanimated = hasReactNativeWorkspace && workspaceFacts.hasReanimatedAwarePackage;
34450
+ const reanimatedVersion = hasReanimated ? workspaceFacts.reanimatedVersion : null;
34391
34451
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
34392
34452
  rootDirectory: directory,
34393
34453
  rootPackageJson: packageJson,
34394
34454
  packageName: "next",
34395
- version: findNextjsVersion(directory, packageJson)
34455
+ version: workspaceFacts.next.version
34396
34456
  }) : null;
34397
34457
  const preactVersion = getPreactVersion(packageJson);
34398
34458
  const isPreES2023Target = hasTypeScript && detectPreES2023Target(directory);
@@ -34403,7 +34463,7 @@ const discoverProject = (directory) => {
34403
34463
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
34404
34464
  tailwindVersion,
34405
34465
  zodVersion,
34406
- zodMajorVersion: parseZodMajor(zodVersion),
34466
+ zodMajorVersion: zodVersion === null ? null : getLowestDependencyMajor(zodVersion),
34407
34467
  framework,
34408
34468
  hasTypeScript,
34409
34469
  hasReactCompiler: detectReactCompiler(directory, packageJson),
@@ -34419,6 +34479,7 @@ const discoverProject = (directory) => {
34419
34479
  hasReanimated,
34420
34480
  reanimatedVersion,
34421
34481
  isPreES2023Target,
34482
+ isStaticExport: framework === "nextjs" && detectNextjsStaticExport(workspaceFacts.next.sourceDirectory ?? directory),
34422
34483
  sourceFileCount
34423
34484
  };
34424
34485
  cachedProjectInfos.set(directory, projectInfo);
@@ -34434,57 +34495,6 @@ const discoverProject = (directory) => {
34434
34495
  * single source file has nothing to analyze.
34435
34496
  */
34436
34497
  const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null || project.sourceFileCount > 0;
34437
- const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
34438
- const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
34439
- const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
34440
- const parseReactMajorMinor = (reactVersion) => {
34441
- if (typeof reactVersion !== "string") return null;
34442
- const trimmed = reactVersion.trim();
34443
- if (trimmed.length === 0) return null;
34444
- const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
34445
- if (lowerBoundsOnly.length === 0) return null;
34446
- const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
34447
- if (majorMinorMatch) {
34448
- const major = Number.parseInt(majorMinorMatch[1], 10);
34449
- const minor = Number.parseInt(majorMinorMatch[2], 10);
34450
- if (!Number.isFinite(major) || major <= 0) return null;
34451
- if (!Number.isFinite(minor) || minor < 0) return null;
34452
- return {
34453
- major,
34454
- minor
34455
- };
34456
- }
34457
- const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
34458
- if (!majorOnlyMatch) return null;
34459
- const major = Number.parseInt(majorOnlyMatch[1], 10);
34460
- if (!Number.isFinite(major) || major <= 0) return null;
34461
- return {
34462
- major,
34463
- minor: 0
34464
- };
34465
- };
34466
- const isReactAtLeast = (detected, required) => {
34467
- if (detected === null) return true;
34468
- if (detected.major !== required.major) return detected.major > required.major;
34469
- return detected.minor >= required.minor;
34470
- };
34471
- const parseLowerBoundVersion = (versionSpec) => import_semver.validRange(versionSpec) !== null ? import_semver.minVersion(versionSpec) : import_semver.coerce(versionSpec);
34472
- const parseTailwindMajorMinor = (tailwindVersion) => {
34473
- if (typeof tailwindVersion !== "string") return null;
34474
- const trimmed = tailwindVersion.trim();
34475
- if (trimmed.length === 0) return null;
34476
- const lowerBound = parseLowerBoundVersion(trimmed);
34477
- if (lowerBound === null || lowerBound.major <= 0) return null;
34478
- return {
34479
- major: lowerBound.major,
34480
- minor: lowerBound.minor
34481
- };
34482
- };
34483
- const isTailwindAtLeast = (detected, required) => {
34484
- if (detected === null) return true;
34485
- if (detected.major !== required.major) return detected.major > required.major;
34486
- return detected.minor >= required.minor;
34487
- };
34488
34498
  const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
34489
34499
  var InvalidGlobPatternError = class extends Error {
34490
34500
  pattern;
@@ -37186,64 +37196,64 @@ function* collectSecurityScanFiles(rootDirectory) {
37186
37196
  }
37187
37197
  }
37188
37198
  }
37189
- /**
37190
- * Whether project discovery resolved a React-compatible runtime (React or
37191
- * Preact, which ships the same hooks + JSX model). This is the same
37192
- * predicate that grants the `react` capability, so `false` means every
37193
- * React-runtime rule family was gated off for the project.
37194
- */
37195
- const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
37199
+ const CLIENT_ONLY_FRAMEWORKS = new Set([
37200
+ "vite",
37201
+ "cra",
37202
+ "gatsby",
37203
+ "react-native",
37204
+ "expo"
37205
+ ]);
37206
+ const addMajorLadder = (capabilities, name, major, earliest, latest) => {
37207
+ if (major === null) return;
37208
+ const cappedMajor = Math.min(major, latest);
37209
+ for (let candidate = earliest; candidate <= cappedMajor; candidate += 1) capabilities.add(`${name}:${candidate}`);
37210
+ };
37196
37211
  const buildCapabilities = (project) => {
37197
37212
  const capabilities = /* @__PURE__ */ new Set();
37198
37213
  capabilities.add(project.framework);
37199
- if (hasReactRuntime(project)) capabilities.add("react");
37214
+ if (project.reactVersion !== null || project.preactVersion !== null) capabilities.add("react");
37200
37215
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
37201
37216
  if (project.expoVersion !== null) capabilities.add("expo");
37217
+ if ((project.framework === "nextjs" || project.framework === "tanstack-start" || project.framework === "remix") && !project.isStaticExport) capabilities.add("server-actions");
37218
+ if (CLIENT_ONLY_FRAMEWORKS.has(project.framework)) capabilities.add("client-only");
37219
+ if (project.isStaticExport) capabilities.add("nextjs:static-export");
37202
37220
  if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
37203
- const reactMajor = project.reactMajorVersion;
37204
- if (reactMajor !== null) {
37205
- const cappedReactMajor = Math.min(reactMajor, 30);
37206
- for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
37207
- if (reactMajor >= 19) {
37208
- if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
37209
- major: 19,
37210
- minor: 2
37211
- })) capabilities.add("react:19.2");
37212
- }
37213
- }
37214
- if (project.tailwindVersion !== null) {
37215
- capabilities.add("tailwind");
37216
- if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
37217
- major: 3,
37218
- minor: 4
37219
- })) capabilities.add("tailwind:3.4");
37220
- }
37221
- if (project.zodVersion !== null) {
37222
- capabilities.add("zod");
37223
- if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
37224
- }
37221
+ addMajorLadder(capabilities, "react", project.reactMajorVersion, 17, 30);
37222
+ if (project.reactMajorVersion !== null && project.reactMajorVersion >= 19 && isMajorMinorAtLeast(parseReactMajorMinor(project.reactVersion), {
37223
+ major: 19,
37224
+ minor: 2
37225
+ })) capabilities.add("react:19.2");
37226
+ if (project.tailwindVersion !== null) capabilities.add("tailwind");
37227
+ if (project.tailwindVersion !== null && isMajorMinorAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
37228
+ major: 3,
37229
+ minor: 4
37230
+ })) capabilities.add("tailwind:3.4");
37231
+ if (project.zodVersion !== null) capabilities.add("zod");
37232
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
37225
37233
  if (project.isPreES2023Target) capabilities.add("pre-es2023");
37226
37234
  if (project.hasReactCompiler) capabilities.add("react-compiler");
37227
37235
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
37228
37236
  if (project.hasTypeScript) capabilities.add("typescript");
37229
- if (project.preactVersion !== null) {
37230
- capabilities.add("preact");
37231
- const preactMajor = project.preactMajorVersion;
37232
- if (preactMajor !== null) {
37233
- const cappedPreactMajor = Math.min(preactMajor, 20);
37234
- for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
37235
- }
37236
- if (project.reactVersion === null) capabilities.add("pure-preact");
37237
- }
37237
+ if (project.preactVersion !== null) capabilities.add("preact");
37238
+ addMajorLadder(capabilities, "preact", project.preactMajorVersion, 10, 20);
37239
+ if (project.preactVersion !== null && project.reactVersion === null) capabilities.add("pure-preact");
37238
37240
  return capabilities;
37239
37241
  };
37240
- const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
37242
+ const capabilitiesByProject = /* @__PURE__ */ new WeakMap();
37243
+ const getCapabilities = (project) => {
37244
+ const cached = capabilitiesByProject.get(project);
37245
+ if (cached !== void 0) return cached;
37246
+ const capabilities = buildCapabilities(project);
37247
+ capabilitiesByProject.set(project, capabilities);
37248
+ return capabilities;
37249
+ };
37250
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhen) => {
37241
37251
  if (requires) {
37242
37252
  for (const capability of requires) if (!capabilities.has(capability)) return false;
37243
37253
  }
37244
37254
  if (tags?.includes("react-jsx-only") && !capabilities.has("react")) return false;
37245
- if (disabledBy) {
37246
- for (const capability of disabledBy) if (capabilities.has(capability)) return false;
37255
+ if (disabledWhen) {
37256
+ for (const capability of disabledWhen) if (capabilities.has(capability)) return false;
37247
37257
  }
37248
37258
  if (tags) {
37249
37259
  for (const tag of tags) if (ignoredTags.has(tag)) return false;
@@ -37254,14 +37264,14 @@ const yieldToEventLoop = () => new Promise((resolve) => {
37254
37264
  setImmediate(resolve);
37255
37265
  });
37256
37266
  const createSecurityScanSession = (rootDirectory, options) => {
37257
- const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
37267
+ const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
37258
37268
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
37259
37269
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
37260
37270
  const rule = entry.rule;
37261
37271
  const scan = rule.scan;
37262
37272
  if (typeof scan !== "function") return [];
37263
37273
  if (rule.defaultEnabled === false) return [];
37264
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) return [];
37274
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) return [];
37265
37275
  return [{
37266
37276
  entry,
37267
37277
  scan,
@@ -37281,6 +37291,7 @@ const createSecurityScanSession = (rootDirectory, options) => {
37281
37291
  return status === true;
37282
37292
  };
37283
37293
  const scanFileByRule = function* (file) {
37294
+ if (file === null) return;
37284
37295
  for (const { entry, scan, committedFilesOnly } of enabledScanRules) {
37285
37296
  for (const finding of scan(file)) {
37286
37297
  if (committedFilesOnly && isFileGitIgnored(file)) continue;
@@ -39161,7 +39172,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
39161
39172
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
39162
39173
  const jsPlugins = [];
39163
39174
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
39164
- const capabilities = buildCapabilities(project);
39175
+ const capabilities = getCapabilities(project);
39165
39176
  const enabledReactDoctorRules = {};
39166
39177
  for (const registryEntry of REACT_DOCTOR_RULES) {
39167
39178
  const rule = reactDoctorPlugin.rules[registryEntry.id];
@@ -39172,7 +39183,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
39172
39183
  if (rule.scan !== void 0) continue;
39173
39184
  if (customRulesOnly && registryEntry.originallyExternal) continue;
39174
39185
  if (rule.framework !== "global" && !rule.requires) continue;
39175
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
39186
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) continue;
39176
39187
  const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
39177
39188
  if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
39178
39189
  const severity = resolveRuleSeverityOverride({
@@ -39203,6 +39214,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
39203
39214
  settings: { "react-doctor": {
39204
39215
  framework: project.framework,
39205
39216
  rootDirectory: resolveSettingsRootDirectory(project.rootDirectory),
39217
+ capabilities: [...capabilities].sort(),
39206
39218
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
39207
39219
  ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
39208
39220
  } },
@@ -39493,21 +39505,6 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39493
39505
  if (errors.length > 0) throw errors[0];
39494
39506
  return results;
39495
39507
  };
39496
- const getPublicEnvPrefix = (framework) => {
39497
- switch (framework) {
39498
- case "nextjs": return "NEXT_PUBLIC_*";
39499
- case "vite":
39500
- case "tanstack-start": return "VITE_*";
39501
- case "cra": return "REACT_APP_*";
39502
- case "gatsby": return "GATSBY_*";
39503
- default: return null;
39504
- }
39505
- };
39506
- const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
39507
- const publicEnvPrefix = getPublicEnvPrefix(project.framework);
39508
- if (!publicEnvPrefix) return fallbackRecommendation;
39509
- return `Move secrets to server-only code. In ${formatFrameworkName(project.framework)}, only \`${publicEnvPrefix}\` env vars are exposed to the browser, and they must not contain secrets`;
39510
- };
39511
39508
  const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
39512
39509
  const ACCESSORS_MIN_VERSION = "3.15.0";
39513
39510
  const supportsAccessors = (reanimatedVersion) => {
@@ -40188,8 +40185,14 @@ const PLUGIN_CATEGORY_MAP = {
40188
40185
  };
40189
40186
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
40190
40187
  const getRuleRecommendation = (ruleName, project) => {
40191
- if (ruleName === "no-secrets-in-client-code") return buildNoSecretsRecommendation(project, reactDoctorPlugin.rules["no-secrets-in-client-code"]?.recommendation ?? "Move secrets to server-only code");
40192
- return reactDoctorPlugin.rules[ruleName]?.recommendation;
40188
+ const rule = reactDoctorPlugin.rules[ruleName];
40189
+ if (!rule) return void 0;
40190
+ if (rule.recommendationFor) {
40191
+ const capabilities = getCapabilities(project);
40192
+ const conditionalRecommendation = rule.recommendationFor((capability) => capabilities.has(capability));
40193
+ if (conditionalRecommendation !== void 0) return conditionalRecommendation;
40194
+ }
40195
+ return rule.recommendation;
40193
40196
  };
40194
40197
  const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
40195
40198
  const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
@@ -40343,7 +40346,9 @@ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
40343
40346
  * - `OxlintBatchExceeded { kind: "output-too-large" }` — stdout+stderr
40344
40347
  * crossed `OXLINT_OUTPUT_MAX_BYTES`.
40345
40348
  * - `OxlintBatchExceeded { kind: "oom" | "killed" }` — child exited
40346
- * on a signal (SIGABRT → OOM, others → generic kill).
40349
+ * on a signal (SIGABRT → OOM, others → generic kill), or with one
40350
+ * of the abort exit codes Windows reports instead of a signal
40351
+ * (`ABORT_EXIT_CODES` → OOM).
40347
40352
  * - `OxlintSpawnFailed { cause }` — `spawn` itself errored, or the
40348
40353
  * child exited successfully but printed only stderr.
40349
40354
  *
@@ -40409,7 +40414,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
40409
40414
  clearAbortListener();
40410
40415
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
40411
40416
  });
40412
- child.on("close", (_code, signal) => {
40417
+ child.on("close", (code, signal) => {
40413
40418
  clearTimeout(timeoutHandle);
40414
40419
  clearAbortListener();
40415
40420
  if (didKillForSize) {
@@ -40419,10 +40424,11 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
40419
40424
  }) }));
40420
40425
  return;
40421
40426
  }
40422
- if (signal) {
40427
+ const isAbortExitCode = code !== null && ABORT_EXIT_CODES.has(code);
40428
+ if (signal || isAbortExitCode) {
40423
40429
  const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
40424
- const isOom = signal === "SIGABRT";
40425
- const detailParts = [`killed by ${signal}`];
40430
+ const isOom = signal === "SIGABRT" || isAbortExitCode;
40431
+ const detailParts = [signal ? `killed by ${signal}` : `aborted with exit code ${code}`];
40426
40432
  if (isOom) detailParts.push("try scanning fewer files with --diff");
40427
40433
  if (stderrOutput) detailParts.push(stderrOutput);
40428
40434
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
@@ -40465,12 +40471,19 @@ const isParallelismRelatedSpawnError = (error) => {
40465
40471
  * (surfaced via `onPartialFailure`) so JSON-mode consumers see WHICH
40466
40472
  * files were skipped instead of silently losing them.
40467
40473
  *
40468
- * Parallel runs (concurrency > 1) get one extra safety net: if the pass
40469
- * fails with a resource-exhaustion error that's exclusive to running
40470
- * many oxlint subprocesses at once (EAGAIN / EMFILE / ENFILE / ENOMEM —
40471
- * see `isParallelismRelatedSpawnError`), the whole pass replays once
40472
- * with a single worker. That's the only failure a serial replay can
40473
- * clear, so every other error class is left to propagate.
40474
+ * Parallel runs (concurrency > 1) get two extra safety nets:
40475
+ * - If the pass fails with a resource-exhaustion error that's exclusive
40476
+ * to running many oxlint subprocesses at once (EAGAIN / EMFILE /
40477
+ * ENFILE / ENOMEM — see `isParallelismRelatedSpawnError`), the whole
40478
+ * pass replays once with a single worker. That's the only failure a
40479
+ * serial replay can clear, so every other error class is left to
40480
+ * propagate.
40481
+ * - Files dropped because oxlint's native binding SIGABRT'd under
40482
+ * memory pressure (`OxlintBatchExceeded { kind: "oom" }`) get one
40483
+ * serial single-file-batch rescue pass: the OOM is usually a function
40484
+ * of N concurrent oxlint allocator arenas, not the file itself, so a
40485
+ * lone process with the machine to itself typically clears it —
40486
+ * turning a partial scan into a complete (if slower) one.
40474
40487
  *
40475
40488
  * Errors that aren't splittable and aren't parallelism-related (oxlint
40476
40489
  * config crash, JS plugin resolution failure, etc.) propagate to the
@@ -40480,13 +40493,16 @@ const isParallelismRelatedSpawnError = (error) => {
40480
40493
  const spawnLintBatches = async (input) => {
40481
40494
  const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
40482
40495
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
40483
- const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
40484
- const runBatchPass = async (concurrency) => {
40496
+ let rescueDeadlineEpochMs;
40497
+ const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0 || rescueDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(rescueDeadlineEpochMs) === 0;
40498
+ const runBatchPass = async (concurrency, passBatches, passOnFileProgress) => {
40499
+ const totalFileCount = passBatches.reduce((sum, batch) => sum + batch.length, 0);
40485
40500
  const allDiagnostics = [];
40486
40501
  const droppedFiles = [];
40502
+ const oomDroppedFiles = [];
40487
40503
  const deadlineSkippedFiles = [];
40488
- const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0;
40489
40504
  let firstDropReason = null;
40505
+ let firstNonOomDropReason = null;
40490
40506
  const spawnLintBatch = async (batch, depth, batchState) => {
40491
40507
  if (isPastDeadline()) {
40492
40508
  deadlineSkippedFiles.push(...batch);
@@ -40508,12 +40524,14 @@ const spawnLintBatches = async (input) => {
40508
40524
  const isDepthCapReached = depth >= splitMaxDepth;
40509
40525
  if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
40510
40526
  droppedFiles.push(...batch);
40511
- if (firstDropReason === null) {
40512
- let limitHint = "";
40513
- if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
40514
- else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
40515
- firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
40516
- }
40527
+ const isOomDrop = error.reason._tag === "OxlintBatchExceeded" && error.reason.kind === "oom";
40528
+ if (isOomDrop) oomDroppedFiles.push(...batch);
40529
+ let limitHint = "";
40530
+ if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
40531
+ else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
40532
+ const dropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
40533
+ firstDropReason ??= dropReason;
40534
+ if (!isOomDrop) firstNonOomDropReason ??= dropReason;
40517
40535
  return [];
40518
40536
  }
40519
40537
  const splitIndex = Math.ceil(batch.length / 2);
@@ -40523,16 +40541,16 @@ const spawnLintBatches = async (input) => {
40523
40541
  let startedFileCount = 0;
40524
40542
  let scannedFileCount = 0;
40525
40543
  let displayedFileCount = 0;
40526
- const progressTimer = onFileProgress && totalFileCount > 1 ? setInterval(() => {
40544
+ const progressTimer = passOnFileProgress && totalFileCount > 1 ? setInterval(() => {
40527
40545
  const ceiling = Math.min(startedFileCount, totalFileCount - 1);
40528
40546
  if (displayedFileCount < ceiling) {
40529
40547
  displayedFileCount += 1;
40530
- onFileProgress(displayedFileCount, totalFileCount);
40548
+ passOnFileProgress(displayedFileCount, totalFileCount);
40531
40549
  }
40532
40550
  }, 50) : null;
40533
40551
  progressTimer?.unref?.();
40534
40552
  try {
40535
- const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
40553
+ const batchResults = await mapWithConcurrency(passBatches, concurrency, async (batch) => {
40536
40554
  if (isPastDeadline()) {
40537
40555
  deadlineSkippedFiles.push(...batch);
40538
40556
  return [];
@@ -40544,9 +40562,9 @@ const spawnLintBatches = async (input) => {
40544
40562
  };
40545
40563
  const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
40546
40564
  scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
40547
- if (onFileProgress) {
40565
+ if (passOnFileProgress) {
40548
40566
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
40549
- onFileProgress(displayedFileCount, totalFileCount);
40567
+ passOnFileProgress(displayedFileCount, totalFileCount);
40550
40568
  }
40551
40569
  return batchDiagnostics;
40552
40570
  });
@@ -40554,22 +40572,53 @@ const spawnLintBatches = async (input) => {
40554
40572
  } finally {
40555
40573
  if (progressTimer !== null) clearInterval(progressTimer);
40556
40574
  }
40557
- const reportSkippedFiles = (skippedFiles, buildMessage) => {
40558
- if (skippedFiles.length === 0 || !onPartialFailure) return;
40559
- onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
40575
+ return {
40576
+ diagnostics: allDiagnostics,
40577
+ droppedFiles,
40578
+ oomDroppedFiles,
40579
+ deadlineSkippedFiles,
40580
+ firstDropReason,
40581
+ firstNonOomDropReason
40560
40582
  };
40561
- const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
40562
- reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
40563
- reportSkippedFiles(deadlineSkippedFiles, (fileListPreview) => `${deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
40564
- return allDiagnostics;
40565
40583
  };
40566
- let diagnostics;
40584
+ let outcome;
40585
+ let effectiveConcurrency = requestedConcurrency;
40567
40586
  try {
40568
- diagnostics = await runBatchPass(requestedConcurrency);
40587
+ outcome = await runBatchPass(requestedConcurrency, fileBatches, onFileProgress);
40569
40588
  } catch (error) {
40570
40589
  if (requestedConcurrency <= 1 || !isParallelismRelatedSpawnError(error)) throw error;
40571
- diagnostics = await runBatchPass(1);
40590
+ effectiveConcurrency = 1;
40591
+ outcome = await runBatchPass(1, fileBatches, onFileProgress);
40592
+ }
40593
+ let { droppedFiles, firstDropReason } = outcome;
40594
+ const diagnostics = outcome.diagnostics;
40595
+ if (outcome.oomDroppedFiles.length > 0 && effectiveConcurrency > 1 && !isPastDeadline()) {
40596
+ rescueDeadlineEpochMs = Date.now() + OXLINT_OOM_RESCUE_BUDGET_MS;
40597
+ const rescuedFiles = /* @__PURE__ */ new Set();
40598
+ let firstRescueFailureReason = null;
40599
+ for (const oomDroppedFile of outcome.oomDroppedFiles) {
40600
+ if (isPastDeadline()) break;
40601
+ try {
40602
+ const rescueOutcome = await runBatchPass(1, [[oomDroppedFile]], void 0);
40603
+ diagnostics.push(...rescueOutcome.diagnostics);
40604
+ if (rescueOutcome.droppedFiles.length > 0 || rescueOutcome.deadlineSkippedFiles.length > 0) firstRescueFailureReason ??= rescueOutcome.firstDropReason;
40605
+ else rescuedFiles.add(oomDroppedFile);
40606
+ } catch (error) {
40607
+ firstRescueFailureReason ??= error instanceof Error ? error.message : String(error);
40608
+ }
40609
+ }
40610
+ rescueDeadlineEpochMs = void 0;
40611
+ droppedFiles = droppedFiles.filter((filePath) => !rescuedFiles.has(filePath));
40612
+ if (droppedFiles.length === 0) firstDropReason = null;
40613
+ else firstDropReason = firstRescueFailureReason ?? outcome.firstNonOomDropReason ?? firstDropReason;
40572
40614
  }
40615
+ const reportSkippedFiles = (skippedFiles, buildMessage) => {
40616
+ if (skippedFiles.length === 0 || !onPartialFailure) return;
40617
+ onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
40618
+ };
40619
+ const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
40620
+ reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
40621
+ reportSkippedFiles(outcome.deadlineSkippedFiles, (fileListPreview) => `${outcome.deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
40573
40622
  return dedupeDiagnostics(diagnostics);
40574
40623
  };
40575
40624
  let didValidate = false;
@@ -42563,7 +42612,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
42563
42612
  /** Display name used in client-facing messages and progress titles. */
42564
42613
  const SERVER_DISPLAY_NAME = "React Doctor";
42565
42614
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
42566
- const SERVER_VERSION = "0.7.2";
42615
+ const SERVER_VERSION = "0.7.3";
42567
42616
  /** `Diagnostic.source` shown next to every published diagnostic. */
42568
42617
  const DIAGNOSTIC_SOURCE = "react-doctor";
42569
42618
  /**
@@ -44614,5 +44663,5 @@ const startLanguageServer = () => {
44614
44663
  };
44615
44664
  //#endregion
44616
44665
  export { startLanguageServer };
44617
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="30e226f8-fb2c-5784-b48c-95e8d218d035")}catch(e){}}();
44618
- //# debugId=30e226f8-fb2c-5784-b48c-95e8d218d035
44666
+ !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]="6105fd41-b8bf-5c49-b0bd-8979e737f179")}catch(e){}}();
44667
+ //# debugId=6105fd41-b8bf-5c49-b0bd-8979e737f179