react-doctor 0.8.0-dev.eb2d687 → 0.8.1-dev.0b0b5ac

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]="cfa479fc-64e6-5081-86f2-1cbef233c1a0")}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]="a9af63ed-7b82-56d3-8549-274ab7f23579")}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";
@@ -36242,7 +36242,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36242
36242
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36243
36243
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36244
36244
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36245
- const CORE_PACKAGE_VERSION = "0.8.0";
36245
+ const CORE_PACKAGE_VERSION = "0.8.1";
36246
36246
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36247
36247
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36248
36248
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -36571,10 +36571,10 @@ const FALLBACK_TSCONFIG_FILENAMES = ["tsconfig.app.json", "tsconfig.build.json"]
36571
36571
  const FRAMEWORK_PACKAGES = {
36572
36572
  next: "nextjs",
36573
36573
  "@tanstack/react-start": "tanstack-start",
36574
- vite: "vite",
36575
- "react-scripts": "cra",
36576
36574
  "@remix-run/react": "remix",
36577
36575
  gatsby: "gatsby",
36576
+ vite: "vite",
36577
+ "react-scripts": "cra",
36578
36578
  expo: "expo",
36579
36579
  "react-native": "react-native"
36580
36580
  };
@@ -36598,11 +36598,17 @@ const NEXT_CONFIG_FILENAMES = [
36598
36598
  ];
36599
36599
  const BABEL_CONFIG_FILENAMES = [
36600
36600
  ".babelrc",
36601
+ ".babelrc.js",
36601
36602
  ".babelrc.json",
36603
+ ".babelrc.cjs",
36604
+ ".babelrc.mjs",
36605
+ ".babelrc.cts",
36602
36606
  "babel.config.js",
36603
36607
  "babel.config.json",
36604
36608
  "babel.config.cjs",
36605
- "babel.config.mjs"
36609
+ "babel.config.mjs",
36610
+ "babel.config.ts",
36611
+ "babel.config.cts"
36606
36612
  ];
36607
36613
  const VITE_CONFIG_FILENAMES = [
36608
36614
  "vite.config.js",
@@ -36614,12 +36620,28 @@ const VITE_CONFIG_FILENAMES = [
36614
36620
  "vitest.config.ts",
36615
36621
  "vitest.config.js"
36616
36622
  ];
36623
+ const RSBUILD_CONFIG_FILENAMES = [
36624
+ "rsbuild.config.ts",
36625
+ "rsbuild.config.js",
36626
+ "rsbuild.config.mts",
36627
+ "rsbuild.config.mjs",
36628
+ "rsbuild.config.cts",
36629
+ "rsbuild.config.cjs"
36630
+ ];
36631
+ const RSPACK_CONFIG_FILENAMES = [
36632
+ "rspack.config.ts",
36633
+ "rspack.config.js",
36634
+ "rspack.config.mts",
36635
+ "rspack.config.mjs",
36636
+ "rspack.config.cts",
36637
+ "rspack.config.cjs"
36638
+ ];
36617
36639
  const EXPO_APP_CONFIG_FILENAMES = [
36618
36640
  "app.json",
36619
36641
  "app.config.js",
36620
36642
  "app.config.ts"
36621
36643
  ];
36622
- const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|["']react-compiler["']/;
36644
+ const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|reactCompilerPreset\s*\(|["']react-compiler["']/;
36623
36645
  const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
36624
36646
  const STATIC_EXPORT_OUTPUT_PATTERN = /(?:^|[^.\w])["']?output["']?\s*:\s*["']export["']/m;
36625
36647
  const hasCompilerPackage = (packageJson, compilerPackages) => {
@@ -36654,6 +36676,8 @@ const detectReactCompiler = (directory, packageJson) => {
36654
36676
  if (hasCompilerInConfigFiles(directory, NEXT_CONFIG_FILENAMES)) return true;
36655
36677
  if (hasCompilerInConfigFiles(directory, BABEL_CONFIG_FILENAMES)) return true;
36656
36678
  if (hasCompilerInConfigFiles(directory, VITE_CONFIG_FILENAMES)) return true;
36679
+ if (hasCompilerInConfigFiles(directory, RSBUILD_CONFIG_FILENAMES)) return true;
36680
+ if (hasCompilerInConfigFiles(directory, RSPACK_CONFIG_FILENAMES)) return true;
36657
36681
  if (hasCompilerInConfigFiles(directory, EXPO_APP_CONFIG_FILENAMES)) return true;
36658
36682
  return hasCompilerPackageInAncestors(directory, REACT_COMPILER_TRANSFORM_PACKAGES);
36659
36683
  };
@@ -37101,19 +37125,6 @@ const getPreactVersion = (packageJson) => {
37101
37125
  ...packageJson.devDependencies
37102
37126
  }.preact ?? null;
37103
37127
  };
37104
- const TANSTACK_QUERY_PACKAGES = new Set([
37105
- "@tanstack/react-query",
37106
- "@tanstack/query-core",
37107
- "react-query"
37108
- ]);
37109
- const hasTanStackQuery = (packageJson) => {
37110
- const allDependencies = {
37111
- ...packageJson.peerDependencies,
37112
- ...packageJson.dependencies,
37113
- ...packageJson.devDependencies
37114
- };
37115
- return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
37116
- };
37117
37128
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
37118
37129
  if (version === null || !isCatalogReference(version)) return version;
37119
37130
  const catalogName = extractCatalogName(version);
@@ -37316,7 +37327,60 @@ const listWorkspacePackages = (rootDirectory) => {
37316
37327
  }
37317
37328
  return packages;
37318
37329
  };
37330
+ const getPreferredDependencyVersion = ({ packageJson, packageNames }) => {
37331
+ const allDependencies = {
37332
+ ...packageJson.devDependencies,
37333
+ ...packageJson.peerDependencies,
37334
+ ...packageJson.dependencies
37335
+ };
37336
+ for (const packageName of packageNames) {
37337
+ const version = allDependencies[packageName];
37338
+ if (version !== void 0) return version;
37339
+ }
37340
+ return null;
37341
+ };
37342
+ const TANSTACK_REACT_QUERY_PACKAGES = ["@tanstack/react-query", "react-query"];
37343
+ const getTanStackQueryVersion = (packageJson) => {
37344
+ return getPreferredDependencyVersion({
37345
+ packageJson,
37346
+ packageNames: TANSTACK_REACT_QUERY_PACKAGES
37347
+ });
37348
+ };
37349
+ const STYLED_COMPONENTS_PACKAGES = ["styled-components"];
37350
+ const getStyledComponentsVersion = (packageJson) => {
37351
+ return getPreferredDependencyVersion({
37352
+ packageJson,
37353
+ packageNames: STYLED_COMPONENTS_PACKAGES
37354
+ });
37355
+ };
37356
+ const I18N_PACKAGES = [
37357
+ "i18next",
37358
+ "react-i18next",
37359
+ "next-i18next",
37360
+ "react-intl",
37361
+ "next-intl",
37362
+ "use-intl",
37363
+ "@formatjs/intl",
37364
+ "@lingui/core",
37365
+ "@lingui/react",
37366
+ "typesafe-i18n",
37367
+ "@tolgee/react",
37368
+ "react-intl-universal"
37369
+ ];
37370
+ const hasI18nDependency = (packageJson) => {
37371
+ const allDependencies = {
37372
+ ...packageJson.peerDependencies,
37373
+ ...packageJson.dependencies,
37374
+ ...packageJson.devDependencies,
37375
+ ...packageJson.optionalDependencies
37376
+ };
37377
+ return I18N_PACKAGES.some((packageName) => allDependencies[packageName] !== void 0);
37378
+ };
37319
37379
  const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
37380
+ const MOBX_REACT_PACKAGE_NAME = "mobx-react";
37381
+ const MOBX_REACT_LITE_PACKAGE_NAME = "mobx-react-lite";
37382
+ const MOBX_STATE_TREE_PACKAGE_NAME = "mobx-state-tree";
37383
+ const MOBX_REACT_OBSERVER_PACKAGE_NAME = "mobx-react-observer";
37320
37384
  const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
37321
37385
  const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
37322
37386
  const dependencyDeclaration = getDependencyDeclaration({
@@ -37335,7 +37399,26 @@ const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
37335
37399
  if (nextMajor === null) return false;
37336
37400
  return nextMajor < currentMajor;
37337
37401
  };
37338
- const evaluateManifestFacts = (facts, packageJson, directory) => {
37402
+ const shouldReplaceSupportedDependencyFact = (currentFact, nextFact, latestSupportedMajor) => {
37403
+ if (currentFact.version === null) return true;
37404
+ if (nextFact.version === null) return false;
37405
+ const currentMajor = getLowestDependencyMajor(currentFact.version);
37406
+ const nextMajor = getLowestDependencyMajor(nextFact.version);
37407
+ if (currentMajor === null) return false;
37408
+ if (nextMajor === null) return true;
37409
+ if (currentMajor > latestSupportedMajor) return false;
37410
+ if (nextMajor > latestSupportedMajor) return true;
37411
+ return nextMajor < currentMajor;
37412
+ };
37413
+ const shouldReplaceStyledComponentsVersion = (currentVersion, nextVersion) => {
37414
+ if (!currentVersion) return true;
37415
+ const currentMajor = getLowestDependencyMajor(currentVersion);
37416
+ const nextMajor = getLowestDependencyMajor(nextVersion);
37417
+ if (currentMajor === null) return nextMajor !== null && nextMajor < 6;
37418
+ if (nextMajor === null) return currentMajor >= 6;
37419
+ return nextMajor < currentMajor;
37420
+ };
37421
+ const evaluateManifestFacts = (facts, packageJson, directory, rootDirectory, rootPackageJson) => {
37339
37422
  if (facts.expo.version === null) {
37340
37423
  const spec = getDependencySpec(packageJson, "expo");
37341
37424
  if (spec !== null) facts.expo = {
@@ -37357,13 +37440,74 @@ const evaluateManifestFacts = (facts, packageJson, directory) => {
37357
37440
  sourceDirectory: directory
37358
37441
  };
37359
37442
  }
37443
+ const mobxSpec = getDependencySpec(packageJson, "mobx");
37444
+ if (mobxSpec !== null) {
37445
+ const nextMobxFact = {
37446
+ version: resolveCatalogBackedDependencyVersion({
37447
+ rootDirectory: directory,
37448
+ rootPackageJson: packageJson,
37449
+ packageName: "mobx",
37450
+ version: mobxSpec
37451
+ }),
37452
+ sourceDirectory: directory
37453
+ };
37454
+ if (shouldReplaceSupportedDependencyFact(facts.mobx, nextMobxFact, 6)) facts.mobx = nextMobxFact;
37455
+ }
37456
+ facts.hasMobxReact = facts.hasMobxReact || getDependencySpec(packageJson, MOBX_REACT_PACKAGE_NAME) !== null;
37457
+ facts.hasMobxReactLite = facts.hasMobxReactLite || getDependencySpec(packageJson, MOBX_REACT_LITE_PACKAGE_NAME) !== null;
37458
+ facts.hasMobxStateTree = facts.hasMobxStateTree || getDependencySpec(packageJson, MOBX_STATE_TREE_PACKAGE_NAME) !== null;
37459
+ facts.hasMobxReactObserver = facts.hasMobxReactObserver || getDependencySpec(packageJson, MOBX_REACT_OBSERVER_PACKAGE_NAME) !== null;
37460
+ const zustandSpec = getDependencySpec(packageJson, "zustand");
37461
+ if (zustandSpec !== null) {
37462
+ const nextZustandFact = {
37463
+ version: resolveCatalogBackedDependencyVersion({
37464
+ rootDirectory: directory,
37465
+ rootPackageJson: packageJson,
37466
+ packageName: "zustand",
37467
+ version: zustandSpec
37468
+ }),
37469
+ sourceDirectory: directory
37470
+ };
37471
+ if (shouldReplaceSupportedDependencyFact(facts.zustand, nextZustandFact, 5)) facts.zustand = nextZustandFact;
37472
+ }
37360
37473
  if (facts.reanimatedVersion === null) {
37361
37474
  const spec = getDependencySpec(packageJson, REANIMATED_DEPENDENCY_NAME);
37362
37475
  if (spec !== null) facts.reanimatedVersion = spec;
37363
37476
  }
37477
+ if (facts.valtioVersion === null) {
37478
+ const spec = getDependencySpec(packageJson, "valtio");
37479
+ if (spec !== null) facts.valtioVersion = spec;
37480
+ }
37481
+ facts.tanstackQueryVersion ??= getTanStackQueryVersion(packageJson);
37482
+ const styledComponentsVersion = resolveCatalogBackedDependencyVersion({
37483
+ rootDirectory: directory,
37484
+ rootPackageJson: packageJson,
37485
+ packageName: "styled-components",
37486
+ version: getStyledComponentsVersion(packageJson)
37487
+ });
37488
+ if (styledComponentsVersion && shouldReplaceStyledComponentsVersion(facts.styledComponentsVersion, styledComponentsVersion)) facts.styledComponentsVersion = styledComponentsVersion;
37489
+ facts.hasI18nLibrary = facts.hasI18nLibrary || hasI18nDependency(packageJson);
37364
37490
  facts.hasReactNativeAwarePackage = facts.hasReactNativeAwarePackage || isPackageJsonReactNativeAware(packageJson);
37365
37491
  facts.hasReanimatedAwarePackage = facts.hasReanimatedAwarePackage || isPackageJsonReanimatedAware(packageJson);
37366
37492
  facts.hasSsrDependency = facts.hasSsrDependency || isPackageJsonSsrAware(packageJson);
37493
+ const remotionSpec = getDependencySpec(packageJson, "remotion");
37494
+ if (remotionSpec !== null) {
37495
+ facts.hasRemotionDependency = true;
37496
+ const resolvedRemotionVersion = resolveCatalogBackedDependencyVersion({
37497
+ rootDirectory,
37498
+ rootPackageJson,
37499
+ packageName: "remotion",
37500
+ version: remotionSpec
37501
+ });
37502
+ const remotionMajorVersion = resolvedRemotionVersion === null ? null : getLowestDependencyMajor(resolvedRemotionVersion);
37503
+ if (remotionMajorVersion === null) {
37504
+ facts.hasUnknownRemotionVersion = true;
37505
+ facts.remotionVersion = null;
37506
+ } else if (!facts.hasUnknownRemotionVersion) {
37507
+ const currentRemotionMajorVersion = facts.remotionVersion === null ? null : getLowestDependencyMajor(facts.remotionVersion);
37508
+ if (currentRemotionMajorVersion === null || remotionMajorVersion < currentRemotionMajorVersion) facts.remotionVersion = resolvedRemotionVersion;
37509
+ }
37510
+ }
37367
37511
  };
37368
37512
  const collectWorkspaceFacts = (rootDirectory, rootPackageJson, { collectReactGroup }) => {
37369
37513
  const facts = {
@@ -37383,12 +37527,31 @@ const collectWorkspaceFacts = (rootDirectory, rootPackageJson, { collectReactGro
37383
37527
  version: null,
37384
37528
  sourceDirectory: null
37385
37529
  },
37530
+ valtioVersion: null,
37531
+ mobx: {
37532
+ version: null,
37533
+ sourceDirectory: null
37534
+ },
37535
+ hasMobxReact: false,
37536
+ hasMobxReactLite: false,
37537
+ hasMobxStateTree: false,
37538
+ hasMobxReactObserver: false,
37539
+ zustand: {
37540
+ version: null,
37541
+ sourceDirectory: null
37542
+ },
37543
+ tanstackQueryVersion: null,
37544
+ styledComponentsVersion: null,
37545
+ hasI18nLibrary: false,
37386
37546
  hasReactNativeAwarePackage: false,
37387
37547
  hasReanimatedAwarePackage: false,
37388
37548
  hasSsrDependency: false,
37549
+ hasRemotionDependency: false,
37550
+ hasUnknownRemotionVersion: false,
37551
+ remotionVersion: null,
37389
37552
  reanimatedVersion: null
37390
37553
  };
37391
- evaluateManifestFacts(facts, rootPackageJson, rootDirectory);
37554
+ evaluateManifestFacts(facts, rootPackageJson, rootDirectory, rootDirectory, rootPackageJson);
37392
37555
  let isReactGroupSettled = !collectReactGroup;
37393
37556
  const visitedDirectories = /* @__PURE__ */ new Set();
37394
37557
  for (const pattern of getWorkspacePatterns(rootDirectory, rootPackageJson)) {
@@ -37397,7 +37560,7 @@ const collectWorkspaceFacts = (rootDirectory, rootPackageJson, { collectReactGro
37397
37560
  if (visitedDirectories.has(workspaceDirectory)) continue;
37398
37561
  visitedDirectories.add(workspaceDirectory);
37399
37562
  const workspacePackageJson = readPackageJson$1(Path.join(workspaceDirectory, "package.json"));
37400
- evaluateManifestFacts(facts, workspacePackageJson, workspaceDirectory);
37563
+ evaluateManifestFacts(facts, workspacePackageJson, workspaceDirectory, rootDirectory, rootPackageJson);
37401
37564
  const info = extractDependencyInfo(workspacePackageJson);
37402
37565
  if (info.framework !== "unknown" && frameworkMergeRank(info.framework) < frameworkMergeRank(facts.framework)) facts.framework = info.framework;
37403
37566
  if (isReactGroupSettled) continue;
@@ -37618,11 +37781,27 @@ const discoverProjectWithoutPackageJson = (directory) => {
37618
37781
  tailwindVersion: null,
37619
37782
  zodVersion: null,
37620
37783
  zodMajorVersion: null,
37784
+ mobxVersion: null,
37785
+ mobxMajorVersion: null,
37786
+ hasMobxReact: false,
37787
+ hasMobxReactLite: false,
37788
+ hasMobxStateTree: false,
37789
+ hasMobxReactObserver: false,
37790
+ zustandVersion: null,
37791
+ zustandMajorVersion: null,
37621
37792
  framework: "unknown",
37622
37793
  hasTypeScript: hasOwnTsConfig,
37623
37794
  hasReactCompiler: false,
37624
37795
  hasReactCompilerLintPlugin: false,
37625
37796
  hasTanStackQuery: false,
37797
+ valtioVersion: null,
37798
+ valtioMajorVersion: null,
37799
+ hasRemotion: false,
37800
+ remotionVersion: null,
37801
+ remotionMajorVersion: null,
37802
+ hasI18nLibrary: false,
37803
+ tanstackQueryVersion: null,
37804
+ styledComponentsVersion: null,
37626
37805
  hasSsrDependency: false,
37627
37806
  preactVersion: null,
37628
37807
  preactMajorVersion: null,
@@ -37721,6 +37900,12 @@ const discoverProject = (directory) => {
37721
37900
  packageName: SHOPIFY_FLASH_LIST_PACKAGE_NAME,
37722
37901
  version: workspaceFacts.shopifyFlashList.version
37723
37902
  }) : null;
37903
+ const zustandVersion = resolveCatalogBackedDependencyVersion({
37904
+ rootDirectory: directory,
37905
+ rootPackageJson: packageJson,
37906
+ packageName: "zustand",
37907
+ version: workspaceFacts.zustand.version
37908
+ });
37724
37909
  const hasReanimated = hasReactNativeWorkspace && workspaceFacts.hasReanimatedAwarePackage;
37725
37910
  const reanimatedVersion = hasReanimated ? workspaceFacts.reanimatedVersion : null;
37726
37911
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
@@ -37729,7 +37914,21 @@ const discoverProject = (directory) => {
37729
37914
  packageName: "next",
37730
37915
  version: workspaceFacts.next.version
37731
37916
  }) : null;
37917
+ const valtioVersion = resolveCatalogBackedDependencyVersion({
37918
+ rootDirectory: directory,
37919
+ rootPackageJson: packageJson,
37920
+ packageName: "valtio",
37921
+ version: workspaceFacts.valtioVersion
37922
+ });
37923
+ const mobxVersion = resolveCatalogBackedDependencyVersion({
37924
+ rootDirectory: directory,
37925
+ rootPackageJson: packageJson,
37926
+ packageName: "mobx",
37927
+ version: workspaceFacts.mobx.version
37928
+ });
37732
37929
  const preactVersion = getPreactVersion(packageJson);
37930
+ const remotionVersion = workspaceFacts.remotionVersion;
37931
+ const tanstackQueryVersion = getTanStackQueryVersion(packageJson) ?? workspaceFacts.tanstackQueryVersion;
37733
37932
  const isPreES2023Target = hasTypeScript && detectPreES2023Target(directory);
37734
37933
  const projectInfo = {
37735
37934
  rootDirectory: directory,
@@ -37739,11 +37938,27 @@ const discoverProject = (directory) => {
37739
37938
  tailwindVersion,
37740
37939
  zodVersion,
37741
37940
  zodMajorVersion: zodVersion === null ? null : getLowestDependencyMajor(zodVersion),
37941
+ mobxVersion,
37942
+ mobxMajorVersion: mobxVersion === null ? null : getLowestDependencyMajor(mobxVersion),
37943
+ hasMobxReact: workspaceFacts.hasMobxReact,
37944
+ hasMobxReactLite: workspaceFacts.hasMobxReactLite,
37945
+ hasMobxStateTree: workspaceFacts.hasMobxStateTree,
37946
+ hasMobxReactObserver: workspaceFacts.hasMobxReactObserver,
37947
+ zustandVersion,
37948
+ zustandMajorVersion: zustandVersion === null ? null : getLowestDependencyMajor(zustandVersion),
37742
37949
  framework,
37743
37950
  hasTypeScript,
37744
37951
  hasReactCompiler: detectReactCompiler(directory, packageJson),
37745
37952
  hasReactCompilerLintPlugin: detectReactCompilerLintPlugin(directory, packageJson),
37746
- hasTanStackQuery: hasTanStackQuery(packageJson),
37953
+ hasTanStackQuery: tanstackQueryVersion !== null,
37954
+ hasI18nLibrary: workspaceFacts.hasI18nLibrary,
37955
+ tanstackQueryVersion,
37956
+ styledComponentsVersion: workspaceFacts.styledComponentsVersion,
37957
+ valtioVersion,
37958
+ valtioMajorVersion: valtioVersion === null ? null : getLowestDependencyMajor(valtioVersion),
37959
+ hasRemotion: workspaceFacts.hasRemotionDependency,
37960
+ remotionVersion,
37961
+ remotionMajorVersion: remotionVersion === null ? null : getLowestDependencyMajor(remotionVersion),
37747
37962
  hasSsrDependency: workspaceFacts.hasSsrDependency,
37748
37963
  preactVersion,
37749
37964
  preactMajorVersion: parseReactMajor(preactVersion),
@@ -37977,7 +38192,6 @@ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
37977
38192
  "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
37978
38193
  "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
37979
38194
  "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
37980
- "react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
37981
38195
  "react/jsx-no-undef": "react-doctor/jsx-no-undef",
37982
38196
  "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
37983
38197
  "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
@@ -40299,9 +40513,25 @@ const BABEL_CONFIG_FILE_NAMES = [
40299
40513
  ".babelrc.json"
40300
40514
  ];
40301
40515
  const LEGACY_PRESET_SPEC = "module:metro-react-native-babel-preset";
40516
+ const LEGACY_PRESET_PACKAGE = "metro-react-native-babel-preset";
40517
+ const REACT_NATIVE_PACKAGE = "react-native";
40518
+ const PRESET_RENAME_MINIMUM_REACT_NATIVE_VERSION = "0.73.0";
40519
+ const LEGACY_PRESET_REFERENCE = new RegExp(`['"]${LEGACY_PRESET_SPEC}['"]`);
40302
40520
  const MODERN_PRESET_REFERENCE = new RegExp(`['"]module:@react-native/babel-preset['"]`);
40303
40521
  const ENABLE_BABEL_RUNTIME_VERSION = /enableBabelRuntime["']?\s*:\s*['"]/;
40522
+ const isReactNativeVersionAtLeastPresetRename = (rootDirectory, packageJson) => {
40523
+ const installedReactNativeManifestPath = Path.join(rootDirectory, "node_modules", REACT_NATIVE_PACKAGE, "package.json");
40524
+ const reactNativeVersionSpec = (isFile(installedReactNativeManifestPath) ? readPackageJson$1(installedReactNativeManifestPath).version : void 0) ?? getDependencySpec(packageJson, REACT_NATIVE_PACKAGE);
40525
+ if (reactNativeVersionSpec === null || reactNativeVersionSpec === void 0) return false;
40526
+ const normalizedVersionRange = import_semver.validRange(reactNativeVersionSpec);
40527
+ if (normalizedVersionRange === null) return false;
40528
+ const minimumVersion = import_semver.minVersion(normalizedVersionRange);
40529
+ return minimumVersion !== null && import_semver.gte(minimumVersion, PRESET_RENAME_MINIMUM_REACT_NATIVE_VERSION);
40530
+ };
40531
+ const canResolveLegacyPreset = (rootDirectory, packageJson) => getDependencySpec(packageJson, LEGACY_PRESET_PACKAGE) !== null || isFile(Path.join(rootDirectory, "node_modules", LEGACY_PRESET_PACKAGE, "package.json"));
40304
40532
  const checkReactNativeMetroBabelPreset = (rootDirectory) => {
40533
+ const packageJson = readPackageJson$1(Path.join(rootDirectory, "package.json"));
40534
+ const shouldReportLegacyPreset = isReactNativeVersionAtLeastPresetRename(rootDirectory, packageJson) && !canResolveLegacyPreset(rootDirectory, packageJson);
40305
40535
  for (const fileName of BABEL_CONFIG_FILE_NAMES) {
40306
40536
  const filePath = Path.join(rootDirectory, fileName);
40307
40537
  if (!isFile(filePath)) continue;
@@ -40311,7 +40541,7 @@ const checkReactNativeMetroBabelPreset = (rootDirectory) => {
40311
40541
  } catch {
40312
40542
  continue;
40313
40543
  }
40314
- if (contents.includes(LEGACY_PRESET_SPEC)) return [buildReactNativeDiagnostic({
40544
+ if (shouldReportLegacyPreset && LEGACY_PRESET_REFERENCE.test(contents)) return [buildReactNativeDiagnostic({
40315
40545
  filePath: fileName,
40316
40546
  rule: "rn-no-metro-babel-preset",
40317
40547
  severity: "error",
@@ -40334,12 +40564,6 @@ const checkReactNativeProject = (rootDirectory, project) => {
40334
40564
  return [...checkReactNativeMetroBabelPreset(rootDirectory), ...checkReactNativeLibraryDependencies(rootDirectory)];
40335
40565
  };
40336
40566
  const RULE_KEY = "no-vulnerable-react-server-components";
40337
- const DEPENDENCY_SECTIONS = [
40338
- "dependencies",
40339
- "devDependencies",
40340
- "peerDependencies",
40341
- "optionalDependencies"
40342
- ];
40343
40567
  const REACT_RSC_ADVISORIES_BY_MINOR = {
40344
40568
  "19.0": {
40345
40569
  rceFixedVersion: "19.0.1",
@@ -40386,20 +40610,13 @@ const enumerateWorkspaceDirectories = (workspaceRoot) => {
40386
40610
  for (const pattern of patterns) for (const directory of resolveWorkspaceDirectories(workspaceRoot, pattern)) directories.add(directory);
40387
40611
  return [...directories];
40388
40612
  };
40389
- const readDeclaredSpec = (packageJson, packageName) => {
40390
- for (const section of DEPENDENCY_SECTIONS) {
40391
- const spec = packageJson[section]?.[packageName];
40392
- if (typeof spec === "string") return spec;
40393
- }
40394
- return null;
40395
- };
40396
40613
  const resolveVersionInDirectory = (directory, packageName, declaredSpecOverride) => {
40397
40614
  const manifestPath = Path.join(directory, "node_modules", packageName, "package.json");
40398
40615
  if (isFile(manifestPath)) {
40399
40616
  const installedVersion = import_semver.valid(readPackageJson$1(manifestPath).version ?? null);
40400
40617
  if (installedVersion !== null) return installedVersion;
40401
40618
  }
40402
- const candidateSpecs = [readDeclaredSpec(readPackageJson$1(Path.join(directory, "package.json")), packageName), declaredSpecOverride];
40619
+ const candidateSpecs = [getDependencySpec(readPackageJson$1(Path.join(directory, "package.json")), packageName), declaredSpecOverride];
40403
40620
  for (const spec of candidateSpecs) {
40404
40621
  const pinnedVersion = spec === null ? null : import_semver.valid(spec);
40405
40622
  if (pinnedVersion !== null) return pinnedVersion;
@@ -41067,21 +41284,50 @@ const buildCapabilities = (project) => {
41067
41284
  if (CLIENT_ONLY_FRAMEWORKS.has(project.framework)) capabilities.add("client-only");
41068
41285
  if (project.isStaticExport) capabilities.add("nextjs:static-export");
41069
41286
  if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
41287
+ if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 16) capabilities.add("nextjs:16");
41070
41288
  addMajorLadder(capabilities, "react", project.reactMajorVersion, 17, 30);
41071
41289
  if (project.reactMajorVersion !== null && project.reactMajorVersion >= 19 && isMajorMinorAtLeast(parseReactMajorMinor(project.reactVersion), {
41072
41290
  major: 19,
41073
41291
  minor: 2
41074
41292
  })) capabilities.add("react:19.2");
41075
- if (project.tailwindVersion !== null) capabilities.add("tailwind");
41076
- if (project.tailwindVersion !== null && isMajorMinorAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
41077
- major: 3,
41078
- minor: 4
41079
- })) capabilities.add("tailwind:3.4");
41293
+ if (project.tailwindVersion !== null) {
41294
+ capabilities.add("tailwind");
41295
+ const tailwindVersion = parseTailwindMajorMinor(project.tailwindVersion);
41296
+ if (isMajorMinorAtLeast(tailwindVersion, {
41297
+ major: 3,
41298
+ minor: 4
41299
+ })) capabilities.add("tailwind:3.4");
41300
+ if (tailwindVersion !== null && isMajorMinorAtLeast(tailwindVersion, {
41301
+ major: 4,
41302
+ minor: 0
41303
+ })) capabilities.add("tailwind:4");
41304
+ }
41080
41305
  if (project.zodVersion !== null) capabilities.add("zod");
41081
41306
  if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
41307
+ if (project.mobxVersion !== void 0 && project.mobxVersion !== null || project.hasMobxReact === true || project.hasMobxReactLite === true || project.hasMobxStateTree === true || project.hasMobxReactObserver === true) capabilities.add("mobx");
41308
+ if (project.hasMobxReact === true) capabilities.add("mobx-react");
41309
+ if (project.hasMobxReactLite === true) capabilities.add("mobx-react-lite");
41310
+ if (project.hasMobxReact === true || project.hasMobxReactLite === true) capabilities.add("mobx-react-binding");
41311
+ if (project.hasMobxStateTree === true) capabilities.add("mobx-state-tree");
41312
+ if (project.hasMobxReactObserver === true) capabilities.add("mobx-react-observer");
41313
+ if (project.mobxMajorVersion !== void 0 && project.mobxMajorVersion !== null && project.mobxMajorVersion >= 4 && project.mobxMajorVersion <= 6) addMajorLadder(capabilities, "mobx", project.mobxMajorVersion, 4, 6);
41314
+ if (project.zustandVersion !== void 0 && project.zustandVersion !== null) capabilities.add("zustand");
41315
+ if (project.zustandMajorVersion !== void 0 && project.zustandMajorVersion !== null && project.zustandMajorVersion >= 1 && project.zustandMajorVersion <= 5) addMajorLadder(capabilities, "zustand", project.zustandMajorVersion, 1, 5);
41082
41316
  if (project.isPreES2023Target) capabilities.add("pre-es2023");
41083
41317
  if (project.hasReactCompiler) capabilities.add("react-compiler");
41084
- if (project.hasTanStackQuery) capabilities.add("tanstack-query");
41318
+ if (Boolean(project.hasTanStackQuery) || Boolean(project.tanstackQueryVersion)) capabilities.add("tanstack-query");
41319
+ if (project.styledComponentsVersion) {
41320
+ capabilities.add("styled-components");
41321
+ const styledComponentsMajor = getLowestDependencyMajor(project.styledComponentsVersion);
41322
+ if (styledComponentsMajor !== null && styledComponentsMajor >= 6) capabilities.add("styled-components:6");
41323
+ }
41324
+ if (project.hasI18nLibrary) capabilities.add("i18n");
41325
+ if (project.valtioVersion !== null) capabilities.add("valtio");
41326
+ addMajorLadder(capabilities, "valtio", project.valtioMajorVersion, 1, 10);
41327
+ if (project.hasRemotion) {
41328
+ capabilities.add("remotion");
41329
+ addMajorLadder(capabilities, "remotion", project.remotionMajorVersion ?? null, 4, 30);
41330
+ }
41085
41331
  if (project.hasTypeScript) capabilities.add("typescript");
41086
41332
  if (project.preactVersion !== null) capabilities.add("preact");
41087
41333
  addMajorLadder(capabilities, "preact", project.preactMajorVersion, 10, 20);
@@ -41096,7 +41342,7 @@ const getCapabilities = (project) => {
41096
41342
  capabilitiesByProject.set(project, capabilities);
41097
41343
  return capabilities;
41098
41344
  };
41099
- const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhen) => {
41345
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhen, includedTags) => {
41100
41346
  if (requires) {
41101
41347
  for (const capability of requires) if (!capabilities.has(capability)) return false;
41102
41348
  }
@@ -41107,20 +41353,29 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhe
41107
41353
  if (tags) {
41108
41354
  for (const tag of tags) if (ignoredTags.has(tag)) return false;
41109
41355
  }
41356
+ if (includedTags && includedTags.size > 0) {
41357
+ if (!tags?.some((tag) => includedTags.has(tag))) return false;
41358
+ }
41110
41359
  return true;
41111
41360
  };
41361
+ const shouldEnableRuleByDefaultStatus = (options) => options.defaultEnabled !== false || options.hasExplicitOverride === true || options.includeTagDefaults && options.hasIncludedTags;
41112
41362
  const yieldToEventLoop = () => new Promise((resolve) => {
41113
41363
  setImmediate(resolve);
41114
41364
  });
41115
41365
  const createSecurityScanSession = (rootDirectory, options) => {
41116
41366
  const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
41117
41367
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
41368
+ const includedTags = options.includedTags ?? /* @__PURE__ */ new Set();
41118
41369
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
41119
41370
  const rule = entry.rule;
41120
41371
  const scan = rule.scan;
41121
41372
  if (typeof scan !== "function") return [];
41122
- if (rule.defaultEnabled === false) return [];
41123
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) return [];
41373
+ if (!shouldEnableRuleByDefaultStatus({
41374
+ defaultEnabled: rule.defaultEnabled,
41375
+ includeTagDefaults: options.includeTagDefaults === true,
41376
+ hasIncludedTags: includedTags.size > 0
41377
+ })) return [];
41378
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen, includedTags)) return [];
41124
41379
  return [{
41125
41380
  entry,
41126
41381
  scan,
@@ -42334,6 +42589,35 @@ const parseGithubViewerPermission = (stdout) => {
42334
42589
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
42335
42590
  };
42336
42591
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
42592
+ const parseBaselineDiffPlan = (value) => {
42593
+ const entries = splitNullSeparated(value);
42594
+ const baseFiles = /* @__PURE__ */ new Set();
42595
+ const headFiles = /* @__PURE__ */ new Set();
42596
+ for (let entryIndex = 0; entryIndex < entries.length; entryIndex += 2) {
42597
+ const status = entries[entryIndex];
42598
+ const filePath = entries[entryIndex + 1];
42599
+ if (status === void 0 || filePath === void 0 || status.length !== 1) return null;
42600
+ if (status === "A") {
42601
+ headFiles.add(filePath);
42602
+ continue;
42603
+ }
42604
+ if (status === "D") {
42605
+ baseFiles.add(filePath);
42606
+ continue;
42607
+ }
42608
+ if (status === "M" || status === "T") {
42609
+ baseFiles.add(filePath);
42610
+ headFiles.add(filePath);
42611
+ continue;
42612
+ }
42613
+ return null;
42614
+ }
42615
+ return {
42616
+ baseFiles: [...baseFiles],
42617
+ headFiles: [...headFiles],
42618
+ untrackedFiles: []
42619
+ };
42620
+ };
42337
42621
  const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
42338
42622
  /**
42339
42623
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
@@ -42536,6 +42820,45 @@ var Git = class Git extends Service()("react-doctor/Git") {
42536
42820
  githubViewerPermission,
42537
42821
  branchExists,
42538
42822
  mergeBase,
42823
+ baselineDiffPlan: (input) => {
42824
+ if (!isSafeGitRevision(input.ref)) return succeed$2(null);
42825
+ return gen(function* () {
42826
+ const unmerged = yield* runGit(input.directory, [
42827
+ "diff",
42828
+ "--no-ext-diff",
42829
+ "-z",
42830
+ "--name-only",
42831
+ "--diff-filter=U",
42832
+ "--relative"
42833
+ ]);
42834
+ if (unmerged.status !== 0 || unmerged.stdout.length > 0) return null;
42835
+ const result = yield* runGit(input.directory, [
42836
+ "diff",
42837
+ "--no-ext-diff",
42838
+ "--no-textconv",
42839
+ "--no-renames",
42840
+ "-z",
42841
+ "--name-status",
42842
+ "--relative",
42843
+ input.ref
42844
+ ]);
42845
+ if (result.status !== 0) return null;
42846
+ const plan = parseBaselineDiffPlan(result.stdout);
42847
+ if (plan === null) return null;
42848
+ const untracked = yield* runGit(input.directory, [
42849
+ "ls-files",
42850
+ "--others",
42851
+ "--exclude-standard",
42852
+ "-z"
42853
+ ]);
42854
+ if (untracked.status !== 0) return null;
42855
+ return {
42856
+ baseFiles: plan.baseFiles,
42857
+ headFiles: plan.headFiles,
42858
+ untrackedFiles: splitNullSeparated(untracked.stdout)
42859
+ };
42860
+ }).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.baselineDiffPlan"));
42861
+ },
42539
42862
  diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
42540
42863
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
42541
42864
  if (explicitBaseBranch !== void 0) {
@@ -42685,6 +43008,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42685
43008
  githubViewerPermission: () => succeed$2(snapshot.githubViewerPermission ?? null),
42686
43009
  branchExists: (_directory, branch) => succeed$2(snapshot.branchExists?.get(branch) ?? false),
42687
43010
  mergeBase: ({ ref }) => succeed$2(snapshot.mergeBase?.get(ref) ?? null),
43011
+ baselineDiffPlan: () => succeed$2(snapshot.baselineDiffPlan ?? null),
42688
43012
  diffSelection: () => succeed$2(snapshot.diffSelection ?? null),
42689
43013
  stagedFilePaths: () => succeed$2(snapshot.stagedFiles ?? []),
42690
43014
  showStagedContent: (_directory, relativePath) => succeed$2(snapshot.stagedContent?.get(relativePath) ?? null),
@@ -43053,8 +43377,9 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
43053
43377
  }
43054
43378
  return enabled;
43055
43379
  };
43056
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
43057
- const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler || project.hasReactCompilerLintPlugin === true, customRulesOnly);
43380
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), includedTags = /* @__PURE__ */ new Set(), includeTagDefaults = false, serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
43381
+ const hasIncludedTags = includedTags.size > 0;
43382
+ const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" || hasIncludedTags ? null : resolveReactHooksJsPlugin(project.hasReactCompiler || project.hasReactCompilerLintPlugin === true, customRulesOnly);
43058
43383
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
43059
43384
  const jsPlugins = [];
43060
43385
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -43069,9 +43394,14 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
43069
43394
  if (rule.scan !== void 0) continue;
43070
43395
  if (customRulesOnly && registryEntry.originallyExternal) continue;
43071
43396
  if (rule.framework !== "global" && !rule.requires) continue;
43072
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) continue;
43397
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen, includedTags)) continue;
43073
43398
  const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
43074
- if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
43399
+ if (!shouldEnableRuleByDefaultStatus({
43400
+ defaultEnabled: rule.defaultEnabled,
43401
+ includeTagDefaults,
43402
+ hasIncludedTags,
43403
+ hasExplicitOverride: explicitRuleOverride !== void 0
43404
+ })) continue;
43075
43405
  const severity = resolveRuleSeverityOverride({
43076
43406
  ruleKey: registryEntry.key,
43077
43407
  category: rule.category
@@ -43080,7 +43410,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
43080
43410
  enabledReactDoctorRules[registryEntry.key] = severity;
43081
43411
  }
43082
43412
  const userPluginRules = {};
43083
- if (ruleSelection !== "sidecar") for (const userPlugin of userPlugins) {
43413
+ if (ruleSelection !== "sidecar" && !hasIncludedTags) for (const userPlugin of userPlugins) {
43084
43414
  Object.assign(userPluginRules, buildUserPluginRules(userPlugin, severityControls));
43085
43415
  jsPlugins.push(userPlugin.entry);
43086
43416
  }
@@ -43297,6 +43627,19 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
43297
43627
  }
43298
43628
  };
43299
43629
  };
43630
+ const fingerprintCacheByFilePath = /* @__PURE__ */ new Map();
43631
+ const fingerprintFileContents = (filePath, fingerprintLength) => {
43632
+ const fileStats = NFS.statSync(filePath, { bigint: true });
43633
+ const cachedFingerprint = fingerprintCacheByFilePath.get(filePath);
43634
+ if (cachedFingerprint?.modifiedTimeNanoseconds === fileStats.mtimeNs && cachedFingerprint.sizeBytes === fileStats.size) return cachedFingerprint.fingerprint;
43635
+ const fingerprint = crypto.createHash("sha256").update(NFS.readFileSync(filePath)).digest("hex").slice(0, fingerprintLength);
43636
+ fingerprintCacheByFilePath.set(filePath, {
43637
+ fingerprint,
43638
+ modifiedTimeNanoseconds: fileStats.mtimeNs,
43639
+ sizeBytes: fileStats.size
43640
+ });
43641
+ return fingerprint;
43642
+ };
43300
43643
  const bundledRequire$3 = createRequire(import.meta.url);
43301
43644
  const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
43302
43645
  const resolveChildNodeVersion = (nodeBinaryPath) => {
@@ -43320,6 +43663,13 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS$1 = [
43320
43663
  "oxlint-plugin-react-doctor/package.json",
43321
43664
  "eslint-plugin-react-hooks/package.json"
43322
43665
  ];
43666
+ const resolvePluginFingerprint = () => {
43667
+ try {
43668
+ return fingerprintFileContents(bundledRequire$3.resolve("oxlint-plugin-react-doctor"), 16);
43669
+ } catch {
43670
+ return "unresolved";
43671
+ }
43672
+ };
43323
43673
  const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
43324
43674
  const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
43325
43675
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS$1) try {
@@ -43329,6 +43679,7 @@ const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
43329
43679
  } catch {
43330
43680
  versions.push(`${specifier}=missing`);
43331
43681
  }
43682
+ versions.push(`oxlint-plugin-react-doctor#fingerprint=${resolvePluginFingerprint()}`);
43332
43683
  return versions;
43333
43684
  };
43334
43685
  const esmRequire = createRequire(import.meta.url);
@@ -44022,12 +44373,17 @@ const REACT_ERROR_BOUNDARY_MESSAGE = "This try/catch cannot catch errors thrown
44022
44373
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
44023
44374
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
44024
44375
  const REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION = "It's how the library works, not a bug in your code. Memoize values you pass from it into other memoized components, or suppress it with `// react-doctor-disable-next-line react-hooks-js/incompatible-library`.";
44376
+ const SET_STATE_IN_EFFECT_ACTION = "Prefer deriving or initializing the value before render. If the effect must read a browser API after mount, treat this as advisory or suppress it with `// react-doctor-disable-next-line react-hooks-js/set-state-in-effect`.";
44025
44377
  const REACT_COMPILER_GENERIC_MESSAGE = `${REACT_COMPILER_IMPACT}. ${REACT_COMPILER_ACTION}`;
44026
44378
  const buildReactCompilerMessage = (reasonSummary, action = REACT_COMPILER_ACTION) => {
44027
44379
  const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
44028
44380
  if (!normalizedSummary) return `${REACT_COMPILER_IMPACT}. ${action}`;
44029
44381
  return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${action}`;
44030
44382
  };
44383
+ const buildSetStateInEffectMessage = (reasonSummary) => {
44384
+ const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
44385
+ return `This synchronous effect update causes an extra render${normalizedSummary ? `: ${normalizedSummary}` : ""}. ${SET_STATE_IN_EFFECT_ACTION}`;
44386
+ };
44031
44387
  const PLUGIN_CATEGORY_MAP = {
44032
44388
  react: "Bugs",
44033
44389
  "react-hooks": "Bugs",
@@ -44085,6 +44441,10 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
44085
44441
  message: REACT_ERROR_BOUNDARY_MESSAGE,
44086
44442
  help: reasonDetail || help
44087
44443
  };
44444
+ if (rule === "set-state-in-effect") return {
44445
+ message: buildSetStateInEffectMessage(reasonSummary.trim()),
44446
+ help: reasonDetail || help
44447
+ };
44088
44448
  return {
44089
44449
  message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
44090
44450
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
@@ -44795,7 +45155,7 @@ const reactHooksJsPluginDropNote = (error) => {
44795
45155
  * 6. always restore disable directives + clean up the temp dir
44796
45156
  */
44797
45157
  const runOxlint = async (options) => {
44798
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, onFileCoverage, perFileLintCacheEnabled = false, sidecarLintCacheEnabled = false, onCacheStats, onSidecarStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
45158
+ const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), includedTags = /* @__PURE__ */ new Set(), includeTagDefaults = false, userConfig, configSourceDirectory = rootDirectory, onPartialFailure, onFileCoverage, perFileLintCacheEnabled = false, sidecarLintCacheEnabled = false, onCacheStats, onSidecarStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
44799
45159
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
44800
45160
  const severityControls = buildRuleSeverityControls(userConfig);
44801
45161
  validateRuleRegistration();
@@ -44809,13 +45169,15 @@ const runOxlint = async (options) => {
44809
45169
  }
44810
45170
  const pluginPath = resolvePluginPath();
44811
45171
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
44812
- const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
45172
+ const userPlugins = includedTags.size > 0 ? [] : resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
44813
45173
  const buildConfig = (overrides) => createOxlintConfig({
44814
45174
  pluginPath,
44815
45175
  project,
44816
45176
  customRulesOnly,
44817
45177
  extendsPaths: overrides.extendsPaths,
44818
45178
  ignoredTags,
45179
+ includedTags,
45180
+ includeTagDefaults,
44819
45181
  serverAuthFunctionNames,
44820
45182
  severityControls,
44821
45183
  userPlugins,
@@ -45204,6 +45566,8 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
45204
45566
  respectInlineDisables: input.respectInlineDisables,
45205
45567
  adoptExistingLintConfig: input.adoptExistingLintConfig,
45206
45568
  ignoredTags: input.ignoredTags,
45569
+ includedTags: input.includedTags,
45570
+ includeTagDefaults: input.includeTagDefaults,
45207
45571
  userConfig: input.userConfig ?? null,
45208
45572
  configSourceDirectory: input.configSourceDirectory,
45209
45573
  onPartialFailure: (reason) => {
@@ -46052,7 +46416,9 @@ const runInspect = (input, hooks = {}) => gen(function* () {
46052
46416
  const securityScanFailedRef = yield* make$13(false);
46053
46417
  const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
46054
46418
  project,
46055
- ignoredTags: input.ignoredTags
46419
+ ignoredTags: input.ignoredTags,
46420
+ includedTags: input.includedTags,
46421
+ includeTagDefaults: input.includeTagDefaults
46056
46422
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
46057
46423
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
46058
46424
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
@@ -46143,6 +46509,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
46143
46509
  respectInlineDisables: input.respectInlineDisables,
46144
46510
  adoptExistingLintConfig: input.adoptExistingLintConfig,
46145
46511
  ignoredTags: input.ignoredTags,
46512
+ includedTags: input.includedTags,
46513
+ includeTagDefaults: input.includeTagDefaults,
46146
46514
  userConfig: resolvedConfig.config ?? void 0,
46147
46515
  configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
46148
46516
  onFileProgress: (scannedFileCount, totalFileCount) => {
@@ -46421,12 +46789,19 @@ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
46421
46789
  */
46422
46790
  const materializeSourceTree = (input) => gen(function* () {
46423
46791
  const materializedFiles = [];
46792
+ const unmaterializedFiles = [];
46424
46793
  const resolvedTempDirectory = path.resolve(input.tempDirectory);
46425
46794
  for (const relativePath of input.files) {
46426
46795
  const content = yield* input.readContent(relativePath).pipe(orElseSucceed(() => null));
46427
- if (content === null) continue;
46796
+ if (content === null) {
46797
+ unmaterializedFiles.push(relativePath);
46798
+ continue;
46799
+ }
46428
46800
  const candidateTargetPath = path.resolve(resolvedTempDirectory, relativePath);
46429
- if (!isPathInsideDirectory(candidateTargetPath, resolvedTempDirectory)) continue;
46801
+ if (!isPathInsideDirectory(candidateTargetPath, resolvedTempDirectory)) {
46802
+ unmaterializedFiles.push(relativePath);
46803
+ continue;
46804
+ }
46430
46805
  yield* sync(() => {
46431
46806
  fs.mkdirSync(path.dirname(candidateTargetPath), { recursive: true });
46432
46807
  fs.writeFileSync(candidateTargetPath, content);
@@ -46443,6 +46818,7 @@ const materializeSourceTree = (input) => gen(function* () {
46443
46818
  return {
46444
46819
  tempDirectory: input.tempDirectory,
46445
46820
  materializedFiles,
46821
+ unmaterializedFiles,
46446
46822
  cleanup: () => {
46447
46823
  try {
46448
46824
  fs.rmSync(input.tempDirectory, {
@@ -46712,54 +47088,80 @@ const buildSkippedChecks = (input) => {
46712
47088
  skippedCheckReasons
46713
47089
  };
46714
47090
  };
46715
- const fingerprintDiagnostic = (diagnostic, lineText) => {
47091
+ const fingerprintText = (text) => createHash("sha256").update(text).digest("hex");
47092
+ const normalizeEvidence = (evidence) => evidence.replace(/\s+/g, " ").trim();
47093
+ const getDiagnosticMatchKeys = (diagnostic, evidence) => {
46716
47094
  const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;
46717
- const snippet = lineText === null || diagnostic.matchByOccurrence ? "" : createHash("sha1").update(lineText.trim()).digest("hex");
46718
- return `${diagnostic.filePath}\u0000${ruleKey}\u0000${snippet}`;
46719
- };
46720
- /**
46721
- * Diffs a head scan against a base scan to isolate the diagnostics a change
46722
- * introduced (and count the ones it resolved). Matching is a multiset over a
46723
- * position-independent fingerprint — `(filePath, plugin/rule, hash(flagged
46724
- * line text))` — so inserting lines above an existing issue doesn't make it
46725
- * look new, while a genuinely new occurrence (new line text, or one more of
46726
- * the same) surfaces. Identical repeated findings are matched by count.
46727
- *
46728
- * Diagnostics carrying `matchByOccurrence` (resolved at diagnostic creation:
46729
- * every Accessibility-category finding, plus rules opting in via their
46730
- * `matchByOccurrence` metadata flag) drop the line-text snippet and match by
46731
- * `(filePath, plugin/rule)` occurrence count alone. Their identity is the
46732
- * flagged element, not the line's text, so editing the line (reindentation,
46733
- * prettier reflow, collapsing a multi-line JSX element) doesn't reclassify a
46734
- * pre-existing finding as new — while one MORE occurrence of the same rule in
46735
- * the file still surfaces. Expression-level rules keep the line-text snippet:
46736
- * there the flagged expression IS the finding, so changed text means new +
46737
- * fixed.
46738
- *
46739
- * v1 limitation: the fingerprint keys on the head-relative `filePath`, and base
46740
- * content is read at that same path. A file renamed by the change therefore has
46741
- * no base match, so its pre-existing findings are reported as new. This
46742
- * over-reports (never hides a real issue) and is rare; rename-aware base
46743
- * resolution is a follow-up.
47095
+ const messageFingerprint = fingerprintText(`${diagnostic.title ?? ""}\0${diagnostic.message}`);
47096
+ const normalizedEvidence = evidence === null ? "" : normalizeEvidence(evidence);
47097
+ const stableEvidenceKey = normalizedEvidence.length > 0 ? `evidence\0${ruleKey}\0${messageFingerprint}\0${fingerprintText(normalizedEvidence)}` : null;
47098
+ return {
47099
+ stableEvidenceKey,
47100
+ sameFileStableEvidenceKey: stableEvidenceKey === null ? null : `${diagnostic.filePath}\0${stableEvidenceKey}`,
47101
+ sameFileFallbackKey: diagnostic.matchByOccurrence || normalizedEvidence.length === 0 ? `fallback\0${diagnostic.filePath}\0${ruleKey}\0${messageFingerprint}` : null
47102
+ };
47103
+ };
47104
+ const addDiagnosticIndex = (buckets, key, diagnosticIndex) => {
47105
+ if (key === null) return;
47106
+ const diagnosticIndexes = buckets.get(key) ?? [];
47107
+ diagnosticIndexes.push(diagnosticIndex);
47108
+ buckets.set(key, diagnosticIndexes);
47109
+ };
47110
+ const takeMatchingDiagnosticIndex = (buckets, key, matchedDiagnosticIndexes) => {
47111
+ if (key === null) return null;
47112
+ for (const diagnosticIndex of buckets.get(key) ?? []) if (!matchedDiagnosticIndexes.has(diagnosticIndex)) return diagnosticIndex;
47113
+ return null;
47114
+ };
47115
+ const readDiagnosticEvidence = (diagnostic, readEvidence, readLine) => readEvidence?.(diagnostic) ?? readLine(diagnostic.filePath, diagnostic.line);
47116
+ const buildMatchCandidates = (diagnostics, readEvidence, readLine) => diagnostics.map((diagnostic, diagnosticIndex) => ({
47117
+ diagnosticIndex,
47118
+ ...getDiagnosticMatchKeys(diagnostic, readDiagnosticEvidence(diagnostic, readEvidence, readLine))
47119
+ }));
47120
+ /**
47121
+ * Diffs a head scan against a base scan using a multiset of construct-level
47122
+ * evidence. Stable identities combine plugin/rule, the diagnostic message,
47123
+ * and normalized diagnosed source, so unchanged findings can move across
47124
+ * files while changed constructs or messages remain new. Cardinality is
47125
+ * retained for identical findings. Diagnostics explicitly marked
47126
+ * `matchByOccurrence` may fall back to same-file plugin/rule/message matching
47127
+ * after same-file strict evidence matching. Cross-file evidence matching runs
47128
+ * last so a copy cannot consume a reformatted local occurrence. Unreadable
47129
+ * evidence uses the same conservative fallback rather than matching across
47130
+ * files without proof.
46744
47131
  */
46745
47132
  const computeDiagnosticDelta = (input) => {
46746
- const unmatchedBaseByFingerprint = /* @__PURE__ */ new Map();
46747
- for (const diagnostic of input.baseDiagnostics) {
46748
- const key = fingerprintDiagnostic(diagnostic, input.readBaseLine(diagnostic.filePath, diagnostic.line));
46749
- unmatchedBaseByFingerprint.set(key, (unmatchedBaseByFingerprint.get(key) ?? 0) + 1);
46750
- }
46751
- const newDiagnostics = [];
46752
- for (const diagnostic of input.headDiagnostics) {
46753
- const key = fingerprintDiagnostic(diagnostic, input.readHeadLine(diagnostic.filePath, diagnostic.line));
46754
- const availableMatches = unmatchedBaseByFingerprint.get(key) ?? 0;
46755
- if (availableMatches > 0) unmatchedBaseByFingerprint.set(key, availableMatches - 1);
46756
- else newDiagnostics.push(diagnostic);
46757
- }
46758
- let fixedCount = 0;
46759
- for (const remaining of unmatchedBaseByFingerprint.values()) fixedCount += remaining;
47133
+ const baseCandidates = buildMatchCandidates(input.baseDiagnostics, input.readBaseEvidence, input.readBaseLine);
47134
+ const headCandidates = buildMatchCandidates(input.headDiagnostics, input.readHeadEvidence, input.readHeadLine);
47135
+ const baseByStableEvidence = /* @__PURE__ */ new Map();
47136
+ const baseBySameFileStableEvidence = /* @__PURE__ */ new Map();
47137
+ const baseBySameFileFallback = /* @__PURE__ */ new Map();
47138
+ for (const candidate of baseCandidates) {
47139
+ addDiagnosticIndex(baseByStableEvidence, candidate.stableEvidenceKey, candidate.diagnosticIndex);
47140
+ addDiagnosticIndex(baseBySameFileStableEvidence, candidate.sameFileStableEvidenceKey, candidate.diagnosticIndex);
47141
+ addDiagnosticIndex(baseBySameFileFallback, candidate.sameFileFallbackKey, candidate.diagnosticIndex);
47142
+ }
47143
+ const matchedHeadDiagnosticIndexes = /* @__PURE__ */ new Set();
47144
+ const matchedBaseDiagnosticIndexes = /* @__PURE__ */ new Set();
47145
+ const matchCandidates = (baseBuckets, getKey, onMatch) => {
47146
+ for (const candidate of headCandidates) {
47147
+ if (matchedHeadDiagnosticIndexes.has(candidate.diagnosticIndex)) continue;
47148
+ const baseDiagnosticIndex = takeMatchingDiagnosticIndex(baseBuckets, getKey(candidate), matchedBaseDiagnosticIndexes);
47149
+ if (baseDiagnosticIndex === null) continue;
47150
+ matchedHeadDiagnosticIndexes.add(candidate.diagnosticIndex);
47151
+ matchedBaseDiagnosticIndexes.add(baseDiagnosticIndex);
47152
+ onMatch?.(candidate.diagnosticIndex, baseDiagnosticIndex);
47153
+ }
47154
+ };
47155
+ matchCandidates(baseBySameFileStableEvidence, (candidate) => candidate.sameFileStableEvidenceKey);
47156
+ matchCandidates(baseBySameFileFallback, (candidate) => candidate.sameFileFallbackKey);
47157
+ let crossFileMatchCount = 0;
47158
+ matchCandidates(baseByStableEvidence, (candidate) => candidate.stableEvidenceKey, (head, base) => {
47159
+ if (input.headDiagnostics[head]?.filePath !== input.baseDiagnostics[base]?.filePath) crossFileMatchCount += 1;
47160
+ });
46760
47161
  return {
46761
- newDiagnostics,
46762
- fixedCount
47162
+ newDiagnostics: input.headDiagnostics.filter((_diagnostic, diagnosticIndex) => !matchedHeadDiagnosticIndexes.has(diagnosticIndex)),
47163
+ fixedCount: input.baseDiagnostics.length - matchedBaseDiagnosticIndexes.size,
47164
+ crossFileMatchCount
46763
47165
  };
46764
47166
  };
46765
47167
  /**
@@ -46791,6 +47193,12 @@ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runProm
46791
47193
  };
46792
47194
  }).pipe(provide(Git.layerNode)));
46793
47195
  const filterSourceFiles = (filePaths) => filePaths.filter(isLintableSourceFile);
47196
+ const getBaselineDiffPlan = (directory, ref) => runPromise(gen(function* () {
47197
+ return yield* (yield* Git).baselineDiffPlan({
47198
+ directory,
47199
+ ref
47200
+ });
47201
+ }).pipe(provide(Git.layerNode)));
46794
47202
  /**
46795
47203
  * Programmatic façade over `Git.changedLineRanges` (the `lines` scope). Diffs
46796
47204
  * `files` with `--unified=0` against `baseRef` (or the index when `cached`),
@@ -47247,7 +47655,7 @@ const makeNoopConsole = () => ({
47247
47655
  });
47248
47656
  //#endregion
47249
47657
  //#region src/cli/utils/version.ts
47250
- const VERSION = "0.8.0-dev.eb2d687";
47658
+ const VERSION = "0.8.1-dev.0b0b5ac";
47251
47659
  //#endregion
47252
47660
  //#region src/cli/utils/json-mode.ts
47253
47661
  let context = null;
@@ -47334,7 +47742,11 @@ const getRunId = () => {
47334
47742
  };
47335
47743
  //#endregion
47336
47744
  //#region src/cli/utils/build-run-context.ts
47337
- const ROOT_SUBCOMMANDS = new Set(["install", "setup"]);
47745
+ const ROOT_SUBCOMMANDS = new Set([
47746
+ "design",
47747
+ "install",
47748
+ "setup"
47749
+ ]);
47338
47750
  const detectInvokedVia = () => {
47339
47751
  const userAgent = process.env.npm_config_user_agent;
47340
47752
  if (!userAgent) return "unknown";
@@ -47418,7 +47830,9 @@ const buildSentryProjectContext = (projectInfo) => ({
47418
47830
  reactMajorVersion: projectInfo.reactMajorVersion,
47419
47831
  hasTypeScript: projectInfo.hasTypeScript,
47420
47832
  hasReactCompiler: projectInfo.hasReactCompiler,
47421
- hasTanStackQuery: projectInfo.hasTanStackQuery,
47833
+ tanstackQueryVersion: projectInfo.tanstackQueryVersion,
47834
+ mobxVersion: projectInfo.mobxVersion,
47835
+ styledComponentsVersion: projectInfo.styledComponentsVersion,
47422
47836
  tailwindVersion: projectInfo.tailwindVersion,
47423
47837
  zodVersion: projectInfo.zodVersion,
47424
47838
  preactVersion: projectInfo.preactVersion,
@@ -47599,13 +48013,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
47599
48013
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
47600
48014
  * standard `SENTRY_RELEASE` override.
47601
48015
  */
47602
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.0-dev.eb2d687`;
48016
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.1-dev.0b0b5ac`;
47603
48017
  /**
47604
48018
  * Deployment environment shown in Sentry's environment filter. Defaults to
47605
48019
  * `production` for tagged releases and `development` for dev/unbuilt versions,
47606
48020
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
47607
48021
  */
47608
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.0-dev.eb2d687") ? "development" : "production");
48022
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.1-dev.0b0b5ac") ? "development" : "production");
47609
48023
  /**
47610
48024
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
47611
48025
  * (set to `0` to disable tracing) and falls back to
@@ -59931,6 +60345,8 @@ const buildScanResultCacheKey = (input) => {
59931
60345
  warnings: input.options.warnings,
59932
60346
  adoptExistingLintConfig: input.options.adoptExistingLintConfig,
59933
60347
  ignoredTags: [...input.options.ignoredTags].sort(),
60348
+ includedTags: [...input.options.includedTags].sort(),
60349
+ includeTagDefaults: input.options.includeTagDefaults,
59934
60350
  concurrency: input.options.concurrency,
59935
60351
  lintBatchOrdering: resolveLintBatchOrdering(),
59936
60352
  baselineRef: input.options.baseline?.ref,
@@ -60228,6 +60644,7 @@ const buildOutcomeAttributes = (input) => {
60228
60644
  new: summary.totalDiagnosticCount,
60229
60645
  fixed: result.baselineDelta.fixedCount,
60230
60646
  baseTotal: result.baselineDelta.baseTotalCount,
60647
+ crossFileMatches: result.baselineDelta.crossFileMatchCount ?? null,
60231
60648
  degraded: false
60232
60649
  }));
60233
60650
  else if (input.gateExempt) attributes["baseline.degraded"] = true;
@@ -60341,28 +60758,49 @@ const diagnosticIntersectsLineRanges = (diagnostic, lineRanges) => {
60341
60758
  return lineRanges.some(([rangeStartLine, rangeEndLine]) => diagnostic.line <= rangeEndLine && diagnosticEndLine >= rangeStartLine);
60342
60759
  };
60343
60760
  //#endregion
60761
+ //#region src/cli/utils/is-materializable-git-source.ts
60762
+ const GIT_LFS_POINTER_HEADER = "version https://git-lfs.github.com/spec/v1";
60763
+ const isMaterializableGitSource = (content) => !content.includes("\0") && !content.startsWith(GIT_LFS_POINTER_HEADER);
60764
+ //#endregion
60344
60765
  //#region src/cli/utils/materialize-baseline-files.ts
60345
60766
  /**
60346
- * Materializes the `ref` version of `files` into `tempDirectory` (via
60347
- * `git show <ref>:<path>`), mirroring the project layout + head's config files
60348
- * so the base lints under the same rules as head. Reuses the shared,
60349
- * zip-slip-guarded `materializeSourceTree`. Per-file read misses (e.g. a file
60350
- * the PR added, absent at base) are skipped, which is exactly what we want:
60351
- * a brand-new file has no base counterpart, so all its findings are "new".
60767
+ * Materializes the base side of the `ref` → worktree diff into
60768
+ * `tempDirectory`, mirroring the project layout plus head's config files so
60769
+ * both sides lint under the same rules. Git rename heuristics are disabled by
60770
+ * the diff planner, so an old path is retained as a base deletion while its
60771
+ * new path is a head addition. Missing head-only additions are expected;
60772
+ * missing paths that the plan says must exist at base make `isComplete` false.
60352
60773
  */
60353
60774
  const materializeBaselineFiles = (input) => runPromise(gen(function* () {
60354
60775
  const git = yield* Git;
60355
- return yield* materializeSourceTree({
60776
+ const diffPlan = yield* git.baselineDiffPlan({
60356
60777
  directory: input.directory,
60357
- files: input.files,
60778
+ ref: input.ref
60779
+ });
60780
+ const baseFiles = input.baseFiles ?? diffPlan?.baseFiles;
60781
+ const headFiles = input.headFiles ?? diffPlan?.headFiles;
60782
+ const untrackedFiles = diffPlan?.untrackedFiles ?? [];
60783
+ if (baseFiles === void 0 || headFiles === void 0) return null;
60784
+ const files = [...new Set([...input.files, ...baseFiles])];
60785
+ const tree = yield* materializeSourceTree({
60786
+ directory: input.directory,
60787
+ files,
60358
60788
  tempDirectory: input.tempDirectory,
60359
60789
  readContent: (relativePath) => git.showRefContent({
60360
60790
  directory: input.directory,
60361
60791
  ref: input.ref,
60362
60792
  relativePath,
60363
60793
  options: { maxBufferBytes: GIT_SHOW_MAX_BUFFER_BYTES }
60364
- })
60794
+ }).pipe(map$3((content) => content !== null && isMaterializableGitSource(content) ? content : null))
60365
60795
  });
60796
+ const materializedFiles = new Set(tree.materializedFiles);
60797
+ return {
60798
+ ...tree,
60799
+ baseFiles,
60800
+ headFiles,
60801
+ isComplete: baseFiles.every((filePath) => materializedFiles.has(filePath)),
60802
+ untrackedFiles
60803
+ };
60366
60804
  }).pipe(provide(Git.layerNode)));
60367
60805
  /**
60368
60806
  * Resolves the commit a baseline scan should read base content from — the
@@ -60385,8 +60823,8 @@ const resolveMergeBaseRef = async (directory, ref) => {
60385
60823
  //#region src/cli/utils/read-source-line.ts
60386
60824
  /**
60387
60825
  * Builds a cached `(filePath, line) -> source text` reader rooted at
60388
- * `rootDirectory`, used to fingerprint a diagnostic by the content of its
60389
- * flagged line (see `computeDiagnosticDelta`). Wraps core's
60826
+ * `rootDirectory`, used as the fallback when construct-level diagnostic
60827
+ * evidence cannot be read. Wraps core's
60390
60828
  * `createNodeReadFileLinesSync` with per-file memoization + 1-indexed line
60391
60829
  * lookup; unreadable files / out-of-range lines return `null`.
60392
60830
  */
@@ -60401,10 +60839,342 @@ const createSourceLineReader = (rootDirectory) => {
60401
60839
  };
60402
60840
  };
60403
60841
  //#endregion
60842
+ //#region src/cli/utils/read-diagnostic-evidence.ts
60843
+ const SOURCE_EXTENSIONS = [
60844
+ ".ts",
60845
+ ".tsx",
60846
+ ".js",
60847
+ ".jsx",
60848
+ ".mts",
60849
+ ".cts",
60850
+ ".mjs",
60851
+ ".cjs"
60852
+ ];
60853
+ const COMPONENT_WRAPPER_NAMES = new Set(["memo", "forwardRef"]);
60854
+ const getScriptKind = (filePath) => {
60855
+ const extension = path.extname(filePath).toLowerCase();
60856
+ if (extension === ".tsx") return ts.ScriptKind.TSX;
60857
+ if (extension === ".jsx") return ts.ScriptKind.JSX;
60858
+ if (extension === ".js" || extension === ".mjs" || extension === ".cjs") return ts.ScriptKind.JS;
60859
+ return ts.ScriptKind.TS;
60860
+ };
60861
+ const resolveSafeSourcePath = (rootDirectory, filePath) => {
60862
+ try {
60863
+ const resolvedRootDirectory = fs.realpathSync(rootDirectory);
60864
+ const absolutePath = path.resolve(resolvedRootDirectory, filePath);
60865
+ const relativePath = path.relative(resolvedRootDirectory, absolutePath);
60866
+ if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) return null;
60867
+ const stats = fs.lstatSync(absolutePath);
60868
+ if (!stats.isFile() || stats.isSymbolicLink()) return null;
60869
+ const realPath = fs.realpathSync(absolutePath);
60870
+ const realRelativePath = path.relative(resolvedRootDirectory, realPath);
60871
+ return realRelativePath !== ".." && !realRelativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(realRelativePath) ? realPath : null;
60872
+ } catch {
60873
+ return null;
60874
+ }
60875
+ };
60876
+ const readSourceRecord = (rootDirectory, filePath, state) => {
60877
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
60878
+ if (state.recordByFilePath.has(normalizedFilePath)) return state.recordByFilePath.get(normalizedFilePath) ?? null;
60879
+ const absolutePath = resolveSafeSourcePath(rootDirectory, normalizedFilePath);
60880
+ if (absolutePath === null) {
60881
+ state.recordByFilePath.set(normalizedFilePath, null);
60882
+ return null;
60883
+ }
60884
+ try {
60885
+ const sourceText = fs.readFileSync(absolutePath, "utf-8");
60886
+ const record = {
60887
+ absolutePath,
60888
+ filePath: normalizedFilePath,
60889
+ sourceFile: ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
60890
+ sourceText
60891
+ };
60892
+ state.recordByFilePath.set(normalizedFilePath, record);
60893
+ return record;
60894
+ } catch {
60895
+ state.recordByFilePath.set(normalizedFilePath, null);
60896
+ return null;
60897
+ }
60898
+ };
60899
+ const listSourceRecords = (rootDirectory, state) => {
60900
+ if (state.sourceRecords !== null) return state.sourceRecords;
60901
+ state.sourceRecords = listSourceFiles(rootDirectory).flatMap((filePath) => {
60902
+ const record = readSourceRecord(rootDirectory, filePath, state);
60903
+ return record === null ? [] : [record];
60904
+ });
60905
+ return state.sourceRecords;
60906
+ };
60907
+ const unwrapExpression = (expression) => {
60908
+ let currentExpression = expression;
60909
+ while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression)) currentExpression = currentExpression.expression;
60910
+ return currentExpression;
60911
+ };
60912
+ const getCalleeName = (expression) => {
60913
+ const unwrappedExpression = unwrapExpression(expression);
60914
+ if (ts.isIdentifier(unwrappedExpression)) return unwrappedExpression.text;
60915
+ if (ts.isPropertyAccessExpression(unwrappedExpression)) return unwrappedExpression.name.text;
60916
+ return null;
60917
+ };
60918
+ const isComponentWrapperCall = (node) => {
60919
+ const wrapperName = getCalleeName(node.expression);
60920
+ return wrapperName !== null && COMPONENT_WRAPPER_NAMES.has(wrapperName);
60921
+ };
60922
+ const getFunctionName = (node) => {
60923
+ if (ts.isFunctionDeclaration(node) && node.name !== void 0) return node.name.text;
60924
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
60925
+ let componentExpression = node;
60926
+ while (true) {
60927
+ const parent = componentExpression.parent;
60928
+ if (ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent) || ts.isTypeAssertionExpression(parent) || ts.isNonNullExpression(parent) || ts.isSatisfiesExpression(parent)) {
60929
+ componentExpression = parent;
60930
+ continue;
60931
+ }
60932
+ if (ts.isCallExpression(parent) && parent.arguments[0] === componentExpression && isComponentWrapperCall(parent)) {
60933
+ componentExpression = parent;
60934
+ continue;
60935
+ }
60936
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) return parent.name.text;
60937
+ return node.name?.text ?? null;
60938
+ }
60939
+ }
60940
+ return null;
60941
+ };
60942
+ const isComponentFunction = (node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
60943
+ const isDefaultExportedComponent = (record, node, componentName) => {
60944
+ if (ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword)) return true;
60945
+ return record.sourceFile.statements.some((statement) => ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression) && statement.expression.text === componentName);
60946
+ };
60947
+ const findEnclosingComponent = (record, diagnostic) => {
60948
+ if (!Number.isInteger(diagnostic.line) || diagnostic.line < 1) return null;
60949
+ const lineIndex = diagnostic.line - 1;
60950
+ const lineStarts = record.sourceFile.getLineStarts();
60951
+ const lineStart = lineStarts[lineIndex];
60952
+ if (lineStart === void 0) return null;
60953
+ const nextLineStart = lineStarts[lineIndex + 1] ?? record.sourceFile.end;
60954
+ const lineEnd = Math.max(lineStart, nextLineStart - 1);
60955
+ const columnIndex = Number.isInteger(diagnostic.column) ? Math.max(0, diagnostic.column - 1) : 0;
60956
+ const diagnosticPosition = Math.min(lineStart + columnIndex, lineEnd);
60957
+ let component = null;
60958
+ let componentWidth = Number.POSITIVE_INFINITY;
60959
+ const visit = (node) => {
60960
+ if (diagnosticPosition < node.getStart(record.sourceFile) || diagnosticPosition > node.end) return;
60961
+ if (isComponentFunction(node)) {
60962
+ const name = getFunctionName(node);
60963
+ const width = node.end - node.getStart(record.sourceFile);
60964
+ if (name !== null && /^[A-Z]/.test(name) && width < componentWidth) {
60965
+ component = {
60966
+ isDefaultExport: isDefaultExportedComponent(record, node, name),
60967
+ name,
60968
+ node,
60969
+ record
60970
+ };
60971
+ componentWidth = width;
60972
+ }
60973
+ }
60974
+ ts.forEachChild(node, visit);
60975
+ };
60976
+ visit(record.sourceFile);
60977
+ return component;
60978
+ };
60979
+ const moduleSpecifierTargetsComponent = (callerRecord, moduleSpecifier, componentRecord) => {
60980
+ if (!moduleSpecifier.startsWith(".")) return false;
60981
+ const unresolvedPath = path.resolve(path.dirname(callerRecord.absolutePath), moduleSpecifier);
60982
+ const specifiedExtension = path.extname(unresolvedPath).toLowerCase();
60983
+ const sourcePathWithoutExtension = SOURCE_EXTENSIONS.includes(specifiedExtension) ? unresolvedPath.slice(0, -specifiedExtension.length) : unresolvedPath;
60984
+ return [
60985
+ unresolvedPath,
60986
+ ...SOURCE_EXTENSIONS.map((extension) => `${sourcePathWithoutExtension}${extension}`),
60987
+ ...sourcePathWithoutExtension === unresolvedPath ? SOURCE_EXTENSIONS.map((extension) => path.join(unresolvedPath, `index${extension}`)) : []
60988
+ ].includes(componentRecord.absolutePath);
60989
+ };
60990
+ const getComponentTagNames = (record, component) => {
60991
+ if (record.absolutePath === component.record.absolutePath) return new Set([component.name]);
60992
+ const tagNames = /* @__PURE__ */ new Set();
60993
+ for (const statement of record.sourceFile.statements) {
60994
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !moduleSpecifierTargetsComponent(record, statement.moduleSpecifier.text, component.record)) continue;
60995
+ const importClause = statement.importClause;
60996
+ if (component.isDefaultExport && importClause?.name !== void 0) tagNames.add(importClause.name.text);
60997
+ const namedBindings = importClause?.namedBindings;
60998
+ if (namedBindings !== void 0 && ts.isNamedImports(namedBindings)) {
60999
+ for (const element of namedBindings.elements) if ((element.propertyName?.text ?? element.name.text) === component.name) tagNames.add(element.name.text);
61000
+ }
61001
+ }
61002
+ return tagNames;
61003
+ };
61004
+ const getTransparentCall = (functionNode) => {
61005
+ if (functionNode.body === void 0) return null;
61006
+ if (!ts.isBlock(functionNode.body)) {
61007
+ const expression = unwrapExpression(functionNode.body);
61008
+ return ts.isCallExpression(expression) ? expression : null;
61009
+ }
61010
+ if (functionNode.body.statements.length !== 1) return null;
61011
+ const statement = functionNode.body.statements[0];
61012
+ if (statement === void 0) return null;
61013
+ const expression = ts.isExpressionStatement(statement) || ts.isReturnStatement(statement) ? statement.expression : void 0;
61014
+ if (expression === void 0) return null;
61015
+ const unwrappedExpression = unwrapExpression(expression);
61016
+ return ts.isCallExpression(unwrappedExpression) ? unwrappedExpression : null;
61017
+ };
61018
+ const resolveTransparentTarget = (functionNode) => {
61019
+ const call = getTransparentCall(functionNode);
61020
+ if (call === null) return null;
61021
+ const target = unwrapExpression(call.expression);
61022
+ if (!ts.isIdentifier(target) || call.arguments.length !== functionNode.parameters.length) return null;
61023
+ for (const [parameterIndex, parameter] of functionNode.parameters.entries()) {
61024
+ const argument = call.arguments[parameterIndex];
61025
+ const unwrappedArgument = argument === void 0 ? null : unwrapExpression(argument);
61026
+ if (!ts.isIdentifier(parameter.name) || unwrappedArgument === null || !ts.isIdentifier(unwrappedArgument) || unwrappedArgument.text !== parameter.name.text) return null;
61027
+ }
61028
+ return target.text;
61029
+ };
61030
+ const getForwardingFunction = (declaration) => {
61031
+ if (ts.isFunctionDeclaration(declaration)) return declaration;
61032
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer === void 0) return null;
61033
+ const initializer = unwrapExpression(declaration.initializer);
61034
+ if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) return initializer;
61035
+ if (ts.isCallExpression(initializer) && getCalleeName(initializer.expression) === "useCallback") {
61036
+ const callback = initializer.arguments[0];
61037
+ if (callback !== void 0 && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) return callback;
61038
+ }
61039
+ return null;
61040
+ };
61041
+ const getDeclaredBindingName = (node) => {
61042
+ if ((ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node) || ts.isFunctionDeclaration(node)) && node.name !== void 0 && ts.isIdentifier(node.name)) return node.name.text;
61043
+ if (ts.isImportClause(node)) return node.name?.text ?? null;
61044
+ if (ts.isImportSpecifier(node) || ts.isNamespaceImport(node)) return node.name.text;
61045
+ return null;
61046
+ };
61047
+ const findBindingScope = (node) => {
61048
+ let currentNode = node;
61049
+ while (currentNode !== void 0) {
61050
+ if (ts.isParameter(currentNode) && ts.isFunctionLike(currentNode.parent)) return currentNode.parent;
61051
+ if (ts.isBlock(currentNode) || ts.isModuleBlock(currentNode) || ts.isSourceFile(currentNode)) return currentNode;
61052
+ currentNode = currentNode.parent;
61053
+ }
61054
+ return node.getSourceFile();
61055
+ };
61056
+ const findBindingDeclaration = (record, binding) => {
61057
+ const usagePosition = binding.getStart(record.sourceFile);
61058
+ const candidates = [];
61059
+ const visit = (node) => {
61060
+ if (getDeclaredBindingName(node) === binding.text) {
61061
+ const scope = findBindingScope(node);
61062
+ const isVisibleScope = usagePosition >= scope.getStart(record.sourceFile) && usagePosition <= scope.end;
61063
+ const isVisibleDeclaration = ts.isFunctionDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node) || ts.isImportClause(node) || ts.isImportSpecifier(node) || ts.isNamespaceImport(node) || node.getStart(record.sourceFile) < usagePosition;
61064
+ if (isVisibleScope && isVisibleDeclaration) candidates.push({
61065
+ declaration: node,
61066
+ scope
61067
+ });
61068
+ }
61069
+ ts.forEachChild(node, visit);
61070
+ };
61071
+ visit(record.sourceFile);
61072
+ let nearestCandidate = null;
61073
+ let nearestScopeWidth = Number.POSITIVE_INFINITY;
61074
+ let isNearestScopeAmbiguous = false;
61075
+ for (const candidate of candidates) {
61076
+ const scopeWidth = candidate.scope.end - candidate.scope.getStart(record.sourceFile);
61077
+ if (scopeWidth < nearestScopeWidth) {
61078
+ nearestCandidate = candidate;
61079
+ nearestScopeWidth = scopeWidth;
61080
+ isNearestScopeAmbiguous = false;
61081
+ } else if (scopeWidth === nearestScopeWidth) isNearestScopeAmbiguous = true;
61082
+ }
61083
+ if (isNearestScopeAmbiguous) return null;
61084
+ const declaration = nearestCandidate?.declaration;
61085
+ return declaration !== void 0 && (ts.isVariableDeclaration(declaration) || ts.isFunctionDeclaration(declaration)) ? declaration : null;
61086
+ };
61087
+ const resolveHandlerBinding = (record, binding) => {
61088
+ const declaration = findBindingDeclaration(record, binding);
61089
+ if (declaration === null) return binding.text;
61090
+ const forwardingFunction = getForwardingFunction(declaration);
61091
+ const target = forwardingFunction === null ? null : resolveTransparentTarget(forwardingFunction);
61092
+ return target === null || target === binding.text ? binding.text : target;
61093
+ };
61094
+ const getJsxTagName = (node) => ts.isIdentifier(node.tagName) ? node.tagName.text : null;
61095
+ const getHandlerBinding = (node, propName) => {
61096
+ for (const property of node.attributes.properties) {
61097
+ if (!ts.isJsxAttribute(property) || property.name.getText() !== propName) continue;
61098
+ const initializer = property.initializer;
61099
+ if (initializer === void 0 || !ts.isJsxExpression(initializer)) return null;
61100
+ if (initializer.expression === void 0) return null;
61101
+ const expression = unwrapExpression(initializer.expression);
61102
+ return ts.isIdentifier(expression) ? expression : null;
61103
+ }
61104
+ return null;
61105
+ };
61106
+ const getForwardedHandlerAliases = (rootDirectory, component, propNames, state) => {
61107
+ const cacheKey = `${component.record.filePath}\0${component.name}\0${[...propNames].sort().join("\0")}`;
61108
+ const cachedAliases = state.aliasesByComponent.get(cacheKey);
61109
+ if (cachedAliases !== void 0) return cachedAliases;
61110
+ const bindingsByPropName = /* @__PURE__ */ new Map();
61111
+ const unresolvedPropNames = /* @__PURE__ */ new Set();
61112
+ let callsiteCount = 0;
61113
+ for (const record of listSourceRecords(rootDirectory, state)) {
61114
+ const tagNames = getComponentTagNames(record, component);
61115
+ if (tagNames.size === 0) continue;
61116
+ const visit = (node) => {
61117
+ if (record === component.record && node === component.node) return;
61118
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
61119
+ const tagName = getJsxTagName(node);
61120
+ if (tagName !== null && tagNames.has(tagName)) {
61121
+ callsiteCount += 1;
61122
+ for (const propName of propNames) {
61123
+ const binding = getHandlerBinding(node, propName);
61124
+ if (binding === null) {
61125
+ unresolvedPropNames.add(propName);
61126
+ continue;
61127
+ }
61128
+ const bindings = bindingsByPropName.get(propName) ?? /* @__PURE__ */ new Set();
61129
+ bindings.add(resolveHandlerBinding(record, binding));
61130
+ bindingsByPropName.set(propName, bindings);
61131
+ }
61132
+ }
61133
+ }
61134
+ ts.forEachChild(node, visit);
61135
+ };
61136
+ visit(record.sourceFile);
61137
+ }
61138
+ const aliases = /* @__PURE__ */ new Map();
61139
+ if (callsiteCount > 0) for (const [propName, bindings] of bindingsByPropName) {
61140
+ if (bindings.size !== 1 || unresolvedPropNames.has(propName)) continue;
61141
+ const [bindingName] = bindings;
61142
+ if (bindingName !== void 0) aliases.set(propName, bindingName);
61143
+ }
61144
+ state.aliasesByComponent.set(cacheKey, aliases);
61145
+ return aliases;
61146
+ };
61147
+ const normalizeForwardedHandlers = (evidence, aliases) => {
61148
+ let normalizedEvidence = evidence;
61149
+ for (const [propName, bindingName] of aliases) normalizedEvidence = normalizedEvidence.replace(new RegExp(`\\{\\s*${propName}\\s*\\}`, "g"), `{${bindingName}}`).replace(new RegExp(`=>\\s*${propName}\\s*\\(`, "g"), `=> ${bindingName}(`);
61150
+ return normalizedEvidence;
61151
+ };
61152
+ const createDiagnosticEvidenceReader = (rootDirectory, options = {}) => {
61153
+ const state = {
61154
+ aliasesByComponent: /* @__PURE__ */ new Map(),
61155
+ recordByFilePath: /* @__PURE__ */ new Map(),
61156
+ sourceRecords: null
61157
+ };
61158
+ return (diagnostic) => {
61159
+ const record = readSourceRecord(rootDirectory, diagnostic.filePath, state);
61160
+ if (record === null || !Number.isInteger(diagnostic.line)) return null;
61161
+ const sourceLines = record.sourceText.split(/\r?\n/);
61162
+ const startLineIndex = Math.max(0, diagnostic.line - 1);
61163
+ const endLineIndex = Math.max(startLineIndex, (diagnostic.endLine ?? diagnostic.line) - 1);
61164
+ const evidence = sourceLines.slice(startLineIndex, endLineIndex + 1).join("\n");
61165
+ if (!options.resolveForwardedHandlers) return evidence;
61166
+ const propNames = new Set(evidence.match(/\bon[A-Z]\w*\b/g) ?? []);
61167
+ if (propNames.size === 0) return evidence;
61168
+ const component = findEnclosingComponent(record, diagnostic);
61169
+ if (component === null) return evidence;
61170
+ return normalizeForwardedHandlers(evidence, getForwardedHandlerAliases(rootDirectory, component, propNames, state));
61171
+ };
61172
+ };
61173
+ //#endregion
60404
61174
  //#region src/cli/utils/build-no-score-message.ts
60405
61175
  const ENTERPRISE_CONTACT_HINT = `Want something custom to your company? Contact us at ${ENTERPRISE_CONTACT_URL}.`;
60406
- const buildNoScoreMessage = (isScoreDisabled) => {
60407
- return `${isScoreDisabled ? "Score disabled by --no-score." : "Score unavailable (could not reach the score API)."} ${ENTERPRISE_CONTACT_HINT}`;
61176
+ const buildNoScoreMessage = (isScoreDisabled, disabledMessage) => {
61177
+ return `${isScoreDisabled ? disabledMessage ?? "Score disabled by --no-score." : "Score unavailable (could not reach the score API)."} ${ENTERPRISE_CONTACT_HINT}`;
60408
61178
  };
60409
61179
  //#endregion
60410
61180
  //#region src/cli/utils/render-agent-guidance.ts
@@ -61529,40 +62299,47 @@ const buildChangedLineMatcher = (directory, changedLineRanges) => {
61529
62299
  return diagnosticIntersectsLineRanges(diagnostic, ranges);
61530
62300
  };
61531
62301
  };
61532
- const buildIgnoredTags = (userConfig) => {
62302
+ const buildIgnoredTags = (userConfig, includedTags) => {
61533
62303
  const tags = /* @__PURE__ */ new Set();
61534
62304
  if (userConfig?.ignore?.tags) for (const tag of userConfig.ignore.tags) tags.add(tag);
62305
+ for (const tag of includedTags) tags.delete(tag);
61535
62306
  return tags;
61536
62307
  };
61537
- const mergeInspectOptions = (inputOptions, userConfig) => ({
61538
- lint: inputOptions.lint ?? userConfig?.lint ?? true,
61539
- deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
61540
- supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
61541
- verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
61542
- outputDirectory: inputOptions.outputDirectory || null,
61543
- scoreOnly: inputOptions.scoreOnly ?? false,
61544
- noScore: inputOptions.noScore ?? userConfig?.noScore ?? false,
61545
- isCi: inputOptions.isCi ?? false,
61546
- isCiOrCodingAgentEnvironment: isCiOrCodingAgentEnvironment(),
61547
- isNonInteractiveEnvironment: isNonInteractiveEnvironment(),
61548
- silent: inputOptions.silent ?? false,
61549
- includePaths: inputOptions.includePaths ?? [],
61550
- customRulesOnly: userConfig?.customRulesOnly ?? false,
61551
- share: userConfig?.share ?? true,
61552
- respectInlineDisables: inputOptions.respectInlineDisables ?? userConfig?.respectInlineDisables ?? true,
61553
- warnings: inputOptions.warnings ?? userConfig?.warnings ?? true,
61554
- categoryFilters: new Set(resolveCliCategories(inputOptions.categoryFilters) ?? []),
61555
- adoptExistingLintConfig: userConfig?.adoptExistingLintConfig ?? true,
61556
- ignoredTags: buildIgnoredTags(userConfig),
61557
- outputSurface: inputOptions.outputSurface ?? "cli",
61558
- suppressRendering: inputOptions.suppressRendering ?? false,
61559
- concurrentScan: inputOptions.concurrentScan ?? false,
61560
- concurrency: inputOptions.concurrency,
61561
- maxDurationMs: inputOptions.maxDurationMs ?? null,
61562
- baseline: inputOptions.baseline ?? null,
61563
- changedLineRanges: inputOptions.changedLineRanges ?? null,
61564
- supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false
61565
- });
62308
+ const mergeInspectOptions = (inputOptions, userConfig) => {
62309
+ const includedTags = inputOptions.includedTags ?? /* @__PURE__ */ new Set();
62310
+ return {
62311
+ lint: inputOptions.lint ?? userConfig?.lint ?? true,
62312
+ deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
62313
+ supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
62314
+ verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
62315
+ outputDirectory: inputOptions.outputDirectory || null,
62316
+ scoreOnly: inputOptions.scoreOnly ?? false,
62317
+ noScore: inputOptions.noScore ?? userConfig?.noScore ?? false,
62318
+ isCi: inputOptions.isCi ?? false,
62319
+ isCiOrCodingAgentEnvironment: isCiOrCodingAgentEnvironment(),
62320
+ isNonInteractiveEnvironment: isNonInteractiveEnvironment(),
62321
+ silent: inputOptions.silent ?? false,
62322
+ includePaths: inputOptions.includePaths ?? [],
62323
+ customRulesOnly: includedTags.size > 0 ? false : userConfig?.customRulesOnly ?? false,
62324
+ share: userConfig?.share ?? true,
62325
+ respectInlineDisables: inputOptions.respectInlineDisables ?? userConfig?.respectInlineDisables ?? true,
62326
+ warnings: inputOptions.warnings ?? userConfig?.warnings ?? true,
62327
+ categoryFilters: new Set(resolveCliCategories(inputOptions.categoryFilters) ?? []),
62328
+ adoptExistingLintConfig: includedTags.size > 0 ? false : userConfig?.adoptExistingLintConfig ?? true,
62329
+ ignoredTags: buildIgnoredTags(userConfig, includedTags),
62330
+ includedTags,
62331
+ includeTagDefaults: inputOptions.includeTagDefaults ?? false,
62332
+ scoreDisabledMessage: inputOptions.scoreDisabledMessage,
62333
+ outputSurface: inputOptions.outputSurface ?? "cli",
62334
+ suppressRendering: inputOptions.suppressRendering ?? false,
62335
+ concurrentScan: inputOptions.concurrentScan ?? false,
62336
+ concurrency: inputOptions.concurrency,
62337
+ maxDurationMs: inputOptions.maxDurationMs ?? null,
62338
+ baseline: inputOptions.baseline ?? null,
62339
+ changedLineRanges: inputOptions.changedLineRanges ?? null,
62340
+ supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false
62341
+ };
62342
+ };
61566
62343
  const deriveScope = (options) => {
61567
62344
  if (options.baseline) return "changed";
61568
62345
  if (options.changedLineRanges !== null) return "lines";
@@ -61644,6 +62421,8 @@ const runBaselineComparison = async (params) => {
61644
62421
  directory: params.directory,
61645
62422
  ref: params.baselineRef,
61646
62423
  files: params.options.includePaths,
62424
+ baseFiles: params.baseFiles,
62425
+ headFiles: params.headFiles,
61647
62426
  tempDirectory
61648
62427
  }).catch((error) => {
61649
62428
  rmSync(tempDirectory, {
@@ -61652,7 +62431,24 @@ const runBaselineComparison = async (params) => {
61652
62431
  });
61653
62432
  throw error;
61654
62433
  });
62434
+ if (snapshot === null) {
62435
+ rmSync(tempDirectory, {
62436
+ recursive: true,
62437
+ force: true
62438
+ });
62439
+ return null;
62440
+ }
61655
62441
  try {
62442
+ if (!snapshot.isComplete) return null;
62443
+ const analyzedHeadFiles = new Set(params.headAnalyzedFiles.map(toForwardSlashes));
62444
+ const baseFiles = new Set(snapshot.baseFiles.map(toForwardSlashes));
62445
+ const trackedHeadFiles = new Set(snapshot.headFiles.map(toForwardSlashes));
62446
+ const expectedHeadFiles = new Set(trackedHeadFiles);
62447
+ for (const filePath of params.options.includePaths) {
62448
+ const normalizedFilePath = toForwardSlashes(filePath);
62449
+ if (!baseFiles.has(normalizedFilePath)) expectedHeadFiles.add(normalizedFilePath);
62450
+ }
62451
+ if (filterSourceFiles([...expectedHeadFiles]).some((filePath) => !analyzedHeadFiles.has(filePath))) return null;
61656
62452
  const baseLayers = buildRuntimeLayers({
61657
62453
  directory: snapshot.tempDirectory,
61658
62454
  hasConfigOverride: true,
@@ -61668,12 +62464,14 @@ const runBaselineComparison = async (params) => {
61668
62464
  });
61669
62465
  const baseOutput = await runPromise(restoreLegacyThrow(runInspect({
61670
62466
  directory: snapshot.tempDirectory,
61671
- includePaths: params.options.includePaths,
62467
+ includePaths: snapshot.materializedFiles,
61672
62468
  customRulesOnly: params.options.customRulesOnly,
61673
62469
  respectInlineDisables: params.options.respectInlineDisables,
61674
62470
  warnings: params.options.warnings,
61675
62471
  adoptExistingLintConfig: params.options.adoptExistingLintConfig,
61676
62472
  ignoredTags: params.options.ignoredTags,
62473
+ includedTags: params.options.includedTags,
62474
+ includeTagDefaults: params.options.includeTagDefaults,
61677
62475
  nodeBinaryPath: params.resolvedNodeBinaryPath ?? void 0,
61678
62476
  runDeadCode: false,
61679
62477
  isCi: params.options.isCi,
@@ -61685,18 +62483,22 @@ const runBaselineComparison = async (params) => {
61685
62483
  deadlineEpochMs: params.deadlineEpochMs ?? void 0
61686
62484
  }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))));
61687
62485
  if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
62486
+ const hasUnscannedUntrackedSourceFiles = filterSourceFiles(snapshot.untrackedFiles.map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
61688
62487
  const delta = computeDiagnosticDelta({
61689
62488
  headDiagnostics: params.headDiagnostics,
61690
62489
  baseDiagnostics: baseOutput.diagnostics,
61691
62490
  readHeadLine: createSourceLineReader(params.directory),
61692
- readBaseLine: createSourceLineReader(snapshot.tempDirectory)
62491
+ readBaseLine: createSourceLineReader(snapshot.tempDirectory),
62492
+ readHeadEvidence: createDiagnosticEvidenceReader(params.directory, { resolveForwardedHandlers: true }),
62493
+ readBaseEvidence: createDiagnosticEvidenceReader(snapshot.tempDirectory)
61693
62494
  });
61694
62495
  return {
61695
62496
  displayDiagnostics: delta.newDiagnostics,
61696
62497
  baselineDelta: {
61697
62498
  baseRef: params.baselineRef,
61698
- fixedCount: delta.fixedCount,
61699
- baseTotalCount: baseOutput.diagnostics.length
62499
+ fixedCount: hasUnscannedUntrackedSourceFiles ? 0 : delta.fixedCount,
62500
+ baseTotalCount: baseOutput.diagnostics.length,
62501
+ crossFileMatchCount: delta.crossFileMatchCount
61700
62502
  }
61701
62503
  };
61702
62504
  } finally {
@@ -61763,6 +62565,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
61763
62565
  warnings: options.warnings,
61764
62566
  adoptExistingLintConfig: options.adoptExistingLintConfig,
61765
62567
  ignoredTags: options.ignoredTags,
62568
+ includedTags: options.includedTags,
62569
+ includeTagDefaults: options.includeTagDefaults,
61766
62570
  nodeBinaryPath: resolvedNodeBinaryPath ?? void 0,
61767
62571
  runDeadCode: options.deadCode,
61768
62572
  isCi: options.isCi,
@@ -61803,6 +62607,9 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
61803
62607
  headDiagnostics: output.diagnostics,
61804
62608
  resolvedNodeBinaryPath,
61805
62609
  baselineRef: options.baseline.ref,
62610
+ baseFiles: options.baseline.baseFiles,
62611
+ headFiles: options.baseline.headFiles,
62612
+ headAnalyzedFiles: output.analyzedFiles,
61806
62613
  deadlineEpochMs
61807
62614
  });
61808
62615
  if (comparison) {
@@ -61948,7 +62755,7 @@ const finalizeAndRender = (input) => gen(function* () {
61948
62755
  securityScanFailed
61949
62756
  });
61950
62757
  const hasSkippedChecks = skippedChecks.length > 0;
61951
- const noScoreMessage = buildNoScoreMessage(options.noScore);
62758
+ const noScoreMessage = buildNoScoreMessage(options.noScore, options.scoreDisabledMessage);
61952
62759
  const buildResult = () => ({
61953
62760
  diagnostics: [...diagnostics],
61954
62761
  score,
@@ -64053,21 +64860,25 @@ const resolveParallelFlag = (parallel) => parallel === false ? 1 : void 0;
64053
64860
  */
64054
64861
  const resolveCliInspectOptions = (flags, userConfig) => {
64055
64862
  const wantsWarningGate = pickBlockingLevel(flags, userConfig) === "warning";
64863
+ const isDesignScan = flags.design === true;
64056
64864
  return {
64057
64865
  lint: flags.lint,
64058
- deadCode: flags.deadCode,
64059
- supplyChain: flags.supplyChain,
64866
+ deadCode: isDesignScan ? false : flags.deadCode,
64867
+ supplyChain: isDesignScan ? false : flags.supplyChain,
64060
64868
  verbose: flags.verbose,
64061
64869
  outputDirectory: flags.outputDir,
64062
64870
  respectInlineDisables: flags.respectInlineDisables === false ? false : void 0,
64063
64871
  warnings: wantsWarningGate ? true : flags.warnings,
64064
64872
  scoreOnly: flags.score === true,
64065
- noScore: flags.score === false || flags.telemetry === false ? true : void 0,
64873
+ noScore: isDesignScan || flags.score === false || flags.telemetry === false ? true : void 0,
64066
64874
  isCi: isCiEnvironment(),
64067
64875
  silent: Boolean(flags.json),
64068
64876
  concurrency: resolveParallelFlag(flags.parallel),
64069
64877
  maxDurationMs: resolveMaxDurationFlag(flags.maxDuration),
64070
- categoryFilters: resolveCliCategories(flags.category)
64878
+ categoryFilters: resolveCliCategories(flags.category),
64879
+ includedTags: isDesignScan ? new Set(["design"]) : void 0,
64880
+ includeTagDefaults: isDesignScan ? true : void 0,
64881
+ scoreDisabledMessage: isDesignScan ? "Design scans do not affect the React health score." : void 0
64071
64882
  };
64072
64883
  };
64073
64884
  //#endregion
@@ -64087,20 +64898,25 @@ const resolveProjectRelativeDirectory = (rootDirectory, projectDirectory) => {
64087
64898
  return relativeProjectDirectory;
64088
64899
  };
64089
64900
  //#endregion
64090
- //#region src/cli/utils/resolve-project-diff-include-paths.ts
64091
- const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
64901
+ //#region src/cli/utils/resolve-project-source-file-paths.ts
64902
+ const resolveProjectSourceFilePaths = (rootDirectory, projectDirectory, filePaths) => {
64092
64903
  const relativeProjectDirectory = resolveProjectRelativeDirectory(rootDirectory, projectDirectory);
64093
64904
  if (relativeProjectDirectory === null) return [];
64094
- const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);
64095
- if (relativeProjectDirectory.length === 0) return changedSourceFiles;
64905
+ const sourceFilePaths = filterSourceFiles([...filePaths]);
64906
+ if (relativeProjectDirectory.length === 0) return sourceFilePaths;
64096
64907
  const projectPrefix = `${relativeProjectDirectory}/`;
64097
- return changedSourceFiles.flatMap((filePath) => {
64908
+ return sourceFilePaths.flatMap((filePath) => {
64098
64909
  const normalizedFilePath = toForwardSlashes(filePath);
64099
64910
  if (!normalizedFilePath.startsWith(projectPrefix)) return [];
64100
64911
  const projectRelativePath = normalizedFilePath.slice(projectPrefix.length);
64101
64912
  return projectRelativePath.length > 0 ? [projectRelativePath] : [];
64102
64913
  });
64103
64914
  };
64915
+ //#endregion
64916
+ //#region src/cli/utils/resolve-project-diff-include-paths.ts
64917
+ const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
64918
+ return resolveProjectSourceFilePaths(rootDirectory, projectDirectory, diffInfo.changedFiles);
64919
+ };
64104
64920
  /**
64105
64921
  * Re-keys repo-relative changed line ranges (from `getChangedLineRanges`) to
64106
64922
  * the scanned project, mirroring `resolveProjectDiffIncludePaths`'s boundary
@@ -64480,7 +65296,7 @@ const maybeMigrateLegacyConfig = async (requestedDirectory, { isQuiet, isStaged
64480
65296
  if (!(!isQuiet && !isStaged && process.stdout.isTTY === true && !isCiOrCodingAgentEnvironment())) return;
64481
65297
  await runProjectMigrations(requestedDirectory);
64482
65298
  };
64483
- const inspectAction = async (directory, flags) => {
65299
+ const inspectAction = async (directory, flags, invocationCommand = "inspect") => {
64484
65300
  const isScoreOnly = Boolean(flags.score);
64485
65301
  const isJsonMode = Boolean(flags.json);
64486
65302
  const isQuiet = isScoreOnly || isJsonMode;
@@ -64494,7 +65310,7 @@ const inspectAction = async (directory, flags) => {
64494
65310
  });
64495
65311
  if (flags.jsonOut) recordCount(METRIC.jsonOutUsed, 1);
64496
65312
  }
64497
- recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
65313
+ recordCount(METRIC.cliInvoked, 1, { command: invocationCommand });
64498
65314
  try {
64499
65315
  validateModeFlags(flags);
64500
65316
  if (flags.staged) setJsonReportMode("staged");
@@ -64639,6 +65455,7 @@ const inspectAction = async (directory, flags) => {
64639
65455
  const isDiffMode = scope !== "full";
64640
65456
  const comparisonBaseRef = isDiffMode && diffInfo && !diffInfo.isCurrentChanges ? diffInfo.baseSha ? await resolveMergeBaseRef(resolvedDirectory, diffInfo.baseSha) : diffInfo.diffBaseRef ?? await resolveMergeBaseRef(resolvedDirectory, diffInfo.baseBranch) : null;
64641
65457
  const baselineRef = scope === "changed" ? comparisonBaseRef : null;
65458
+ const baselineDiffPlan = baselineRef === null ? null : await getBaselineDiffPlan(resolvedDirectory, baselineRef);
64642
65459
  const linesBaseRef = diffInfo?.isCurrentChanges ? "HEAD" : comparisonBaseRef;
64643
65460
  const changedLineRanges = scope === "lines" && diffInfo !== null && (diffInfo.isCurrentChanges || linesBaseRef !== null) && diffInfo !== null ? await getChangedLineRanges({
64644
65461
  directory: resolvedDirectory,
@@ -64669,10 +65486,13 @@ const inspectAction = async (directory, flags) => {
64669
65486
  const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
64670
65487
  let includePaths;
64671
65488
  let supplyChainManifestChanged = false;
65489
+ const projectBaselineBaseFiles = baselineDiffPlan === null ? null : resolveProjectSourceFilePaths(resolvedDirectory, scanDirectory, baselineDiffPlan.baseFiles);
65490
+ const projectBaselineHeadFiles = baselineDiffPlan === null ? null : resolveProjectSourceFilePaths(resolvedDirectory, scanDirectory, baselineDiffPlan.headFiles);
64672
65491
  if (isDiffMode) {
64673
65492
  const changedSourceFiles = diffInfo === null ? [] : resolveProjectDiffIncludePaths(resolvedDirectory, scanDirectory, diffInfo);
64674
65493
  supplyChainManifestChanged = supplyChainEnabled && diffInfo !== null && projectManifestChanged(resolvedDirectory, scanDirectory, diffInfo);
64675
- if (changedSourceFiles.length === 0 && !supplyChainManifestChanged) {
65494
+ const hasBaselineOnlyFiles = (projectBaselineBaseFiles?.length ?? 0) > 0;
65495
+ if (changedSourceFiles.length === 0 && !supplyChainManifestChanged && !hasBaselineOnlyFiles) {
64676
65496
  if (!isQuiet) {
64677
65497
  cliLogger.dim(`No changed source files in ${scanDirectory}, skipping.`);
64678
65498
  cliLogger.break();
@@ -64680,6 +65500,7 @@ const inspectAction = async (directory, flags) => {
64680
65500
  return null;
64681
65501
  }
64682
65502
  includePaths = [...changedSourceFiles];
65503
+ if (includePaths.length === 0 && hasBaselineOnlyFiles) includePaths.push(...projectBaselineBaseFiles ?? []);
64683
65504
  if (supplyChainManifestChanged) includePaths.push("package.json");
64684
65505
  }
64685
65506
  if (!isQuiet && !isMultiProject) cliLogger.dim(" ");
@@ -64691,7 +65512,11 @@ const inspectAction = async (directory, flags) => {
64691
65512
  configSourceDirectory: projectConfigSourceDirectory ?? void 0,
64692
65513
  suppressRendering: isMultiProject,
64693
65514
  concurrentScan: isMultiProject,
64694
- baseline: baselineRef ? { ref: baselineRef } : void 0,
65515
+ baseline: baselineRef !== null && projectBaselineBaseFiles !== null && projectBaselineHeadFiles !== null ? {
65516
+ ref: baselineRef,
65517
+ baseFiles: projectBaselineBaseFiles,
65518
+ headFiles: projectBaselineHeadFiles
65519
+ } : void 0,
64695
65520
  changedLineRanges: scope === "lines" && changedLineRanges !== null ? resolveProjectChangedLineRanges(resolvedDirectory, scanDirectory, changedLineRanges) : void 0,
64696
65521
  supplyChainManifestChanged
64697
65522
  });
@@ -64797,6 +65622,13 @@ const inspectAction = async (directory, flags) => {
64797
65622
  }
64798
65623
  };
64799
65624
  //#endregion
65625
+ //#region src/cli/commands/design.ts
65626
+ const designAction = async (directory, flags) => inspectAction(directory, {
65627
+ ...flags,
65628
+ design: true,
65629
+ lint: flags.lint ?? true
65630
+ }, "design");
65631
+ //#endregion
64800
65632
  //#region src/cli/commands/install.ts
64801
65633
  const installAction = async (options, command) => {
64802
65634
  recordCount(METRIC.cliInvoked, 1, { command: "install" });
@@ -65633,6 +66465,7 @@ const INSTALL_FLAG_SPEC = {
65633
66465
  shortOptionsWithRequiredValues: new Set(["-c"])
65634
66466
  };
65635
66467
  const COMMAND_FLAG_SPECS = new Map([
66468
+ ["design", ROOT_FLAG_SPEC],
65636
66469
  ["install", INSTALL_FLAG_SPEC],
65637
66470
  ["setup", INSTALL_FLAG_SPEC],
65638
66471
  ["version", {
@@ -65789,6 +66622,7 @@ ${formatExampleLines([
65789
66622
  ["react-doctor --scope changed --base main", "scan only new issues vs. main"],
65790
66623
  ["react-doctor --project modules/a,modules/b", "score each module separately (names or paths)"],
65791
66624
  ["react-doctor --staged", "scan staged files (pre-commit hook)"],
66625
+ ["react-doctor design", "run the focused UI design audit"],
65792
66626
  ["react-doctor --category Security", "show only one diagnostic category"],
65793
66627
  ["react-doctor --blocking warning", "fail CI on warnings too (default: error)"],
65794
66628
  ["react-doctor --json > report.json", "write a machine-readable report"],
@@ -65822,6 +66656,20 @@ ${highlighter.dim("Managing CI:")}
65822
66656
  ${highlighter.dim("Learn more:")}
65823
66657
  ${highlighter.info(CANONICAL_GITHUB_URL)}
65824
66658
  `;
66659
+ const renderDesignHelpEpilog = () => `
66660
+ ${highlighter.dim("Examples:")}
66661
+ ${formatExampleLines([
66662
+ ["react-doctor design", "audit UI design in the current project"],
66663
+ ["react-doctor design ./apps/web", "audit one application"],
66664
+ ["react-doctor design --verbose", "show every design finding"],
66665
+ ["react-doctor design --json", "write a design-only JSON report"]
66666
+ ])}
66667
+
66668
+ ${highlighter.dim("Scope:")}
66669
+ Runs only rules tagged ${highlighter.info("design")}, including focused rules that stay opt-in during a general health scan.
66670
+ Dead-code, supply-chain, external lint-config, custom-plugin, and health-score passes are skipped.
66671
+ Standard scan flags such as ${highlighter.info("--scope")}, ${highlighter.info("--project")}, ${highlighter.info("--verbose")}, and ${highlighter.info("--json")} still work.
66672
+ `;
65825
66673
  const renderCiHelpEpilog = () => `
65826
66674
  ${highlighter.dim("Examples:")}
65827
66675
  ${formatExampleLines([
@@ -65843,6 +66691,7 @@ ${highlighter.dim("Learn more:")}
65843
66691
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
65844
66692
  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 supported JS/TS source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").option("--include-untracked", "with --scope files/changed/lines, also scan ordinary untracked files (respects .gitignore)").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);
65845
66693
  program.action(inspectAction);
66694
+ program.command("design [directory]").description("Run only the focused UI design diagnostics").addHelpText("after", renderDesignHelpEpilog).action((directory, _options, command) => designAction(directory ?? ".", command.optsWithGlobals()));
65846
66695
  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));
65847
66696
  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);
65848
66697
  const providerOption = ["--provider <name>", "ci provider: github-actions or gitlab-ci (auto-detected by default)"];
@@ -65891,4 +66740,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
65891
66740
  export {};
65892
66741
 
65893
66742
  //# sourceMappingURL=cli.js.map
65894
- //# debugId=cfa479fc-64e6-5081-86f2-1cbef233c1a0
66743
+ //# debugId=a9af63ed-7b82-56d3-8549-274ab7f23579