react-doctor 0.7.0 → 0.7.1-dev.ce9dabf

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]="bbc083b3-ec5f-541e-8e68-a28e5b06f6eb")}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.ce9dabf";
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.ce9dabf`;
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.ce9dabf") ? "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
@@ -62975,6 +63012,11 @@ const finalizeScans = (input) => {
62975
63012
  const baselineComputed = input.completedScans.length > 0 && input.completedScans.every((scan) => scan.result.baselineDelta !== void 0);
62976
63013
  const baselineDegraded = input.baselineIntended && !baselineComputed;
62977
63014
  const mode = baselineDegraded ? "diff" : input.mode;
63015
+ const isReactDetected = input.completedScans.some((scan) => hasReactRuntime(scan.result.project));
63016
+ if (input.completedScans.length > 0 && !isReactDetected) {
63017
+ recordCount(METRIC.scanNoReactDetected, 1);
63018
+ cliLogger.warn(`No React project detected at ${input.resolvedDirectory} — React rules were gated off; this is not the same as a clean scan.`);
63019
+ }
62978
63020
  const jsonCompletedScans = filterCompletedScansByCategories(input.completedScans, input.categoryFilters);
62979
63021
  if (input.isJsonMode) {
62980
63022
  const baseline = baselineComputed && baselineDeltas.length > 0 ? {
@@ -64412,4 +64454,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
64412
64454
  export {};
64413
64455
 
64414
64456
  //# sourceMappingURL=cli.js.map
64415
- //# debugId=96cac03b-44a6-5d88-a5c9-697a5e8cf45e
64457
+ //# debugId=bbc083b3-ec5f-541e-8e68-a28e5b06f6eb
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.ce9dabf",
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.ce9dabf"
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/language-server": "0.7.1",
78
+ "@react-doctor/api": "0.7.1",
79
+ "@react-doctor/core": "0.7.1"
80
80
  },
81
81
  "engines": {
82
82
  "node": "^20.19.0 || >=22.13.0"