react-doctor 0.0.31 → 0.0.33

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/index.d.ts CHANGED
@@ -43,6 +43,9 @@ interface ReactDoctorConfig {
43
43
  verbose?: boolean;
44
44
  diff?: boolean | string;
45
45
  failOn?: FailOnLevel;
46
+ customRulesOnly?: boolean;
47
+ share?: boolean;
48
+ textComponents?: string[];
46
49
  }
47
50
  //#endregion
48
51
  //#region src/utils/get-diff-files.d.ts
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,WAAA;AAAA,KAEA,SAAA;AAAA,UAUK,WAAA;EACf,aAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA,EAAW,SAAA;EACX,aAAA;EACA,gBAAA;EACA,eAAA;AAAA;AAAA,UAiCe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UA4Be,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAyBe,QAAA;EACf,aAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,UA2Ce,uBAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EACf,MAAA,GAAS,uBAAA;EACT,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA,GAAS,WAAA;AAAA;;;cChGE,WAAA,GAAe,SAAA,UAAmB,kBAAA,cAA8B,QAAA;AAAA,cAiBhE,iBAAA,GAAqB,SAAA;;;UClFjB,eAAA;EACf,IAAA;EACA,QAAA;EACA,YAAA;AAAA;AAAA,UAGe,cAAA;EACf,WAAA,EAAa,UAAA;EACb,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,WAAA;EACT,mBAAA;AAAA;AAAA,cAGW,QAAA,GACX,SAAA,UACA,OAAA,GAAS,eAAA,KACR,OAAA,CAAQ,cAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,WAAA;AAAA,KAEA,SAAA;AAAA,UAUK,WAAA;EACf,aAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA,EAAW,SAAA;EACX,aAAA;EACA,gBAAA;EACA,eAAA;AAAA;AAAA,UAiCe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UA4Be,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAmBe,QAAA;EACf,aAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,UA2Ce,uBAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EACf,MAAA,GAAS,uBAAA;EACT,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA,GAAS,WAAA;EACT,eAAA;EACA,KAAA;EACA,cAAA;AAAA;;;cC7FW,WAAA,GAAe,SAAA,UAAmB,kBAAA,cAA8B,QAAA;AAAA,cAiBhE,iBAAA,GAAqB,SAAA;;;UClFjB,eAAA;EACf,IAAA;EACA,QAAA;EACA,YAAA;AAAA;AAAA,UAGe,cAAA;EACf,WAAA,EAAa,UAAA;EACb,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,WAAA;EACT,mBAAA;AAAA;AAAA,cAGW,QAAA,GACX,SAAA,UACA,OAAA,GAAS,eAAA,KACR,OAAA,CAAQ,cAAA"}
package/dist/index.js CHANGED
@@ -19,20 +19,18 @@ const SCORE_API_URL = "https://www.react.doctor/api/score";
19
19
  const FETCH_TIMEOUT_MS = 1e4;
20
20
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
21
21
  const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
22
+ const OXLINT_MAX_FILES_PER_BATCH = 500;
22
23
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
23
24
  const ERROR_RULE_PENALTY = 1.5;
24
25
  const WARNING_RULE_PENALTY = .75;
25
- const ERROR_ESTIMATED_FIX_RATE = .85;
26
- const WARNING_ESTIMATED_FIX_RATE = .8;
27
26
  const MAX_KNIP_RETRIES = 5;
27
+ const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
28
28
  const IGNORED_DIRECTORIES = new Set([
29
29
  "node_modules",
30
30
  "dist",
31
31
  "build",
32
32
  "coverage"
33
33
  ]);
34
- const AMI_WEBSITE_URL = "https://ami.dev";
35
- const AMI_INSTALL_URL = `${AMI_WEBSITE_URL}/install.sh`;
36
34
 
37
35
  //#endregion
38
36
  //#region src/utils/proxy-fetch.ts
@@ -106,22 +104,12 @@ const scoreFromRuleCounts = (errorRuleCount, warningRuleCount) => {
106
104
  const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;
107
105
  return Math.max(0, Math.round(PERFECT_SCORE - penalty));
108
106
  };
109
- const estimateScoreLocally = (diagnostics) => {
110
- const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);
111
- const currentScore = scoreFromRuleCounts(errorRuleCount, warningRuleCount);
112
- const estimatedScore = scoreFromRuleCounts(Math.round(errorRuleCount * (1 - ERROR_ESTIMATED_FIX_RATE)), Math.round(warningRuleCount * (1 - WARNING_ESTIMATED_FIX_RATE)));
113
- return {
114
- currentScore,
115
- currentLabel: getScoreLabel(currentScore),
116
- estimatedScore,
117
- estimatedLabel: getScoreLabel(estimatedScore)
118
- };
119
- };
120
107
  const calculateScoreLocally = (diagnostics) => {
121
- const { currentScore, currentLabel } = estimateScoreLocally(diagnostics);
108
+ const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);
109
+ const score = scoreFromRuleCounts(errorRuleCount, warningRuleCount);
122
110
  return {
123
- score: currentScore,
124
- label: currentLabel
111
+ score,
112
+ label: getScoreLabel(score)
125
113
  };
126
114
  };
127
115
  const calculateScore = async (diagnostics) => {
@@ -158,6 +146,7 @@ const readPackageJson = (packageJsonPath) => {
158
146
  try {
159
147
  return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
160
148
  } catch (error) {
149
+ if (error instanceof SyntaxError) return {};
161
150
  if (error instanceof Error && "code" in error) {
162
151
  const { code } = error;
163
152
  if (code === "EISDIR" || code === "EACCES") return {};
@@ -253,38 +242,58 @@ const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
253
242
 
254
243
  //#endregion
255
244
  //#region src/utils/filter-diagnostics.ts
256
- const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory) => {
257
- const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
258
- const ignoredFilePatterns = compileIgnoredFilePatterns(config);
259
- if (ignoredRules.size === 0 && ignoredFilePatterns.length === 0) return diagnostics;
260
- return diagnostics.filter((diagnostic) => {
261
- const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
262
- if (ignoredRules.has(ruleIdentifier)) return false;
263
- if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
264
- return true;
265
- });
266
- };
245
+ const OPENING_TAG_PATTERN = /<([A-Z][\w.]*)/;
267
246
  const DISABLE_NEXT_LINE_PATTERN = /\/\/\s*react-doctor-disable-next-line\b(?:\s+(.+))?/;
268
247
  const DISABLE_LINE_PATTERN = /\/\/\s*react-doctor-disable-line\b(?:\s+(.+))?/;
269
- const isRuleSuppressed = (commentRules, ruleId) => {
270
- if (!commentRules?.trim()) return true;
271
- return commentRules.split(/[,\s]+/).some((rule) => rule.trim() === ruleId);
272
- };
273
- const filterInlineSuppressions = (diagnostics, rootDirectory) => {
274
- const fileLineCache = /* @__PURE__ */ new Map();
275
- const getFileLines = (filePath) => {
276
- const cached = fileLineCache.get(filePath);
248
+ const createFileLinesCache = (rootDirectory) => {
249
+ const cache = /* @__PURE__ */ new Map();
250
+ return (filePath) => {
251
+ const cached = cache.get(filePath);
277
252
  if (cached !== void 0) return cached;
278
253
  const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
279
254
  try {
280
255
  const lines = fs.readFileSync(absolutePath, "utf-8").split("\n");
281
- fileLineCache.set(filePath, lines);
256
+ cache.set(filePath, lines);
282
257
  return lines;
283
258
  } catch {
284
- fileLineCache.set(filePath, null);
259
+ cache.set(filePath, null);
285
260
  return null;
286
261
  }
287
262
  };
263
+ };
264
+ const isInsideTextComponent = (lines, diagnosticLine, textComponentNames) => {
265
+ for (let lineIndex = diagnosticLine - 1; lineIndex >= 0; lineIndex--) {
266
+ const match = lines[lineIndex].match(OPENING_TAG_PATTERN);
267
+ if (!match) continue;
268
+ const fullTagName = match[1];
269
+ const leafTagName = fullTagName.includes(".") ? fullTagName.split(".").at(-1) ?? fullTagName : fullTagName;
270
+ return textComponentNames.has(fullTagName) || textComponentNames.has(leafTagName);
271
+ }
272
+ return false;
273
+ };
274
+ const isRuleSuppressed = (commentRules, ruleId) => {
275
+ if (!commentRules?.trim()) return true;
276
+ return commentRules.split(/[,\s]+/).some((rule) => rule.trim() === ruleId);
277
+ };
278
+ const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory) => {
279
+ const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
280
+ const ignoredFilePatterns = compileIgnoredFilePatterns(config);
281
+ const textComponentNames = new Set(Array.isArray(config.textComponents) ? config.textComponents : []);
282
+ const hasTextComponents = textComponentNames.size > 0;
283
+ const getFileLines = createFileLinesCache(rootDirectory);
284
+ return diagnostics.filter((diagnostic) => {
285
+ const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
286
+ if (ignoredRules.has(ruleIdentifier)) return false;
287
+ if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
288
+ if (hasTextComponents && diagnostic.rule === "rn-no-raw-text" && diagnostic.line > 0) {
289
+ const lines = getFileLines(diagnostic.filePath);
290
+ if (lines && isInsideTextComponent(lines, diagnostic.line, textComponentNames)) return false;
291
+ }
292
+ return true;
293
+ });
294
+ };
295
+ const filterInlineSuppressions = (diagnostics, rootDirectory) => {
296
+ const getFileLines = createFileLinesCache(rootDirectory);
288
297
  return diagnostics.filter((diagnostic) => {
289
298
  if (diagnostic.line <= 0) return true;
290
299
  const lines = getFileLines(diagnostic.filePath);
@@ -296,9 +305,9 @@ const filterInlineSuppressions = (diagnostics, rootDirectory) => {
296
305
  if (lineMatch && isRuleSuppressed(lineMatch[1], ruleId)) return false;
297
306
  }
298
307
  if (diagnostic.line >= 2) {
299
- const prevLine = lines[diagnostic.line - 2];
300
- if (prevLine) {
301
- const nextLineMatch = prevLine.match(DISABLE_NEXT_LINE_PATTERN);
308
+ const previousLine = lines[diagnostic.line - 2];
309
+ if (previousLine) {
310
+ const nextLineMatch = previousLine.match(DISABLE_NEXT_LINE_PATTERN);
302
311
  if (nextLineMatch && isRuleSuppressed(nextLineMatch[1], ruleId)) return false;
303
312
  }
304
313
  }
@@ -685,8 +694,8 @@ const discoverProject = (directory) => {
685
694
  //#region src/utils/load-config.ts
686
695
  const CONFIG_FILENAME = "react-doctor.config.json";
687
696
  const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
688
- const loadConfig = (rootDirectory) => {
689
- const configFilePath = path.join(rootDirectory, CONFIG_FILENAME);
697
+ const loadConfigFromDirectory = (directory) => {
698
+ const configFilePath = path.join(directory, CONFIG_FILENAME);
690
699
  if (isFile(configFilePath)) try {
691
700
  const fileContent = fs.readFileSync(configFilePath, "utf-8");
692
701
  const parsed = JSON.parse(fileContent);
@@ -695,7 +704,7 @@ const loadConfig = (rootDirectory) => {
695
704
  } catch (error) {
696
705
  console.warn(`Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
697
706
  }
698
- const packageJsonPath = path.join(rootDirectory, "package.json");
707
+ const packageJsonPath = path.join(directory, "package.json");
699
708
  if (isFile(packageJsonPath)) try {
700
709
  const fileContent = fs.readFileSync(packageJsonPath, "utf-8");
701
710
  const embeddedConfig = JSON.parse(fileContent)[PACKAGE_JSON_CONFIG_KEY];
@@ -705,6 +714,17 @@ const loadConfig = (rootDirectory) => {
705
714
  }
706
715
  return null;
707
716
  };
717
+ const loadConfig = (rootDirectory) => {
718
+ const localConfig = loadConfigFromDirectory(rootDirectory);
719
+ if (localConfig) return localConfig;
720
+ let ancestorDirectory = path.dirname(rootDirectory);
721
+ while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
722
+ const ancestorConfig = loadConfigFromDirectory(ancestorDirectory);
723
+ if (ancestorConfig) return ancestorConfig;
724
+ ancestorDirectory = path.dirname(ancestorDirectory);
725
+ }
726
+ return null;
727
+ };
708
728
 
709
729
  //#endregion
710
730
  //#region src/utils/resolve-lint-include-paths.ts
@@ -915,7 +935,37 @@ const REACT_COMPILER_RULES = {
915
935
  "react-hooks-js/incompatible-library": "error",
916
936
  "react-hooks-js/todo": "error"
917
937
  };
918
- const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
938
+ const BUILTIN_REACT_RULES = {
939
+ "react/rules-of-hooks": "error",
940
+ "react/no-direct-mutation-state": "error",
941
+ "react/jsx-no-duplicate-props": "error",
942
+ "react/jsx-key": "error",
943
+ "react/no-children-prop": "warn",
944
+ "react/no-danger": "warn",
945
+ "react/jsx-no-script-url": "error",
946
+ "react/no-render-return-value": "warn",
947
+ "react/no-string-refs": "warn",
948
+ "react/no-is-mounted": "warn",
949
+ "react/require-render-return": "error",
950
+ "react/no-unknown-property": "warn"
951
+ };
952
+ const BUILTIN_A11Y_RULES = {
953
+ "jsx-a11y/alt-text": "error",
954
+ "jsx-a11y/anchor-is-valid": "warn",
955
+ "jsx-a11y/click-events-have-key-events": "warn",
956
+ "jsx-a11y/no-static-element-interactions": "warn",
957
+ "jsx-a11y/role-has-required-aria-props": "error",
958
+ "jsx-a11y/no-autofocus": "warn",
959
+ "jsx-a11y/heading-has-content": "warn",
960
+ "jsx-a11y/html-has-lang": "warn",
961
+ "jsx-a11y/no-redundant-roles": "warn",
962
+ "jsx-a11y/scope": "warn",
963
+ "jsx-a11y/tabindex-no-positive": "warn",
964
+ "jsx-a11y/label-has-associated-control": "warn",
965
+ "jsx-a11y/no-distracting-elements": "error",
966
+ "jsx-a11y/iframe-has-title": "warn"
967
+ };
968
+ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRulesOnly = false }) => ({
919
969
  categories: {
920
970
  correctness: "off",
921
971
  suspicious: "off",
@@ -930,39 +980,14 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
930
980
  "jsx-a11y",
931
981
  ...hasReactCompiler ? [] : ["react-perf"]
932
982
  ],
933
- jsPlugins: [...hasReactCompiler ? [{
983
+ jsPlugins: [...hasReactCompiler && !customRulesOnly ? [{
934
984
  name: "react-hooks-js",
935
985
  specifier: esmRequire$1.resolve("eslint-plugin-react-hooks")
936
986
  }] : [], pluginPath],
937
987
  rules: {
938
- "react/rules-of-hooks": "error",
939
- "react/no-direct-mutation-state": "error",
940
- "react/jsx-no-duplicate-props": "error",
941
- "react/jsx-key": "error",
942
- "react/no-children-prop": "warn",
943
- "react/no-danger": "warn",
944
- "react/jsx-no-script-url": "error",
945
- "react/no-render-return-value": "warn",
946
- "react/no-string-refs": "warn",
947
- "react/no-is-mounted": "warn",
948
- "react/require-render-return": "error",
949
- "react/no-unknown-property": "warn",
950
- "jsx-a11y/alt-text": "error",
951
- "jsx-a11y/anchor-is-valid": "warn",
952
- "jsx-a11y/click-events-have-key-events": "warn",
953
- "jsx-a11y/no-static-element-interactions": "warn",
954
- "jsx-a11y/no-noninteractive-element-interactions": "warn",
955
- "jsx-a11y/role-has-required-aria-props": "error",
956
- "jsx-a11y/no-autofocus": "warn",
957
- "jsx-a11y/heading-has-content": "warn",
958
- "jsx-a11y/html-has-lang": "warn",
959
- "jsx-a11y/no-redundant-roles": "warn",
960
- "jsx-a11y/scope": "warn",
961
- "jsx-a11y/tabindex-no-positive": "warn",
962
- "jsx-a11y/label-has-associated-control": "warn",
963
- "jsx-a11y/no-distracting-elements": "error",
964
- "jsx-a11y/iframe-has-title": "warn",
965
- ...hasReactCompiler ? REACT_COMPILER_RULES : {},
988
+ ...customRulesOnly ? {} : BUILTIN_REACT_RULES,
989
+ ...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
990
+ ...hasReactCompiler && !customRulesOnly ? REACT_COMPILER_RULES : {},
966
991
  "react-doctor/no-derived-state-effect": "error",
967
992
  "react-doctor/no-fetch-in-effect": "error",
968
993
  "react-doctor/no-cascading-set-state": "warn",
@@ -1233,7 +1258,9 @@ const batchIncludePaths = (baseArgs, includePaths) => {
1233
1258
  let currentBatchLength = baseArgsLength;
1234
1259
  for (const filePath of includePaths) {
1235
1260
  const entryLength = filePath.length + 1;
1236
- if (currentBatch.length > 0 && currentBatchLength + entryLength > SPAWN_ARGS_MAX_LENGTH_CHARS) {
1261
+ const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > SPAWN_ARGS_MAX_LENGTH_CHARS;
1262
+ const exceedsFileCount = currentBatch.length >= OXLINT_MAX_FILES_PER_BATCH;
1263
+ if (exceedsArgLength || exceedsFileCount) {
1237
1264
  batches.push(currentBatch);
1238
1265
  currentBatch = [];
1239
1266
  currentBatchLength = baseArgsLength;
@@ -1295,13 +1322,14 @@ const parseOxlintOutput = (stdout) => {
1295
1322
  };
1296
1323
  });
1297
1324
  };
1298
- const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompiler, includePaths, nodeBinaryPath = process.execPath) => {
1325
+ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompiler, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false) => {
1299
1326
  if (includePaths !== void 0 && includePaths.length === 0) return [];
1300
1327
  const configPath = path.join(os.tmpdir(), `react-doctor-oxlintrc-${process.pid}.json`);
1301
1328
  const config = createOxlintConfig({
1302
1329
  pluginPath: resolvePluginPath(),
1303
1330
  framework,
1304
- hasReactCompiler
1331
+ hasReactCompiler,
1332
+ customRulesOnly
1305
1333
  });
1306
1334
  const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory, includePaths);
1307
1335
  try {
@@ -1421,7 +1449,8 @@ const diagnose = async (directory, options = {}) => {
1421
1449
  if (!projectInfo.reactVersion) throw new Error("No React dependency found in package.json");
1422
1450
  const lintIncludePaths = computeJsxIncludePaths(includePaths) ?? resolveLintIncludePaths(resolvedDirectory, userConfig);
1423
1451
  const emptyDiagnostics = [];
1424
- const lintPromise = effectiveLint ? runOxlint(resolvedDirectory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler, lintIncludePaths).catch((error) => {
1452
+ const effectiveCustomRulesOnly = userConfig?.customRulesOnly ?? false;
1453
+ const lintPromise = effectiveLint ? runOxlint(resolvedDirectory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler, lintIncludePaths, void 0, effectiveCustomRulesOnly).catch((error) => {
1425
1454
  console.error("Lint failed:", error);
1426
1455
  return emptyDiagnostics;
1427
1456
  }) : Promise.resolve(emptyDiagnostics);