react-doctor 0.8.0 → 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]="5cb8a881-e8fc-5561-8b89-ad2c8357acbc")}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",
@@ -38359,6 +38573,9 @@ const SCRIPT_EXTENSION_FRAGMENT = "[cm]?[jt]sx?";
38359
38573
  const STORY_FILE_SUFFIX_PATTERN = new RegExp(`\\.(?:stories|story)\\.(?:${SCRIPT_EXTENSION_FRAGMENT})$`);
38360
38574
  const TEST_FILE_SUFFIX_PATTERN = new RegExp(`\\.(?:test|spec|fixture|fixtures)\\.(?:${SCRIPT_EXTENSION_FRAGMENT})$`);
38361
38575
  const TEST_FILE_DIRECTORY_PATTERN = /(?:^|\/)(?:__tests__|__test__|tests|test|__mocks__|cypress|e2e|playwright)\//;
38576
+ const AMBIGUOUS_TEST_ROUTE_DIRECTORY_PATTERN = /(?:^|\/)(?:tests|test)\//;
38577
+ const NEXT_APP_ROUTE_FILE_SUFFIX_PATTERN = /\/(?:default|error|forbidden|global-error|layout|loading|not-found|page|route|template|unauthorized)\.[cm]?[jt]sx?$/;
38578
+ const SCRIPT_FILE_SUFFIX_PATTERN = /\.[cm]?[jt]sx?$/;
38362
38579
  const FIXTURE_PROJECT_PATTERN = /\/(?:fixtures|__fixtures__)\//;
38363
38580
  const SOURCE_ROOT_PATTERN = /\/(?:src|app|lib|components|pages|features|modules|packages|apps|frontend|client)\//g;
38364
38581
  const stripAboveSourceRoot = (relativePath) => {
@@ -38369,6 +38586,14 @@ const stripAboveSourceRoot = (relativePath) => {
38369
38586
  if (lastIdx >= 0) return relativePath.slice(lastIdx);
38370
38587
  return relativePath.slice(fixtureMatch.index + fixtureMatch[0].length - 1);
38371
38588
  };
38589
+ const isFrameworkRouteBeforeAmbiguousTestDirectory = (relativePath) => {
38590
+ const testDirectoryMatch = AMBIGUOUS_TEST_ROUTE_DIRECTORY_PATTERN.exec(relativePath);
38591
+ if (testDirectoryMatch === null) return false;
38592
+ let nearestRouteRoot;
38593
+ for (const pathSegment of relativePath.slice(0, testDirectoryMatch.index).split("/")) if (pathSegment === "app" || pathSegment === "pages") nearestRouteRoot = pathSegment;
38594
+ if (nearestRouteRoot === void 0) return false;
38595
+ return nearestRouteRoot === "pages" ? SCRIPT_FILE_SUFFIX_PATTERN.test(relativePath) : NEXT_APP_ROUTE_FILE_SUFFIX_PATTERN.test(relativePath);
38596
+ };
38372
38597
  /**
38373
38598
  * Classifies where a file sits relative to shipped code. A finding in a
38374
38599
  * `.stories.tsx` or `.spec.ts` file never runs in front of users, so
@@ -38385,6 +38610,7 @@ const classifyFileContext = (relativePath) => {
38385
38610
  if (STORY_FILE_SUFFIX_PATTERN.test(forwardSlashed)) return "story";
38386
38611
  if (TEST_FILE_SUFFIX_PATTERN.test(forwardSlashed)) return "test";
38387
38612
  const scoped = stripAboveSourceRoot(forwardSlashed);
38613
+ if (isFrameworkRouteBeforeAmbiguousTestDirectory(scoped)) return "production";
38388
38614
  return TEST_FILE_DIRECTORY_PATTERN.test(scoped) ? "test" : "production";
38389
38615
  };
38390
38616
  /**
@@ -39244,6 +39470,7 @@ const VALID_RULE_SEVERITIES = [
39244
39470
  "warn",
39245
39471
  "off"
39246
39472
  ];
39473
+ const VALID_INCLUDED_FILE_CONTEXTS = ["test", "story"];
39247
39474
  const KNOWN_CATEGORY_LABEL = DIAGNOSTIC_CATEGORY_BUCKETS.join(", ");
39248
39475
  const isDiagnosticCategoryBucket = (value) => DIAGNOSTIC_CATEGORY_BUCKETS.includes(value);
39249
39476
  const filterKnownCategories = (fieldName, categories) => categories.filter((category) => {
@@ -39278,6 +39505,7 @@ const SURFACE_CONTROL_FIELD_NAMES = [
39278
39505
  "includeRules",
39279
39506
  "excludeRules"
39280
39507
  ];
39508
+ const isIncludedFileContext = (value) => VALID_INCLUDED_FILE_CONTEXTS.some((fileContext) => fileContext === value);
39281
39509
  const SEVERITY_FIELD_NAMES = ["rules", "categories"];
39282
39510
  const isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
39283
39511
  const formatType = (value) => typeof value === "string" ? `"${value}"` : typeof value;
@@ -39312,6 +39540,15 @@ const validateSurfaceControls = (surface, rawControls) => {
39312
39540
  return;
39313
39541
  }
39314
39542
  const validatedSurfaceControls = {};
39543
+ if (rawControls.includeFileContexts !== void 0) {
39544
+ const qualifiedName = `surfaces.${surface}.includeFileContexts`;
39545
+ const result = validateStringArrayField(qualifiedName, rawControls.includeFileContexts);
39546
+ if (result !== void 0) validatedSurfaceControls.includeFileContexts = result.filter((fileContext) => {
39547
+ if (isIncludedFileContext(fileContext)) return true;
39548
+ warnConfigIssue(`config field "${qualifiedName}" lists "${fileContext}", which is not a non-production file context (expected one of: ${VALID_INCLUDED_FILE_CONTEXTS.join(", ")}); ignoring the entry.`);
39549
+ return false;
39550
+ });
39551
+ }
39315
39552
  for (const fieldName of SURFACE_CONTROL_FIELD_NAMES) {
39316
39553
  if (rawControls[fieldName] === void 0) continue;
39317
39554
  const qualifiedName = `surfaces.${surface}.${fieldName}`;
@@ -40276,9 +40513,25 @@ const BABEL_CONFIG_FILE_NAMES = [
40276
40513
  ".babelrc.json"
40277
40514
  ];
40278
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}['"]`);
40279
40520
  const MODERN_PRESET_REFERENCE = new RegExp(`['"]module:@react-native/babel-preset['"]`);
40280
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"));
40281
40532
  const checkReactNativeMetroBabelPreset = (rootDirectory) => {
40533
+ const packageJson = readPackageJson$1(Path.join(rootDirectory, "package.json"));
40534
+ const shouldReportLegacyPreset = isReactNativeVersionAtLeastPresetRename(rootDirectory, packageJson) && !canResolveLegacyPreset(rootDirectory, packageJson);
40282
40535
  for (const fileName of BABEL_CONFIG_FILE_NAMES) {
40283
40536
  const filePath = Path.join(rootDirectory, fileName);
40284
40537
  if (!isFile(filePath)) continue;
@@ -40288,7 +40541,7 @@ const checkReactNativeMetroBabelPreset = (rootDirectory) => {
40288
40541
  } catch {
40289
40542
  continue;
40290
40543
  }
40291
- if (contents.includes(LEGACY_PRESET_SPEC)) return [buildReactNativeDiagnostic({
40544
+ if (shouldReportLegacyPreset && LEGACY_PRESET_REFERENCE.test(contents)) return [buildReactNativeDiagnostic({
40292
40545
  filePath: fileName,
40293
40546
  rule: "rn-no-metro-babel-preset",
40294
40547
  severity: "error",
@@ -40311,12 +40564,6 @@ const checkReactNativeProject = (rootDirectory, project) => {
40311
40564
  return [...checkReactNativeMetroBabelPreset(rootDirectory), ...checkReactNativeLibraryDependencies(rootDirectory)];
40312
40565
  };
40313
40566
  const RULE_KEY = "no-vulnerable-react-server-components";
40314
- const DEPENDENCY_SECTIONS = [
40315
- "dependencies",
40316
- "devDependencies",
40317
- "peerDependencies",
40318
- "optionalDependencies"
40319
- ];
40320
40567
  const REACT_RSC_ADVISORIES_BY_MINOR = {
40321
40568
  "19.0": {
40322
40569
  rceFixedVersion: "19.0.1",
@@ -40363,20 +40610,13 @@ const enumerateWorkspaceDirectories = (workspaceRoot) => {
40363
40610
  for (const pattern of patterns) for (const directory of resolveWorkspaceDirectories(workspaceRoot, pattern)) directories.add(directory);
40364
40611
  return [...directories];
40365
40612
  };
40366
- const readDeclaredSpec = (packageJson, packageName) => {
40367
- for (const section of DEPENDENCY_SECTIONS) {
40368
- const spec = packageJson[section]?.[packageName];
40369
- if (typeof spec === "string") return spec;
40370
- }
40371
- return null;
40372
- };
40373
40613
  const resolveVersionInDirectory = (directory, packageName, declaredSpecOverride) => {
40374
40614
  const manifestPath = Path.join(directory, "node_modules", packageName, "package.json");
40375
40615
  if (isFile(manifestPath)) {
40376
40616
  const installedVersion = import_semver.valid(readPackageJson$1(manifestPath).version ?? null);
40377
40617
  if (installedVersion !== null) return installedVersion;
40378
40618
  }
40379
- 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];
40380
40620
  for (const spec of candidateSpecs) {
40381
40621
  const pinnedVersion = spec === null ? null : import_semver.valid(spec);
40382
40622
  if (pinnedVersion !== null) return pinnedVersion;
@@ -41044,21 +41284,50 @@ const buildCapabilities = (project) => {
41044
41284
  if (CLIENT_ONLY_FRAMEWORKS.has(project.framework)) capabilities.add("client-only");
41045
41285
  if (project.isStaticExport) capabilities.add("nextjs:static-export");
41046
41286
  if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
41287
+ if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 16) capabilities.add("nextjs:16");
41047
41288
  addMajorLadder(capabilities, "react", project.reactMajorVersion, 17, 30);
41048
41289
  if (project.reactMajorVersion !== null && project.reactMajorVersion >= 19 && isMajorMinorAtLeast(parseReactMajorMinor(project.reactVersion), {
41049
41290
  major: 19,
41050
41291
  minor: 2
41051
41292
  })) capabilities.add("react:19.2");
41052
- if (project.tailwindVersion !== null) capabilities.add("tailwind");
41053
- if (project.tailwindVersion !== null && isMajorMinorAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
41054
- major: 3,
41055
- minor: 4
41056
- })) 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
+ }
41057
41305
  if (project.zodVersion !== null) capabilities.add("zod");
41058
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);
41059
41316
  if (project.isPreES2023Target) capabilities.add("pre-es2023");
41060
41317
  if (project.hasReactCompiler) capabilities.add("react-compiler");
41061
- 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
+ }
41062
41331
  if (project.hasTypeScript) capabilities.add("typescript");
41063
41332
  if (project.preactVersion !== null) capabilities.add("preact");
41064
41333
  addMajorLadder(capabilities, "preact", project.preactMajorVersion, 10, 20);
@@ -41073,7 +41342,7 @@ const getCapabilities = (project) => {
41073
41342
  capabilitiesByProject.set(project, capabilities);
41074
41343
  return capabilities;
41075
41344
  };
41076
- const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhen) => {
41345
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhen, includedTags) => {
41077
41346
  if (requires) {
41078
41347
  for (const capability of requires) if (!capabilities.has(capability)) return false;
41079
41348
  }
@@ -41084,20 +41353,29 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhe
41084
41353
  if (tags) {
41085
41354
  for (const tag of tags) if (ignoredTags.has(tag)) return false;
41086
41355
  }
41356
+ if (includedTags && includedTags.size > 0) {
41357
+ if (!tags?.some((tag) => includedTags.has(tag))) return false;
41358
+ }
41087
41359
  return true;
41088
41360
  };
41361
+ const shouldEnableRuleByDefaultStatus = (options) => options.defaultEnabled !== false || options.hasExplicitOverride === true || options.includeTagDefaults && options.hasIncludedTags;
41089
41362
  const yieldToEventLoop = () => new Promise((resolve) => {
41090
41363
  setImmediate(resolve);
41091
41364
  });
41092
41365
  const createSecurityScanSession = (rootDirectory, options) => {
41093
41366
  const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
41094
41367
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
41368
+ const includedTags = options.includedTags ?? /* @__PURE__ */ new Set();
41095
41369
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
41096
41370
  const rule = entry.rule;
41097
41371
  const scan = rule.scan;
41098
41372
  if (typeof scan !== "function") return [];
41099
- if (rule.defaultEnabled === false) return [];
41100
- 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 [];
41101
41379
  return [{
41102
41380
  entry,
41103
41381
  scan,
@@ -42047,6 +42325,7 @@ const buildResolvedControls = (surface, userControls) => {
42047
42325
  for (const tag of includeTags) excludeTags.delete(tag);
42048
42326
  for (const tag of toStringSet(userControls?.excludeTags)) excludeTags.add(tag);
42049
42327
  return {
42328
+ includeFileContexts: toStringSet(userControls?.includeFileContexts),
42050
42329
  includeTags,
42051
42330
  excludeTags,
42052
42331
  includeCategories: toStringSet(userControls?.includeCategories),
@@ -42062,7 +42341,7 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
42062
42341
  if (resolved.includeRuleKeys.has(ruleKey)) return true;
42063
42342
  if (resolved.includeCategories.has(category)) return true;
42064
42343
  if (intersects(tags, resolved.includeTags)) return true;
42065
- if (diagnostic.fileContext !== void 0 && (surface === "score" || surface === "ciFailure")) return false;
42344
+ if (diagnostic.fileContext !== void 0 && (surface === "score" || surface === "ciFailure") && !resolved.includeFileContexts.has(diagnostic.fileContext)) return false;
42066
42345
  if (resolved.excludeRuleKeys.has(ruleKey)) return false;
42067
42346
  if (resolved.excludeCategories.has(category)) return false;
42068
42347
  if (intersects(tags, resolved.excludeTags)) return false;
@@ -42310,6 +42589,35 @@ const parseGithubViewerPermission = (stdout) => {
42310
42589
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
42311
42590
  };
42312
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
+ };
42313
42621
  const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
42314
42622
  /**
42315
42623
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
@@ -42512,6 +42820,45 @@ var Git = class Git extends Service()("react-doctor/Git") {
42512
42820
  githubViewerPermission,
42513
42821
  branchExists,
42514
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
+ },
42515
42862
  diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
42516
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." }) }));
42517
42864
  if (explicitBaseBranch !== void 0) {
@@ -42661,6 +43008,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42661
43008
  githubViewerPermission: () => succeed$2(snapshot.githubViewerPermission ?? null),
42662
43009
  branchExists: (_directory, branch) => succeed$2(snapshot.branchExists?.get(branch) ?? false),
42663
43010
  mergeBase: ({ ref }) => succeed$2(snapshot.mergeBase?.get(ref) ?? null),
43011
+ baselineDiffPlan: () => succeed$2(snapshot.baselineDiffPlan ?? null),
42664
43012
  diffSelection: () => succeed$2(snapshot.diffSelection ?? null),
42665
43013
  stagedFilePaths: () => succeed$2(snapshot.stagedFiles ?? []),
42666
43014
  showStagedContent: (_directory, relativePath) => succeed$2(snapshot.stagedContent?.get(relativePath) ?? null),
@@ -43029,8 +43377,9 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
43029
43377
  }
43030
43378
  return enabled;
43031
43379
  };
43032
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
43033
- 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);
43034
43383
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
43035
43384
  const jsPlugins = [];
43036
43385
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -43045,9 +43394,14 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
43045
43394
  if (rule.scan !== void 0) continue;
43046
43395
  if (customRulesOnly && registryEntry.originallyExternal) continue;
43047
43396
  if (rule.framework !== "global" && !rule.requires) continue;
43048
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen)) continue;
43397
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen, includedTags)) continue;
43049
43398
  const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
43050
- 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;
43051
43405
  const severity = resolveRuleSeverityOverride({
43052
43406
  ruleKey: registryEntry.key,
43053
43407
  category: rule.category
@@ -43056,7 +43410,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
43056
43410
  enabledReactDoctorRules[registryEntry.key] = severity;
43057
43411
  }
43058
43412
  const userPluginRules = {};
43059
- if (ruleSelection !== "sidecar") for (const userPlugin of userPlugins) {
43413
+ if (ruleSelection !== "sidecar" && !hasIncludedTags) for (const userPlugin of userPlugins) {
43060
43414
  Object.assign(userPluginRules, buildUserPluginRules(userPlugin, severityControls));
43061
43415
  jsPlugins.push(userPlugin.entry);
43062
43416
  }
@@ -43273,6 +43627,19 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
43273
43627
  }
43274
43628
  };
43275
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
+ };
43276
43643
  const bundledRequire$3 = createRequire(import.meta.url);
43277
43644
  const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
43278
43645
  const resolveChildNodeVersion = (nodeBinaryPath) => {
@@ -43296,6 +43663,13 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS$1 = [
43296
43663
  "oxlint-plugin-react-doctor/package.json",
43297
43664
  "eslint-plugin-react-hooks/package.json"
43298
43665
  ];
43666
+ const resolvePluginFingerprint = () => {
43667
+ try {
43668
+ return fingerprintFileContents(bundledRequire$3.resolve("oxlint-plugin-react-doctor"), 16);
43669
+ } catch {
43670
+ return "unresolved";
43671
+ }
43672
+ };
43299
43673
  const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
43300
43674
  const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
43301
43675
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS$1) try {
@@ -43305,6 +43679,7 @@ const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
43305
43679
  } catch {
43306
43680
  versions.push(`${specifier}=missing`);
43307
43681
  }
43682
+ versions.push(`oxlint-plugin-react-doctor#fingerprint=${resolvePluginFingerprint()}`);
43308
43683
  return versions;
43309
43684
  };
43310
43685
  const esmRequire = createRequire(import.meta.url);
@@ -43998,12 +44373,17 @@ const REACT_ERROR_BOUNDARY_MESSAGE = "This try/catch cannot catch errors thrown
43998
44373
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
43999
44374
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
44000
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`.";
44001
44377
  const REACT_COMPILER_GENERIC_MESSAGE = `${REACT_COMPILER_IMPACT}. ${REACT_COMPILER_ACTION}`;
44002
44378
  const buildReactCompilerMessage = (reasonSummary, action = REACT_COMPILER_ACTION) => {
44003
44379
  const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
44004
44380
  if (!normalizedSummary) return `${REACT_COMPILER_IMPACT}. ${action}`;
44005
44381
  return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${action}`;
44006
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
+ };
44007
44387
  const PLUGIN_CATEGORY_MAP = {
44008
44388
  react: "Bugs",
44009
44389
  "react-hooks": "Bugs",
@@ -44061,6 +44441,10 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
44061
44441
  message: REACT_ERROR_BOUNDARY_MESSAGE,
44062
44442
  help: reasonDetail || help
44063
44443
  };
44444
+ if (rule === "set-state-in-effect") return {
44445
+ message: buildSetStateInEffectMessage(reasonSummary.trim()),
44446
+ help: reasonDetail || help
44447
+ };
44064
44448
  return {
44065
44449
  message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
44066
44450
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
@@ -44771,7 +45155,7 @@ const reactHooksJsPluginDropNote = (error) => {
44771
45155
  * 6. always restore disable directives + clean up the temp dir
44772
45156
  */
44773
45157
  const runOxlint = async (options) => {
44774
- 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;
44775
45159
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
44776
45160
  const severityControls = buildRuleSeverityControls(userConfig);
44777
45161
  validateRuleRegistration();
@@ -44785,13 +45169,15 @@ const runOxlint = async (options) => {
44785
45169
  }
44786
45170
  const pluginPath = resolvePluginPath();
44787
45171
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
44788
- const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
45172
+ const userPlugins = includedTags.size > 0 ? [] : resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
44789
45173
  const buildConfig = (overrides) => createOxlintConfig({
44790
45174
  pluginPath,
44791
45175
  project,
44792
45176
  customRulesOnly,
44793
45177
  extendsPaths: overrides.extendsPaths,
44794
45178
  ignoredTags,
45179
+ includedTags,
45180
+ includeTagDefaults,
44795
45181
  serverAuthFunctionNames,
44796
45182
  severityControls,
44797
45183
  userPlugins,
@@ -45180,6 +45566,8 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
45180
45566
  respectInlineDisables: input.respectInlineDisables,
45181
45567
  adoptExistingLintConfig: input.adoptExistingLintConfig,
45182
45568
  ignoredTags: input.ignoredTags,
45569
+ includedTags: input.includedTags,
45570
+ includeTagDefaults: input.includeTagDefaults,
45183
45571
  userConfig: input.userConfig ?? null,
45184
45572
  configSourceDirectory: input.configSourceDirectory,
45185
45573
  onPartialFailure: (reason) => {
@@ -46028,7 +46416,9 @@ const runInspect = (input, hooks = {}) => gen(function* () {
46028
46416
  const securityScanFailedRef = yield* make$13(false);
46029
46417
  const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
46030
46418
  project,
46031
- ignoredTags: input.ignoredTags
46419
+ ignoredTags: input.ignoredTags,
46420
+ includedTags: input.includedTags,
46421
+ includeTagDefaults: input.includeTagDefaults
46032
46422
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
46033
46423
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
46034
46424
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
@@ -46119,6 +46509,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
46119
46509
  respectInlineDisables: input.respectInlineDisables,
46120
46510
  adoptExistingLintConfig: input.adoptExistingLintConfig,
46121
46511
  ignoredTags: input.ignoredTags,
46512
+ includedTags: input.includedTags,
46513
+ includeTagDefaults: input.includeTagDefaults,
46122
46514
  userConfig: resolvedConfig.config ?? void 0,
46123
46515
  configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
46124
46516
  onFileProgress: (scannedFileCount, totalFileCount) => {
@@ -46397,12 +46789,19 @@ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
46397
46789
  */
46398
46790
  const materializeSourceTree = (input) => gen(function* () {
46399
46791
  const materializedFiles = [];
46792
+ const unmaterializedFiles = [];
46400
46793
  const resolvedTempDirectory = path.resolve(input.tempDirectory);
46401
46794
  for (const relativePath of input.files) {
46402
46795
  const content = yield* input.readContent(relativePath).pipe(orElseSucceed(() => null));
46403
- if (content === null) continue;
46796
+ if (content === null) {
46797
+ unmaterializedFiles.push(relativePath);
46798
+ continue;
46799
+ }
46404
46800
  const candidateTargetPath = path.resolve(resolvedTempDirectory, relativePath);
46405
- if (!isPathInsideDirectory(candidateTargetPath, resolvedTempDirectory)) continue;
46801
+ if (!isPathInsideDirectory(candidateTargetPath, resolvedTempDirectory)) {
46802
+ unmaterializedFiles.push(relativePath);
46803
+ continue;
46804
+ }
46406
46805
  yield* sync(() => {
46407
46806
  fs.mkdirSync(path.dirname(candidateTargetPath), { recursive: true });
46408
46807
  fs.writeFileSync(candidateTargetPath, content);
@@ -46419,6 +46818,7 @@ const materializeSourceTree = (input) => gen(function* () {
46419
46818
  return {
46420
46819
  tempDirectory: input.tempDirectory,
46421
46820
  materializedFiles,
46821
+ unmaterializedFiles,
46422
46822
  cleanup: () => {
46423
46823
  try {
46424
46824
  fs.rmSync(input.tempDirectory, {
@@ -46688,54 +47088,80 @@ const buildSkippedChecks = (input) => {
46688
47088
  skippedCheckReasons
46689
47089
  };
46690
47090
  };
46691
- 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) => {
46692
47094
  const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;
46693
- const snippet = lineText === null || diagnostic.matchByOccurrence ? "" : createHash("sha1").update(lineText.trim()).digest("hex");
46694
- return `${diagnostic.filePath}\u0000${ruleKey}\u0000${snippet}`;
46695
- };
46696
- /**
46697
- * Diffs a head scan against a base scan to isolate the diagnostics a change
46698
- * introduced (and count the ones it resolved). Matching is a multiset over a
46699
- * position-independent fingerprint — `(filePath, plugin/rule, hash(flagged
46700
- * line text))` — so inserting lines above an existing issue doesn't make it
46701
- * look new, while a genuinely new occurrence (new line text, or one more of
46702
- * the same) surfaces. Identical repeated findings are matched by count.
46703
- *
46704
- * Diagnostics carrying `matchByOccurrence` (resolved at diagnostic creation:
46705
- * every Accessibility-category finding, plus rules opting in via their
46706
- * `matchByOccurrence` metadata flag) drop the line-text snippet and match by
46707
- * `(filePath, plugin/rule)` occurrence count alone. Their identity is the
46708
- * flagged element, not the line's text, so editing the line (reindentation,
46709
- * prettier reflow, collapsing a multi-line JSX element) doesn't reclassify a
46710
- * pre-existing finding as new — while one MORE occurrence of the same rule in
46711
- * the file still surfaces. Expression-level rules keep the line-text snippet:
46712
- * there the flagged expression IS the finding, so changed text means new +
46713
- * fixed.
46714
- *
46715
- * v1 limitation: the fingerprint keys on the head-relative `filePath`, and base
46716
- * content is read at that same path. A file renamed by the change therefore has
46717
- * no base match, so its pre-existing findings are reported as new. This
46718
- * over-reports (never hides a real issue) and is rare; rename-aware base
46719
- * 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.
46720
47131
  */
46721
47132
  const computeDiagnosticDelta = (input) => {
46722
- const unmatchedBaseByFingerprint = /* @__PURE__ */ new Map();
46723
- for (const diagnostic of input.baseDiagnostics) {
46724
- const key = fingerprintDiagnostic(diagnostic, input.readBaseLine(diagnostic.filePath, diagnostic.line));
46725
- unmatchedBaseByFingerprint.set(key, (unmatchedBaseByFingerprint.get(key) ?? 0) + 1);
46726
- }
46727
- const newDiagnostics = [];
46728
- for (const diagnostic of input.headDiagnostics) {
46729
- const key = fingerprintDiagnostic(diagnostic, input.readHeadLine(diagnostic.filePath, diagnostic.line));
46730
- const availableMatches = unmatchedBaseByFingerprint.get(key) ?? 0;
46731
- if (availableMatches > 0) unmatchedBaseByFingerprint.set(key, availableMatches - 1);
46732
- else newDiagnostics.push(diagnostic);
46733
- }
46734
- let fixedCount = 0;
46735
- 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
+ });
46736
47161
  return {
46737
- newDiagnostics,
46738
- fixedCount
47162
+ newDiagnostics: input.headDiagnostics.filter((_diagnostic, diagnosticIndex) => !matchedHeadDiagnosticIndexes.has(diagnosticIndex)),
47163
+ fixedCount: input.baseDiagnostics.length - matchedBaseDiagnosticIndexes.size,
47164
+ crossFileMatchCount
46739
47165
  };
46740
47166
  };
46741
47167
  /**
@@ -46767,6 +47193,12 @@ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runProm
46767
47193
  };
46768
47194
  }).pipe(provide(Git.layerNode)));
46769
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)));
46770
47202
  /**
46771
47203
  * Programmatic façade over `Git.changedLineRanges` (the `lines` scope). Diffs
46772
47204
  * `files` with `--unified=0` against `baseRef` (or the index when `cached`),
@@ -47223,7 +47655,7 @@ const makeNoopConsole = () => ({
47223
47655
  });
47224
47656
  //#endregion
47225
47657
  //#region src/cli/utils/version.ts
47226
- const VERSION = "0.8.0";
47658
+ const VERSION = "0.8.1-dev.0b0b5ac";
47227
47659
  //#endregion
47228
47660
  //#region src/cli/utils/json-mode.ts
47229
47661
  let context = null;
@@ -47310,7 +47742,11 @@ const getRunId = () => {
47310
47742
  };
47311
47743
  //#endregion
47312
47744
  //#region src/cli/utils/build-run-context.ts
47313
- const ROOT_SUBCOMMANDS = new Set(["install", "setup"]);
47745
+ const ROOT_SUBCOMMANDS = new Set([
47746
+ "design",
47747
+ "install",
47748
+ "setup"
47749
+ ]);
47314
47750
  const detectInvokedVia = () => {
47315
47751
  const userAgent = process.env.npm_config_user_agent;
47316
47752
  if (!userAgent) return "unknown";
@@ -47394,7 +47830,9 @@ const buildSentryProjectContext = (projectInfo) => ({
47394
47830
  reactMajorVersion: projectInfo.reactMajorVersion,
47395
47831
  hasTypeScript: projectInfo.hasTypeScript,
47396
47832
  hasReactCompiler: projectInfo.hasReactCompiler,
47397
- hasTanStackQuery: projectInfo.hasTanStackQuery,
47833
+ tanstackQueryVersion: projectInfo.tanstackQueryVersion,
47834
+ mobxVersion: projectInfo.mobxVersion,
47835
+ styledComponentsVersion: projectInfo.styledComponentsVersion,
47398
47836
  tailwindVersion: projectInfo.tailwindVersion,
47399
47837
  zodVersion: projectInfo.zodVersion,
47400
47838
  preactVersion: projectInfo.preactVersion,
@@ -47575,13 +48013,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
47575
48013
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
47576
48014
  * standard `SENTRY_RELEASE` override.
47577
48015
  */
47578
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.0`;
48016
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.1-dev.0b0b5ac`;
47579
48017
  /**
47580
48018
  * Deployment environment shown in Sentry's environment filter. Defaults to
47581
48019
  * `production` for tagged releases and `development` for dev/unbuilt versions,
47582
48020
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
47583
48021
  */
47584
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.0") ? "development" : "production");
48022
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.1-dev.0b0b5ac") ? "development" : "production");
47585
48023
  /**
47586
48024
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
47587
48025
  * (set to `0` to disable tracing) and falls back to
@@ -59907,6 +60345,8 @@ const buildScanResultCacheKey = (input) => {
59907
60345
  warnings: input.options.warnings,
59908
60346
  adoptExistingLintConfig: input.options.adoptExistingLintConfig,
59909
60347
  ignoredTags: [...input.options.ignoredTags].sort(),
60348
+ includedTags: [...input.options.includedTags].sort(),
60349
+ includeTagDefaults: input.options.includeTagDefaults,
59910
60350
  concurrency: input.options.concurrency,
59911
60351
  lintBatchOrdering: resolveLintBatchOrdering(),
59912
60352
  baselineRef: input.options.baseline?.ref,
@@ -60204,6 +60644,7 @@ const buildOutcomeAttributes = (input) => {
60204
60644
  new: summary.totalDiagnosticCount,
60205
60645
  fixed: result.baselineDelta.fixedCount,
60206
60646
  baseTotal: result.baselineDelta.baseTotalCount,
60647
+ crossFileMatches: result.baselineDelta.crossFileMatchCount ?? null,
60207
60648
  degraded: false
60208
60649
  }));
60209
60650
  else if (input.gateExempt) attributes["baseline.degraded"] = true;
@@ -60317,28 +60758,49 @@ const diagnosticIntersectsLineRanges = (diagnostic, lineRanges) => {
60317
60758
  return lineRanges.some(([rangeStartLine, rangeEndLine]) => diagnostic.line <= rangeEndLine && diagnosticEndLine >= rangeStartLine);
60318
60759
  };
60319
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
60320
60765
  //#region src/cli/utils/materialize-baseline-files.ts
60321
60766
  /**
60322
- * Materializes the `ref` version of `files` into `tempDirectory` (via
60323
- * `git show <ref>:<path>`), mirroring the project layout + head's config files
60324
- * so the base lints under the same rules as head. Reuses the shared,
60325
- * zip-slip-guarded `materializeSourceTree`. Per-file read misses (e.g. a file
60326
- * the PR added, absent at base) are skipped, which is exactly what we want:
60327
- * 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.
60328
60773
  */
60329
60774
  const materializeBaselineFiles = (input) => runPromise(gen(function* () {
60330
60775
  const git = yield* Git;
60331
- return yield* materializeSourceTree({
60776
+ const diffPlan = yield* git.baselineDiffPlan({
60777
+ directory: input.directory,
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({
60332
60786
  directory: input.directory,
60333
- files: input.files,
60787
+ files,
60334
60788
  tempDirectory: input.tempDirectory,
60335
60789
  readContent: (relativePath) => git.showRefContent({
60336
60790
  directory: input.directory,
60337
60791
  ref: input.ref,
60338
60792
  relativePath,
60339
60793
  options: { maxBufferBytes: GIT_SHOW_MAX_BUFFER_BYTES }
60340
- })
60794
+ }).pipe(map$3((content) => content !== null && isMaterializableGitSource(content) ? content : null))
60341
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
+ };
60342
60804
  }).pipe(provide(Git.layerNode)));
60343
60805
  /**
60344
60806
  * Resolves the commit a baseline scan should read base content from — the
@@ -60361,8 +60823,8 @@ const resolveMergeBaseRef = async (directory, ref) => {
60361
60823
  //#region src/cli/utils/read-source-line.ts
60362
60824
  /**
60363
60825
  * Builds a cached `(filePath, line) -> source text` reader rooted at
60364
- * `rootDirectory`, used to fingerprint a diagnostic by the content of its
60365
- * 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
60366
60828
  * `createNodeReadFileLinesSync` with per-file memoization + 1-indexed line
60367
60829
  * lookup; unreadable files / out-of-range lines return `null`.
60368
60830
  */
@@ -60377,10 +60839,342 @@ const createSourceLineReader = (rootDirectory) => {
60377
60839
  };
60378
60840
  };
60379
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
60380
61174
  //#region src/cli/utils/build-no-score-message.ts
60381
61175
  const ENTERPRISE_CONTACT_HINT = `Want something custom to your company? Contact us at ${ENTERPRISE_CONTACT_URL}.`;
60382
- const buildNoScoreMessage = (isScoreDisabled) => {
60383
- 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}`;
60384
61178
  };
60385
61179
  //#endregion
60386
61180
  //#region src/cli/utils/render-agent-guidance.ts
@@ -61505,40 +62299,47 @@ const buildChangedLineMatcher = (directory, changedLineRanges) => {
61505
62299
  return diagnosticIntersectsLineRanges(diagnostic, ranges);
61506
62300
  };
61507
62301
  };
61508
- const buildIgnoredTags = (userConfig) => {
62302
+ const buildIgnoredTags = (userConfig, includedTags) => {
61509
62303
  const tags = /* @__PURE__ */ new Set();
61510
62304
  if (userConfig?.ignore?.tags) for (const tag of userConfig.ignore.tags) tags.add(tag);
62305
+ for (const tag of includedTags) tags.delete(tag);
61511
62306
  return tags;
61512
62307
  };
61513
- const mergeInspectOptions = (inputOptions, userConfig) => ({
61514
- lint: inputOptions.lint ?? userConfig?.lint ?? true,
61515
- deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
61516
- supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
61517
- verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
61518
- outputDirectory: inputOptions.outputDirectory || null,
61519
- scoreOnly: inputOptions.scoreOnly ?? false,
61520
- noScore: inputOptions.noScore ?? userConfig?.noScore ?? false,
61521
- isCi: inputOptions.isCi ?? false,
61522
- isCiOrCodingAgentEnvironment: isCiOrCodingAgentEnvironment(),
61523
- isNonInteractiveEnvironment: isNonInteractiveEnvironment(),
61524
- silent: inputOptions.silent ?? false,
61525
- includePaths: inputOptions.includePaths ?? [],
61526
- customRulesOnly: userConfig?.customRulesOnly ?? false,
61527
- share: userConfig?.share ?? true,
61528
- respectInlineDisables: inputOptions.respectInlineDisables ?? userConfig?.respectInlineDisables ?? true,
61529
- warnings: inputOptions.warnings ?? userConfig?.warnings ?? true,
61530
- categoryFilters: new Set(resolveCliCategories(inputOptions.categoryFilters) ?? []),
61531
- adoptExistingLintConfig: userConfig?.adoptExistingLintConfig ?? true,
61532
- ignoredTags: buildIgnoredTags(userConfig),
61533
- outputSurface: inputOptions.outputSurface ?? "cli",
61534
- suppressRendering: inputOptions.suppressRendering ?? false,
61535
- concurrentScan: inputOptions.concurrentScan ?? false,
61536
- concurrency: inputOptions.concurrency,
61537
- maxDurationMs: inputOptions.maxDurationMs ?? null,
61538
- baseline: inputOptions.baseline ?? null,
61539
- changedLineRanges: inputOptions.changedLineRanges ?? null,
61540
- supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false
61541
- });
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
+ };
61542
62343
  const deriveScope = (options) => {
61543
62344
  if (options.baseline) return "changed";
61544
62345
  if (options.changedLineRanges !== null) return "lines";
@@ -61620,6 +62421,8 @@ const runBaselineComparison = async (params) => {
61620
62421
  directory: params.directory,
61621
62422
  ref: params.baselineRef,
61622
62423
  files: params.options.includePaths,
62424
+ baseFiles: params.baseFiles,
62425
+ headFiles: params.headFiles,
61623
62426
  tempDirectory
61624
62427
  }).catch((error) => {
61625
62428
  rmSync(tempDirectory, {
@@ -61628,7 +62431,24 @@ const runBaselineComparison = async (params) => {
61628
62431
  });
61629
62432
  throw error;
61630
62433
  });
62434
+ if (snapshot === null) {
62435
+ rmSync(tempDirectory, {
62436
+ recursive: true,
62437
+ force: true
62438
+ });
62439
+ return null;
62440
+ }
61631
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;
61632
62452
  const baseLayers = buildRuntimeLayers({
61633
62453
  directory: snapshot.tempDirectory,
61634
62454
  hasConfigOverride: true,
@@ -61644,12 +62464,14 @@ const runBaselineComparison = async (params) => {
61644
62464
  });
61645
62465
  const baseOutput = await runPromise(restoreLegacyThrow(runInspect({
61646
62466
  directory: snapshot.tempDirectory,
61647
- includePaths: params.options.includePaths,
62467
+ includePaths: snapshot.materializedFiles,
61648
62468
  customRulesOnly: params.options.customRulesOnly,
61649
62469
  respectInlineDisables: params.options.respectInlineDisables,
61650
62470
  warnings: params.options.warnings,
61651
62471
  adoptExistingLintConfig: params.options.adoptExistingLintConfig,
61652
62472
  ignoredTags: params.options.ignoredTags,
62473
+ includedTags: params.options.includedTags,
62474
+ includeTagDefaults: params.options.includeTagDefaults,
61653
62475
  nodeBinaryPath: params.resolvedNodeBinaryPath ?? void 0,
61654
62476
  runDeadCode: false,
61655
62477
  isCi: params.options.isCi,
@@ -61661,18 +62483,22 @@ const runBaselineComparison = async (params) => {
61661
62483
  deadlineEpochMs: params.deadlineEpochMs ?? void 0
61662
62484
  }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))));
61663
62485
  if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
62486
+ const hasUnscannedUntrackedSourceFiles = filterSourceFiles(snapshot.untrackedFiles.map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
61664
62487
  const delta = computeDiagnosticDelta({
61665
62488
  headDiagnostics: params.headDiagnostics,
61666
62489
  baseDiagnostics: baseOutput.diagnostics,
61667
62490
  readHeadLine: createSourceLineReader(params.directory),
61668
- readBaseLine: createSourceLineReader(snapshot.tempDirectory)
62491
+ readBaseLine: createSourceLineReader(snapshot.tempDirectory),
62492
+ readHeadEvidence: createDiagnosticEvidenceReader(params.directory, { resolveForwardedHandlers: true }),
62493
+ readBaseEvidence: createDiagnosticEvidenceReader(snapshot.tempDirectory)
61669
62494
  });
61670
62495
  return {
61671
62496
  displayDiagnostics: delta.newDiagnostics,
61672
62497
  baselineDelta: {
61673
62498
  baseRef: params.baselineRef,
61674
- fixedCount: delta.fixedCount,
61675
- baseTotalCount: baseOutput.diagnostics.length
62499
+ fixedCount: hasUnscannedUntrackedSourceFiles ? 0 : delta.fixedCount,
62500
+ baseTotalCount: baseOutput.diagnostics.length,
62501
+ crossFileMatchCount: delta.crossFileMatchCount
61676
62502
  }
61677
62503
  };
61678
62504
  } finally {
@@ -61739,6 +62565,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
61739
62565
  warnings: options.warnings,
61740
62566
  adoptExistingLintConfig: options.adoptExistingLintConfig,
61741
62567
  ignoredTags: options.ignoredTags,
62568
+ includedTags: options.includedTags,
62569
+ includeTagDefaults: options.includeTagDefaults,
61742
62570
  nodeBinaryPath: resolvedNodeBinaryPath ?? void 0,
61743
62571
  runDeadCode: options.deadCode,
61744
62572
  isCi: options.isCi,
@@ -61779,6 +62607,9 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
61779
62607
  headDiagnostics: output.diagnostics,
61780
62608
  resolvedNodeBinaryPath,
61781
62609
  baselineRef: options.baseline.ref,
62610
+ baseFiles: options.baseline.baseFiles,
62611
+ headFiles: options.baseline.headFiles,
62612
+ headAnalyzedFiles: output.analyzedFiles,
61782
62613
  deadlineEpochMs
61783
62614
  });
61784
62615
  if (comparison) {
@@ -61924,7 +62755,7 @@ const finalizeAndRender = (input) => gen(function* () {
61924
62755
  securityScanFailed
61925
62756
  });
61926
62757
  const hasSkippedChecks = skippedChecks.length > 0;
61927
- const noScoreMessage = buildNoScoreMessage(options.noScore);
62758
+ const noScoreMessage = buildNoScoreMessage(options.noScore, options.scoreDisabledMessage);
61928
62759
  const buildResult = () => ({
61929
62760
  diagnostics: [...diagnostics],
61930
62761
  score,
@@ -64029,21 +64860,25 @@ const resolveParallelFlag = (parallel) => parallel === false ? 1 : void 0;
64029
64860
  */
64030
64861
  const resolveCliInspectOptions = (flags, userConfig) => {
64031
64862
  const wantsWarningGate = pickBlockingLevel(flags, userConfig) === "warning";
64863
+ const isDesignScan = flags.design === true;
64032
64864
  return {
64033
64865
  lint: flags.lint,
64034
- deadCode: flags.deadCode,
64035
- supplyChain: flags.supplyChain,
64866
+ deadCode: isDesignScan ? false : flags.deadCode,
64867
+ supplyChain: isDesignScan ? false : flags.supplyChain,
64036
64868
  verbose: flags.verbose,
64037
64869
  outputDirectory: flags.outputDir,
64038
64870
  respectInlineDisables: flags.respectInlineDisables === false ? false : void 0,
64039
64871
  warnings: wantsWarningGate ? true : flags.warnings,
64040
64872
  scoreOnly: flags.score === true,
64041
- noScore: flags.score === false || flags.telemetry === false ? true : void 0,
64873
+ noScore: isDesignScan || flags.score === false || flags.telemetry === false ? true : void 0,
64042
64874
  isCi: isCiEnvironment(),
64043
64875
  silent: Boolean(flags.json),
64044
64876
  concurrency: resolveParallelFlag(flags.parallel),
64045
64877
  maxDurationMs: resolveMaxDurationFlag(flags.maxDuration),
64046
- 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
64047
64882
  };
64048
64883
  };
64049
64884
  //#endregion
@@ -64063,20 +64898,25 @@ const resolveProjectRelativeDirectory = (rootDirectory, projectDirectory) => {
64063
64898
  return relativeProjectDirectory;
64064
64899
  };
64065
64900
  //#endregion
64066
- //#region src/cli/utils/resolve-project-diff-include-paths.ts
64067
- const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
64901
+ //#region src/cli/utils/resolve-project-source-file-paths.ts
64902
+ const resolveProjectSourceFilePaths = (rootDirectory, projectDirectory, filePaths) => {
64068
64903
  const relativeProjectDirectory = resolveProjectRelativeDirectory(rootDirectory, projectDirectory);
64069
64904
  if (relativeProjectDirectory === null) return [];
64070
- const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);
64071
- if (relativeProjectDirectory.length === 0) return changedSourceFiles;
64905
+ const sourceFilePaths = filterSourceFiles([...filePaths]);
64906
+ if (relativeProjectDirectory.length === 0) return sourceFilePaths;
64072
64907
  const projectPrefix = `${relativeProjectDirectory}/`;
64073
- return changedSourceFiles.flatMap((filePath) => {
64908
+ return sourceFilePaths.flatMap((filePath) => {
64074
64909
  const normalizedFilePath = toForwardSlashes(filePath);
64075
64910
  if (!normalizedFilePath.startsWith(projectPrefix)) return [];
64076
64911
  const projectRelativePath = normalizedFilePath.slice(projectPrefix.length);
64077
64912
  return projectRelativePath.length > 0 ? [projectRelativePath] : [];
64078
64913
  });
64079
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
+ };
64080
64920
  /**
64081
64921
  * Re-keys repo-relative changed line ranges (from `getChangedLineRanges`) to
64082
64922
  * the scanned project, mirroring `resolveProjectDiffIncludePaths`'s boundary
@@ -64456,7 +65296,7 @@ const maybeMigrateLegacyConfig = async (requestedDirectory, { isQuiet, isStaged
64456
65296
  if (!(!isQuiet && !isStaged && process.stdout.isTTY === true && !isCiOrCodingAgentEnvironment())) return;
64457
65297
  await runProjectMigrations(requestedDirectory);
64458
65298
  };
64459
- const inspectAction = async (directory, flags) => {
65299
+ const inspectAction = async (directory, flags, invocationCommand = "inspect") => {
64460
65300
  const isScoreOnly = Boolean(flags.score);
64461
65301
  const isJsonMode = Boolean(flags.json);
64462
65302
  const isQuiet = isScoreOnly || isJsonMode;
@@ -64470,7 +65310,7 @@ const inspectAction = async (directory, flags) => {
64470
65310
  });
64471
65311
  if (flags.jsonOut) recordCount(METRIC.jsonOutUsed, 1);
64472
65312
  }
64473
- recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
65313
+ recordCount(METRIC.cliInvoked, 1, { command: invocationCommand });
64474
65314
  try {
64475
65315
  validateModeFlags(flags);
64476
65316
  if (flags.staged) setJsonReportMode("staged");
@@ -64615,6 +65455,7 @@ const inspectAction = async (directory, flags) => {
64615
65455
  const isDiffMode = scope !== "full";
64616
65456
  const comparisonBaseRef = isDiffMode && diffInfo && !diffInfo.isCurrentChanges ? diffInfo.baseSha ? await resolveMergeBaseRef(resolvedDirectory, diffInfo.baseSha) : diffInfo.diffBaseRef ?? await resolveMergeBaseRef(resolvedDirectory, diffInfo.baseBranch) : null;
64617
65457
  const baselineRef = scope === "changed" ? comparisonBaseRef : null;
65458
+ const baselineDiffPlan = baselineRef === null ? null : await getBaselineDiffPlan(resolvedDirectory, baselineRef);
64618
65459
  const linesBaseRef = diffInfo?.isCurrentChanges ? "HEAD" : comparisonBaseRef;
64619
65460
  const changedLineRanges = scope === "lines" && diffInfo !== null && (diffInfo.isCurrentChanges || linesBaseRef !== null) && diffInfo !== null ? await getChangedLineRanges({
64620
65461
  directory: resolvedDirectory,
@@ -64645,10 +65486,13 @@ const inspectAction = async (directory, flags) => {
64645
65486
  const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
64646
65487
  let includePaths;
64647
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);
64648
65491
  if (isDiffMode) {
64649
65492
  const changedSourceFiles = diffInfo === null ? [] : resolveProjectDiffIncludePaths(resolvedDirectory, scanDirectory, diffInfo);
64650
65493
  supplyChainManifestChanged = supplyChainEnabled && diffInfo !== null && projectManifestChanged(resolvedDirectory, scanDirectory, diffInfo);
64651
- if (changedSourceFiles.length === 0 && !supplyChainManifestChanged) {
65494
+ const hasBaselineOnlyFiles = (projectBaselineBaseFiles?.length ?? 0) > 0;
65495
+ if (changedSourceFiles.length === 0 && !supplyChainManifestChanged && !hasBaselineOnlyFiles) {
64652
65496
  if (!isQuiet) {
64653
65497
  cliLogger.dim(`No changed source files in ${scanDirectory}, skipping.`);
64654
65498
  cliLogger.break();
@@ -64656,6 +65500,7 @@ const inspectAction = async (directory, flags) => {
64656
65500
  return null;
64657
65501
  }
64658
65502
  includePaths = [...changedSourceFiles];
65503
+ if (includePaths.length === 0 && hasBaselineOnlyFiles) includePaths.push(...projectBaselineBaseFiles ?? []);
64659
65504
  if (supplyChainManifestChanged) includePaths.push("package.json");
64660
65505
  }
64661
65506
  if (!isQuiet && !isMultiProject) cliLogger.dim(" ");
@@ -64667,7 +65512,11 @@ const inspectAction = async (directory, flags) => {
64667
65512
  configSourceDirectory: projectConfigSourceDirectory ?? void 0,
64668
65513
  suppressRendering: isMultiProject,
64669
65514
  concurrentScan: isMultiProject,
64670
- 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,
64671
65520
  changedLineRanges: scope === "lines" && changedLineRanges !== null ? resolveProjectChangedLineRanges(resolvedDirectory, scanDirectory, changedLineRanges) : void 0,
64672
65521
  supplyChainManifestChanged
64673
65522
  });
@@ -64773,6 +65622,13 @@ const inspectAction = async (directory, flags) => {
64773
65622
  }
64774
65623
  };
64775
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
64776
65632
  //#region src/cli/commands/install.ts
64777
65633
  const installAction = async (options, command) => {
64778
65634
  recordCount(METRIC.cliInvoked, 1, { command: "install" });
@@ -65609,6 +66465,7 @@ const INSTALL_FLAG_SPEC = {
65609
66465
  shortOptionsWithRequiredValues: new Set(["-c"])
65610
66466
  };
65611
66467
  const COMMAND_FLAG_SPECS = new Map([
66468
+ ["design", ROOT_FLAG_SPEC],
65612
66469
  ["install", INSTALL_FLAG_SPEC],
65613
66470
  ["setup", INSTALL_FLAG_SPEC],
65614
66471
  ["version", {
@@ -65765,6 +66622,7 @@ ${formatExampleLines([
65765
66622
  ["react-doctor --scope changed --base main", "scan only new issues vs. main"],
65766
66623
  ["react-doctor --project modules/a,modules/b", "score each module separately (names or paths)"],
65767
66624
  ["react-doctor --staged", "scan staged files (pre-commit hook)"],
66625
+ ["react-doctor design", "run the focused UI design audit"],
65768
66626
  ["react-doctor --category Security", "show only one diagnostic category"],
65769
66627
  ["react-doctor --blocking warning", "fail CI on warnings too (default: error)"],
65770
66628
  ["react-doctor --json > report.json", "write a machine-readable report"],
@@ -65798,6 +66656,20 @@ ${highlighter.dim("Managing CI:")}
65798
66656
  ${highlighter.dim("Learn more:")}
65799
66657
  ${highlighter.info(CANONICAL_GITHUB_URL)}
65800
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
+ `;
65801
66673
  const renderCiHelpEpilog = () => `
65802
66674
  ${highlighter.dim("Examples:")}
65803
66675
  ${formatExampleLines([
@@ -65819,6 +66691,7 @@ ${highlighter.dim("Learn more:")}
65819
66691
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
65820
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);
65821
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()));
65822
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));
65823
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);
65824
66697
  const providerOption = ["--provider <name>", "ci provider: github-actions or gitlab-ci (auto-detected by default)"];
@@ -65867,4 +66740,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
65867
66740
  export {};
65868
66741
 
65869
66742
  //# sourceMappingURL=cli.js.map
65870
- //# debugId=5cb8a881-e8fc-5561-8b89-ad2c8357acbc
66743
+ //# debugId=a9af63ed-7b82-56d3-8549-274ab7f23579