react-doctor 0.0.18 → 0.0.19

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
@@ -25,11 +25,32 @@ interface ScoreResult {
25
25
  score: number;
26
26
  label: string;
27
27
  }
28
+ interface DiffInfo {
29
+ currentBranch: string;
30
+ baseBranch: string;
31
+ changedFiles: string[];
32
+ }
33
+ interface ReactDoctorIgnoreConfig {
34
+ rules?: string[];
35
+ files?: string[];
36
+ }
37
+ interface ReactDoctorConfig {
38
+ ignore?: ReactDoctorIgnoreConfig;
39
+ lint?: boolean;
40
+ deadCode?: boolean;
41
+ verbose?: boolean;
42
+ diff?: boolean | string;
43
+ }
44
+ //#endregion
45
+ //#region src/utils/get-diff-files.d.ts
46
+ declare const getDiffInfo: (directory: string, explicitBaseBranch?: string) => DiffInfo | null;
47
+ declare const filterSourceFiles: (filePaths: string[]) => string[];
28
48
  //#endregion
29
49
  //#region src/index.d.ts
30
50
  interface DiagnoseOptions {
31
51
  lint?: boolean;
32
52
  deadCode?: boolean;
53
+ includePaths?: string[];
33
54
  }
34
55
  interface DiagnoseResult {
35
56
  diagnostics: Diagnostic[];
@@ -39,5 +60,5 @@ interface DiagnoseResult {
39
60
  }
40
61
  declare const diagnose: (directory: string, options?: DiagnoseOptions) => Promise<DiagnoseResult>;
41
62
  //#endregion
42
- export { DiagnoseOptions, DiagnoseResult, type Diagnostic, type ProjectInfo, type ScoreResult, diagnose };
63
+ export { DiagnoseOptions, DiagnoseResult, type Diagnostic, type DiffInfo, type ProjectInfo, type ReactDoctorConfig, type ScoreResult, diagnose, filterSourceFiles, getDiffInfo };
43
64
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":";KAAY,SAAA;AAAA,UAEK,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,UA2Be,WAAA;EACf,KAAA;EACA,KAAA;AAAA;;;UCtEe,eAAA;EACf,IAAA;EACA,QAAA;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,SAAA;AAAA,UAEK,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,UAWe,QAAA;EACf,aAAA;EACA,UAAA;EACA,YAAA;AAAA;AAAA,UAqDe,uBAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EACf,MAAA,GAAS,uBAAA;EACT,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;AAAA;;;cC/FW,WAAA,GAAe,SAAA,UAAmB,kBAAA,cAA8B,QAAA;AAAA,cAYhE,iBAAA,GAAqB,SAAA;;;UC7DjB,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
@@ -14,6 +14,7 @@ const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
14
14
  const ERROR_PREVIEW_LENGTH_CHARS = 200;
15
15
  const SCORE_API_URL = "https://www.react.doctor/api/score";
16
16
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
17
+ const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
17
18
 
18
19
  //#endregion
19
20
  //#region src/utils/calculate-score.ts
@@ -136,6 +137,7 @@ const countSourceFiles = (rootDirectory) => {
136
137
  return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;
137
138
  };
138
139
  const collectAllDependencies = (packageJson) => ({
140
+ ...packageJson.peerDependencies,
139
141
  ...packageJson.dependencies,
140
142
  ...packageJson.devDependencies
141
143
  });
@@ -286,6 +288,79 @@ const discoverProject = (directory) => {
286
288
  };
287
289
  };
288
290
 
291
+ //#endregion
292
+ //#region src/utils/match-glob-pattern.ts
293
+ const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
294
+ const compileGlobPattern = (pattern) => {
295
+ const normalizedPattern = pattern.replace(/\\/g, "/");
296
+ let regexSource = "^";
297
+ let characterIndex = 0;
298
+ while (characterIndex < normalizedPattern.length) if (normalizedPattern[characterIndex] === "*" && normalizedPattern[characterIndex + 1] === "*") if (normalizedPattern[characterIndex + 2] === "/") {
299
+ regexSource += "(?:.+/)?";
300
+ characterIndex += 3;
301
+ } else {
302
+ regexSource += ".*";
303
+ characterIndex += 2;
304
+ }
305
+ else if (normalizedPattern[characterIndex] === "*") {
306
+ regexSource += "[^/]*";
307
+ characterIndex++;
308
+ } else if (normalizedPattern[characterIndex] === "?") {
309
+ regexSource += "[^/]";
310
+ characterIndex++;
311
+ } else {
312
+ regexSource += normalizedPattern[characterIndex].replace(REGEX_SPECIAL_CHARACTERS, "\\$&");
313
+ characterIndex++;
314
+ }
315
+ regexSource += "$";
316
+ return new RegExp(regexSource);
317
+ };
318
+
319
+ //#endregion
320
+ //#region src/utils/filter-diagnostics.ts
321
+ const filterIgnoredDiagnostics = (diagnostics, config) => {
322
+ const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
323
+ const ignoredFilePatterns = Array.isArray(config.ignore?.files) ? config.ignore.files.map(compileGlobPattern) : [];
324
+ if (ignoredRules.size === 0 && ignoredFilePatterns.length === 0) return diagnostics;
325
+ return diagnostics.filter((diagnostic) => {
326
+ const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
327
+ if (ignoredRules.has(ruleIdentifier)) return false;
328
+ const normalizedPath = diagnostic.filePath.replace(/\\/g, "/");
329
+ if (ignoredFilePatterns.some((pattern) => pattern.test(normalizedPath))) return false;
330
+ return true;
331
+ });
332
+ };
333
+
334
+ //#endregion
335
+ //#region src/utils/load-config.ts
336
+ const CONFIG_FILENAME = "react-doctor.config.json";
337
+ const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
338
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
339
+ const loadConfig = (rootDirectory) => {
340
+ const configFilePath = path.join(rootDirectory, CONFIG_FILENAME);
341
+ if (fs.existsSync(configFilePath)) try {
342
+ const fileContent = fs.readFileSync(configFilePath, "utf-8");
343
+ const parsed = JSON.parse(fileContent);
344
+ if (!isPlainObject(parsed)) {
345
+ console.warn(`Warning: ${CONFIG_FILENAME} must be a JSON object, ignoring.`);
346
+ return null;
347
+ }
348
+ return parsed;
349
+ } catch (error) {
350
+ console.warn(`Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
351
+ return null;
352
+ }
353
+ const packageJsonPath = path.join(rootDirectory, "package.json");
354
+ if (fs.existsSync(packageJsonPath)) try {
355
+ const fileContent = fs.readFileSync(packageJsonPath, "utf-8");
356
+ const embeddedConfig = JSON.parse(fileContent)[PACKAGE_JSON_CONFIG_KEY];
357
+ if (isPlainObject(embeddedConfig)) return embeddedConfig;
358
+ } catch {
359
+ return null;
360
+ }
361
+ return null;
362
+ };
363
+
289
364
  //#endregion
290
365
  //#region src/utils/run-knip.ts
291
366
  const KNIP_CATEGORY_MAP = {
@@ -702,7 +777,8 @@ const resolvePluginPath = () => {
702
777
  const resolveDiagnosticCategory = (plugin, rule) => {
703
778
  return RULE_CATEGORY_MAP[`${plugin}/${rule}`] ?? PLUGIN_CATEGORY_MAP[plugin] ?? "Other";
704
779
  };
705
- const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompiler) => {
780
+ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompiler, includePaths) => {
781
+ if (includePaths !== void 0 && includePaths.length === 0) return [];
706
782
  const configPath = path.join(os.tmpdir(), `react-doctor-oxlintrc-${process.pid}.json`);
707
783
  const config = createOxlintConfig({
708
784
  pluginPath: resolvePluginPath(),
@@ -719,7 +795,8 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
719
795
  "json"
720
796
  ];
721
797
  if (hasTypeScript) args.push("--tsconfig", "./tsconfig.json");
722
- args.push(".");
798
+ if (includePaths !== void 0) args.push(...includePaths);
799
+ else args.push(".");
723
800
  const stdout = await new Promise((resolve, reject) => {
724
801
  const child = spawn(process.execPath, args, { cwd: rootDirectory });
725
802
  const stdoutBuffers = [];
@@ -746,7 +823,7 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
746
823
  } catch {
747
824
  throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, ERROR_PREVIEW_LENGTH_CHARS)}`);
748
825
  }
749
- return output.diagnostics.filter((diagnostic) => JSX_FILE_PATTERN.test(diagnostic.filename)).map((diagnostic) => {
826
+ return output.diagnostics.filter((diagnostic) => diagnostic.code && JSX_FILE_PATTERN.test(diagnostic.filename)).map((diagnostic) => {
750
827
  const { plugin, rule } = parseRuleCode(diagnostic.code);
751
828
  const primaryLabel = diagnostic.labels[0];
752
829
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule);
@@ -767,28 +844,93 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
767
844
  }
768
845
  };
769
846
 
847
+ //#endregion
848
+ //#region src/utils/get-diff-files.ts
849
+ const getCurrentBranch = (directory) => {
850
+ try {
851
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
852
+ cwd: directory,
853
+ stdio: "pipe"
854
+ }).toString().trim();
855
+ return branch === "HEAD" ? null : branch;
856
+ } catch {
857
+ return null;
858
+ }
859
+ };
860
+ const detectDefaultBranch = (directory) => {
861
+ try {
862
+ return execSync("git symbolic-ref refs/remotes/origin/HEAD", {
863
+ cwd: directory,
864
+ stdio: "pipe"
865
+ }).toString().trim().replace("refs/remotes/origin/", "");
866
+ } catch {
867
+ for (const candidate of DEFAULT_BRANCH_CANDIDATES) try {
868
+ execSync(`git rev-parse --verify ${candidate}`, {
869
+ cwd: directory,
870
+ stdio: "pipe"
871
+ });
872
+ return candidate;
873
+ } catch {}
874
+ return null;
875
+ }
876
+ };
877
+ const getChangedFilesSinceBranch = (directory, baseBranch) => {
878
+ try {
879
+ const output = execSync(`git diff --name-only --diff-filter=ACMR --relative ${execSync(`git merge-base ${baseBranch} HEAD`, {
880
+ cwd: directory,
881
+ stdio: "pipe"
882
+ }).toString().trim()}`, {
883
+ cwd: directory,
884
+ stdio: "pipe"
885
+ }).toString().trim();
886
+ if (!output) return [];
887
+ return output.split("\n").filter(Boolean);
888
+ } catch {
889
+ return [];
890
+ }
891
+ };
892
+ const getDiffInfo = (directory, explicitBaseBranch) => {
893
+ const currentBranch = getCurrentBranch(directory);
894
+ if (!currentBranch) return null;
895
+ const baseBranch = explicitBaseBranch ?? detectDefaultBranch(directory);
896
+ if (!baseBranch) return null;
897
+ if (currentBranch === baseBranch) return null;
898
+ return {
899
+ currentBranch,
900
+ baseBranch,
901
+ changedFiles: getChangedFilesSinceBranch(directory, baseBranch)
902
+ };
903
+ };
904
+ const filterSourceFiles = (filePaths) => filePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));
905
+
770
906
  //#endregion
771
907
  //#region src/index.ts
772
908
  const diagnose = async (directory, options = {}) => {
773
- const { lint = true, deadCode = true } = options;
909
+ const { includePaths = [] } = options;
910
+ const isDiffMode = includePaths.length > 0;
774
911
  const startTime = performance.now();
775
912
  const resolvedDirectory = path.resolve(directory);
776
913
  const projectInfo = discoverProject(resolvedDirectory);
914
+ const userConfig = loadConfig(resolvedDirectory);
915
+ const effectiveLint = options.lint ?? userConfig?.lint ?? true;
916
+ const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;
777
917
  if (!projectInfo.reactVersion) throw new Error("No React dependency found in package.json");
778
- const lintPromise = lint ? runOxlint(resolvedDirectory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler).catch((error) => {
918
+ const jsxIncludePaths = isDiffMode ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
919
+ const lintPromise = effectiveLint ? runOxlint(resolvedDirectory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler, jsxIncludePaths).catch((error) => {
779
920
  console.error("Lint failed:", error);
780
921
  return [];
781
922
  }) : Promise.resolve([]);
782
- const deadCodePromise = deadCode ? runKnip(resolvedDirectory).catch((error) => {
923
+ const deadCodePromise = effectiveDeadCode && !isDiffMode ? runKnip(resolvedDirectory).catch((error) => {
783
924
  console.error("Dead code analysis failed:", error);
784
925
  return [];
785
926
  }) : Promise.resolve([]);
786
927
  const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);
787
- const diagnostics = [
928
+ const allDiagnostics = [
788
929
  ...lintDiagnostics,
789
930
  ...deadCodeDiagnostics,
790
- ...checkReducedMotion(resolvedDirectory)
931
+ ...isDiffMode ? [] : checkReducedMotion(resolvedDirectory)
791
932
  ];
933
+ const diagnostics = userConfig ? filterIgnoredDiagnostics(allDiagnostics, userConfig) : allDiagnostics;
792
934
  const elapsedMilliseconds = performance.now() - startTime;
793
935
  return {
794
936
  diagnostics,
@@ -799,5 +941,5 @@ const diagnose = async (directory, options = {}) => {
799
941
  };
800
942
 
801
943
  //#endregion
802
- export { diagnose };
944
+ export { diagnose, filterSourceFiles, getDiffInfo };
803
945
  //# sourceMappingURL=index.js.map