dep-brain 0.1.0
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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/depbrain.config.json +20 -0
- package/depbrain.config.schema.json +38 -0
- package/dist/checks/duplicate.d.ts +3 -0
- package/dist/checks/duplicate.js +10 -0
- package/dist/checks/outdated.d.ts +6 -0
- package/dist/checks/outdated.js +51 -0
- package/dist/checks/risk.d.ts +3 -0
- package/dist/checks/risk.js +31 -0
- package/dist/checks/unused.d.ts +3 -0
- package/dist/checks/unused.js +120 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +118 -0
- package/dist/core/analyzer.d.ts +60 -0
- package/dist/core/analyzer.js +158 -0
- package/dist/core/graph-builder.d.ts +14 -0
- package/dist/core/graph-builder.js +63 -0
- package/dist/core/scorer.d.ts +7 -0
- package/dist/core/scorer.js +8 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/reporters/console.d.ts +2 -0
- package/dist/reporters/console.js +51 -0
- package/dist/reporters/json.d.ts +2 -0
- package/dist/reporters/json.js +3 -0
- package/dist/utils/config.d.ts +27 -0
- package/dist/utils/config.js +66 -0
- package/dist/utils/file-parser.d.ts +3 -0
- package/dist/utils/file-parser.js +27 -0
- package/dist/utils/npm-api.d.ts +8 -0
- package/dist/utils/npm-api.js +52 -0
- package/dist/utils/workspaces.d.ts +6 -0
- package/dist/utils/workspaces.js +68 -0
- package/package.json +53 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type DepBrainConfig, type DepBrainConfigOverrides } from "../utils/config.js";
|
|
2
|
+
export interface AnalysisOptions {
|
|
3
|
+
rootDir?: string;
|
|
4
|
+
configPath?: string;
|
|
5
|
+
config?: DepBrainConfigOverrides;
|
|
6
|
+
}
|
|
7
|
+
export interface DuplicateInstance {
|
|
8
|
+
path: string;
|
|
9
|
+
version: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DuplicateDependency {
|
|
12
|
+
name: string;
|
|
13
|
+
versions: string[];
|
|
14
|
+
instances: DuplicateInstance[];
|
|
15
|
+
}
|
|
16
|
+
export interface UnusedDependency {
|
|
17
|
+
name: string;
|
|
18
|
+
section: "dependencies" | "devDependencies";
|
|
19
|
+
package?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface OutdatedDependency {
|
|
22
|
+
name: string;
|
|
23
|
+
current: string;
|
|
24
|
+
latest: string;
|
|
25
|
+
updateType: "major" | "minor" | "patch" | "unknown";
|
|
26
|
+
package?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface RiskDependency {
|
|
29
|
+
name: string;
|
|
30
|
+
reasons: string[];
|
|
31
|
+
package?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface AnalysisResult {
|
|
34
|
+
rootDir: string;
|
|
35
|
+
score: number;
|
|
36
|
+
policy: PolicyResult;
|
|
37
|
+
duplicates: DuplicateDependency[];
|
|
38
|
+
unused: UnusedDependency[];
|
|
39
|
+
outdated: OutdatedDependency[];
|
|
40
|
+
risks: RiskDependency[];
|
|
41
|
+
suggestions: string[];
|
|
42
|
+
config: DepBrainConfig;
|
|
43
|
+
packages?: PackageAnalysisResult[];
|
|
44
|
+
}
|
|
45
|
+
export interface PolicyResult {
|
|
46
|
+
passed: boolean;
|
|
47
|
+
reasons: string[];
|
|
48
|
+
}
|
|
49
|
+
export interface PackageAnalysisResult {
|
|
50
|
+
name: string;
|
|
51
|
+
rootDir: string;
|
|
52
|
+
score: number;
|
|
53
|
+
policy: PolicyResult;
|
|
54
|
+
duplicates: DuplicateDependency[];
|
|
55
|
+
unused: UnusedDependency[];
|
|
56
|
+
outdated: OutdatedDependency[];
|
|
57
|
+
risks: RiskDependency[];
|
|
58
|
+
suggestions: string[];
|
|
59
|
+
}
|
|
60
|
+
export declare function analyzeProject(options?: AnalysisOptions): Promise<AnalysisResult>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { findDuplicateDependencies } from "../checks/duplicate.js";
|
|
3
|
+
import { findOutdatedDependencies } from "../checks/outdated.js";
|
|
4
|
+
import { findRiskDependencies } from "../checks/risk.js";
|
|
5
|
+
import { findUnusedDependencies } from "../checks/unused.js";
|
|
6
|
+
import { loadDepBrainConfig } from "../utils/config.js";
|
|
7
|
+
import { findWorkspacePackages } from "../utils/workspaces.js";
|
|
8
|
+
import { buildDependencyGraph } from "./graph-builder.js";
|
|
9
|
+
import { calculateHealthScore } from "./scorer.js";
|
|
10
|
+
export async function analyzeProject(options = {}) {
|
|
11
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
12
|
+
const loadedConfig = await loadDepBrainConfig(rootDir, options.configPath);
|
|
13
|
+
const config = mergeConfig(loadedConfig, options.config);
|
|
14
|
+
const workspaces = await findWorkspacePackages(rootDir);
|
|
15
|
+
if (workspaces.length === 0) {
|
|
16
|
+
return analyzeSingleProject(rootDir, config);
|
|
17
|
+
}
|
|
18
|
+
const rootGraph = await buildDependencyGraph(rootDir);
|
|
19
|
+
const rawDuplicates = await findDuplicateDependencies(rootGraph);
|
|
20
|
+
const duplicates = rawDuplicates.filter((item) => !config.ignore.duplicates.includes(item.name));
|
|
21
|
+
const packages = [];
|
|
22
|
+
for (const workspace of workspaces) {
|
|
23
|
+
const result = await analyzeSingleProject(workspace.rootDir, config, {
|
|
24
|
+
packageName: workspace.name
|
|
25
|
+
});
|
|
26
|
+
packages.push({ ...result, name: workspace.name });
|
|
27
|
+
}
|
|
28
|
+
const unused = packages.flatMap((pkg) => pkg.unused.map((item) => ({ ...item, package: pkg.name })));
|
|
29
|
+
const outdated = packages.flatMap((pkg) => pkg.outdated.map((item) => ({ ...item, package: pkg.name })));
|
|
30
|
+
const risks = packages.flatMap((pkg) => pkg.risks.map((item) => ({ ...item, package: pkg.name })));
|
|
31
|
+
const score = calculateHealthScore({
|
|
32
|
+
duplicates: duplicates.length,
|
|
33
|
+
unused: unused.length,
|
|
34
|
+
outdated: outdated.length,
|
|
35
|
+
risks: risks.length
|
|
36
|
+
});
|
|
37
|
+
const suggestions = [
|
|
38
|
+
...packages.flatMap((pkg) => pkg.suggestions.map((suggestion) => `[${pkg.name}] ${suggestion}`))
|
|
39
|
+
].slice(0, config.report.maxSuggestions);
|
|
40
|
+
const policy = evaluatePolicy({
|
|
41
|
+
score,
|
|
42
|
+
duplicates: duplicates.length,
|
|
43
|
+
unused: unused.length,
|
|
44
|
+
outdated: outdated.length,
|
|
45
|
+
risks: risks.length
|
|
46
|
+
}, config);
|
|
47
|
+
return {
|
|
48
|
+
rootDir,
|
|
49
|
+
score,
|
|
50
|
+
policy,
|
|
51
|
+
duplicates,
|
|
52
|
+
unused,
|
|
53
|
+
outdated,
|
|
54
|
+
risks,
|
|
55
|
+
suggestions,
|
|
56
|
+
config,
|
|
57
|
+
packages
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function mergeConfig(base, overrides) {
|
|
61
|
+
if (!overrides) {
|
|
62
|
+
return base;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
ignore: {
|
|
66
|
+
dependencies: overrides.ignore?.dependencies ?? base.ignore.dependencies,
|
|
67
|
+
devDependencies: overrides.ignore?.devDependencies ?? base.ignore.devDependencies,
|
|
68
|
+
duplicates: overrides.ignore?.duplicates ?? base.ignore.duplicates,
|
|
69
|
+
outdated: overrides.ignore?.outdated ?? base.ignore.outdated,
|
|
70
|
+
risks: overrides.ignore?.risks ?? base.ignore.risks,
|
|
71
|
+
unused: overrides.ignore?.unused ?? base.ignore.unused
|
|
72
|
+
},
|
|
73
|
+
policy: {
|
|
74
|
+
minScore: overrides.policy?.minScore ?? base.policy.minScore,
|
|
75
|
+
failOnDuplicates: overrides.policy?.failOnDuplicates ?? base.policy.failOnDuplicates,
|
|
76
|
+
failOnOutdated: overrides.policy?.failOnOutdated ?? base.policy.failOnOutdated,
|
|
77
|
+
failOnRisks: overrides.policy?.failOnRisks ?? base.policy.failOnRisks,
|
|
78
|
+
failOnUnused: overrides.policy?.failOnUnused ?? base.policy.failOnUnused
|
|
79
|
+
},
|
|
80
|
+
report: {
|
|
81
|
+
maxSuggestions: overrides.report?.maxSuggestions ?? base.report.maxSuggestions
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function evaluatePolicy(summary, config) {
|
|
86
|
+
const reasons = [];
|
|
87
|
+
if (summary.score < config.policy.minScore) {
|
|
88
|
+
reasons.push(`Score ${summary.score} is below minimum ${config.policy.minScore}`);
|
|
89
|
+
}
|
|
90
|
+
if (config.policy.failOnDuplicates && summary.duplicates > 0) {
|
|
91
|
+
reasons.push(`Found ${summary.duplicates} duplicate dependencies`);
|
|
92
|
+
}
|
|
93
|
+
if (config.policy.failOnUnused && summary.unused > 0) {
|
|
94
|
+
reasons.push(`Found ${summary.unused} unused dependencies`);
|
|
95
|
+
}
|
|
96
|
+
if (config.policy.failOnOutdated && summary.outdated > 0) {
|
|
97
|
+
reasons.push(`Found ${summary.outdated} outdated dependencies`);
|
|
98
|
+
}
|
|
99
|
+
if (config.policy.failOnRisks && summary.risks > 0) {
|
|
100
|
+
reasons.push(`Found ${summary.risks} risky dependencies`);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
passed: reasons.length === 0,
|
|
104
|
+
reasons
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function analyzeSingleProject(rootDir, config, options = {}) {
|
|
108
|
+
const graph = await buildDependencyGraph(rootDir);
|
|
109
|
+
const [rawDuplicates, rawUnused, rawOutdated, rawRisks] = await Promise.all([
|
|
110
|
+
findDuplicateDependencies(graph),
|
|
111
|
+
findUnusedDependencies(rootDir, graph),
|
|
112
|
+
findOutdatedDependencies(graph),
|
|
113
|
+
findRiskDependencies(graph)
|
|
114
|
+
]);
|
|
115
|
+
const duplicates = rawDuplicates.filter((item) => !config.ignore.duplicates.includes(item.name));
|
|
116
|
+
const unused = rawUnused.filter((item) => !config.ignore.unused.includes(item.name) &&
|
|
117
|
+
!config.ignore[item.section].includes(item.name));
|
|
118
|
+
const outdated = rawOutdated.filter((item) => !config.ignore.outdated.includes(item.name));
|
|
119
|
+
const risks = rawRisks.filter((item) => !config.ignore.risks.includes(item.name));
|
|
120
|
+
const score = calculateHealthScore({
|
|
121
|
+
duplicates: duplicates.length,
|
|
122
|
+
unused: unused.length,
|
|
123
|
+
outdated: outdated.length,
|
|
124
|
+
risks: risks.length
|
|
125
|
+
});
|
|
126
|
+
const suggestions = [
|
|
127
|
+
...unused.map((item) => `Remove ${item.name} from ${item.section}`),
|
|
128
|
+
...duplicates.map((item) => `Consider consolidating ${item.name} to one version`),
|
|
129
|
+
...outdated.map((item) => `Review ${item.name}: ${item.current} -> ${item.latest} (${item.updateType})`)
|
|
130
|
+
].slice(0, config.report.maxSuggestions);
|
|
131
|
+
const policy = evaluatePolicy({
|
|
132
|
+
score,
|
|
133
|
+
duplicates: duplicates.length,
|
|
134
|
+
unused: unused.length,
|
|
135
|
+
outdated: outdated.length,
|
|
136
|
+
risks: risks.length
|
|
137
|
+
}, config);
|
|
138
|
+
const scopedUnused = options.packageName && options.packageName.trim().length > 0
|
|
139
|
+
? unused.map((item) => ({ ...item, package: options.packageName }))
|
|
140
|
+
: unused;
|
|
141
|
+
const scopedOutdated = options.packageName && options.packageName.trim().length > 0
|
|
142
|
+
? outdated.map((item) => ({ ...item, package: options.packageName }))
|
|
143
|
+
: outdated;
|
|
144
|
+
const scopedRisks = options.packageName && options.packageName.trim().length > 0
|
|
145
|
+
? risks.map((item) => ({ ...item, package: options.packageName }))
|
|
146
|
+
: risks;
|
|
147
|
+
return {
|
|
148
|
+
rootDir,
|
|
149
|
+
score,
|
|
150
|
+
policy,
|
|
151
|
+
duplicates,
|
|
152
|
+
unused: scopedUnused,
|
|
153
|
+
outdated: scopedOutdated,
|
|
154
|
+
risks: scopedRisks,
|
|
155
|
+
suggestions,
|
|
156
|
+
config
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface LockPackageInstance {
|
|
2
|
+
path: string;
|
|
3
|
+
version: string;
|
|
4
|
+
}
|
|
5
|
+
export interface DependencyGraph {
|
|
6
|
+
rootDir: string;
|
|
7
|
+
packageJsonPath: string;
|
|
8
|
+
lockfilePath?: string;
|
|
9
|
+
dependencies: Record<string, string>;
|
|
10
|
+
devDependencies: Record<string, string>;
|
|
11
|
+
scripts: Record<string, string>;
|
|
12
|
+
lockPackages: Record<string, LockPackageInstance[]>;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildDependencyGraph(rootDir: string): Promise<DependencyGraph>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readJsonFile } from "../utils/file-parser.js";
|
|
3
|
+
export async function buildDependencyGraph(rootDir) {
|
|
4
|
+
const packageJsonPath = path.join(rootDir, "package.json");
|
|
5
|
+
const lockfilePath = path.join(rootDir, "package-lock.json");
|
|
6
|
+
const packageJson = await readJsonFile(packageJsonPath);
|
|
7
|
+
const lockPackages = new Map();
|
|
8
|
+
try {
|
|
9
|
+
const packageLock = await readJsonFile(lockfilePath);
|
|
10
|
+
for (const [packagePath, details] of Object.entries(packageLock.packages ?? {})) {
|
|
11
|
+
const name = extractPackageName(packagePath);
|
|
12
|
+
const version = details.version;
|
|
13
|
+
if (!name || !version) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const instances = lockPackages.get(name) ?? new Map();
|
|
17
|
+
const normalizedPath = packagePath || "node_modules/" + name;
|
|
18
|
+
instances.set(normalizedPath, { path: normalizedPath, version });
|
|
19
|
+
lockPackages.set(name, instances);
|
|
20
|
+
}
|
|
21
|
+
for (const [name, details] of Object.entries(packageLock.dependencies ?? {})) {
|
|
22
|
+
if (!details.version) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const instances = lockPackages.get(name) ?? new Map();
|
|
26
|
+
const normalizedPath = `node_modules/${name}`;
|
|
27
|
+
instances.set(normalizedPath, { path: normalizedPath, version: details.version });
|
|
28
|
+
lockPackages.set(name, instances);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {
|
|
33
|
+
rootDir,
|
|
34
|
+
packageJsonPath,
|
|
35
|
+
dependencies: packageJson.dependencies ?? {},
|
|
36
|
+
devDependencies: packageJson.devDependencies ?? {},
|
|
37
|
+
scripts: packageJson.scripts ?? {},
|
|
38
|
+
lockPackages: {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
rootDir,
|
|
43
|
+
packageJsonPath,
|
|
44
|
+
lockfilePath,
|
|
45
|
+
dependencies: packageJson.dependencies ?? {},
|
|
46
|
+
devDependencies: packageJson.devDependencies ?? {},
|
|
47
|
+
scripts: packageJson.scripts ?? {},
|
|
48
|
+
lockPackages: Object.fromEntries(Array.from(lockPackages.entries()).map(([name, instances]) => [
|
|
49
|
+
name,
|
|
50
|
+
Array.from(instances.values()).sort((left, right) => left.path.localeCompare(right.path))
|
|
51
|
+
]))
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function extractPackageName(packagePath) {
|
|
55
|
+
if (!packagePath) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const match = packagePath.match(/(?:^|\/)node_modules\/(.+)$/);
|
|
59
|
+
if (!match) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return match[1];
|
|
63
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { analyzeProject } from "./core/analyzer.js";
|
|
2
|
+
export type { AnalysisOptions, AnalysisResult, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, RiskDependency, UnusedDependency } from "./core/analyzer.js";
|
|
3
|
+
export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
|
|
4
|
+
export type { WorkspacePackage } from "./utils/workspaces.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { analyzeProject } from "./core/analyzer.js";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function renderConsoleReport(result) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
lines.push(`Project Health: ${result.score}/100`);
|
|
4
|
+
lines.push(`Path: ${result.rootDir}`);
|
|
5
|
+
lines.push(`Policy: ${result.policy.passed ? "PASS" : "FAIL"}`);
|
|
6
|
+
lines.push("");
|
|
7
|
+
lines.push(summaryLine("Duplicates", result.duplicates.length));
|
|
8
|
+
lines.push(summaryLine("Unused", result.unused.length));
|
|
9
|
+
lines.push(summaryLine("Outdated", result.outdated.length));
|
|
10
|
+
lines.push(summaryLine("Risks", result.risks.length));
|
|
11
|
+
if (result.packages && result.packages.length > 0) {
|
|
12
|
+
lines.push("");
|
|
13
|
+
lines.push("Packages:");
|
|
14
|
+
for (const pkg of result.packages) {
|
|
15
|
+
lines.push(`- ${pkg.name}: ${pkg.score}/100, D:${pkg.duplicates.length} U:${pkg.unused.length} O:${pkg.outdated.length} R:${pkg.risks.length}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => `${item.name}: ${item.versions.join(", ")}`));
|
|
19
|
+
appendSection(lines, "Unused dependencies", result.unused.map((item) => item.package
|
|
20
|
+
? `${item.name} (${item.section}) [${item.package}]`
|
|
21
|
+
: `${item.name} (${item.section})`));
|
|
22
|
+
appendSection(lines, "Outdated dependencies", result.outdated.map((item) => item.package
|
|
23
|
+
? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
|
|
24
|
+
: `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`));
|
|
25
|
+
appendSection(lines, "Risky dependencies", result.risks.map((item) => item.package
|
|
26
|
+
? `${item.name}: ${item.reasons.join("; ")} [${item.package}]`
|
|
27
|
+
: `${item.name}: ${item.reasons.join("; ")}`));
|
|
28
|
+
appendSection(lines, "Policy reasons", result.policy.reasons);
|
|
29
|
+
if (result.suggestions.length > 0) {
|
|
30
|
+
lines.push("");
|
|
31
|
+
lines.push("Suggestions:");
|
|
32
|
+
for (const suggestion of result.suggestions) {
|
|
33
|
+
lines.push(`- ${suggestion}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
function summaryLine(label, count) {
|
|
39
|
+
const indicator = count === 0 ? "OK" : "WARN";
|
|
40
|
+
return `${indicator} ${label}: ${count}`;
|
|
41
|
+
}
|
|
42
|
+
function appendSection(lines, title, entries) {
|
|
43
|
+
if (entries.length === 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push(`${title}:`);
|
|
48
|
+
for (const entry of entries.slice(0, 10)) {
|
|
49
|
+
lines.push(`- ${entry}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface DepBrainConfig {
|
|
2
|
+
ignore: {
|
|
3
|
+
dependencies: string[];
|
|
4
|
+
devDependencies: string[];
|
|
5
|
+
duplicates: string[];
|
|
6
|
+
outdated: string[];
|
|
7
|
+
risks: string[];
|
|
8
|
+
unused: string[];
|
|
9
|
+
};
|
|
10
|
+
policy: {
|
|
11
|
+
minScore: number;
|
|
12
|
+
failOnDuplicates: boolean;
|
|
13
|
+
failOnOutdated: boolean;
|
|
14
|
+
failOnRisks: boolean;
|
|
15
|
+
failOnUnused: boolean;
|
|
16
|
+
};
|
|
17
|
+
report: {
|
|
18
|
+
maxSuggestions: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface DepBrainConfigOverrides {
|
|
22
|
+
ignore?: Partial<DepBrainConfig["ignore"]>;
|
|
23
|
+
policy?: Partial<DepBrainConfig["policy"]>;
|
|
24
|
+
report?: Partial<DepBrainConfig["report"]>;
|
|
25
|
+
}
|
|
26
|
+
export declare const defaultConfig: DepBrainConfig;
|
|
27
|
+
export declare function loadDepBrainConfig(rootDir: string, configPath?: string): Promise<DepBrainConfig>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readJsonFile } from "./file-parser.js";
|
|
3
|
+
export const defaultConfig = {
|
|
4
|
+
ignore: {
|
|
5
|
+
dependencies: [],
|
|
6
|
+
devDependencies: [],
|
|
7
|
+
duplicates: [],
|
|
8
|
+
outdated: [],
|
|
9
|
+
risks: [],
|
|
10
|
+
unused: []
|
|
11
|
+
},
|
|
12
|
+
policy: {
|
|
13
|
+
minScore: 0,
|
|
14
|
+
failOnDuplicates: false,
|
|
15
|
+
failOnOutdated: false,
|
|
16
|
+
failOnRisks: false,
|
|
17
|
+
failOnUnused: false
|
|
18
|
+
},
|
|
19
|
+
report: {
|
|
20
|
+
maxSuggestions: 5
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export async function loadDepBrainConfig(rootDir, configPath) {
|
|
24
|
+
const resolvedPath = path.resolve(rootDir, configPath ?? "depbrain.config.json");
|
|
25
|
+
try {
|
|
26
|
+
const loaded = await readJsonFile(resolvedPath);
|
|
27
|
+
return normalizeConfig(loaded);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return defaultConfig;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function normalizeConfig(loaded) {
|
|
34
|
+
return {
|
|
35
|
+
ignore: {
|
|
36
|
+
dependencies: normalizeStringArray(loaded.ignore?.dependencies, defaultConfig.ignore.dependencies),
|
|
37
|
+
devDependencies: normalizeStringArray(loaded.ignore?.devDependencies, defaultConfig.ignore.devDependencies),
|
|
38
|
+
duplicates: normalizeStringArray(loaded.ignore?.duplicates, defaultConfig.ignore.duplicates),
|
|
39
|
+
outdated: normalizeStringArray(loaded.ignore?.outdated, defaultConfig.ignore.outdated),
|
|
40
|
+
risks: normalizeStringArray(loaded.ignore?.risks, defaultConfig.ignore.risks),
|
|
41
|
+
unused: normalizeStringArray(loaded.ignore?.unused, defaultConfig.ignore.unused)
|
|
42
|
+
},
|
|
43
|
+
policy: {
|
|
44
|
+
minScore: normalizeNumber(loaded.policy?.minScore, defaultConfig.policy.minScore),
|
|
45
|
+
failOnDuplicates: normalizeBoolean(loaded.policy?.failOnDuplicates, defaultConfig.policy.failOnDuplicates),
|
|
46
|
+
failOnOutdated: normalizeBoolean(loaded.policy?.failOnOutdated, defaultConfig.policy.failOnOutdated),
|
|
47
|
+
failOnRisks: normalizeBoolean(loaded.policy?.failOnRisks, defaultConfig.policy.failOnRisks),
|
|
48
|
+
failOnUnused: normalizeBoolean(loaded.policy?.failOnUnused, defaultConfig.policy.failOnUnused)
|
|
49
|
+
},
|
|
50
|
+
report: {
|
|
51
|
+
maxSuggestions: normalizeNumber(loaded.report?.maxSuggestions, defaultConfig.report.maxSuggestions)
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function normalizeStringArray(value, fallback) {
|
|
56
|
+
if (!Array.isArray(value)) {
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
return value.filter((item) => typeof item === "string");
|
|
60
|
+
}
|
|
61
|
+
function normalizeBoolean(value, fallback) {
|
|
62
|
+
return typeof value === "boolean" ? value : fallback;
|
|
63
|
+
}
|
|
64
|
+
function normalizeNumber(value, fallback) {
|
|
65
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
66
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function readJsonFile(filePath) {
|
|
4
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
5
|
+
return JSON.parse(content);
|
|
6
|
+
}
|
|
7
|
+
export async function readTextFile(filePath) {
|
|
8
|
+
return fs.readFile(filePath, "utf8");
|
|
9
|
+
}
|
|
10
|
+
export async function collectProjectFiles(rootDir, pattern) {
|
|
11
|
+
const entries = await fs.readdir(rootDir, { withFileTypes: true });
|
|
12
|
+
const files = [];
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
if (entry.name === ".git") {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
files.push(...(await collectProjectFiles(fullPath, pattern)));
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (pattern.test(entry.name)) {
|
|
23
|
+
files.push(fullPath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface PackageMetadata {
|
|
2
|
+
latestVersion: string | null;
|
|
3
|
+
repository: string | null;
|
|
4
|
+
downloads: number | null;
|
|
5
|
+
daysSincePublish: number | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function getLatestVersion(name: string): Promise<string | null>;
|
|
8
|
+
export declare function getPackageMetadata(name: string): Promise<PackageMetadata | null>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const metadataCache = new Map();
|
|
2
|
+
export async function getLatestVersion(name) {
|
|
3
|
+
const metadata = await getPackageMetadata(name);
|
|
4
|
+
return metadata?.latestVersion ?? null;
|
|
5
|
+
}
|
|
6
|
+
export async function getPackageMetadata(name) {
|
|
7
|
+
const existing = metadataCache.get(name);
|
|
8
|
+
if (existing) {
|
|
9
|
+
return existing;
|
|
10
|
+
}
|
|
11
|
+
const request = fetchPackageMetadata(name);
|
|
12
|
+
metadataCache.set(name, request);
|
|
13
|
+
return request;
|
|
14
|
+
}
|
|
15
|
+
async function fetchPackageMetadata(name) {
|
|
16
|
+
try {
|
|
17
|
+
const [packageResponse, downloadsResponse] = await Promise.all([
|
|
18
|
+
fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}`, {
|
|
19
|
+
signal: AbortSignal.timeout(5000)
|
|
20
|
+
}),
|
|
21
|
+
fetch(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(name)}`, {
|
|
22
|
+
signal: AbortSignal.timeout(5000)
|
|
23
|
+
})
|
|
24
|
+
]);
|
|
25
|
+
if (!packageResponse.ok) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const packageJson = (await packageResponse.json());
|
|
29
|
+
const downloadsJson = downloadsResponse.ok
|
|
30
|
+
? (await downloadsResponse.json())
|
|
31
|
+
: {};
|
|
32
|
+
const latestVersion = packageJson["dist-tags"]?.latest ?? null;
|
|
33
|
+
const latestPublishedAt = latestVersion && packageJson.time?.[latestVersion]
|
|
34
|
+
? new Date(packageJson.time[latestVersion])
|
|
35
|
+
: null;
|
|
36
|
+
const daysSincePublish = latestPublishedAt === null
|
|
37
|
+
? null
|
|
38
|
+
: Math.floor((Date.now() - latestPublishedAt.getTime()) / (1000 * 60 * 60 * 24));
|
|
39
|
+
const repository = typeof packageJson.repository === "string"
|
|
40
|
+
? packageJson.repository
|
|
41
|
+
: packageJson.repository?.url ?? null;
|
|
42
|
+
return {
|
|
43
|
+
latestVersion,
|
|
44
|
+
repository,
|
|
45
|
+
downloads: downloadsJson.downloads ?? null,
|
|
46
|
+
daysSincePublish
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readJsonFile } from "./file-parser.js";
|
|
4
|
+
export async function findWorkspacePackages(rootDir) {
|
|
5
|
+
const rootPackageJsonPath = path.join(rootDir, "package.json");
|
|
6
|
+
const rootPackage = await readJsonFile(rootPackageJsonPath).catch(() => null);
|
|
7
|
+
if (!rootPackage?.workspaces) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
const patterns = Array.isArray(rootPackage.workspaces)
|
|
11
|
+
? rootPackage.workspaces
|
|
12
|
+
: rootPackage.workspaces.packages ?? [];
|
|
13
|
+
if (patterns.length === 0) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const packageJsonFiles = await collectPackageJsonFiles(rootDir);
|
|
17
|
+
const matches = packageJsonFiles.filter((filePath) => matchesWorkspacePatterns(rootDir, filePath, patterns));
|
|
18
|
+
const packages = [];
|
|
19
|
+
for (const packageJsonPath of matches) {
|
|
20
|
+
const pkg = await readJsonFile(packageJsonPath).catch(() => null);
|
|
21
|
+
if (!pkg?.name) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
packages.push({
|
|
25
|
+
name: pkg.name,
|
|
26
|
+
rootDir: path.dirname(packageJsonPath),
|
|
27
|
+
packageJsonPath
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return packages.sort((left, right) => left.name.localeCompare(right.name));
|
|
31
|
+
}
|
|
32
|
+
async function collectPackageJsonFiles(rootDir) {
|
|
33
|
+
const entries = await fs.readdir(rootDir, { withFileTypes: true });
|
|
34
|
+
const files = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (entry.name === "node_modules" || entry.name === ".git") {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
files.push(...(await collectPackageJsonFiles(fullPath)));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (entry.isFile() && entry.name === "package.json") {
|
|
45
|
+
files.push(fullPath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
function matchesWorkspacePatterns(rootDir, packageJsonPath, patterns) {
|
|
51
|
+
const rel = normalizePath(path.relative(rootDir, path.dirname(packageJsonPath)));
|
|
52
|
+
return patterns.some((pattern) => {
|
|
53
|
+
const regex = globToRegExp(pattern);
|
|
54
|
+
return regex.test(rel);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function globToRegExp(pattern) {
|
|
58
|
+
const normalized = normalizePath(pattern)
|
|
59
|
+
.replace(/\/+$/, "")
|
|
60
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
61
|
+
.replace(/\*\*/g, "___DEPBRAIN_GLOBSTAR___")
|
|
62
|
+
.replace(/\*/g, "[^/]*")
|
|
63
|
+
.replace(/___DEPBRAIN_GLOBSTAR___/g, ".*");
|
|
64
|
+
return new RegExp(`^${normalized}$`);
|
|
65
|
+
}
|
|
66
|
+
function normalizePath(value) {
|
|
67
|
+
return value.split(path.sep).join("/");
|
|
68
|
+
}
|