react-doctor 0.7.1 → 0.7.2-dev.08b768b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lsp.js CHANGED
@@ -8,7 +8,7 @@ import path from "node:path";
8
8
  import * as NodeChildProcess from "node:child_process";
9
9
  import { execFileSync, spawn, spawnSync } from "node:child_process";
10
10
  import * as ts from "typescript";
11
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
11
+ import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
12
  import { parseJSON5 } from "confbox";
13
13
  import * as NodeUrl from "node:url";
14
14
  import { fileURLToPath } from "node:url";
@@ -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,10 +32853,24 @@ 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;
32870
+ const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
32839
32871
  const MINIFIED_SNIFF_BYTES = 65536;
32840
32872
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
32873
+ const SCANNED_DOT_DIRECTORIES = new Set([".dumi", ".storybook"]);
32841
32874
  const IGNORED_DIRECTORIES = new Set([
32842
32875
  ".angular",
32843
32876
  ".astro",
@@ -32956,6 +32989,8 @@ const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
32956
32989
  const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
32957
32990
  const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
32958
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;
32959
32994
  const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
32960
32995
  const LINT_PHASE_TIMEOUT_MS = 3e5;
32961
32996
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
@@ -33002,7 +33037,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33002
33037
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33003
33038
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33004
33039
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
33005
- const CORE_PACKAGE_VERSION = "0.7.1";
33040
+ const CORE_PACKAGE_VERSION = "0.7.2";
33006
33041
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
33007
33042
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
33008
33043
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -33012,8 +33047,9 @@ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
33012
33047
  const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
33013
33048
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
33014
33049
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
33015
- const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
33016
- const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath);
33050
+ const isIgnoredDirectoryName = (directoryName) => IGNORED_DIRECTORIES.has(directoryName) || directoryName.startsWith(".") && !SCANNED_DOT_DIRECTORIES.has(directoryName);
33051
+ const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => isIgnoredDirectoryName(segment));
33052
+ const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath) && !GENERATED_SOURCE_DIRECTORY_PATTERN.test(filePath);
33017
33053
  const isMinifiedSource = (absolutePath) => {
33018
33054
  let fileDescriptor;
33019
33055
  try {
@@ -33050,25 +33086,6 @@ const isLargeMinifiedFile = (absolutePath, knownSizeBytes) => {
33050
33086
  cachedIsLargeMinifiedByPath.set(absolutePath, result);
33051
33087
  return result;
33052
33088
  };
33053
- const isErrnoException = (error) => error instanceof Error && "code" in error;
33054
- const IGNORABLE_READDIR_ERROR_CODES = new Set([
33055
- "EACCES",
33056
- "EPERM",
33057
- "ENOENT",
33058
- "ENOTDIR",
33059
- "EINVAL",
33060
- "ELOOP",
33061
- "ENAMETOOLONG"
33062
- ]);
33063
- const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
33064
- const readDirectoryEntries = (directoryPath) => {
33065
- try {
33066
- return NFS.readdirSync(directoryPath, { withFileTypes: true });
33067
- } catch (error) {
33068
- if (isIgnorableReaddirError(error)) return [];
33069
- throw error;
33070
- }
33071
- };
33072
33089
  function* walkSourceTreeFiles(rootDirectory) {
33073
33090
  const stack = [rootDirectory];
33074
33091
  while (stack.length > 0) {
@@ -33077,7 +33094,7 @@ function* walkSourceTreeFiles(rootDirectory) {
33077
33094
  for (const entry of readDirectoryEntries(currentDirectory)) {
33078
33095
  const absolutePath = Path.join(currentDirectory, entry.name);
33079
33096
  if (entry.isDirectory()) {
33080
- if (!IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
33097
+ if (!isIgnoredDirectoryName(entry.name)) stack.push(absolutePath);
33081
33098
  continue;
33082
33099
  }
33083
33100
  if (entry.isFile()) yield {
@@ -33174,6 +33191,146 @@ const findMonorepoRoot = (startDirectory) => {
33174
33191
  * byte-equivalent copy.
33175
33192
  */
33176
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
+ };
33177
33334
  const REACT_COMPILER_PACKAGES = new Set([
33178
33335
  "babel-plugin-react-compiler",
33179
33336
  "react-compiler-runtime",
@@ -33210,6 +33367,7 @@ const EXPO_APP_CONFIG_FILENAMES = [
33210
33367
  ];
33211
33368
  const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
33212
33369
  const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
33370
+ const STATIC_EXPORT_OUTPUT_PATTERN = /(?:^|[^.\w])["']?output["']?\s*:\s*["']export["']/m;
33213
33371
  const hasCompilerPackage = (packageJson) => {
33214
33372
  const allDependencies = {
33215
33373
  ...packageJson.peerDependencies,
@@ -33242,34 +33400,10 @@ const detectReactCompiler = (directory, packageJson) => {
33242
33400
  }
33243
33401
  return false;
33244
33402
  };
33245
- const FRAMEWORK_PACKAGES = {
33246
- next: "nextjs",
33247
- "@tanstack/react-start": "tanstack-start",
33248
- vite: "vite",
33249
- "react-scripts": "cra",
33250
- "@remix-run/react": "remix",
33251
- gatsby: "gatsby",
33252
- expo: "expo",
33253
- "react-native": "react-native"
33254
- };
33255
- const FRAMEWORK_DISPLAY_NAMES = {
33256
- nextjs: "Next.js",
33257
- "tanstack-start": "TanStack Start",
33258
- vite: "Vite",
33259
- cra: "Create React App",
33260
- remix: "Remix",
33261
- gatsby: "Gatsby",
33262
- expo: "Expo",
33263
- "react-native": "React Native",
33264
- preact: "Preact",
33265
- unknown: "React"
33266
- };
33267
- const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
33268
- const detectFramework = (dependencies) => {
33269
- for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
33270
- if (dependencies.preact && !dependencies.react) return "preact";
33271
- return "unknown";
33272
- };
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
+ });
33273
33407
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
33274
33408
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
33275
33409
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
@@ -33407,10 +33541,76 @@ const isConcreteDependencyVersion = (version) => {
33407
33541
  const normalizedVersion = normalizeDependencyVersion(version);
33408
33542
  return normalizedVersion !== null && /\d/.test(normalizedVersion);
33409
33543
  };
33410
- const isPlainObject = (value) => {
33411
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
33412
- const prototype = Object.getPrototypeOf(value);
33413
- return prototype === null || prototype === Object.prototype;
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;
33548
+ };
33549
+ const parseReactMajor = (reactVersion) => {
33550
+ if (typeof reactVersion !== "string") return null;
33551
+ return getLowestDependencyMajor(reactVersion);
33552
+ };
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;
33414
33614
  };
33415
33615
  const isCatalogReference = (version) => version.startsWith("catalog:");
33416
33616
  const extractCatalogName = (version) => {
@@ -33552,6 +33752,16 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
33552
33752
  }
33553
33753
  return null;
33554
33754
  };
33755
+ const REACT_SECTIONS = [
33756
+ "dependencies",
33757
+ "peerDependencies",
33758
+ "devDependencies"
33759
+ ];
33760
+ const TAILWIND_ZOD_SECTIONS = [
33761
+ "dependencies",
33762
+ "devDependencies",
33763
+ "peerDependencies"
33764
+ ];
33555
33765
  const EMPTY_DEPENDENCY_INFO = {
33556
33766
  reactVersion: null,
33557
33767
  tailwindVersion: null,
@@ -33592,6 +33802,10 @@ const extractDependencyInfo = (packageJson) => {
33592
33802
  framework: detectFramework(allDependencies)
33593
33803
  };
33594
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
+ };
33595
33809
  const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
33596
33810
  for (const section of sections) {
33597
33811
  const version = packageJson[section]?.[packageName];
@@ -33608,222 +33822,91 @@ const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
33608
33822
  version: null
33609
33823
  };
33610
33824
  };
33611
- const isDirectory = (directoryPath) => {
33612
- try {
33613
- return NFS.statSync(directoryPath).isDirectory();
33614
- } catch {
33615
- return false;
33616
- }
33617
- };
33618
- const NX_PROJECT_DISCOVERY_DIRS = [
33619
- "apps",
33620
- "libs",
33621
- "packages"
33622
- ];
33623
- const getNxWorkspaceDirectories = (rootDirectory) => {
33624
- if (!isFile(Path.join(rootDirectory, "nx.json"))) return [];
33625
- const collected = [];
33626
- for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
33627
- const candidatePath = Path.join(rootDirectory, candidate);
33628
- if (!isDirectory(candidatePath)) continue;
33629
- for (const entry of readDirectoryEntries(candidatePath)) {
33630
- if (!entry.isDirectory()) continue;
33631
- const projectDirectory = Path.join(candidatePath, entry.name);
33632
- if (isFile(Path.join(projectDirectory, "project.json")) || isFile(Path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
33633
- }
33634
- }
33635
- return collected;
33636
- };
33637
- const parsePnpmWorkspacePatterns = (rootDirectory) => {
33638
- const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
33639
- if (!isFile(workspacePath)) return [];
33640
- const content = NFS.readFileSync(workspacePath, "utf-8");
33641
- const patterns = [];
33642
- let isInsidePackagesBlock = false;
33643
- for (const line of content.split("\n")) {
33644
- const trimmed = line.trim();
33645
- if (trimmed === "packages:") {
33646
- isInsidePackagesBlock = true;
33647
- continue;
33648
- }
33649
- if (isInsidePackagesBlock && trimmed.startsWith("-")) patterns.push(trimmed.replace(/^-\s*/, "").replace(/["']/g, ""));
33650
- else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith("#")) isInsidePackagesBlock = false;
33651
- }
33652
- return patterns;
33653
- };
33654
- const getWorkspacePatterns = (rootDirectory, packageJson) => {
33655
- const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
33656
- if (pnpmPatterns.length > 0) return pnpmPatterns;
33657
- if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
33658
- if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
33659
- const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
33660
- if (nxPatterns.length > 0) return nxPatterns;
33661
- return [];
33662
- };
33663
- const parseReactMajor = (reactVersion) => {
33664
- if (typeof reactVersion !== "string") return null;
33665
- return getLowestDependencyMajor(reactVersion);
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));
33666
33838
  };
33667
- const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
33668
- const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
33669
- if (!cleanPattern.includes("*")) {
33670
- const directoryPath = Path.join(rootDirectory, cleanPattern);
33671
- if (isDirectory(directoryPath) && isFile(Path.join(directoryPath, "package.json"))) return [directoryPath];
33672
- return [];
33673
- }
33674
- const wildcardIndex = cleanPattern.indexOf("*");
33675
- const baseDirectory = Path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
33676
- const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
33677
- if (!isDirectory(baseDirectory)) return [];
33678
- const resolved = [];
33679
- for (const entry of readDirectoryEntries(baseDirectory)) {
33680
- const entryPath = Path.join(baseDirectory, entry.name, suffixAfterWildcard);
33681
- if (isDirectory(entryPath) && isFile(Path.join(entryPath, "package.json"))) resolved.push(entryPath);
33682
- }
33683
- return resolved;
33839
+ const getPreactVersion = (packageJson) => {
33840
+ return {
33841
+ ...packageJson.peerDependencies,
33842
+ ...packageJson.dependencies,
33843
+ ...packageJson.devDependencies
33844
+ }.preact ?? null;
33684
33845
  };
33685
- const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
33686
- const dependencyDeclaration = getDependencyDeclaration({
33687
- packageJson: workspacePackageJson,
33688
- packageName,
33689
- sections
33690
- });
33691
- if (!dependencyDeclaration.hasDeclaration) return null;
33692
- return concreteVersion ?? resolveCatalogVersion(workspacePackageJson, packageName, workspaceDirectory, dependencyDeclaration.catalogReference) ?? resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, dependencyDeclaration.catalogReference);
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));
33693
33858
  };
33694
- const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
33695
- if (!currentVersion) return true;
33696
- const currentMajor = parseReactMajor(currentVersion);
33697
- const nextMajor = parseReactMajor(nextVersion);
33698
- if (currentMajor === null) return nextMajor !== null;
33699
- if (nextMajor === null) return false;
33700
- return nextMajor < currentMajor;
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;
33701
33869
  };
33702
- const findReactInWorkspaces = (rootDirectory, packageJson) => {
33703
- const patterns = getWorkspacePatterns(rootDirectory, packageJson);
33704
- const result = { ...EMPTY_DEPENDENCY_INFO };
33705
- for (const pattern of patterns) {
33706
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
33707
- for (const workspaceDirectory of directories) {
33708
- const workspacePackageJson = readPackageJson(Path.join(workspaceDirectory, "package.json"));
33709
- const info = extractDependencyInfo(workspacePackageJson);
33710
- const reactVersion = resolveWorkspaceDependencyVersion({
33711
- concreteVersion: info.reactVersion,
33712
- packageName: "react",
33713
- rootDirectory,
33714
- rootPackageJson: packageJson,
33715
- sections: [
33716
- "dependencies",
33717
- "peerDependencies",
33718
- "devDependencies"
33719
- ],
33720
- workspaceDirectory,
33721
- workspacePackageJson
33722
- });
33723
- const tailwindVersion = resolveWorkspaceDependencyVersion({
33724
- concreteVersion: info.tailwindVersion,
33725
- packageName: "tailwindcss",
33726
- rootDirectory,
33727
- rootPackageJson: packageJson,
33728
- sections: [
33729
- "dependencies",
33730
- "devDependencies",
33731
- "peerDependencies"
33732
- ],
33733
- workspaceDirectory,
33734
- workspacePackageJson
33735
- });
33736
- const zodVersion = resolveWorkspaceDependencyVersion({
33737
- concreteVersion: info.zodVersion,
33738
- packageName: "zod",
33739
- rootDirectory,
33740
- rootPackageJson: packageJson,
33741
- sections: [
33742
- "dependencies",
33743
- "devDependencies",
33744
- "peerDependencies"
33745
- ],
33746
- workspaceDirectory,
33747
- workspacePackageJson
33748
- });
33749
- if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
33750
- if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
33751
- if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
33752
- if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
33753
- const resultReactMajor = parseReactMajor(result.reactVersion);
33754
- if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
33755
- }
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;
33756
33885
  }
33757
- return result;
33758
33886
  };
33759
- const findDependencyInfoFromMonorepoRoot = (directory) => {
33760
- const monorepoRoot = findMonorepoRoot(directory);
33761
- if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
33762
- const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
33763
- if (!isFile(monorepoPackageJsonPath)) return EMPTY_DEPENDENCY_INFO;
33764
- const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
33765
- const rootInfo = extractDependencyInfo(rootPackageJson);
33766
- const leafPackageJsonPath = Path.join(directory, "package.json");
33767
- const leafPackageJson = isFile(leafPackageJsonPath) ? readPackageJson(leafPackageJsonPath) : null;
33768
- const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
33769
- packageJson: leafPackageJson,
33770
- packageName: "react",
33771
- sections: [
33772
- "dependencies",
33773
- "peerDependencies",
33774
- "devDependencies"
33775
- ]
33776
- }) : null;
33777
- const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
33778
- packageJson: leafPackageJson,
33779
- packageName: "tailwindcss",
33780
- sections: [
33781
- "dependencies",
33782
- "devDependencies",
33783
- "peerDependencies"
33784
- ]
33785
- }) : null;
33786
- const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
33787
- packageJson: leafPackageJson,
33788
- packageName: "zod",
33789
- sections: [
33790
- "dependencies",
33791
- "devDependencies",
33792
- "peerDependencies"
33793
- ]
33794
- }) : null;
33795
- const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
33796
- const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
33797
- const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
33798
- const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
33799
- const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
33800
- const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
33801
- const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
33802
- return {
33803
- reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
33804
- tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
33805
- zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
33806
- framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
33807
- };
33808
- };
33809
- const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
33810
- const rootValue = select(rootPackageJson);
33811
- if (rootValue !== null) return rootValue;
33812
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
33813
- if (patterns.length === 0) return null;
33814
- const visitedDirectories = /* @__PURE__ */ new Set();
33815
- for (const pattern of patterns) {
33816
- const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
33817
- for (const workspaceDirectory of directories) {
33818
- if (visitedDirectories.has(workspaceDirectory)) continue;
33819
- visitedDirectories.add(workspaceDirectory);
33820
- const value = select(readPackageJson(Path.join(workspaceDirectory, "package.json")));
33821
- if (value !== null) return value;
33822
- }
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;
33823
33907
  }
33824
33908
  return null;
33825
33909
  };
33826
- const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => predicate(packageJson) ? true : null) !== null;
33827
33910
  const NAMES = new Set([
33828
33911
  "react-native",
33829
33912
  "react-native-tvos",
@@ -33856,47 +33939,7 @@ const isPackageJsonReactNativeAware = (packageJson) => {
33856
33939
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
33857
33940
  return false;
33858
33941
  };
33859
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
33860
- const getDependencySpec = (packageJson, packageName) => {
33861
- const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
33862
- return typeof spec === "string" ? spec : null;
33863
- };
33864
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
33865
- const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
33866
- const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
33867
- const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
33868
- if (version === null || !isCatalogReference(version)) return version;
33869
- const catalogName = extractCatalogName(version);
33870
- const resolvedLocalVersion = resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, catalogName);
33871
- if (resolvedLocalVersion) return resolvedLocalVersion;
33872
- const monorepoRoot = findMonorepoRoot(rootDirectory);
33873
- if (!monorepoRoot) return version;
33874
- const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
33875
- if (!isFile(monorepoPackageJsonPath)) return version;
33876
- return resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
33877
- };
33878
- const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
33879
- const getPreactVersion = (packageJson) => {
33880
- return {
33881
- ...packageJson.peerDependencies,
33882
- ...packageJson.dependencies,
33883
- ...packageJson.devDependencies
33884
- }.preact ?? null;
33885
- };
33886
- const TANSTACK_QUERY_PACKAGES = new Set([
33887
- "@tanstack/react-query",
33888
- "@tanstack/query-core",
33889
- "react-query"
33890
- ]);
33891
- const hasTanStackQuery = (packageJson) => {
33892
- const allDependencies = {
33893
- ...packageJson.peerDependencies,
33894
- ...packageJson.dependencies,
33895
- ...packageJson.devDependencies
33896
- };
33897
- return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
33898
- };
33899
- const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
33942
+ const REANIMATED_DEPENDENCY_NAME$1 = "react-native-reanimated";
33900
33943
  const isPackageJsonReanimatedAware = (packageJson) => {
33901
33944
  const allDependencies = {
33902
33945
  ...packageJson.peerDependencies,
@@ -33904,144 +33947,70 @@ const isPackageJsonReanimatedAware = (packageJson) => {
33904
33947
  ...packageJson.devDependencies,
33905
33948
  ...packageJson.optionalDependencies
33906
33949
  };
33907
- return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
33908
- };
33909
- const parseZodMajor = (zodVersion) => {
33910
- if (typeof zodVersion !== "string") return null;
33911
- return getLowestDependencyMajor(zodVersion);
33950
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME$1);
33912
33951
  };
33913
- const hasUpperBoundOnlyPeerRange = (range) => {
33914
- if (typeof range !== "string") return false;
33915
- const normalizedRange = normalizeDependencyVersion(range);
33916
- if (normalizedRange === null) return false;
33917
- return splitDependencyVersionBranches(normalizedRange).some((branch) => {
33918
- const normalizedBranch = normalizeDependencyVersion(branch);
33919
- return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
33920
- });
33921
- };
33922
- const peerRangeMinMajor = (range) => {
33923
- if (typeof range !== "string") return null;
33924
- return getLowestDependencyMajor(range);
33925
- };
33926
- const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
33927
- const installedReactMajor = parseReactMajor(reactVersion);
33928
- const peerReactRange = packageJson.peerDependencies?.react;
33929
- if (typeof peerReactRange !== "string") return installedReactMajor;
33930
- const peerFloor = peerRangeMinMajor(peerReactRange);
33931
- if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
33932
- return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
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 [];
33933
33960
  };
33934
- const TSCONFIG_FILENAME = "tsconfig.json";
33935
- const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
33936
- const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
33937
- const resolvePackageExtendsPath = (extendsValue, fromConfigDirectory) => {
33938
- const requireFromConfig = createRequire(Path.join(fromConfigDirectory, "tsconfig.json"));
33939
- const candidates = [
33940
- extendsValue,
33941
- ensureJsonExtension(extendsValue),
33942
- `${extendsValue.replace(/\/$/, "")}/tsconfig.json`
33943
- ];
33944
- for (const candidate of candidates) try {
33945
- return requireFromConfig.resolve(candidate);
33946
- } catch {
33947
- continue;
33961
+ const parsePnpmWorkspacePatterns = (rootDirectory) => {
33962
+ const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
33963
+ if (!isFile(workspacePath)) return [];
33964
+ const content = NFS.readFileSync(workspacePath, "utf-8");
33965
+ const patterns = [];
33966
+ let isInsidePackagesBlock = false;
33967
+ for (const line of content.split("\n")) {
33968
+ const trimmed = line.trim();
33969
+ if (trimmed === "packages:") {
33970
+ isInsidePackagesBlock = true;
33971
+ continue;
33972
+ }
33973
+ if (isInsidePackagesBlock && trimmed.startsWith("-")) patterns.push(trimmed.replace(/^-\s*/, "").replace(/["']/g, ""));
33974
+ else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith("#")) isInsidePackagesBlock = false;
33948
33975
  }
33949
- return null;
33950
- };
33951
- const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
33952
- if (isRelativeExtendsValue(extendsValue)) return ensureJsonExtension(Path.resolve(fromConfigDirectory, extendsValue));
33953
- return resolvePackageExtendsPath(extendsValue, fromConfigDirectory);
33954
- };
33955
- const normalizeCompilerOptions = (compilerOptions) => {
33956
- if (!isPlainObject(compilerOptions)) return { hasExplicitLib: false };
33957
- const target = typeof compilerOptions.target === "string" ? compilerOptions.target : void 0;
33958
- const hasExplicitLib = Object.hasOwn(compilerOptions, "lib");
33959
- return {
33960
- target,
33961
- lib: Array.isArray(compilerOptions.lib) ? compilerOptions.lib.filter((entry) => typeof entry === "string") : void 0,
33962
- hasExplicitLib
33963
- };
33976
+ return patterns;
33964
33977
  };
33965
- const readTsConfig = (filePath) => {
33966
- let content;
33967
- try {
33968
- content = NFS.readFileSync(filePath, "utf-8");
33969
- } catch {
33970
- return null;
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
+ }
33971
33994
  }
33972
- const parsed = ts.parseConfigFileTextToJson(filePath, content);
33973
- if (!isPlainObject(parsed.config)) return null;
33974
- return {
33975
- extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
33976
- compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
33977
- };
33978
- };
33979
- const mergeCompilerOptions = (inherited, current) => {
33980
- const target = current.target ?? inherited?.target;
33981
- const hasExplicitLib = current.hasExplicitLib || Boolean(inherited?.hasExplicitLib);
33982
- return {
33983
- target,
33984
- lib: current.hasExplicitLib ? current.lib : inherited?.lib,
33985
- hasExplicitLib
33986
- };
33995
+ return collected;
33987
33996
  };
33988
- const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
33989
- const realPath = NFS.realpathSync.native(tsConfigPath);
33990
- if (visitedPaths.has(realPath)) return null;
33991
- const tsConfig = readTsConfig(realPath);
33992
- if (!tsConfig) return null;
33993
- const nextVisitedPaths = new Set(visitedPaths);
33994
- nextVisitedPaths.add(realPath);
33995
- if (tsConfig.extends && extendsDepth < 8) {
33996
- const parentPath = resolveExtendsPath(tsConfig.extends, Path.dirname(realPath));
33997
- if (parentPath && isFile(parentPath)) return mergeCompilerOptions(readResolvedCompilerOptions(parentPath, extendsDepth + 1, nextVisitedPaths), tsConfig.compilerOptions);
33997
+ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
33998
+ const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
33999
+ if (!cleanPattern.includes("*")) {
34000
+ const directoryPath = Path.join(rootDirectory, cleanPattern);
34001
+ if (isDirectory(directoryPath) && isFile(Path.join(directoryPath, "package.json"))) return [directoryPath];
34002
+ return [];
33998
34003
  }
33999
- return tsConfig.compilerOptions;
34000
- };
34001
- const targetYearIsPreES2023 = (target) => {
34002
- const year = ES_TARGET_YEAR_BY_NAME[target.toLowerCase()];
34003
- return year !== void 0 && year < 2023;
34004
- };
34005
- const libEntryIncludesES2023Array = (entry) => {
34006
- const normalizedEntry = entry.toLowerCase();
34007
- if (normalizedEntry === "esnext" || normalizedEntry === "esnext.array") return true;
34008
- const esYearMatch = /^es(\d{4})(?:\.(.+))?$/.exec(normalizedEntry);
34009
- if (!esYearMatch) return false;
34010
- if (Number(esYearMatch[1]) < 2023) return false;
34011
- const component = esYearMatch[2];
34012
- return component === void 0 || component === "array";
34013
- };
34014
- const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
34015
- const compilerOptionsArePreES2023 = (compilerOptions) => {
34016
- if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
34017
- if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
34018
- return false;
34019
- };
34020
- const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
34021
- const detectPreES2023FromConfig = (tsConfigPath) => {
34022
- const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
34023
- if (!compilerOptions) return false;
34024
- if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) return false;
34025
- return compilerOptionsArePreES2023(compilerOptions);
34026
- };
34027
- const detectPreES2023Target = (directory) => {
34028
- const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
34029
- if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
34030
- return false;
34031
- };
34032
- const REACT_DEPENDENCY_NAMES = new Set([
34033
- "react",
34034
- "react-native",
34035
- "next",
34036
- "preact"
34037
- ]);
34038
- const hasReactDependency = (packageJson) => {
34039
- const allDependencies = {
34040
- ...packageJson.peerDependencies,
34041
- ...packageJson.dependencies,
34042
- ...packageJson.devDependencies
34043
- };
34044
- return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
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;
34045
34014
  };
34046
34015
  const listWorkspacePackages = (rootDirectory) => {
34047
34016
  const packageJsonPath = Path.join(rootDirectory, "package.json");
@@ -34073,6 +34042,208 @@ const listWorkspacePackages = (rootDirectory) => {
34073
34042
  }
34074
34043
  return packages;
34075
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
+ };
34076
34247
  const toReactWorkspacePackages = (directories) => {
34077
34248
  const packages = [];
34078
34249
  for (const directory of directories) {
@@ -34156,8 +34327,8 @@ const clearProjectCache = () => {
34156
34327
  const discoverProjectWithoutPackageJson = (directory) => {
34157
34328
  const sourceFileCount = countSourceFiles(directory);
34158
34329
  const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
34159
- const monorepoRoot = findMonorepoRoot(directory);
34160
- const enclosingProject = monorepoRoot !== null && isFile(Path.join(monorepoRoot, "package.json")) ? discoverProject(monorepoRoot) : null;
34330
+ const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
34331
+ const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
34161
34332
  if (enclosingProject !== null) return {
34162
34333
  ...enclosingProject,
34163
34334
  rootDirectory: directory,
@@ -34187,7 +34358,9 @@ const discoverProjectWithoutPackageJson = (directory) => {
34187
34358
  shopifyFlashListVersion: null,
34188
34359
  shopifyFlashListMajorVersion: null,
34189
34360
  hasReanimated: false,
34361
+ reanimatedVersion: null,
34190
34362
  isPreES2023Target: hasOwnTsConfig && detectPreES2023Target(directory),
34363
+ isStaticExport: false,
34191
34364
  sourceFileCount
34192
34365
  };
34193
34366
  };
@@ -34201,88 +34374,85 @@ const discoverProject = (directory) => {
34201
34374
  return synthesized;
34202
34375
  }
34203
34376
  const packageJson = readPackageJson(packageJsonPath);
34204
- let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
34205
- const reactDeclaration = getDependencyDeclaration({
34206
- packageJson,
34207
- packageName: "react",
34208
- sections: [
34209
- "dependencies",
34210
- "peerDependencies",
34211
- "devDependencies"
34212
- ]
34213
- });
34214
- const tailwindDeclaration = getDependencyDeclaration({
34215
- packageJson,
34216
- packageName: "tailwindcss",
34217
- sections: [
34218
- "dependencies",
34219
- "devDependencies",
34220
- "peerDependencies"
34221
- ]
34222
- });
34223
- 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({
34224
34394
  packageJson,
34225
- packageName: "zod",
34226
- sections: [
34227
- "dependencies",
34228
- "devDependencies",
34229
- "peerDependencies"
34230
- ]
34231
- });
34232
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
34233
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
34234
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
34235
- 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) {
34236
34403
  const monorepoRoot = findMonorepoRoot(directory);
34237
34404
  if (monorepoRoot) {
34238
34405
  const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
34239
- if (isFile(monorepoPackageJsonPath)) {
34240
- const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
34241
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
34242
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
34243
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
34244
- }
34406
+ if (isFile(monorepoPackageJsonPath)) fillFromCatalogs(readPackageJson(monorepoPackageJsonPath), monorepoRoot);
34245
34407
  }
34246
34408
  }
34247
- if (!reactVersion || framework === "unknown") {
34248
- const workspaceInfo = findReactInWorkspaces(directory, packageJson);
34249
- if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
34250
- if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
34251
- if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
34252
- 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;
34253
34416
  }
34254
- if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
34417
+ if ((!tracked.react.version || framework === "unknown") && !isMonorepoRoot(directory)) {
34255
34418
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
34256
- if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
34257
- if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
34258
- if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
34419
+ tracked.react.version ||= monorepoInfo.reactVersion;
34420
+ tracked.tailwindcss.version ||= monorepoInfo.tailwindVersion;
34421
+ tracked.zod.version ||= monorepoInfo.zodVersion;
34259
34422
  if (framework === "unknown") framework = monorepoInfo.framework;
34260
34423
  }
34261
- if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
34262
- if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
34263
- 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;
34430
+ if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
34431
+ const tailwindVersion = tailwindcss.version;
34432
+ const zodVersion = zod.version;
34264
34433
  const projectName = packageJson.name ?? Path.basename(directory);
34265
34434
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
34266
34435
  const sourceFileCount = countSourceFiles(directory);
34267
- const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
34436
+ const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
34268
34437
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
34269
34438
  rootDirectory: directory,
34270
34439
  rootPackageJson: packageJson,
34271
34440
  packageName: "expo",
34272
- version: findExpoVersion(directory, packageJson)
34441
+ version: workspaceFacts.expo.version
34273
34442
  }) : null;
34274
34443
  const shopifyFlashListVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
34275
34444
  rootDirectory: directory,
34276
34445
  rootPackageJson: packageJson,
34277
34446
  packageName: SHOPIFY_FLASH_LIST_PACKAGE_NAME,
34278
- version: findShopifyFlashListVersion(directory, packageJson)
34447
+ version: workspaceFacts.shopifyFlashList.version
34279
34448
  }) : null;
34280
- const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
34449
+ const hasReanimated = hasReactNativeWorkspace && workspaceFacts.hasReanimatedAwarePackage;
34450
+ const reanimatedVersion = hasReanimated ? workspaceFacts.reanimatedVersion : null;
34281
34451
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
34282
34452
  rootDirectory: directory,
34283
34453
  rootPackageJson: packageJson,
34284
34454
  packageName: "next",
34285
- version: findNextjsVersion(directory, packageJson)
34455
+ version: workspaceFacts.next.version
34286
34456
  }) : null;
34287
34457
  const preactVersion = getPreactVersion(packageJson);
34288
34458
  const isPreES2023Target = hasTypeScript && detectPreES2023Target(directory);
@@ -34293,7 +34463,7 @@ const discoverProject = (directory) => {
34293
34463
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
34294
34464
  tailwindVersion,
34295
34465
  zodVersion,
34296
- zodMajorVersion: parseZodMajor(zodVersion),
34466
+ zodMajorVersion: zodVersion === null ? null : getLowestDependencyMajor(zodVersion),
34297
34467
  framework,
34298
34468
  hasTypeScript,
34299
34469
  hasReactCompiler: detectReactCompiler(directory, packageJson),
@@ -34307,7 +34477,9 @@ const discoverProject = (directory) => {
34307
34477
  shopifyFlashListVersion,
34308
34478
  shopifyFlashListMajorVersion: shopifyFlashListVersion === null ? null : getLowestDependencyMajor(shopifyFlashListVersion),
34309
34479
  hasReanimated,
34480
+ reanimatedVersion,
34310
34481
  isPreES2023Target,
34482
+ isStaticExport: framework === "nextjs" && detectNextjsStaticExport(workspaceFacts.next.sourceDirectory ?? directory),
34311
34483
  sourceFileCount
34312
34484
  };
34313
34485
  cachedProjectInfos.set(directory, projectInfo);
@@ -34323,57 +34495,6 @@ const discoverProject = (directory) => {
34323
34495
  * single source file has nothing to analyze.
34324
34496
  */
34325
34497
  const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null || project.sourceFileCount > 0;
34326
- const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
34327
- const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
34328
- const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
34329
- const parseReactMajorMinor = (reactVersion) => {
34330
- if (typeof reactVersion !== "string") return null;
34331
- const trimmed = reactVersion.trim();
34332
- if (trimmed.length === 0) return null;
34333
- const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
34334
- if (lowerBoundsOnly.length === 0) return null;
34335
- const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
34336
- if (majorMinorMatch) {
34337
- const major = Number.parseInt(majorMinorMatch[1], 10);
34338
- const minor = Number.parseInt(majorMinorMatch[2], 10);
34339
- if (!Number.isFinite(major) || major <= 0) return null;
34340
- if (!Number.isFinite(minor) || minor < 0) return null;
34341
- return {
34342
- major,
34343
- minor
34344
- };
34345
- }
34346
- const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
34347
- if (!majorOnlyMatch) return null;
34348
- const major = Number.parseInt(majorOnlyMatch[1], 10);
34349
- if (!Number.isFinite(major) || major <= 0) return null;
34350
- return {
34351
- major,
34352
- minor: 0
34353
- };
34354
- };
34355
- const isReactAtLeast = (detected, required) => {
34356
- if (detected === null) return true;
34357
- if (detected.major !== required.major) return detected.major > required.major;
34358
- return detected.minor >= required.minor;
34359
- };
34360
- const parseLowerBoundVersion = (versionSpec) => import_semver.validRange(versionSpec) !== null ? import_semver.minVersion(versionSpec) : import_semver.coerce(versionSpec);
34361
- const parseTailwindMajorMinor = (tailwindVersion) => {
34362
- if (typeof tailwindVersion !== "string") return null;
34363
- const trimmed = tailwindVersion.trim();
34364
- if (trimmed.length === 0) return null;
34365
- const lowerBound = parseLowerBoundVersion(trimmed);
34366
- if (lowerBound === null || lowerBound.major <= 0) return null;
34367
- return {
34368
- major: lowerBound.major,
34369
- minor: lowerBound.minor
34370
- };
34371
- };
34372
- const isTailwindAtLeast = (detected, required) => {
34373
- if (detected === null) return true;
34374
- if (detected.major !== required.major) return detected.major > required.major;
34375
- return detected.minor >= required.minor;
34376
- };
34377
34498
  const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
34378
34499
  var InvalidGlobPatternError = class extends Error {
34379
34500
  pattern;
@@ -34499,6 +34620,9 @@ const buildRuleSeverityControls = (config) => {
34499
34620
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
34500
34621
  };
34501
34622
  };
34623
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34624
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34625
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34502
34626
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34503
34627
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
34504
34628
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -34576,6 +34700,7 @@ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34576
34700
  "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
34577
34701
  "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
34578
34702
  "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
34703
+ "react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
34579
34704
  "react/jsx-no-undef": "react-doctor/jsx-no-undef",
34580
34705
  "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
34581
34706
  "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
@@ -34649,9 +34774,6 @@ const tokenizeRuleList = (ruleList) => {
34649
34774
  if (!ruleSection) return [];
34650
34775
  return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
34651
34776
  };
34652
- const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34653
- const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34654
- const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34655
34777
  const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — add \`${ruleId}\` to the comment (e.g. \`${token}, ${ruleId}\`).`;
34656
34778
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34657
34779
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
@@ -34705,6 +34827,53 @@ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34705
34827
  if (!ruleId.startsWith("react-doctor/")) return null;
34706
34828
  return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
34707
34829
  };
34830
+ const ESLINT_REACT_HOOKS_PLUGIN_PREFIX = "react-hooks/";
34831
+ const buildAcceptedTokens = (ruleId) => {
34832
+ const shortRuleId = ruleId.slice(15);
34833
+ return new Set([ruleId, `${ESLINT_REACT_HOOKS_PLUGIN_PREFIX}${shortRuleId}`]);
34834
+ };
34835
+ const listNamesRule = (ruleList, acceptedTokens) => tokenizeRuleList(ruleList).some((token) => acceptedTokens.has(token));
34836
+ const isInlineDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
34837
+ const candidates = [{
34838
+ line: lines[diagnosticLineIndex],
34839
+ requiredScope: "line"
34840
+ }, {
34841
+ line: lines[diagnosticLineIndex - 1],
34842
+ requiredScope: "next-line"
34843
+ }];
34844
+ for (const { line, requiredScope } of candidates) {
34845
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
34846
+ if (!match) continue;
34847
+ const [, , scope, ruleList] = match;
34848
+ if (scope !== requiredScope) continue;
34849
+ if (listNamesRule(ruleList, acceptedTokens)) return true;
34850
+ }
34851
+ return false;
34852
+ };
34853
+ const isBlockDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
34854
+ let isRangeOpen = false;
34855
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
34856
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
34857
+ const line = lines[lineIndex];
34858
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
34859
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
34860
+ if (disableMatch && listNamesRule(disableMatch[2], acceptedTokens)) {
34861
+ isRangeOpen = true;
34862
+ continue;
34863
+ }
34864
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
34865
+ if (enableMatch) {
34866
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
34867
+ if (enabledRules.length === 0 || enabledRules.some((rule) => acceptedTokens.has(rule))) isRangeOpen = false;
34868
+ }
34869
+ }
34870
+ return isRangeOpen;
34871
+ };
34872
+ const isCompilerRuleForeignDisabled = (lines, diagnosticLineIndex, ruleId) => {
34873
+ if (!ruleId.startsWith("react-hooks-js/")) return false;
34874
+ const acceptedTokens = buildAcceptedTokens(ruleId);
34875
+ return isInlineDisabled(lines, diagnosticLineIndex, acceptedTokens) || isBlockDisabled(lines, diagnosticLineIndex, acceptedTokens);
34876
+ };
34708
34877
  const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34709
34878
  const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34710
34879
  const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
@@ -34848,6 +35017,11 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
34848
35017
  isSuppressed: true,
34849
35018
  nearMissHint: null
34850
35019
  };
35020
+ if (isCompilerRuleForeignDisabled(lines, diagnosticLineIndex, ruleId)) return {
35021
+ isSuppressed: true,
35022
+ nearMissHint: null,
35023
+ isForeignDirective: true
35024
+ };
34851
35025
  const directComments = findStackedDisableCommentsAbove(lines, diagnosticLineIndex);
34852
35026
  if (hasChainSuppressor(directComments, ruleId)) return {
34853
35027
  isSuppressed: true,
@@ -35129,12 +35303,18 @@ const collectStringSet = (values) => {
35129
35303
  return new Set(values.filter((value) => typeof value === "string"));
35130
35304
  };
35131
35305
  const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
35306
+ const NODE_CLI_PATH_SEGMENTS = new Set([
35307
+ "scripts",
35308
+ "bin",
35309
+ "tools"
35310
+ ]);
35311
+ const hasNodeCliPathSegment = (filePath) => filePath.split(/[\\/]/).slice(0, -1).some((segment) => NODE_CLI_PATH_SEGMENTS.has(segment));
35132
35312
  /**
35133
35313
  * Pre-compiles every stateful filter and returns a single
35134
35314
  * `apply(diagnostic)` closure that runs (in order):
35135
35315
  *
35136
35316
  * 1. auto-suppress (test-noise rules in test files; `migration-hint`
35137
- * wins over `test-noise`)
35317
+ * wins over `test-noise`; `no-console` on Node CLI scripts)
35138
35318
  * 2. severity overrides (top-level `rules` / `categories`, with
35139
35319
  * `"off"` dropping)
35140
35320
  * 3. warning suppression (only when `showWarnings` is false: drops every
@@ -35232,6 +35412,12 @@ const buildDiagnosticPipeline = (input) => {
35232
35412
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35233
35413
  return false;
35234
35414
  };
35415
+ const isNoConsoleOnNodeCliScript = (diagnostic) => {
35416
+ if (diagnostic.rule !== "no-console") return false;
35417
+ if (hasNodeCliPathSegment(diagnostic.filePath)) return true;
35418
+ const lines = getFileLines(diagnostic.filePath);
35419
+ return lines !== null && Boolean(lines[0]?.startsWith("#!"));
35420
+ };
35235
35421
  const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35236
35422
  if (!hasRuntimeGlobals) return false;
35237
35423
  if (diagnostic.rule !== "jsx-no-undef") return false;
@@ -35241,6 +35427,7 @@ const buildDiagnosticPipeline = (input) => {
35241
35427
  return {
35242
35428
  apply: (diagnostic) => {
35243
35429
  if (shouldAutoSuppress(diagnostic)) return null;
35430
+ if (isNoConsoleOnNodeCliScript(diagnostic)) return null;
35244
35431
  let current = diagnostic;
35245
35432
  let explicitSeverityOverride;
35246
35433
  let explicitRuleOverride;
@@ -35270,7 +35457,7 @@ const buildDiagnosticPipeline = (input) => {
35270
35457
  if (lines) {
35271
35458
  const ruleIdentifier = `${current.plugin}/${current.rule}`;
35272
35459
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35273
- if (evaluation.isSuppressed) return suppress(current, "inline");
35460
+ if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
35274
35461
  if (evaluation.nearMissHint) current = {
35275
35462
  ...current,
35276
35463
  suppressionHint: evaluation.nearMissHint
@@ -37009,64 +37196,64 @@ function* collectSecurityScanFiles(rootDirectory) {
37009
37196
  }
37010
37197
  }
37011
37198
  }
37012
- /**
37013
- * Whether project discovery resolved a React-compatible runtime (React or
37014
- * Preact, which ships the same hooks + JSX model). This is the same
37015
- * predicate that grants the `react` capability, so `false` means every
37016
- * React-runtime rule family was gated off for the project.
37017
- */
37018
- 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
+ };
37019
37211
  const buildCapabilities = (project) => {
37020
37212
  const capabilities = /* @__PURE__ */ new Set();
37021
37213
  capabilities.add(project.framework);
37022
- if (hasReactRuntime(project)) capabilities.add("react");
37214
+ if (project.reactVersion !== null || project.preactVersion !== null) capabilities.add("react");
37023
37215
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
37024
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");
37025
37220
  if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
37026
- const reactMajor = project.reactMajorVersion;
37027
- if (reactMajor !== null) {
37028
- const cappedReactMajor = Math.min(reactMajor, 30);
37029
- for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
37030
- if (reactMajor >= 19) {
37031
- if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
37032
- major: 19,
37033
- minor: 2
37034
- })) capabilities.add("react:19.2");
37035
- }
37036
- }
37037
- if (project.tailwindVersion !== null) {
37038
- capabilities.add("tailwind");
37039
- if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
37040
- major: 3,
37041
- minor: 4
37042
- })) capabilities.add("tailwind:3.4");
37043
- }
37044
- if (project.zodVersion !== null) {
37045
- capabilities.add("zod");
37046
- if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
37047
- }
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");
37048
37233
  if (project.isPreES2023Target) capabilities.add("pre-es2023");
37049
37234
  if (project.hasReactCompiler) capabilities.add("react-compiler");
37050
37235
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
37051
37236
  if (project.hasTypeScript) capabilities.add("typescript");
37052
- if (project.preactVersion !== null) {
37053
- capabilities.add("preact");
37054
- const preactMajor = project.preactMajorVersion;
37055
- if (preactMajor !== null) {
37056
- const cappedPreactMajor = Math.min(preactMajor, 20);
37057
- for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
37058
- }
37059
- if (project.reactVersion === null) capabilities.add("pure-preact");
37060
- }
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");
37240
+ return capabilities;
37241
+ };
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);
37061
37248
  return capabilities;
37062
37249
  };
37063
- const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
37250
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhen) => {
37064
37251
  if (requires) {
37065
37252
  for (const capability of requires) if (!capabilities.has(capability)) return false;
37066
37253
  }
37067
37254
  if (tags?.includes("react-jsx-only") && !capabilities.has("react")) return false;
37068
- if (disabledBy) {
37069
- 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;
37070
37257
  }
37071
37258
  if (tags) {
37072
37259
  for (const tag of tags) if (ignoredTags.has(tag)) return false;
@@ -37077,14 +37264,14 @@ const yieldToEventLoop = () => new Promise((resolve) => {
37077
37264
  setImmediate(resolve);
37078
37265
  });
37079
37266
  const createSecurityScanSession = (rootDirectory, options) => {
37080
- const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
37267
+ const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
37081
37268
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
37082
37269
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
37083
37270
  const rule = entry.rule;
37084
37271
  const scan = rule.scan;
37085
37272
  if (typeof scan !== "function") return [];
37086
37273
  if (rule.defaultEnabled === false) return [];
37087
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) return [];
37274
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) return [];
37088
37275
  return [{
37089
37276
  entry,
37090
37277
  scan,
@@ -37104,6 +37291,7 @@ const createSecurityScanSession = (rootDirectory, options) => {
37104
37291
  return status === true;
37105
37292
  };
37106
37293
  const scanFileByRule = function* (file) {
37294
+ if (file === null) return;
37107
37295
  for (const { entry, scan, committedFilesOnly } of enabledScanRules) {
37108
37296
  for (const finding of scan(file)) {
37109
37297
  if (committedFilesOnly && isFileGitIgnored(file)) continue;
@@ -38549,7 +38737,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38549
38737
  })),
38550
38738
  showStagedContent: (directory, relativePath, options) => runCommand({
38551
38739
  command: "git",
38552
- args: ["show", `:${relativePath}`],
38740
+ args: ["show", `:./${relativePath}`],
38553
38741
  directory,
38554
38742
  maxStdoutBytes: options?.maxBufferBytes
38555
38743
  }).pipe(map$3((result) => result.status === 0 ? result.stdout : null)),
@@ -38984,7 +39172,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38984
39172
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38985
39173
  const jsPlugins = [];
38986
39174
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
38987
- const capabilities = buildCapabilities(project);
39175
+ const capabilities = getCapabilities(project);
38988
39176
  const enabledReactDoctorRules = {};
38989
39177
  for (const registryEntry of REACT_DOCTOR_RULES) {
38990
39178
  const rule = reactDoctorPlugin.rules[registryEntry.id];
@@ -38995,7 +39183,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38995
39183
  if (rule.scan !== void 0) continue;
38996
39184
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38997
39185
  if (rule.framework !== "global" && !rule.requires) continue;
38998
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
39186
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) continue;
38999
39187
  const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
39000
39188
  if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
39001
39189
  const severity = resolveRuleSeverityOverride({
@@ -39026,6 +39214,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
39026
39214
  settings: { "react-doctor": {
39027
39215
  framework: project.framework,
39028
39216
  rootDirectory: resolveSettingsRootDirectory(project.rootDirectory),
39217
+ capabilities: [...capabilities].sort(),
39029
39218
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
39030
39219
  ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
39031
39220
  } },
@@ -39316,25 +39505,18 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39316
39505
  if (errors.length > 0) throw errors[0];
39317
39506
  return results;
39318
39507
  };
39319
- const getPublicEnvPrefix = (framework) => {
39320
- switch (framework) {
39321
- case "nextjs": return "NEXT_PUBLIC_*";
39322
- case "vite":
39323
- case "tanstack-start": return "VITE_*";
39324
- case "cra": return "REACT_APP_*";
39325
- case "gatsby": return "GATSBY_*";
39326
- default: return null;
39327
- }
39328
- };
39329
- const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
39330
- const publicEnvPrefix = getPublicEnvPrefix(project.framework);
39331
- if (!publicEnvPrefix) return fallbackRecommendation;
39332
- 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`;
39333
- };
39334
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";
39509
+ const ACCESSORS_MIN_VERSION = "3.15.0";
39510
+ const supportsAccessors = (reanimatedVersion) => {
39511
+ if (reanimatedVersion === null) return true;
39512
+ const lowerBound = import_semver.validRange(reanimatedVersion) !== null ? import_semver.minVersion(reanimatedVersion) : import_semver.coerce(reanimatedVersion);
39513
+ if (lowerBound === null) return true;
39514
+ return import_semver.gte(lowerBound, ACCESSORS_MIN_VERSION);
39515
+ };
39335
39516
  const appendReanimatedSharedValueHint = (help, rule, project) => {
39336
39517
  if (rule !== "immutability") return help;
39337
39518
  if (!project.hasReanimated) return help;
39519
+ if (!supportsAccessors(project.reanimatedVersion)) return help;
39338
39520
  if (!help) return REANIMATED_SHARED_VALUE_HINT;
39339
39521
  return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
39340
39522
  };
@@ -39496,13 +39678,13 @@ const REACT_USE_BINDING_RESOLUTION = {
39496
39678
  isReactUseBinding: true,
39497
39679
  isReactNamespaceBinding: false
39498
39680
  };
39499
- const getScriptKind = (filename) => {
39681
+ const getScriptKind$2 = (filename) => {
39500
39682
  if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
39501
39683
  if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
39502
39684
  if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
39503
39685
  return ts.ScriptKind.JS;
39504
39686
  };
39505
- const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39687
+ const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39506
39688
  const unwrapExpression = (expression) => {
39507
39689
  let currentExpression = expression;
39508
39690
  while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
@@ -39751,8 +39933,8 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
39751
39933
  return matchedIdentifier;
39752
39934
  };
39753
39935
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
39754
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filename));
39755
- const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset(sourceText, utf8Offset));
39936
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
39937
+ const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
39756
39938
  if (!useIdentifier) return null;
39757
39939
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
39758
39940
  };
@@ -39773,6 +39955,201 @@ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
39773
39955
  const bindingResolution = resolveUseCallBinding(sourceText, absolutePath, primaryLabel.span.offset);
39774
39956
  return bindingResolution !== null && !bindingResolution.isReactUseBinding;
39775
39957
  };
39958
+ const SUPPRESSED_COMPILER_CODES = new Set(["react-hooks-js(immutability)", "react-hooks-js(refs)"]);
39959
+ const WORKLET_DIRECTIVE = "worklet";
39960
+ const WORKLET_ACCEPTING_CALLEES = new Set([
39961
+ "useAnimatedStyle",
39962
+ "useAnimatedProps",
39963
+ "useDerivedValue",
39964
+ "useAnimatedReaction",
39965
+ "useAnimatedScrollHandler",
39966
+ "useAnimatedGestureHandler",
39967
+ "useFrameCallback",
39968
+ "runOnUI",
39969
+ "executeOnUIRuntimeSync"
39970
+ ]);
39971
+ const GESTURE_ROOT_IDENTIFIER = "Gesture";
39972
+ const getScriptKind$1 = (filename) => {
39973
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
39974
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
39975
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
39976
+ return ts.ScriptKind.JS;
39977
+ };
39978
+ const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39979
+ const hasWorkletDirective = (node) => {
39980
+ if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
39981
+ const body = node.body;
39982
+ if (!body || !ts.isBlock(body)) return false;
39983
+ for (const statement of body.statements) {
39984
+ if (!ts.isExpressionStatement(statement)) return false;
39985
+ if (!ts.isStringLiteral(statement.expression)) return false;
39986
+ if (statement.expression.text === WORKLET_DIRECTIVE) return true;
39987
+ }
39988
+ return false;
39989
+ };
39990
+ const rootIdentifierOfCallChain = (expression) => {
39991
+ let currentExpression = expression;
39992
+ while (true) {
39993
+ if (ts.isIdentifier(currentExpression)) return currentExpression.text;
39994
+ if (ts.isPropertyAccessExpression(currentExpression)) {
39995
+ currentExpression = currentExpression.expression;
39996
+ continue;
39997
+ }
39998
+ if (ts.isCallExpression(currentExpression)) {
39999
+ currentExpression = currentExpression.expression;
40000
+ continue;
40001
+ }
40002
+ return null;
40003
+ }
40004
+ };
40005
+ const isWorkletAcceptingCall = (callExpression) => {
40006
+ const callee = callExpression.expression;
40007
+ if (ts.isIdentifier(callee)) return WORKLET_ACCEPTING_CALLEES.has(callee.text);
40008
+ if (ts.isPropertyAccessExpression(callee)) {
40009
+ if (WORKLET_ACCEPTING_CALLEES.has(callee.name.text)) return true;
40010
+ return rootIdentifierOfCallChain(callee) === GESTURE_ROOT_IDENTIFIER;
40011
+ }
40012
+ return false;
40013
+ };
40014
+ const isFunctionWorklet = (functionNode) => {
40015
+ if (hasWorkletDirective(functionNode)) return true;
40016
+ const parent = functionNode.parent;
40017
+ return ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === functionNode) && isWorkletAcceptingCall(parent);
40018
+ };
40019
+ const findNodeAtOffset = (sourceFile, targetOffset) => {
40020
+ let matchedNode = null;
40021
+ const visit = (node) => {
40022
+ if (node.getStart(sourceFile) > targetOffset || node.getEnd() <= targetOffset) return;
40023
+ matchedNode = node;
40024
+ ts.forEachChild(node, visit);
40025
+ };
40026
+ visit(sourceFile);
40027
+ return matchedNode;
40028
+ };
40029
+ const isOffsetInsideWorklet = (sourceFile, targetOffset) => {
40030
+ let currentNode = findNodeAtOffset(sourceFile, targetOffset);
40031
+ while (currentNode) {
40032
+ if (ts.isFunctionLike(currentNode) && isFunctionWorklet(currentNode)) return true;
40033
+ currentNode = currentNode.parent;
40034
+ }
40035
+ return false;
40036
+ };
40037
+ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirectory) => {
40038
+ if (!SUPPRESSED_COMPILER_CODES.has(diagnostic.code)) return false;
40039
+ if (!project.hasReanimated) return false;
40040
+ const primaryLabel = diagnostic.labels[0];
40041
+ if (!primaryLabel) return false;
40042
+ const absolutePath = Path.isAbsolute(diagnostic.filename) ? diagnostic.filename : Path.join(rootDirectory, diagnostic.filename);
40043
+ let sourceText;
40044
+ try {
40045
+ sourceText = NFS.readFileSync(absolutePath, "utf-8");
40046
+ } catch {
40047
+ return false;
40048
+ }
40049
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
40050
+ };
40051
+ const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
40052
+ const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
40053
+ const REACT_COMPILER_PLUGIN = "react-hooks-js";
40054
+ const getScriptKind = (filename) => {
40055
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
40056
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
40057
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
40058
+ return ts.ScriptKind.JS;
40059
+ };
40060
+ const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
40061
+ const nodeLineRange = (sourceFile, node) => ({
40062
+ startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
40063
+ endLine: sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1
40064
+ });
40065
+ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40066
+ let foundRange = null;
40067
+ const visit = (node) => {
40068
+ if (foundRange) return;
40069
+ if (ts.isFunctionLike(node)) {
40070
+ const range = nodeLineRange(sourceFile, node);
40071
+ if (range.startLine <= targetLine && targetLine <= range.endLine) foundRange = range;
40072
+ return;
40073
+ }
40074
+ ts.forEachChild(node, visit);
40075
+ };
40076
+ visit(sourceFile);
40077
+ return foundRange;
40078
+ };
40079
+ const NEWLINE_BYTE = 10;
40080
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40081
+ let lineNumber = 1;
40082
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40083
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40084
+ return lineNumber;
40085
+ };
40086
+ /**
40087
+ * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40088
+ * whose enclosing function also carries a `react-hooks-js` (React
40089
+ * Compiler) diagnostic. Every react-hooks-js finding marks a function the
40090
+ * compiler could NOT compile (a bail-out / skip / preserve-manual-
40091
+ * memoization failure), so "React Compiler already caches every value
40092
+ * here" is false for that function and the manual memoization is
40093
+ * load-bearing, not dead weight.
40094
+ *
40095
+ * Matching is per enclosing function, not per file: a bailed-out
40096
+ * component must not silence redundant-memo findings in a sibling
40097
+ * component the compiler did optimize.
40098
+ *
40099
+ * Module-scope `memo(() => …)` wrappers have no enclosing function; for
40100
+ * those the memo call's own span is the containment range, so a bail-out
40101
+ * inside the inline-wrapped component still suppresses. Every failure
40102
+ * path (unreadable file, missing span) fails open and keeps the
40103
+ * diagnostic.
40104
+ */
40105
+ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) => {
40106
+ const compilerDiagnosticLinesByFile = /* @__PURE__ */ new Map();
40107
+ let hasManualMemoizationDiagnostic = false;
40108
+ for (const diagnostic of diagnostics) {
40109
+ if (isManualMemoizationDiagnostic(diagnostic)) {
40110
+ hasManualMemoizationDiagnostic = true;
40111
+ continue;
40112
+ }
40113
+ if (diagnostic.plugin !== REACT_COMPILER_PLUGIN || diagnostic.line <= 0) continue;
40114
+ const lines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
40115
+ if (lines) lines.push(diagnostic.line);
40116
+ else compilerDiagnosticLinesByFile.set(diagnostic.filePath, [diagnostic.line]);
40117
+ }
40118
+ if (!hasManualMemoizationDiagnostic || compilerDiagnosticLinesByFile.size === 0) return diagnostics;
40119
+ const parsedFileCache = /* @__PURE__ */ new Map();
40120
+ const readAndParse = (filePath) => {
40121
+ const cached = parsedFileCache.get(filePath);
40122
+ if (cached !== void 0) return cached;
40123
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
40124
+ let parsed;
40125
+ try {
40126
+ const buffer = NFS.readFileSync(absolutePath);
40127
+ parsed = {
40128
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
40129
+ buffer
40130
+ };
40131
+ } catch {
40132
+ parsed = null;
40133
+ }
40134
+ parsedFileCache.set(filePath, parsed);
40135
+ return parsed;
40136
+ };
40137
+ const isInsideBailedOutFunction = (diagnostic) => {
40138
+ const compilerLines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
40139
+ if (!compilerLines || diagnostic.line <= 0) return false;
40140
+ const parsed = readAndParse(diagnostic.filePath);
40141
+ if (!parsed) return false;
40142
+ let containmentRange = findOutermostEnclosingFunctionRange(parsed.sourceFile, diagnostic.line);
40143
+ if (!containmentRange && diagnostic.offset !== void 0 && diagnostic.length !== void 0) containmentRange = {
40144
+ startLine: diagnostic.line,
40145
+ endLine: lineOfUtf8Offset(parsed.buffer, diagnostic.offset + diagnostic.length)
40146
+ };
40147
+ if (!containmentRange) return false;
40148
+ const { startLine, endLine } = containmentRange;
40149
+ return compilerLines.some((compilerLine) => startLine <= compilerLine && compilerLine <= endLine);
40150
+ };
40151
+ return diagnostics.filter((diagnostic) => !isManualMemoizationDiagnostic(diagnostic) || !isInsideBailedOutFunction(diagnostic));
40152
+ };
39776
40153
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
39777
40154
  const LEADING_SEVERITY_LABEL_PATTERN = /^(?:Error|Warning):\s*/;
39778
40155
  const TRAILING_PERIOD_PATTERN = /\.$/;
@@ -39808,8 +40185,14 @@ const PLUGIN_CATEGORY_MAP = {
39808
40185
  };
39809
40186
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
39810
40187
  const getRuleRecommendation = (ruleName, project) => {
39811
- if (ruleName === "no-secrets-in-client-code") return buildNoSecretsRecommendation(project, reactDoctorPlugin.rules["no-secrets-in-client-code"]?.recommendation ?? "Move secrets to server-only code");
39812
- 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;
39813
40196
  };
39814
40197
  const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
39815
40198
  const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
@@ -39912,7 +40295,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
39912
40295
  minifiedFileCache.set(absolutePath, minified);
39913
40296
  return minified;
39914
40297
  };
39915
- return parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
40298
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
39916
40299
  const { plugin, rule } = parseRuleCode(diagnostic.code);
39917
40300
  const primaryLabel = diagnostic.labels[0];
39918
40301
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -39939,7 +40322,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
39939
40322
  ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
39940
40323
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
39941
40324
  };
39942
- });
40325
+ }), rootDirectory);
39943
40326
  };
39944
40327
  const buildOxlintChildEnv = (sourceEnv) => {
39945
40328
  const childEnv = {};
@@ -39963,7 +40346,9 @@ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
39963
40346
  * - `OxlintBatchExceeded { kind: "output-too-large" }` — stdout+stderr
39964
40347
  * crossed `OXLINT_OUTPUT_MAX_BYTES`.
39965
40348
  * - `OxlintBatchExceeded { kind: "oom" | "killed" }` — child exited
39966
- * 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).
39967
40352
  * - `OxlintSpawnFailed { cause }` — `spawn` itself errored, or the
39968
40353
  * child exited successfully but printed only stderr.
39969
40354
  *
@@ -40029,7 +40414,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
40029
40414
  clearAbortListener();
40030
40415
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
40031
40416
  });
40032
- child.on("close", (_code, signal) => {
40417
+ child.on("close", (code, signal) => {
40033
40418
  clearTimeout(timeoutHandle);
40034
40419
  clearAbortListener();
40035
40420
  if (didKillForSize) {
@@ -40039,10 +40424,11 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
40039
40424
  }) }));
40040
40425
  return;
40041
40426
  }
40042
- if (signal) {
40427
+ const isAbortExitCode = code !== null && ABORT_EXIT_CODES.has(code);
40428
+ if (signal || isAbortExitCode) {
40043
40429
  const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
40044
- const isOom = signal === "SIGABRT";
40045
- const detailParts = [`killed by ${signal}`];
40430
+ const isOom = signal === "SIGABRT" || isAbortExitCode;
40431
+ const detailParts = [signal ? `killed by ${signal}` : `aborted with exit code ${code}`];
40046
40432
  if (isOom) detailParts.push("try scanning fewer files with --diff");
40047
40433
  if (stderrOutput) detailParts.push(stderrOutput);
40048
40434
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
@@ -40085,12 +40471,19 @@ const isParallelismRelatedSpawnError = (error) => {
40085
40471
  * (surfaced via `onPartialFailure`) so JSON-mode consumers see WHICH
40086
40472
  * files were skipped instead of silently losing them.
40087
40473
  *
40088
- * Parallel runs (concurrency > 1) get one extra safety net: if the pass
40089
- * fails with a resource-exhaustion error that's exclusive to running
40090
- * many oxlint subprocesses at once (EAGAIN / EMFILE / ENFILE / ENOMEM —
40091
- * see `isParallelismRelatedSpawnError`), the whole pass replays once
40092
- * with a single worker. That's the only failure a serial replay can
40093
- * 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.
40094
40487
  *
40095
40488
  * Errors that aren't splittable and aren't parallelism-related (oxlint
40096
40489
  * config crash, JS plugin resolution failure, etc.) propagate to the
@@ -40100,13 +40493,16 @@ const isParallelismRelatedSpawnError = (error) => {
40100
40493
  const spawnLintBatches = async (input) => {
40101
40494
  const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
40102
40495
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
40103
- const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
40104
- 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);
40105
40500
  const allDiagnostics = [];
40106
40501
  const droppedFiles = [];
40502
+ const oomDroppedFiles = [];
40107
40503
  const deadlineSkippedFiles = [];
40108
- const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0;
40109
40504
  let firstDropReason = null;
40505
+ let firstNonOomDropReason = null;
40110
40506
  const spawnLintBatch = async (batch, depth, batchState) => {
40111
40507
  if (isPastDeadline()) {
40112
40508
  deadlineSkippedFiles.push(...batch);
@@ -40128,12 +40524,14 @@ const spawnLintBatches = async (input) => {
40128
40524
  const isDepthCapReached = depth >= splitMaxDepth;
40129
40525
  if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
40130
40526
  droppedFiles.push(...batch);
40131
- if (firstDropReason === null) {
40132
- let limitHint = "";
40133
- if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
40134
- else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
40135
- firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
40136
- }
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;
40137
40535
  return [];
40138
40536
  }
40139
40537
  const splitIndex = Math.ceil(batch.length / 2);
@@ -40143,16 +40541,16 @@ const spawnLintBatches = async (input) => {
40143
40541
  let startedFileCount = 0;
40144
40542
  let scannedFileCount = 0;
40145
40543
  let displayedFileCount = 0;
40146
- const progressTimer = onFileProgress && totalFileCount > 1 ? setInterval(() => {
40544
+ const progressTimer = passOnFileProgress && totalFileCount > 1 ? setInterval(() => {
40147
40545
  const ceiling = Math.min(startedFileCount, totalFileCount - 1);
40148
40546
  if (displayedFileCount < ceiling) {
40149
40547
  displayedFileCount += 1;
40150
- onFileProgress(displayedFileCount, totalFileCount);
40548
+ passOnFileProgress(displayedFileCount, totalFileCount);
40151
40549
  }
40152
40550
  }, 50) : null;
40153
40551
  progressTimer?.unref?.();
40154
40552
  try {
40155
- const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
40553
+ const batchResults = await mapWithConcurrency(passBatches, concurrency, async (batch) => {
40156
40554
  if (isPastDeadline()) {
40157
40555
  deadlineSkippedFiles.push(...batch);
40158
40556
  return [];
@@ -40164,9 +40562,9 @@ const spawnLintBatches = async (input) => {
40164
40562
  };
40165
40563
  const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
40166
40564
  scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
40167
- if (onFileProgress) {
40565
+ if (passOnFileProgress) {
40168
40566
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
40169
- onFileProgress(displayedFileCount, totalFileCount);
40567
+ passOnFileProgress(displayedFileCount, totalFileCount);
40170
40568
  }
40171
40569
  return batchDiagnostics;
40172
40570
  });
@@ -40174,22 +40572,53 @@ const spawnLintBatches = async (input) => {
40174
40572
  } finally {
40175
40573
  if (progressTimer !== null) clearInterval(progressTimer);
40176
40574
  }
40177
- const reportSkippedFiles = (skippedFiles, buildMessage) => {
40178
- if (skippedFiles.length === 0 || !onPartialFailure) return;
40179
- 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
40180
40582
  };
40181
- const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
40182
- reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
40183
- reportSkippedFiles(deadlineSkippedFiles, (fileListPreview) => `${deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
40184
- return allDiagnostics;
40185
40583
  };
40186
- let diagnostics;
40584
+ let outcome;
40585
+ let effectiveConcurrency = requestedConcurrency;
40187
40586
  try {
40188
- diagnostics = await runBatchPass(requestedConcurrency);
40587
+ outcome = await runBatchPass(requestedConcurrency, fileBatches, onFileProgress);
40189
40588
  } catch (error) {
40190
40589
  if (requestedConcurrency <= 1 || !isParallelismRelatedSpawnError(error)) throw error;
40191
- 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;
40192
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})`);
40193
40622
  return dedupeDiagnostics(diagnostics);
40194
40623
  };
40195
40624
  let didValidate = false;
@@ -40450,6 +40879,7 @@ const runOxlint = async (options) => {
40450
40879
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
40451
40880
  const severityControls = buildRuleSeverityControls(userConfig);
40452
40881
  validateRuleRegistration();
40882
+ resetManifestCaches();
40453
40883
  if (includePaths !== void 0 && includePaths.length === 0) return [];
40454
40884
  const pluginPath = resolvePluginPath();
40455
40885
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
@@ -41112,9 +41542,10 @@ const CONTEXT_AXES = [
41112
41542
  }
41113
41543
  ];
41114
41544
  const SCORE_AXES = [...GATED_AXES, ...CONTEXT_AXES];
41115
- const worstGatedAxis = (score) => {
41116
- let worst = GATED_AXES[0];
41117
- for (const axis of GATED_AXES) if (score[axis.key] < score[worst.key]) worst = axis;
41545
+ const gatedAxesForSection = (section) => section === "devDependencies" ? GATED_AXES.filter((axis) => axis.key !== "vulnerability") : GATED_AXES;
41546
+ const worstGatedAxis = (score, gatedAxes) => {
41547
+ let worst = gatedAxes[0];
41548
+ for (const axis of gatedAxes) if (score[axis.key] < score[worst.key]) worst = axis;
41118
41549
  return worst;
41119
41550
  };
41120
41551
  const clampScore = (value) => {
@@ -41138,6 +41569,69 @@ const resolveConcreteVersion = (spec) => {
41138
41569
  if (range === null || range === "*") return null;
41139
41570
  return import_semver.minVersion(trimmed)?.version ?? null;
41140
41571
  };
41572
+ const readTextFileOrNull = (filePath) => {
41573
+ try {
41574
+ return NFS.readFileSync(filePath, "utf-8");
41575
+ } catch {
41576
+ return null;
41577
+ }
41578
+ };
41579
+ const buildNpmLockLookup = (lockText) => {
41580
+ let parsed;
41581
+ try {
41582
+ parsed = JSON.parse(lockText);
41583
+ } catch {
41584
+ return null;
41585
+ }
41586
+ if (typeof parsed !== "object" || parsed === null) return null;
41587
+ const lock = parsed;
41588
+ return (name) => {
41589
+ const entry = lock.packages?.[`node_modules/${name}`] ?? lock.dependencies?.[name];
41590
+ return typeof entry?.version === "string" ? entry.version : null;
41591
+ };
41592
+ };
41593
+ const buildYarnLockLookup = (lockText) => {
41594
+ const lines = lockText.split(/\r?\n/);
41595
+ return (name, spec) => {
41596
+ const descriptor = `${name}@${spec}`;
41597
+ const berryDescriptor = `${name}@npm:${spec}`;
41598
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
41599
+ const line = lines[lineIndex];
41600
+ if (line.startsWith(" ") || line.startsWith("#") || !line.includes(":")) continue;
41601
+ const descriptors = line.replace(/:\s*$/, "").split(",").map((part) => part.trim().replace(/^"|"$/g, ""));
41602
+ if (!descriptors.includes(descriptor) && !descriptors.includes(berryDescriptor)) continue;
41603
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && (lines[bodyIndex].startsWith(" ") || lines[bodyIndex] === ""); bodyIndex += 1) {
41604
+ const versionMatch = /^\s+version:?\s+"?([^"\s]+)"?/.exec(lines[bodyIndex]);
41605
+ if (versionMatch) return versionMatch[1];
41606
+ }
41607
+ return null;
41608
+ }
41609
+ return null;
41610
+ };
41611
+ };
41612
+ const buildPnpmLockLookup = (lockText) => {
41613
+ return (name) => {
41614
+ const entryPattern = new RegExp(`^ {4,8}['"]?${name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}['"]?:\\s*$`);
41615
+ const lines = lockText.split(/\r?\n/);
41616
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
41617
+ if (!entryPattern.test(lines[lineIndex])) continue;
41618
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && /^ {5,}\S/.test(lines[bodyIndex]); bodyIndex += 1) {
41619
+ const versionMatch = /^\s+version:\s+['"]?([^'"\s(]+)/.exec(lines[bodyIndex]);
41620
+ if (versionMatch) return versionMatch[1];
41621
+ }
41622
+ }
41623
+ return null;
41624
+ };
41625
+ };
41626
+ const buildLockedVersionLookup = (rootDirectory) => {
41627
+ const npmLockText = readTextFileOrNull(Path.join(rootDirectory, "package-lock.json"));
41628
+ if (npmLockText !== null) return buildNpmLockLookup(npmLockText);
41629
+ const yarnLockText = readTextFileOrNull(Path.join(rootDirectory, "yarn.lock"));
41630
+ if (yarnLockText !== null) return buildYarnLockLookup(yarnLockText);
41631
+ const pnpmLockText = readTextFileOrNull(Path.join(rootDirectory, "pnpm-lock.yaml"));
41632
+ if (pnpmLockText !== null) return buildPnpmLockLookup(pnpmLockText);
41633
+ return null;
41634
+ };
41141
41635
  const locateDependencyKey = (packageJsonText, section, name) => {
41142
41636
  const needle = `"${name}"`;
41143
41637
  const sectionHeader = new RegExp(`"${section}"\\s*:\\s*\\{`);
@@ -41164,7 +41658,7 @@ const locateDependencyKey = (packageJsonText, section, name) => {
41164
41658
  }
41165
41659
  return null;
41166
41660
  };
41167
- const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies) => {
41661
+ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies, lockedVersionLookup) => {
41168
41662
  const sectionByName = /* @__PURE__ */ new Map();
41169
41663
  for (const name of Object.keys(packageJson.dependencies ?? {})) sectionByName.set(name, "dependencies");
41170
41664
  if (includeDevDependencies) {
@@ -41174,13 +41668,18 @@ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDepe
41174
41668
  for (const [name, section] of sectionByName) {
41175
41669
  if (SUPPLY_CHAIN_IGNORED_PACKAGES.has(name)) continue;
41176
41670
  const spec = (packageJson[section] ?? {})[name] ?? "";
41177
- const version = resolveConcreteVersion(spec);
41178
- if (version === null) continue;
41671
+ const floorVersion = resolveConcreteVersion(spec);
41672
+ if (floorVersion === null) continue;
41673
+ const lockedVersion = lockedVersionLookup?.(name, spec) ?? null;
41674
+ const lockSatisfiesSpec = lockedVersion !== null && import_semver.valid(lockedVersion) !== null && import_semver.satisfies(lockedVersion, spec);
41675
+ const isExactPin = import_semver.valid(spec.trim()) !== null;
41179
41676
  const location = locateDependencyKey(packageJsonText, section, name);
41180
41677
  dependencies.push({
41181
41678
  name,
41182
- version,
41679
+ version: lockSatisfiesSpec ? lockedVersion : floorVersion,
41183
41680
  spec,
41681
+ section,
41682
+ resolution: isExactPin ? "pin" : lockSatisfiesSpec ? "locked" : "floor",
41184
41683
  line: location?.line ?? 0,
41185
41684
  column: location?.column ?? 0
41186
41685
  });
@@ -41331,7 +41830,11 @@ const formatAlertReason = (topAlerts, totalCount) => {
41331
41830
  }
41332
41831
  return `Socket flagged ${totalCount} alerts (${topAlerts.map((alert) => friendlyAlertType(alert.type)).join(", ")}${totalCount > topAlerts.length ? ` (+${totalCount - topAlerts.length} more)` : ""}); most severe: ${displaySeverity(topAlerts[0].severity)}.`;
41333
41832
  };
41334
- const formatDependencyIdentity = (dependency) => import_semver.valid(dependency.spec) !== null ? `${dependency.name}@${dependency.version}` : `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
41833
+ const formatDependencyIdentity = (dependency) => {
41834
+ if (dependency.resolution === "pin") return `${dependency.name}@${dependency.version}`;
41835
+ if (dependency.resolution === "locked") return `${dependency.name}@${dependency.version} (lockfile resolution of "${dependency.spec}")`;
41836
+ return `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
41837
+ };
41335
41838
  const buildSupplyChainHelp = (dependency, failingAxis, topAlerts, packagePageUrl, options) => {
41336
41839
  const hasCriticalAlert = topAlerts.some((alert) => alert.severity.toLowerCase() === "critical");
41337
41840
  const entry = `\`"${dependency.name}": "${dependency.spec}"\``;
@@ -41374,7 +41877,7 @@ const buildLowScoreDiagnostic = (dependency, artifact, failingAxis, options) =>
41374
41877
  const checkSupplyChain = (input) => gen(function* () {
41375
41878
  const options = resolveOptions(input.userConfig);
41376
41879
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
41377
- const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
41880
+ const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies, buildLockedVersionLookup(input.rootDirectory));
41378
41881
  if (dependencies.length === 0) return [];
41379
41882
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
41380
41883
  if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
@@ -41383,7 +41886,7 @@ const checkSupplyChain = (input) => gen(function* () {
41383
41886
  for (let index = 0; index < dependencies.length; index += 1) {
41384
41887
  const artifact = artifacts[index];
41385
41888
  if (!artifact) continue;
41386
- const worstAxis = worstGatedAxis(artifact.score);
41889
+ const worstAxis = worstGatedAxis(artifact.score, gatedAxesForSection(dependencies[index].section));
41387
41890
  if (toHundred(artifact.score[worstAxis.key]) >= options.minScore) continue;
41388
41891
  diagnostics.push(buildLowScoreDiagnostic(dependencies[index], artifact, worstAxis, options));
41389
41892
  }
@@ -42109,7 +42612,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
42109
42612
  /** Display name used in client-facing messages and progress titles. */
42110
42613
  const SERVER_DISPLAY_NAME = "React Doctor";
42111
42614
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
42112
- const SERVER_VERSION = "0.7.1";
42615
+ const SERVER_VERSION = "0.7.2";
42113
42616
  /** `Diagnostic.source` shown next to every published diagnostic. */
42114
42617
  const DIAGNOSTIC_SOURCE = "react-doctor";
42115
42618
  /**
@@ -44160,5 +44663,5 @@ const startLanguageServer = () => {
44160
44663
  };
44161
44664
  //#endregion
44162
44665
  export { startLanguageServer };
44163
- !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]="027c66d6-b30a-59de-ad83-58f5ad304c94")}catch(e){}}();
44164
- //# debugId=027c66d6-b30a-59de-ad83-58f5ad304c94
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]="fff2fb6a-e10e-51f4-b9c3-f531f8dab288")}catch(e){}}();
44667
+ //# debugId=fff2fb6a-e10e-51f4-b9c3-f531f8dab288