react-doctor 0.7.0 → 0.7.1-dev.5809083

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]="96cac03b-44a6-5d88-a5c9-697a5e8cf45e")}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]="69a45de3-fe65-53dc-aed2-17f9cea22159")}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";
@@ -22405,6 +22405,7 @@ var Diagnostic = class extends Class("Diagnostic")({
22405
22405
  endLine: optional(Number$1),
22406
22406
  endColumn: optional(Number$1),
22407
22407
  category: String$1,
22408
+ matchByOccurrence: optional(Boolean$1),
22408
22409
  fileContext: optional(Literals(["test", "story"])),
22409
22410
  suppressionHint: optional(String$1),
22410
22411
  relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
@@ -22475,6 +22476,16 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
22475
22476
  * successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
22476
22477
  */
22477
22478
  baselineDegraded: optional(Boolean$1),
22479
+ /**
22480
+ * Whether any scanned project resolved a React-compatible runtime (React
22481
+ * or Preact). `false` means every React-runtime rule family was gated off,
22482
+ * so an empty `diagnostics` array is vacuous — NOT the same as a clean
22483
+ * React scan. Consumers gating on the report (CI, verifiers, hooks) should
22484
+ * treat `reactDetected === false` as "wrong scan target", not "all clear".
22485
+ * Absent when nothing was scanned (`projects` is empty), on error reports,
22486
+ * and on reports from older CLI versions.
22487
+ */
22488
+ reactDetected: optional(Boolean$1),
22478
22489
  diff: NullOr(JsonReportDiffInfo),
22479
22490
  projects: ArraySchema(JsonReportProjectEntry),
22480
22491
  diagnostics: ArraySchema(Diagnostic),
@@ -22500,6 +22511,8 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
22500
22511
  ok: Boolean$1,
22501
22512
  directory: String$1,
22502
22513
  mode: JsonReportMode,
22514
+ /** See `JsonReportV1.reactDetected`. */
22515
+ reactDetected: optional(Boolean$1),
22503
22516
  diff: NullOr(JsonReportDiffInfo),
22504
22517
  baseline: JsonReportBaseline,
22505
22518
  projects: ArraySchema(JsonReportProjectEntry),
@@ -36108,6 +36121,7 @@ const DIAGNOSTIC_CATEGORY_BUCKETS = [
36108
36121
  "Accessibility",
36109
36122
  "Maintainability"
36110
36123
  ];
36124
+ const OCCURRENCE_MATCHED_CATEGORIES = new Set(["Accessibility"]);
36111
36125
  const APP_ONLY_RULE_KEYS = new Set([
36112
36126
  "react-hooks-js/static-components",
36113
36127
  "react-doctor/no-render-prop-children",
@@ -36130,7 +36144,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36130
36144
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36131
36145
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36132
36146
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36133
- const CORE_PACKAGE_VERSION = "0.7.0";
36147
+ const CORE_PACKAGE_VERSION = "0.7.1";
36134
36148
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36135
36149
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36136
36150
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -40237,10 +40251,17 @@ function* collectSecurityScanFiles(rootDirectory) {
40237
40251
  }
40238
40252
  }
40239
40253
  }
40254
+ /**
40255
+ * Whether project discovery resolved a React-compatible runtime (React or
40256
+ * Preact, which ships the same hooks + JSX model). This is the same
40257
+ * predicate that grants the `react` capability, so `false` means every
40258
+ * React-runtime rule family was gated off for the project.
40259
+ */
40260
+ const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
40240
40261
  const buildCapabilities = (project) => {
40241
40262
  const capabilities = /* @__PURE__ */ new Set();
40242
40263
  capabilities.add(project.framework);
40243
- if (project.reactVersion !== null || project.preactVersion !== null) capabilities.add("react");
40264
+ if (hasReactRuntime(project)) capabilities.add("react");
40244
40265
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
40245
40266
  if (project.expoVersion !== null) capabilities.add("expo");
40246
40267
  if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
@@ -41786,7 +41807,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
41786
41807
  })),
41787
41808
  showStagedContent: (directory, relativePath, options) => runCommand({
41788
41809
  command: "git",
41789
- args: ["show", `:${relativePath}`],
41810
+ args: ["show", `:./${relativePath}`],
41790
41811
  directory,
41791
41812
  maxStdoutBytes: options?.maxBufferBytes
41792
41813
  }).pipe(map$3((result) => result.status === 0 ? result.stdout : null)),
@@ -43092,6 +43113,7 @@ const parseRuleCode = (code) => {
43092
43113
  };
43093
43114
  };
43094
43115
  const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
43116
+ const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(reactDoctorPlugin.rules[rule]?.matchByOccurrence);
43095
43117
  /**
43096
43118
  * Maps oxlint's non-primary labels (`labels[1..]`) into related source
43097
43119
  * locations. Editors surface these as a diagnostic's
@@ -43155,6 +43177,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43155
43177
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
43156
43178
  const primarySpan = primaryLabel?.span;
43157
43179
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
43180
+ const category = resolveDiagnosticCategory(plugin, rule);
43158
43181
  return {
43159
43182
  filePath: normalizedFilePath,
43160
43183
  plugin,
@@ -43170,7 +43193,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43170
43193
  offset: primarySpan.offset,
43171
43194
  length: primarySpan.length
43172
43195
  } : {},
43173
- category: resolveDiagnosticCategory(plugin, rule),
43196
+ category,
43197
+ ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
43174
43198
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
43175
43199
  };
43176
43200
  });
@@ -45355,6 +45379,7 @@ const buildJsonReport = (input) => {
45355
45379
  const worstScoredProject = findWorstScoredProject(projects);
45356
45380
  const summary = summarizeDiagnostics(flattenedDiagnostics, worstScoredProject?.score?.score ?? null, worstScoredProject?.score?.label ?? null);
45357
45381
  const shared = {
45382
+ ...input.scans.length > 0 ? { reactDetected: input.scans.some((scan) => hasReactRuntime(scan.result.project)) } : {},
45358
45383
  version: input.version,
45359
45384
  ok: true,
45360
45385
  directory: input.directory,
@@ -45405,7 +45430,7 @@ const buildSkippedChecks = (input) => {
45405
45430
  };
45406
45431
  const fingerprintDiagnostic = (diagnostic, lineText) => {
45407
45432
  const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;
45408
- const snippet = lineText === null ? "" : createHash("sha1").update(lineText.trim()).digest("hex");
45433
+ const snippet = lineText === null || diagnostic.matchByOccurrence ? "" : createHash("sha1").update(lineText.trim()).digest("hex");
45409
45434
  return `${diagnostic.filePath}\u0000${ruleKey}\u0000${snippet}`;
45410
45435
  };
45411
45436
  /**
@@ -45416,6 +45441,17 @@ const fingerprintDiagnostic = (diagnostic, lineText) => {
45416
45441
  * look new, while a genuinely new occurrence (new line text, or one more of
45417
45442
  * the same) surfaces. Identical repeated findings are matched by count.
45418
45443
  *
45444
+ * Diagnostics carrying `matchByOccurrence` (resolved at diagnostic creation:
45445
+ * every Accessibility-category finding, plus rules opting in via their
45446
+ * `matchByOccurrence` metadata flag) drop the line-text snippet and match by
45447
+ * `(filePath, plugin/rule)` occurrence count alone. Their identity is the
45448
+ * flagged element, not the line's text, so editing the line (reindentation,
45449
+ * prettier reflow, collapsing a multi-line JSX element) doesn't reclassify a
45450
+ * pre-existing finding as new — while one MORE occurrence of the same rule in
45451
+ * the file still surfaces. Expression-level rules keep the line-text snippet:
45452
+ * there the flagged expression IS the finding, so changed text means new +
45453
+ * fixed.
45454
+ *
45419
45455
  * v1 limitation: the fingerprint keys on the head-relative `filePath`, and base
45420
45456
  * content is read at that same path. A file renamed by the change therefore has
45421
45457
  * no base match, so its pre-existing findings are reported as new. This
@@ -45818,6 +45854,7 @@ const METRIC = {
45818
45854
  scanScore: "scan.score",
45819
45855
  scanClean: "scan.clean",
45820
45856
  scanCheckSkipped: "scan.check_skipped",
45857
+ scanNoReactDetected: "scan.no_react_detected",
45821
45858
  baselineDegraded: "baseline.degraded",
45822
45859
  ruleFired: "rule.fired",
45823
45860
  ruleDisabled: "rule.disabled",
@@ -45887,7 +45924,7 @@ const makeNoopConsole = () => ({
45887
45924
  });
45888
45925
  //#endregion
45889
45926
  //#region src/cli/utils/version.ts
45890
- const VERSION = "0.7.0";
45927
+ const VERSION = "0.7.1-dev.5809083";
45891
45928
  //#endregion
45892
45929
  //#region src/cli/utils/json-mode.ts
45893
45930
  let context = null;
@@ -46256,13 +46293,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
46256
46293
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
46257
46294
  * standard `SENTRY_RELEASE` override.
46258
46295
  */
46259
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.0`;
46296
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.1-dev.5809083`;
46260
46297
  /**
46261
46298
  * Deployment environment shown in Sentry's environment filter. Defaults to
46262
46299
  * `production` for tagged releases and `development` for dev/unbuilt versions,
46263
46300
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
46264
46301
  */
46265
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.0") ? "development" : "production");
46302
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.1-dev.5809083") ? "development" : "production");
46266
46303
  /**
46267
46304
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
46268
46305
  * (set to `0` to disable tracing) and falls back to
@@ -58106,7 +58143,7 @@ const buildRuntimeLayers = (input) => {
58106
58143
  const linterLayer = input.shouldSkipLint ? Linter.layerOf([]) : Linter.layerOxlint;
58107
58144
  const deadCodeLayer = input.shouldRunDeadCode ? DeadCode.layerNode : DeadCode.layerOf([]);
58108
58145
  const scoreLayer = input.shouldComputeScore ? Score.layerHttp : Score.layerOf(null);
58109
- const supplyChainLayer = input.userConfig?.supplyChain?.enabled === false ? SupplyChain.layerOf([]) : SupplyChain.layerNode;
58146
+ const supplyChainLayer = input.shouldRunSupplyChain ? SupplyChain.layerNode : SupplyChain.layerOf([]);
58110
58147
  const projectLayer = input.projectInfoOverride === void 0 ? Project.layerNode : Project.layerOf(input.projectInfoOverride);
58111
58148
  const progressLayer = input.shouldShowProgressSpinners ? Progress.layerOra(buildSpinnerProgressHandle) : Progress.layerNoop;
58112
58149
  const baseLayers = mergeAll$1(projectLayer, input.hasConfigOverride ? Config.layerOf({
@@ -58462,21 +58499,22 @@ const resolveDotenvFingerprint = (projectDirectory) => {
58462
58499
  return [];
58463
58500
  }
58464
58501
  };
58465
- const hasHiddenTrackedFileState = (projectDirectory) => {
58502
+ const isGitIdentityTrustworthy = (projectDirectory) => {
58466
58503
  const output = runGit(projectDirectory, ["ls-files", "-v"]);
58467
- if (output === null) return true;
58468
- return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
58504
+ if (output === null) return false;
58505
+ const entryLines = output.split("\n").filter((line) => line.length > 0);
58506
+ return entryLines.length > 0 && entryLines.every((line) => line[0] === "H");
58469
58507
  };
58470
58508
  const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
58471
58509
  const readPersistedCache = (cacheFilePath) => {
58472
58510
  try {
58473
58511
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
58474
- if (!isRecord(parsed) || parsed.version !== 3) return {
58475
- version: 3,
58512
+ if (!isRecord(parsed) || parsed.version !== 4) return {
58513
+ version: 4,
58476
58514
  entries: []
58477
58515
  };
58478
58516
  if (!Array.isArray(parsed.entries)) return {
58479
- version: 3,
58517
+ version: 4,
58480
58518
  entries: []
58481
58519
  };
58482
58520
  const entries = [];
@@ -58486,12 +58524,12 @@ const readPersistedCache = (cacheFilePath) => {
58486
58524
  entries.push(entry);
58487
58525
  }
58488
58526
  return {
58489
- version: 3,
58527
+ version: 4,
58490
58528
  entries
58491
58529
  };
58492
58530
  } catch {
58493
58531
  return {
58494
- version: 3,
58532
+ version: 4,
58495
58533
  entries: []
58496
58534
  };
58497
58535
  }
@@ -58523,6 +58561,7 @@ const resolveProjectIdentity = (projectDirectory) => {
58523
58561
  return path.resolve(projectDirectory);
58524
58562
  }
58525
58563
  };
58564
+ const resolveManifestContentHash = (projectDirectory) => hashFileContents(getPackageJsonPath(projectDirectory));
58526
58565
  const fileFingerprint = (filePath) => {
58527
58566
  try {
58528
58567
  const stat = fs.statSync(filePath);
@@ -58545,14 +58584,14 @@ const resolveToolchainFingerprint = (nodeBinaryPath) => {
58545
58584
  };
58546
58585
  const buildScanResultCacheKey = (input) => {
58547
58586
  if (isCacheGloballyDisabled()) return null;
58548
- if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
58587
+ if (!isGitIdentityTrustworthy(input.projectDirectory)) return null;
58549
58588
  const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
58550
58589
  if (worktreeFingerprint === null) return null;
58551
58590
  const headSha = readHeadSha(input.projectDirectory);
58552
58591
  if (headSha === null) return null;
58553
58592
  if (stringifyStableJson(input.userConfig) === null) return null;
58554
58593
  const cacheKeyJson = stringifyStableJson({
58555
- schemaVersion: 3,
58594
+ schemaVersion: 4,
58556
58595
  projectIdentity: resolveProjectIdentity(input.projectDirectory),
58557
58596
  headSha,
58558
58597
  worktreeFingerprint,
@@ -58567,6 +58606,7 @@ const buildScanResultCacheKey = (input) => {
58567
58606
  engineOptions: {
58568
58607
  lint: input.options.lint,
58569
58608
  deadCode: input.options.deadCode,
58609
+ supplyChain: input.options.supplyChain,
58570
58610
  includePaths: [...input.options.includePaths].sort(),
58571
58611
  customRulesOnly: input.options.customRulesOnly,
58572
58612
  respectInlineDisables: input.options.respectInlineDisables,
@@ -58592,17 +58632,24 @@ const createScanResultCache = (projectDirectory) => {
58592
58632
  for (const entry of persistedCache.entries) entries.set(entry.key, entry);
58593
58633
  const persist = () => {
58594
58634
  writePersistedCache(cacheFilePath, {
58595
- version: 3,
58635
+ version: 4,
58596
58636
  entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
58597
58637
  });
58598
58638
  };
58599
58639
  return {
58600
- lookup: (key) => entries.get(key)?.payload ?? null,
58640
+ lookup: (key) => {
58641
+ const payload = entries.get(key)?.payload;
58642
+ if (payload === void 0) return null;
58643
+ return resolveProjectIdentity(payload.directory) === resolveProjectIdentity(projectDirectory) && (payload.manifestContentHash ?? null) === resolveManifestContentHash(projectDirectory) ? payload : null;
58644
+ },
58601
58645
  store: (key, payload) => {
58602
58646
  entries.set(key, {
58603
58647
  key,
58604
58648
  createdAtMs: Date.now(),
58605
- payload
58649
+ payload: {
58650
+ ...payload,
58651
+ manifestContentHash: resolveManifestContentHash(projectDirectory)
58652
+ }
58606
58653
  });
58607
58654
  persist();
58608
58655
  }
@@ -58867,6 +58914,7 @@ const buildScanAttributes = (input) => {
58867
58914
  maxDurationMs: input.maxDurationMs,
58868
58915
  lint: input.lint,
58869
58916
  deadCode: input.deadCode,
58917
+ supplyChain: input.supplyChain,
58870
58918
  scoreOnly: input.scoreOnly,
58871
58919
  noScore: input.noScore,
58872
58920
  respectInlineDisables: input.respectInlineDisables,
@@ -60141,6 +60189,7 @@ const buildIgnoredTags = (userConfig) => {
60141
60189
  const mergeInspectOptions = (inputOptions, userConfig) => ({
60142
60190
  lint: inputOptions.lint ?? userConfig?.lint ?? true,
60143
60191
  deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
60192
+ supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
60144
60193
  verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
60145
60194
  outputDirectory: inputOptions.outputDirectory || null,
60146
60195
  scoreOnly: inputOptions.scoreOnly ?? false,
@@ -60180,6 +60229,7 @@ const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorke
60180
60229
  maxDurationMs: options.maxDurationMs,
60181
60230
  lint: options.lint,
60182
60231
  deadCode: options.deadCode,
60232
+ supplyChain: options.supplyChain,
60183
60233
  scoreOnly: options.scoreOnly,
60184
60234
  noScore: options.noScore,
60185
60235
  respectInlineDisables: options.respectInlineDisables,
@@ -60263,6 +60313,7 @@ const runBaselineComparison = async (params) => {
60263
60313
  projectInfoOverride: params.headProjectInfo,
60264
60314
  shouldSkipLint: !params.options.lint || !params.resolvedNodeBinaryPath,
60265
60315
  shouldRunDeadCode: false,
60316
+ shouldRunSupplyChain: params.options.supplyChain,
60266
60317
  shouldComputeScore: false,
60267
60318
  shouldShowProgressSpinners: false,
60268
60319
  oxlintConcurrency: params.options.concurrency
@@ -60351,6 +60402,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
60351
60402
  configSourceDirectory,
60352
60403
  shouldSkipLint: !options.lint || lintBindingMissing,
60353
60404
  shouldRunDeadCode: options.deadCode,
60405
+ shouldRunSupplyChain: options.supplyChain,
60354
60406
  shouldComputeScore: !options.noScore,
60355
60407
  shouldShowProgressSpinners,
60356
60408
  oxlintConcurrency: options.concurrency
@@ -62636,8 +62688,8 @@ const resolveParallelFlag = (parallel) => parallel === false ? 1 : void 0;
62636
62688
  * accepts. Flag-derived fields only (`scoreOnly`, `noScore`, `silent`,
62637
62689
  * `isCi`): every `userConfig` fallback — including `noScore` — lives in
62638
62690
  * `inspect()`'s merge layer, which sees each project's module-merged
62639
- * config. The plain boolean knobs (`lint`, `deadCode`, `verbose`) pass
62640
- * through unchanged for the same reason. This resolver reads
62691
+ * config. The plain boolean knobs (`lint`, `deadCode`, `supplyChain`,
62692
+ * `verbose`) pass through unchanged for the same reason. This resolver reads
62641
62693
  * `userConfig` only to decide the `--blocking warning` gate.
62642
62694
  */
62643
62695
  const resolveCliInspectOptions = (flags, userConfig) => {
@@ -62645,6 +62697,7 @@ const resolveCliInspectOptions = (flags, userConfig) => {
62645
62697
  return {
62646
62698
  lint: flags.lint,
62647
62699
  deadCode: flags.deadCode,
62700
+ supplyChain: flags.supplyChain,
62648
62701
  verbose: flags.verbose,
62649
62702
  outputDirectory: flags.outputDir,
62650
62703
  respectInlineDisables: flags.respectInlineDisables === false ? false : void 0,
@@ -62975,6 +63028,11 @@ const finalizeScans = (input) => {
62975
63028
  const baselineComputed = input.completedScans.length > 0 && input.completedScans.every((scan) => scan.result.baselineDelta !== void 0);
62976
63029
  const baselineDegraded = input.baselineIntended && !baselineComputed;
62977
63030
  const mode = baselineDegraded ? "diff" : input.mode;
63031
+ const isReactDetected = input.completedScans.some((scan) => hasReactRuntime(scan.result.project));
63032
+ if (input.completedScans.length > 0 && !isReactDetected) {
63033
+ recordCount(METRIC.scanNoReactDetected, 1);
63034
+ cliLogger.warn(`No React project detected at ${input.resolvedDirectory} — React rules were gated off; this is not the same as a clean scan.`);
63035
+ }
62978
63036
  const jsonCompletedScans = filterCompletedScansByCategories(input.completedScans, input.categoryFilters);
62979
63037
  if (input.isJsonMode) {
62980
63038
  const baseline = baselineComputed && baselineDeltas.length > 0 ? {
@@ -63190,7 +63248,7 @@ const inspectAction = async (directory, flags) => {
63190
63248
  const scanDirectory = projectScanTarget.resolvedDirectory;
63191
63249
  const projectConfig = projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0);
63192
63250
  const projectConfigSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
63193
- const supplyChainEnabled = projectConfig?.supplyChain?.enabled !== false;
63251
+ const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
63194
63252
  let includePaths;
63195
63253
  let supplyChainManifestChanged = false;
63196
63254
  if (isDiffMode) {
@@ -64110,10 +64168,12 @@ const ROOT_FLAG_SPEC = {
64110
64168
  "--no-parallel",
64111
64169
  "--no-respect-inline-disables",
64112
64170
  "--no-score",
64171
+ "--no-supply-chain",
64113
64172
  "--no-telemetry",
64114
64173
  "--no-warnings",
64115
64174
  "--score",
64116
64175
  "--staged",
64176
+ "--supply-chain",
64117
64177
  "--verbose",
64118
64178
  "--version",
64119
64179
  "--warnings",
@@ -64362,7 +64422,7 @@ ${highlighter.dim("Learn more:")}
64362
64422
  ${highlighter.info(CI_URL)}
64363
64423
  `;
64364
64424
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
64365
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
64425
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
64366
64426
  program.action(inspectAction);
64367
64427
  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));
64368
64428
  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);
@@ -64412,4 +64472,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
64412
64472
  export {};
64413
64473
 
64414
64474
  //# sourceMappingURL=cli.js.map
64415
- //# debugId=96cac03b-44a6-5d88-a5c9-697a5e8cf45e
64475
+ //# debugId=69a45de3-fe65-53dc-aed2-17f9cea22159
package/dist/index.d.ts CHANGED
@@ -9493,6 +9493,17 @@ interface Diagnostic {
9493
9493
  /** 1-indexed end column of the primary span, when derivable. */
9494
9494
  endColumn?: number;
9495
9495
  category: string;
9496
+ /**
9497
+ * Set when the finding's identity is the flagged element itself (a missing
9498
+ * attribute, a wrong element) rather than the flagged line's text — every
9499
+ * Accessibility-category finding, plus rules that opt in via their
9500
+ * `matchByOccurrence` metadata flag. `computeDiagnosticDelta` matches these
9501
+ * by `(filePath, plugin/rule)` occurrence count, so reformatting the flagged
9502
+ * line doesn't reclassify a pre-existing finding as new. Resolved at
9503
+ * diagnostic creation, where rule metadata is available. Absent means
9504
+ * line-text-sensitive matching (the default).
9505
+ */
9506
+ matchByOccurrence?: boolean;
9496
9507
  /**
9497
9508
  * Set when the file never ships to users (`"test"` / `"story"`), so
9498
9509
  * renderers can label the site instead of implying production impact.
@@ -9644,6 +9655,17 @@ interface DiagnoseResult {
9644
9655
  /** See `InspectResult.skippedCheckReasons`. */
9645
9656
  skippedCheckReasons?: Record<string, string>;
9646
9657
  project: ProjectInfo;
9658
+ /**
9659
+ * Whether the scanned project resolved a React-compatible runtime (React
9660
+ * or Preact). `false` means every React-runtime rule family was gated
9661
+ * off, so an empty `diagnostics` array is vacuous — NOT the same as a
9662
+ * clean React scan. Consumers gating on the result should treat
9663
+ * `reactDetected === false` as "wrong scan target", not "all clear".
9664
+ * Mirrors `JsonReport.reactDetected`; same predicate as
9665
+ * `hasReactRuntime(result.project)`. Always set by `diagnose()`;
9666
+ * optional so hand-constructed results keep compiling.
9667
+ */
9668
+ reactDetected?: boolean;
9647
9669
  elapsedMilliseconds: number;
9648
9670
  }
9649
9671
  /**
@@ -9693,6 +9715,13 @@ interface DiagnoseProjectsResult {
9693
9715
  projects: ProjectResult[];
9694
9716
  diagnostics: Diagnostic[];
9695
9717
  score: ScoreResult | null;
9718
+ /**
9719
+ * Whether any successfully scanned project resolved a React-compatible
9720
+ * runtime (React or Preact). Absent when no project scanned successfully.
9721
+ * See `DiagnoseResult.reactDetected` for gating guidance; per-project
9722
+ * detail is on each `ProjectResultOk.reactDetected`.
9723
+ */
9724
+ reactDetected?: boolean;
9696
9725
  elapsedMilliseconds: number;
9697
9726
  } //#endregion
9698
9727
  //#region src/types/handle-error.d.ts
@@ -9888,6 +9917,17 @@ interface JsonReportV1$1 {
9888
9917
  ok: boolean;
9889
9918
  directory: string;
9890
9919
  mode: JsonReportMode;
9920
+ /**
9921
+ * Whether any scanned project resolved a React-compatible runtime (React
9922
+ * or Preact). `false` means every React-runtime rule family was gated off,
9923
+ * so an empty `diagnostics` array is vacuous — NOT the same as a clean
9924
+ * React scan. Consumers gating on the report (CI, verifiers, hooks) should
9925
+ * treat `reactDetected === false` as "wrong scan target", not "all clear"
9926
+ * (per project, `projects[].project.reactVersion` / `preactVersion` say
9927
+ * which roots were non-React). Absent when nothing was scanned (`projects`
9928
+ * is empty), on error reports, and on reports from older CLI versions.
9929
+ */
9930
+ reactDetected?: boolean;
9891
9931
  diff: JsonReportDiffInfo | null;
9892
9932
  projects: JsonReportProjectEntry[];
9893
9933
  /**
@@ -10172,6 +10212,16 @@ declare const summarizeDiagnostics: (diagnostics: Diagnostic[], worstScore?: num
10172
10212
  declare const defineConfig: (config: ReactDoctorConfig) => ReactDoctorConfig; //#endregion
10173
10213
  //#region src/utils/detect-ai-training-environment.d.ts
10174
10214
  //#endregion
10215
+ //#region src/utils/has-react-runtime.d.ts
10216
+ /**
10217
+ * Whether project discovery resolved a React-compatible runtime (React or
10218
+ * Preact, which ships the same hooks + JSX model). This is the same
10219
+ * predicate that grants the `react` capability, so `false` means every
10220
+ * React-runtime rule family was gated off for the project.
10221
+ */
10222
+ declare const hasReactRuntime: (project: ProjectInfo) => boolean; //#endregion
10223
+ //#region src/utils/is-errno-exception.d.ts
10224
+ //#endregion
10175
10225
  //#region ../api/dist/index.d.ts
10176
10226
  //#region src/diagnose.d.ts
10177
10227
  interface Diagnose {
@@ -10198,5 +10248,5 @@ interface ToJsonReportOptions {
10198
10248
  }
10199
10249
  declare const toJsonReport: (result: DiagnoseResult, options: ToJsonReportOptions) => JsonReport;
10200
10250
  //#endregion
10201
- export { AmbiguousProjectError, type DiagnoseOptions, type DiagnoseProjectsInput, type DiagnoseProjectsResult, type DiagnoseResult, type Diagnostic, type DiffInfo, type JsonReport, type JsonReportDiffInfo, type JsonReportError, type JsonReportMode, type JsonReportProjectEntry, type JsonReportSummary, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, type ProjectDefinition, type ProjectInfo, ProjectNotFoundError, type ProjectResult, type ProjectResultError, type ProjectResultOk, type ReactDoctorConfig, ReactDoctorError, type ScoreResult, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
10251
+ export { AmbiguousProjectError, type DiagnoseOptions, type DiagnoseProjectsInput, type DiagnoseProjectsResult, type DiagnoseResult, type Diagnostic, type DiffInfo, type JsonReport, type JsonReportDiffInfo, type JsonReportError, type JsonReportMode, type JsonReportProjectEntry, type JsonReportSummary, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, type ProjectDefinition, type ProjectInfo, ProjectNotFoundError, type ProjectResult, type ProjectResultError, type ProjectResultOk, type ReactDoctorConfig, ReactDoctorError, type ScoreResult, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
10202
10252
  //# sourceMappingURL=index.d.ts.map
package/dist/index.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]="5c52dece-7b5e-5102-9381-7d05fa45734f")}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]="f1919957-a013-5ded-b0ed-3d2e8469d0e2")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import * as NFS from "node:fs";
@@ -19304,6 +19304,7 @@ var Diagnostic = class extends Class("Diagnostic")({
19304
19304
  endLine: optional(Number$1),
19305
19305
  endColumn: optional(Number$1),
19306
19306
  category: String$1,
19307
+ matchByOccurrence: optional(Boolean$1),
19307
19308
  fileContext: optional(Literals(["test", "story"])),
19308
19309
  suppressionHint: optional(String$1),
19309
19310
  relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
@@ -19374,6 +19375,16 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
19374
19375
  * successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
19375
19376
  */
19376
19377
  baselineDegraded: optional(Boolean$1),
19378
+ /**
19379
+ * Whether any scanned project resolved a React-compatible runtime (React
19380
+ * or Preact). `false` means every React-runtime rule family was gated off,
19381
+ * so an empty `diagnostics` array is vacuous — NOT the same as a clean
19382
+ * React scan. Consumers gating on the report (CI, verifiers, hooks) should
19383
+ * treat `reactDetected === false` as "wrong scan target", not "all clear".
19384
+ * Absent when nothing was scanned (`projects` is empty), on error reports,
19385
+ * and on reports from older CLI versions.
19386
+ */
19387
+ reactDetected: optional(Boolean$1),
19377
19388
  diff: NullOr(JsonReportDiffInfo),
19378
19389
  projects: ArraySchema(JsonReportProjectEntry),
19379
19390
  diagnostics: ArraySchema(Diagnostic),
@@ -19399,6 +19410,8 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
19399
19410
  ok: Boolean$1,
19400
19411
  directory: String$1,
19401
19412
  mode: JsonReportMode,
19413
+ /** See `JsonReportV1.reactDetected`. */
19414
+ reactDetected: optional(Boolean$1),
19402
19415
  diff: NullOr(JsonReportDiffInfo),
19403
19416
  baseline: JsonReportBaseline,
19404
19417
  projects: ArraySchema(JsonReportProjectEntry),
@@ -32907,6 +32920,7 @@ const DIAGNOSTIC_CATEGORY_BUCKETS = [
32907
32920
  "Accessibility",
32908
32921
  "Maintainability"
32909
32922
  ];
32923
+ const OCCURRENCE_MATCHED_CATEGORIES = new Set(["Accessibility"]);
32910
32924
  const APP_ONLY_RULE_KEYS = new Set([
32911
32925
  "react-hooks-js/static-components",
32912
32926
  "react-doctor/no-render-prop-children",
@@ -32929,7 +32943,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32929
32943
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32930
32944
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32931
32945
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
32932
- const CORE_PACKAGE_VERSION = "0.7.0";
32946
+ const CORE_PACKAGE_VERSION = "0.7.1";
32933
32947
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32934
32948
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
32935
32949
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -37010,10 +37024,17 @@ function* collectSecurityScanFiles(rootDirectory) {
37010
37024
  }
37011
37025
  }
37012
37026
  }
37027
+ /**
37028
+ * Whether project discovery resolved a React-compatible runtime (React or
37029
+ * Preact, which ships the same hooks + JSX model). This is the same
37030
+ * predicate that grants the `react` capability, so `false` means every
37031
+ * React-runtime rule family was gated off for the project.
37032
+ */
37033
+ const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
37013
37034
  const buildCapabilities = (project) => {
37014
37035
  const capabilities = /* @__PURE__ */ new Set();
37015
37036
  capabilities.add(project.framework);
37016
- if (project.reactVersion !== null || project.preactVersion !== null) capabilities.add("react");
37037
+ if (hasReactRuntime(project)) capabilities.add("react");
37017
37038
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
37018
37039
  if (project.expoVersion !== null) capabilities.add("expo");
37019
37040
  if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
@@ -38543,7 +38564,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38543
38564
  })),
38544
38565
  showStagedContent: (directory, relativePath, options) => runCommand({
38545
38566
  command: "git",
38546
- args: ["show", `:${relativePath}`],
38567
+ args: ["show", `:./${relativePath}`],
38547
38568
  directory,
38548
38569
  maxStdoutBytes: options?.maxBufferBytes
38549
38570
  }).pipe(map$3((result) => result.status === 0 ? result.stdout : null)),
@@ -39849,6 +39870,7 @@ const parseRuleCode = (code) => {
39849
39870
  };
39850
39871
  };
39851
39872
  const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
39873
+ const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(reactDoctorPlugin.rules[rule]?.matchByOccurrence);
39852
39874
  /**
39853
39875
  * Maps oxlint's non-primary labels (`labels[1..]`) into related source
39854
39876
  * locations. Editors surface these as a diagnostic's
@@ -39912,6 +39934,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
39912
39934
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
39913
39935
  const primarySpan = primaryLabel?.span;
39914
39936
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
39937
+ const category = resolveDiagnosticCategory(plugin, rule);
39915
39938
  return {
39916
39939
  filePath: normalizedFilePath,
39917
39940
  plugin,
@@ -39927,7 +39950,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
39927
39950
  offset: primarySpan.offset,
39928
39951
  length: primarySpan.length
39929
39952
  } : {},
39930
- category: resolveDiagnosticCategory(plugin, rule),
39953
+ category,
39954
+ ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
39931
39955
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
39932
39956
  };
39933
39957
  });
@@ -42088,6 +42112,7 @@ const buildJsonReport = (input) => {
42088
42112
  const worstScoredProject = findWorstScoredProject(projects);
42089
42113
  const summary = summarizeDiagnostics(flattenedDiagnostics, worstScoredProject?.score?.score ?? null, worstScoredProject?.score?.label ?? null);
42090
42114
  const shared = {
42115
+ ...input.scans.length > 0 ? { reactDetected: input.scans.some((scan) => hasReactRuntime(scan.result.project)) } : {},
42091
42116
  version: input.version,
42092
42117
  ok: true,
42093
42118
  directory: input.directory,
@@ -42317,6 +42342,7 @@ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
42317
42342
  skippedChecks,
42318
42343
  ...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
42319
42344
  project: output.project,
42345
+ reactDetected: hasReactRuntime(output.project),
42320
42346
  elapsedMilliseconds
42321
42347
  };
42322
42348
  };
@@ -42372,10 +42398,12 @@ const diagnoseProjectBatch = async (input) => {
42372
42398
  const startTime = globalThis.performance.now();
42373
42399
  const { projects, concurrency, config: batchConfig, ...baseOptions } = input;
42374
42400
  const projectResults = await mapWithConcurrency(projects, concurrency ?? 4, (projectDefinition) => diagnoseProject(projectDefinition, baseOptions, batchConfig));
42401
+ const succeededProjects = projectResults.filter((projectResult) => projectResult.ok);
42375
42402
  return {
42376
42403
  projects: projectResults,
42377
- diagnostics: projectResults.flatMap((projectResult) => projectResult.ok ? projectResult.diagnostics : []),
42404
+ diagnostics: succeededProjects.flatMap((projectResult) => projectResult.diagnostics),
42378
42405
  score: findWorstScore(projectResults),
42406
+ ...succeededProjects.length > 0 ? { reactDetected: succeededProjects.some((projectResult) => projectResult.reactDetected) } : {},
42379
42407
  elapsedMilliseconds: globalThis.performance.now() - startTime
42380
42408
  };
42381
42409
  };
@@ -42410,7 +42438,7 @@ const toJsonReport = (result, options) => buildJsonReport({
42410
42438
  totalElapsedMilliseconds: result.elapsedMilliseconds
42411
42439
  });
42412
42440
  //#endregion
42413
- export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
42441
+ export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
42414
42442
 
42415
42443
  //# sourceMappingURL=index.js.map
42416
- //# debugId=5c52dece-7b5e-5102-9381-7d05fa45734f
42444
+ //# debugId=f1919957-a013-5ded-b0ed-3d2e8469d0e2
package/dist/lsp.js CHANGED
@@ -19333,6 +19333,7 @@ var Diagnostic = class extends Class("Diagnostic")({
19333
19333
  endLine: optional(Number$1),
19334
19334
  endColumn: optional(Number$1),
19335
19335
  category: String$1,
19336
+ matchByOccurrence: optional(Boolean$1),
19336
19337
  fileContext: optional(Literals(["test", "story"])),
19337
19338
  suppressionHint: optional(String$1),
19338
19339
  relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
@@ -19410,6 +19411,16 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
19410
19411
  * successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
19411
19412
  */
19412
19413
  baselineDegraded: optional(Boolean$1),
19414
+ /**
19415
+ * Whether any scanned project resolved a React-compatible runtime (React
19416
+ * or Preact). `false` means every React-runtime rule family was gated off,
19417
+ * so an empty `diagnostics` array is vacuous — NOT the same as a clean
19418
+ * React scan. Consumers gating on the report (CI, verifiers, hooks) should
19419
+ * treat `reactDetected === false` as "wrong scan target", not "all clear".
19420
+ * Absent when nothing was scanned (`projects` is empty), on error reports,
19421
+ * and on reports from older CLI versions.
19422
+ */
19423
+ reactDetected: optional(Boolean$1),
19413
19424
  diff: NullOr(JsonReportDiffInfo),
19414
19425
  projects: ArraySchema(JsonReportProjectEntry),
19415
19426
  diagnostics: ArraySchema(Diagnostic),
@@ -19435,6 +19446,8 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
19435
19446
  ok: Boolean$1,
19436
19447
  directory: String$1,
19437
19448
  mode: JsonReportMode,
19449
+ /** See `JsonReportV1.reactDetected`. */
19450
+ reactDetected: optional(Boolean$1),
19438
19451
  diff: NullOr(JsonReportDiffInfo),
19439
19452
  baseline: JsonReportBaseline,
19440
19453
  projects: ArraySchema(JsonReportProjectEntry),
@@ -32966,6 +32979,7 @@ const DIAGNOSTIC_CATEGORY_BUCKETS = [
32966
32979
  "Accessibility",
32967
32980
  "Maintainability"
32968
32981
  ];
32982
+ const OCCURRENCE_MATCHED_CATEGORIES = new Set(["Accessibility"]);
32969
32983
  const APP_ONLY_RULE_KEYS = new Set([
32970
32984
  "react-hooks-js/static-components",
32971
32985
  "react-doctor/no-render-prop-children",
@@ -32988,7 +33002,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32988
33002
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32989
33003
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32990
33004
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
32991
- const CORE_PACKAGE_VERSION = "0.7.0";
33005
+ const CORE_PACKAGE_VERSION = "0.7.1";
32992
33006
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32993
33007
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
32994
33008
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -36995,10 +37009,17 @@ function* collectSecurityScanFiles(rootDirectory) {
36995
37009
  }
36996
37010
  }
36997
37011
  }
37012
+ /**
37013
+ * Whether project discovery resolved a React-compatible runtime (React or
37014
+ * Preact, which ships the same hooks + JSX model). This is the same
37015
+ * predicate that grants the `react` capability, so `false` means every
37016
+ * React-runtime rule family was gated off for the project.
37017
+ */
37018
+ const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
36998
37019
  const buildCapabilities = (project) => {
36999
37020
  const capabilities = /* @__PURE__ */ new Set();
37000
37021
  capabilities.add(project.framework);
37001
- if (project.reactVersion !== null || project.preactVersion !== null) capabilities.add("react");
37022
+ if (hasReactRuntime(project)) capabilities.add("react");
37002
37023
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
37003
37024
  if (project.expoVersion !== null) capabilities.add("expo");
37004
37025
  if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
@@ -38528,7 +38549,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38528
38549
  })),
38529
38550
  showStagedContent: (directory, relativePath, options) => runCommand({
38530
38551
  command: "git",
38531
- args: ["show", `:${relativePath}`],
38552
+ args: ["show", `:./${relativePath}`],
38532
38553
  directory,
38533
38554
  maxStdoutBytes: options?.maxBufferBytes
38534
38555
  }).pipe(map$3((result) => result.status === 0 ? result.stdout : null)),
@@ -39834,6 +39855,7 @@ const parseRuleCode = (code) => {
39834
39855
  };
39835
39856
  };
39836
39857
  const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
39858
+ const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(reactDoctorPlugin.rules[rule]?.matchByOccurrence);
39837
39859
  /**
39838
39860
  * Maps oxlint's non-primary labels (`labels[1..]`) into related source
39839
39861
  * locations. Editors surface these as a diagnostic's
@@ -39897,6 +39919,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
39897
39919
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
39898
39920
  const primarySpan = primaryLabel?.span;
39899
39921
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
39922
+ const category = resolveDiagnosticCategory(plugin, rule);
39900
39923
  return {
39901
39924
  filePath: normalizedFilePath,
39902
39925
  plugin,
@@ -39912,7 +39935,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
39912
39935
  offset: primarySpan.offset,
39913
39936
  length: primarySpan.length
39914
39937
  } : {},
39915
- category: resolveDiagnosticCategory(plugin, rule),
39938
+ category,
39939
+ ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
39916
39940
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
39917
39941
  };
39918
39942
  });
@@ -42085,7 +42109,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
42085
42109
  /** Display name used in client-facing messages and progress titles. */
42086
42110
  const SERVER_DISPLAY_NAME = "React Doctor";
42087
42111
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
42088
- const SERVER_VERSION = "0.7.0";
42112
+ const SERVER_VERSION = "0.7.1";
42089
42113
  /** `Diagnostic.source` shown next to every published diagnostic. */
42090
42114
  const DIAGNOSTIC_SOURCE = "react-doctor";
42091
42115
  /**
@@ -43792,6 +43816,7 @@ const METRIC = {
43792
43816
  scanScore: "scan.score",
43793
43817
  scanClean: "scan.clean",
43794
43818
  scanCheckSkipped: "scan.check_skipped",
43819
+ scanNoReactDetected: "scan.no_react_detected",
43795
43820
  baselineDegraded: "baseline.degraded",
43796
43821
  ruleFired: "rule.fired",
43797
43822
  ruleDisabled: "rule.disabled",
@@ -44135,5 +44160,5 @@ const startLanguageServer = () => {
44135
44160
  };
44136
44161
  //#endregion
44137
44162
  export { startLanguageServer };
44138
- !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]="1b40bd68-7ad4-55e0-a6bd-91b6cdcd0ba8")}catch(e){}}();
44139
- //# debugId=1b40bd68-7ad4-55e0-a6bd-91b6cdcd0ba8
44163
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="0cab34bf-65ac-5994-9a93-41f1b5670387")}catch(e){}}();
44164
+ //# debugId=0cab34bf-65ac-5994-9a93-41f1b5670387
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.7.0",
3
+ "version": "0.7.1-dev.5809083",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -65,8 +65,8 @@
65
65
  "vscode-languageserver-textdocument": "^1.0.12",
66
66
  "vscode-uri": "^3.1.0",
67
67
  "yaml": "^2.9.0",
68
- "deslop-js": "0.7.0",
69
- "oxlint-plugin-react-doctor": "0.7.0"
68
+ "deslop-js": "0.7.1",
69
+ "oxlint-plugin-react-doctor": "0.7.1-dev.5809083"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/babel__code-frame": "^7.27.0",
@@ -74,9 +74,9 @@
74
74
  "@xterm/headless": "^6.0.0",
75
75
  "commander": "^14.0.3",
76
76
  "ora": "^9.4.0",
77
- "@react-doctor/api": "0.7.0",
78
- "@react-doctor/core": "0.7.0",
79
- "@react-doctor/language-server": "0.7.0"
77
+ "@react-doctor/api": "0.7.1",
78
+ "@react-doctor/core": "0.7.1",
79
+ "@react-doctor/language-server": "0.7.1"
80
80
  },
81
81
  "engines": {
82
82
  "node": "^20.19.0 || >=22.13.0"