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/README.md +41 -0
- package/dist/cli.js +405 -204
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +151 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
|
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 =
|
|
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
|
|
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
|