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/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="8cb1a73e-b148-513d-a3eb-69f3a12430a5")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9aed4e70-dd94-51e8-9810-f8e8945d80a3")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -9,7 +9,7 @@ import * as NFS from "node:fs";
9
9
  import fs, { mkdtempSync, rmSync } from "node:fs";
10
10
  import process$1 from "node:process";
11
11
  import * as ts from "typescript";
12
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
+ import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
13
13
  import * as OS from "node:os";
14
14
  import os, { tmpdir } from "node:os";
15
15
  import { parseJSON5 } from "confbox";
@@ -35959,6 +35959,25 @@ var AmbiguousProjectError = class extends Error {
35959
35959
  }
35960
35960
  };
35961
35961
  const isProjectDiscoveryError = (value) => value instanceof ProjectNotFoundError || value instanceof NoReactDependencyError || value instanceof PackageJsonNotFoundError || value instanceof NotADirectoryError || value instanceof AmbiguousProjectError;
35962
+ const isErrnoException = (error) => error instanceof Error && "code" in error;
35963
+ const IGNORABLE_READDIR_ERROR_CODES = new Set([
35964
+ "EACCES",
35965
+ "EPERM",
35966
+ "ENOENT",
35967
+ "ENOTDIR",
35968
+ "EINVAL",
35969
+ "ELOOP",
35970
+ "ENAMETOOLONG"
35971
+ ]);
35972
+ const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
35973
+ const readDirectoryEntries = (directoryPath) => {
35974
+ try {
35975
+ return NFS.readdirSync(directoryPath, { withFileTypes: true });
35976
+ } catch (error) {
35977
+ if (isIgnorableReaddirError(error)) return [];
35978
+ throw error;
35979
+ }
35980
+ };
35962
35981
  const isFile = (filePath) => {
35963
35982
  try {
35964
35983
  return NFS.statSync(filePath).isFile();
@@ -35966,10 +35985,24 @@ const isFile = (filePath) => {
35966
35985
  return false;
35967
35986
  }
35968
35987
  };
35988
+ const isDirectory = (directoryPath) => {
35989
+ try {
35990
+ return NFS.statSync(directoryPath).isDirectory();
35991
+ } catch {
35992
+ return false;
35993
+ }
35994
+ };
35995
+ const isPlainObject = (value) => {
35996
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
35997
+ const prototype = Object.getPrototypeOf(value);
35998
+ return prototype === null || prototype === Object.prototype;
35999
+ };
35969
36000
  const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?|mts|mjs)$/;
35970
36001
  const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
36002
+ const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
35971
36003
  const MINIFIED_SNIFF_BYTES = 65536;
35972
36004
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
36005
+ const SCANNED_DOT_DIRECTORIES = new Set([".dumi", ".storybook"]);
35973
36006
  const IGNORED_DIRECTORIES = new Set([
35974
36007
  ".angular",
35975
36008
  ".astro",
@@ -36098,6 +36131,8 @@ const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
36098
36131
  const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
36099
36132
  const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
36100
36133
  const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
36134
+ const ABORT_EXIT_CODES = new Set([134, 3221226505]);
36135
+ const OXLINT_OOM_RESCUE_BUDGET_MS = 6e4;
36101
36136
  const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
36102
36137
  const LINT_PHASE_TIMEOUT_MS = 3e5;
36103
36138
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
@@ -36144,7 +36179,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36144
36179
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36145
36180
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36146
36181
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36147
- const CORE_PACKAGE_VERSION = "0.7.1";
36182
+ const CORE_PACKAGE_VERSION = "0.7.2";
36148
36183
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36149
36184
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36150
36185
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -36154,8 +36189,9 @@ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
36154
36189
  const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
36155
36190
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
36156
36191
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
36157
- const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
36158
- const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath);
36192
+ const isIgnoredDirectoryName = (directoryName) => IGNORED_DIRECTORIES.has(directoryName) || directoryName.startsWith(".") && !SCANNED_DOT_DIRECTORIES.has(directoryName);
36193
+ const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => isIgnoredDirectoryName(segment));
36194
+ const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath) && !GENERATED_SOURCE_DIRECTORY_PATTERN.test(filePath);
36159
36195
  const isMinifiedSource = (absolutePath) => {
36160
36196
  let fileDescriptor;
36161
36197
  try {
@@ -36189,25 +36225,6 @@ const isLargeMinifiedFile = (absolutePath, knownSizeBytes) => {
36189
36225
  cachedIsLargeMinifiedByPath.set(absolutePath, result);
36190
36226
  return result;
36191
36227
  };
36192
- const isErrnoException = (error) => error instanceof Error && "code" in error;
36193
- const IGNORABLE_READDIR_ERROR_CODES = new Set([
36194
- "EACCES",
36195
- "EPERM",
36196
- "ENOENT",
36197
- "ENOTDIR",
36198
- "EINVAL",
36199
- "ELOOP",
36200
- "ENAMETOOLONG"
36201
- ]);
36202
- const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
36203
- const readDirectoryEntries = (directoryPath) => {
36204
- try {
36205
- return NFS.readdirSync(directoryPath, { withFileTypes: true });
36206
- } catch (error) {
36207
- if (isIgnorableReaddirError(error)) return [];
36208
- throw error;
36209
- }
36210
- };
36211
36228
  function* walkSourceTreeFiles(rootDirectory) {
36212
36229
  const stack = [rootDirectory];
36213
36230
  while (stack.length > 0) {
@@ -36216,7 +36233,7 @@ function* walkSourceTreeFiles(rootDirectory) {
36216
36233
  for (const entry of readDirectoryEntries(currentDirectory)) {
36217
36234
  const absolutePath = Path.join(currentDirectory, entry.name);
36218
36235
  if (entry.isDirectory()) {
36219
- if (!IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
36236
+ if (!isIgnoredDirectoryName(entry.name)) stack.push(absolutePath);
36220
36237
  continue;
36221
36238
  }
36222
36239
  if (entry.isFile()) yield {
@@ -36310,6 +36327,146 @@ const findMonorepoRoot = (startDirectory) => {
36310
36327
  * byte-equivalent copy.
36311
36328
  */
36312
36329
  const isProjectBoundary = (directory) => NFS.existsSync(Path.join(directory, ".git")) || isMonorepoRoot(directory);
36330
+ const TSCONFIG_FILENAME = "tsconfig.json";
36331
+ const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
36332
+ const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
36333
+ const resolvePackageExtendsPath = (extendsValue, fromConfigDirectory) => {
36334
+ const requireFromConfig = createRequire(Path.join(fromConfigDirectory, "tsconfig.json"));
36335
+ const candidates = [
36336
+ extendsValue,
36337
+ ensureJsonExtension(extendsValue),
36338
+ `${extendsValue.replace(/\/$/, "")}/tsconfig.json`
36339
+ ];
36340
+ for (const candidate of candidates) try {
36341
+ return requireFromConfig.resolve(candidate);
36342
+ } catch {
36343
+ continue;
36344
+ }
36345
+ return null;
36346
+ };
36347
+ const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
36348
+ if (isRelativeExtendsValue(extendsValue)) return ensureJsonExtension(Path.resolve(fromConfigDirectory, extendsValue));
36349
+ return resolvePackageExtendsPath(extendsValue, fromConfigDirectory);
36350
+ };
36351
+ const normalizeCompilerOptions = (compilerOptions) => {
36352
+ if (!isPlainObject(compilerOptions)) return { hasExplicitLib: false };
36353
+ const target = typeof compilerOptions.target === "string" ? compilerOptions.target : void 0;
36354
+ const hasExplicitLib = Object.hasOwn(compilerOptions, "lib");
36355
+ return {
36356
+ target,
36357
+ lib: Array.isArray(compilerOptions.lib) ? compilerOptions.lib.filter((entry) => typeof entry === "string") : void 0,
36358
+ hasExplicitLib
36359
+ };
36360
+ };
36361
+ const readTsConfig = (filePath) => {
36362
+ let content;
36363
+ try {
36364
+ content = NFS.readFileSync(filePath, "utf-8");
36365
+ } catch {
36366
+ return null;
36367
+ }
36368
+ const parsed = ts.parseConfigFileTextToJson(filePath, content);
36369
+ if (!isPlainObject(parsed.config)) return null;
36370
+ return {
36371
+ extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
36372
+ referencePaths: normalizeReferencePaths(parsed.config.references),
36373
+ compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
36374
+ };
36375
+ };
36376
+ const normalizeReferencePaths = (references) => {
36377
+ if (!Array.isArray(references)) return [];
36378
+ return references.map((reference) => isPlainObject(reference) && typeof reference.path === "string" ? reference.path : null).filter((referencePath) => referencePath !== null);
36379
+ };
36380
+ const mergeCompilerOptions = (inherited, current) => {
36381
+ const target = current.target ?? inherited?.target;
36382
+ const hasExplicitLib = current.hasExplicitLib || Boolean(inherited?.hasExplicitLib);
36383
+ return {
36384
+ target,
36385
+ lib: current.hasExplicitLib ? current.lib : inherited?.lib,
36386
+ hasExplicitLib
36387
+ };
36388
+ };
36389
+ const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
36390
+ const realPath = NFS.realpathSync.native(tsConfigPath);
36391
+ if (visitedPaths.has(realPath)) return null;
36392
+ const tsConfig = readTsConfig(realPath);
36393
+ if (!tsConfig) return null;
36394
+ const nextVisitedPaths = new Set(visitedPaths);
36395
+ nextVisitedPaths.add(realPath);
36396
+ if (tsConfig.extends && extendsDepth < 8) {
36397
+ const parentPath = resolveExtendsPath(tsConfig.extends, Path.dirname(realPath));
36398
+ if (parentPath && isFile(parentPath)) return mergeCompilerOptions(readResolvedCompilerOptions(parentPath, extendsDepth + 1, nextVisitedPaths), tsConfig.compilerOptions);
36399
+ }
36400
+ return tsConfig.compilerOptions;
36401
+ };
36402
+ const targetYearIsPreES2023 = (target) => {
36403
+ const year = ES_TARGET_YEAR_BY_NAME[target.toLowerCase()];
36404
+ return year !== void 0 && year < 2023;
36405
+ };
36406
+ const libEntryIncludesES2023Array = (entry) => {
36407
+ const normalizedEntry = entry.toLowerCase();
36408
+ if (normalizedEntry === "esnext" || normalizedEntry === "esnext.array") return true;
36409
+ const esYearMatch = /^es(\d{4})(?:\.(.+))?$/.exec(normalizedEntry);
36410
+ if (!esYearMatch) return false;
36411
+ if (Number(esYearMatch[1]) < 2023) return false;
36412
+ const component = esYearMatch[2];
36413
+ return component === void 0 || component === "array";
36414
+ };
36415
+ const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
36416
+ const compilerOptionsArePreES2023 = (compilerOptions) => {
36417
+ if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
36418
+ if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
36419
+ return false;
36420
+ };
36421
+ const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
36422
+ const detectPreES2023FromConfig = (tsConfigPath, visitedConfigPaths = /* @__PURE__ */ new Set()) => {
36423
+ if (visitedConfigPaths.has(tsConfigPath)) return false;
36424
+ const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
36425
+ if (!compilerOptions) return false;
36426
+ if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) {
36427
+ const tsConfig = readTsConfig(tsConfigPath);
36428
+ if (!tsConfig) return false;
36429
+ const nextVisitedConfigPaths = new Set(visitedConfigPaths);
36430
+ nextVisitedConfigPaths.add(tsConfigPath);
36431
+ const configDirectory = Path.dirname(tsConfigPath);
36432
+ return tsConfig.referencePaths.some((referencePath) => {
36433
+ const resolvedReferencePath = Path.resolve(configDirectory, referencePath);
36434
+ const referencedConfigPath = isFile(resolvedReferencePath) ? resolvedReferencePath : Path.join(resolvedReferencePath, TSCONFIG_FILENAME);
36435
+ return isFile(referencedConfigPath) && detectPreES2023FromConfig(referencedConfigPath, nextVisitedConfigPaths);
36436
+ });
36437
+ }
36438
+ return compilerOptionsArePreES2023(compilerOptions);
36439
+ };
36440
+ const detectPreES2023Target = (directory) => {
36441
+ const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
36442
+ if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
36443
+ for (const fallbackFilename of FALLBACK_TSCONFIG_FILENAMES) {
36444
+ const fallbackPath = Path.join(directory, fallbackFilename);
36445
+ if (isFile(fallbackPath)) return detectPreES2023FromConfig(fallbackPath);
36446
+ }
36447
+ return false;
36448
+ };
36449
+ const FALLBACK_TSCONFIG_FILENAMES = ["tsconfig.app.json", "tsconfig.build.json"];
36450
+ const FRAMEWORK_PACKAGES = {
36451
+ next: "nextjs",
36452
+ "@tanstack/react-start": "tanstack-start",
36453
+ vite: "vite",
36454
+ "react-scripts": "cra",
36455
+ "@remix-run/react": "remix",
36456
+ gatsby: "gatsby",
36457
+ expo: "expo",
36458
+ "react-native": "react-native"
36459
+ };
36460
+ const detectFramework = (dependencies) => {
36461
+ for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
36462
+ if (dependencies.preact && !dependencies.react) return "preact";
36463
+ return "unknown";
36464
+ };
36465
+ const MOBILE_FRAMEWORKS = new Set(["expo", "react-native"]);
36466
+ const frameworkMergeRank = (framework) => {
36467
+ if (framework === "unknown") return 3;
36468
+ return MOBILE_FRAMEWORKS.has(framework) ? 2 : 1;
36469
+ };
36313
36470
  const REACT_COMPILER_PACKAGES = new Set([
36314
36471
  "babel-plugin-react-compiler",
36315
36472
  "react-compiler-runtime",
@@ -36346,6 +36503,7 @@ const EXPO_APP_CONFIG_FILENAMES = [
36346
36503
  ];
36347
36504
  const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
36348
36505
  const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
36506
+ const STATIC_EXPORT_OUTPUT_PATTERN = /(?:^|[^.\w])["']?output["']?\s*:\s*["']export["']/m;
36349
36507
  const hasCompilerPackage = (packageJson) => {
36350
36508
  const allDependencies = {
36351
36509
  ...packageJson.peerDependencies,
@@ -36378,34 +36536,10 @@ const detectReactCompiler = (directory, packageJson) => {
36378
36536
  }
36379
36537
  return false;
36380
36538
  };
36381
- const FRAMEWORK_PACKAGES = {
36382
- next: "nextjs",
36383
- "@tanstack/react-start": "tanstack-start",
36384
- vite: "vite",
36385
- "react-scripts": "cra",
36386
- "@remix-run/react": "remix",
36387
- gatsby: "gatsby",
36388
- expo: "expo",
36389
- "react-native": "react-native"
36390
- };
36391
- const FRAMEWORK_DISPLAY_NAMES = {
36392
- nextjs: "Next.js",
36393
- "tanstack-start": "TanStack Start",
36394
- vite: "Vite",
36395
- cra: "Create React App",
36396
- remix: "Remix",
36397
- gatsby: "Gatsby",
36398
- expo: "Expo",
36399
- "react-native": "React Native",
36400
- preact: "Preact",
36401
- unknown: "React"
36402
- };
36403
- const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
36404
- const detectFramework = (dependencies) => {
36405
- for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
36406
- if (dependencies.preact && !dependencies.react) return "preact";
36407
- return "unknown";
36408
- };
36539
+ const detectNextjsStaticExport = (directory) => NEXT_CONFIG_FILENAMES.some((filename) => {
36540
+ const filePath = Path.join(directory, filename);
36541
+ return isFile(filePath) && STATIC_EXPORT_OUTPUT_PATTERN.test(NFS.readFileSync(filePath, "utf-8"));
36542
+ });
36409
36543
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
36410
36544
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
36411
36545
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
@@ -36543,10 +36677,76 @@ const isConcreteDependencyVersion = (version) => {
36543
36677
  const normalizedVersion = normalizeDependencyVersion(version);
36544
36678
  return normalizedVersion !== null && /\d/.test(normalizedVersion);
36545
36679
  };
36546
- const isPlainObject = (value) => {
36547
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
36548
- const prototype = Object.getPrototypeOf(value);
36549
- return prototype === null || prototype === Object.prototype;
36680
+ const isMajorMinorAtLeast = (detected, required) => {
36681
+ if (detected === null) return true;
36682
+ if (detected.major !== required.major) return detected.major > required.major;
36683
+ return detected.minor >= required.minor;
36684
+ };
36685
+ const parseReactMajor = (reactVersion) => {
36686
+ if (typeof reactVersion !== "string") return null;
36687
+ return getLowestDependencyMajor(reactVersion);
36688
+ };
36689
+ const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
36690
+ const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
36691
+ const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
36692
+ const parseReactMajorMinor = (reactVersion) => {
36693
+ if (typeof reactVersion !== "string") return null;
36694
+ const trimmed = reactVersion.trim();
36695
+ if (trimmed.length === 0) return null;
36696
+ const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
36697
+ if (lowerBoundsOnly.length === 0) return null;
36698
+ const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
36699
+ if (majorMinorMatch) {
36700
+ const major = Number.parseInt(majorMinorMatch[1], 10);
36701
+ const minor = Number.parseInt(majorMinorMatch[2], 10);
36702
+ if (!Number.isFinite(major) || major <= 0) return null;
36703
+ if (!Number.isFinite(minor) || minor < 0) return null;
36704
+ return {
36705
+ major,
36706
+ minor
36707
+ };
36708
+ }
36709
+ const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
36710
+ if (!majorOnlyMatch) return null;
36711
+ const major = Number.parseInt(majorOnlyMatch[1], 10);
36712
+ if (!Number.isFinite(major) || major <= 0) return null;
36713
+ return {
36714
+ major,
36715
+ minor: 0
36716
+ };
36717
+ };
36718
+ const parseLowerBoundVersion = (versionSpec) => import_semver.validRange(versionSpec) !== null ? import_semver.minVersion(versionSpec) : import_semver.coerce(versionSpec);
36719
+ const parseTailwindMajorMinor = (tailwindVersion) => {
36720
+ if (typeof tailwindVersion !== "string") return null;
36721
+ const trimmed = tailwindVersion.trim();
36722
+ if (trimmed.length === 0) return null;
36723
+ const lowerBound = parseLowerBoundVersion(trimmed);
36724
+ if (lowerBound === null || lowerBound.major <= 0) return null;
36725
+ return {
36726
+ major: lowerBound.major,
36727
+ minor: lowerBound.minor
36728
+ };
36729
+ };
36730
+ const hasUpperBoundOnlyPeerRange = (range) => {
36731
+ if (typeof range !== "string") return false;
36732
+ const normalizedRange = normalizeDependencyVersion(range);
36733
+ if (normalizedRange === null) return false;
36734
+ return splitDependencyVersionBranches(normalizedRange).some((branch) => {
36735
+ const normalizedBranch = normalizeDependencyVersion(branch);
36736
+ return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
36737
+ });
36738
+ };
36739
+ const peerRangeMinMajor = (range) => {
36740
+ if (typeof range !== "string") return null;
36741
+ return getLowestDependencyMajor(range);
36742
+ };
36743
+ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
36744
+ const installedReactMajor = parseReactMajor(reactVersion);
36745
+ const peerReactRange = packageJson.peerDependencies?.react;
36746
+ if (typeof peerReactRange !== "string") return installedReactMajor;
36747
+ const peerFloor = peerRangeMinMajor(peerReactRange);
36748
+ if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
36749
+ return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
36550
36750
  };
36551
36751
  const isCatalogReference = (version) => version.startsWith("catalog:");
36552
36752
  const extractCatalogName = (version) => {
@@ -36688,6 +36888,16 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
36688
36888
  }
36689
36889
  return null;
36690
36890
  };
36891
+ const REACT_SECTIONS = [
36892
+ "dependencies",
36893
+ "peerDependencies",
36894
+ "devDependencies"
36895
+ ];
36896
+ const TAILWIND_ZOD_SECTIONS = [
36897
+ "dependencies",
36898
+ "devDependencies",
36899
+ "peerDependencies"
36900
+ ];
36691
36901
  const EMPTY_DEPENDENCY_INFO = {
36692
36902
  reactVersion: null,
36693
36903
  tailwindVersion: null,
@@ -36728,6 +36938,10 @@ const extractDependencyInfo = (packageJson) => {
36728
36938
  framework: detectFramework(allDependencies)
36729
36939
  };
36730
36940
  };
36941
+ const getDependencySpec = (packageJson, packageName) => {
36942
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
36943
+ return typeof spec === "string" ? spec : null;
36944
+ };
36731
36945
  const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
36732
36946
  for (const section of sections) {
36733
36947
  const version = packageJson[section]?.[packageName];
@@ -36744,222 +36958,91 @@ const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
36744
36958
  version: null
36745
36959
  };
36746
36960
  };
36747
- const isDirectory = (directoryPath) => {
36748
- try {
36749
- return NFS.statSync(directoryPath).isDirectory();
36750
- } catch {
36751
- return false;
36752
- }
36753
- };
36754
- const NX_PROJECT_DISCOVERY_DIRS = [
36755
- "apps",
36756
- "libs",
36757
- "packages"
36758
- ];
36759
- const getNxWorkspaceDirectories = (rootDirectory) => {
36760
- if (!isFile(Path.join(rootDirectory, "nx.json"))) return [];
36761
- const collected = [];
36762
- for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
36763
- const candidatePath = Path.join(rootDirectory, candidate);
36764
- if (!isDirectory(candidatePath)) continue;
36765
- for (const entry of readDirectoryEntries(candidatePath)) {
36766
- if (!entry.isDirectory()) continue;
36767
- const projectDirectory = Path.join(candidatePath, entry.name);
36768
- if (isFile(Path.join(projectDirectory, "project.json")) || isFile(Path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
36769
- }
36770
- }
36771
- return collected;
36772
- };
36773
- const parsePnpmWorkspacePatterns = (rootDirectory) => {
36774
- const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
36775
- if (!isFile(workspacePath)) return [];
36776
- const content = NFS.readFileSync(workspacePath, "utf-8");
36777
- const patterns = [];
36778
- let isInsidePackagesBlock = false;
36779
- for (const line of content.split("\n")) {
36780
- const trimmed = line.trim();
36781
- if (trimmed === "packages:") {
36782
- isInsidePackagesBlock = true;
36783
- continue;
36784
- }
36785
- if (isInsidePackagesBlock && trimmed.startsWith("-")) patterns.push(trimmed.replace(/^-\s*/, "").replace(/["']/g, ""));
36786
- else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith("#")) isInsidePackagesBlock = false;
36787
- }
36788
- return patterns;
36789
- };
36790
- const getWorkspacePatterns = (rootDirectory, packageJson) => {
36791
- const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
36792
- if (pnpmPatterns.length > 0) return pnpmPatterns;
36793
- if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
36794
- if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
36795
- const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
36796
- if (nxPatterns.length > 0) return nxPatterns;
36797
- return [];
36798
- };
36799
- const parseReactMajor = (reactVersion) => {
36800
- if (typeof reactVersion !== "string") return null;
36801
- return getLowestDependencyMajor(reactVersion);
36961
+ const REACT_DEPENDENCY_NAMES = new Set([
36962
+ "react",
36963
+ "react-native",
36964
+ "next",
36965
+ "preact"
36966
+ ]);
36967
+ const hasReactDependency = (packageJson) => {
36968
+ const allDependencies = {
36969
+ ...packageJson.peerDependencies,
36970
+ ...packageJson.dependencies,
36971
+ ...packageJson.devDependencies
36972
+ };
36973
+ return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
36802
36974
  };
36803
- const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
36804
- const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
36805
- if (!cleanPattern.includes("*")) {
36806
- const directoryPath = Path.join(rootDirectory, cleanPattern);
36807
- if (isDirectory(directoryPath) && isFile(Path.join(directoryPath, "package.json"))) return [directoryPath];
36808
- return [];
36809
- }
36810
- const wildcardIndex = cleanPattern.indexOf("*");
36811
- const baseDirectory = Path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
36812
- const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
36813
- if (!isDirectory(baseDirectory)) return [];
36814
- const resolved = [];
36815
- for (const entry of readDirectoryEntries(baseDirectory)) {
36816
- const entryPath = Path.join(baseDirectory, entry.name, suffixAfterWildcard);
36817
- if (isDirectory(entryPath) && isFile(Path.join(entryPath, "package.json"))) resolved.push(entryPath);
36818
- }
36819
- return resolved;
36975
+ const getPreactVersion = (packageJson) => {
36976
+ return {
36977
+ ...packageJson.peerDependencies,
36978
+ ...packageJson.dependencies,
36979
+ ...packageJson.devDependencies
36980
+ }.preact ?? null;
36820
36981
  };
36821
- const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
36822
- const dependencyDeclaration = getDependencyDeclaration({
36823
- packageJson: workspacePackageJson,
36824
- packageName,
36825
- sections
36826
- });
36827
- if (!dependencyDeclaration.hasDeclaration) return null;
36828
- return concreteVersion ?? resolveCatalogVersion(workspacePackageJson, packageName, workspaceDirectory, dependencyDeclaration.catalogReference) ?? resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, dependencyDeclaration.catalogReference);
36982
+ const TANSTACK_QUERY_PACKAGES = new Set([
36983
+ "@tanstack/react-query",
36984
+ "@tanstack/query-core",
36985
+ "react-query"
36986
+ ]);
36987
+ const hasTanStackQuery = (packageJson) => {
36988
+ const allDependencies = {
36989
+ ...packageJson.peerDependencies,
36990
+ ...packageJson.dependencies,
36991
+ ...packageJson.devDependencies
36992
+ };
36993
+ return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
36829
36994
  };
36830
- const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
36831
- if (!currentVersion) return true;
36832
- const currentMajor = parseReactMajor(currentVersion);
36833
- const nextMajor = parseReactMajor(nextVersion);
36834
- if (currentMajor === null) return nextMajor !== null;
36835
- if (nextMajor === null) return false;
36836
- return nextMajor < currentMajor;
36995
+ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
36996
+ if (version === null || !isCatalogReference(version)) return version;
36997
+ const catalogName = extractCatalogName(version);
36998
+ const resolvedLocalVersion = resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, catalogName);
36999
+ if (resolvedLocalVersion) return resolvedLocalVersion;
37000
+ const monorepoRoot = findMonorepoRoot(rootDirectory);
37001
+ if (!monorepoRoot) return version;
37002
+ const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
37003
+ if (!isFile(monorepoPackageJsonPath)) return version;
37004
+ return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
36837
37005
  };
36838
- const findReactInWorkspaces = (rootDirectory, packageJson) => {
36839
- const patterns = getWorkspacePatterns(rootDirectory, packageJson);
36840
- const result = { ...EMPTY_DEPENDENCY_INFO };
36841
- for (const pattern of patterns) {
36842
- const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
36843
- for (const workspaceDirectory of directories) {
36844
- const workspacePackageJson = readPackageJson$1(Path.join(workspaceDirectory, "package.json"));
36845
- const info = extractDependencyInfo(workspacePackageJson);
36846
- const reactVersion = resolveWorkspaceDependencyVersion({
36847
- concreteVersion: info.reactVersion,
36848
- packageName: "react",
36849
- rootDirectory,
36850
- rootPackageJson: packageJson,
36851
- sections: [
36852
- "dependencies",
36853
- "peerDependencies",
36854
- "devDependencies"
36855
- ],
36856
- workspaceDirectory,
36857
- workspacePackageJson
36858
- });
36859
- const tailwindVersion = resolveWorkspaceDependencyVersion({
36860
- concreteVersion: info.tailwindVersion,
36861
- packageName: "tailwindcss",
36862
- rootDirectory,
36863
- rootPackageJson: packageJson,
36864
- sections: [
36865
- "dependencies",
36866
- "devDependencies",
36867
- "peerDependencies"
36868
- ],
36869
- workspaceDirectory,
36870
- workspacePackageJson
36871
- });
36872
- const zodVersion = resolveWorkspaceDependencyVersion({
36873
- concreteVersion: info.zodVersion,
36874
- packageName: "zod",
36875
- rootDirectory,
36876
- rootPackageJson: packageJson,
36877
- sections: [
36878
- "dependencies",
36879
- "devDependencies",
36880
- "peerDependencies"
36881
- ],
36882
- workspaceDirectory,
36883
- workspacePackageJson
36884
- });
36885
- if (reactVersion && shouldReplaceReactVersion(result.reactVersion, reactVersion)) result.reactVersion = reactVersion;
36886
- if (tailwindVersion && !result.tailwindVersion) result.tailwindVersion = tailwindVersion;
36887
- if (zodVersion && !result.zodVersion) result.zodVersion = zodVersion;
36888
- if (info.framework !== "unknown" && result.framework === "unknown") result.framework = info.framework;
36889
- const resultReactMajor = parseReactMajor(result.reactVersion);
36890
- if (result.reactVersion && result.tailwindVersion && result.framework !== "unknown" && resultReactMajor !== null && resultReactMajor <= 17) return result;
36891
- }
37006
+ /**
37007
+ * The chain of ancestor directories of `startDirectory`, nearest first, up to
37008
+ * and including the filesystem root. With `includeStart`, the chain begins at
37009
+ * `startDirectory` itself; otherwise at its parent. The one place the "walk up
37010
+ * the tree" traversal is expressed, so each caller adds only its own match /
37011
+ * stop policy instead of re-deriving the `path.dirname` loop.
37012
+ */
37013
+ const ancestorDirectories = (startDirectory, { includeStart }) => {
37014
+ const directories = [];
37015
+ let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
37016
+ while (true) {
37017
+ directories.push(currentDirectory);
37018
+ const parentDirectory = Path.dirname(currentDirectory);
37019
+ if (parentDirectory === currentDirectory) return directories;
37020
+ currentDirectory = parentDirectory;
36892
37021
  }
36893
- return result;
36894
37022
  };
36895
- const findDependencyInfoFromMonorepoRoot = (directory) => {
36896
- const monorepoRoot = findMonorepoRoot(directory);
36897
- if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
36898
- const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
36899
- if (!isFile(monorepoPackageJsonPath)) return EMPTY_DEPENDENCY_INFO;
36900
- const rootPackageJson = readPackageJson$1(monorepoPackageJsonPath);
36901
- const rootInfo = extractDependencyInfo(rootPackageJson);
36902
- const leafPackageJsonPath = Path.join(directory, "package.json");
36903
- const leafPackageJson = isFile(leafPackageJsonPath) ? readPackageJson$1(leafPackageJsonPath) : null;
36904
- const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
36905
- packageJson: leafPackageJson,
36906
- packageName: "react",
36907
- sections: [
36908
- "dependencies",
36909
- "peerDependencies",
36910
- "devDependencies"
36911
- ]
36912
- }) : null;
36913
- const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
36914
- packageJson: leafPackageJson,
36915
- packageName: "tailwindcss",
36916
- sections: [
36917
- "dependencies",
36918
- "devDependencies",
36919
- "peerDependencies"
36920
- ]
36921
- }) : null;
36922
- const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
36923
- packageJson: leafPackageJson,
36924
- packageName: "zod",
36925
- sections: [
36926
- "dependencies",
36927
- "devDependencies",
36928
- "peerDependencies"
36929
- ]
36930
- }) : null;
36931
- const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
36932
- const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
36933
- const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
36934
- const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
36935
- const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
36936
- const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
36937
- const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
36938
- return {
36939
- reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
36940
- tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
36941
- zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceInfo.zodVersion : null,
36942
- framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
36943
- };
36944
- };
36945
- const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
36946
- const rootValue = select(rootPackageJson);
36947
- if (rootValue !== null) return rootValue;
36948
- const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
36949
- if (patterns.length === 0) return null;
36950
- const visitedDirectories = /* @__PURE__ */ new Set();
36951
- for (const pattern of patterns) {
36952
- const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
36953
- for (const workspaceDirectory of directories) {
36954
- if (visitedDirectories.has(workspaceDirectory)) continue;
36955
- visitedDirectories.add(workspaceDirectory);
36956
- const value = select(readPackageJson$1(Path.join(workspaceDirectory, "package.json")));
36957
- if (value !== null) return value;
36958
- }
37023
+ /**
37024
+ * Walk up from `startDirectory` to the nearest ancestor that owns a
37025
+ * `package.json`, stopping at (and including) the enclosing project boundary
37026
+ * — the working tree's git root or a monorepo root. Lets a scan of a package
37027
+ * subfolder that has no `package.json` of its own adopt the nearest enclosing
37028
+ * package as its project root, so it inherits that package's dependency +
37029
+ * framework detection instead of synthesizing an empty (React-blind) project.
37030
+ *
37031
+ * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
37032
+ * most specific owning package (a leaf workspace, not just the repo root), so
37033
+ * a subfolder of one workspace no longer borrows a sibling's capabilities.
37034
+ *
37035
+ * Returns `null` when no ancestor `package.json` exists inside the project
37036
+ * boundary (a loose tree of files, or a subfolder of a package-less repo).
37037
+ */
37038
+ const findNearestAncestorPackageJson = (startDirectory) => {
37039
+ if (isProjectBoundary(startDirectory)) return null;
37040
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
37041
+ if (isFile(Path.join(directory, "package.json"))) return directory;
37042
+ if (isProjectBoundary(directory)) return null;
36959
37043
  }
36960
37044
  return null;
36961
37045
  };
36962
- const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => predicate(packageJson) ? true : null) !== null;
36963
37046
  const NAMES = new Set([
36964
37047
  "react-native",
36965
37048
  "react-native-tvos",
@@ -36992,47 +37075,7 @@ const isPackageJsonReactNativeAware = (packageJson) => {
36992
37075
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
36993
37076
  return false;
36994
37077
  };
36995
- const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
36996
- const getDependencySpec = (packageJson, packageName) => {
36997
- const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
36998
- return typeof spec === "string" ? spec : null;
36999
- };
37000
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
37001
- const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
37002
- const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
37003
- const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
37004
- if (version === null || !isCatalogReference(version)) return version;
37005
- const catalogName = extractCatalogName(version);
37006
- const resolvedLocalVersion = resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, catalogName);
37007
- if (resolvedLocalVersion) return resolvedLocalVersion;
37008
- const monorepoRoot = findMonorepoRoot(rootDirectory);
37009
- if (!monorepoRoot) return version;
37010
- const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
37011
- if (!isFile(monorepoPackageJsonPath)) return version;
37012
- return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
37013
- };
37014
- const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
37015
- const getPreactVersion = (packageJson) => {
37016
- return {
37017
- ...packageJson.peerDependencies,
37018
- ...packageJson.dependencies,
37019
- ...packageJson.devDependencies
37020
- }.preact ?? null;
37021
- };
37022
- const TANSTACK_QUERY_PACKAGES = new Set([
37023
- "@tanstack/react-query",
37024
- "@tanstack/query-core",
37025
- "react-query"
37026
- ]);
37027
- const hasTanStackQuery = (packageJson) => {
37028
- const allDependencies = {
37029
- ...packageJson.peerDependencies,
37030
- ...packageJson.dependencies,
37031
- ...packageJson.devDependencies
37032
- };
37033
- return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
37034
- };
37035
- const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
37078
+ const REANIMATED_DEPENDENCY_NAME$1 = "react-native-reanimated";
37036
37079
  const isPackageJsonReanimatedAware = (packageJson) => {
37037
37080
  const allDependencies = {
37038
37081
  ...packageJson.peerDependencies,
@@ -37040,144 +37083,70 @@ const isPackageJsonReanimatedAware = (packageJson) => {
37040
37083
  ...packageJson.devDependencies,
37041
37084
  ...packageJson.optionalDependencies
37042
37085
  };
37043
- return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
37086
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME$1);
37044
37087
  };
37045
- const parseZodMajor = (zodVersion) => {
37046
- if (typeof zodVersion !== "string") return null;
37047
- return getLowestDependencyMajor(zodVersion);
37048
- };
37049
- const hasUpperBoundOnlyPeerRange = (range) => {
37050
- if (typeof range !== "string") return false;
37051
- const normalizedRange = normalizeDependencyVersion(range);
37052
- if (normalizedRange === null) return false;
37053
- return splitDependencyVersionBranches(normalizedRange).some((branch) => {
37054
- const normalizedBranch = normalizeDependencyVersion(branch);
37055
- return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
37056
- });
37057
- };
37058
- const peerRangeMinMajor = (range) => {
37059
- if (typeof range !== "string") return null;
37060
- return getLowestDependencyMajor(range);
37061
- };
37062
- const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
37063
- const installedReactMajor = parseReactMajor(reactVersion);
37064
- const peerReactRange = packageJson.peerDependencies?.react;
37065
- if (typeof peerReactRange !== "string") return installedReactMajor;
37066
- const peerFloor = peerRangeMinMajor(peerReactRange);
37067
- if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
37068
- return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
37088
+ const getWorkspacePatterns = (rootDirectory, packageJson) => {
37089
+ const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
37090
+ if (pnpmPatterns.length > 0) return pnpmPatterns;
37091
+ if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
37092
+ if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
37093
+ const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
37094
+ if (nxPatterns.length > 0) return nxPatterns;
37095
+ return [];
37069
37096
  };
37070
- const TSCONFIG_FILENAME = "tsconfig.json";
37071
- const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
37072
- const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
37073
- const resolvePackageExtendsPath = (extendsValue, fromConfigDirectory) => {
37074
- const requireFromConfig = createRequire(Path.join(fromConfigDirectory, "tsconfig.json"));
37075
- const candidates = [
37076
- extendsValue,
37077
- ensureJsonExtension(extendsValue),
37078
- `${extendsValue.replace(/\/$/, "")}/tsconfig.json`
37079
- ];
37080
- for (const candidate of candidates) try {
37081
- return requireFromConfig.resolve(candidate);
37082
- } catch {
37083
- continue;
37097
+ const parsePnpmWorkspacePatterns = (rootDirectory) => {
37098
+ const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
37099
+ if (!isFile(workspacePath)) return [];
37100
+ const content = NFS.readFileSync(workspacePath, "utf-8");
37101
+ const patterns = [];
37102
+ let isInsidePackagesBlock = false;
37103
+ for (const line of content.split("\n")) {
37104
+ const trimmed = line.trim();
37105
+ if (trimmed === "packages:") {
37106
+ isInsidePackagesBlock = true;
37107
+ continue;
37108
+ }
37109
+ if (isInsidePackagesBlock && trimmed.startsWith("-")) patterns.push(trimmed.replace(/^-\s*/, "").replace(/["']/g, ""));
37110
+ else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith("#")) isInsidePackagesBlock = false;
37084
37111
  }
37085
- return null;
37086
- };
37087
- const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
37088
- if (isRelativeExtendsValue(extendsValue)) return ensureJsonExtension(Path.resolve(fromConfigDirectory, extendsValue));
37089
- return resolvePackageExtendsPath(extendsValue, fromConfigDirectory);
37090
- };
37091
- const normalizeCompilerOptions = (compilerOptions) => {
37092
- if (!isPlainObject(compilerOptions)) return { hasExplicitLib: false };
37093
- const target = typeof compilerOptions.target === "string" ? compilerOptions.target : void 0;
37094
- const hasExplicitLib = Object.hasOwn(compilerOptions, "lib");
37095
- return {
37096
- target,
37097
- lib: Array.isArray(compilerOptions.lib) ? compilerOptions.lib.filter((entry) => typeof entry === "string") : void 0,
37098
- hasExplicitLib
37099
- };
37112
+ return patterns;
37100
37113
  };
37101
- const readTsConfig = (filePath) => {
37102
- let content;
37103
- try {
37104
- content = NFS.readFileSync(filePath, "utf-8");
37105
- } catch {
37106
- return null;
37114
+ const NX_PROJECT_DISCOVERY_DIRS = [
37115
+ "apps",
37116
+ "libs",
37117
+ "packages"
37118
+ ];
37119
+ const getNxWorkspaceDirectories = (rootDirectory) => {
37120
+ if (!isFile(Path.join(rootDirectory, "nx.json"))) return [];
37121
+ const collected = [];
37122
+ for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
37123
+ const candidatePath = Path.join(rootDirectory, candidate);
37124
+ if (!isDirectory(candidatePath)) continue;
37125
+ for (const entry of readDirectoryEntries(candidatePath)) {
37126
+ if (!entry.isDirectory()) continue;
37127
+ const projectDirectory = Path.join(candidatePath, entry.name);
37128
+ if (isFile(Path.join(projectDirectory, "project.json")) || isFile(Path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
37129
+ }
37107
37130
  }
37108
- const parsed = ts.parseConfigFileTextToJson(filePath, content);
37109
- if (!isPlainObject(parsed.config)) return null;
37110
- return {
37111
- extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
37112
- compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
37113
- };
37114
- };
37115
- const mergeCompilerOptions = (inherited, current) => {
37116
- const target = current.target ?? inherited?.target;
37117
- const hasExplicitLib = current.hasExplicitLib || Boolean(inherited?.hasExplicitLib);
37118
- return {
37119
- target,
37120
- lib: current.hasExplicitLib ? current.lib : inherited?.lib,
37121
- hasExplicitLib
37122
- };
37131
+ return collected;
37123
37132
  };
37124
- const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
37125
- const realPath = NFS.realpathSync.native(tsConfigPath);
37126
- if (visitedPaths.has(realPath)) return null;
37127
- const tsConfig = readTsConfig(realPath);
37128
- if (!tsConfig) return null;
37129
- const nextVisitedPaths = new Set(visitedPaths);
37130
- nextVisitedPaths.add(realPath);
37131
- if (tsConfig.extends && extendsDepth < 8) {
37132
- const parentPath = resolveExtendsPath(tsConfig.extends, Path.dirname(realPath));
37133
- if (parentPath && isFile(parentPath)) return mergeCompilerOptions(readResolvedCompilerOptions(parentPath, extendsDepth + 1, nextVisitedPaths), tsConfig.compilerOptions);
37133
+ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
37134
+ const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
37135
+ if (!cleanPattern.includes("*")) {
37136
+ const directoryPath = Path.join(rootDirectory, cleanPattern);
37137
+ if (isDirectory(directoryPath) && isFile(Path.join(directoryPath, "package.json"))) return [directoryPath];
37138
+ return [];
37134
37139
  }
37135
- return tsConfig.compilerOptions;
37136
- };
37137
- const targetYearIsPreES2023 = (target) => {
37138
- const year = ES_TARGET_YEAR_BY_NAME[target.toLowerCase()];
37139
- return year !== void 0 && year < 2023;
37140
- };
37141
- const libEntryIncludesES2023Array = (entry) => {
37142
- const normalizedEntry = entry.toLowerCase();
37143
- if (normalizedEntry === "esnext" || normalizedEntry === "esnext.array") return true;
37144
- const esYearMatch = /^es(\d{4})(?:\.(.+))?$/.exec(normalizedEntry);
37145
- if (!esYearMatch) return false;
37146
- if (Number(esYearMatch[1]) < 2023) return false;
37147
- const component = esYearMatch[2];
37148
- return component === void 0 || component === "array";
37149
- };
37150
- const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
37151
- const compilerOptionsArePreES2023 = (compilerOptions) => {
37152
- if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
37153
- if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
37154
- return false;
37155
- };
37156
- const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
37157
- const detectPreES2023FromConfig = (tsConfigPath) => {
37158
- const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
37159
- if (!compilerOptions) return false;
37160
- if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) return false;
37161
- return compilerOptionsArePreES2023(compilerOptions);
37162
- };
37163
- const detectPreES2023Target = (directory) => {
37164
- const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
37165
- if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
37166
- return false;
37167
- };
37168
- const REACT_DEPENDENCY_NAMES = new Set([
37169
- "react",
37170
- "react-native",
37171
- "next",
37172
- "preact"
37173
- ]);
37174
- const hasReactDependency = (packageJson) => {
37175
- const allDependencies = {
37176
- ...packageJson.peerDependencies,
37177
- ...packageJson.dependencies,
37178
- ...packageJson.devDependencies
37179
- };
37180
- return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
37140
+ const wildcardIndex = cleanPattern.indexOf("*");
37141
+ const baseDirectory = Path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
37142
+ const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
37143
+ if (!isDirectory(baseDirectory)) return [];
37144
+ const resolved = [];
37145
+ for (const entry of readDirectoryEntries(baseDirectory)) {
37146
+ const entryPath = Path.join(baseDirectory, entry.name, suffixAfterWildcard);
37147
+ if (isDirectory(entryPath) && isFile(Path.join(entryPath, "package.json"))) resolved.push(entryPath);
37148
+ }
37149
+ return resolved;
37181
37150
  };
37182
37151
  const listWorkspacePackages = (rootDirectory) => {
37183
37152
  const packageJsonPath = Path.join(rootDirectory, "package.json");
@@ -37209,6 +37178,208 @@ const listWorkspacePackages = (rootDirectory) => {
37209
37178
  }
37210
37179
  return packages;
37211
37180
  };
37181
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
37182
+ const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
37183
+ const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
37184
+ const dependencyDeclaration = getDependencyDeclaration({
37185
+ packageJson: workspacePackageJson,
37186
+ packageName,
37187
+ sections
37188
+ });
37189
+ if (!dependencyDeclaration.hasDeclaration) return null;
37190
+ return concreteVersion ?? resolveCatalogVersion(workspacePackageJson, packageName, workspaceDirectory, dependencyDeclaration.catalogReference) ?? resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, dependencyDeclaration.catalogReference);
37191
+ };
37192
+ const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
37193
+ if (!currentVersion) return true;
37194
+ const currentMajor = parseReactMajor(currentVersion);
37195
+ const nextMajor = parseReactMajor(nextVersion);
37196
+ if (currentMajor === null) return nextMajor !== null;
37197
+ if (nextMajor === null) return false;
37198
+ return nextMajor < currentMajor;
37199
+ };
37200
+ const evaluateManifestFacts = (facts, packageJson, directory) => {
37201
+ if (facts.expo.version === null) {
37202
+ const spec = getDependencySpec(packageJson, "expo");
37203
+ if (spec !== null) facts.expo = {
37204
+ version: spec,
37205
+ sourceDirectory: directory
37206
+ };
37207
+ }
37208
+ if (facts.next.version === null) {
37209
+ const spec = getDependencySpec(packageJson, "next");
37210
+ if (spec !== null) facts.next = {
37211
+ version: spec,
37212
+ sourceDirectory: directory
37213
+ };
37214
+ }
37215
+ if (facts.shopifyFlashList.version === null) {
37216
+ const spec = getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME);
37217
+ if (spec !== null) facts.shopifyFlashList = {
37218
+ version: spec,
37219
+ sourceDirectory: directory
37220
+ };
37221
+ }
37222
+ if (facts.reanimatedVersion === null) {
37223
+ const spec = getDependencySpec(packageJson, REANIMATED_DEPENDENCY_NAME);
37224
+ if (spec !== null) facts.reanimatedVersion = spec;
37225
+ }
37226
+ facts.hasReactNativeAwarePackage = facts.hasReactNativeAwarePackage || isPackageJsonReactNativeAware(packageJson);
37227
+ facts.hasReanimatedAwarePackage = facts.hasReanimatedAwarePackage || isPackageJsonReanimatedAware(packageJson);
37228
+ };
37229
+ const collectWorkspaceFacts = (rootDirectory, rootPackageJson, { collectReactGroup }) => {
37230
+ const facts = {
37231
+ reactVersion: null,
37232
+ tailwindVersion: null,
37233
+ zodVersion: null,
37234
+ framework: "unknown",
37235
+ expo: {
37236
+ version: null,
37237
+ sourceDirectory: null
37238
+ },
37239
+ next: {
37240
+ version: null,
37241
+ sourceDirectory: null
37242
+ },
37243
+ shopifyFlashList: {
37244
+ version: null,
37245
+ sourceDirectory: null
37246
+ },
37247
+ hasReactNativeAwarePackage: false,
37248
+ hasReanimatedAwarePackage: false,
37249
+ reanimatedVersion: null
37250
+ };
37251
+ evaluateManifestFacts(facts, rootPackageJson, rootDirectory);
37252
+ let isReactGroupSettled = !collectReactGroup;
37253
+ const visitedDirectories = /* @__PURE__ */ new Set();
37254
+ for (const pattern of getWorkspacePatterns(rootDirectory, rootPackageJson)) {
37255
+ const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
37256
+ for (const workspaceDirectory of directories) {
37257
+ if (visitedDirectories.has(workspaceDirectory)) continue;
37258
+ visitedDirectories.add(workspaceDirectory);
37259
+ const workspacePackageJson = readPackageJson$1(Path.join(workspaceDirectory, "package.json"));
37260
+ evaluateManifestFacts(facts, workspacePackageJson, workspaceDirectory);
37261
+ const info = extractDependencyInfo(workspacePackageJson);
37262
+ if (info.framework !== "unknown" && frameworkMergeRank(info.framework) < frameworkMergeRank(facts.framework)) facts.framework = info.framework;
37263
+ if (isReactGroupSettled) continue;
37264
+ const reactVersion = resolveWorkspaceDependencyVersion({
37265
+ concreteVersion: info.reactVersion,
37266
+ packageName: "react",
37267
+ rootDirectory,
37268
+ rootPackageJson,
37269
+ sections: REACT_SECTIONS,
37270
+ workspaceDirectory,
37271
+ workspacePackageJson
37272
+ });
37273
+ const tailwindVersion = resolveWorkspaceDependencyVersion({
37274
+ concreteVersion: info.tailwindVersion,
37275
+ packageName: "tailwindcss",
37276
+ rootDirectory,
37277
+ rootPackageJson,
37278
+ sections: TAILWIND_ZOD_SECTIONS,
37279
+ workspaceDirectory,
37280
+ workspacePackageJson
37281
+ });
37282
+ const zodVersion = resolveWorkspaceDependencyVersion({
37283
+ concreteVersion: info.zodVersion,
37284
+ packageName: "zod",
37285
+ rootDirectory,
37286
+ rootPackageJson,
37287
+ sections: TAILWIND_ZOD_SECTIONS,
37288
+ workspaceDirectory,
37289
+ workspacePackageJson
37290
+ });
37291
+ if (reactVersion && shouldReplaceReactVersion(facts.reactVersion, reactVersion)) facts.reactVersion = reactVersion;
37292
+ if (tailwindVersion && !facts.tailwindVersion) facts.tailwindVersion = tailwindVersion;
37293
+ if (zodVersion && !facts.zodVersion) facts.zodVersion = zodVersion;
37294
+ const settledReactMajor = parseReactMajor(facts.reactVersion);
37295
+ isReactGroupSettled = Boolean(facts.reactVersion && facts.tailwindVersion && facts.framework !== "unknown" && settledReactMajor !== null && settledReactMajor <= 17);
37296
+ }
37297
+ }
37298
+ return facts;
37299
+ };
37300
+ const findDependencyInfoFromMonorepoRoot = (directory) => {
37301
+ const monorepoRoot = findMonorepoRoot(directory);
37302
+ if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
37303
+ const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
37304
+ if (!isFile(monorepoPackageJsonPath)) return EMPTY_DEPENDENCY_INFO;
37305
+ const rootPackageJson = readPackageJson$1(monorepoPackageJsonPath);
37306
+ const rootInfo = extractDependencyInfo(rootPackageJson);
37307
+ const leafPackageJsonPath = Path.join(directory, "package.json");
37308
+ const leafPackageJson = isFile(leafPackageJsonPath) ? readPackageJson$1(leafPackageJsonPath) : null;
37309
+ const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
37310
+ packageJson: leafPackageJson,
37311
+ packageName: "react",
37312
+ sections: REACT_SECTIONS
37313
+ }) : null;
37314
+ const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
37315
+ packageJson: leafPackageJson,
37316
+ packageName: "tailwindcss",
37317
+ sections: TAILWIND_ZOD_SECTIONS
37318
+ }) : null;
37319
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
37320
+ packageJson: leafPackageJson,
37321
+ packageName: "zod",
37322
+ sections: TAILWIND_ZOD_SECTIONS
37323
+ }) : null;
37324
+ const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
37325
+ const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
37326
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
37327
+ const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
37328
+ const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
37329
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
37330
+ const workspaceFacts = collectWorkspaceFacts(monorepoRoot, rootPackageJson, { collectReactGroup: true });
37331
+ return {
37332
+ reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceFacts.reactVersion : rootInfo.reactVersion ?? workspaceFacts.reactVersion,
37333
+ tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceFacts.tailwindVersion : null,
37334
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceFacts.zodVersion : null,
37335
+ framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceFacts.framework
37336
+ };
37337
+ };
37338
+ /**
37339
+ * Last-resort React detection: locate React the way Node's `node_modules`
37340
+ * resolution would from `directory` — check each `node_modules/react` up the
37341
+ * tree — but bounded to the enclosing repo so a globally installed React can't
37342
+ * masquerade as the project's version. Stops at (and including) the nearest
37343
+ * project boundary — the git root or a monorepo root — and returns the first
37344
+ * installed `version`.
37345
+ *
37346
+ * Makes "React is installed and importable" ⇒ "React is detected" an invariant
37347
+ * for packages whose only React declaration is a version-less spec
37348
+ * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
37349
+ * `node_modules` the declaration walks never reach — the profile of a component
37350
+ * package inside a monorepo. Matching the `node_modules/react` entry rather than
37351
+ * its realpath means a project whose `node_modules` is symlinked elsewhere (a
37352
+ * Docker volume, a shared store) still counts, while a global install outside
37353
+ * the repo does not.
37354
+ *
37355
+ * Returns `null` when React isn't installed within the repo or its package.json
37356
+ * carries no version string. A tree with no boundary marker at all bounds the
37357
+ * search to `directory`, so a dependency hoisted above the scanned package there
37358
+ * is conservatively not adopted.
37359
+ */
37360
+ /**
37361
+ * The nearest enclosing project boundary (git root or monorepo root), or
37362
+ * `directory` itself when the scan target sits outside any repo. Bounds the
37363
+ * node_modules walk so a React hoisted anywhere inside the repo counts, but a
37364
+ * React in an ancestor above the repo — a home-directory or global
37365
+ * `node_modules` — can't leak in.
37366
+ */
37367
+ const findContainmentRoot = (directory) => {
37368
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
37369
+ return directory;
37370
+ };
37371
+ const resolveInstalledReactVersion = (directory) => {
37372
+ const containmentRoot = findContainmentRoot(directory);
37373
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
37374
+ const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
37375
+ if (isFile(reactPackageJsonPath)) {
37376
+ const installedVersion = readPackageJson$1(reactPackageJsonPath).version;
37377
+ return typeof installedVersion === "string" ? installedVersion : null;
37378
+ }
37379
+ if (ancestorDirectory === containmentRoot) return null;
37380
+ }
37381
+ return null;
37382
+ };
37212
37383
  const toReactWorkspacePackages = (directories) => {
37213
37384
  const packages = [];
37214
37385
  for (const directory of directories) {
@@ -37289,8 +37460,8 @@ const cachedProjectInfos = /* @__PURE__ */ new Map();
37289
37460
  const discoverProjectWithoutPackageJson = (directory) => {
37290
37461
  const sourceFileCount = countSourceFiles(directory);
37291
37462
  const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
37292
- const monorepoRoot = findMonorepoRoot(directory);
37293
- const enclosingProject = monorepoRoot !== null && isFile(Path.join(monorepoRoot, "package.json")) ? discoverProject(monorepoRoot) : null;
37463
+ const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
37464
+ const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
37294
37465
  if (enclosingProject !== null) return {
37295
37466
  ...enclosingProject,
37296
37467
  rootDirectory: directory,
@@ -37320,7 +37491,9 @@ const discoverProjectWithoutPackageJson = (directory) => {
37320
37491
  shopifyFlashListVersion: null,
37321
37492
  shopifyFlashListMajorVersion: null,
37322
37493
  hasReanimated: false,
37494
+ reanimatedVersion: null,
37323
37495
  isPreES2023Target: hasOwnTsConfig && detectPreES2023Target(directory),
37496
+ isStaticExport: false,
37324
37497
  sourceFileCount
37325
37498
  };
37326
37499
  };
@@ -37334,88 +37507,85 @@ const discoverProject = (directory) => {
37334
37507
  return synthesized;
37335
37508
  }
37336
37509
  const packageJson = readPackageJson$1(packageJsonPath);
37337
- let { reactVersion, tailwindVersion, zodVersion, framework } = extractDependencyInfo(packageJson);
37338
- const reactDeclaration = getDependencyDeclaration({
37339
- packageJson,
37340
- packageName: "react",
37341
- sections: [
37342
- "dependencies",
37343
- "peerDependencies",
37344
- "devDependencies"
37345
- ]
37346
- });
37347
- const tailwindDeclaration = getDependencyDeclaration({
37348
- packageJson,
37349
- packageName: "tailwindcss",
37350
- sections: [
37351
- "dependencies",
37352
- "devDependencies",
37353
- "peerDependencies"
37354
- ]
37355
- });
37356
- const zodDeclaration = getDependencyDeclaration({
37510
+ const rootInfo = extractDependencyInfo(packageJson);
37511
+ let framework = rootInfo.framework;
37512
+ const tracked = {
37513
+ react: {
37514
+ version: rootInfo.reactVersion,
37515
+ sections: REACT_SECTIONS
37516
+ },
37517
+ tailwindcss: {
37518
+ version: rootInfo.tailwindVersion,
37519
+ sections: TAILWIND_ZOD_SECTIONS
37520
+ },
37521
+ zod: {
37522
+ version: rootInfo.zodVersion,
37523
+ sections: TAILWIND_ZOD_SECTIONS
37524
+ }
37525
+ };
37526
+ const declarations = Object.fromEntries(Object.entries(tracked).map(([packageName, entry]) => [packageName, getDependencyDeclaration({
37357
37527
  packageJson,
37358
- packageName: "zod",
37359
- sections: [
37360
- "dependencies",
37361
- "devDependencies",
37362
- "peerDependencies"
37363
- ]
37364
- });
37365
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(packageJson, "react", directory, reactDeclaration.catalogReference);
37366
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(packageJson, "tailwindcss", directory, tailwindDeclaration.catalogReference);
37367
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(packageJson, "zod", directory, zodDeclaration.catalogReference);
37368
- if (!reactVersion || !tailwindVersion || !zodVersion) {
37528
+ packageName,
37529
+ sections: entry.sections
37530
+ })]));
37531
+ const fillFromCatalogs = (source, sourceDirectory) => {
37532
+ for (const [packageName, entry] of Object.entries(tracked)) if (!entry.version && declarations[packageName].hasDeclaration) entry.version = resolveCatalogVersion(source, packageName, sourceDirectory, declarations[packageName].catalogReference);
37533
+ };
37534
+ fillFromCatalogs(packageJson, directory);
37535
+ if (!tracked.react.version || !tracked.tailwindcss.version || !tracked.zod.version) {
37369
37536
  const monorepoRoot = findMonorepoRoot(directory);
37370
37537
  if (monorepoRoot) {
37371
37538
  const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
37372
- if (isFile(monorepoPackageJsonPath)) {
37373
- const rootPackageJson = readPackageJson$1(monorepoPackageJsonPath);
37374
- if (!reactVersion && reactDeclaration.hasDeclaration) reactVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, reactDeclaration.catalogReference);
37375
- if (!tailwindVersion && tailwindDeclaration.hasDeclaration) tailwindVersion = resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, tailwindDeclaration.catalogReference);
37376
- if (!zodVersion && zodDeclaration.hasDeclaration) zodVersion = resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, zodDeclaration.catalogReference);
37377
- }
37539
+ if (isFile(monorepoPackageJsonPath)) fillFromCatalogs(readPackageJson$1(monorepoPackageJsonPath), monorepoRoot);
37378
37540
  }
37379
37541
  }
37380
- if (!reactVersion || framework === "unknown") {
37381
- const workspaceInfo = findReactInWorkspaces(directory, packageJson);
37382
- if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
37383
- if (!tailwindVersion && workspaceInfo.tailwindVersion) tailwindVersion = workspaceInfo.tailwindVersion;
37384
- if (!zodVersion && workspaceInfo.zodVersion) zodVersion = workspaceInfo.zodVersion;
37385
- if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
37542
+ const shouldCollectReactGroup = !tracked.react.version || framework === "unknown";
37543
+ const workspaceFacts = collectWorkspaceFacts(directory, packageJson, { collectReactGroup: shouldCollectReactGroup });
37544
+ if (shouldCollectReactGroup) {
37545
+ tracked.react.version ||= workspaceFacts.reactVersion;
37546
+ tracked.tailwindcss.version ||= workspaceFacts.tailwindVersion;
37547
+ tracked.zod.version ||= workspaceFacts.zodVersion;
37548
+ if (framework === "unknown" && workspaceFacts.framework !== "unknown") framework = workspaceFacts.framework;
37386
37549
  }
37387
- if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
37550
+ if ((!tracked.react.version || framework === "unknown") && !isMonorepoRoot(directory)) {
37388
37551
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
37389
- if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
37390
- if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
37391
- if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
37552
+ tracked.react.version ||= monorepoInfo.reactVersion;
37553
+ tracked.tailwindcss.version ||= monorepoInfo.tailwindVersion;
37554
+ tracked.zod.version ||= monorepoInfo.zodVersion;
37392
37555
  if (framework === "unknown") framework = monorepoInfo.framework;
37393
37556
  }
37394
- if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
37395
- if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
37396
- if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
37557
+ for (const [packageName, entry] of Object.entries(tracked)) {
37558
+ const declaredVersion = declarations[packageName].version;
37559
+ if (!entry.version && declaredVersion && !isCatalogReference(declaredVersion)) entry.version = declaredVersion;
37560
+ }
37561
+ const { react, tailwindcss, zod } = tracked;
37562
+ let reactVersion = react.version;
37563
+ if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
37564
+ const tailwindVersion = tailwindcss.version;
37565
+ const zodVersion = zod.version;
37397
37566
  const projectName = packageJson.name ?? Path.basename(directory);
37398
37567
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
37399
37568
  const sourceFileCount = countSourceFiles(directory);
37400
- const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
37569
+ const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
37401
37570
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
37402
37571
  rootDirectory: directory,
37403
37572
  rootPackageJson: packageJson,
37404
37573
  packageName: "expo",
37405
- version: findExpoVersion(directory, packageJson)
37574
+ version: workspaceFacts.expo.version
37406
37575
  }) : null;
37407
37576
  const shopifyFlashListVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
37408
37577
  rootDirectory: directory,
37409
37578
  rootPackageJson: packageJson,
37410
37579
  packageName: SHOPIFY_FLASH_LIST_PACKAGE_NAME,
37411
- version: findShopifyFlashListVersion(directory, packageJson)
37580
+ version: workspaceFacts.shopifyFlashList.version
37412
37581
  }) : null;
37413
- const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
37582
+ const hasReanimated = hasReactNativeWorkspace && workspaceFacts.hasReanimatedAwarePackage;
37583
+ const reanimatedVersion = hasReanimated ? workspaceFacts.reanimatedVersion : null;
37414
37584
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
37415
37585
  rootDirectory: directory,
37416
37586
  rootPackageJson: packageJson,
37417
37587
  packageName: "next",
37418
- version: findNextjsVersion(directory, packageJson)
37588
+ version: workspaceFacts.next.version
37419
37589
  }) : null;
37420
37590
  const preactVersion = getPreactVersion(packageJson);
37421
37591
  const isPreES2023Target = hasTypeScript && detectPreES2023Target(directory);
@@ -37426,7 +37596,7 @@ const discoverProject = (directory) => {
37426
37596
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
37427
37597
  tailwindVersion,
37428
37598
  zodVersion,
37429
- zodMajorVersion: parseZodMajor(zodVersion),
37599
+ zodMajorVersion: zodVersion === null ? null : getLowestDependencyMajor(zodVersion),
37430
37600
  framework,
37431
37601
  hasTypeScript,
37432
37602
  hasReactCompiler: detectReactCompiler(directory, packageJson),
@@ -37440,7 +37610,9 @@ const discoverProject = (directory) => {
37440
37610
  shopifyFlashListVersion,
37441
37611
  shopifyFlashListMajorVersion: shopifyFlashListVersion === null ? null : getLowestDependencyMajor(shopifyFlashListVersion),
37442
37612
  hasReanimated,
37613
+ reanimatedVersion,
37443
37614
  isPreES2023Target,
37615
+ isStaticExport: framework === "nextjs" && detectNextjsStaticExport(workspaceFacts.next.sourceDirectory ?? directory),
37444
37616
  sourceFileCount
37445
37617
  };
37446
37618
  cachedProjectInfos.set(directory, projectInfo);
@@ -37456,57 +37628,6 @@ const discoverProject = (directory) => {
37456
37628
  * single source file has nothing to analyze.
37457
37629
  */
37458
37630
  const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null || project.sourceFileCount > 0;
37459
- const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
37460
- const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
37461
- const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
37462
- const parseReactMajorMinor = (reactVersion) => {
37463
- if (typeof reactVersion !== "string") return null;
37464
- const trimmed = reactVersion.trim();
37465
- if (trimmed.length === 0) return null;
37466
- const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
37467
- if (lowerBoundsOnly.length === 0) return null;
37468
- const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
37469
- if (majorMinorMatch) {
37470
- const major = Number.parseInt(majorMinorMatch[1], 10);
37471
- const minor = Number.parseInt(majorMinorMatch[2], 10);
37472
- if (!Number.isFinite(major) || major <= 0) return null;
37473
- if (!Number.isFinite(minor) || minor < 0) return null;
37474
- return {
37475
- major,
37476
- minor
37477
- };
37478
- }
37479
- const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
37480
- if (!majorOnlyMatch) return null;
37481
- const major = Number.parseInt(majorOnlyMatch[1], 10);
37482
- if (!Number.isFinite(major) || major <= 0) return null;
37483
- return {
37484
- major,
37485
- minor: 0
37486
- };
37487
- };
37488
- const isReactAtLeast = (detected, required) => {
37489
- if (detected === null) return true;
37490
- if (detected.major !== required.major) return detected.major > required.major;
37491
- return detected.minor >= required.minor;
37492
- };
37493
- const parseLowerBoundVersion = (versionSpec) => import_semver.validRange(versionSpec) !== null ? import_semver.minVersion(versionSpec) : import_semver.coerce(versionSpec);
37494
- const parseTailwindMajorMinor = (tailwindVersion) => {
37495
- if (typeof tailwindVersion !== "string") return null;
37496
- const trimmed = tailwindVersion.trim();
37497
- if (trimmed.length === 0) return null;
37498
- const lowerBound = parseLowerBoundVersion(trimmed);
37499
- if (lowerBound === null || lowerBound.major <= 0) return null;
37500
- return {
37501
- major: lowerBound.major,
37502
- minor: lowerBound.minor
37503
- };
37504
- };
37505
- const isTailwindAtLeast = (detected, required) => {
37506
- if (detected === null) return true;
37507
- if (detected.major !== required.major) return detected.major > required.major;
37508
- return detected.minor >= required.minor;
37509
- };
37510
37631
  const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
37511
37632
  var InvalidGlobPatternError = class extends Error {
37512
37633
  pattern;
@@ -37632,6 +37753,9 @@ const buildRuleSeverityControls = (config) => {
37632
37753
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
37633
37754
  };
37634
37755
  };
37756
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
37757
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
37758
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
37635
37759
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
37636
37760
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
37637
37761
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -37709,6 +37833,7 @@ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
37709
37833
  "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
37710
37834
  "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
37711
37835
  "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
37836
+ "react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
37712
37837
  "react/jsx-no-undef": "react-doctor/jsx-no-undef",
37713
37838
  "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
37714
37839
  "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
@@ -37793,9 +37918,6 @@ const tokenizeRuleList = (ruleList) => {
37793
37918
  if (!ruleSection) return [];
37794
37919
  return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
37795
37920
  };
37796
- const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
37797
- const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
37798
- const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
37799
37921
  const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — add \`${ruleId}\` to the comment (e.g. \`${token}, ${ruleId}\`).`;
37800
37922
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
37801
37923
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
@@ -37849,6 +37971,53 @@ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
37849
37971
  if (!ruleId.startsWith("react-doctor/")) return null;
37850
37972
  return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
37851
37973
  };
37974
+ const ESLINT_REACT_HOOKS_PLUGIN_PREFIX = "react-hooks/";
37975
+ const buildAcceptedTokens = (ruleId) => {
37976
+ const shortRuleId = ruleId.slice(15);
37977
+ return new Set([ruleId, `${ESLINT_REACT_HOOKS_PLUGIN_PREFIX}${shortRuleId}`]);
37978
+ };
37979
+ const listNamesRule = (ruleList, acceptedTokens) => tokenizeRuleList(ruleList).some((token) => acceptedTokens.has(token));
37980
+ const isInlineDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
37981
+ const candidates = [{
37982
+ line: lines[diagnosticLineIndex],
37983
+ requiredScope: "line"
37984
+ }, {
37985
+ line: lines[diagnosticLineIndex - 1],
37986
+ requiredScope: "next-line"
37987
+ }];
37988
+ for (const { line, requiredScope } of candidates) {
37989
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
37990
+ if (!match) continue;
37991
+ const [, , scope, ruleList] = match;
37992
+ if (scope !== requiredScope) continue;
37993
+ if (listNamesRule(ruleList, acceptedTokens)) return true;
37994
+ }
37995
+ return false;
37996
+ };
37997
+ const isBlockDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
37998
+ let isRangeOpen = false;
37999
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
38000
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
38001
+ const line = lines[lineIndex];
38002
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
38003
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
38004
+ if (disableMatch && listNamesRule(disableMatch[2], acceptedTokens)) {
38005
+ isRangeOpen = true;
38006
+ continue;
38007
+ }
38008
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
38009
+ if (enableMatch) {
38010
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
38011
+ if (enabledRules.length === 0 || enabledRules.some((rule) => acceptedTokens.has(rule))) isRangeOpen = false;
38012
+ }
38013
+ }
38014
+ return isRangeOpen;
38015
+ };
38016
+ const isCompilerRuleForeignDisabled = (lines, diagnosticLineIndex, ruleId) => {
38017
+ if (!ruleId.startsWith("react-hooks-js/")) return false;
38018
+ const acceptedTokens = buildAcceptedTokens(ruleId);
38019
+ return isInlineDisabled(lines, diagnosticLineIndex, acceptedTokens) || isBlockDisabled(lines, diagnosticLineIndex, acceptedTokens);
38020
+ };
37852
38021
  const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
37853
38022
  const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
37854
38023
  const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
@@ -37992,6 +38161,11 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
37992
38161
  isSuppressed: true,
37993
38162
  nearMissHint: null
37994
38163
  };
38164
+ if (isCompilerRuleForeignDisabled(lines, diagnosticLineIndex, ruleId)) return {
38165
+ isSuppressed: true,
38166
+ nearMissHint: null,
38167
+ isForeignDirective: true
38168
+ };
37995
38169
  const directComments = findStackedDisableCommentsAbove(lines, diagnosticLineIndex);
37996
38170
  if (hasChainSuppressor(directComments, ruleId)) return {
37997
38171
  isSuppressed: true,
@@ -38273,12 +38447,18 @@ const collectStringSet = (values) => {
38273
38447
  return new Set(values.filter((value) => typeof value === "string"));
38274
38448
  };
38275
38449
  const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
38450
+ const NODE_CLI_PATH_SEGMENTS = new Set([
38451
+ "scripts",
38452
+ "bin",
38453
+ "tools"
38454
+ ]);
38455
+ const hasNodeCliPathSegment = (filePath) => filePath.split(/[\\/]/).slice(0, -1).some((segment) => NODE_CLI_PATH_SEGMENTS.has(segment));
38276
38456
  /**
38277
38457
  * Pre-compiles every stateful filter and returns a single
38278
38458
  * `apply(diagnostic)` closure that runs (in order):
38279
38459
  *
38280
38460
  * 1. auto-suppress (test-noise rules in test files; `migration-hint`
38281
- * wins over `test-noise`)
38461
+ * wins over `test-noise`; `no-console` on Node CLI scripts)
38282
38462
  * 2. severity overrides (top-level `rules` / `categories`, with
38283
38463
  * `"off"` dropping)
38284
38464
  * 3. warning suppression (only when `showWarnings` is false: drops every
@@ -38376,6 +38556,12 @@ const buildDiagnosticPipeline = (input) => {
38376
38556
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
38377
38557
  return false;
38378
38558
  };
38559
+ const isNoConsoleOnNodeCliScript = (diagnostic) => {
38560
+ if (diagnostic.rule !== "no-console") return false;
38561
+ if (hasNodeCliPathSegment(diagnostic.filePath)) return true;
38562
+ const lines = getFileLines(diagnostic.filePath);
38563
+ return lines !== null && Boolean(lines[0]?.startsWith("#!"));
38564
+ };
38379
38565
  const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
38380
38566
  if (!hasRuntimeGlobals) return false;
38381
38567
  if (diagnostic.rule !== "jsx-no-undef") return false;
@@ -38385,6 +38571,7 @@ const buildDiagnosticPipeline = (input) => {
38385
38571
  return {
38386
38572
  apply: (diagnostic) => {
38387
38573
  if (shouldAutoSuppress(diagnostic)) return null;
38574
+ if (isNoConsoleOnNodeCliScript(diagnostic)) return null;
38388
38575
  let current = diagnostic;
38389
38576
  let explicitSeverityOverride;
38390
38577
  let explicitRuleOverride;
@@ -38414,7 +38601,7 @@ const buildDiagnosticPipeline = (input) => {
38414
38601
  if (lines) {
38415
38602
  const ruleIdentifier = `${current.plugin}/${current.rule}`;
38416
38603
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
38417
- if (evaluation.isSuppressed) return suppress(current, "inline");
38604
+ if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
38418
38605
  if (evaluation.nearMissHint) current = {
38419
38606
  ...current,
38420
38607
  suppressionHint: evaluation.nearMissHint
@@ -40251,64 +40438,64 @@ function* collectSecurityScanFiles(rootDirectory) {
40251
40438
  }
40252
40439
  }
40253
40440
  }
40254
- /**
40255
- * Whether project discovery resolved a React-compatible runtime (React or
40256
- * Preact, which ships the same hooks + JSX model). This is the same
40257
- * predicate that grants the `react` capability, so `false` means every
40258
- * React-runtime rule family was gated off for the project.
40259
- */
40260
- const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
40441
+ const CLIENT_ONLY_FRAMEWORKS = new Set([
40442
+ "vite",
40443
+ "cra",
40444
+ "gatsby",
40445
+ "react-native",
40446
+ "expo"
40447
+ ]);
40448
+ const addMajorLadder = (capabilities, name, major, earliest, latest) => {
40449
+ if (major === null) return;
40450
+ const cappedMajor = Math.min(major, latest);
40451
+ for (let candidate = earliest; candidate <= cappedMajor; candidate += 1) capabilities.add(`${name}:${candidate}`);
40452
+ };
40261
40453
  const buildCapabilities = (project) => {
40262
40454
  const capabilities = /* @__PURE__ */ new Set();
40263
40455
  capabilities.add(project.framework);
40264
- if (hasReactRuntime(project)) capabilities.add("react");
40456
+ if (project.reactVersion !== null || project.preactVersion !== null) capabilities.add("react");
40265
40457
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
40266
40458
  if (project.expoVersion !== null) capabilities.add("expo");
40459
+ if ((project.framework === "nextjs" || project.framework === "tanstack-start" || project.framework === "remix") && !project.isStaticExport) capabilities.add("server-actions");
40460
+ if (CLIENT_ONLY_FRAMEWORKS.has(project.framework)) capabilities.add("client-only");
40461
+ if (project.isStaticExport) capabilities.add("nextjs:static-export");
40267
40462
  if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
40268
- const reactMajor = project.reactMajorVersion;
40269
- if (reactMajor !== null) {
40270
- const cappedReactMajor = Math.min(reactMajor, 30);
40271
- for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
40272
- if (reactMajor >= 19) {
40273
- if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
40274
- major: 19,
40275
- minor: 2
40276
- })) capabilities.add("react:19.2");
40277
- }
40278
- }
40279
- if (project.tailwindVersion !== null) {
40280
- capabilities.add("tailwind");
40281
- if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
40282
- major: 3,
40283
- minor: 4
40284
- })) capabilities.add("tailwind:3.4");
40285
- }
40286
- if (project.zodVersion !== null) {
40287
- capabilities.add("zod");
40288
- if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
40289
- }
40463
+ addMajorLadder(capabilities, "react", project.reactMajorVersion, 17, 30);
40464
+ if (project.reactMajorVersion !== null && project.reactMajorVersion >= 19 && isMajorMinorAtLeast(parseReactMajorMinor(project.reactVersion), {
40465
+ major: 19,
40466
+ minor: 2
40467
+ })) capabilities.add("react:19.2");
40468
+ if (project.tailwindVersion !== null) capabilities.add("tailwind");
40469
+ if (project.tailwindVersion !== null && isMajorMinorAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
40470
+ major: 3,
40471
+ minor: 4
40472
+ })) capabilities.add("tailwind:3.4");
40473
+ if (project.zodVersion !== null) capabilities.add("zod");
40474
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
40290
40475
  if (project.isPreES2023Target) capabilities.add("pre-es2023");
40291
40476
  if (project.hasReactCompiler) capabilities.add("react-compiler");
40292
40477
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
40293
40478
  if (project.hasTypeScript) capabilities.add("typescript");
40294
- if (project.preactVersion !== null) {
40295
- capabilities.add("preact");
40296
- const preactMajor = project.preactMajorVersion;
40297
- if (preactMajor !== null) {
40298
- const cappedPreactMajor = Math.min(preactMajor, 20);
40299
- for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
40300
- }
40301
- if (project.reactVersion === null) capabilities.add("pure-preact");
40302
- }
40479
+ if (project.preactVersion !== null) capabilities.add("preact");
40480
+ addMajorLadder(capabilities, "preact", project.preactMajorVersion, 10, 20);
40481
+ if (project.preactVersion !== null && project.reactVersion === null) capabilities.add("pure-preact");
40303
40482
  return capabilities;
40304
40483
  };
40305
- const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
40484
+ const capabilitiesByProject = /* @__PURE__ */ new WeakMap();
40485
+ const getCapabilities = (project) => {
40486
+ const cached = capabilitiesByProject.get(project);
40487
+ if (cached !== void 0) return cached;
40488
+ const capabilities = buildCapabilities(project);
40489
+ capabilitiesByProject.set(project, capabilities);
40490
+ return capabilities;
40491
+ };
40492
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhen) => {
40306
40493
  if (requires) {
40307
40494
  for (const capability of requires) if (!capabilities.has(capability)) return false;
40308
40495
  }
40309
40496
  if (tags?.includes("react-jsx-only") && !capabilities.has("react")) return false;
40310
- if (disabledBy) {
40311
- for (const capability of disabledBy) if (capabilities.has(capability)) return false;
40497
+ if (disabledWhen) {
40498
+ for (const capability of disabledWhen) if (capabilities.has(capability)) return false;
40312
40499
  }
40313
40500
  if (tags) {
40314
40501
  for (const tag of tags) if (ignoredTags.has(tag)) return false;
@@ -40319,14 +40506,14 @@ const yieldToEventLoop = () => new Promise((resolve) => {
40319
40506
  setImmediate(resolve);
40320
40507
  });
40321
40508
  const createSecurityScanSession = (rootDirectory, options) => {
40322
- const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
40509
+ const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
40323
40510
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
40324
40511
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
40325
40512
  const rule = entry.rule;
40326
40513
  const scan = rule.scan;
40327
40514
  if (typeof scan !== "function") return [];
40328
40515
  if (rule.defaultEnabled === false) return [];
40329
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) return [];
40516
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) return [];
40330
40517
  return [{
40331
40518
  entry,
40332
40519
  scan,
@@ -40346,6 +40533,7 @@ const createSecurityScanSession = (rootDirectory, options) => {
40346
40533
  return status === true;
40347
40534
  };
40348
40535
  const scanFileByRule = function* (file) {
40536
+ if (file === null) return;
40349
40537
  for (const { entry, scan, committedFilesOnly } of enabledScanRules) {
40350
40538
  for (const finding of scan(file)) {
40351
40539
  if (committedFilesOnly && isFileGitIgnored(file)) continue;
@@ -41807,7 +41995,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
41807
41995
  })),
41808
41996
  showStagedContent: (directory, relativePath, options) => runCommand({
41809
41997
  command: "git",
41810
- args: ["show", `:${relativePath}`],
41998
+ args: ["show", `:./${relativePath}`],
41811
41999
  directory,
41812
42000
  maxStdoutBytes: options?.maxBufferBytes
41813
42001
  }).pipe(map$3((result) => result.status === 0 ? result.stdout : null)),
@@ -42242,7 +42430,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42242
42430
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
42243
42431
  const jsPlugins = [];
42244
42432
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
42245
- const capabilities = buildCapabilities(project);
42433
+ const capabilities = getCapabilities(project);
42246
42434
  const enabledReactDoctorRules = {};
42247
42435
  for (const registryEntry of REACT_DOCTOR_RULES) {
42248
42436
  const rule = reactDoctorPlugin.rules[registryEntry.id];
@@ -42253,7 +42441,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42253
42441
  if (rule.scan !== void 0) continue;
42254
42442
  if (customRulesOnly && registryEntry.originallyExternal) continue;
42255
42443
  if (rule.framework !== "global" && !rule.requires) continue;
42256
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
42444
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) continue;
42257
42445
  const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
42258
42446
  if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
42259
42447
  const severity = resolveRuleSeverityOverride({
@@ -42284,6 +42472,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42284
42472
  settings: { "react-doctor": {
42285
42473
  framework: project.framework,
42286
42474
  rootDirectory: resolveSettingsRootDirectory(project.rootDirectory),
42475
+ capabilities: [...capabilities].sort(),
42287
42476
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
42288
42477
  ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
42289
42478
  } },
@@ -42574,25 +42763,18 @@ const mapWithConcurrency = async (items, concurrency, task) => {
42574
42763
  if (errors.length > 0) throw errors[0];
42575
42764
  return results;
42576
42765
  };
42577
- const getPublicEnvPrefix = (framework) => {
42578
- switch (framework) {
42579
- case "nextjs": return "NEXT_PUBLIC_*";
42580
- case "vite":
42581
- case "tanstack-start": return "VITE_*";
42582
- case "cra": return "REACT_APP_*";
42583
- case "gatsby": return "GATSBY_*";
42584
- default: return null;
42585
- }
42586
- };
42587
- const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
42588
- const publicEnvPrefix = getPublicEnvPrefix(project.framework);
42589
- if (!publicEnvPrefix) return fallbackRecommendation;
42590
- 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`;
42591
- };
42592
42766
  const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
42767
+ const ACCESSORS_MIN_VERSION = "3.15.0";
42768
+ const supportsAccessors = (reanimatedVersion) => {
42769
+ if (reanimatedVersion === null) return true;
42770
+ const lowerBound = import_semver.validRange(reanimatedVersion) !== null ? import_semver.minVersion(reanimatedVersion) : import_semver.coerce(reanimatedVersion);
42771
+ if (lowerBound === null) return true;
42772
+ return import_semver.gte(lowerBound, ACCESSORS_MIN_VERSION);
42773
+ };
42593
42774
  const appendReanimatedSharedValueHint = (help, rule, project) => {
42594
42775
  if (rule !== "immutability") return help;
42595
42776
  if (!project.hasReanimated) return help;
42777
+ if (!supportsAccessors(project.reanimatedVersion)) return help;
42596
42778
  if (!help) return REANIMATED_SHARED_VALUE_HINT;
42597
42779
  return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
42598
42780
  };
@@ -42754,13 +42936,13 @@ const REACT_USE_BINDING_RESOLUTION = {
42754
42936
  isReactUseBinding: true,
42755
42937
  isReactNamespaceBinding: false
42756
42938
  };
42757
- const getScriptKind = (filename) => {
42939
+ const getScriptKind$2 = (filename) => {
42758
42940
  if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
42759
42941
  if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
42760
42942
  if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
42761
42943
  return ts.ScriptKind.JS;
42762
42944
  };
42763
- const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
42945
+ const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
42764
42946
  const unwrapExpression = (expression) => {
42765
42947
  let currentExpression = expression;
42766
42948
  while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
@@ -43009,8 +43191,8 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
43009
43191
  return matchedIdentifier;
43010
43192
  };
43011
43193
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
43012
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filename));
43013
- const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset(sourceText, utf8Offset));
43194
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
43195
+ const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
43014
43196
  if (!useIdentifier) return null;
43015
43197
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
43016
43198
  };
@@ -43031,6 +43213,201 @@ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
43031
43213
  const bindingResolution = resolveUseCallBinding(sourceText, absolutePath, primaryLabel.span.offset);
43032
43214
  return bindingResolution !== null && !bindingResolution.isReactUseBinding;
43033
43215
  };
43216
+ const SUPPRESSED_COMPILER_CODES = new Set(["react-hooks-js(immutability)", "react-hooks-js(refs)"]);
43217
+ const WORKLET_DIRECTIVE = "worklet";
43218
+ const WORKLET_ACCEPTING_CALLEES = new Set([
43219
+ "useAnimatedStyle",
43220
+ "useAnimatedProps",
43221
+ "useDerivedValue",
43222
+ "useAnimatedReaction",
43223
+ "useAnimatedScrollHandler",
43224
+ "useAnimatedGestureHandler",
43225
+ "useFrameCallback",
43226
+ "runOnUI",
43227
+ "executeOnUIRuntimeSync"
43228
+ ]);
43229
+ const GESTURE_ROOT_IDENTIFIER = "Gesture";
43230
+ const getScriptKind$1 = (filename) => {
43231
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43232
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43233
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43234
+ return ts.ScriptKind.JS;
43235
+ };
43236
+ const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
43237
+ const hasWorkletDirective = (node) => {
43238
+ if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
43239
+ const body = node.body;
43240
+ if (!body || !ts.isBlock(body)) return false;
43241
+ for (const statement of body.statements) {
43242
+ if (!ts.isExpressionStatement(statement)) return false;
43243
+ if (!ts.isStringLiteral(statement.expression)) return false;
43244
+ if (statement.expression.text === WORKLET_DIRECTIVE) return true;
43245
+ }
43246
+ return false;
43247
+ };
43248
+ const rootIdentifierOfCallChain = (expression) => {
43249
+ let currentExpression = expression;
43250
+ while (true) {
43251
+ if (ts.isIdentifier(currentExpression)) return currentExpression.text;
43252
+ if (ts.isPropertyAccessExpression(currentExpression)) {
43253
+ currentExpression = currentExpression.expression;
43254
+ continue;
43255
+ }
43256
+ if (ts.isCallExpression(currentExpression)) {
43257
+ currentExpression = currentExpression.expression;
43258
+ continue;
43259
+ }
43260
+ return null;
43261
+ }
43262
+ };
43263
+ const isWorkletAcceptingCall = (callExpression) => {
43264
+ const callee = callExpression.expression;
43265
+ if (ts.isIdentifier(callee)) return WORKLET_ACCEPTING_CALLEES.has(callee.text);
43266
+ if (ts.isPropertyAccessExpression(callee)) {
43267
+ if (WORKLET_ACCEPTING_CALLEES.has(callee.name.text)) return true;
43268
+ return rootIdentifierOfCallChain(callee) === GESTURE_ROOT_IDENTIFIER;
43269
+ }
43270
+ return false;
43271
+ };
43272
+ const isFunctionWorklet = (functionNode) => {
43273
+ if (hasWorkletDirective(functionNode)) return true;
43274
+ const parent = functionNode.parent;
43275
+ return ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === functionNode) && isWorkletAcceptingCall(parent);
43276
+ };
43277
+ const findNodeAtOffset = (sourceFile, targetOffset) => {
43278
+ let matchedNode = null;
43279
+ const visit = (node) => {
43280
+ if (node.getStart(sourceFile) > targetOffset || node.getEnd() <= targetOffset) return;
43281
+ matchedNode = node;
43282
+ ts.forEachChild(node, visit);
43283
+ };
43284
+ visit(sourceFile);
43285
+ return matchedNode;
43286
+ };
43287
+ const isOffsetInsideWorklet = (sourceFile, targetOffset) => {
43288
+ let currentNode = findNodeAtOffset(sourceFile, targetOffset);
43289
+ while (currentNode) {
43290
+ if (ts.isFunctionLike(currentNode) && isFunctionWorklet(currentNode)) return true;
43291
+ currentNode = currentNode.parent;
43292
+ }
43293
+ return false;
43294
+ };
43295
+ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirectory) => {
43296
+ if (!SUPPRESSED_COMPILER_CODES.has(diagnostic.code)) return false;
43297
+ if (!project.hasReanimated) return false;
43298
+ const primaryLabel = diagnostic.labels[0];
43299
+ if (!primaryLabel) return false;
43300
+ const absolutePath = Path.isAbsolute(diagnostic.filename) ? diagnostic.filename : Path.join(rootDirectory, diagnostic.filename);
43301
+ let sourceText;
43302
+ try {
43303
+ sourceText = NFS.readFileSync(absolutePath, "utf-8");
43304
+ } catch {
43305
+ return false;
43306
+ }
43307
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
43308
+ };
43309
+ const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
43310
+ const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
43311
+ const REACT_COMPILER_PLUGIN = "react-hooks-js";
43312
+ const getScriptKind = (filename) => {
43313
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43314
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43315
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43316
+ return ts.ScriptKind.JS;
43317
+ };
43318
+ const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
43319
+ const nodeLineRange = (sourceFile, node) => ({
43320
+ startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
43321
+ endLine: sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1
43322
+ });
43323
+ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
43324
+ let foundRange = null;
43325
+ const visit = (node) => {
43326
+ if (foundRange) return;
43327
+ if (ts.isFunctionLike(node)) {
43328
+ const range = nodeLineRange(sourceFile, node);
43329
+ if (range.startLine <= targetLine && targetLine <= range.endLine) foundRange = range;
43330
+ return;
43331
+ }
43332
+ ts.forEachChild(node, visit);
43333
+ };
43334
+ visit(sourceFile);
43335
+ return foundRange;
43336
+ };
43337
+ const NEWLINE_BYTE = 10;
43338
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
43339
+ let lineNumber = 1;
43340
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
43341
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
43342
+ return lineNumber;
43343
+ };
43344
+ /**
43345
+ * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
43346
+ * whose enclosing function also carries a `react-hooks-js` (React
43347
+ * Compiler) diagnostic. Every react-hooks-js finding marks a function the
43348
+ * compiler could NOT compile (a bail-out / skip / preserve-manual-
43349
+ * memoization failure), so "React Compiler already caches every value
43350
+ * here" is false for that function and the manual memoization is
43351
+ * load-bearing, not dead weight.
43352
+ *
43353
+ * Matching is per enclosing function, not per file: a bailed-out
43354
+ * component must not silence redundant-memo findings in a sibling
43355
+ * component the compiler did optimize.
43356
+ *
43357
+ * Module-scope `memo(() => …)` wrappers have no enclosing function; for
43358
+ * those the memo call's own span is the containment range, so a bail-out
43359
+ * inside the inline-wrapped component still suppresses. Every failure
43360
+ * path (unreadable file, missing span) fails open and keeps the
43361
+ * diagnostic.
43362
+ */
43363
+ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) => {
43364
+ const compilerDiagnosticLinesByFile = /* @__PURE__ */ new Map();
43365
+ let hasManualMemoizationDiagnostic = false;
43366
+ for (const diagnostic of diagnostics) {
43367
+ if (isManualMemoizationDiagnostic(diagnostic)) {
43368
+ hasManualMemoizationDiagnostic = true;
43369
+ continue;
43370
+ }
43371
+ if (diagnostic.plugin !== REACT_COMPILER_PLUGIN || diagnostic.line <= 0) continue;
43372
+ const lines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
43373
+ if (lines) lines.push(diagnostic.line);
43374
+ else compilerDiagnosticLinesByFile.set(diagnostic.filePath, [diagnostic.line]);
43375
+ }
43376
+ if (!hasManualMemoizationDiagnostic || compilerDiagnosticLinesByFile.size === 0) return diagnostics;
43377
+ const parsedFileCache = /* @__PURE__ */ new Map();
43378
+ const readAndParse = (filePath) => {
43379
+ const cached = parsedFileCache.get(filePath);
43380
+ if (cached !== void 0) return cached;
43381
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
43382
+ let parsed;
43383
+ try {
43384
+ const buffer = NFS.readFileSync(absolutePath);
43385
+ parsed = {
43386
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
43387
+ buffer
43388
+ };
43389
+ } catch {
43390
+ parsed = null;
43391
+ }
43392
+ parsedFileCache.set(filePath, parsed);
43393
+ return parsed;
43394
+ };
43395
+ const isInsideBailedOutFunction = (diagnostic) => {
43396
+ const compilerLines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
43397
+ if (!compilerLines || diagnostic.line <= 0) return false;
43398
+ const parsed = readAndParse(diagnostic.filePath);
43399
+ if (!parsed) return false;
43400
+ let containmentRange = findOutermostEnclosingFunctionRange(parsed.sourceFile, diagnostic.line);
43401
+ if (!containmentRange && diagnostic.offset !== void 0 && diagnostic.length !== void 0) containmentRange = {
43402
+ startLine: diagnostic.line,
43403
+ endLine: lineOfUtf8Offset(parsed.buffer, diagnostic.offset + diagnostic.length)
43404
+ };
43405
+ if (!containmentRange) return false;
43406
+ const { startLine, endLine } = containmentRange;
43407
+ return compilerLines.some((compilerLine) => startLine <= compilerLine && compilerLine <= endLine);
43408
+ };
43409
+ return diagnostics.filter((diagnostic) => !isManualMemoizationDiagnostic(diagnostic) || !isInsideBailedOutFunction(diagnostic));
43410
+ };
43034
43411
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
43035
43412
  const LEADING_SEVERITY_LABEL_PATTERN = /^(?:Error|Warning):\s*/;
43036
43413
  const TRAILING_PERIOD_PATTERN = /\.$/;
@@ -43066,8 +43443,14 @@ const PLUGIN_CATEGORY_MAP = {
43066
43443
  };
43067
43444
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
43068
43445
  const getRuleRecommendation = (ruleName, project) => {
43069
- if (ruleName === "no-secrets-in-client-code") return buildNoSecretsRecommendation(project, reactDoctorPlugin.rules["no-secrets-in-client-code"]?.recommendation ?? "Move secrets to server-only code");
43070
- return reactDoctorPlugin.rules[ruleName]?.recommendation;
43446
+ const rule = reactDoctorPlugin.rules[ruleName];
43447
+ if (!rule) return void 0;
43448
+ if (rule.recommendationFor) {
43449
+ const capabilities = getCapabilities(project);
43450
+ const conditionalRecommendation = rule.recommendationFor((capability) => capabilities.has(capability));
43451
+ if (conditionalRecommendation !== void 0) return conditionalRecommendation;
43452
+ }
43453
+ return rule.recommendation;
43071
43454
  };
43072
43455
  const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
43073
43456
  const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
@@ -43170,7 +43553,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43170
43553
  minifiedFileCache.set(absolutePath, minified);
43171
43554
  return minified;
43172
43555
  };
43173
- return parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
43556
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
43174
43557
  const { plugin, rule } = parseRuleCode(diagnostic.code);
43175
43558
  const primaryLabel = diagnostic.labels[0];
43176
43559
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -43197,7 +43580,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43197
43580
  ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
43198
43581
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
43199
43582
  };
43200
- });
43583
+ }), rootDirectory);
43201
43584
  };
43202
43585
  const buildOxlintChildEnv = (sourceEnv) => {
43203
43586
  const childEnv = {};
@@ -43221,7 +43604,9 @@ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
43221
43604
  * - `OxlintBatchExceeded { kind: "output-too-large" }` — stdout+stderr
43222
43605
  * crossed `OXLINT_OUTPUT_MAX_BYTES`.
43223
43606
  * - `OxlintBatchExceeded { kind: "oom" | "killed" }` — child exited
43224
- * on a signal (SIGABRT → OOM, others → generic kill).
43607
+ * on a signal (SIGABRT → OOM, others → generic kill), or with one
43608
+ * of the abort exit codes Windows reports instead of a signal
43609
+ * (`ABORT_EXIT_CODES` → OOM).
43225
43610
  * - `OxlintSpawnFailed { cause }` — `spawn` itself errored, or the
43226
43611
  * child exited successfully but printed only stderr.
43227
43612
  *
@@ -43287,7 +43672,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
43287
43672
  clearAbortListener();
43288
43673
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
43289
43674
  });
43290
- child.on("close", (_code, signal) => {
43675
+ child.on("close", (code, signal) => {
43291
43676
  clearTimeout(timeoutHandle);
43292
43677
  clearAbortListener();
43293
43678
  if (didKillForSize) {
@@ -43297,10 +43682,11 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
43297
43682
  }) }));
43298
43683
  return;
43299
43684
  }
43300
- if (signal) {
43685
+ const isAbortExitCode = code !== null && ABORT_EXIT_CODES.has(code);
43686
+ if (signal || isAbortExitCode) {
43301
43687
  const stderrOutput = Buffer.concat(stderrBuffers).toString("utf-8").trim();
43302
- const isOom = signal === "SIGABRT";
43303
- const detailParts = [`killed by ${signal}`];
43688
+ const isOom = signal === "SIGABRT" || isAbortExitCode;
43689
+ const detailParts = [signal ? `killed by ${signal}` : `aborted with exit code ${code}`];
43304
43690
  if (isOom) detailParts.push("try scanning fewer files with --diff");
43305
43691
  if (stderrOutput) detailParts.push(stderrOutput);
43306
43692
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
@@ -43343,12 +43729,19 @@ const isParallelismRelatedSpawnError = (error) => {
43343
43729
  * (surfaced via `onPartialFailure`) so JSON-mode consumers see WHICH
43344
43730
  * files were skipped instead of silently losing them.
43345
43731
  *
43346
- * Parallel runs (concurrency > 1) get one extra safety net: if the pass
43347
- * fails with a resource-exhaustion error that's exclusive to running
43348
- * many oxlint subprocesses at once (EAGAIN / EMFILE / ENFILE / ENOMEM —
43349
- * see `isParallelismRelatedSpawnError`), the whole pass replays once
43350
- * with a single worker. That's the only failure a serial replay can
43351
- * clear, so every other error class is left to propagate.
43732
+ * Parallel runs (concurrency > 1) get two extra safety nets:
43733
+ * - If the pass fails with a resource-exhaustion error that's exclusive
43734
+ * to running many oxlint subprocesses at once (EAGAIN / EMFILE /
43735
+ * ENFILE / ENOMEM — see `isParallelismRelatedSpawnError`), the whole
43736
+ * pass replays once with a single worker. That's the only failure a
43737
+ * serial replay can clear, so every other error class is left to
43738
+ * propagate.
43739
+ * - Files dropped because oxlint's native binding SIGABRT'd under
43740
+ * memory pressure (`OxlintBatchExceeded { kind: "oom" }`) get one
43741
+ * serial single-file-batch rescue pass: the OOM is usually a function
43742
+ * of N concurrent oxlint allocator arenas, not the file itself, so a
43743
+ * lone process with the machine to itself typically clears it —
43744
+ * turning a partial scan into a complete (if slower) one.
43352
43745
  *
43353
43746
  * Errors that aren't splittable and aren't parallelism-related (oxlint
43354
43747
  * config crash, JS plugin resolution failure, etc.) propagate to the
@@ -43358,13 +43751,16 @@ const isParallelismRelatedSpawnError = (error) => {
43358
43751
  const spawnLintBatches = async (input) => {
43359
43752
  const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
43360
43753
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
43361
- const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
43362
- const runBatchPass = async (concurrency) => {
43754
+ let rescueDeadlineEpochMs;
43755
+ const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0 || rescueDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(rescueDeadlineEpochMs) === 0;
43756
+ const runBatchPass = async (concurrency, passBatches, passOnFileProgress) => {
43757
+ const totalFileCount = passBatches.reduce((sum, batch) => sum + batch.length, 0);
43363
43758
  const allDiagnostics = [];
43364
43759
  const droppedFiles = [];
43760
+ const oomDroppedFiles = [];
43365
43761
  const deadlineSkippedFiles = [];
43366
- const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0;
43367
43762
  let firstDropReason = null;
43763
+ let firstNonOomDropReason = null;
43368
43764
  const spawnLintBatch = async (batch, depth, batchState) => {
43369
43765
  if (isPastDeadline()) {
43370
43766
  deadlineSkippedFiles.push(...batch);
@@ -43386,12 +43782,14 @@ const spawnLintBatches = async (input) => {
43386
43782
  const isDepthCapReached = depth >= splitMaxDepth;
43387
43783
  if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
43388
43784
  droppedFiles.push(...batch);
43389
- if (firstDropReason === null) {
43390
- let limitHint = "";
43391
- if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
43392
- else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
43393
- firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
43394
- }
43785
+ const isOomDrop = error.reason._tag === "OxlintBatchExceeded" && error.reason.kind === "oom";
43786
+ if (isOomDrop) oomDroppedFiles.push(...batch);
43787
+ let limitHint = "";
43788
+ if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
43789
+ else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
43790
+ const dropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
43791
+ firstDropReason ??= dropReason;
43792
+ if (!isOomDrop) firstNonOomDropReason ??= dropReason;
43395
43793
  return [];
43396
43794
  }
43397
43795
  const splitIndex = Math.ceil(batch.length / 2);
@@ -43401,16 +43799,16 @@ const spawnLintBatches = async (input) => {
43401
43799
  let startedFileCount = 0;
43402
43800
  let scannedFileCount = 0;
43403
43801
  let displayedFileCount = 0;
43404
- const progressTimer = onFileProgress && totalFileCount > 1 ? setInterval(() => {
43802
+ const progressTimer = passOnFileProgress && totalFileCount > 1 ? setInterval(() => {
43405
43803
  const ceiling = Math.min(startedFileCount, totalFileCount - 1);
43406
43804
  if (displayedFileCount < ceiling) {
43407
43805
  displayedFileCount += 1;
43408
- onFileProgress(displayedFileCount, totalFileCount);
43806
+ passOnFileProgress(displayedFileCount, totalFileCount);
43409
43807
  }
43410
43808
  }, 50) : null;
43411
43809
  progressTimer?.unref?.();
43412
43810
  try {
43413
- const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
43811
+ const batchResults = await mapWithConcurrency(passBatches, concurrency, async (batch) => {
43414
43812
  if (isPastDeadline()) {
43415
43813
  deadlineSkippedFiles.push(...batch);
43416
43814
  return [];
@@ -43422,9 +43820,9 @@ const spawnLintBatches = async (input) => {
43422
43820
  };
43423
43821
  const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
43424
43822
  scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
43425
- if (onFileProgress) {
43823
+ if (passOnFileProgress) {
43426
43824
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
43427
- onFileProgress(displayedFileCount, totalFileCount);
43825
+ passOnFileProgress(displayedFileCount, totalFileCount);
43428
43826
  }
43429
43827
  return batchDiagnostics;
43430
43828
  });
@@ -43432,22 +43830,53 @@ const spawnLintBatches = async (input) => {
43432
43830
  } finally {
43433
43831
  if (progressTimer !== null) clearInterval(progressTimer);
43434
43832
  }
43435
- const reportSkippedFiles = (skippedFiles, buildMessage) => {
43436
- if (skippedFiles.length === 0 || !onPartialFailure) return;
43437
- onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
43833
+ return {
43834
+ diagnostics: allDiagnostics,
43835
+ droppedFiles,
43836
+ oomDroppedFiles,
43837
+ deadlineSkippedFiles,
43838
+ firstDropReason,
43839
+ firstNonOomDropReason
43438
43840
  };
43439
- const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
43440
- reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
43441
- reportSkippedFiles(deadlineSkippedFiles, (fileListPreview) => `${deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
43442
- return allDiagnostics;
43443
43841
  };
43444
- let diagnostics;
43842
+ let outcome;
43843
+ let effectiveConcurrency = requestedConcurrency;
43445
43844
  try {
43446
- diagnostics = await runBatchPass(requestedConcurrency);
43845
+ outcome = await runBatchPass(requestedConcurrency, fileBatches, onFileProgress);
43447
43846
  } catch (error) {
43448
43847
  if (requestedConcurrency <= 1 || !isParallelismRelatedSpawnError(error)) throw error;
43449
- diagnostics = await runBatchPass(1);
43848
+ effectiveConcurrency = 1;
43849
+ outcome = await runBatchPass(1, fileBatches, onFileProgress);
43850
+ }
43851
+ let { droppedFiles, firstDropReason } = outcome;
43852
+ const diagnostics = outcome.diagnostics;
43853
+ if (outcome.oomDroppedFiles.length > 0 && effectiveConcurrency > 1 && !isPastDeadline()) {
43854
+ rescueDeadlineEpochMs = Date.now() + OXLINT_OOM_RESCUE_BUDGET_MS;
43855
+ const rescuedFiles = /* @__PURE__ */ new Set();
43856
+ let firstRescueFailureReason = null;
43857
+ for (const oomDroppedFile of outcome.oomDroppedFiles) {
43858
+ if (isPastDeadline()) break;
43859
+ try {
43860
+ const rescueOutcome = await runBatchPass(1, [[oomDroppedFile]], void 0);
43861
+ diagnostics.push(...rescueOutcome.diagnostics);
43862
+ if (rescueOutcome.droppedFiles.length > 0 || rescueOutcome.deadlineSkippedFiles.length > 0) firstRescueFailureReason ??= rescueOutcome.firstDropReason;
43863
+ else rescuedFiles.add(oomDroppedFile);
43864
+ } catch (error) {
43865
+ firstRescueFailureReason ??= error instanceof Error ? error.message : String(error);
43866
+ }
43867
+ }
43868
+ rescueDeadlineEpochMs = void 0;
43869
+ droppedFiles = droppedFiles.filter((filePath) => !rescuedFiles.has(filePath));
43870
+ if (droppedFiles.length === 0) firstDropReason = null;
43871
+ else firstDropReason = firstRescueFailureReason ?? outcome.firstNonOomDropReason ?? firstDropReason;
43450
43872
  }
43873
+ const reportSkippedFiles = (skippedFiles, buildMessage) => {
43874
+ if (skippedFiles.length === 0 || !onPartialFailure) return;
43875
+ onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
43876
+ };
43877
+ const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
43878
+ reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
43879
+ reportSkippedFiles(outcome.deadlineSkippedFiles, (fileListPreview) => `${outcome.deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
43451
43880
  return dedupeDiagnostics(diagnostics);
43452
43881
  };
43453
43882
  let didValidate = false;
@@ -43708,6 +44137,7 @@ const runOxlint = async (options) => {
43708
44137
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
43709
44138
  const severityControls = buildRuleSeverityControls(userConfig);
43710
44139
  validateRuleRegistration();
44140
+ resetManifestCaches();
43711
44141
  if (includePaths !== void 0 && includePaths.length === 0) return [];
43712
44142
  const pluginPath = resolvePluginPath();
43713
44143
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
@@ -44370,9 +44800,10 @@ const CONTEXT_AXES = [
44370
44800
  }
44371
44801
  ];
44372
44802
  const SCORE_AXES = [...GATED_AXES, ...CONTEXT_AXES];
44373
- const worstGatedAxis = (score) => {
44374
- let worst = GATED_AXES[0];
44375
- for (const axis of GATED_AXES) if (score[axis.key] < score[worst.key]) worst = axis;
44803
+ const gatedAxesForSection = (section) => section === "devDependencies" ? GATED_AXES.filter((axis) => axis.key !== "vulnerability") : GATED_AXES;
44804
+ const worstGatedAxis = (score, gatedAxes) => {
44805
+ let worst = gatedAxes[0];
44806
+ for (const axis of gatedAxes) if (score[axis.key] < score[worst.key]) worst = axis;
44376
44807
  return worst;
44377
44808
  };
44378
44809
  const clampScore = (value) => {
@@ -44396,6 +44827,69 @@ const resolveConcreteVersion = (spec) => {
44396
44827
  if (range === null || range === "*") return null;
44397
44828
  return import_semver.minVersion(trimmed)?.version ?? null;
44398
44829
  };
44830
+ const readTextFileOrNull = (filePath) => {
44831
+ try {
44832
+ return NFS.readFileSync(filePath, "utf-8");
44833
+ } catch {
44834
+ return null;
44835
+ }
44836
+ };
44837
+ const buildNpmLockLookup = (lockText) => {
44838
+ let parsed;
44839
+ try {
44840
+ parsed = JSON.parse(lockText);
44841
+ } catch {
44842
+ return null;
44843
+ }
44844
+ if (typeof parsed !== "object" || parsed === null) return null;
44845
+ const lock = parsed;
44846
+ return (name) => {
44847
+ const entry = lock.packages?.[`node_modules/${name}`] ?? lock.dependencies?.[name];
44848
+ return typeof entry?.version === "string" ? entry.version : null;
44849
+ };
44850
+ };
44851
+ const buildYarnLockLookup = (lockText) => {
44852
+ const lines = lockText.split(/\r?\n/);
44853
+ return (name, spec) => {
44854
+ const descriptor = `${name}@${spec}`;
44855
+ const berryDescriptor = `${name}@npm:${spec}`;
44856
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
44857
+ const line = lines[lineIndex];
44858
+ if (line.startsWith(" ") || line.startsWith("#") || !line.includes(":")) continue;
44859
+ const descriptors = line.replace(/:\s*$/, "").split(",").map((part) => part.trim().replace(/^"|"$/g, ""));
44860
+ if (!descriptors.includes(descriptor) && !descriptors.includes(berryDescriptor)) continue;
44861
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && (lines[bodyIndex].startsWith(" ") || lines[bodyIndex] === ""); bodyIndex += 1) {
44862
+ const versionMatch = /^\s+version:?\s+"?([^"\s]+)"?/.exec(lines[bodyIndex]);
44863
+ if (versionMatch) return versionMatch[1];
44864
+ }
44865
+ return null;
44866
+ }
44867
+ return null;
44868
+ };
44869
+ };
44870
+ const buildPnpmLockLookup = (lockText) => {
44871
+ return (name) => {
44872
+ const entryPattern = new RegExp(`^ {4,8}['"]?${name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}['"]?:\\s*$`);
44873
+ const lines = lockText.split(/\r?\n/);
44874
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
44875
+ if (!entryPattern.test(lines[lineIndex])) continue;
44876
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && /^ {5,}\S/.test(lines[bodyIndex]); bodyIndex += 1) {
44877
+ const versionMatch = /^\s+version:\s+['"]?([^'"\s(]+)/.exec(lines[bodyIndex]);
44878
+ if (versionMatch) return versionMatch[1];
44879
+ }
44880
+ }
44881
+ return null;
44882
+ };
44883
+ };
44884
+ const buildLockedVersionLookup = (rootDirectory) => {
44885
+ const npmLockText = readTextFileOrNull(Path.join(rootDirectory, "package-lock.json"));
44886
+ if (npmLockText !== null) return buildNpmLockLookup(npmLockText);
44887
+ const yarnLockText = readTextFileOrNull(Path.join(rootDirectory, "yarn.lock"));
44888
+ if (yarnLockText !== null) return buildYarnLockLookup(yarnLockText);
44889
+ const pnpmLockText = readTextFileOrNull(Path.join(rootDirectory, "pnpm-lock.yaml"));
44890
+ if (pnpmLockText !== null) return buildPnpmLockLookup(pnpmLockText);
44891
+ return null;
44892
+ };
44399
44893
  const locateDependencyKey = (packageJsonText, section, name) => {
44400
44894
  const needle = `"${name}"`;
44401
44895
  const sectionHeader = new RegExp(`"${section}"\\s*:\\s*\\{`);
@@ -44422,7 +44916,7 @@ const locateDependencyKey = (packageJsonText, section, name) => {
44422
44916
  }
44423
44917
  return null;
44424
44918
  };
44425
- const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies) => {
44919
+ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies, lockedVersionLookup) => {
44426
44920
  const sectionByName = /* @__PURE__ */ new Map();
44427
44921
  for (const name of Object.keys(packageJson.dependencies ?? {})) sectionByName.set(name, "dependencies");
44428
44922
  if (includeDevDependencies) {
@@ -44432,13 +44926,18 @@ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDepe
44432
44926
  for (const [name, section] of sectionByName) {
44433
44927
  if (SUPPLY_CHAIN_IGNORED_PACKAGES.has(name)) continue;
44434
44928
  const spec = (packageJson[section] ?? {})[name] ?? "";
44435
- const version = resolveConcreteVersion(spec);
44436
- if (version === null) continue;
44929
+ const floorVersion = resolveConcreteVersion(spec);
44930
+ if (floorVersion === null) continue;
44931
+ const lockedVersion = lockedVersionLookup?.(name, spec) ?? null;
44932
+ const lockSatisfiesSpec = lockedVersion !== null && import_semver.valid(lockedVersion) !== null && import_semver.satisfies(lockedVersion, spec);
44933
+ const isExactPin = import_semver.valid(spec.trim()) !== null;
44437
44934
  const location = locateDependencyKey(packageJsonText, section, name);
44438
44935
  dependencies.push({
44439
44936
  name,
44440
- version,
44937
+ version: lockSatisfiesSpec ? lockedVersion : floorVersion,
44441
44938
  spec,
44939
+ section,
44940
+ resolution: isExactPin ? "pin" : lockSatisfiesSpec ? "locked" : "floor",
44442
44941
  line: location?.line ?? 0,
44443
44942
  column: location?.column ?? 0
44444
44943
  });
@@ -44589,7 +45088,11 @@ const formatAlertReason = (topAlerts, totalCount) => {
44589
45088
  }
44590
45089
  return `Socket flagged ${totalCount} alerts (${topAlerts.map((alert) => friendlyAlertType(alert.type)).join(", ")}${totalCount > topAlerts.length ? ` (+${totalCount - topAlerts.length} more)` : ""}); most severe: ${displaySeverity(topAlerts[0].severity)}.`;
44591
45090
  };
44592
- const formatDependencyIdentity = (dependency) => import_semver.valid(dependency.spec) !== null ? `${dependency.name}@${dependency.version}` : `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
45091
+ const formatDependencyIdentity = (dependency) => {
45092
+ if (dependency.resolution === "pin") return `${dependency.name}@${dependency.version}`;
45093
+ if (dependency.resolution === "locked") return `${dependency.name}@${dependency.version} (lockfile resolution of "${dependency.spec}")`;
45094
+ return `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
45095
+ };
44593
45096
  const buildSupplyChainHelp = (dependency, failingAxis, topAlerts, packagePageUrl, options) => {
44594
45097
  const hasCriticalAlert = topAlerts.some((alert) => alert.severity.toLowerCase() === "critical");
44595
45098
  const entry = `\`"${dependency.name}": "${dependency.spec}"\``;
@@ -44632,7 +45135,7 @@ const buildLowScoreDiagnostic = (dependency, artifact, failingAxis, options) =>
44632
45135
  const checkSupplyChain = (input) => gen(function* () {
44633
45136
  const options = resolveOptions(input.userConfig);
44634
45137
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
44635
- const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
45138
+ const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies, buildLockedVersionLookup(input.rootDirectory));
44636
45139
  if (dependencies.length === 0) return [];
44637
45140
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
44638
45141
  if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
@@ -44641,7 +45144,7 @@ const checkSupplyChain = (input) => gen(function* () {
44641
45144
  for (let index = 0; index < dependencies.length; index += 1) {
44642
45145
  const artifact = artifacts[index];
44643
45146
  if (!artifact) continue;
44644
- const worstAxis = worstGatedAxis(artifact.score);
45147
+ const worstAxis = worstGatedAxis(artifact.score, gatedAxesForSection(dependencies[index].section));
44645
45148
  if (toHundred(artifact.score[worstAxis.key]) >= options.minScore) continue;
44646
45149
  diagnostics.push(buildLowScoreDiagnostic(dependencies[index], artifact, worstAxis, options));
44647
45150
  }
@@ -45342,6 +45845,13 @@ const summarizeDiagnostics = (diagnostics, worstScore = null, worstScoreLabel =
45342
45845
  scoreLabel: worstScoreLabel
45343
45846
  };
45344
45847
  };
45848
+ /**
45849
+ * Whether project discovery resolved a React-compatible runtime (React or
45850
+ * Preact, which ships the same hooks + JSX model). This is the same
45851
+ * predicate that grants the `react` capability, so `false` means every
45852
+ * React-runtime rule family was gated off for the project.
45853
+ */
45854
+ const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
45345
45855
  const toJsonDiff = (diff) => {
45346
45856
  if (!diff) return null;
45347
45857
  return {
@@ -45924,7 +46434,7 @@ const makeNoopConsole = () => ({
45924
46434
  });
45925
46435
  //#endregion
45926
46436
  //#region src/cli/utils/version.ts
45927
- const VERSION = "0.7.1";
46437
+ const VERSION = "0.7.2-dev.08b768b";
45928
46438
  //#endregion
45929
46439
  //#region src/cli/utils/json-mode.ts
45930
46440
  let context = null;
@@ -46293,13 +46803,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
46293
46803
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
46294
46804
  * standard `SENTRY_RELEASE` override.
46295
46805
  */
46296
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.1`;
46806
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.2-dev.08b768b`;
46297
46807
  /**
46298
46808
  * Deployment environment shown in Sentry's environment filter. Defaults to
46299
46809
  * `production` for tagged releases and `development` for dev/unbuilt versions,
46300
46810
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
46301
46811
  */
46302
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.1") ? "development" : "production");
46812
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.2-dev.08b768b") ? "development" : "production");
46303
46813
  /**
46304
46814
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
46305
46815
  * (set to `0` to disable tracing) and falls back to
@@ -58143,7 +58653,7 @@ const buildRuntimeLayers = (input) => {
58143
58653
  const linterLayer = input.shouldSkipLint ? Linter.layerOf([]) : Linter.layerOxlint;
58144
58654
  const deadCodeLayer = input.shouldRunDeadCode ? DeadCode.layerNode : DeadCode.layerOf([]);
58145
58655
  const scoreLayer = input.shouldComputeScore ? Score.layerHttp : Score.layerOf(null);
58146
- const supplyChainLayer = input.userConfig?.supplyChain?.enabled === false ? SupplyChain.layerOf([]) : SupplyChain.layerNode;
58656
+ const supplyChainLayer = input.shouldRunSupplyChain ? SupplyChain.layerNode : SupplyChain.layerOf([]);
58147
58657
  const projectLayer = input.projectInfoOverride === void 0 ? Project.layerNode : Project.layerOf(input.projectInfoOverride);
58148
58658
  const progressLayer = input.shouldShowProgressSpinners ? Progress.layerOra(buildSpinnerProgressHandle) : Progress.layerNoop;
58149
58659
  const baseLayers = mergeAll$1(projectLayer, input.hasConfigOverride ? Config.layerOf({
@@ -58499,21 +59009,22 @@ const resolveDotenvFingerprint = (projectDirectory) => {
58499
59009
  return [];
58500
59010
  }
58501
59011
  };
58502
- const hasHiddenTrackedFileState = (projectDirectory) => {
59012
+ const isGitIdentityTrustworthy = (projectDirectory) => {
58503
59013
  const output = runGit(projectDirectory, ["ls-files", "-v"]);
58504
- if (output === null) return true;
58505
- return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
59014
+ if (output === null) return false;
59015
+ const entryLines = output.split("\n").filter((line) => line.length > 0);
59016
+ return entryLines.length > 0 && entryLines.every((line) => line[0] === "H");
58506
59017
  };
58507
59018
  const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
58508
59019
  const readPersistedCache = (cacheFilePath) => {
58509
59020
  try {
58510
59021
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
58511
- if (!isRecord(parsed) || parsed.version !== 3) return {
58512
- version: 3,
59022
+ if (!isRecord(parsed) || parsed.version !== 4) return {
59023
+ version: 4,
58513
59024
  entries: []
58514
59025
  };
58515
59026
  if (!Array.isArray(parsed.entries)) return {
58516
- version: 3,
59027
+ version: 4,
58517
59028
  entries: []
58518
59029
  };
58519
59030
  const entries = [];
@@ -58523,12 +59034,12 @@ const readPersistedCache = (cacheFilePath) => {
58523
59034
  entries.push(entry);
58524
59035
  }
58525
59036
  return {
58526
- version: 3,
59037
+ version: 4,
58527
59038
  entries
58528
59039
  };
58529
59040
  } catch {
58530
59041
  return {
58531
- version: 3,
59042
+ version: 4,
58532
59043
  entries: []
58533
59044
  };
58534
59045
  }
@@ -58560,6 +59071,7 @@ const resolveProjectIdentity = (projectDirectory) => {
58560
59071
  return path.resolve(projectDirectory);
58561
59072
  }
58562
59073
  };
59074
+ const resolveManifestContentHash = (projectDirectory) => hashFileContents(getPackageJsonPath(projectDirectory));
58563
59075
  const fileFingerprint = (filePath) => {
58564
59076
  try {
58565
59077
  const stat = fs.statSync(filePath);
@@ -58582,14 +59094,14 @@ const resolveToolchainFingerprint = (nodeBinaryPath) => {
58582
59094
  };
58583
59095
  const buildScanResultCacheKey = (input) => {
58584
59096
  if (isCacheGloballyDisabled()) return null;
58585
- if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
59097
+ if (!isGitIdentityTrustworthy(input.projectDirectory)) return null;
58586
59098
  const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
58587
59099
  if (worktreeFingerprint === null) return null;
58588
59100
  const headSha = readHeadSha(input.projectDirectory);
58589
59101
  if (headSha === null) return null;
58590
59102
  if (stringifyStableJson(input.userConfig) === null) return null;
58591
59103
  const cacheKeyJson = stringifyStableJson({
58592
- schemaVersion: 3,
59104
+ schemaVersion: 4,
58593
59105
  projectIdentity: resolveProjectIdentity(input.projectDirectory),
58594
59106
  headSha,
58595
59107
  worktreeFingerprint,
@@ -58604,6 +59116,7 @@ const buildScanResultCacheKey = (input) => {
58604
59116
  engineOptions: {
58605
59117
  lint: input.options.lint,
58606
59118
  deadCode: input.options.deadCode,
59119
+ supplyChain: input.options.supplyChain,
58607
59120
  includePaths: [...input.options.includePaths].sort(),
58608
59121
  customRulesOnly: input.options.customRulesOnly,
58609
59122
  respectInlineDisables: input.options.respectInlineDisables,
@@ -58629,17 +59142,24 @@ const createScanResultCache = (projectDirectory) => {
58629
59142
  for (const entry of persistedCache.entries) entries.set(entry.key, entry);
58630
59143
  const persist = () => {
58631
59144
  writePersistedCache(cacheFilePath, {
58632
- version: 3,
59145
+ version: 4,
58633
59146
  entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
58634
59147
  });
58635
59148
  };
58636
59149
  return {
58637
- lookup: (key) => entries.get(key)?.payload ?? null,
59150
+ lookup: (key) => {
59151
+ const payload = entries.get(key)?.payload;
59152
+ if (payload === void 0) return null;
59153
+ return resolveProjectIdentity(payload.directory) === resolveProjectIdentity(projectDirectory) && (payload.manifestContentHash ?? null) === resolveManifestContentHash(projectDirectory) ? payload : null;
59154
+ },
58638
59155
  store: (key, payload) => {
58639
59156
  entries.set(key, {
58640
59157
  key,
58641
59158
  createdAtMs: Date.now(),
58642
- payload
59159
+ payload: {
59160
+ ...payload,
59161
+ manifestContentHash: resolveManifestContentHash(projectDirectory)
59162
+ }
58643
59163
  });
58644
59164
  persist();
58645
59165
  }
@@ -58806,13 +59326,15 @@ const buildOutcomeAttributes = (input) => {
58806
59326
  const countBySource = {
58807
59327
  config: 0,
58808
59328
  override: 0,
58809
- inline: 0
59329
+ inline: 0,
59330
+ "foreign-inline": 0
58810
59331
  };
58811
59332
  for (const suppression of input.suppressedRuleCounts) countBySource[suppression.source] += suppression.count;
58812
- suppressionRollup.suppressed = countBySource.config + countBySource.override + countBySource.inline;
59333
+ suppressionRollup.suppressed = countBySource.config + countBySource.override + countBySource.inline + countBySource["foreign-inline"];
58813
59334
  suppressionRollup.suppressedConfig = countBySource.config;
58814
59335
  suppressionRollup.suppressedOverride = countBySource.override;
58815
59336
  suppressionRollup.suppressedInline = countBySource.inline;
59337
+ suppressionRollup.suppressedForeignInline = countBySource["foreign-inline"];
58816
59338
  }
58817
59339
  const attributes = {
58818
59340
  ...withNamespace("outcome", {
@@ -58904,6 +59426,7 @@ const buildScanAttributes = (input) => {
58904
59426
  maxDurationMs: input.maxDurationMs,
58905
59427
  lint: input.lint,
58906
59428
  deadCode: input.deadCode,
59429
+ supplyChain: input.supplyChain,
58907
59430
  scoreOnly: input.scoreOnly,
58908
59431
  noScore: input.noScore,
58909
59432
  respectInlineDisables: input.respectInlineDisables,
@@ -60178,6 +60701,7 @@ const buildIgnoredTags = (userConfig) => {
60178
60701
  const mergeInspectOptions = (inputOptions, userConfig) => ({
60179
60702
  lint: inputOptions.lint ?? userConfig?.lint ?? true,
60180
60703
  deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
60704
+ supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
60181
60705
  verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
60182
60706
  outputDirectory: inputOptions.outputDirectory || null,
60183
60707
  scoreOnly: inputOptions.scoreOnly ?? false,
@@ -60217,6 +60741,7 @@ const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorke
60217
60741
  maxDurationMs: options.maxDurationMs,
60218
60742
  lint: options.lint,
60219
60743
  deadCode: options.deadCode,
60744
+ supplyChain: options.supplyChain,
60220
60745
  scoreOnly: options.scoreOnly,
60221
60746
  noScore: options.noScore,
60222
60747
  respectInlineDisables: options.respectInlineDisables,
@@ -60300,6 +60825,7 @@ const runBaselineComparison = async (params) => {
60300
60825
  projectInfoOverride: params.headProjectInfo,
60301
60826
  shouldSkipLint: !params.options.lint || !params.resolvedNodeBinaryPath,
60302
60827
  shouldRunDeadCode: false,
60828
+ shouldRunSupplyChain: params.options.supplyChain,
60303
60829
  shouldComputeScore: false,
60304
60830
  shouldShowProgressSpinners: false,
60305
60831
  oxlintConcurrency: params.options.concurrency
@@ -60388,6 +60914,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
60388
60914
  configSourceDirectory,
60389
60915
  shouldSkipLint: !options.lint || lintBindingMissing,
60390
60916
  shouldRunDeadCode: options.deadCode,
60917
+ shouldRunSupplyChain: options.supplyChain,
60391
60918
  shouldComputeScore: !options.noScore,
60392
60919
  shouldShowProgressSpinners,
60393
60920
  oxlintConcurrency: options.concurrency
@@ -62673,8 +63200,8 @@ const resolveParallelFlag = (parallel) => parallel === false ? 1 : void 0;
62673
63200
  * accepts. Flag-derived fields only (`scoreOnly`, `noScore`, `silent`,
62674
63201
  * `isCi`): every `userConfig` fallback — including `noScore` — lives in
62675
63202
  * `inspect()`'s merge layer, which sees each project's module-merged
62676
- * config. The plain boolean knobs (`lint`, `deadCode`, `verbose`) pass
62677
- * through unchanged for the same reason. This resolver reads
63203
+ * config. The plain boolean knobs (`lint`, `deadCode`, `supplyChain`,
63204
+ * `verbose`) pass through unchanged for the same reason. This resolver reads
62678
63205
  * `userConfig` only to decide the `--blocking warning` gate.
62679
63206
  */
62680
63207
  const resolveCliInspectOptions = (flags, userConfig) => {
@@ -62682,6 +63209,7 @@ const resolveCliInspectOptions = (flags, userConfig) => {
62682
63209
  return {
62683
63210
  lint: flags.lint,
62684
63211
  deadCode: flags.deadCode,
63212
+ supplyChain: flags.supplyChain,
62685
63213
  verbose: flags.verbose,
62686
63214
  outputDirectory: flags.outputDir,
62687
63215
  respectInlineDisables: flags.respectInlineDisables === false ? false : void 0,
@@ -63232,7 +63760,7 @@ const inspectAction = async (directory, flags) => {
63232
63760
  const scanDirectory = projectScanTarget.resolvedDirectory;
63233
63761
  const projectConfig = projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0);
63234
63762
  const projectConfigSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
63235
- const supplyChainEnabled = projectConfig?.supplyChain?.enabled !== false;
63763
+ const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
63236
63764
  let includePaths;
63237
63765
  let supplyChainManifestChanged = false;
63238
63766
  if (isDiffMode) {
@@ -64152,10 +64680,12 @@ const ROOT_FLAG_SPEC = {
64152
64680
  "--no-parallel",
64153
64681
  "--no-respect-inline-disables",
64154
64682
  "--no-score",
64683
+ "--no-supply-chain",
64155
64684
  "--no-telemetry",
64156
64685
  "--no-warnings",
64157
64686
  "--score",
64158
64687
  "--staged",
64688
+ "--supply-chain",
64159
64689
  "--verbose",
64160
64690
  "--version",
64161
64691
  "--warnings",
@@ -64404,7 +64934,7 @@ ${highlighter.dim("Learn more:")}
64404
64934
  ${highlighter.info(CI_URL)}
64405
64935
  `;
64406
64936
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
64407
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
64937
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
64408
64938
  program.action(inspectAction);
64409
64939
  program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
64410
64940
  program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
@@ -64454,4 +64984,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
64454
64984
  export {};
64455
64985
 
64456
64986
  //# sourceMappingURL=cli.js.map
64457
- //# debugId=8cb1a73e-b148-513d-a3eb-69f3a12430a5
64987
+ //# debugId=9aed4e70-dd94-51e8-9810-f8e8945d80a3