react-doctor 0.9.3-dev.ba04952 → 0.9.3-dev.c825c59

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.
Files changed (4) hide show
  1. package/dist/cli.js +228 -304
  2. package/dist/index.js +177 -227
  3. package/dist/lsp.js +181 -204
  4. package/package.json +4 -4
package/dist/lsp.js CHANGED
@@ -64258,13 +64258,7 @@ const hasReactDependency = (packageJson) => {
64258
64258
  };
64259
64259
  return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
64260
64260
  };
64261
- const getPreactVersion = (packageJson) => {
64262
- return {
64263
- ...packageJson.peerDependencies,
64264
- ...packageJson.dependencies,
64265
- ...packageJson.devDependencies
64266
- }.preact ?? null;
64267
- };
64261
+ const getPreactVersion = (packageJson) => getDependencySpec(packageJson, "preact");
64268
64262
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, sourceDirectory = rootDirectory, sourcePackageJson = rootPackageJson, version }) => {
64269
64263
  if (version === null || !isCatalogReference(version)) return version;
64270
64264
  const catalogName = extractCatalogName(version);
@@ -65130,7 +65124,7 @@ const clearProjectCache = () => {
65130
65124
  */
65131
65125
  const discoverProjectWithoutPackageJson = (directory, options) => {
65132
65126
  const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
65133
- const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
65127
+ const hasOwnTsConfig = isFile(Path.join(directory, "tsconfig.json"));
65134
65128
  const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
65135
65129
  const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
65136
65130
  if (enclosingProject !== null) return {
@@ -65266,7 +65260,7 @@ const discoverProject = (directory, options = {}) => {
65266
65260
  const tailwindVersion = tailwindcss.version;
65267
65261
  const zodVersion = zod.version;
65268
65262
  const projectName = packageJson.name ?? Path.basename(directory);
65269
- const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
65263
+ const hasTypeScript = isFile(Path.join(directory, "tsconfig.json"));
65270
65264
  const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
65271
65265
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
65272
65266
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
@@ -65396,134 +65390,6 @@ const discoverProject = (directory, options = {}) => {
65396
65390
  * single source file has nothing to analyze.
65397
65391
  */
65398
65392
  const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null || project.sourceFileCount > 0;
65399
- const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
65400
- var InvalidGlobPatternError = class extends Error {
65401
- pattern;
65402
- reason;
65403
- constructor(pattern, reason) {
65404
- super(`Invalid glob pattern ${JSON.stringify(pattern)}: ${reason}`);
65405
- this.name = "InvalidGlobPatternError";
65406
- this.pattern = pattern;
65407
- this.reason = reason;
65408
- }
65409
- };
65410
- const assertGlobPattern = (condition, pattern, reason) => {
65411
- if (!condition) throw new InvalidGlobPatternError(pattern, reason);
65412
- };
65413
- const countGlobWildcards = (pattern) => (pattern.match(/[*?]/g) ?? []).length;
65414
- const normalizeGlobPattern = (pattern) => pattern.replace(/\\/g, "/").replace(/^\//, "");
65415
- const PICOMATCH_OPTIONS = {
65416
- dot: true,
65417
- strictSlashes: false,
65418
- windows: false
65419
- };
65420
- const compileGlobPattern = (rawPattern) => {
65421
- assertGlobPattern(typeof rawPattern === "string" && rawPattern.length > 0, String(rawPattern), "pattern must be a non-empty string.");
65422
- assertGlobPattern(rawPattern.length <= MAX_GLOB_PATTERN_LENGTH_CHARS, rawPattern, `pattern length ${rawPattern.length} exceeds the maximum of ${MAX_GLOB_PATTERN_LENGTH_CHARS} characters.`);
65423
- const wildcardCount = countGlobWildcards(rawPattern);
65424
- assertGlobPattern(wildcardCount <= 24, rawPattern, `pattern uses ${wildcardCount} wildcards (\`*\` / \`?\`), exceeding the maximum of 24. This guards against catastrophic backtracking from pathological patterns; split the pattern into multiple smaller entries.`);
65425
- try {
65426
- return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
65427
- } catch (caughtError) {
65428
- throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
65429
- }
65430
- };
65431
- const compileGlobPatternsLenient = (patterns, onInvalid) => {
65432
- const compiled = [];
65433
- for (const pattern of patterns) try {
65434
- compiled.push(compileGlobPattern(pattern));
65435
- } catch (caughtError) {
65436
- if (!(caughtError instanceof InvalidGlobPatternError)) throw caughtError;
65437
- onInvalid(caughtError);
65438
- }
65439
- return compiled;
65440
- };
65441
- const toRelativePath = (filePath, rootDirectory) => {
65442
- const normalizedFilePath = filePath.replace(/\\/g, "/");
65443
- const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
65444
- if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
65445
- return normalizedFilePath.replace(/^\.\//, "");
65446
- };
65447
- const warnConfigIssue = (message) => {
65448
- process.stderr.write(`[react-doctor] ${message}\n`);
65449
- };
65450
- const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
65451
- const collectStringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
65452
- const validateOverrideEntry = (entry, index) => {
65453
- if (!isPlainObject(entry)) {
65454
- warnConfigIssue(`ignore.overrides[${index}] must be an object with { files, rules }; ignoring this entry.`);
65455
- return null;
65456
- }
65457
- if (!isStringArray(entry.files)) {
65458
- warnConfigIssue(`ignore.overrides[${index}].files must be an array of strings; ignoring this entry.`);
65459
- return null;
65460
- }
65461
- if (entry.rules !== void 0 && !isStringArray(entry.rules)) {
65462
- warnConfigIssue(`ignore.overrides[${index}].rules must be an array of "plugin/rule" strings or omitted; treating as missing (override would suppress every rule for the matched files).`);
65463
- return { files: entry.files };
65464
- }
65465
- return entry.rules === void 0 ? { files: entry.files } : {
65466
- files: entry.files,
65467
- rules: entry.rules
65468
- };
65469
- };
65470
- const compileIgnoreOverrides = (userConfig) => {
65471
- const overrides = userConfig?.ignore?.overrides;
65472
- if (overrides === void 0) return [];
65473
- if (!Array.isArray(overrides)) {
65474
- warnConfigIssue(`ignore.overrides must be an array of { files, rules } entries; ignoring.`);
65475
- return [];
65476
- }
65477
- return overrides.flatMap((entry, index) => {
65478
- const validated = validateOverrideEntry(entry, index);
65479
- if (!validated) return [];
65480
- const filePatterns = compileGlobPatternsLenient(collectStringList(validated.files), (error) => warnConfigIssue(`ignore.overrides[${index}]: ${error.message}`));
65481
- if (filePatterns.length === 0) return [];
65482
- return [{
65483
- filePatterns,
65484
- ruleIds: new Set(collectStringList(validated.rules))
65485
- }];
65486
- });
65487
- };
65488
- const isDiagnosticIgnoredByOverrides = (diagnostic, rootDirectory, overrides) => {
65489
- if (overrides.length === 0) return false;
65490
- const relativeFilePath = toRelativePath(diagnostic.filePath, rootDirectory);
65491
- const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
65492
- return overrides.some((override) => override.filePatterns.some((pattern) => pattern.test(relativeFilePath)) && (override.ruleIds.size === 0 || override.ruleIds.has(ruleIdentifier)));
65493
- };
65494
- const SEVERITY_FOR_OVERRIDE = {
65495
- error: "error",
65496
- warn: "warning"
65497
- };
65498
- const restampSeverity = (diagnostic, override) => {
65499
- const targetSeverity = SEVERITY_FOR_OVERRIDE[override];
65500
- if (diagnostic.severity === targetSeverity) return diagnostic;
65501
- return {
65502
- ...diagnostic,
65503
- severity: targetSeverity
65504
- };
65505
- };
65506
- /**
65507
- * Assembles the internal `RuleSeverityControls` shape from a user
65508
- * config's top-level `rules` and `categories` fields — the
65509
- * ESLint / oxlint-shaped severity surface.
65510
- *
65511
- * Returns `undefined` when neither field is present so the common
65512
- * path (no severity config at all) stays allocation-free for
65513
- * downstream consumers.
65514
- */
65515
- const buildRuleSeverityControls = (config) => {
65516
- if (!config) return void 0;
65517
- if (config.rules === void 0 && config.categories === void 0 && config.buckets === void 0) return;
65518
- return {
65519
- ...config.rules !== void 0 ? { rules: config.rules } : {},
65520
- ...config.categories !== void 0 ? { categories: config.categories } : {},
65521
- ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
65522
- };
65523
- };
65524
- const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
65525
- const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
65526
- const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
65527
65393
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
65528
65394
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
65529
65395
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -65662,6 +65528,139 @@ const getEquivalentRuleKeys = (ruleKey) => {
65662
65528
  const nativeRuleKey = canonicalizeRuleKey(ruleKey);
65663
65529
  return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
65664
65530
  };
65531
+ const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
65532
+ var InvalidGlobPatternError = class extends Error {
65533
+ pattern;
65534
+ reason;
65535
+ constructor(pattern, reason) {
65536
+ super(`Invalid glob pattern ${JSON.stringify(pattern)}: ${reason}`);
65537
+ this.name = "InvalidGlobPatternError";
65538
+ this.pattern = pattern;
65539
+ this.reason = reason;
65540
+ }
65541
+ };
65542
+ const assertGlobPattern = (condition, pattern, reason) => {
65543
+ if (!condition) throw new InvalidGlobPatternError(pattern, reason);
65544
+ };
65545
+ const countGlobWildcards = (pattern) => (pattern.match(/[*?]/g) ?? []).length;
65546
+ const normalizeGlobPattern = (pattern) => pattern.replace(/\\/g, "/").replace(/^\//, "");
65547
+ const PICOMATCH_OPTIONS = {
65548
+ dot: true,
65549
+ strictSlashes: false,
65550
+ windows: false
65551
+ };
65552
+ const compileGlobPattern = (rawPattern) => {
65553
+ assertGlobPattern(typeof rawPattern === "string" && rawPattern.length > 0, String(rawPattern), "pattern must be a non-empty string.");
65554
+ assertGlobPattern(rawPattern.length <= MAX_GLOB_PATTERN_LENGTH_CHARS, rawPattern, `pattern length ${rawPattern.length} exceeds the maximum of ${MAX_GLOB_PATTERN_LENGTH_CHARS} characters.`);
65555
+ const wildcardCount = countGlobWildcards(rawPattern);
65556
+ assertGlobPattern(wildcardCount <= 24, rawPattern, `pattern uses ${wildcardCount} wildcards (\`*\` / \`?\`), exceeding the maximum of 24. This guards against catastrophic backtracking from pathological patterns; split the pattern into multiple smaller entries.`);
65557
+ try {
65558
+ return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
65559
+ } catch (caughtError) {
65560
+ throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
65561
+ }
65562
+ };
65563
+ const compileGlobPatternsLenient = (patterns, onInvalid) => {
65564
+ const compiled = [];
65565
+ for (const pattern of patterns) try {
65566
+ compiled.push(compileGlobPattern(pattern));
65567
+ } catch (caughtError) {
65568
+ if (!(caughtError instanceof InvalidGlobPatternError)) throw caughtError;
65569
+ onInvalid(caughtError);
65570
+ }
65571
+ return compiled;
65572
+ };
65573
+ const toRelativePath = (filePath, rootDirectory) => {
65574
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
65575
+ const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
65576
+ if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
65577
+ return normalizedFilePath.replace(/^\.\//, "");
65578
+ };
65579
+ const warnConfigIssue = (message) => {
65580
+ process.stderr.write(`[react-doctor] ${message}\n`);
65581
+ };
65582
+ const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
65583
+ const collectStringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
65584
+ const hasMatchingRuleOverride = (ruleIds, ruleIdentifier) => {
65585
+ if (ruleIds.size === 0) return true;
65586
+ for (const ruleId of ruleIds) if (isSameRuleKey(ruleId, ruleIdentifier)) return true;
65587
+ return false;
65588
+ };
65589
+ const validateOverrideEntry = (entry, index) => {
65590
+ if (!isPlainObject(entry)) {
65591
+ warnConfigIssue(`ignore.overrides[${index}] must be an object with { files, rules }; ignoring this entry.`);
65592
+ return null;
65593
+ }
65594
+ if (!isStringArray(entry.files)) {
65595
+ warnConfigIssue(`ignore.overrides[${index}].files must be an array of strings; ignoring this entry.`);
65596
+ return null;
65597
+ }
65598
+ if (entry.rules !== void 0 && !isStringArray(entry.rules)) {
65599
+ warnConfigIssue(`ignore.overrides[${index}].rules must be an array of "plugin/rule" strings or omitted; treating as missing (override would suppress every rule for the matched files).`);
65600
+ return { files: entry.files };
65601
+ }
65602
+ return entry.rules === void 0 ? { files: entry.files } : {
65603
+ files: entry.files,
65604
+ rules: entry.rules
65605
+ };
65606
+ };
65607
+ const compileIgnoreOverrides = (userConfig) => {
65608
+ const overrides = userConfig?.ignore?.overrides;
65609
+ if (overrides === void 0) return [];
65610
+ if (!Array.isArray(overrides)) {
65611
+ warnConfigIssue(`ignore.overrides must be an array of { files, rules } entries; ignoring.`);
65612
+ return [];
65613
+ }
65614
+ return overrides.flatMap((entry, index) => {
65615
+ const validated = validateOverrideEntry(entry, index);
65616
+ if (!validated) return [];
65617
+ const filePatterns = compileGlobPatternsLenient(collectStringList(validated.files), (error) => warnConfigIssue(`ignore.overrides[${index}]: ${error.message}`));
65618
+ if (filePatterns.length === 0) return [];
65619
+ return [{
65620
+ filePatterns,
65621
+ ruleIds: new Set(collectStringList(validated.rules))
65622
+ }];
65623
+ });
65624
+ };
65625
+ const isDiagnosticIgnoredByOverrides = (diagnostic, rootDirectory, overrides) => {
65626
+ if (overrides.length === 0) return false;
65627
+ const relativeFilePath = toRelativePath(diagnostic.filePath, rootDirectory);
65628
+ const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
65629
+ return overrides.some((override) => override.filePatterns.some((pattern) => pattern.test(relativeFilePath)) && hasMatchingRuleOverride(override.ruleIds, ruleIdentifier));
65630
+ };
65631
+ const SEVERITY_FOR_OVERRIDE = {
65632
+ error: "error",
65633
+ warn: "warning"
65634
+ };
65635
+ const restampSeverity = (diagnostic, override) => {
65636
+ const targetSeverity = SEVERITY_FOR_OVERRIDE[override];
65637
+ if (diagnostic.severity === targetSeverity) return diagnostic;
65638
+ return {
65639
+ ...diagnostic,
65640
+ severity: targetSeverity
65641
+ };
65642
+ };
65643
+ /**
65644
+ * Assembles the internal `RuleSeverityControls` shape from a user
65645
+ * config's top-level `rules` and `categories` fields — the
65646
+ * ESLint / oxlint-shaped severity surface.
65647
+ *
65648
+ * Returns `undefined` when neither field is present so the common
65649
+ * path (no severity config at all) stays allocation-free for
65650
+ * downstream consumers.
65651
+ */
65652
+ const buildRuleSeverityControls = (config) => {
65653
+ if (!config) return void 0;
65654
+ if (config.rules === void 0 && config.categories === void 0 && config.buckets === void 0) return;
65655
+ return {
65656
+ ...config.rules !== void 0 ? { rules: config.rules } : {},
65657
+ ...config.categories !== void 0 ? { categories: config.categories } : {},
65658
+ ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
65659
+ };
65660
+ };
65661
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
65662
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
65663
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
65665
65664
  const stripDescriptionTail = (ruleList) => {
65666
65665
  const descriptionMatch = ruleList.match(/(?:^|\s)--\s/);
65667
65666
  if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
@@ -66074,6 +66073,10 @@ const classifyByDirectoryCohort = (packageDirectory) => {
66074
66073
  current = parent;
66075
66074
  }
66076
66075
  };
66076
+ const clearPackageRoleCache = () => {
66077
+ cachedRoleByPackageDirectory.clear();
66078
+ cachedPackageDirectoryByFilename.clear();
66079
+ };
66077
66080
  const classifyPackageRole = (filename) => {
66078
66081
  if (!filename) return "unknown";
66079
66082
  const packageDirectory = findNearestPackageDirectory(filename);
@@ -66308,12 +66311,8 @@ const buildDiagnosticPipeline = (input) => {
66308
66311
  if (rule.tags.includes("migration-hint")) return false;
66309
66312
  return getFileContext(diagnostic.filePath) !== "production";
66310
66313
  };
66311
- const isRuleIgnored = (ruleIdentifier) => {
66312
- for (const ignored of ignoredRules) if (isSameRuleKey(ignored, ruleIdentifier)) return true;
66313
- return false;
66314
- };
66315
- const isAppOnlyRule = (ruleIdentifier) => {
66316
- for (const appOnlyRuleKey of APP_ONLY_RULE_KEYS) if (isSameRuleKey(appOnlyRuleKey, ruleIdentifier)) return true;
66314
+ const matchesRuleKey = (ruleIdentifier, candidates) => {
66315
+ for (const candidate of candidates) if (isSameRuleKey(candidate, ruleIdentifier)) return true;
66317
66316
  return false;
66318
66317
  };
66319
66318
  const isRnRawTextSuppressedByConfig = (diagnostic) => {
@@ -66355,12 +66354,13 @@ const buildDiagnosticPipeline = (input) => {
66355
66354
  if (explicitSeverityOverride === "off") return suppress(current, "config");
66356
66355
  if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
66357
66356
  }
66357
+ const ruleIdentifier = `${current.plugin}/${current.rule}`;
66358
66358
  if (explicitRuleOverride === void 0) {
66359
- if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
66359
+ if (matchesRuleKey(ruleIdentifier, APP_ONLY_RULE_KEYS) && isLibraryFile(current.filePath)) return null;
66360
66360
  }
66361
66361
  if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
66362
66362
  if (userConfig) {
66363
- if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return suppress(current, "config");
66363
+ if (matchesRuleKey(ruleIdentifier, ignoredRules)) return suppress(current, "config");
66364
66364
  if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
66365
66365
  if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return suppress(current, "override");
66366
66366
  if (isRnRawTextSuppressedByConfig(current)) return null;
@@ -66369,7 +66369,6 @@ const buildDiagnosticPipeline = (input) => {
66369
66369
  if (respectInlineDisables && current.line > 0) {
66370
66370
  const lines = getFileLines(current.filePath);
66371
66371
  if (lines) {
66372
- const ruleIdentifier = `${current.plugin}/${current.rule}`;
66373
66372
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
66374
66373
  if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
66375
66374
  if (evaluation.nearMissHint) current = {
@@ -66843,6 +66842,7 @@ const DEFAULT_SURFACE_EXCLUDED_TAGS = {
66843
66842
  score: ["design"],
66844
66843
  ciFailure: ["design"]
66845
66844
  };
66845
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
66846
66846
  const VALID_RULE_SEVERITIES = [
66847
66847
  "error",
66848
66848
  "warn",
@@ -66885,7 +66885,6 @@ const SURFACE_CONTROL_FIELD_NAMES = [
66885
66885
  ];
66886
66886
  const isIncludedFileContext = (value) => VALID_INCLUDED_FILE_CONTEXTS.some((fileContext) => fileContext === value);
66887
66887
  const SEVERITY_FIELD_NAMES = ["rules", "categories"];
66888
- const isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
66889
66888
  const formatType = (value) => typeof value === "string" ? `"${value}"` : typeof value;
66890
66889
  const isRuleSeverity = (value) => typeof value === "string" && VALID_RULE_SEVERITIES.includes(value);
66891
66890
  const coerceMaybeBooleanString = (fieldName, value) => {
@@ -66913,7 +66912,7 @@ const validateStringArrayField = (fieldName, value) => {
66913
66912
  });
66914
66913
  };
66915
66914
  const validateSurfaceControls = (surface, rawControls) => {
66916
- if (!isPlainObject$1(rawControls)) {
66915
+ if (!isRecord(rawControls)) {
66917
66916
  warnConfigIssue(`config field "surfaces.${surface}" must be an object (got ${typeof rawControls}); ignoring this surface.`);
66918
66917
  return;
66919
66918
  }
@@ -66937,7 +66936,7 @@ const validateSurfaceControls = (surface, rawControls) => {
66937
66936
  return validatedSurfaceControls;
66938
66937
  };
66939
66938
  const validateSurfacesField = (rawSurfaces) => {
66940
- if (!isPlainObject$1(rawSurfaces)) {
66939
+ if (!isRecord(rawSurfaces)) {
66941
66940
  warnConfigIssue(`config field "surfaces" must be an object (got ${typeof rawSurfaces}); ignoring this field.`);
66942
66941
  return;
66943
66942
  }
@@ -66953,7 +66952,7 @@ const validateSurfacesField = (rawSurfaces) => {
66953
66952
  return validated;
66954
66953
  };
66955
66954
  const validateSeverityMap = (fieldName, rawMap, keysAreCategories = false) => {
66956
- if (!isPlainObject$1(rawMap)) {
66955
+ if (!isRecord(rawMap)) {
66957
66956
  warnConfigIssue(`config field "${fieldName}" must be an object (got ${typeof rawMap}); ignoring this field.`);
66958
66957
  return;
66959
66958
  }
@@ -69259,25 +69258,18 @@ const collectIgnorePatterns = (rootDirectory) => {
69259
69258
  cachedPatternsByRoot.set(rootDirectory, patterns);
69260
69259
  return patterns;
69261
69260
  };
69262
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
69263
- const KNIP_JSON_FILENAME = "knip.json";
69264
- const readJsonFileSafe = (filePath) => {
69265
- let rawContents;
69266
- try {
69267
- rawContents = fs.readFileSync(filePath, "utf-8");
69268
- } catch {
69269
- return null;
69270
- }
69261
+ const failOpenReadJson = (filePath, fallback) => {
69271
69262
  try {
69272
- return JSON.parse(rawContents);
69263
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
69273
69264
  } catch {
69274
- return null;
69265
+ return fallback;
69275
69266
  }
69276
69267
  };
69268
+ const KNIP_JSON_FILENAME = "knip.json";
69277
69269
  const readKnipConfig = (rootDirectory) => {
69278
- const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
69270
+ const knipJson = failOpenReadJson(path.join(rootDirectory, KNIP_JSON_FILENAME), null);
69279
69271
  if (isRecord(knipJson)) return knipJson;
69280
- const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
69272
+ const packageJson = failOpenReadJson(path.join(rootDirectory, "package.json"), null);
69281
69273
  const packageKnipConfig = isRecord(packageJson) ? packageJson.knip : null;
69282
69274
  return isRecord(packageKnipConfig) ? packageKnipConfig : null;
69283
69275
  };
@@ -69325,13 +69317,6 @@ const atomicWriteJson = (filePath, value) => {
69325
69317
  return;
69326
69318
  }
69327
69319
  };
69328
- const failOpenReadJson = (filePath, fallback) => {
69329
- try {
69330
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
69331
- } catch {
69332
- return fallback;
69333
- }
69334
- };
69335
69320
  const hashFileContents = (filePath) => {
69336
69321
  try {
69337
69322
  return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
@@ -71373,11 +71358,17 @@ const collectUnpluginAutoImportGlobalScopes = ({ rootDirectory, candidateFiles }
71373
71358
  return scopes.some((scope) => scope.names.length > 0) ? scopes : [];
71374
71359
  };
71375
71360
  const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
71361
+ const decodeDiagnostic = (raw) => {
71362
+ const diagnostic = validateDiagnostic$1(raw);
71363
+ return {
71364
+ ...diagnostic,
71365
+ relatedLocations: diagnostic.relatedLocations?.map((location) => ({ ...location }))
71366
+ };
71367
+ };
71376
71368
  const decodeFileDiagnostics = (raw) => {
71377
71369
  if (!Array.isArray(raw)) return null;
71378
71370
  try {
71379
- for (const entry of raw) validateDiagnostic$1(entry);
71380
- return raw;
71371
+ return raw.map((entry) => decodeDiagnostic(entry));
71381
71372
  } catch {
71382
71373
  return null;
71383
71374
  }
@@ -74672,10 +74663,6 @@ const NO_HOOKS = {
74672
74663
  beforeLint: () => void_,
74673
74664
  afterLint: () => void_
74674
74665
  };
74675
- const filterMapNullable = (transform) => fromPredicateOption((value) => {
74676
- const result = transform(value);
74677
- return result === null ? none() : some(result);
74678
- });
74679
74666
  const fileReader = (filesService, rootDirectory) => (filePath) => {
74680
74667
  const lines = runSync(filesService.readLines({
74681
74668
  filePath,
@@ -74699,7 +74686,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
74699
74686
  * The GitHub viewer-permission lookup is forked onto a background
74700
74687
  * fiber here and joined late (it feeds score metadata, not
74701
74688
  * diagnostics).
74702
- * 2. beforeLint hook (e.g. CLI renders the project-detection block)
74689
+ * 2. beforeLint hook (e.g. CLI records project telemetry)
74703
74690
  * 3. environment checks (reduced-motion + pnpm hardening +
74704
74691
  * expo/react-native), collected synchronously. The heavier
74705
74692
  * content-regex security scan is forked instead (like supply-chain
@@ -74712,13 +74699,11 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
74712
74699
  * `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
74713
74700
  * socket can't drag out its join; on timeout it fails open to no
74714
74701
  * diagnostics — the same outcome class as a Socket outage.
74715
- * 5. Linter.run runs; DeadCode.run runs concurrently (forked child
74716
- * fiber) ONLY when the memory gate has headroom to run the 8 GB
74717
- * dead-code child alongside the oxlint workers — or when overlap is
74718
- * forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
74719
- * runs sequentially after lint, exactly as it did pre-overlap. The
74720
- * fiber is joined (or interrupted, SIGKILLing its worker, on lint
74721
- * failure) before diagnostics are concatenated. The afterLint hook
74702
+ * 5. Linter.run runs; DeadCode.run forks only when
74703
+ * REACT_DOCTOR_DEAD_CODE_OVERLAP=on. The default "auto" and explicit
74704
+ * "off" paths stay sequential. The fiber is joined (or interrupted,
74705
+ * SIGKILLing its worker, on lint failure) before diagnostics are
74706
+ * concatenated. The afterLint hook
74722
74707
  * fires between lint and dead-code. Progress spinner labels AND the
74723
74708
  * final diagnostic / score order stay independent of execution
74724
74709
  * order, so terminal output is identical either way; supply-chain
@@ -74794,7 +74779,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74794
74779
  const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
74795
74780
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
74796
74781
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
74797
- yield* beforeLint(project, lintIncludePaths ?? void 0);
74782
+ yield* beforeLint(project);
74798
74783
  const isDiffMode = input.includePaths.length > 0;
74799
74784
  const showWarnings = input.warnings ?? resolvedConfig.config?.warnings ?? true;
74800
74785
  const transform = buildDiagnosticPipeline({
@@ -74804,7 +74789,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74804
74789
  respectInlineDisables: input.respectInlineDisables,
74805
74790
  showWarnings
74806
74791
  });
74807
- const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)));
74792
+ const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(fromPredicateOption((diagnostic) => {
74793
+ const filteredDiagnostic = transform.apply(diagnostic);
74794
+ return filteredDiagnostic === null ? none() : some(filteredDiagnostic);
74795
+ })));
74808
74796
  const applyPerElementPipeline = (rawStream) => filterPerElementPipeline(rawStream).pipe(tap((diagnostic) => reporterService.emit(diagnostic)));
74809
74797
  const envCollected = yield* runCollect(applyPerElementPipeline(fromIterable$1(isDiffMode ? [] : [
74810
74798
  ...checkReducedMotion(scanDirectory),
@@ -75359,14 +75347,6 @@ const runEditorScan = async (input) => {
75359
75347
  error: messageFromUnknown(error)
75360
75348
  };
75361
75349
  };
75362
- /**
75363
- * Back-compat alias: the streaming pipeline holds its caches in
75364
- * per-pipeline closures that are garbage-collected when the pipeline
75365
- * goes out of scope, so there is nothing to clear at module scope. The
75366
- * public CLI's `clearCaches()` still calls this for symmetry with the
75367
- * other `clear*` helpers.
75368
- */
75369
- const clearAutoSuppressionCaches = () => {};
75370
75350
  const lookupOwnRule = (rule) => Object.hasOwn(REACT_DOCTOR_RULE_REGISTRY, rule) ? REACT_DOCTOR_RULE_REGISTRY[rule] : void 0;
75371
75351
  /**
75372
75352
  * Returns presentation metadata for `<plugin>/<rule>`, or `null` when the
@@ -76034,10 +76014,7 @@ const createProjectGraph = (options) => {
76034
76014
  for (const root of roots) try {
76035
76015
  for (const workspacePackage of discoverReactSubprojects(root)) {
76036
76016
  const directory = normalizeDirectory(workspacePackage.directory);
76037
- if (!seen.has(directory)) seen.set(directory, {
76038
- directory,
76039
- name: workspacePackage.name
76040
- });
76017
+ if (!seen.has(directory)) seen.set(directory, { directory });
76041
76018
  }
76042
76019
  } catch (error) {
76043
76020
  logger.warn(`Project discovery failed for ${root}: ${messageFromUnknown(error)}`);
@@ -76062,8 +76039,8 @@ const createProjectGraph = (options) => {
76062
76039
  clearProjectCache();
76063
76040
  clearConfigCache();
76064
76041
  clearPackageJsonCache();
76042
+ clearPackageRoleCache();
76065
76043
  clearIgnorePatternsCache();
76066
- clearAutoSuppressionCaches();
76067
76044
  clearMinifiedFileCache();
76068
76045
  projects = null;
76069
76046
  }
@@ -76459,11 +76436,9 @@ const createScheduler = (options) => {
76459
76436
  timers.clear();
76460
76437
  queue.length = 0;
76461
76438
  };
76462
- const pendingCount = () => queue.length + running + timers.size;
76463
76439
  return {
76464
76440
  enqueue,
76465
76441
  cancelProject,
76466
- pendingCount,
76467
76442
  dispose
76468
76443
  };
76469
76444
  };
@@ -77163,6 +77138,9 @@ const METRIC = {
77163
77138
  */
77164
77139
  const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
77165
77140
  //#endregion
77141
+ //#region src/cli/utils/is-env-flag-enabled.ts
77142
+ const isEnvFlagEnabled = (value) => value === "1" || value?.toLowerCase() === "true";
77143
+ //#endregion
77166
77144
  //#region src/cli/utils/anonymize-text.ts
77167
77145
  /**
77168
77146
  * Free-text fields can carry both a home-directory path (the OS username) and a
@@ -77323,7 +77301,6 @@ const resolveTracesSampleRate = () => {
77323
77301
  * session.
77324
77302
  */
77325
77303
  let lspTracesSampleRate = 0;
77326
- const isEnvFlagEnabled = (value) => value === "1" || value?.toLowerCase() === "true";
77327
77304
  const nodeMajorVersion = () => Number.parseInt(process.versions.node.split(".", 1)[0] ?? "", 10) || 0;
77328
77305
  const shouldEnableLspTelemetry = () => {
77329
77306
  if (isEnvFlagEnabled(process.env.REACT_DOCTOR_NO_TELEMETRY)) return false;
@@ -77451,5 +77428,5 @@ const startLanguageServer = () => {
77451
77428
  };
77452
77429
  //#endregion
77453
77430
  export { startLanguageServer };
77454
- !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]="fceafa22-ed8a-5111-8924-bf8de3935968")}catch(e){}}();
77455
- //# debugId=fceafa22-ed8a-5111-8924-bf8de3935968
77431
+ !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]="02495a55-39cb-51a9-9d93-04929a1ac660")}catch(e){}}();
77432
+ //# debugId=02495a55-39cb-51a9-9d93-04929a1ac660
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.9.3-dev.ba04952",
3
+ "version": "0.9.3-dev.c825c59",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -73,7 +73,7 @@
73
73
  "vscode-uri": "^3.1.0",
74
74
  "yaml": "^2.9.0",
75
75
  "deslop-js": "0.9.3",
76
- "oxlint-plugin-react-doctor": "0.9.3-dev.ba04952"
76
+ "oxlint-plugin-react-doctor": "0.9.3-dev.c825c59"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@types/babel__code-frame": "^7.27.0",
@@ -83,9 +83,9 @@
83
83
  "commander": "^14.0.3",
84
84
  "ink-testing-library": "^4.0.0",
85
85
  "ora": "^9.4.0",
86
+ "@react-doctor/api": "0.9.3",
86
87
  "@react-doctor/core": "0.9.3",
87
- "@react-doctor/language-server": "0.9.3",
88
- "@react-doctor/api": "0.9.3"
88
+ "@react-doctor/language-server": "0.9.3"
89
89
  },
90
90
  "engines": {
91
91
  "node": "^20.19.0 || >=22.13.0"