react-doctor 0.7.9-dev.e86bc57 → 0.8.0-dev.0618ff6

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]="a51e06c5-66da-5c06-be51-50358579743c")}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]="4433c20d-e5c5-50db-ab5b-0ff72ef6f226")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -36242,7 +36242,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36242
36242
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36243
36243
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36244
36244
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36245
- const CORE_PACKAGE_VERSION = "0.7.9";
36245
+ const CORE_PACKAGE_VERSION = "0.8.0";
36246
36246
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36247
36247
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36248
36248
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -36252,6 +36252,53 @@ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
36252
36252
  const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
36253
36253
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
36254
36254
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
36255
+ const lastSourceMappingUrl = (fileContent) => {
36256
+ const lastReference = [...fileContent.matchAll(/\/\/# sourceMappingURL=(\S+)/g)].at(-1);
36257
+ return lastReference ? lastReference[1] : null;
36258
+ };
36259
+ const mapTargetsSource = (mapContent, input) => {
36260
+ const parsedMap = JSON.parse(mapContent);
36261
+ if (typeof parsedMap !== "object" || parsedMap === null) return false;
36262
+ const { file, sources } = parsedMap;
36263
+ if (file !== input.expectedFile) return false;
36264
+ if (!Array.isArray(sources) || sources.length !== 1 || typeof sources[0] !== "string") return false;
36265
+ return Path.posix.normalize(Path.posix.join(input.mapDirectory, sources[0])) === input.sourcePath;
36266
+ };
36267
+ const quartetProvesEmit = (stem, sourcePath, readFileText) => {
36268
+ try {
36269
+ const emitBaseName = Path.posix.basename(stem);
36270
+ const mapDirectory = Path.posix.dirname(stem);
36271
+ return lastSourceMappingUrl(readFileText(`${stem}.js`)) === `${emitBaseName}.js.map` && lastSourceMappingUrl(readFileText(`${stem}.d.ts`)) === `${emitBaseName}.d.ts.map` && mapTargetsSource(readFileText(`${stem}.js.map`), {
36272
+ expectedFile: `${emitBaseName}.js`,
36273
+ mapDirectory,
36274
+ sourcePath
36275
+ }) && mapTargetsSource(readFileText(`${stem}.d.ts.map`), {
36276
+ expectedFile: `${emitBaseName}.d.ts`,
36277
+ mapDirectory,
36278
+ sourcePath
36279
+ });
36280
+ } catch {
36281
+ return false;
36282
+ }
36283
+ };
36284
+ const collectTypeScriptEmitDuplicateJsPaths = (input) => {
36285
+ const untracked = new Set(input.untrackedPaths);
36286
+ const duplicateJsPaths = /* @__PURE__ */ new Set();
36287
+ for (const jsPath of untracked) {
36288
+ if (!jsPath.endsWith(".js")) continue;
36289
+ const stem = jsPath.slice(0, -3);
36290
+ if (![
36291
+ jsPath,
36292
+ `${stem}.js.map`,
36293
+ `${stem}.d.ts`,
36294
+ `${stem}.d.ts.map`
36295
+ ].every((emitPath) => untracked.has(emitPath) && !input.trackedPaths.has(emitPath))) continue;
36296
+ const sourcePath = [`${stem}.ts`, `${stem}.tsx`].find((candidatePath) => input.trackedPaths.has(candidatePath));
36297
+ if (!sourcePath) continue;
36298
+ if (quartetProvesEmit(stem, sourcePath, input.readFileText)) duplicateJsPaths.add(jsPath);
36299
+ }
36300
+ return duplicateJsPaths;
36301
+ };
36255
36302
  const isIgnoredDirectoryName = (directoryName) => IGNORED_DIRECTORIES.has(directoryName) || directoryName.startsWith(".") && !SCANNED_DOT_DIRECTORIES.has(directoryName);
36256
36303
  const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => isIgnoredDirectoryName(segment));
36257
36304
  const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath) && !GENERATED_SOURCE_DIRECTORY_PATTERN.test(filePath);
@@ -36319,20 +36366,29 @@ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
36319
36366
  }
36320
36367
  return entries;
36321
36368
  };
36322
- const listSourceFilesViaGit = (rootDirectory) => {
36369
+ const gitLsFiles = (rootDirectory, flags) => {
36323
36370
  const result = spawnSync("git", [
36324
36371
  "ls-files",
36325
36372
  "-z",
36326
- "--cached",
36327
- "--others",
36328
- "--exclude-standard"
36373
+ ...flags
36329
36374
  ], {
36330
36375
  cwd: rootDirectory,
36331
36376
  encoding: "utf-8",
36332
36377
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
36333
36378
  });
36334
36379
  if (result.error || result.status !== 0) return null;
36335
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath));
36380
+ return result.stdout.split("\0").filter((filePath) => filePath.length > 0);
36381
+ };
36382
+ const listSourceFilesViaGit = (rootDirectory) => {
36383
+ const trackedPaths = gitLsFiles(rootDirectory, ["--cached"]);
36384
+ const untrackedPaths = gitLsFiles(rootDirectory, ["--others", "--exclude-standard"]);
36385
+ if (trackedPaths === null || untrackedPaths === null) return null;
36386
+ const emitDuplicateJsPaths = collectTypeScriptEmitDuplicateJsPaths({
36387
+ trackedPaths: new Set(trackedPaths),
36388
+ untrackedPaths,
36389
+ readFileText: (relativePath) => NFS.readFileSync(Path.resolve(rootDirectory, relativePath), "utf-8")
36390
+ });
36391
+ return [...trackedPaths, ...untrackedPaths].filter((filePath) => isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath) && !emitDuplicateJsPaths.has(filePath));
36336
36392
  };
36337
36393
  const listSourceFilesViaFilesystem = (rootDirectory) => {
36338
36394
  const filePaths = [];
@@ -36532,11 +36588,8 @@ const frameworkMergeRank = (framework) => {
36532
36588
  if (framework === "unknown") return 3;
36533
36589
  return MOBILE_FRAMEWORKS.has(framework) ? 2 : 1;
36534
36590
  };
36535
- const REACT_COMPILER_PACKAGES = new Set([
36536
- "babel-plugin-react-compiler",
36537
- "react-compiler-runtime",
36538
- "eslint-plugin-react-compiler"
36539
- ]);
36591
+ const REACT_COMPILER_TRANSFORM_PACKAGES = new Set(["babel-plugin-react-compiler", "react-compiler-runtime"]);
36592
+ const REACT_COMPILER_LINT_PACKAGES = new Set(["eslint-plugin-react-compiler"]);
36540
36593
  const NEXT_CONFIG_FILENAMES = [
36541
36594
  "next.config.js",
36542
36595
  "next.config.mjs",
@@ -36566,16 +36619,29 @@ const EXPO_APP_CONFIG_FILENAMES = [
36566
36619
  "app.config.js",
36567
36620
  "app.config.ts"
36568
36621
  ];
36569
- const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
36622
+ const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|["']react-compiler["']/;
36570
36623
  const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
36571
36624
  const STATIC_EXPORT_OUTPUT_PATTERN = /(?:^|[^.\w])["']?output["']?\s*:\s*["']export["']/m;
36572
- const hasCompilerPackage = (packageJson) => {
36625
+ const hasCompilerPackage = (packageJson, compilerPackages) => {
36573
36626
  const allDependencies = {
36574
36627
  ...packageJson.peerDependencies,
36575
36628
  ...packageJson.dependencies,
36576
36629
  ...packageJson.devDependencies
36577
36630
  };
36578
- return Object.keys(allDependencies).some((packageName) => REACT_COMPILER_PACKAGES.has(packageName));
36631
+ return Object.keys(allDependencies).some((packageName) => compilerPackages.has(packageName));
36632
+ };
36633
+ const hasCompilerPackageInAncestors = (directory, compilerPackages) => {
36634
+ if (isProjectBoundary(directory)) return false;
36635
+ let ancestorDirectory = Path.dirname(directory);
36636
+ while (ancestorDirectory !== Path.dirname(ancestorDirectory)) {
36637
+ const ancestorPackagePath = Path.join(ancestorDirectory, "package.json");
36638
+ if (isFile(ancestorPackagePath)) {
36639
+ if (hasCompilerPackage(readPackageJson$1(ancestorPackagePath), compilerPackages)) return true;
36640
+ }
36641
+ if (isProjectBoundary(ancestorDirectory)) return false;
36642
+ ancestorDirectory = Path.dirname(ancestorDirectory);
36643
+ }
36644
+ return false;
36579
36645
  };
36580
36646
  const hasCompilerInConfigFile = (filePath) => {
36581
36647
  if (!isFile(filePath)) return false;
@@ -36584,23 +36650,14 @@ const hasCompilerInConfigFile = (filePath) => {
36584
36650
  };
36585
36651
  const hasCompilerInConfigFiles = (directory, filenames) => filenames.some((filename) => hasCompilerInConfigFile(Path.join(directory, filename)));
36586
36652
  const detectReactCompiler = (directory, packageJson) => {
36587
- if (hasCompilerPackage(packageJson)) return true;
36653
+ if (hasCompilerPackage(packageJson, REACT_COMPILER_TRANSFORM_PACKAGES)) return true;
36588
36654
  if (hasCompilerInConfigFiles(directory, NEXT_CONFIG_FILENAMES)) return true;
36589
36655
  if (hasCompilerInConfigFiles(directory, BABEL_CONFIG_FILENAMES)) return true;
36590
36656
  if (hasCompilerInConfigFiles(directory, VITE_CONFIG_FILENAMES)) return true;
36591
36657
  if (hasCompilerInConfigFiles(directory, EXPO_APP_CONFIG_FILENAMES)) return true;
36592
- if (isProjectBoundary(directory)) return false;
36593
- let ancestorDirectory = Path.dirname(directory);
36594
- while (ancestorDirectory !== Path.dirname(ancestorDirectory)) {
36595
- const ancestorPackagePath = Path.join(ancestorDirectory, "package.json");
36596
- if (isFile(ancestorPackagePath)) {
36597
- if (hasCompilerPackage(readPackageJson$1(ancestorPackagePath))) return true;
36598
- }
36599
- if (isProjectBoundary(ancestorDirectory)) return false;
36600
- ancestorDirectory = Path.dirname(ancestorDirectory);
36601
- }
36602
- return false;
36658
+ return hasCompilerPackageInAncestors(directory, REACT_COMPILER_TRANSFORM_PACKAGES);
36603
36659
  };
36660
+ const detectReactCompilerLintPlugin = (directory, packageJson) => hasCompilerPackage(packageJson, REACT_COMPILER_LINT_PACKAGES) || hasCompilerPackageInAncestors(directory, REACT_COMPILER_LINT_PACKAGES);
36604
36661
  const detectNextjsStaticExport = (directory) => NEXT_CONFIG_FILENAMES.some((filename) => {
36605
36662
  const filePath = Path.join(directory, filename);
36606
36663
  return isFile(filePath) && STATIC_EXPORT_OUTPUT_PATTERN.test(NFS.readFileSync(filePath, "utf-8"));
@@ -37564,6 +37621,7 @@ const discoverProjectWithoutPackageJson = (directory) => {
37564
37621
  framework: "unknown",
37565
37622
  hasTypeScript: hasOwnTsConfig,
37566
37623
  hasReactCompiler: false,
37624
+ hasReactCompilerLintPlugin: false,
37567
37625
  hasTanStackQuery: false,
37568
37626
  hasSsrDependency: false,
37569
37627
  preactVersion: null,
@@ -37684,6 +37742,7 @@ const discoverProject = (directory) => {
37684
37742
  framework,
37685
37743
  hasTypeScript,
37686
37744
  hasReactCompiler: detectReactCompiler(directory, packageJson),
37745
+ hasReactCompilerLintPlugin: detectReactCompilerLintPlugin(directory, packageJson),
37687
37746
  hasTanStackQuery: hasTanStackQuery(packageJson),
37688
37747
  hasSsrDependency: workspaceFacts.hasSsrDependency,
37689
37748
  preactVersion,
@@ -39169,8 +39228,10 @@ const isDiagnosticSurface = (value) => typeof value === "string" && DIAGNOSTIC_S
39169
39228
  * to the local CLI so developers see them while editing, but they're
39170
39229
  * removed from the PR comment surface, the score, and the CI gate so
39171
39230
  * they can't bury real React findings or fail a build over a Tailwind
39172
- * shorthand. Override per-surface via `config.surfaces.<surface>` to
39173
- * promote individual rules back in by tag, category, or rule id.
39231
+ * shorthand. Diagnostics from test and story files likewise remain in
39232
+ * raw output while staying out of the production-health score and CI
39233
+ * gate. Override per-surface via `config.surfaces.<surface>` to promote
39234
+ * individual rules back in by tag, category, or rule id.
39174
39235
  */
39175
39236
  const DEFAULT_SURFACE_EXCLUDED_TAGS = {
39176
39237
  cli: [],
@@ -42001,6 +42062,7 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
42001
42062
  if (resolved.includeRuleKeys.has(ruleKey)) return true;
42002
42063
  if (resolved.includeCategories.has(category)) return true;
42003
42064
  if (intersects(tags, resolved.includeTags)) return true;
42065
+ if (diagnostic.fileContext !== void 0 && (surface === "score" || surface === "ciFailure")) return false;
42004
42066
  if (resolved.excludeRuleKeys.has(ruleKey)) return false;
42005
42067
  if (resolved.excludeCategories.has(category)) return false;
42006
42068
  if (intersects(tags, resolved.excludeTags)) return false;
@@ -42425,6 +42487,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42425
42487
  }
42426
42488
  const diff = yield* runGit(input.directory, [
42427
42489
  "diff",
42490
+ "--no-ext-diff",
42428
42491
  "-z",
42429
42492
  "--name-only",
42430
42493
  "--diff-filter=ACMR",
@@ -42471,6 +42534,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42471
42534
  if (resolvedCurrentBranch !== null && resolvedCurrentBranch === baseBranch) {
42472
42535
  const uncommitted = yield* runGit(directory, [
42473
42536
  "diff",
42537
+ "--no-ext-diff",
42474
42538
  "-z",
42475
42539
  "--name-only",
42476
42540
  "--diff-filter=ACMR",
@@ -42497,6 +42561,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42497
42561
  if (mergeBaseRef === null) return null;
42498
42562
  const diff = yield* runGit(directory, [
42499
42563
  "diff",
42564
+ "--no-ext-diff",
42500
42565
  "-z",
42501
42566
  "--name-only",
42502
42567
  "--diff-filter=ACMR",
@@ -42514,6 +42579,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42514
42579
  }).pipe(withSpan("Git.diffSelection")),
42515
42580
  stagedFilePaths: (directory) => runGit(directory, [
42516
42581
  "diff",
42582
+ "--no-ext-diff",
42517
42583
  "--cached",
42518
42584
  "-z",
42519
42585
  "--name-only",
@@ -42559,6 +42625,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42559
42625
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
42560
42626
  const result = yield* runGit(directory, [
42561
42627
  "diff",
42628
+ "--no-ext-diff",
42562
42629
  "--unified=0",
42563
42630
  "--diff-filter=ACMR",
42564
42631
  "--relative",
@@ -42963,7 +43030,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
42963
43030
  return enabled;
42964
43031
  };
42965
43032
  const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
42966
- const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
43033
+ const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler || project.hasReactCompilerLintPlugin === true, customRulesOnly);
42967
43034
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
42968
43035
  const jsPlugins = [];
42969
43036
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -43926,6 +43993,8 @@ const LEADING_SEVERITY_LABEL_PATTERN = /^(?:Error|Warning):\s*/;
43926
43993
  const TRAILING_PERIOD_PATTERN = /\.$/;
43927
43994
  const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
43928
43995
  const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax";
43996
+ const REACT_ERROR_BOUNDARY_TITLE = "JSX render errors need an Error Boundary";
43997
+ const REACT_ERROR_BOUNDARY_MESSAGE = "This try/catch cannot catch errors thrown while the JSX child renders. Use an Error Boundary instead.";
43929
43998
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
43930
43999
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
43931
44000
  const REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION = "It's how the library works, not a bug in your code. Memoize values you pass from it into other memoized components, or suppress it with `// react-doctor-disable-next-line react-hooks-js/incompatible-library`.";
@@ -43969,6 +44038,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
43969
44038
  const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
43970
44039
  const resolveDiagnosticTitle = (plugin, rule) => {
43971
44040
  if (plugin !== "react-hooks-js") return getRuleTitle(rule);
44041
+ if (rule === "error-boundaries") return REACT_ERROR_BOUNDARY_TITLE;
43972
44042
  return rule === "todo" ? REACT_COMPILER_TODO_TITLE : REACT_COMPILER_TITLE;
43973
44043
  };
43974
44044
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
@@ -43987,6 +44057,10 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
43987
44057
  };
43988
44058
  const [reasonSummary = "", ...reasonDetailLines] = bailoutReason.split("\n");
43989
44059
  const reasonDetail = reasonDetailLines.join("\n").trim();
44060
+ if (rule === "error-boundaries") return {
44061
+ message: REACT_ERROR_BOUNDARY_MESSAGE,
44062
+ help: reasonDetail || help
44063
+ };
43990
44064
  return {
43991
44065
  message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
43992
44066
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
@@ -44008,7 +44082,10 @@ const parseRuleCode = (code) => {
44008
44082
  rule: match[2]
44009
44083
  };
44010
44084
  };
44011
- const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
44085
+ const resolveDiagnosticCategory = (plugin, rule) => {
44086
+ if (plugin === "react-hooks-js" && rule === "error-boundaries") return "Bugs";
44087
+ return getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
44088
+ };
44012
44089
  const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(reactDoctorPlugin.rules[rule]?.matchByOccurrence);
44013
44090
  /**
44014
44091
  * Maps oxlint's non-primary labels (`labels[1..]`) into related source
@@ -44821,7 +44898,7 @@ const runOxlint = async (options) => {
44821
44898
  };
44822
44899
  }
44823
44900
  };
44824
- if (perFileLintCacheEnabled && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
44901
+ if (perFileLintCacheEnabled && !project.hasReactCompiler && project.hasReactCompilerLintPlugin !== true && extendsPaths.length === 0 && userPlugins.length === 0) {
44825
44902
  const rulesetHash = computeRulesetHash({
44826
44903
  config: buildConfig({
44827
44904
  extendsPaths: [],
@@ -45263,6 +45340,14 @@ var Reporter = class Reporter extends Service()("react-doctor/Reporter") {
45263
45340
  });
45264
45341
  }));
45265
45342
  };
45343
+ const HOME_DIRECTORY = os.homedir();
45344
+ const USER_HOME_PATTERNS = [/[A-Za-z]:[\\/]Users[\\/][^\\/]+/gi, /(?:\/Users\/|\/home\/)[^/\\]+/gi];
45345
+ const scrubSensitivePaths = (text) => {
45346
+ let scrubbed = text;
45347
+ if (HOME_DIRECTORY.length > 1) scrubbed = scrubbed.split(HOME_DIRECTORY).join("~");
45348
+ for (const pattern of USER_HOME_PATTERNS) scrubbed = scrubbed.replace(pattern, "~");
45349
+ return scrubbed;
45350
+ };
45266
45351
  const ScoreApiResponseSchema = Struct({
45267
45352
  score: Number$1,
45268
45353
  label: String$1,
@@ -45277,7 +45362,10 @@ const ScoreApiResponseSchema = Struct({
45277
45362
  })))
45278
45363
  });
45279
45364
  const parseScoreResult = (value) => getOrNull(decodeUnknownOption(ScoreApiResponseSchema)(value));
45280
- const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, fixGroupId: _fixGroupId, ...rest }) => rest);
45365
+ const sanitizeScoreDiagnostics = (diagnostics) => diagnostics.map(({ filePath, fileContext: _fileContext, fixGroupId: _fixGroupId, ...rest }) => ({
45366
+ ...rest,
45367
+ filePath: redactSensitiveText(scrubSensitivePaths(filePath))
45368
+ }));
45281
45369
  const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
45282
45370
  const describeFailure = (error) => {
45283
45371
  if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
@@ -45290,7 +45378,7 @@ const calculateScore = async (diagnostics, options = {}) => {
45290
45378
  const requestUrl = options.isCi ? `${SCORE_API_URL}?ci=1` : SCORE_API_URL;
45291
45379
  try {
45292
45380
  const compressedBody = gzipSync(JSON.stringify({
45293
- diagnostics: stripLocalFileFields(diagnostics),
45381
+ diagnostics: sanitizeScoreDiagnostics(diagnostics),
45294
45382
  ...options.metadata?.repo ? { repo: options.metadata.repo } : {},
45295
45383
  ...options.metadata?.sha ? { sha: options.metadata.sha } : {},
45296
45384
  ...options.metadata?.framework ? { framework: options.metadata.framework } : {},
@@ -47135,7 +47223,7 @@ const makeNoopConsole = () => ({
47135
47223
  });
47136
47224
  //#endregion
47137
47225
  //#region src/cli/utils/version.ts
47138
- const VERSION = "0.7.9-dev.e86bc57";
47226
+ const VERSION = "0.8.0-dev.0618ff6";
47139
47227
  //#endregion
47140
47228
  //#region src/cli/utils/json-mode.ts
47141
47229
  let context = null;
@@ -47221,23 +47309,6 @@ const getRunId = () => {
47221
47309
  return cachedRunId;
47222
47310
  };
47223
47311
  //#endregion
47224
- //#region src/cli/utils/scrub-sensitive-text.ts
47225
- const HOME_DIRECTORY = os.homedir();
47226
- const USER_HOME_PATTERNS = [/[A-Za-z]:[\\/]Users[\\/][^\\/]+/gi, /(?:\/Users\/|\/home\/)[^/\\]+/gi];
47227
- /**
47228
- * Replaces the user's home directory (and generic `/Users|/home|C:\Users\<name>`
47229
- * roots) with `~` so absolute paths can't be tied back to an individual. Keeps
47230
- * the path's relative structure intact, which stays useful for debugging while
47231
- * dropping the personally-identifying prefix. Idempotent — re-running on an
47232
- * already-scrubbed `~/...` path is a no-op.
47233
- */
47234
- const scrubSensitivePaths = (text) => {
47235
- let scrubbed = text;
47236
- if (HOME_DIRECTORY.length > 1) scrubbed = scrubbed.split(HOME_DIRECTORY).join("~");
47237
- for (const pattern of USER_HOME_PATTERNS) scrubbed = scrubbed.replace(pattern, "~");
47238
- return scrubbed;
47239
- };
47240
- //#endregion
47241
47312
  //#region src/cli/utils/build-run-context.ts
47242
47313
  const ROOT_SUBCOMMANDS = new Set(["install", "setup"]);
47243
47314
  const detectInvokedVia = () => {
@@ -47504,13 +47575,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
47504
47575
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
47505
47576
  * standard `SENTRY_RELEASE` override.
47506
47577
  */
47507
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.9-dev.e86bc57`;
47578
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.0-dev.0618ff6`;
47508
47579
  /**
47509
47580
  * Deployment environment shown in Sentry's environment filter. Defaults to
47510
47581
  * `production` for tagged releases and `development` for dev/unbuilt versions,
47511
47582
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
47512
47583
  */
47513
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.9-dev.e86bc57") ? "development" : "production");
47584
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.0-dev.0618ff6") ? "development" : "production");
47514
47585
  /**
47515
47586
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
47516
47587
  * (set to `0` to disable tracing) and falls back to
@@ -60005,6 +60076,7 @@ const buildOutcomeAttributes = (input) => {
60005
60076
  const summary = summarizeDiagnostics(result.diagnostics);
60006
60077
  const blockingLevel = resolveTelemetryBlocking(input.userConfig);
60007
60078
  const gateDiagnostics = filterDiagnosticsForSurface(result.diagnostics, "ciFailure", input.userConfig);
60079
+ const gateDiagnosticSet = new Set(gateDiagnostics);
60008
60080
  const complete = isInspectResultComplete(result);
60009
60081
  const didLintHardFail = hasLintHardFailure(result);
60010
60082
  const failsOnHardFailure = didLintHardFail && blockingLevel !== "none";
@@ -60027,11 +60099,13 @@ const buildOutcomeAttributes = (input) => {
60027
60099
  const largestRuleBucket = buildRuleBlastRadii(result.diagnostics)[0] ?? null;
60028
60100
  let diagnosticsInTestFiles = 0;
60029
60101
  let diagnosticsInStoryFiles = 0;
60102
+ let nonProductionGateExcluded = 0;
60030
60103
  const diagnosticSiteCounts = /* @__PURE__ */ new Map();
60031
60104
  const findingsPerFixGroup = /* @__PURE__ */ new Map();
60032
60105
  for (const diagnostic of result.diagnostics) {
60033
60106
  if (diagnostic.fileContext === "test") diagnosticsInTestFiles += 1;
60034
60107
  if (diagnostic.fileContext === "story") diagnosticsInStoryFiles += 1;
60108
+ if (diagnostic.fileContext !== void 0 && !gateDiagnosticSet.has(diagnostic)) nonProductionGateExcluded += 1;
60035
60109
  const diagnosticSite = JSON.stringify([
60036
60110
  diagnostic.filePath,
60037
60111
  diagnostic.line,
@@ -60080,6 +60154,7 @@ const buildOutcomeAttributes = (input) => {
60080
60154
  affectedFiles: summary.affectedFileCount,
60081
60155
  inTestFiles: diagnosticsInTestFiles,
60082
60156
  inStoryFiles: diagnosticsInStoryFiles,
60157
+ nonProductionGateExcluded,
60083
60158
  distinctRules: countByRule.size,
60084
60159
  topRule,
60085
60160
  sameSiteOccurrences,
@@ -61919,7 +61994,9 @@ const finalizeAndRender = (input) => gen(function* () {
61919
61994
  yield* log(highlighter.gray(` ${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface (e.g. design cleanup) — run \`npx react-doctor@latest .\` locally for the full list.`));
61920
61995
  yield* log("");
61921
61996
  }
61922
- const potentialScore = score ? yield* promise(() => computeProjectedScore([...printedDiagnostics], [...surfaceDiagnostics], score)) : null;
61997
+ const scoreDiagnostics = filterDiagnosticsForSurface([...diagnostics], "score", userConfig);
61998
+ const displayedScoreDiagnostics = filterDiagnosticsForSurface([...printedDiagnostics], "score", userConfig);
61999
+ const potentialScore = score ? yield* promise(() => computeProjectedScore(displayedScoreDiagnostics, scoreDiagnostics, score)) : null;
61923
62000
  const shouldShowShareLink = !options.noScore && options.share && !options.isCi;
61924
62001
  yield* pause;
61925
62002
  yield* printSummary({
@@ -63820,6 +63897,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
63820
63897
  const categoryFilters = input.categoryFilters ?? /* @__PURE__ */ new Set();
63821
63898
  const animateRender = !verbose && canAnimateOnboarding(process.stdout);
63822
63899
  const displayDiagnostics = filterDiagnosticsByCategories(filterScansForSurface(completedScans, "cli"), categoryFilters);
63900
+ const scoreDiagnostics = new Set(filterScansForSurface(completedScans, "score"));
63901
+ const displayedScoreDiagnostics = displayDiagnostics.filter((diagnostic) => scoreDiagnostics.has(diagnostic));
63823
63902
  const projectRootByDiagnostic = /* @__PURE__ */ new WeakMap();
63824
63903
  for (const scan of completedScans) for (const diagnostic of scan.result.diagnostics) projectRootByDiagnostic.set(diagnostic, scan.result.project.rootDirectory);
63825
63904
  const resolveDiagnosticSourceRoot = (diagnostic) => projectRootByDiagnostic.get(diagnostic) ?? "";
@@ -63843,7 +63922,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
63843
63922
  diagnostics: displayDiagnostics,
63844
63923
  elapsedMilliseconds: totalElapsedMilliseconds,
63845
63924
  scoreResult: aggregateScore,
63846
- potentialScore: lowestScoredScan ? yield* promise(() => computeProjectedScore(displayDiagnostics, filterScansForSurface([lowestScoredScan], "cli"), lowestScoredScan.result.score)) : null,
63925
+ potentialScore: lowestScoredScan ? yield* promise(() => computeProjectedScore(displayedScoreDiagnostics, filterScansForSurface([lowestScoredScan], "score"), lowestScoredScan.result.score)) : null,
63847
63926
  totalSourceFileCount,
63848
63927
  noScoreMessage: "Score unavailable.",
63849
63928
  verbose,
@@ -65788,4 +65867,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
65788
65867
  export {};
65789
65868
 
65790
65869
  //# sourceMappingURL=cli.js.map
65791
- //# debugId=a51e06c5-66da-5c06-be51-50358579743c
65870
+ //# debugId=4433c20d-e5c5-50db-ab5b-0ff72ef6f226
package/dist/index.d.ts CHANGED
@@ -9056,8 +9056,10 @@ interface ReactDoctorIgnoreConfig {
9056
9056
  *
9057
9057
  * Defaults: design rules (tag `"design"`) are excluded from `prComment`,
9058
9058
  * `score`, and `ciFailure` so style cleanup doesn't dilute meaningful
9059
- * React findings. They remain in `cli` so locally-running developers
9060
- * still see the suggestion when they touch the file.
9059
+ * React findings. Diagnostics stamped with `fileContext: "test" | "story"`
9060
+ * are excluded from `score` and `ciFailure`. Both remain in `cli`, and an
9061
+ * explicit rule, category, or tag include promotes a diagnostic back onto
9062
+ * the selected surface.
9061
9063
  */
9062
9064
  type DiagnosticSurface = "cli" | "prComment" | "score" | "ciFailure";
9063
9065
  /**
@@ -9338,23 +9340,22 @@ interface ReactDoctorConfig {
9338
9340
  */
9339
9341
  adoptExistingLintConfig?: boolean;
9340
9342
  /**
9341
- * Per-surface include/exclude controls. Each `DiagnosticSurface` is
9342
- * resolved independently against rule tags, category, and id so a
9343
- * single rule can be visible locally yet hidden from PR comments,
9344
- * neutralized from the score, and excluded from the CI gate — all
9345
- * without touching the rule's severity or activation.
9343
+ * Per-surface include/exclude controls. Each `DiagnosticSurface` resolves
9344
+ * rule tags, categories, and identifiers independently. This lets you keep a rule
9345
+ * visible locally while hiding it from PR comments, the score, or the CI
9346
+ * gate. These controls don't change rule severity or activation.
9346
9347
  *
9347
9348
  * Defaults (applied before user overrides):
9348
9349
  *
9349
9350
  * - `prComment` excludes tag `"design"`
9350
9351
  * - `score` excludes tag `"design"`
9351
9352
  * - `ciFailure` excludes tag `"design"`
9353
+ * - `score` and `ciFailure` exclude test/story file contexts unless explicitly included
9352
9354
  *
9353
- * Pass any controls block (even an empty `{}`) to keep the default
9354
- * exclusions; the user's include/exclude entries layer on top.
9355
- * Include entries always win over exclude entries — handy for
9356
- * promoting a single high-signal `design-*` rule back into the
9357
- * score or PR-comment surface.
9355
+ * Pass any controls block, including an empty `{}`, to keep the defaults.
9356
+ * Your include/exclude entries layer on top. Includes always win over
9357
+ * excludes. For example, you can promote one high-signal `design-*` rule
9358
+ * back into the score or PR-comment surface.
9358
9359
  */
9359
9360
  surfaces?: Partial<Record<DiagnosticSurface, SurfaceControls>>;
9360
9361
  /**
@@ -9540,6 +9541,7 @@ interface ProjectInfo {
9540
9541
  framework: Framework;
9541
9542
  hasTypeScript: boolean;
9542
9543
  hasReactCompiler: boolean;
9544
+ hasReactCompilerLintPlugin?: boolean;
9543
9545
  hasTanStackQuery: boolean;
9544
9546
  /**
9545
9547
  * `true` when the project or a workspace declares a Vite-based SSR runtime
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]="b173f528-892d-5c34-b63a-37cb26d03f0f")}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]="81e065f1-a43b-51c2-ac53-2f38390032ed")}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 reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
@@ -33040,7 +33040,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33040
33040
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33041
33041
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33042
33042
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
33043
- const CORE_PACKAGE_VERSION = "0.7.9";
33043
+ const CORE_PACKAGE_VERSION = "0.8.0";
33044
33044
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
33045
33045
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
33046
33046
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -33050,6 +33050,53 @@ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
33050
33050
  const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
33051
33051
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
33052
33052
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
33053
+ const lastSourceMappingUrl = (fileContent) => {
33054
+ const lastReference = [...fileContent.matchAll(/\/\/# sourceMappingURL=(\S+)/g)].at(-1);
33055
+ return lastReference ? lastReference[1] : null;
33056
+ };
33057
+ const mapTargetsSource = (mapContent, input) => {
33058
+ const parsedMap = JSON.parse(mapContent);
33059
+ if (typeof parsedMap !== "object" || parsedMap === null) return false;
33060
+ const { file, sources } = parsedMap;
33061
+ if (file !== input.expectedFile) return false;
33062
+ if (!Array.isArray(sources) || sources.length !== 1 || typeof sources[0] !== "string") return false;
33063
+ return Path.posix.normalize(Path.posix.join(input.mapDirectory, sources[0])) === input.sourcePath;
33064
+ };
33065
+ const quartetProvesEmit = (stem, sourcePath, readFileText) => {
33066
+ try {
33067
+ const emitBaseName = Path.posix.basename(stem);
33068
+ const mapDirectory = Path.posix.dirname(stem);
33069
+ return lastSourceMappingUrl(readFileText(`${stem}.js`)) === `${emitBaseName}.js.map` && lastSourceMappingUrl(readFileText(`${stem}.d.ts`)) === `${emitBaseName}.d.ts.map` && mapTargetsSource(readFileText(`${stem}.js.map`), {
33070
+ expectedFile: `${emitBaseName}.js`,
33071
+ mapDirectory,
33072
+ sourcePath
33073
+ }) && mapTargetsSource(readFileText(`${stem}.d.ts.map`), {
33074
+ expectedFile: `${emitBaseName}.d.ts`,
33075
+ mapDirectory,
33076
+ sourcePath
33077
+ });
33078
+ } catch {
33079
+ return false;
33080
+ }
33081
+ };
33082
+ const collectTypeScriptEmitDuplicateJsPaths = (input) => {
33083
+ const untracked = new Set(input.untrackedPaths);
33084
+ const duplicateJsPaths = /* @__PURE__ */ new Set();
33085
+ for (const jsPath of untracked) {
33086
+ if (!jsPath.endsWith(".js")) continue;
33087
+ const stem = jsPath.slice(0, -3);
33088
+ if (![
33089
+ jsPath,
33090
+ `${stem}.js.map`,
33091
+ `${stem}.d.ts`,
33092
+ `${stem}.d.ts.map`
33093
+ ].every((emitPath) => untracked.has(emitPath) && !input.trackedPaths.has(emitPath))) continue;
33094
+ const sourcePath = [`${stem}.ts`, `${stem}.tsx`].find((candidatePath) => input.trackedPaths.has(candidatePath));
33095
+ if (!sourcePath) continue;
33096
+ if (quartetProvesEmit(stem, sourcePath, input.readFileText)) duplicateJsPaths.add(jsPath);
33097
+ }
33098
+ return duplicateJsPaths;
33099
+ };
33053
33100
  const isIgnoredDirectoryName = (directoryName) => IGNORED_DIRECTORIES.has(directoryName) || directoryName.startsWith(".") && !SCANNED_DOT_DIRECTORIES.has(directoryName);
33054
33101
  const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => isIgnoredDirectoryName(segment));
33055
33102
  const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath) && !GENERATED_SOURCE_DIRECTORY_PATTERN.test(filePath);
@@ -33120,20 +33167,29 @@ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
33120
33167
  }
33121
33168
  return entries;
33122
33169
  };
33123
- const listSourceFilesViaGit = (rootDirectory) => {
33170
+ const gitLsFiles = (rootDirectory, flags) => {
33124
33171
  const result = spawnSync("git", [
33125
33172
  "ls-files",
33126
33173
  "-z",
33127
- "--cached",
33128
- "--others",
33129
- "--exclude-standard"
33174
+ ...flags
33130
33175
  ], {
33131
33176
  cwd: rootDirectory,
33132
33177
  encoding: "utf-8",
33133
33178
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
33134
33179
  });
33135
33180
  if (result.error || result.status !== 0) return null;
33136
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath));
33181
+ return result.stdout.split("\0").filter((filePath) => filePath.length > 0);
33182
+ };
33183
+ const listSourceFilesViaGit = (rootDirectory) => {
33184
+ const trackedPaths = gitLsFiles(rootDirectory, ["--cached"]);
33185
+ const untrackedPaths = gitLsFiles(rootDirectory, ["--others", "--exclude-standard"]);
33186
+ if (trackedPaths === null || untrackedPaths === null) return null;
33187
+ const emitDuplicateJsPaths = collectTypeScriptEmitDuplicateJsPaths({
33188
+ trackedPaths: new Set(trackedPaths),
33189
+ untrackedPaths,
33190
+ readFileText: (relativePath) => NFS.readFileSync(Path.resolve(rootDirectory, relativePath), "utf-8")
33191
+ });
33192
+ return [...trackedPaths, ...untrackedPaths].filter((filePath) => isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath) && !emitDuplicateJsPaths.has(filePath));
33137
33193
  };
33138
33194
  const listSourceFilesViaFilesystem = (rootDirectory) => {
33139
33195
  const filePaths = [];
@@ -33336,11 +33392,8 @@ const frameworkMergeRank = (framework) => {
33336
33392
  if (framework === "unknown") return 3;
33337
33393
  return MOBILE_FRAMEWORKS.has(framework) ? 2 : 1;
33338
33394
  };
33339
- const REACT_COMPILER_PACKAGES = new Set([
33340
- "babel-plugin-react-compiler",
33341
- "react-compiler-runtime",
33342
- "eslint-plugin-react-compiler"
33343
- ]);
33395
+ const REACT_COMPILER_TRANSFORM_PACKAGES = new Set(["babel-plugin-react-compiler", "react-compiler-runtime"]);
33396
+ const REACT_COMPILER_LINT_PACKAGES = new Set(["eslint-plugin-react-compiler"]);
33344
33397
  const NEXT_CONFIG_FILENAMES = [
33345
33398
  "next.config.js",
33346
33399
  "next.config.mjs",
@@ -33370,16 +33423,29 @@ const EXPO_APP_CONFIG_FILENAMES = [
33370
33423
  "app.config.js",
33371
33424
  "app.config.ts"
33372
33425
  ];
33373
- const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
33426
+ const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|["']react-compiler["']/;
33374
33427
  const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
33375
33428
  const STATIC_EXPORT_OUTPUT_PATTERN = /(?:^|[^.\w])["']?output["']?\s*:\s*["']export["']/m;
33376
- const hasCompilerPackage = (packageJson) => {
33429
+ const hasCompilerPackage = (packageJson, compilerPackages) => {
33377
33430
  const allDependencies = {
33378
33431
  ...packageJson.peerDependencies,
33379
33432
  ...packageJson.dependencies,
33380
33433
  ...packageJson.devDependencies
33381
33434
  };
33382
- return Object.keys(allDependencies).some((packageName) => REACT_COMPILER_PACKAGES.has(packageName));
33435
+ return Object.keys(allDependencies).some((packageName) => compilerPackages.has(packageName));
33436
+ };
33437
+ const hasCompilerPackageInAncestors = (directory, compilerPackages) => {
33438
+ if (isProjectBoundary(directory)) return false;
33439
+ let ancestorDirectory = Path.dirname(directory);
33440
+ while (ancestorDirectory !== Path.dirname(ancestorDirectory)) {
33441
+ const ancestorPackagePath = Path.join(ancestorDirectory, "package.json");
33442
+ if (isFile(ancestorPackagePath)) {
33443
+ if (hasCompilerPackage(readPackageJson(ancestorPackagePath), compilerPackages)) return true;
33444
+ }
33445
+ if (isProjectBoundary(ancestorDirectory)) return false;
33446
+ ancestorDirectory = Path.dirname(ancestorDirectory);
33447
+ }
33448
+ return false;
33383
33449
  };
33384
33450
  const hasCompilerInConfigFile = (filePath) => {
33385
33451
  if (!isFile(filePath)) return false;
@@ -33388,23 +33454,14 @@ const hasCompilerInConfigFile = (filePath) => {
33388
33454
  };
33389
33455
  const hasCompilerInConfigFiles = (directory, filenames) => filenames.some((filename) => hasCompilerInConfigFile(Path.join(directory, filename)));
33390
33456
  const detectReactCompiler = (directory, packageJson) => {
33391
- if (hasCompilerPackage(packageJson)) return true;
33457
+ if (hasCompilerPackage(packageJson, REACT_COMPILER_TRANSFORM_PACKAGES)) return true;
33392
33458
  if (hasCompilerInConfigFiles(directory, NEXT_CONFIG_FILENAMES)) return true;
33393
33459
  if (hasCompilerInConfigFiles(directory, BABEL_CONFIG_FILENAMES)) return true;
33394
33460
  if (hasCompilerInConfigFiles(directory, VITE_CONFIG_FILENAMES)) return true;
33395
33461
  if (hasCompilerInConfigFiles(directory, EXPO_APP_CONFIG_FILENAMES)) return true;
33396
- if (isProjectBoundary(directory)) return false;
33397
- let ancestorDirectory = Path.dirname(directory);
33398
- while (ancestorDirectory !== Path.dirname(ancestorDirectory)) {
33399
- const ancestorPackagePath = Path.join(ancestorDirectory, "package.json");
33400
- if (isFile(ancestorPackagePath)) {
33401
- if (hasCompilerPackage(readPackageJson(ancestorPackagePath))) return true;
33402
- }
33403
- if (isProjectBoundary(ancestorDirectory)) return false;
33404
- ancestorDirectory = Path.dirname(ancestorDirectory);
33405
- }
33406
- return false;
33462
+ return hasCompilerPackageInAncestors(directory, REACT_COMPILER_TRANSFORM_PACKAGES);
33407
33463
  };
33464
+ const detectReactCompilerLintPlugin = (directory, packageJson) => hasCompilerPackage(packageJson, REACT_COMPILER_LINT_PACKAGES) || hasCompilerPackageInAncestors(directory, REACT_COMPILER_LINT_PACKAGES);
33408
33465
  const detectNextjsStaticExport = (directory) => NEXT_CONFIG_FILENAMES.some((filename) => {
33409
33466
  const filePath = Path.join(directory, filename);
33410
33467
  return isFile(filePath) && STATIC_EXPORT_OUTPUT_PATTERN.test(NFS.readFileSync(filePath, "utf-8"));
@@ -34371,6 +34428,7 @@ const discoverProjectWithoutPackageJson = (directory) => {
34371
34428
  framework: "unknown",
34372
34429
  hasTypeScript: hasOwnTsConfig,
34373
34430
  hasReactCompiler: false,
34431
+ hasReactCompilerLintPlugin: false,
34374
34432
  hasTanStackQuery: false,
34375
34433
  hasSsrDependency: false,
34376
34434
  preactVersion: null,
@@ -34491,6 +34549,7 @@ const discoverProject = (directory) => {
34491
34549
  framework,
34492
34550
  hasTypeScript,
34493
34551
  hasReactCompiler: detectReactCompiler(directory, packageJson),
34552
+ hasReactCompilerLintPlugin: detectReactCompilerLintPlugin(directory, packageJson),
34494
34553
  hasTanStackQuery: hasTanStackQuery(packageJson),
34495
34554
  hasSsrDependency: workspaceFacts.hasSsrDependency,
34496
34555
  preactVersion,
@@ -35969,8 +36028,10 @@ const isDiagnosticSurface = (value) => typeof value === "string" && DIAGNOSTIC_S
35969
36028
  * to the local CLI so developers see them while editing, but they're
35970
36029
  * removed from the PR comment surface, the score, and the CI gate so
35971
36030
  * they can't bury real React findings or fail a build over a Tailwind
35972
- * shorthand. Override per-surface via `config.surfaces.<surface>` to
35973
- * promote individual rules back in by tag, category, or rule id.
36031
+ * shorthand. Diagnostics from test and story files likewise remain in
36032
+ * raw output while staying out of the production-health score and CI
36033
+ * gate. Override per-surface via `config.surfaces.<surface>` to promote
36034
+ * individual rules back in by tag, category, or rule id.
35974
36035
  */
35975
36036
  const DEFAULT_SURFACE_EXCLUDED_TAGS = {
35976
36037
  cli: [],
@@ -38757,6 +38818,7 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
38757
38818
  if (resolved.includeRuleKeys.has(ruleKey)) return true;
38758
38819
  if (resolved.includeCategories.has(category)) return true;
38759
38820
  if (intersects(tags, resolved.includeTags)) return true;
38821
+ if (diagnostic.fileContext !== void 0 && (surface === "score" || surface === "ciFailure")) return false;
38760
38822
  if (resolved.excludeRuleKeys.has(ruleKey)) return false;
38761
38823
  if (resolved.excludeCategories.has(category)) return false;
38762
38824
  if (intersects(tags, resolved.excludeTags)) return false;
@@ -39181,6 +39243,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
39181
39243
  }
39182
39244
  const diff = yield* runGit(input.directory, [
39183
39245
  "diff",
39246
+ "--no-ext-diff",
39184
39247
  "-z",
39185
39248
  "--name-only",
39186
39249
  "--diff-filter=ACMR",
@@ -39227,6 +39290,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
39227
39290
  if (resolvedCurrentBranch !== null && resolvedCurrentBranch === baseBranch) {
39228
39291
  const uncommitted = yield* runGit(directory, [
39229
39292
  "diff",
39293
+ "--no-ext-diff",
39230
39294
  "-z",
39231
39295
  "--name-only",
39232
39296
  "--diff-filter=ACMR",
@@ -39253,6 +39317,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
39253
39317
  if (mergeBaseRef === null) return null;
39254
39318
  const diff = yield* runGit(directory, [
39255
39319
  "diff",
39320
+ "--no-ext-diff",
39256
39321
  "-z",
39257
39322
  "--name-only",
39258
39323
  "--diff-filter=ACMR",
@@ -39270,6 +39335,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
39270
39335
  }).pipe(withSpan("Git.diffSelection")),
39271
39336
  stagedFilePaths: (directory) => runGit(directory, [
39272
39337
  "diff",
39338
+ "--no-ext-diff",
39273
39339
  "--cached",
39274
39340
  "-z",
39275
39341
  "--name-only",
@@ -39315,6 +39381,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
39315
39381
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
39316
39382
  const result = yield* runGit(directory, [
39317
39383
  "diff",
39384
+ "--no-ext-diff",
39318
39385
  "--unified=0",
39319
39386
  "--diff-filter=ACMR",
39320
39387
  "--relative",
@@ -39719,7 +39786,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
39719
39786
  return enabled;
39720
39787
  };
39721
39788
  const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
39722
- const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
39789
+ const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler || project.hasReactCompilerLintPlugin === true, customRulesOnly);
39723
39790
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
39724
39791
  const jsPlugins = [];
39725
39792
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -40682,6 +40749,8 @@ const LEADING_SEVERITY_LABEL_PATTERN = /^(?:Error|Warning):\s*/;
40682
40749
  const TRAILING_PERIOD_PATTERN = /\.$/;
40683
40750
  const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
40684
40751
  const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax";
40752
+ const REACT_ERROR_BOUNDARY_TITLE = "JSX render errors need an Error Boundary";
40753
+ const REACT_ERROR_BOUNDARY_MESSAGE = "This try/catch cannot catch errors thrown while the JSX child renders. Use an Error Boundary instead.";
40685
40754
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
40686
40755
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
40687
40756
  const REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION = "It's how the library works, not a bug in your code. Memoize values you pass from it into other memoized components, or suppress it with `// react-doctor-disable-next-line react-hooks-js/incompatible-library`.";
@@ -40725,6 +40794,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
40725
40794
  const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
40726
40795
  const resolveDiagnosticTitle = (plugin, rule) => {
40727
40796
  if (plugin !== "react-hooks-js") return getRuleTitle(rule);
40797
+ if (rule === "error-boundaries") return REACT_ERROR_BOUNDARY_TITLE;
40728
40798
  return rule === "todo" ? REACT_COMPILER_TODO_TITLE : REACT_COMPILER_TITLE;
40729
40799
  };
40730
40800
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
@@ -40743,6 +40813,10 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
40743
40813
  };
40744
40814
  const [reasonSummary = "", ...reasonDetailLines] = bailoutReason.split("\n");
40745
40815
  const reasonDetail = reasonDetailLines.join("\n").trim();
40816
+ if (rule === "error-boundaries") return {
40817
+ message: REACT_ERROR_BOUNDARY_MESSAGE,
40818
+ help: reasonDetail || help
40819
+ };
40746
40820
  return {
40747
40821
  message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
40748
40822
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
@@ -40764,7 +40838,10 @@ const parseRuleCode = (code) => {
40764
40838
  rule: match[2]
40765
40839
  };
40766
40840
  };
40767
- const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
40841
+ const resolveDiagnosticCategory = (plugin, rule) => {
40842
+ if (plugin === "react-hooks-js" && rule === "error-boundaries") return "Bugs";
40843
+ return getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
40844
+ };
40768
40845
  const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(reactDoctorPlugin.rules[rule]?.matchByOccurrence);
40769
40846
  /**
40770
40847
  * Maps oxlint's non-primary labels (`labels[1..]`) into related source
@@ -41577,7 +41654,7 @@ const runOxlint = async (options) => {
41577
41654
  };
41578
41655
  }
41579
41656
  };
41580
- if (perFileLintCacheEnabled && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
41657
+ if (perFileLintCacheEnabled && !project.hasReactCompiler && project.hasReactCompilerLintPlugin !== true && extendsPaths.length === 0 && userPlugins.length === 0) {
41581
41658
  const rulesetHash = computeRulesetHash({
41582
41659
  config: buildConfig({
41583
41660
  extendsPaths: [],
@@ -42019,6 +42096,14 @@ var Reporter = class Reporter extends Service()("react-doctor/Reporter") {
42019
42096
  });
42020
42097
  }));
42021
42098
  };
42099
+ const HOME_DIRECTORY = os.homedir();
42100
+ const USER_HOME_PATTERNS = [/[A-Za-z]:[\\/]Users[\\/][^\\/]+/gi, /(?:\/Users\/|\/home\/)[^/\\]+/gi];
42101
+ const scrubSensitivePaths = (text) => {
42102
+ let scrubbed = text;
42103
+ if (HOME_DIRECTORY.length > 1) scrubbed = scrubbed.split(HOME_DIRECTORY).join("~");
42104
+ for (const pattern of USER_HOME_PATTERNS) scrubbed = scrubbed.replace(pattern, "~");
42105
+ return scrubbed;
42106
+ };
42022
42107
  const ScoreApiResponseSchema = Struct({
42023
42108
  score: Number$1,
42024
42109
  label: String$1,
@@ -42033,7 +42118,10 @@ const ScoreApiResponseSchema = Struct({
42033
42118
  })))
42034
42119
  });
42035
42120
  const parseScoreResult = (value) => getOrNull(decodeUnknownOption(ScoreApiResponseSchema)(value));
42036
- const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, fixGroupId: _fixGroupId, ...rest }) => rest);
42121
+ const sanitizeScoreDiagnostics = (diagnostics) => diagnostics.map(({ filePath, fileContext: _fileContext, fixGroupId: _fixGroupId, ...rest }) => ({
42122
+ ...rest,
42123
+ filePath: redactSensitiveText(scrubSensitivePaths(filePath))
42124
+ }));
42037
42125
  const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
42038
42126
  const describeFailure = (error) => {
42039
42127
  if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
@@ -42046,7 +42134,7 @@ const calculateScore = async (diagnostics, options = {}) => {
42046
42134
  const requestUrl = options.isCi ? `${SCORE_API_URL}?ci=1` : SCORE_API_URL;
42047
42135
  try {
42048
42136
  const compressedBody = gzipSync(JSON.stringify({
42049
- diagnostics: stripLocalFileFields(diagnostics),
42137
+ diagnostics: sanitizeScoreDiagnostics(diagnostics),
42050
42138
  ...options.metadata?.repo ? { repo: options.metadata.repo } : {},
42051
42139
  ...options.metadata?.sha ? { sha: options.metadata.sha } : {},
42052
42140
  ...options.metadata?.framework ? { framework: options.metadata.framework } : {},
@@ -43639,4 +43727,4 @@ const toJsonReport = (result, options) => buildJsonReport({
43639
43727
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
43640
43728
 
43641
43729
  //# sourceMappingURL=index.js.map
43642
- //# debugId=b173f528-892d-5c34-b63a-37cb26d03f0f
43730
+ //# debugId=81e065f1-a43b-51c2-ac53-2f38390032ed
package/dist/lsp.js CHANGED
@@ -33092,7 +33092,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33092
33092
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33093
33093
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33094
33094
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
33095
- const CORE_PACKAGE_VERSION = "0.7.9";
33095
+ const CORE_PACKAGE_VERSION = "0.8.0";
33096
33096
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
33097
33097
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
33098
33098
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -33102,6 +33102,53 @@ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
33102
33102
  const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
33103
33103
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
33104
33104
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
33105
+ const lastSourceMappingUrl = (fileContent) => {
33106
+ const lastReference = [...fileContent.matchAll(/\/\/# sourceMappingURL=(\S+)/g)].at(-1);
33107
+ return lastReference ? lastReference[1] : null;
33108
+ };
33109
+ const mapTargetsSource = (mapContent, input) => {
33110
+ const parsedMap = JSON.parse(mapContent);
33111
+ if (typeof parsedMap !== "object" || parsedMap === null) return false;
33112
+ const { file, sources } = parsedMap;
33113
+ if (file !== input.expectedFile) return false;
33114
+ if (!Array.isArray(sources) || sources.length !== 1 || typeof sources[0] !== "string") return false;
33115
+ return Path.posix.normalize(Path.posix.join(input.mapDirectory, sources[0])) === input.sourcePath;
33116
+ };
33117
+ const quartetProvesEmit = (stem, sourcePath, readFileText) => {
33118
+ try {
33119
+ const emitBaseName = Path.posix.basename(stem);
33120
+ const mapDirectory = Path.posix.dirname(stem);
33121
+ return lastSourceMappingUrl(readFileText(`${stem}.js`)) === `${emitBaseName}.js.map` && lastSourceMappingUrl(readFileText(`${stem}.d.ts`)) === `${emitBaseName}.d.ts.map` && mapTargetsSource(readFileText(`${stem}.js.map`), {
33122
+ expectedFile: `${emitBaseName}.js`,
33123
+ mapDirectory,
33124
+ sourcePath
33125
+ }) && mapTargetsSource(readFileText(`${stem}.d.ts.map`), {
33126
+ expectedFile: `${emitBaseName}.d.ts`,
33127
+ mapDirectory,
33128
+ sourcePath
33129
+ });
33130
+ } catch {
33131
+ return false;
33132
+ }
33133
+ };
33134
+ const collectTypeScriptEmitDuplicateJsPaths = (input) => {
33135
+ const untracked = new Set(input.untrackedPaths);
33136
+ const duplicateJsPaths = /* @__PURE__ */ new Set();
33137
+ for (const jsPath of untracked) {
33138
+ if (!jsPath.endsWith(".js")) continue;
33139
+ const stem = jsPath.slice(0, -3);
33140
+ if (![
33141
+ jsPath,
33142
+ `${stem}.js.map`,
33143
+ `${stem}.d.ts`,
33144
+ `${stem}.d.ts.map`
33145
+ ].every((emitPath) => untracked.has(emitPath) && !input.trackedPaths.has(emitPath))) continue;
33146
+ const sourcePath = [`${stem}.ts`, `${stem}.tsx`].find((candidatePath) => input.trackedPaths.has(candidatePath));
33147
+ if (!sourcePath) continue;
33148
+ if (quartetProvesEmit(stem, sourcePath, input.readFileText)) duplicateJsPaths.add(jsPath);
33149
+ }
33150
+ return duplicateJsPaths;
33151
+ };
33105
33152
  const isIgnoredDirectoryName = (directoryName) => IGNORED_DIRECTORIES.has(directoryName) || directoryName.startsWith(".") && !SCANNED_DOT_DIRECTORIES.has(directoryName);
33106
33153
  const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => isIgnoredDirectoryName(segment));
33107
33154
  const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath) && !GENERATED_SOURCE_DIRECTORY_PATTERN.test(filePath);
@@ -33172,20 +33219,29 @@ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
33172
33219
  }
33173
33220
  return entries;
33174
33221
  };
33175
- const listSourceFilesViaGit = (rootDirectory) => {
33222
+ const gitLsFiles = (rootDirectory, flags) => {
33176
33223
  const result = spawnSync("git", [
33177
33224
  "ls-files",
33178
33225
  "-z",
33179
- "--cached",
33180
- "--others",
33181
- "--exclude-standard"
33226
+ ...flags
33182
33227
  ], {
33183
33228
  cwd: rootDirectory,
33184
33229
  encoding: "utf-8",
33185
33230
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
33186
33231
  });
33187
33232
  if (result.error || result.status !== 0) return null;
33188
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath));
33233
+ return result.stdout.split("\0").filter((filePath) => filePath.length > 0);
33234
+ };
33235
+ const listSourceFilesViaGit = (rootDirectory) => {
33236
+ const trackedPaths = gitLsFiles(rootDirectory, ["--cached"]);
33237
+ const untrackedPaths = gitLsFiles(rootDirectory, ["--others", "--exclude-standard"]);
33238
+ if (trackedPaths === null || untrackedPaths === null) return null;
33239
+ const emitDuplicateJsPaths = collectTypeScriptEmitDuplicateJsPaths({
33240
+ trackedPaths: new Set(trackedPaths),
33241
+ untrackedPaths,
33242
+ readFileText: (relativePath) => NFS.readFileSync(Path.resolve(rootDirectory, relativePath), "utf-8")
33243
+ });
33244
+ return [...trackedPaths, ...untrackedPaths].filter((filePath) => isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath) && !emitDuplicateJsPaths.has(filePath));
33189
33245
  };
33190
33246
  const listSourceFilesViaFilesystem = (rootDirectory) => {
33191
33247
  const filePaths = [];
@@ -33388,11 +33444,8 @@ const frameworkMergeRank = (framework) => {
33388
33444
  if (framework === "unknown") return 3;
33389
33445
  return MOBILE_FRAMEWORKS.has(framework) ? 2 : 1;
33390
33446
  };
33391
- const REACT_COMPILER_PACKAGES = new Set([
33392
- "babel-plugin-react-compiler",
33393
- "react-compiler-runtime",
33394
- "eslint-plugin-react-compiler"
33395
- ]);
33447
+ const REACT_COMPILER_TRANSFORM_PACKAGES = new Set(["babel-plugin-react-compiler", "react-compiler-runtime"]);
33448
+ const REACT_COMPILER_LINT_PACKAGES = new Set(["eslint-plugin-react-compiler"]);
33396
33449
  const NEXT_CONFIG_FILENAMES = [
33397
33450
  "next.config.js",
33398
33451
  "next.config.mjs",
@@ -33422,16 +33475,29 @@ const EXPO_APP_CONFIG_FILENAMES = [
33422
33475
  "app.config.js",
33423
33476
  "app.config.ts"
33424
33477
  ];
33425
- const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
33478
+ const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|["']react-compiler["']/;
33426
33479
  const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*(?:true\b|\{)/;
33427
33480
  const STATIC_EXPORT_OUTPUT_PATTERN = /(?:^|[^.\w])["']?output["']?\s*:\s*["']export["']/m;
33428
- const hasCompilerPackage = (packageJson) => {
33481
+ const hasCompilerPackage = (packageJson, compilerPackages) => {
33429
33482
  const allDependencies = {
33430
33483
  ...packageJson.peerDependencies,
33431
33484
  ...packageJson.dependencies,
33432
33485
  ...packageJson.devDependencies
33433
33486
  };
33434
- return Object.keys(allDependencies).some((packageName) => REACT_COMPILER_PACKAGES.has(packageName));
33487
+ return Object.keys(allDependencies).some((packageName) => compilerPackages.has(packageName));
33488
+ };
33489
+ const hasCompilerPackageInAncestors = (directory, compilerPackages) => {
33490
+ if (isProjectBoundary(directory)) return false;
33491
+ let ancestorDirectory = Path.dirname(directory);
33492
+ while (ancestorDirectory !== Path.dirname(ancestorDirectory)) {
33493
+ const ancestorPackagePath = Path.join(ancestorDirectory, "package.json");
33494
+ if (isFile(ancestorPackagePath)) {
33495
+ if (hasCompilerPackage(readPackageJson(ancestorPackagePath), compilerPackages)) return true;
33496
+ }
33497
+ if (isProjectBoundary(ancestorDirectory)) return false;
33498
+ ancestorDirectory = Path.dirname(ancestorDirectory);
33499
+ }
33500
+ return false;
33435
33501
  };
33436
33502
  const hasCompilerInConfigFile = (filePath) => {
33437
33503
  if (!isFile(filePath)) return false;
@@ -33440,23 +33506,14 @@ const hasCompilerInConfigFile = (filePath) => {
33440
33506
  };
33441
33507
  const hasCompilerInConfigFiles = (directory, filenames) => filenames.some((filename) => hasCompilerInConfigFile(Path.join(directory, filename)));
33442
33508
  const detectReactCompiler = (directory, packageJson) => {
33443
- if (hasCompilerPackage(packageJson)) return true;
33509
+ if (hasCompilerPackage(packageJson, REACT_COMPILER_TRANSFORM_PACKAGES)) return true;
33444
33510
  if (hasCompilerInConfigFiles(directory, NEXT_CONFIG_FILENAMES)) return true;
33445
33511
  if (hasCompilerInConfigFiles(directory, BABEL_CONFIG_FILENAMES)) return true;
33446
33512
  if (hasCompilerInConfigFiles(directory, VITE_CONFIG_FILENAMES)) return true;
33447
33513
  if (hasCompilerInConfigFiles(directory, EXPO_APP_CONFIG_FILENAMES)) return true;
33448
- if (isProjectBoundary(directory)) return false;
33449
- let ancestorDirectory = Path.dirname(directory);
33450
- while (ancestorDirectory !== Path.dirname(ancestorDirectory)) {
33451
- const ancestorPackagePath = Path.join(ancestorDirectory, "package.json");
33452
- if (isFile(ancestorPackagePath)) {
33453
- if (hasCompilerPackage(readPackageJson(ancestorPackagePath))) return true;
33454
- }
33455
- if (isProjectBoundary(ancestorDirectory)) return false;
33456
- ancestorDirectory = Path.dirname(ancestorDirectory);
33457
- }
33458
- return false;
33514
+ return hasCompilerPackageInAncestors(directory, REACT_COMPILER_TRANSFORM_PACKAGES);
33459
33515
  };
33516
+ const detectReactCompilerLintPlugin = (directory, packageJson) => hasCompilerPackage(packageJson, REACT_COMPILER_LINT_PACKAGES) || hasCompilerPackageInAncestors(directory, REACT_COMPILER_LINT_PACKAGES);
33460
33517
  const detectNextjsStaticExport = (directory) => NEXT_CONFIG_FILENAMES.some((filename) => {
33461
33518
  const filePath = Path.join(directory, filename);
33462
33519
  return isFile(filePath) && STATIC_EXPORT_OUTPUT_PATTERN.test(NFS.readFileSync(filePath, "utf-8"));
@@ -34423,6 +34480,7 @@ const discoverProjectWithoutPackageJson = (directory) => {
34423
34480
  framework: "unknown",
34424
34481
  hasTypeScript: hasOwnTsConfig,
34425
34482
  hasReactCompiler: false,
34483
+ hasReactCompilerLintPlugin: false,
34426
34484
  hasTanStackQuery: false,
34427
34485
  hasSsrDependency: false,
34428
34486
  preactVersion: null,
@@ -34543,6 +34601,7 @@ const discoverProject = (directory) => {
34543
34601
  framework,
34544
34602
  hasTypeScript,
34545
34603
  hasReactCompiler: detectReactCompiler(directory, packageJson),
34604
+ hasReactCompilerLintPlugin: detectReactCompilerLintPlugin(directory, packageJson),
34546
34605
  hasTanStackQuery: hasTanStackQuery(packageJson),
34547
34606
  hasSsrDependency: workspaceFacts.hasSsrDependency,
34548
34607
  preactVersion,
@@ -35995,8 +36054,10 @@ const isDiagnosticSurface = (value) => typeof value === "string" && DIAGNOSTIC_S
35995
36054
  * to the local CLI so developers see them while editing, but they're
35996
36055
  * removed from the PR comment surface, the score, and the CI gate so
35997
36056
  * they can't bury real React findings or fail a build over a Tailwind
35998
- * shorthand. Override per-surface via `config.surfaces.<surface>` to
35999
- * promote individual rules back in by tag, category, or rule id.
36057
+ * shorthand. Diagnostics from test and story files likewise remain in
36058
+ * raw output while staying out of the production-health score and CI
36059
+ * gate. Override per-surface via `config.surfaces.<surface>` to promote
36060
+ * individual rules back in by tag, category, or rule id.
36000
36061
  */
36001
36062
  const DEFAULT_SURFACE_EXCLUDED_TAGS = {
36002
36063
  cli: [],
@@ -38735,6 +38796,7 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
38735
38796
  if (resolved.includeRuleKeys.has(ruleKey)) return true;
38736
38797
  if (resolved.includeCategories.has(category)) return true;
38737
38798
  if (intersects(tags, resolved.includeTags)) return true;
38799
+ if (diagnostic.fileContext !== void 0 && (surface === "score" || surface === "ciFailure")) return false;
38738
38800
  if (resolved.excludeRuleKeys.has(ruleKey)) return false;
38739
38801
  if (resolved.excludeCategories.has(category)) return false;
38740
38802
  if (intersects(tags, resolved.excludeTags)) return false;
@@ -39159,6 +39221,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
39159
39221
  }
39160
39222
  const diff = yield* runGit(input.directory, [
39161
39223
  "diff",
39224
+ "--no-ext-diff",
39162
39225
  "-z",
39163
39226
  "--name-only",
39164
39227
  "--diff-filter=ACMR",
@@ -39205,6 +39268,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
39205
39268
  if (resolvedCurrentBranch !== null && resolvedCurrentBranch === baseBranch) {
39206
39269
  const uncommitted = yield* runGit(directory, [
39207
39270
  "diff",
39271
+ "--no-ext-diff",
39208
39272
  "-z",
39209
39273
  "--name-only",
39210
39274
  "--diff-filter=ACMR",
@@ -39231,6 +39295,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
39231
39295
  if (mergeBaseRef === null) return null;
39232
39296
  const diff = yield* runGit(directory, [
39233
39297
  "diff",
39298
+ "--no-ext-diff",
39234
39299
  "-z",
39235
39300
  "--name-only",
39236
39301
  "--diff-filter=ACMR",
@@ -39248,6 +39313,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
39248
39313
  }).pipe(withSpan("Git.diffSelection")),
39249
39314
  stagedFilePaths: (directory) => runGit(directory, [
39250
39315
  "diff",
39316
+ "--no-ext-diff",
39251
39317
  "--cached",
39252
39318
  "-z",
39253
39319
  "--name-only",
@@ -39293,6 +39359,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
39293
39359
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
39294
39360
  const result = yield* runGit(directory, [
39295
39361
  "diff",
39362
+ "--no-ext-diff",
39296
39363
  "--unified=0",
39297
39364
  "--diff-filter=ACMR",
39298
39365
  "--relative",
@@ -39697,7 +39764,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
39697
39764
  return enabled;
39698
39765
  };
39699
39766
  const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
39700
- const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
39767
+ const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler || project.hasReactCompilerLintPlugin === true, customRulesOnly);
39701
39768
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
39702
39769
  const jsPlugins = [];
39703
39770
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -40660,6 +40727,8 @@ const LEADING_SEVERITY_LABEL_PATTERN = /^(?:Error|Warning):\s*/;
40660
40727
  const TRAILING_PERIOD_PATTERN = /\.$/;
40661
40728
  const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
40662
40729
  const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax";
40730
+ const REACT_ERROR_BOUNDARY_TITLE = "JSX render errors need an Error Boundary";
40731
+ const REACT_ERROR_BOUNDARY_MESSAGE = "This try/catch cannot catch errors thrown while the JSX child renders. Use an Error Boundary instead.";
40663
40732
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
40664
40733
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
40665
40734
  const REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION = "It's how the library works, not a bug in your code. Memoize values you pass from it into other memoized components, or suppress it with `// react-doctor-disable-next-line react-hooks-js/incompatible-library`.";
@@ -40703,6 +40772,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
40703
40772
  const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
40704
40773
  const resolveDiagnosticTitle = (plugin, rule) => {
40705
40774
  if (plugin !== "react-hooks-js") return getRuleTitle(rule);
40775
+ if (rule === "error-boundaries") return REACT_ERROR_BOUNDARY_TITLE;
40706
40776
  return rule === "todo" ? REACT_COMPILER_TODO_TITLE : REACT_COMPILER_TITLE;
40707
40777
  };
40708
40778
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
@@ -40721,6 +40791,10 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
40721
40791
  };
40722
40792
  const [reasonSummary = "", ...reasonDetailLines] = bailoutReason.split("\n");
40723
40793
  const reasonDetail = reasonDetailLines.join("\n").trim();
40794
+ if (rule === "error-boundaries") return {
40795
+ message: REACT_ERROR_BOUNDARY_MESSAGE,
40796
+ help: reasonDetail || help
40797
+ };
40724
40798
  return {
40725
40799
  message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
40726
40800
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
@@ -40742,7 +40816,10 @@ const parseRuleCode = (code) => {
40742
40816
  rule: match[2]
40743
40817
  };
40744
40818
  };
40745
- const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
40819
+ const resolveDiagnosticCategory = (plugin, rule) => {
40820
+ if (plugin === "react-hooks-js" && rule === "error-boundaries") return "Bugs";
40821
+ return getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
40822
+ };
40746
40823
  const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(reactDoctorPlugin.rules[rule]?.matchByOccurrence);
40747
40824
  /**
40748
40825
  * Maps oxlint's non-primary labels (`labels[1..]`) into related source
@@ -41555,7 +41632,7 @@ const runOxlint = async (options) => {
41555
41632
  };
41556
41633
  }
41557
41634
  };
41558
- if (perFileLintCacheEnabled && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
41635
+ if (perFileLintCacheEnabled && !project.hasReactCompiler && project.hasReactCompilerLintPlugin !== true && extendsPaths.length === 0 && userPlugins.length === 0) {
41559
41636
  const rulesetHash = computeRulesetHash({
41560
41637
  config: buildConfig({
41561
41638
  extendsPaths: [],
@@ -41997,6 +42074,14 @@ var Reporter = class Reporter extends Service()("react-doctor/Reporter") {
41997
42074
  });
41998
42075
  }));
41999
42076
  };
42077
+ const HOME_DIRECTORY = os.homedir();
42078
+ const USER_HOME_PATTERNS = [/[A-Za-z]:[\\/]Users[\\/][^\\/]+/gi, /(?:\/Users\/|\/home\/)[^/\\]+/gi];
42079
+ const scrubSensitivePaths = (text) => {
42080
+ let scrubbed = text;
42081
+ if (HOME_DIRECTORY.length > 1) scrubbed = scrubbed.split(HOME_DIRECTORY).join("~");
42082
+ for (const pattern of USER_HOME_PATTERNS) scrubbed = scrubbed.replace(pattern, "~");
42083
+ return scrubbed;
42084
+ };
42000
42085
  const ScoreApiResponseSchema = Struct({
42001
42086
  score: Number$1,
42002
42087
  label: String$1,
@@ -42011,7 +42096,10 @@ const ScoreApiResponseSchema = Struct({
42011
42096
  })))
42012
42097
  });
42013
42098
  const parseScoreResult = (value) => getOrNull(decodeUnknownOption(ScoreApiResponseSchema)(value));
42014
- const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, fixGroupId: _fixGroupId, ...rest }) => rest);
42099
+ const sanitizeScoreDiagnostics = (diagnostics) => diagnostics.map(({ filePath, fileContext: _fileContext, fixGroupId: _fixGroupId, ...rest }) => ({
42100
+ ...rest,
42101
+ filePath: redactSensitiveText(scrubSensitivePaths(filePath))
42102
+ }));
42015
42103
  const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
42016
42104
  const describeFailure = (error) => {
42017
42105
  if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
@@ -42024,7 +42112,7 @@ const calculateScore = async (diagnostics, options = {}) => {
42024
42112
  const requestUrl = options.isCi ? `${SCORE_API_URL}?ci=1` : SCORE_API_URL;
42025
42113
  try {
42026
42114
  const compressedBody = gzipSync(JSON.stringify({
42027
- diagnostics: stripLocalFileFields(diagnostics),
42115
+ diagnostics: sanitizeScoreDiagnostics(diagnostics),
42028
42116
  ...options.metadata?.repo ? { repo: options.metadata.repo } : {},
42029
42117
  ...options.metadata?.sha ? { sha: options.metadata.sha } : {},
42030
42118
  ...options.metadata?.framework ? { framework: options.metadata.framework } : {},
@@ -43229,7 +43317,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
43229
43317
  /** Display name used in client-facing messages and progress titles. */
43230
43318
  const SERVER_DISPLAY_NAME = "React Doctor";
43231
43319
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
43232
- const SERVER_VERSION = "0.7.9";
43320
+ const SERVER_VERSION = "0.8.0";
43233
43321
  /** `Diagnostic.source` shown next to every published diagnostic. */
43234
43322
  const DIAGNOSTIC_SOURCE = "react-doctor";
43235
43323
  /**
@@ -44971,23 +45059,6 @@ const METRIC = {
44971
45059
  */
44972
45060
  const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
44973
45061
  //#endregion
44974
- //#region src/cli/utils/scrub-sensitive-text.ts
44975
- const HOME_DIRECTORY = os.homedir();
44976
- const USER_HOME_PATTERNS = [/[A-Za-z]:[\\/]Users[\\/][^\\/]+/gi, /(?:\/Users\/|\/home\/)[^/\\]+/gi];
44977
- /**
44978
- * Replaces the user's home directory (and generic `/Users|/home|C:\Users\<name>`
44979
- * roots) with `~` so absolute paths can't be tied back to an individual. Keeps
44980
- * the path's relative structure intact, which stays useful for debugging while
44981
- * dropping the personally-identifying prefix. Idempotent — re-running on an
44982
- * already-scrubbed `~/...` path is a no-op.
44983
- */
44984
- const scrubSensitivePaths = (text) => {
44985
- let scrubbed = text;
44986
- if (HOME_DIRECTORY.length > 1) scrubbed = scrubbed.split(HOME_DIRECTORY).join("~");
44987
- for (const pattern of USER_HOME_PATTERNS) scrubbed = scrubbed.replace(pattern, "~");
44988
- return scrubbed;
44989
- };
44990
- //#endregion
44991
45062
  //#region src/cli/utils/anonymize-text.ts
44992
45063
  /**
44993
45064
  * Free-text fields can carry both a home-directory path (the OS username) and a
@@ -45276,5 +45347,5 @@ const startLanguageServer = () => {
45276
45347
  };
45277
45348
  //#endregion
45278
45349
  export { startLanguageServer };
45279
- !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]="8227a9ff-6d2c-5cc4-99b6-7016d7554b99")}catch(e){}}();
45280
- //# debugId=8227a9ff-6d2c-5cc4-99b6-7016d7554b99
45350
+ !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]="9eca6678-56ba-5032-b04c-b1ed965dffb4")}catch(e){}}();
45351
+ //# debugId=9eca6678-56ba-5032-b04c-b1ed965dffb4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.7.9-dev.e86bc57",
3
+ "version": "0.8.0-dev.0618ff6",
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.9",
69
- "oxlint-plugin-react-doctor": "0.7.9-dev.e86bc57"
68
+ "deslop-js": "0.8.0",
69
+ "oxlint-plugin-react-doctor": "0.8.0-dev.0618ff6"
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.9",
78
- "@react-doctor/core": "0.7.9",
79
- "@react-doctor/language-server": "0.7.9"
77
+ "@react-doctor/api": "0.8.0",
78
+ "@react-doctor/language-server": "0.8.0",
79
+ "@react-doctor/core": "0.8.0"
80
80
  },
81
81
  "engines": {
82
82
  "node": "^20.19.0 || >=22.13.0"