react-doctor 0.7.1 → 0.7.2-dev.0eb5293

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]="91ba16f6-8b1a-5cd9-a5d2-fc0b6fce2508")}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",
@@ -36144,7 +36177,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36144
36177
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36145
36178
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36146
36179
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36147
- const CORE_PACKAGE_VERSION = "0.7.1";
36180
+ const CORE_PACKAGE_VERSION = "0.7.2";
36148
36181
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36149
36182
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36150
36183
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -36154,8 +36187,9 @@ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
36154
36187
  const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
36155
36188
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
36156
36189
  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);
36190
+ const isIgnoredDirectoryName = (directoryName) => IGNORED_DIRECTORIES.has(directoryName) || directoryName.startsWith(".") && !SCANNED_DOT_DIRECTORIES.has(directoryName);
36191
+ const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => isIgnoredDirectoryName(segment));
36192
+ const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath) && !GENERATED_SOURCE_DIRECTORY_PATTERN.test(filePath);
36159
36193
  const isMinifiedSource = (absolutePath) => {
36160
36194
  let fileDescriptor;
36161
36195
  try {
@@ -36189,25 +36223,6 @@ const isLargeMinifiedFile = (absolutePath, knownSizeBytes) => {
36189
36223
  cachedIsLargeMinifiedByPath.set(absolutePath, result);
36190
36224
  return result;
36191
36225
  };
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
36226
  function* walkSourceTreeFiles(rootDirectory) {
36212
36227
  const stack = [rootDirectory];
36213
36228
  while (stack.length > 0) {
@@ -36216,7 +36231,7 @@ function* walkSourceTreeFiles(rootDirectory) {
36216
36231
  for (const entry of readDirectoryEntries(currentDirectory)) {
36217
36232
  const absolutePath = Path.join(currentDirectory, entry.name);
36218
36233
  if (entry.isDirectory()) {
36219
- if (!IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
36234
+ if (!isIgnoredDirectoryName(entry.name)) stack.push(absolutePath);
36220
36235
  continue;
36221
36236
  }
36222
36237
  if (entry.isFile()) yield {
@@ -36310,6 +36325,146 @@ const findMonorepoRoot = (startDirectory) => {
36310
36325
  * byte-equivalent copy.
36311
36326
  */
36312
36327
  const isProjectBoundary = (directory) => NFS.existsSync(Path.join(directory, ".git")) || isMonorepoRoot(directory);
36328
+ const TSCONFIG_FILENAME = "tsconfig.json";
36329
+ const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
36330
+ const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
36331
+ const resolvePackageExtendsPath = (extendsValue, fromConfigDirectory) => {
36332
+ const requireFromConfig = createRequire(Path.join(fromConfigDirectory, "tsconfig.json"));
36333
+ const candidates = [
36334
+ extendsValue,
36335
+ ensureJsonExtension(extendsValue),
36336
+ `${extendsValue.replace(/\/$/, "")}/tsconfig.json`
36337
+ ];
36338
+ for (const candidate of candidates) try {
36339
+ return requireFromConfig.resolve(candidate);
36340
+ } catch {
36341
+ continue;
36342
+ }
36343
+ return null;
36344
+ };
36345
+ const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
36346
+ if (isRelativeExtendsValue(extendsValue)) return ensureJsonExtension(Path.resolve(fromConfigDirectory, extendsValue));
36347
+ return resolvePackageExtendsPath(extendsValue, fromConfigDirectory);
36348
+ };
36349
+ const normalizeCompilerOptions = (compilerOptions) => {
36350
+ if (!isPlainObject(compilerOptions)) return { hasExplicitLib: false };
36351
+ const target = typeof compilerOptions.target === "string" ? compilerOptions.target : void 0;
36352
+ const hasExplicitLib = Object.hasOwn(compilerOptions, "lib");
36353
+ return {
36354
+ target,
36355
+ lib: Array.isArray(compilerOptions.lib) ? compilerOptions.lib.filter((entry) => typeof entry === "string") : void 0,
36356
+ hasExplicitLib
36357
+ };
36358
+ };
36359
+ const readTsConfig = (filePath) => {
36360
+ let content;
36361
+ try {
36362
+ content = NFS.readFileSync(filePath, "utf-8");
36363
+ } catch {
36364
+ return null;
36365
+ }
36366
+ const parsed = ts.parseConfigFileTextToJson(filePath, content);
36367
+ if (!isPlainObject(parsed.config)) return null;
36368
+ return {
36369
+ extends: typeof parsed.config.extends === "string" ? parsed.config.extends : void 0,
36370
+ referencePaths: normalizeReferencePaths(parsed.config.references),
36371
+ compilerOptions: normalizeCompilerOptions(parsed.config.compilerOptions)
36372
+ };
36373
+ };
36374
+ const normalizeReferencePaths = (references) => {
36375
+ if (!Array.isArray(references)) return [];
36376
+ return references.map((reference) => isPlainObject(reference) && typeof reference.path === "string" ? reference.path : null).filter((referencePath) => referencePath !== null);
36377
+ };
36378
+ const mergeCompilerOptions = (inherited, current) => {
36379
+ const target = current.target ?? inherited?.target;
36380
+ const hasExplicitLib = current.hasExplicitLib || Boolean(inherited?.hasExplicitLib);
36381
+ return {
36382
+ target,
36383
+ lib: current.hasExplicitLib ? current.lib : inherited?.lib,
36384
+ hasExplicitLib
36385
+ };
36386
+ };
36387
+ const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
36388
+ const realPath = NFS.realpathSync.native(tsConfigPath);
36389
+ if (visitedPaths.has(realPath)) return null;
36390
+ const tsConfig = readTsConfig(realPath);
36391
+ if (!tsConfig) return null;
36392
+ const nextVisitedPaths = new Set(visitedPaths);
36393
+ nextVisitedPaths.add(realPath);
36394
+ if (tsConfig.extends && extendsDepth < 8) {
36395
+ const parentPath = resolveExtendsPath(tsConfig.extends, Path.dirname(realPath));
36396
+ if (parentPath && isFile(parentPath)) return mergeCompilerOptions(readResolvedCompilerOptions(parentPath, extendsDepth + 1, nextVisitedPaths), tsConfig.compilerOptions);
36397
+ }
36398
+ return tsConfig.compilerOptions;
36399
+ };
36400
+ const targetYearIsPreES2023 = (target) => {
36401
+ const year = ES_TARGET_YEAR_BY_NAME[target.toLowerCase()];
36402
+ return year !== void 0 && year < 2023;
36403
+ };
36404
+ const libEntryIncludesES2023Array = (entry) => {
36405
+ const normalizedEntry = entry.toLowerCase();
36406
+ if (normalizedEntry === "esnext" || normalizedEntry === "esnext.array") return true;
36407
+ const esYearMatch = /^es(\d{4})(?:\.(.+))?$/.exec(normalizedEntry);
36408
+ if (!esYearMatch) return false;
36409
+ if (Number(esYearMatch[1]) < 2023) return false;
36410
+ const component = esYearMatch[2];
36411
+ return component === void 0 || component === "array";
36412
+ };
36413
+ const libIncludesES2023 = (lib) => lib.some(libEntryIncludesES2023Array);
36414
+ const compilerOptionsArePreES2023 = (compilerOptions) => {
36415
+ if (compilerOptions.target) return targetYearIsPreES2023(compilerOptions.target);
36416
+ if (compilerOptions.hasExplicitLib) return !libIncludesES2023(compilerOptions.lib ?? []);
36417
+ return false;
36418
+ };
36419
+ const compilerOptionsDeclareTargetOrLib = (compilerOptions) => compilerOptions.hasExplicitLib || compilerOptions.target !== void 0;
36420
+ const detectPreES2023FromConfig = (tsConfigPath, visitedConfigPaths = /* @__PURE__ */ new Set()) => {
36421
+ if (visitedConfigPaths.has(tsConfigPath)) return false;
36422
+ const compilerOptions = readResolvedCompilerOptions(tsConfigPath, 0, /* @__PURE__ */ new Set());
36423
+ if (!compilerOptions) return false;
36424
+ if (!compilerOptionsDeclareTargetOrLib(compilerOptions)) {
36425
+ const tsConfig = readTsConfig(tsConfigPath);
36426
+ if (!tsConfig) return false;
36427
+ const nextVisitedConfigPaths = new Set(visitedConfigPaths);
36428
+ nextVisitedConfigPaths.add(tsConfigPath);
36429
+ const configDirectory = Path.dirname(tsConfigPath);
36430
+ return tsConfig.referencePaths.some((referencePath) => {
36431
+ const resolvedReferencePath = Path.resolve(configDirectory, referencePath);
36432
+ const referencedConfigPath = isFile(resolvedReferencePath) ? resolvedReferencePath : Path.join(resolvedReferencePath, TSCONFIG_FILENAME);
36433
+ return isFile(referencedConfigPath) && detectPreES2023FromConfig(referencedConfigPath, nextVisitedConfigPaths);
36434
+ });
36435
+ }
36436
+ return compilerOptionsArePreES2023(compilerOptions);
36437
+ };
36438
+ const detectPreES2023Target = (directory) => {
36439
+ const tsConfigPath = Path.join(directory, TSCONFIG_FILENAME);
36440
+ if (isFile(tsConfigPath)) return detectPreES2023FromConfig(tsConfigPath);
36441
+ for (const fallbackFilename of FALLBACK_TSCONFIG_FILENAMES) {
36442
+ const fallbackPath = Path.join(directory, fallbackFilename);
36443
+ if (isFile(fallbackPath)) return detectPreES2023FromConfig(fallbackPath);
36444
+ }
36445
+ return false;
36446
+ };
36447
+ const FALLBACK_TSCONFIG_FILENAMES = ["tsconfig.app.json", "tsconfig.build.json"];
36448
+ const FRAMEWORK_PACKAGES = {
36449
+ next: "nextjs",
36450
+ "@tanstack/react-start": "tanstack-start",
36451
+ vite: "vite",
36452
+ "react-scripts": "cra",
36453
+ "@remix-run/react": "remix",
36454
+ gatsby: "gatsby",
36455
+ expo: "expo",
36456
+ "react-native": "react-native"
36457
+ };
36458
+ const detectFramework = (dependencies) => {
36459
+ for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
36460
+ if (dependencies.preact && !dependencies.react) return "preact";
36461
+ return "unknown";
36462
+ };
36463
+ const MOBILE_FRAMEWORKS = new Set(["expo", "react-native"]);
36464
+ const frameworkMergeRank = (framework) => {
36465
+ if (framework === "unknown") return 3;
36466
+ return MOBILE_FRAMEWORKS.has(framework) ? 2 : 1;
36467
+ };
36313
36468
  const REACT_COMPILER_PACKAGES = new Set([
36314
36469
  "babel-plugin-react-compiler",
36315
36470
  "react-compiler-runtime",
@@ -36346,6 +36501,7 @@ const EXPO_APP_CONFIG_FILENAMES = [
36346
36501
  ];
36347
36502
  const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
36348
36503
  const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
36504
+ const STATIC_EXPORT_OUTPUT_PATTERN = /(?:^|[^.\w])["']?output["']?\s*:\s*["']export["']/m;
36349
36505
  const hasCompilerPackage = (packageJson) => {
36350
36506
  const allDependencies = {
36351
36507
  ...packageJson.peerDependencies,
@@ -36378,34 +36534,10 @@ const detectReactCompiler = (directory, packageJson) => {
36378
36534
  }
36379
36535
  return false;
36380
36536
  };
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
- };
36537
+ const detectNextjsStaticExport = (directory) => NEXT_CONFIG_FILENAMES.some((filename) => {
36538
+ const filePath = Path.join(directory, filename);
36539
+ return isFile(filePath) && STATIC_EXPORT_OUTPUT_PATTERN.test(NFS.readFileSync(filePath, "utf-8"));
36540
+ });
36409
36541
  const UNRESOLVABLE_PROTOCOL_VERSION = /^(?:file|git|github|https?|link|patch|portal|workspace|npm):/i;
36410
36542
  const DIST_TAG_VERSION = /^[a-z][a-z0-9._-]*$/i;
36411
36543
  const WILDCARD_VERSION = /^[*xX](?:\.[*xX])*$/;
@@ -36543,10 +36675,76 @@ const isConcreteDependencyVersion = (version) => {
36543
36675
  const normalizedVersion = normalizeDependencyVersion(version);
36544
36676
  return normalizedVersion !== null && /\d/.test(normalizedVersion);
36545
36677
  };
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;
36678
+ const isMajorMinorAtLeast = (detected, required) => {
36679
+ if (detected === null) return true;
36680
+ if (detected.major !== required.major) return detected.major > required.major;
36681
+ return detected.minor >= required.minor;
36682
+ };
36683
+ const parseReactMajor = (reactVersion) => {
36684
+ if (typeof reactVersion !== "string") return null;
36685
+ return getLowestDependencyMajor(reactVersion);
36686
+ };
36687
+ const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
36688
+ const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
36689
+ const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
36690
+ const parseReactMajorMinor = (reactVersion) => {
36691
+ if (typeof reactVersion !== "string") return null;
36692
+ const trimmed = reactVersion.trim();
36693
+ if (trimmed.length === 0) return null;
36694
+ const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
36695
+ if (lowerBoundsOnly.length === 0) return null;
36696
+ const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
36697
+ if (majorMinorMatch) {
36698
+ const major = Number.parseInt(majorMinorMatch[1], 10);
36699
+ const minor = Number.parseInt(majorMinorMatch[2], 10);
36700
+ if (!Number.isFinite(major) || major <= 0) return null;
36701
+ if (!Number.isFinite(minor) || minor < 0) return null;
36702
+ return {
36703
+ major,
36704
+ minor
36705
+ };
36706
+ }
36707
+ const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
36708
+ if (!majorOnlyMatch) return null;
36709
+ const major = Number.parseInt(majorOnlyMatch[1], 10);
36710
+ if (!Number.isFinite(major) || major <= 0) return null;
36711
+ return {
36712
+ major,
36713
+ minor: 0
36714
+ };
36715
+ };
36716
+ const parseLowerBoundVersion = (versionSpec) => import_semver.validRange(versionSpec) !== null ? import_semver.minVersion(versionSpec) : import_semver.coerce(versionSpec);
36717
+ const parseTailwindMajorMinor = (tailwindVersion) => {
36718
+ if (typeof tailwindVersion !== "string") return null;
36719
+ const trimmed = tailwindVersion.trim();
36720
+ if (trimmed.length === 0) return null;
36721
+ const lowerBound = parseLowerBoundVersion(trimmed);
36722
+ if (lowerBound === null || lowerBound.major <= 0) return null;
36723
+ return {
36724
+ major: lowerBound.major,
36725
+ minor: lowerBound.minor
36726
+ };
36727
+ };
36728
+ const hasUpperBoundOnlyPeerRange = (range) => {
36729
+ if (typeof range !== "string") return false;
36730
+ const normalizedRange = normalizeDependencyVersion(range);
36731
+ if (normalizedRange === null) return false;
36732
+ return splitDependencyVersionBranches(normalizedRange).some((branch) => {
36733
+ const normalizedBranch = normalizeDependencyVersion(branch);
36734
+ return normalizedBranch !== null && getBranchLowestMajor(normalizedBranch) === null && hasUpperBoundComparator(normalizedBranch);
36735
+ });
36736
+ };
36737
+ const peerRangeMinMajor = (range) => {
36738
+ if (typeof range !== "string") return null;
36739
+ return getLowestDependencyMajor(range);
36740
+ };
36741
+ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
36742
+ const installedReactMajor = parseReactMajor(reactVersion);
36743
+ const peerReactRange = packageJson.peerDependencies?.react;
36744
+ if (typeof peerReactRange !== "string") return installedReactMajor;
36745
+ const peerFloor = peerRangeMinMajor(peerReactRange);
36746
+ if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
36747
+ return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
36550
36748
  };
36551
36749
  const isCatalogReference = (version) => version.startsWith("catalog:");
36552
36750
  const extractCatalogName = (version) => {
@@ -36688,6 +36886,16 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicit
36688
36886
  }
36689
36887
  return null;
36690
36888
  };
36889
+ const REACT_SECTIONS = [
36890
+ "dependencies",
36891
+ "peerDependencies",
36892
+ "devDependencies"
36893
+ ];
36894
+ const TAILWIND_ZOD_SECTIONS = [
36895
+ "dependencies",
36896
+ "devDependencies",
36897
+ "peerDependencies"
36898
+ ];
36691
36899
  const EMPTY_DEPENDENCY_INFO = {
36692
36900
  reactVersion: null,
36693
36901
  tailwindVersion: null,
@@ -36728,6 +36936,10 @@ const extractDependencyInfo = (packageJson) => {
36728
36936
  framework: detectFramework(allDependencies)
36729
36937
  };
36730
36938
  };
36939
+ const getDependencySpec = (packageJson, packageName) => {
36940
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
36941
+ return typeof spec === "string" ? spec : null;
36942
+ };
36731
36943
  const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
36732
36944
  for (const section of sections) {
36733
36945
  const version = packageJson[section]?.[packageName];
@@ -36744,222 +36956,91 @@ const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
36744
36956
  version: null
36745
36957
  };
36746
36958
  };
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);
36959
+ const REACT_DEPENDENCY_NAMES = new Set([
36960
+ "react",
36961
+ "react-native",
36962
+ "next",
36963
+ "preact"
36964
+ ]);
36965
+ const hasReactDependency = (packageJson) => {
36966
+ const allDependencies = {
36967
+ ...packageJson.peerDependencies,
36968
+ ...packageJson.dependencies,
36969
+ ...packageJson.devDependencies
36970
+ };
36971
+ return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
36802
36972
  };
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;
36973
+ const getPreactVersion = (packageJson) => {
36974
+ return {
36975
+ ...packageJson.peerDependencies,
36976
+ ...packageJson.dependencies,
36977
+ ...packageJson.devDependencies
36978
+ }.preact ?? null;
36820
36979
  };
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);
36980
+ const TANSTACK_QUERY_PACKAGES = new Set([
36981
+ "@tanstack/react-query",
36982
+ "@tanstack/query-core",
36983
+ "react-query"
36984
+ ]);
36985
+ const hasTanStackQuery = (packageJson) => {
36986
+ const allDependencies = {
36987
+ ...packageJson.peerDependencies,
36988
+ ...packageJson.dependencies,
36989
+ ...packageJson.devDependencies
36990
+ };
36991
+ return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
36829
36992
  };
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;
36993
+ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
36994
+ if (version === null || !isCatalogReference(version)) return version;
36995
+ const catalogName = extractCatalogName(version);
36996
+ const resolvedLocalVersion = resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, catalogName);
36997
+ if (resolvedLocalVersion) return resolvedLocalVersion;
36998
+ const monorepoRoot = findMonorepoRoot(rootDirectory);
36999
+ if (!monorepoRoot) return version;
37000
+ const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
37001
+ if (!isFile(monorepoPackageJsonPath)) return version;
37002
+ return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
36837
37003
  };
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
- }
37004
+ /**
37005
+ * The chain of ancestor directories of `startDirectory`, nearest first, up to
37006
+ * and including the filesystem root. With `includeStart`, the chain begins at
37007
+ * `startDirectory` itself; otherwise at its parent. The one place the "walk up
37008
+ * the tree" traversal is expressed, so each caller adds only its own match /
37009
+ * stop policy instead of re-deriving the `path.dirname` loop.
37010
+ */
37011
+ const ancestorDirectories = (startDirectory, { includeStart }) => {
37012
+ const directories = [];
37013
+ let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
37014
+ while (true) {
37015
+ directories.push(currentDirectory);
37016
+ const parentDirectory = Path.dirname(currentDirectory);
37017
+ if (parentDirectory === currentDirectory) return directories;
37018
+ currentDirectory = parentDirectory;
36892
37019
  }
36893
- return result;
36894
37020
  };
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
- }
37021
+ /**
37022
+ * Walk up from `startDirectory` to the nearest ancestor that owns a
37023
+ * `package.json`, stopping at (and including) the enclosing project boundary
37024
+ * — the working tree's git root or a monorepo root. Lets a scan of a package
37025
+ * subfolder that has no `package.json` of its own adopt the nearest enclosing
37026
+ * package as its project root, so it inherits that package's dependency +
37027
+ * framework detection instead of synthesizing an empty (React-blind) project.
37028
+ *
37029
+ * Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
37030
+ * most specific owning package (a leaf workspace, not just the repo root), so
37031
+ * a subfolder of one workspace no longer borrows a sibling's capabilities.
37032
+ *
37033
+ * Returns `null` when no ancestor `package.json` exists inside the project
37034
+ * boundary (a loose tree of files, or a subfolder of a package-less repo).
37035
+ */
37036
+ const findNearestAncestorPackageJson = (startDirectory) => {
37037
+ if (isProjectBoundary(startDirectory)) return null;
37038
+ for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
37039
+ if (isFile(Path.join(directory, "package.json"))) return directory;
37040
+ if (isProjectBoundary(directory)) return null;
36959
37041
  }
36960
37042
  return null;
36961
37043
  };
36962
- const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => predicate(packageJson) ? true : null) !== null;
36963
37044
  const NAMES = new Set([
36964
37045
  "react-native",
36965
37046
  "react-native-tvos",
@@ -36992,47 +37073,7 @@ const isPackageJsonReactNativeAware = (packageJson) => {
36992
37073
  if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
36993
37074
  return false;
36994
37075
  };
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";
37076
+ const REANIMATED_DEPENDENCY_NAME$1 = "react-native-reanimated";
37036
37077
  const isPackageJsonReanimatedAware = (packageJson) => {
37037
37078
  const allDependencies = {
37038
37079
  ...packageJson.peerDependencies,
@@ -37040,144 +37081,70 @@ const isPackageJsonReanimatedAware = (packageJson) => {
37040
37081
  ...packageJson.devDependencies,
37041
37082
  ...packageJson.optionalDependencies
37042
37083
  };
37043
- return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
37084
+ return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME$1);
37044
37085
  };
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;
37086
+ const getWorkspacePatterns = (rootDirectory, packageJson) => {
37087
+ const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
37088
+ if (pnpmPatterns.length > 0) return pnpmPatterns;
37089
+ if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
37090
+ if (packageJson.workspaces?.packages) return packageJson.workspaces.packages;
37091
+ const nxPatterns = getNxWorkspaceDirectories(rootDirectory);
37092
+ if (nxPatterns.length > 0) return nxPatterns;
37093
+ return [];
37069
37094
  };
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;
37095
+ const parsePnpmWorkspacePatterns = (rootDirectory) => {
37096
+ const workspacePath = Path.join(rootDirectory, "pnpm-workspace.yaml");
37097
+ if (!isFile(workspacePath)) return [];
37098
+ const content = NFS.readFileSync(workspacePath, "utf-8");
37099
+ const patterns = [];
37100
+ let isInsidePackagesBlock = false;
37101
+ for (const line of content.split("\n")) {
37102
+ const trimmed = line.trim();
37103
+ if (trimmed === "packages:") {
37104
+ isInsidePackagesBlock = true;
37105
+ continue;
37106
+ }
37107
+ if (isInsidePackagesBlock && trimmed.startsWith("-")) patterns.push(trimmed.replace(/^-\s*/, "").replace(/["']/g, ""));
37108
+ else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith("#")) isInsidePackagesBlock = false;
37084
37109
  }
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
- };
37110
+ return patterns;
37100
37111
  };
37101
- const readTsConfig = (filePath) => {
37102
- let content;
37103
- try {
37104
- content = NFS.readFileSync(filePath, "utf-8");
37105
- } catch {
37106
- return null;
37112
+ const NX_PROJECT_DISCOVERY_DIRS = [
37113
+ "apps",
37114
+ "libs",
37115
+ "packages"
37116
+ ];
37117
+ const getNxWorkspaceDirectories = (rootDirectory) => {
37118
+ if (!isFile(Path.join(rootDirectory, "nx.json"))) return [];
37119
+ const collected = [];
37120
+ for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
37121
+ const candidatePath = Path.join(rootDirectory, candidate);
37122
+ if (!isDirectory(candidatePath)) continue;
37123
+ for (const entry of readDirectoryEntries(candidatePath)) {
37124
+ if (!entry.isDirectory()) continue;
37125
+ const projectDirectory = Path.join(candidatePath, entry.name);
37126
+ if (isFile(Path.join(projectDirectory, "project.json")) || isFile(Path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
37127
+ }
37107
37128
  }
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
- };
37129
+ return collected;
37123
37130
  };
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);
37131
+ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
37132
+ const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
37133
+ if (!cleanPattern.includes("*")) {
37134
+ const directoryPath = Path.join(rootDirectory, cleanPattern);
37135
+ if (isDirectory(directoryPath) && isFile(Path.join(directoryPath, "package.json"))) return [directoryPath];
37136
+ return [];
37134
37137
  }
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));
37138
+ const wildcardIndex = cleanPattern.indexOf("*");
37139
+ const baseDirectory = Path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
37140
+ const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
37141
+ if (!isDirectory(baseDirectory)) return [];
37142
+ const resolved = [];
37143
+ for (const entry of readDirectoryEntries(baseDirectory)) {
37144
+ const entryPath = Path.join(baseDirectory, entry.name, suffixAfterWildcard);
37145
+ if (isDirectory(entryPath) && isFile(Path.join(entryPath, "package.json"))) resolved.push(entryPath);
37146
+ }
37147
+ return resolved;
37181
37148
  };
37182
37149
  const listWorkspacePackages = (rootDirectory) => {
37183
37150
  const packageJsonPath = Path.join(rootDirectory, "package.json");
@@ -37209,6 +37176,208 @@ const listWorkspacePackages = (rootDirectory) => {
37209
37176
  }
37210
37177
  return packages;
37211
37178
  };
37179
+ const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
37180
+ const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
37181
+ const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
37182
+ const dependencyDeclaration = getDependencyDeclaration({
37183
+ packageJson: workspacePackageJson,
37184
+ packageName,
37185
+ sections
37186
+ });
37187
+ if (!dependencyDeclaration.hasDeclaration) return null;
37188
+ return concreteVersion ?? resolveCatalogVersion(workspacePackageJson, packageName, workspaceDirectory, dependencyDeclaration.catalogReference) ?? resolveCatalogVersion(rootPackageJson, packageName, rootDirectory, dependencyDeclaration.catalogReference);
37189
+ };
37190
+ const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
37191
+ if (!currentVersion) return true;
37192
+ const currentMajor = parseReactMajor(currentVersion);
37193
+ const nextMajor = parseReactMajor(nextVersion);
37194
+ if (currentMajor === null) return nextMajor !== null;
37195
+ if (nextMajor === null) return false;
37196
+ return nextMajor < currentMajor;
37197
+ };
37198
+ const evaluateManifestFacts = (facts, packageJson, directory) => {
37199
+ if (facts.expo.version === null) {
37200
+ const spec = getDependencySpec(packageJson, "expo");
37201
+ if (spec !== null) facts.expo = {
37202
+ version: spec,
37203
+ sourceDirectory: directory
37204
+ };
37205
+ }
37206
+ if (facts.next.version === null) {
37207
+ const spec = getDependencySpec(packageJson, "next");
37208
+ if (spec !== null) facts.next = {
37209
+ version: spec,
37210
+ sourceDirectory: directory
37211
+ };
37212
+ }
37213
+ if (facts.shopifyFlashList.version === null) {
37214
+ const spec = getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME);
37215
+ if (spec !== null) facts.shopifyFlashList = {
37216
+ version: spec,
37217
+ sourceDirectory: directory
37218
+ };
37219
+ }
37220
+ if (facts.reanimatedVersion === null) {
37221
+ const spec = getDependencySpec(packageJson, REANIMATED_DEPENDENCY_NAME);
37222
+ if (spec !== null) facts.reanimatedVersion = spec;
37223
+ }
37224
+ facts.hasReactNativeAwarePackage = facts.hasReactNativeAwarePackage || isPackageJsonReactNativeAware(packageJson);
37225
+ facts.hasReanimatedAwarePackage = facts.hasReanimatedAwarePackage || isPackageJsonReanimatedAware(packageJson);
37226
+ };
37227
+ const collectWorkspaceFacts = (rootDirectory, rootPackageJson, { collectReactGroup }) => {
37228
+ const facts = {
37229
+ reactVersion: null,
37230
+ tailwindVersion: null,
37231
+ zodVersion: null,
37232
+ framework: "unknown",
37233
+ expo: {
37234
+ version: null,
37235
+ sourceDirectory: null
37236
+ },
37237
+ next: {
37238
+ version: null,
37239
+ sourceDirectory: null
37240
+ },
37241
+ shopifyFlashList: {
37242
+ version: null,
37243
+ sourceDirectory: null
37244
+ },
37245
+ hasReactNativeAwarePackage: false,
37246
+ hasReanimatedAwarePackage: false,
37247
+ reanimatedVersion: null
37248
+ };
37249
+ evaluateManifestFacts(facts, rootPackageJson, rootDirectory);
37250
+ let isReactGroupSettled = !collectReactGroup;
37251
+ const visitedDirectories = /* @__PURE__ */ new Set();
37252
+ for (const pattern of getWorkspacePatterns(rootDirectory, rootPackageJson)) {
37253
+ const directories = [...resolveWorkspaceDirectories(rootDirectory, pattern)].sort();
37254
+ for (const workspaceDirectory of directories) {
37255
+ if (visitedDirectories.has(workspaceDirectory)) continue;
37256
+ visitedDirectories.add(workspaceDirectory);
37257
+ const workspacePackageJson = readPackageJson$1(Path.join(workspaceDirectory, "package.json"));
37258
+ evaluateManifestFacts(facts, workspacePackageJson, workspaceDirectory);
37259
+ const info = extractDependencyInfo(workspacePackageJson);
37260
+ if (info.framework !== "unknown" && frameworkMergeRank(info.framework) < frameworkMergeRank(facts.framework)) facts.framework = info.framework;
37261
+ if (isReactGroupSettled) continue;
37262
+ const reactVersion = resolveWorkspaceDependencyVersion({
37263
+ concreteVersion: info.reactVersion,
37264
+ packageName: "react",
37265
+ rootDirectory,
37266
+ rootPackageJson,
37267
+ sections: REACT_SECTIONS,
37268
+ workspaceDirectory,
37269
+ workspacePackageJson
37270
+ });
37271
+ const tailwindVersion = resolveWorkspaceDependencyVersion({
37272
+ concreteVersion: info.tailwindVersion,
37273
+ packageName: "tailwindcss",
37274
+ rootDirectory,
37275
+ rootPackageJson,
37276
+ sections: TAILWIND_ZOD_SECTIONS,
37277
+ workspaceDirectory,
37278
+ workspacePackageJson
37279
+ });
37280
+ const zodVersion = resolveWorkspaceDependencyVersion({
37281
+ concreteVersion: info.zodVersion,
37282
+ packageName: "zod",
37283
+ rootDirectory,
37284
+ rootPackageJson,
37285
+ sections: TAILWIND_ZOD_SECTIONS,
37286
+ workspaceDirectory,
37287
+ workspacePackageJson
37288
+ });
37289
+ if (reactVersion && shouldReplaceReactVersion(facts.reactVersion, reactVersion)) facts.reactVersion = reactVersion;
37290
+ if (tailwindVersion && !facts.tailwindVersion) facts.tailwindVersion = tailwindVersion;
37291
+ if (zodVersion && !facts.zodVersion) facts.zodVersion = zodVersion;
37292
+ const settledReactMajor = parseReactMajor(facts.reactVersion);
37293
+ isReactGroupSettled = Boolean(facts.reactVersion && facts.tailwindVersion && facts.framework !== "unknown" && settledReactMajor !== null && settledReactMajor <= 17);
37294
+ }
37295
+ }
37296
+ return facts;
37297
+ };
37298
+ const findDependencyInfoFromMonorepoRoot = (directory) => {
37299
+ const monorepoRoot = findMonorepoRoot(directory);
37300
+ if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
37301
+ const monorepoPackageJsonPath = Path.join(monorepoRoot, "package.json");
37302
+ if (!isFile(monorepoPackageJsonPath)) return EMPTY_DEPENDENCY_INFO;
37303
+ const rootPackageJson = readPackageJson$1(monorepoPackageJsonPath);
37304
+ const rootInfo = extractDependencyInfo(rootPackageJson);
37305
+ const leafPackageJsonPath = Path.join(directory, "package.json");
37306
+ const leafPackageJson = isFile(leafPackageJsonPath) ? readPackageJson$1(leafPackageJsonPath) : null;
37307
+ const leafReactDeclaration = leafPackageJson ? getDependencyDeclaration({
37308
+ packageJson: leafPackageJson,
37309
+ packageName: "react",
37310
+ sections: REACT_SECTIONS
37311
+ }) : null;
37312
+ const leafTailwindDeclaration = leafPackageJson ? getDependencyDeclaration({
37313
+ packageJson: leafPackageJson,
37314
+ packageName: "tailwindcss",
37315
+ sections: TAILWIND_ZOD_SECTIONS
37316
+ }) : null;
37317
+ const leafZodDeclaration = leafPackageJson ? getDependencyDeclaration({
37318
+ packageJson: leafPackageJson,
37319
+ packageName: "zod",
37320
+ sections: TAILWIND_ZOD_SECTIONS
37321
+ }) : null;
37322
+ const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
37323
+ const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
37324
+ const shouldUseZodFallback = leafZodDeclaration?.hasDeclaration ?? true;
37325
+ const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
37326
+ const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
37327
+ const zodCatalogVersion = shouldUseZodFallback ? resolveCatalogVersion(rootPackageJson, "zod", monorepoRoot, leafZodDeclaration?.catalogReference) : null;
37328
+ const workspaceFacts = collectWorkspaceFacts(monorepoRoot, rootPackageJson, { collectReactGroup: true });
37329
+ return {
37330
+ reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceFacts.reactVersion : rootInfo.reactVersion ?? workspaceFacts.reactVersion,
37331
+ tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceFacts.tailwindVersion : null,
37332
+ zodVersion: shouldUseZodFallback ? zodCatalogVersion ?? rootInfo.zodVersion ?? workspaceFacts.zodVersion : null,
37333
+ framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceFacts.framework
37334
+ };
37335
+ };
37336
+ /**
37337
+ * Last-resort React detection: locate React the way Node's `node_modules`
37338
+ * resolution would from `directory` — check each `node_modules/react` up the
37339
+ * tree — but bounded to the enclosing repo so a globally installed React can't
37340
+ * masquerade as the project's version. Stops at (and including) the nearest
37341
+ * project boundary — the git root or a monorepo root — and returns the first
37342
+ * installed `version`.
37343
+ *
37344
+ * Makes "React is installed and importable" ⇒ "React is detected" an invariant
37345
+ * for packages whose only React declaration is a version-less spec
37346
+ * (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
37347
+ * `node_modules` the declaration walks never reach — the profile of a component
37348
+ * package inside a monorepo. Matching the `node_modules/react` entry rather than
37349
+ * its realpath means a project whose `node_modules` is symlinked elsewhere (a
37350
+ * Docker volume, a shared store) still counts, while a global install outside
37351
+ * the repo does not.
37352
+ *
37353
+ * Returns `null` when React isn't installed within the repo or its package.json
37354
+ * carries no version string. A tree with no boundary marker at all bounds the
37355
+ * search to `directory`, so a dependency hoisted above the scanned package there
37356
+ * is conservatively not adopted.
37357
+ */
37358
+ /**
37359
+ * The nearest enclosing project boundary (git root or monorepo root), or
37360
+ * `directory` itself when the scan target sits outside any repo. Bounds the
37361
+ * node_modules walk so a React hoisted anywhere inside the repo counts, but a
37362
+ * React in an ancestor above the repo — a home-directory or global
37363
+ * `node_modules` — can't leak in.
37364
+ */
37365
+ const findContainmentRoot = (directory) => {
37366
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
37367
+ return directory;
37368
+ };
37369
+ const resolveInstalledReactVersion = (directory) => {
37370
+ const containmentRoot = findContainmentRoot(directory);
37371
+ for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
37372
+ const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
37373
+ if (isFile(reactPackageJsonPath)) {
37374
+ const installedVersion = readPackageJson$1(reactPackageJsonPath).version;
37375
+ return typeof installedVersion === "string" ? installedVersion : null;
37376
+ }
37377
+ if (ancestorDirectory === containmentRoot) return null;
37378
+ }
37379
+ return null;
37380
+ };
37212
37381
  const toReactWorkspacePackages = (directories) => {
37213
37382
  const packages = [];
37214
37383
  for (const directory of directories) {
@@ -37289,8 +37458,8 @@ const cachedProjectInfos = /* @__PURE__ */ new Map();
37289
37458
  const discoverProjectWithoutPackageJson = (directory) => {
37290
37459
  const sourceFileCount = countSourceFiles(directory);
37291
37460
  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;
37461
+ const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
37462
+ const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
37294
37463
  if (enclosingProject !== null) return {
37295
37464
  ...enclosingProject,
37296
37465
  rootDirectory: directory,
@@ -37320,7 +37489,9 @@ const discoverProjectWithoutPackageJson = (directory) => {
37320
37489
  shopifyFlashListVersion: null,
37321
37490
  shopifyFlashListMajorVersion: null,
37322
37491
  hasReanimated: false,
37492
+ reanimatedVersion: null,
37323
37493
  isPreES2023Target: hasOwnTsConfig && detectPreES2023Target(directory),
37494
+ isStaticExport: false,
37324
37495
  sourceFileCount
37325
37496
  };
37326
37497
  };
@@ -37334,88 +37505,85 @@ const discoverProject = (directory) => {
37334
37505
  return synthesized;
37335
37506
  }
37336
37507
  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({
37508
+ const rootInfo = extractDependencyInfo(packageJson);
37509
+ let framework = rootInfo.framework;
37510
+ const tracked = {
37511
+ react: {
37512
+ version: rootInfo.reactVersion,
37513
+ sections: REACT_SECTIONS
37514
+ },
37515
+ tailwindcss: {
37516
+ version: rootInfo.tailwindVersion,
37517
+ sections: TAILWIND_ZOD_SECTIONS
37518
+ },
37519
+ zod: {
37520
+ version: rootInfo.zodVersion,
37521
+ sections: TAILWIND_ZOD_SECTIONS
37522
+ }
37523
+ };
37524
+ const declarations = Object.fromEntries(Object.entries(tracked).map(([packageName, entry]) => [packageName, getDependencyDeclaration({
37357
37525
  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) {
37526
+ packageName,
37527
+ sections: entry.sections
37528
+ })]));
37529
+ const fillFromCatalogs = (source, sourceDirectory) => {
37530
+ for (const [packageName, entry] of Object.entries(tracked)) if (!entry.version && declarations[packageName].hasDeclaration) entry.version = resolveCatalogVersion(source, packageName, sourceDirectory, declarations[packageName].catalogReference);
37531
+ };
37532
+ fillFromCatalogs(packageJson, directory);
37533
+ if (!tracked.react.version || !tracked.tailwindcss.version || !tracked.zod.version) {
37369
37534
  const monorepoRoot = findMonorepoRoot(directory);
37370
37535
  if (monorepoRoot) {
37371
37536
  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
- }
37537
+ if (isFile(monorepoPackageJsonPath)) fillFromCatalogs(readPackageJson$1(monorepoPackageJsonPath), monorepoRoot);
37378
37538
  }
37379
37539
  }
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;
37540
+ const shouldCollectReactGroup = !tracked.react.version || framework === "unknown";
37541
+ const workspaceFacts = collectWorkspaceFacts(directory, packageJson, { collectReactGroup: shouldCollectReactGroup });
37542
+ if (shouldCollectReactGroup) {
37543
+ tracked.react.version ||= workspaceFacts.reactVersion;
37544
+ tracked.tailwindcss.version ||= workspaceFacts.tailwindVersion;
37545
+ tracked.zod.version ||= workspaceFacts.zodVersion;
37546
+ if (framework === "unknown" && workspaceFacts.framework !== "unknown") framework = workspaceFacts.framework;
37386
37547
  }
37387
- if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
37548
+ if ((!tracked.react.version || framework === "unknown") && !isMonorepoRoot(directory)) {
37388
37549
  const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
37389
- if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
37390
- if (!tailwindVersion) tailwindVersion = monorepoInfo.tailwindVersion;
37391
- if (!zodVersion) zodVersion = monorepoInfo.zodVersion;
37550
+ tracked.react.version ||= monorepoInfo.reactVersion;
37551
+ tracked.tailwindcss.version ||= monorepoInfo.tailwindVersion;
37552
+ tracked.zod.version ||= monorepoInfo.zodVersion;
37392
37553
  if (framework === "unknown") framework = monorepoInfo.framework;
37393
37554
  }
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;
37555
+ for (const [packageName, entry] of Object.entries(tracked)) {
37556
+ const declaredVersion = declarations[packageName].version;
37557
+ if (!entry.version && declaredVersion && !isCatalogReference(declaredVersion)) entry.version = declaredVersion;
37558
+ }
37559
+ const { react, tailwindcss, zod } = tracked;
37560
+ let reactVersion = react.version;
37561
+ if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
37562
+ const tailwindVersion = tailwindcss.version;
37563
+ const zodVersion = zod.version;
37397
37564
  const projectName = packageJson.name ?? Path.basename(directory);
37398
37565
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
37399
37566
  const sourceFileCount = countSourceFiles(directory);
37400
- const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
37567
+ const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
37401
37568
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
37402
37569
  rootDirectory: directory,
37403
37570
  rootPackageJson: packageJson,
37404
37571
  packageName: "expo",
37405
- version: findExpoVersion(directory, packageJson)
37572
+ version: workspaceFacts.expo.version
37406
37573
  }) : null;
37407
37574
  const shopifyFlashListVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
37408
37575
  rootDirectory: directory,
37409
37576
  rootPackageJson: packageJson,
37410
37577
  packageName: SHOPIFY_FLASH_LIST_PACKAGE_NAME,
37411
- version: findShopifyFlashListVersion(directory, packageJson)
37578
+ version: workspaceFacts.shopifyFlashList.version
37412
37579
  }) : null;
37413
- const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
37580
+ const hasReanimated = hasReactNativeWorkspace && workspaceFacts.hasReanimatedAwarePackage;
37581
+ const reanimatedVersion = hasReanimated ? workspaceFacts.reanimatedVersion : null;
37414
37582
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
37415
37583
  rootDirectory: directory,
37416
37584
  rootPackageJson: packageJson,
37417
37585
  packageName: "next",
37418
- version: findNextjsVersion(directory, packageJson)
37586
+ version: workspaceFacts.next.version
37419
37587
  }) : null;
37420
37588
  const preactVersion = getPreactVersion(packageJson);
37421
37589
  const isPreES2023Target = hasTypeScript && detectPreES2023Target(directory);
@@ -37426,7 +37594,7 @@ const discoverProject = (directory) => {
37426
37594
  reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageJson),
37427
37595
  tailwindVersion,
37428
37596
  zodVersion,
37429
- zodMajorVersion: parseZodMajor(zodVersion),
37597
+ zodMajorVersion: zodVersion === null ? null : getLowestDependencyMajor(zodVersion),
37430
37598
  framework,
37431
37599
  hasTypeScript,
37432
37600
  hasReactCompiler: detectReactCompiler(directory, packageJson),
@@ -37440,7 +37608,9 @@ const discoverProject = (directory) => {
37440
37608
  shopifyFlashListVersion,
37441
37609
  shopifyFlashListMajorVersion: shopifyFlashListVersion === null ? null : getLowestDependencyMajor(shopifyFlashListVersion),
37442
37610
  hasReanimated,
37611
+ reanimatedVersion,
37443
37612
  isPreES2023Target,
37613
+ isStaticExport: framework === "nextjs" && detectNextjsStaticExport(workspaceFacts.next.sourceDirectory ?? directory),
37444
37614
  sourceFileCount
37445
37615
  };
37446
37616
  cachedProjectInfos.set(directory, projectInfo);
@@ -37456,57 +37626,6 @@ const discoverProject = (directory) => {
37456
37626
  * single source file has nothing to analyze.
37457
37627
  */
37458
37628
  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
37629
  const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
37511
37630
  var InvalidGlobPatternError = class extends Error {
37512
37631
  pattern;
@@ -37632,6 +37751,9 @@ const buildRuleSeverityControls = (config) => {
37632
37751
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
37633
37752
  };
37634
37753
  };
37754
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
37755
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
37756
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
37635
37757
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
37636
37758
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
37637
37759
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -37709,6 +37831,7 @@ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
37709
37831
  "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
37710
37832
  "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
37711
37833
  "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
37834
+ "react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
37712
37835
  "react/jsx-no-undef": "react-doctor/jsx-no-undef",
37713
37836
  "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
37714
37837
  "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
@@ -37793,9 +37916,6 @@ const tokenizeRuleList = (ruleList) => {
37793
37916
  if (!ruleSection) return [];
37794
37917
  return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
37795
37918
  };
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
37919
  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
37920
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
37801
37921
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
@@ -37849,6 +37969,53 @@ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
37849
37969
  if (!ruleId.startsWith("react-doctor/")) return null;
37850
37970
  return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
37851
37971
  };
37972
+ const ESLINT_REACT_HOOKS_PLUGIN_PREFIX = "react-hooks/";
37973
+ const buildAcceptedTokens = (ruleId) => {
37974
+ const shortRuleId = ruleId.slice(15);
37975
+ return new Set([ruleId, `${ESLINT_REACT_HOOKS_PLUGIN_PREFIX}${shortRuleId}`]);
37976
+ };
37977
+ const listNamesRule = (ruleList, acceptedTokens) => tokenizeRuleList(ruleList).some((token) => acceptedTokens.has(token));
37978
+ const isInlineDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
37979
+ const candidates = [{
37980
+ line: lines[diagnosticLineIndex],
37981
+ requiredScope: "line"
37982
+ }, {
37983
+ line: lines[diagnosticLineIndex - 1],
37984
+ requiredScope: "next-line"
37985
+ }];
37986
+ for (const { line, requiredScope } of candidates) {
37987
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
37988
+ if (!match) continue;
37989
+ const [, , scope, ruleList] = match;
37990
+ if (scope !== requiredScope) continue;
37991
+ if (listNamesRule(ruleList, acceptedTokens)) return true;
37992
+ }
37993
+ return false;
37994
+ };
37995
+ const isBlockDisabled = (lines, diagnosticLineIndex, acceptedTokens) => {
37996
+ let isRangeOpen = false;
37997
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
37998
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
37999
+ const line = lines[lineIndex];
38000
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
38001
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
38002
+ if (disableMatch && listNamesRule(disableMatch[2], acceptedTokens)) {
38003
+ isRangeOpen = true;
38004
+ continue;
38005
+ }
38006
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
38007
+ if (enableMatch) {
38008
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
38009
+ if (enabledRules.length === 0 || enabledRules.some((rule) => acceptedTokens.has(rule))) isRangeOpen = false;
38010
+ }
38011
+ }
38012
+ return isRangeOpen;
38013
+ };
38014
+ const isCompilerRuleForeignDisabled = (lines, diagnosticLineIndex, ruleId) => {
38015
+ if (!ruleId.startsWith("react-hooks-js/")) return false;
38016
+ const acceptedTokens = buildAcceptedTokens(ruleId);
38017
+ return isInlineDisabled(lines, diagnosticLineIndex, acceptedTokens) || isBlockDisabled(lines, diagnosticLineIndex, acceptedTokens);
38018
+ };
37852
38019
  const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
37853
38020
  const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
37854
38021
  const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
@@ -37992,6 +38159,11 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
37992
38159
  isSuppressed: true,
37993
38160
  nearMissHint: null
37994
38161
  };
38162
+ if (isCompilerRuleForeignDisabled(lines, diagnosticLineIndex, ruleId)) return {
38163
+ isSuppressed: true,
38164
+ nearMissHint: null,
38165
+ isForeignDirective: true
38166
+ };
37995
38167
  const directComments = findStackedDisableCommentsAbove(lines, diagnosticLineIndex);
37996
38168
  if (hasChainSuppressor(directComments, ruleId)) return {
37997
38169
  isSuppressed: true,
@@ -38273,12 +38445,18 @@ const collectStringSet = (values) => {
38273
38445
  return new Set(values.filter((value) => typeof value === "string"));
38274
38446
  };
38275
38447
  const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
38448
+ const NODE_CLI_PATH_SEGMENTS = new Set([
38449
+ "scripts",
38450
+ "bin",
38451
+ "tools"
38452
+ ]);
38453
+ const hasNodeCliPathSegment = (filePath) => filePath.split(/[\\/]/).slice(0, -1).some((segment) => NODE_CLI_PATH_SEGMENTS.has(segment));
38276
38454
  /**
38277
38455
  * Pre-compiles every stateful filter and returns a single
38278
38456
  * `apply(diagnostic)` closure that runs (in order):
38279
38457
  *
38280
38458
  * 1. auto-suppress (test-noise rules in test files; `migration-hint`
38281
- * wins over `test-noise`)
38459
+ * wins over `test-noise`; `no-console` on Node CLI scripts)
38282
38460
  * 2. severity overrides (top-level `rules` / `categories`, with
38283
38461
  * `"off"` dropping)
38284
38462
  * 3. warning suppression (only when `showWarnings` is false: drops every
@@ -38376,6 +38554,12 @@ const buildDiagnosticPipeline = (input) => {
38376
38554
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
38377
38555
  return false;
38378
38556
  };
38557
+ const isNoConsoleOnNodeCliScript = (diagnostic) => {
38558
+ if (diagnostic.rule !== "no-console") return false;
38559
+ if (hasNodeCliPathSegment(diagnostic.filePath)) return true;
38560
+ const lines = getFileLines(diagnostic.filePath);
38561
+ return lines !== null && Boolean(lines[0]?.startsWith("#!"));
38562
+ };
38379
38563
  const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
38380
38564
  if (!hasRuntimeGlobals) return false;
38381
38565
  if (diagnostic.rule !== "jsx-no-undef") return false;
@@ -38385,6 +38569,7 @@ const buildDiagnosticPipeline = (input) => {
38385
38569
  return {
38386
38570
  apply: (diagnostic) => {
38387
38571
  if (shouldAutoSuppress(diagnostic)) return null;
38572
+ if (isNoConsoleOnNodeCliScript(diagnostic)) return null;
38388
38573
  let current = diagnostic;
38389
38574
  let explicitSeverityOverride;
38390
38575
  let explicitRuleOverride;
@@ -38414,7 +38599,7 @@ const buildDiagnosticPipeline = (input) => {
38414
38599
  if (lines) {
38415
38600
  const ruleIdentifier = `${current.plugin}/${current.rule}`;
38416
38601
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
38417
- if (evaluation.isSuppressed) return suppress(current, "inline");
38602
+ if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
38418
38603
  if (evaluation.nearMissHint) current = {
38419
38604
  ...current,
38420
38605
  suppressionHint: evaluation.nearMissHint
@@ -40251,64 +40436,64 @@ function* collectSecurityScanFiles(rootDirectory) {
40251
40436
  }
40252
40437
  }
40253
40438
  }
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;
40439
+ const CLIENT_ONLY_FRAMEWORKS = new Set([
40440
+ "vite",
40441
+ "cra",
40442
+ "gatsby",
40443
+ "react-native",
40444
+ "expo"
40445
+ ]);
40446
+ const addMajorLadder = (capabilities, name, major, earliest, latest) => {
40447
+ if (major === null) return;
40448
+ const cappedMajor = Math.min(major, latest);
40449
+ for (let candidate = earliest; candidate <= cappedMajor; candidate += 1) capabilities.add(`${name}:${candidate}`);
40450
+ };
40261
40451
  const buildCapabilities = (project) => {
40262
40452
  const capabilities = /* @__PURE__ */ new Set();
40263
40453
  capabilities.add(project.framework);
40264
- if (hasReactRuntime(project)) capabilities.add("react");
40454
+ if (project.reactVersion !== null || project.preactVersion !== null) capabilities.add("react");
40265
40455
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
40266
40456
  if (project.expoVersion !== null) capabilities.add("expo");
40457
+ if ((project.framework === "nextjs" || project.framework === "tanstack-start" || project.framework === "remix") && !project.isStaticExport) capabilities.add("server-actions");
40458
+ if (CLIENT_ONLY_FRAMEWORKS.has(project.framework)) capabilities.add("client-only");
40459
+ if (project.isStaticExport) capabilities.add("nextjs:static-export");
40267
40460
  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
- }
40461
+ addMajorLadder(capabilities, "react", project.reactMajorVersion, 17, 30);
40462
+ if (project.reactMajorVersion !== null && project.reactMajorVersion >= 19 && isMajorMinorAtLeast(parseReactMajorMinor(project.reactVersion), {
40463
+ major: 19,
40464
+ minor: 2
40465
+ })) capabilities.add("react:19.2");
40466
+ if (project.tailwindVersion !== null) capabilities.add("tailwind");
40467
+ if (project.tailwindVersion !== null && isMajorMinorAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
40468
+ major: 3,
40469
+ minor: 4
40470
+ })) capabilities.add("tailwind:3.4");
40471
+ if (project.zodVersion !== null) capabilities.add("zod");
40472
+ if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
40290
40473
  if (project.isPreES2023Target) capabilities.add("pre-es2023");
40291
40474
  if (project.hasReactCompiler) capabilities.add("react-compiler");
40292
40475
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
40293
40476
  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
- }
40477
+ if (project.preactVersion !== null) capabilities.add("preact");
40478
+ addMajorLadder(capabilities, "preact", project.preactMajorVersion, 10, 20);
40479
+ if (project.preactVersion !== null && project.reactVersion === null) capabilities.add("pure-preact");
40303
40480
  return capabilities;
40304
40481
  };
40305
- const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
40482
+ const capabilitiesByProject = /* @__PURE__ */ new WeakMap();
40483
+ const getCapabilities = (project) => {
40484
+ const cached = capabilitiesByProject.get(project);
40485
+ if (cached !== void 0) return cached;
40486
+ const capabilities = buildCapabilities(project);
40487
+ capabilitiesByProject.set(project, capabilities);
40488
+ return capabilities;
40489
+ };
40490
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhen) => {
40306
40491
  if (requires) {
40307
40492
  for (const capability of requires) if (!capabilities.has(capability)) return false;
40308
40493
  }
40309
40494
  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;
40495
+ if (disabledWhen) {
40496
+ for (const capability of disabledWhen) if (capabilities.has(capability)) return false;
40312
40497
  }
40313
40498
  if (tags) {
40314
40499
  for (const tag of tags) if (ignoredTags.has(tag)) return false;
@@ -40319,14 +40504,14 @@ const yieldToEventLoop = () => new Promise((resolve) => {
40319
40504
  setImmediate(resolve);
40320
40505
  });
40321
40506
  const createSecurityScanSession = (rootDirectory, options) => {
40322
- const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
40507
+ const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
40323
40508
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
40324
40509
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
40325
40510
  const rule = entry.rule;
40326
40511
  const scan = rule.scan;
40327
40512
  if (typeof scan !== "function") return [];
40328
40513
  if (rule.defaultEnabled === false) return [];
40329
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) return [];
40514
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) return [];
40330
40515
  return [{
40331
40516
  entry,
40332
40517
  scan,
@@ -40346,6 +40531,7 @@ const createSecurityScanSession = (rootDirectory, options) => {
40346
40531
  return status === true;
40347
40532
  };
40348
40533
  const scanFileByRule = function* (file) {
40534
+ if (file === null) return;
40349
40535
  for (const { entry, scan, committedFilesOnly } of enabledScanRules) {
40350
40536
  for (const finding of scan(file)) {
40351
40537
  if (committedFilesOnly && isFileGitIgnored(file)) continue;
@@ -41807,7 +41993,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
41807
41993
  })),
41808
41994
  showStagedContent: (directory, relativePath, options) => runCommand({
41809
41995
  command: "git",
41810
- args: ["show", `:${relativePath}`],
41996
+ args: ["show", `:./${relativePath}`],
41811
41997
  directory,
41812
41998
  maxStdoutBytes: options?.maxBufferBytes
41813
41999
  }).pipe(map$3((result) => result.status === 0 ? result.stdout : null)),
@@ -42242,7 +42428,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42242
42428
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
42243
42429
  const jsPlugins = [];
42244
42430
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
42245
- const capabilities = buildCapabilities(project);
42431
+ const capabilities = getCapabilities(project);
42246
42432
  const enabledReactDoctorRules = {};
42247
42433
  for (const registryEntry of REACT_DOCTOR_RULES) {
42248
42434
  const rule = reactDoctorPlugin.rules[registryEntry.id];
@@ -42253,7 +42439,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42253
42439
  if (rule.scan !== void 0) continue;
42254
42440
  if (customRulesOnly && registryEntry.originallyExternal) continue;
42255
42441
  if (rule.framework !== "global" && !rule.requires) continue;
42256
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
42442
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) continue;
42257
42443
  const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
42258
42444
  if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
42259
42445
  const severity = resolveRuleSeverityOverride({
@@ -42284,6 +42470,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42284
42470
  settings: { "react-doctor": {
42285
42471
  framework: project.framework,
42286
42472
  rootDirectory: resolveSettingsRootDirectory(project.rootDirectory),
42473
+ capabilities: [...capabilities].sort(),
42287
42474
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
42288
42475
  ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
42289
42476
  } },
@@ -42574,25 +42761,18 @@ const mapWithConcurrency = async (items, concurrency, task) => {
42574
42761
  if (errors.length > 0) throw errors[0];
42575
42762
  return results;
42576
42763
  };
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
42764
  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";
42765
+ const ACCESSORS_MIN_VERSION = "3.15.0";
42766
+ const supportsAccessors = (reanimatedVersion) => {
42767
+ if (reanimatedVersion === null) return true;
42768
+ const lowerBound = import_semver.validRange(reanimatedVersion) !== null ? import_semver.minVersion(reanimatedVersion) : import_semver.coerce(reanimatedVersion);
42769
+ if (lowerBound === null) return true;
42770
+ return import_semver.gte(lowerBound, ACCESSORS_MIN_VERSION);
42771
+ };
42593
42772
  const appendReanimatedSharedValueHint = (help, rule, project) => {
42594
42773
  if (rule !== "immutability") return help;
42595
42774
  if (!project.hasReanimated) return help;
42775
+ if (!supportsAccessors(project.reanimatedVersion)) return help;
42596
42776
  if (!help) return REANIMATED_SHARED_VALUE_HINT;
42597
42777
  return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
42598
42778
  };
@@ -42754,13 +42934,13 @@ const REACT_USE_BINDING_RESOLUTION = {
42754
42934
  isReactUseBinding: true,
42755
42935
  isReactNamespaceBinding: false
42756
42936
  };
42757
- const getScriptKind = (filename) => {
42937
+ const getScriptKind$2 = (filename) => {
42758
42938
  if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
42759
42939
  if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
42760
42940
  if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
42761
42941
  return ts.ScriptKind.JS;
42762
42942
  };
42763
- const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
42943
+ const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
42764
42944
  const unwrapExpression = (expression) => {
42765
42945
  let currentExpression = expression;
42766
42946
  while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
@@ -43009,8 +43189,8 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
43009
43189
  return matchedIdentifier;
43010
43190
  };
43011
43191
  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));
43192
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
43193
+ const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
43014
43194
  if (!useIdentifier) return null;
43015
43195
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
43016
43196
  };
@@ -43031,6 +43211,201 @@ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
43031
43211
  const bindingResolution = resolveUseCallBinding(sourceText, absolutePath, primaryLabel.span.offset);
43032
43212
  return bindingResolution !== null && !bindingResolution.isReactUseBinding;
43033
43213
  };
43214
+ const SUPPRESSED_COMPILER_CODES = new Set(["react-hooks-js(immutability)", "react-hooks-js(refs)"]);
43215
+ const WORKLET_DIRECTIVE = "worklet";
43216
+ const WORKLET_ACCEPTING_CALLEES = new Set([
43217
+ "useAnimatedStyle",
43218
+ "useAnimatedProps",
43219
+ "useDerivedValue",
43220
+ "useAnimatedReaction",
43221
+ "useAnimatedScrollHandler",
43222
+ "useAnimatedGestureHandler",
43223
+ "useFrameCallback",
43224
+ "runOnUI",
43225
+ "executeOnUIRuntimeSync"
43226
+ ]);
43227
+ const GESTURE_ROOT_IDENTIFIER = "Gesture";
43228
+ const getScriptKind$1 = (filename) => {
43229
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43230
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43231
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43232
+ return ts.ScriptKind.JS;
43233
+ };
43234
+ const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
43235
+ const hasWorkletDirective = (node) => {
43236
+ if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
43237
+ const body = node.body;
43238
+ if (!body || !ts.isBlock(body)) return false;
43239
+ for (const statement of body.statements) {
43240
+ if (!ts.isExpressionStatement(statement)) return false;
43241
+ if (!ts.isStringLiteral(statement.expression)) return false;
43242
+ if (statement.expression.text === WORKLET_DIRECTIVE) return true;
43243
+ }
43244
+ return false;
43245
+ };
43246
+ const rootIdentifierOfCallChain = (expression) => {
43247
+ let currentExpression = expression;
43248
+ while (true) {
43249
+ if (ts.isIdentifier(currentExpression)) return currentExpression.text;
43250
+ if (ts.isPropertyAccessExpression(currentExpression)) {
43251
+ currentExpression = currentExpression.expression;
43252
+ continue;
43253
+ }
43254
+ if (ts.isCallExpression(currentExpression)) {
43255
+ currentExpression = currentExpression.expression;
43256
+ continue;
43257
+ }
43258
+ return null;
43259
+ }
43260
+ };
43261
+ const isWorkletAcceptingCall = (callExpression) => {
43262
+ const callee = callExpression.expression;
43263
+ if (ts.isIdentifier(callee)) return WORKLET_ACCEPTING_CALLEES.has(callee.text);
43264
+ if (ts.isPropertyAccessExpression(callee)) {
43265
+ if (WORKLET_ACCEPTING_CALLEES.has(callee.name.text)) return true;
43266
+ return rootIdentifierOfCallChain(callee) === GESTURE_ROOT_IDENTIFIER;
43267
+ }
43268
+ return false;
43269
+ };
43270
+ const isFunctionWorklet = (functionNode) => {
43271
+ if (hasWorkletDirective(functionNode)) return true;
43272
+ const parent = functionNode.parent;
43273
+ return ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === functionNode) && isWorkletAcceptingCall(parent);
43274
+ };
43275
+ const findNodeAtOffset = (sourceFile, targetOffset) => {
43276
+ let matchedNode = null;
43277
+ const visit = (node) => {
43278
+ if (node.getStart(sourceFile) > targetOffset || node.getEnd() <= targetOffset) return;
43279
+ matchedNode = node;
43280
+ ts.forEachChild(node, visit);
43281
+ };
43282
+ visit(sourceFile);
43283
+ return matchedNode;
43284
+ };
43285
+ const isOffsetInsideWorklet = (sourceFile, targetOffset) => {
43286
+ let currentNode = findNodeAtOffset(sourceFile, targetOffset);
43287
+ while (currentNode) {
43288
+ if (ts.isFunctionLike(currentNode) && isFunctionWorklet(currentNode)) return true;
43289
+ currentNode = currentNode.parent;
43290
+ }
43291
+ return false;
43292
+ };
43293
+ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirectory) => {
43294
+ if (!SUPPRESSED_COMPILER_CODES.has(diagnostic.code)) return false;
43295
+ if (!project.hasReanimated) return false;
43296
+ const primaryLabel = diagnostic.labels[0];
43297
+ if (!primaryLabel) return false;
43298
+ const absolutePath = Path.isAbsolute(diagnostic.filename) ? diagnostic.filename : Path.join(rootDirectory, diagnostic.filename);
43299
+ let sourceText;
43300
+ try {
43301
+ sourceText = NFS.readFileSync(absolutePath, "utf-8");
43302
+ } catch {
43303
+ return false;
43304
+ }
43305
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
43306
+ };
43307
+ const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
43308
+ const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
43309
+ const REACT_COMPILER_PLUGIN = "react-hooks-js";
43310
+ const getScriptKind = (filename) => {
43311
+ if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43312
+ if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43313
+ if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43314
+ return ts.ScriptKind.JS;
43315
+ };
43316
+ const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
43317
+ const nodeLineRange = (sourceFile, node) => ({
43318
+ startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
43319
+ endLine: sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1
43320
+ });
43321
+ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
43322
+ let foundRange = null;
43323
+ const visit = (node) => {
43324
+ if (foundRange) return;
43325
+ if (ts.isFunctionLike(node)) {
43326
+ const range = nodeLineRange(sourceFile, node);
43327
+ if (range.startLine <= targetLine && targetLine <= range.endLine) foundRange = range;
43328
+ return;
43329
+ }
43330
+ ts.forEachChild(node, visit);
43331
+ };
43332
+ visit(sourceFile);
43333
+ return foundRange;
43334
+ };
43335
+ const NEWLINE_BYTE = 10;
43336
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
43337
+ let lineNumber = 1;
43338
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
43339
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
43340
+ return lineNumber;
43341
+ };
43342
+ /**
43343
+ * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
43344
+ * whose enclosing function also carries a `react-hooks-js` (React
43345
+ * Compiler) diagnostic. Every react-hooks-js finding marks a function the
43346
+ * compiler could NOT compile (a bail-out / skip / preserve-manual-
43347
+ * memoization failure), so "React Compiler already caches every value
43348
+ * here" is false for that function and the manual memoization is
43349
+ * load-bearing, not dead weight.
43350
+ *
43351
+ * Matching is per enclosing function, not per file: a bailed-out
43352
+ * component must not silence redundant-memo findings in a sibling
43353
+ * component the compiler did optimize.
43354
+ *
43355
+ * Module-scope `memo(() => …)` wrappers have no enclosing function; for
43356
+ * those the memo call's own span is the containment range, so a bail-out
43357
+ * inside the inline-wrapped component still suppresses. Every failure
43358
+ * path (unreadable file, missing span) fails open and keeps the
43359
+ * diagnostic.
43360
+ */
43361
+ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) => {
43362
+ const compilerDiagnosticLinesByFile = /* @__PURE__ */ new Map();
43363
+ let hasManualMemoizationDiagnostic = false;
43364
+ for (const diagnostic of diagnostics) {
43365
+ if (isManualMemoizationDiagnostic(diagnostic)) {
43366
+ hasManualMemoizationDiagnostic = true;
43367
+ continue;
43368
+ }
43369
+ if (diagnostic.plugin !== REACT_COMPILER_PLUGIN || diagnostic.line <= 0) continue;
43370
+ const lines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
43371
+ if (lines) lines.push(diagnostic.line);
43372
+ else compilerDiagnosticLinesByFile.set(diagnostic.filePath, [diagnostic.line]);
43373
+ }
43374
+ if (!hasManualMemoizationDiagnostic || compilerDiagnosticLinesByFile.size === 0) return diagnostics;
43375
+ const parsedFileCache = /* @__PURE__ */ new Map();
43376
+ const readAndParse = (filePath) => {
43377
+ const cached = parsedFileCache.get(filePath);
43378
+ if (cached !== void 0) return cached;
43379
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
43380
+ let parsed;
43381
+ try {
43382
+ const buffer = NFS.readFileSync(absolutePath);
43383
+ parsed = {
43384
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
43385
+ buffer
43386
+ };
43387
+ } catch {
43388
+ parsed = null;
43389
+ }
43390
+ parsedFileCache.set(filePath, parsed);
43391
+ return parsed;
43392
+ };
43393
+ const isInsideBailedOutFunction = (diagnostic) => {
43394
+ const compilerLines = compilerDiagnosticLinesByFile.get(diagnostic.filePath);
43395
+ if (!compilerLines || diagnostic.line <= 0) return false;
43396
+ const parsed = readAndParse(diagnostic.filePath);
43397
+ if (!parsed) return false;
43398
+ let containmentRange = findOutermostEnclosingFunctionRange(parsed.sourceFile, diagnostic.line);
43399
+ if (!containmentRange && diagnostic.offset !== void 0 && diagnostic.length !== void 0) containmentRange = {
43400
+ startLine: diagnostic.line,
43401
+ endLine: lineOfUtf8Offset(parsed.buffer, diagnostic.offset + diagnostic.length)
43402
+ };
43403
+ if (!containmentRange) return false;
43404
+ const { startLine, endLine } = containmentRange;
43405
+ return compilerLines.some((compilerLine) => startLine <= compilerLine && compilerLine <= endLine);
43406
+ };
43407
+ return diagnostics.filter((diagnostic) => !isManualMemoizationDiagnostic(diagnostic) || !isInsideBailedOutFunction(diagnostic));
43408
+ };
43034
43409
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
43035
43410
  const LEADING_SEVERITY_LABEL_PATTERN = /^(?:Error|Warning):\s*/;
43036
43411
  const TRAILING_PERIOD_PATTERN = /\.$/;
@@ -43066,8 +43441,14 @@ const PLUGIN_CATEGORY_MAP = {
43066
43441
  };
43067
43442
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
43068
43443
  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;
43444
+ const rule = reactDoctorPlugin.rules[ruleName];
43445
+ if (!rule) return void 0;
43446
+ if (rule.recommendationFor) {
43447
+ const capabilities = getCapabilities(project);
43448
+ const conditionalRecommendation = rule.recommendationFor((capability) => capabilities.has(capability));
43449
+ if (conditionalRecommendation !== void 0) return conditionalRecommendation;
43450
+ }
43451
+ return rule.recommendation;
43071
43452
  };
43072
43453
  const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
43073
43454
  const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
@@ -43170,7 +43551,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43170
43551
  minifiedFileCache.set(absolutePath, minified);
43171
43552
  return minified;
43172
43553
  };
43173
- return parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
43554
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
43174
43555
  const { plugin, rule } = parseRuleCode(diagnostic.code);
43175
43556
  const primaryLabel = diagnostic.labels[0];
43176
43557
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -43197,7 +43578,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43197
43578
  ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
43198
43579
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
43199
43580
  };
43200
- });
43581
+ }), rootDirectory);
43201
43582
  };
43202
43583
  const buildOxlintChildEnv = (sourceEnv) => {
43203
43584
  const childEnv = {};
@@ -43708,6 +44089,7 @@ const runOxlint = async (options) => {
43708
44089
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
43709
44090
  const severityControls = buildRuleSeverityControls(userConfig);
43710
44091
  validateRuleRegistration();
44092
+ resetManifestCaches();
43711
44093
  if (includePaths !== void 0 && includePaths.length === 0) return [];
43712
44094
  const pluginPath = resolvePluginPath();
43713
44095
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
@@ -44370,9 +44752,10 @@ const CONTEXT_AXES = [
44370
44752
  }
44371
44753
  ];
44372
44754
  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;
44755
+ const gatedAxesForSection = (section) => section === "devDependencies" ? GATED_AXES.filter((axis) => axis.key !== "vulnerability") : GATED_AXES;
44756
+ const worstGatedAxis = (score, gatedAxes) => {
44757
+ let worst = gatedAxes[0];
44758
+ for (const axis of gatedAxes) if (score[axis.key] < score[worst.key]) worst = axis;
44376
44759
  return worst;
44377
44760
  };
44378
44761
  const clampScore = (value) => {
@@ -44396,6 +44779,69 @@ const resolveConcreteVersion = (spec) => {
44396
44779
  if (range === null || range === "*") return null;
44397
44780
  return import_semver.minVersion(trimmed)?.version ?? null;
44398
44781
  };
44782
+ const readTextFileOrNull = (filePath) => {
44783
+ try {
44784
+ return NFS.readFileSync(filePath, "utf-8");
44785
+ } catch {
44786
+ return null;
44787
+ }
44788
+ };
44789
+ const buildNpmLockLookup = (lockText) => {
44790
+ let parsed;
44791
+ try {
44792
+ parsed = JSON.parse(lockText);
44793
+ } catch {
44794
+ return null;
44795
+ }
44796
+ if (typeof parsed !== "object" || parsed === null) return null;
44797
+ const lock = parsed;
44798
+ return (name) => {
44799
+ const entry = lock.packages?.[`node_modules/${name}`] ?? lock.dependencies?.[name];
44800
+ return typeof entry?.version === "string" ? entry.version : null;
44801
+ };
44802
+ };
44803
+ const buildYarnLockLookup = (lockText) => {
44804
+ const lines = lockText.split(/\r?\n/);
44805
+ return (name, spec) => {
44806
+ const descriptor = `${name}@${spec}`;
44807
+ const berryDescriptor = `${name}@npm:${spec}`;
44808
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
44809
+ const line = lines[lineIndex];
44810
+ if (line.startsWith(" ") || line.startsWith("#") || !line.includes(":")) continue;
44811
+ const descriptors = line.replace(/:\s*$/, "").split(",").map((part) => part.trim().replace(/^"|"$/g, ""));
44812
+ if (!descriptors.includes(descriptor) && !descriptors.includes(berryDescriptor)) continue;
44813
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && (lines[bodyIndex].startsWith(" ") || lines[bodyIndex] === ""); bodyIndex += 1) {
44814
+ const versionMatch = /^\s+version:?\s+"?([^"\s]+)"?/.exec(lines[bodyIndex]);
44815
+ if (versionMatch) return versionMatch[1];
44816
+ }
44817
+ return null;
44818
+ }
44819
+ return null;
44820
+ };
44821
+ };
44822
+ const buildPnpmLockLookup = (lockText) => {
44823
+ return (name) => {
44824
+ const entryPattern = new RegExp(`^ {4,8}['"]?${name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}['"]?:\\s*$`);
44825
+ const lines = lockText.split(/\r?\n/);
44826
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
44827
+ if (!entryPattern.test(lines[lineIndex])) continue;
44828
+ for (let bodyIndex = lineIndex + 1; bodyIndex < lines.length && /^ {5,}\S/.test(lines[bodyIndex]); bodyIndex += 1) {
44829
+ const versionMatch = /^\s+version:\s+['"]?([^'"\s(]+)/.exec(lines[bodyIndex]);
44830
+ if (versionMatch) return versionMatch[1];
44831
+ }
44832
+ }
44833
+ return null;
44834
+ };
44835
+ };
44836
+ const buildLockedVersionLookup = (rootDirectory) => {
44837
+ const npmLockText = readTextFileOrNull(Path.join(rootDirectory, "package-lock.json"));
44838
+ if (npmLockText !== null) return buildNpmLockLookup(npmLockText);
44839
+ const yarnLockText = readTextFileOrNull(Path.join(rootDirectory, "yarn.lock"));
44840
+ if (yarnLockText !== null) return buildYarnLockLookup(yarnLockText);
44841
+ const pnpmLockText = readTextFileOrNull(Path.join(rootDirectory, "pnpm-lock.yaml"));
44842
+ if (pnpmLockText !== null) return buildPnpmLockLookup(pnpmLockText);
44843
+ return null;
44844
+ };
44399
44845
  const locateDependencyKey = (packageJsonText, section, name) => {
44400
44846
  const needle = `"${name}"`;
44401
44847
  const sectionHeader = new RegExp(`"${section}"\\s*:\\s*\\{`);
@@ -44422,7 +44868,7 @@ const locateDependencyKey = (packageJsonText, section, name) => {
44422
44868
  }
44423
44869
  return null;
44424
44870
  };
44425
- const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies) => {
44871
+ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDependencies, lockedVersionLookup) => {
44426
44872
  const sectionByName = /* @__PURE__ */ new Map();
44427
44873
  for (const name of Object.keys(packageJson.dependencies ?? {})) sectionByName.set(name, "dependencies");
44428
44874
  if (includeDevDependencies) {
@@ -44432,13 +44878,18 @@ const collectDependenciesToScore = (packageJson, packageJsonText, includeDevDepe
44432
44878
  for (const [name, section] of sectionByName) {
44433
44879
  if (SUPPLY_CHAIN_IGNORED_PACKAGES.has(name)) continue;
44434
44880
  const spec = (packageJson[section] ?? {})[name] ?? "";
44435
- const version = resolveConcreteVersion(spec);
44436
- if (version === null) continue;
44881
+ const floorVersion = resolveConcreteVersion(spec);
44882
+ if (floorVersion === null) continue;
44883
+ const lockedVersion = lockedVersionLookup?.(name, spec) ?? null;
44884
+ const lockSatisfiesSpec = lockedVersion !== null && import_semver.valid(lockedVersion) !== null && import_semver.satisfies(lockedVersion, spec);
44885
+ const isExactPin = import_semver.valid(spec.trim()) !== null;
44437
44886
  const location = locateDependencyKey(packageJsonText, section, name);
44438
44887
  dependencies.push({
44439
44888
  name,
44440
- version,
44889
+ version: lockSatisfiesSpec ? lockedVersion : floorVersion,
44441
44890
  spec,
44891
+ section,
44892
+ resolution: isExactPin ? "pin" : lockSatisfiesSpec ? "locked" : "floor",
44442
44893
  line: location?.line ?? 0,
44443
44894
  column: location?.column ?? 0
44444
44895
  });
@@ -44589,7 +45040,11 @@ const formatAlertReason = (topAlerts, totalCount) => {
44589
45040
  }
44590
45041
  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
45042
  };
44592
- const formatDependencyIdentity = (dependency) => import_semver.valid(dependency.spec) !== null ? `${dependency.name}@${dependency.version}` : `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
45043
+ const formatDependencyIdentity = (dependency) => {
45044
+ if (dependency.resolution === "pin") return `${dependency.name}@${dependency.version}`;
45045
+ if (dependency.resolution === "locked") return `${dependency.name}@${dependency.version} (lockfile resolution of "${dependency.spec}")`;
45046
+ return `${dependency.name}@${dependency.version} (lowest version "${dependency.spec}" allows)`;
45047
+ };
44593
45048
  const buildSupplyChainHelp = (dependency, failingAxis, topAlerts, packagePageUrl, options) => {
44594
45049
  const hasCriticalAlert = topAlerts.some((alert) => alert.severity.toLowerCase() === "critical");
44595
45050
  const entry = `\`"${dependency.name}": "${dependency.spec}"\``;
@@ -44632,7 +45087,7 @@ const buildLowScoreDiagnostic = (dependency, artifact, failingAxis, options) =>
44632
45087
  const checkSupplyChain = (input) => gen(function* () {
44633
45088
  const options = resolveOptions(input.userConfig);
44634
45089
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
44635
- const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
45090
+ const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies, buildLockedVersionLookup(input.rootDirectory));
44636
45091
  if (dependencies.length === 0) return [];
44637
45092
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
44638
45093
  if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
@@ -44641,7 +45096,7 @@ const checkSupplyChain = (input) => gen(function* () {
44641
45096
  for (let index = 0; index < dependencies.length; index += 1) {
44642
45097
  const artifact = artifacts[index];
44643
45098
  if (!artifact) continue;
44644
- const worstAxis = worstGatedAxis(artifact.score);
45099
+ const worstAxis = worstGatedAxis(artifact.score, gatedAxesForSection(dependencies[index].section));
44645
45100
  if (toHundred(artifact.score[worstAxis.key]) >= options.minScore) continue;
44646
45101
  diagnostics.push(buildLowScoreDiagnostic(dependencies[index], artifact, worstAxis, options));
44647
45102
  }
@@ -45342,6 +45797,13 @@ const summarizeDiagnostics = (diagnostics, worstScore = null, worstScoreLabel =
45342
45797
  scoreLabel: worstScoreLabel
45343
45798
  };
45344
45799
  };
45800
+ /**
45801
+ * Whether project discovery resolved a React-compatible runtime (React or
45802
+ * Preact, which ships the same hooks + JSX model). This is the same
45803
+ * predicate that grants the `react` capability, so `false` means every
45804
+ * React-runtime rule family was gated off for the project.
45805
+ */
45806
+ const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
45345
45807
  const toJsonDiff = (diff) => {
45346
45808
  if (!diff) return null;
45347
45809
  return {
@@ -45924,7 +46386,7 @@ const makeNoopConsole = () => ({
45924
46386
  });
45925
46387
  //#endregion
45926
46388
  //#region src/cli/utils/version.ts
45927
- const VERSION = "0.7.1";
46389
+ const VERSION = "0.7.2-dev.0eb5293";
45928
46390
  //#endregion
45929
46391
  //#region src/cli/utils/json-mode.ts
45930
46392
  let context = null;
@@ -46293,13 +46755,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
46293
46755
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
46294
46756
  * standard `SENTRY_RELEASE` override.
46295
46757
  */
46296
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.1`;
46758
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.2-dev.0eb5293`;
46297
46759
  /**
46298
46760
  * Deployment environment shown in Sentry's environment filter. Defaults to
46299
46761
  * `production` for tagged releases and `development` for dev/unbuilt versions,
46300
46762
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
46301
46763
  */
46302
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.1") ? "development" : "production");
46764
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.2-dev.0eb5293") ? "development" : "production");
46303
46765
  /**
46304
46766
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
46305
46767
  * (set to `0` to disable tracing) and falls back to
@@ -58143,7 +58605,7 @@ const buildRuntimeLayers = (input) => {
58143
58605
  const linterLayer = input.shouldSkipLint ? Linter.layerOf([]) : Linter.layerOxlint;
58144
58606
  const deadCodeLayer = input.shouldRunDeadCode ? DeadCode.layerNode : DeadCode.layerOf([]);
58145
58607
  const scoreLayer = input.shouldComputeScore ? Score.layerHttp : Score.layerOf(null);
58146
- const supplyChainLayer = input.userConfig?.supplyChain?.enabled === false ? SupplyChain.layerOf([]) : SupplyChain.layerNode;
58608
+ const supplyChainLayer = input.shouldRunSupplyChain ? SupplyChain.layerNode : SupplyChain.layerOf([]);
58147
58609
  const projectLayer = input.projectInfoOverride === void 0 ? Project.layerNode : Project.layerOf(input.projectInfoOverride);
58148
58610
  const progressLayer = input.shouldShowProgressSpinners ? Progress.layerOra(buildSpinnerProgressHandle) : Progress.layerNoop;
58149
58611
  const baseLayers = mergeAll$1(projectLayer, input.hasConfigOverride ? Config.layerOf({
@@ -58499,21 +58961,22 @@ const resolveDotenvFingerprint = (projectDirectory) => {
58499
58961
  return [];
58500
58962
  }
58501
58963
  };
58502
- const hasHiddenTrackedFileState = (projectDirectory) => {
58964
+ const isGitIdentityTrustworthy = (projectDirectory) => {
58503
58965
  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");
58966
+ if (output === null) return false;
58967
+ const entryLines = output.split("\n").filter((line) => line.length > 0);
58968
+ return entryLines.length > 0 && entryLines.every((line) => line[0] === "H");
58506
58969
  };
58507
58970
  const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
58508
58971
  const readPersistedCache = (cacheFilePath) => {
58509
58972
  try {
58510
58973
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
58511
- if (!isRecord(parsed) || parsed.version !== 3) return {
58512
- version: 3,
58974
+ if (!isRecord(parsed) || parsed.version !== 4) return {
58975
+ version: 4,
58513
58976
  entries: []
58514
58977
  };
58515
58978
  if (!Array.isArray(parsed.entries)) return {
58516
- version: 3,
58979
+ version: 4,
58517
58980
  entries: []
58518
58981
  };
58519
58982
  const entries = [];
@@ -58523,12 +58986,12 @@ const readPersistedCache = (cacheFilePath) => {
58523
58986
  entries.push(entry);
58524
58987
  }
58525
58988
  return {
58526
- version: 3,
58989
+ version: 4,
58527
58990
  entries
58528
58991
  };
58529
58992
  } catch {
58530
58993
  return {
58531
- version: 3,
58994
+ version: 4,
58532
58995
  entries: []
58533
58996
  };
58534
58997
  }
@@ -58560,6 +59023,7 @@ const resolveProjectIdentity = (projectDirectory) => {
58560
59023
  return path.resolve(projectDirectory);
58561
59024
  }
58562
59025
  };
59026
+ const resolveManifestContentHash = (projectDirectory) => hashFileContents(getPackageJsonPath(projectDirectory));
58563
59027
  const fileFingerprint = (filePath) => {
58564
59028
  try {
58565
59029
  const stat = fs.statSync(filePath);
@@ -58582,14 +59046,14 @@ const resolveToolchainFingerprint = (nodeBinaryPath) => {
58582
59046
  };
58583
59047
  const buildScanResultCacheKey = (input) => {
58584
59048
  if (isCacheGloballyDisabled()) return null;
58585
- if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
59049
+ if (!isGitIdentityTrustworthy(input.projectDirectory)) return null;
58586
59050
  const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
58587
59051
  if (worktreeFingerprint === null) return null;
58588
59052
  const headSha = readHeadSha(input.projectDirectory);
58589
59053
  if (headSha === null) return null;
58590
59054
  if (stringifyStableJson(input.userConfig) === null) return null;
58591
59055
  const cacheKeyJson = stringifyStableJson({
58592
- schemaVersion: 3,
59056
+ schemaVersion: 4,
58593
59057
  projectIdentity: resolveProjectIdentity(input.projectDirectory),
58594
59058
  headSha,
58595
59059
  worktreeFingerprint,
@@ -58604,6 +59068,7 @@ const buildScanResultCacheKey = (input) => {
58604
59068
  engineOptions: {
58605
59069
  lint: input.options.lint,
58606
59070
  deadCode: input.options.deadCode,
59071
+ supplyChain: input.options.supplyChain,
58607
59072
  includePaths: [...input.options.includePaths].sort(),
58608
59073
  customRulesOnly: input.options.customRulesOnly,
58609
59074
  respectInlineDisables: input.options.respectInlineDisables,
@@ -58629,17 +59094,24 @@ const createScanResultCache = (projectDirectory) => {
58629
59094
  for (const entry of persistedCache.entries) entries.set(entry.key, entry);
58630
59095
  const persist = () => {
58631
59096
  writePersistedCache(cacheFilePath, {
58632
- version: 3,
59097
+ version: 4,
58633
59098
  entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
58634
59099
  });
58635
59100
  };
58636
59101
  return {
58637
- lookup: (key) => entries.get(key)?.payload ?? null,
59102
+ lookup: (key) => {
59103
+ const payload = entries.get(key)?.payload;
59104
+ if (payload === void 0) return null;
59105
+ return resolveProjectIdentity(payload.directory) === resolveProjectIdentity(projectDirectory) && (payload.manifestContentHash ?? null) === resolveManifestContentHash(projectDirectory) ? payload : null;
59106
+ },
58638
59107
  store: (key, payload) => {
58639
59108
  entries.set(key, {
58640
59109
  key,
58641
59110
  createdAtMs: Date.now(),
58642
- payload
59111
+ payload: {
59112
+ ...payload,
59113
+ manifestContentHash: resolveManifestContentHash(projectDirectory)
59114
+ }
58643
59115
  });
58644
59116
  persist();
58645
59117
  }
@@ -58806,13 +59278,15 @@ const buildOutcomeAttributes = (input) => {
58806
59278
  const countBySource = {
58807
59279
  config: 0,
58808
59280
  override: 0,
58809
- inline: 0
59281
+ inline: 0,
59282
+ "foreign-inline": 0
58810
59283
  };
58811
59284
  for (const suppression of input.suppressedRuleCounts) countBySource[suppression.source] += suppression.count;
58812
- suppressionRollup.suppressed = countBySource.config + countBySource.override + countBySource.inline;
59285
+ suppressionRollup.suppressed = countBySource.config + countBySource.override + countBySource.inline + countBySource["foreign-inline"];
58813
59286
  suppressionRollup.suppressedConfig = countBySource.config;
58814
59287
  suppressionRollup.suppressedOverride = countBySource.override;
58815
59288
  suppressionRollup.suppressedInline = countBySource.inline;
59289
+ suppressionRollup.suppressedForeignInline = countBySource["foreign-inline"];
58816
59290
  }
58817
59291
  const attributes = {
58818
59292
  ...withNamespace("outcome", {
@@ -58904,6 +59378,7 @@ const buildScanAttributes = (input) => {
58904
59378
  maxDurationMs: input.maxDurationMs,
58905
59379
  lint: input.lint,
58906
59380
  deadCode: input.deadCode,
59381
+ supplyChain: input.supplyChain,
58907
59382
  scoreOnly: input.scoreOnly,
58908
59383
  noScore: input.noScore,
58909
59384
  respectInlineDisables: input.respectInlineDisables,
@@ -60178,6 +60653,7 @@ const buildIgnoredTags = (userConfig) => {
60178
60653
  const mergeInspectOptions = (inputOptions, userConfig) => ({
60179
60654
  lint: inputOptions.lint ?? userConfig?.lint ?? true,
60180
60655
  deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
60656
+ supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
60181
60657
  verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
60182
60658
  outputDirectory: inputOptions.outputDirectory || null,
60183
60659
  scoreOnly: inputOptions.scoreOnly ?? false,
@@ -60217,6 +60693,7 @@ const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorke
60217
60693
  maxDurationMs: options.maxDurationMs,
60218
60694
  lint: options.lint,
60219
60695
  deadCode: options.deadCode,
60696
+ supplyChain: options.supplyChain,
60220
60697
  scoreOnly: options.scoreOnly,
60221
60698
  noScore: options.noScore,
60222
60699
  respectInlineDisables: options.respectInlineDisables,
@@ -60300,6 +60777,7 @@ const runBaselineComparison = async (params) => {
60300
60777
  projectInfoOverride: params.headProjectInfo,
60301
60778
  shouldSkipLint: !params.options.lint || !params.resolvedNodeBinaryPath,
60302
60779
  shouldRunDeadCode: false,
60780
+ shouldRunSupplyChain: params.options.supplyChain,
60303
60781
  shouldComputeScore: false,
60304
60782
  shouldShowProgressSpinners: false,
60305
60783
  oxlintConcurrency: params.options.concurrency
@@ -60388,6 +60866,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
60388
60866
  configSourceDirectory,
60389
60867
  shouldSkipLint: !options.lint || lintBindingMissing,
60390
60868
  shouldRunDeadCode: options.deadCode,
60869
+ shouldRunSupplyChain: options.supplyChain,
60391
60870
  shouldComputeScore: !options.noScore,
60392
60871
  shouldShowProgressSpinners,
60393
60872
  oxlintConcurrency: options.concurrency
@@ -62673,8 +63152,8 @@ const resolveParallelFlag = (parallel) => parallel === false ? 1 : void 0;
62673
63152
  * accepts. Flag-derived fields only (`scoreOnly`, `noScore`, `silent`,
62674
63153
  * `isCi`): every `userConfig` fallback — including `noScore` — lives in
62675
63154
  * `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
63155
+ * config. The plain boolean knobs (`lint`, `deadCode`, `supplyChain`,
63156
+ * `verbose`) pass through unchanged for the same reason. This resolver reads
62678
63157
  * `userConfig` only to decide the `--blocking warning` gate.
62679
63158
  */
62680
63159
  const resolveCliInspectOptions = (flags, userConfig) => {
@@ -62682,6 +63161,7 @@ const resolveCliInspectOptions = (flags, userConfig) => {
62682
63161
  return {
62683
63162
  lint: flags.lint,
62684
63163
  deadCode: flags.deadCode,
63164
+ supplyChain: flags.supplyChain,
62685
63165
  verbose: flags.verbose,
62686
63166
  outputDirectory: flags.outputDir,
62687
63167
  respectInlineDisables: flags.respectInlineDisables === false ? false : void 0,
@@ -63232,7 +63712,7 @@ const inspectAction = async (directory, flags) => {
63232
63712
  const scanDirectory = projectScanTarget.resolvedDirectory;
63233
63713
  const projectConfig = projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0);
63234
63714
  const projectConfigSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
63235
- const supplyChainEnabled = projectConfig?.supplyChain?.enabled !== false;
63715
+ const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
63236
63716
  let includePaths;
63237
63717
  let supplyChainManifestChanged = false;
63238
63718
  if (isDiffMode) {
@@ -64152,10 +64632,12 @@ const ROOT_FLAG_SPEC = {
64152
64632
  "--no-parallel",
64153
64633
  "--no-respect-inline-disables",
64154
64634
  "--no-score",
64635
+ "--no-supply-chain",
64155
64636
  "--no-telemetry",
64156
64637
  "--no-warnings",
64157
64638
  "--score",
64158
64639
  "--staged",
64640
+ "--supply-chain",
64159
64641
  "--verbose",
64160
64642
  "--version",
64161
64643
  "--warnings",
@@ -64404,7 +64886,7 @@ ${highlighter.dim("Learn more:")}
64404
64886
  ${highlighter.info(CI_URL)}
64405
64887
  `;
64406
64888
  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);
64889
+ 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
64890
  program.action(inspectAction);
64409
64891
  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
64892
  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 +64936,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
64454
64936
  export {};
64455
64937
 
64456
64938
  //# sourceMappingURL=cli.js.map
64457
- //# debugId=8cb1a73e-b148-513d-a3eb-69f3a12430a5
64939
+ //# debugId=91ba16f6-8b1a-5cd9-a5d2-fc0b6fce2508