dep-brain 0.3.0 → 0.5.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/README.md +19 -1
- package/depbrain.config.json +6 -1
- package/depbrain.config.schema.json +10 -1
- package/dist/checks/duplicate.d.ts +2 -0
- package/dist/checks/duplicate.js +17 -0
- package/dist/checks/outdated.d.ts +2 -0
- package/dist/checks/outdated.js +18 -0
- package/dist/checks/risk.d.ts +2 -0
- package/dist/checks/risk.js +16 -0
- package/dist/checks/unused.d.ts +8 -1
- package/dist/checks/unused.js +28 -19
- package/dist/cli.js +44 -10
- package/dist/core/analyzer.js +121 -19
- package/dist/core/context.d.ts +3 -0
- package/dist/core/context.js +31 -0
- package/dist/core/types.d.ts +28 -0
- package/dist/core/types.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/utils/config.d.ts +6 -0
- package/dist/utils/config.js +12 -2
- package/dist/utils/file-parser.d.ts +1 -1
- package/dist/utils/file-parser.js +34 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,9 @@ npx dep-brain analyze ./path-to-project
|
|
|
46
46
|
npx dep-brain analyze --config depbrain.config.json
|
|
47
47
|
npx dep-brain analyze --min-score 90 --fail-on-risks
|
|
48
48
|
npx dep-brain analyze ./path-to-project --fail-on-unused --json
|
|
49
|
+
npx dep-brain analyze --md > depbrain.md
|
|
50
|
+
npx dep-brain analyze --json --out depbrain.json
|
|
51
|
+
npx dep-brain report --from depbrain.json --md --out depbrain.md
|
|
49
52
|
|
|
50
53
|
dep-brain config
|
|
51
54
|
dep-brain config --config depbrain.config.json
|
|
@@ -101,6 +104,13 @@ Output includes `outputVersion` for schema stability and can be validated with:
|
|
|
101
104
|
dep-brain analyze --md
|
|
102
105
|
```
|
|
103
106
|
|
|
107
|
+
## Report From JSON
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
dep-brain analyze --json --out depbrain.json
|
|
111
|
+
dep-brain report --from depbrain.json --md --out depbrain.md
|
|
112
|
+
```
|
|
113
|
+
|
|
104
114
|
## Config File
|
|
105
115
|
|
|
106
116
|
Create a `depbrain.config.json` file in the project root:
|
|
@@ -109,7 +119,9 @@ Create a `depbrain.config.json` file in the project root:
|
|
|
109
119
|
{
|
|
110
120
|
"ignore": {
|
|
111
121
|
"unused": ["eslint"],
|
|
112
|
-
"outdated": ["typescript"]
|
|
122
|
+
"outdated": ["typescript"],
|
|
123
|
+
"prefixes": ["@nestjs/"],
|
|
124
|
+
"patterns": ["^@aws-sdk/"]
|
|
113
125
|
},
|
|
114
126
|
"policy": {
|
|
115
127
|
"minScore": 90,
|
|
@@ -124,6 +136,9 @@ Create a `depbrain.config.json` file in the project root:
|
|
|
124
136
|
"outdatedWeight": 1,
|
|
125
137
|
"unusedWeight": 2,
|
|
126
138
|
"riskWeight": 6
|
|
139
|
+
},
|
|
140
|
+
"scan": {
|
|
141
|
+
"excludePaths": ["node_modules", "dist", "build", "coverage", ".git"]
|
|
127
142
|
}
|
|
128
143
|
}
|
|
129
144
|
```
|
|
@@ -146,6 +161,9 @@ Supported sections:
|
|
|
146
161
|
- `scoring.outdatedWeight`
|
|
147
162
|
- `scoring.unusedWeight`
|
|
148
163
|
- `scoring.riskWeight`
|
|
164
|
+
- `ignore.prefixes`
|
|
165
|
+
- `ignore.patterns`
|
|
166
|
+
- `scan.excludePaths`
|
|
149
167
|
|
|
150
168
|
Sample config file:
|
|
151
169
|
|
package/depbrain.config.json
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
"duplicates": [],
|
|
6
6
|
"risks": [],
|
|
7
7
|
"dependencies": [],
|
|
8
|
-
"devDependencies": []
|
|
8
|
+
"devDependencies": [],
|
|
9
|
+
"prefixes": [],
|
|
10
|
+
"patterns": []
|
|
9
11
|
},
|
|
10
12
|
"policy": {
|
|
11
13
|
"minScore": 85,
|
|
@@ -22,5 +24,8 @@
|
|
|
22
24
|
"outdatedWeight": 3,
|
|
23
25
|
"unusedWeight": 4,
|
|
24
26
|
"riskWeight": 10
|
|
27
|
+
},
|
|
28
|
+
"scan": {
|
|
29
|
+
"excludePaths": ["node_modules", "dist", "build", "coverage", ".git"]
|
|
25
30
|
}
|
|
26
31
|
}
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"unused": { "type": "array", "items": { "type": "string" } },
|
|
14
14
|
"duplicates": { "type": "array", "items": { "type": "string" } },
|
|
15
15
|
"outdated": { "type": "array", "items": { "type": "string" } },
|
|
16
|
-
"risks": { "type": "array", "items": { "type": "string" } }
|
|
16
|
+
"risks": { "type": "array", "items": { "type": "string" } },
|
|
17
|
+
"prefixes": { "type": "array", "items": { "type": "string" } },
|
|
18
|
+
"patterns": { "type": "array", "items": { "type": "string" } }
|
|
17
19
|
}
|
|
18
20
|
},
|
|
19
21
|
"policy": {
|
|
@@ -43,6 +45,13 @@
|
|
|
43
45
|
"unusedWeight": { "type": "number" },
|
|
44
46
|
"riskWeight": { "type": "number" }
|
|
45
47
|
}
|
|
48
|
+
},
|
|
49
|
+
"scan": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"additionalProperties": false,
|
|
52
|
+
"properties": {
|
|
53
|
+
"excludePaths": { "type": "array", "items": { "type": "string" } }
|
|
54
|
+
}
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { DuplicateDependency } from "../core/analyzer.js";
|
|
2
2
|
import type { DependencyGraph } from "../core/graph-builder.js";
|
|
3
|
+
import type { CheckResult } from "../core/types.js";
|
|
3
4
|
export declare function findDuplicateDependencies(graph: DependencyGraph): Promise<DuplicateDependency[]>;
|
|
5
|
+
export declare function runDuplicateCheck(graph: DependencyGraph): Promise<CheckResult>;
|
package/dist/checks/duplicate.js
CHANGED
|
@@ -8,3 +8,20 @@ export async function findDuplicateDependencies(graph) {
|
|
|
8
8
|
.filter((dependency) => dependency.versions.length > 1)
|
|
9
9
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
10
10
|
}
|
|
11
|
+
export async function runDuplicateCheck(graph) {
|
|
12
|
+
const duplicates = await findDuplicateDependencies(graph);
|
|
13
|
+
return {
|
|
14
|
+
name: "duplicate",
|
|
15
|
+
summary: `${duplicates.length} duplicate dependencies found`,
|
|
16
|
+
issues: duplicates.map((item) => ({
|
|
17
|
+
id: `duplicate:${item.name}`,
|
|
18
|
+
message: `${item.name} has ${item.versions.length} versions`,
|
|
19
|
+
severity: "warning",
|
|
20
|
+
meta: {
|
|
21
|
+
name: item.name,
|
|
22
|
+
versions: item.versions,
|
|
23
|
+
instances: item.instances
|
|
24
|
+
}
|
|
25
|
+
}))
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { OutdatedDependency } from "../core/analyzer.js";
|
|
2
2
|
import type { DependencyGraph } from "../core/graph-builder.js";
|
|
3
|
+
import type { CheckResult } from "../core/types.js";
|
|
3
4
|
export interface OutdatedOptions {
|
|
4
5
|
resolveLatestVersion?: (name: string) => Promise<string | null>;
|
|
5
6
|
}
|
|
6
7
|
export declare function findOutdatedDependencies(graph: DependencyGraph, options?: OutdatedOptions): Promise<OutdatedDependency[]>;
|
|
8
|
+
export declare function runOutdatedCheck(graph: DependencyGraph): Promise<CheckResult>;
|
package/dist/checks/outdated.js
CHANGED
|
@@ -22,6 +22,24 @@ export async function findOutdatedDependencies(graph, options = {}) {
|
|
|
22
22
|
.filter((item) => item !== null)
|
|
23
23
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
24
24
|
}
|
|
25
|
+
export async function runOutdatedCheck(graph) {
|
|
26
|
+
const outdated = await findOutdatedDependencies(graph);
|
|
27
|
+
return {
|
|
28
|
+
name: "outdated",
|
|
29
|
+
summary: `${outdated.length} outdated dependencies found`,
|
|
30
|
+
issues: outdated.map((item) => ({
|
|
31
|
+
id: `outdated:${item.name}`,
|
|
32
|
+
message: `${item.name} ${item.current} -> ${item.latest}`,
|
|
33
|
+
severity: item.updateType === "major" ? "critical" : "warning",
|
|
34
|
+
meta: {
|
|
35
|
+
name: item.name,
|
|
36
|
+
current: item.current,
|
|
37
|
+
latest: item.latest,
|
|
38
|
+
updateType: item.updateType
|
|
39
|
+
}
|
|
40
|
+
}))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
25
43
|
function normalizeVersion(versionRange) {
|
|
26
44
|
return versionRange.trim().replace(/^[~^><=\s]+/, "");
|
|
27
45
|
}
|
package/dist/checks/risk.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { DependencyGraph } from "../core/graph-builder.js";
|
|
2
2
|
import type { RiskDependency } from "../core/analyzer.js";
|
|
3
|
+
import type { CheckResult } from "../core/types.js";
|
|
3
4
|
export declare function findRiskDependencies(graph: DependencyGraph): Promise<RiskDependency[]>;
|
|
5
|
+
export declare function runRiskCheck(graph: DependencyGraph): Promise<CheckResult>;
|
package/dist/checks/risk.js
CHANGED
|
@@ -29,3 +29,19 @@ export async function findRiskDependencies(graph) {
|
|
|
29
29
|
.filter((item) => item !== null)
|
|
30
30
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
31
31
|
}
|
|
32
|
+
export async function runRiskCheck(graph) {
|
|
33
|
+
const risks = await findRiskDependencies(graph);
|
|
34
|
+
return {
|
|
35
|
+
name: "risk",
|
|
36
|
+
summary: `${risks.length} risky dependencies found`,
|
|
37
|
+
issues: risks.map((item) => ({
|
|
38
|
+
id: `risk:${item.name}`,
|
|
39
|
+
message: `${item.name}: ${item.reasons.join("; ")}`,
|
|
40
|
+
severity: "warning",
|
|
41
|
+
meta: {
|
|
42
|
+
name: item.name,
|
|
43
|
+
reasons: item.reasons
|
|
44
|
+
}
|
|
45
|
+
}))
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/checks/unused.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import type { UnusedDependency } from "../core/analyzer.js";
|
|
2
2
|
import type { DependencyGraph } from "../core/graph-builder.js";
|
|
3
|
-
|
|
3
|
+
import type { AnalysisContext, CheckResult } from "../core/types.js";
|
|
4
|
+
export declare function findUnusedDependencies(rootDir: string, graph: DependencyGraph, fileEntries: {
|
|
5
|
+
path: string;
|
|
6
|
+
content: string;
|
|
7
|
+
}[], options: {
|
|
8
|
+
hasTypeScriptConfig: boolean;
|
|
9
|
+
}): Promise<UnusedDependency[]>;
|
|
10
|
+
export declare function runUnusedCheck(context: AnalysisContext): Promise<CheckResult>;
|
package/dist/checks/unused.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { collectProjectFiles, readTextFile } from "../utils/file-parser.js";
|
|
3
2
|
const SOURCE_FILE_PATTERN = /\.(c|m)?(t|j)sx?$/;
|
|
4
3
|
const CONFIG_FILE_PATTERN = /(^|[\\/])(vite|vitest|jest|eslint|prettier|rollup|webpack|babel|tsup|eslint\.config|commitlint|playwright|storybook|tailwind|postcss)\.config\.(c|m)?(t|j)s$/;
|
|
5
4
|
const TEST_FILE_PATTERN = /(^|[\\/])(__tests__|test|tests|spec|specs)([\\/]|$)|\.(test|spec)\.(c|m)?(t|j)sx?$/;
|
|
6
5
|
const RUNTIME_DIR_PATTERN = /(^|[\\/])(src|app|lib|server|client|pages|components)([\\/]|$)/;
|
|
7
|
-
export async function findUnusedDependencies(rootDir, graph) {
|
|
8
|
-
const
|
|
9
|
-
|
|
6
|
+
export async function findUnusedDependencies(rootDir, graph, fileEntries, options) {
|
|
7
|
+
const projectFiles = fileEntries
|
|
8
|
+
.map((entry) => entry.path)
|
|
9
|
+
.filter((filePath) => !filePath.includes(`${path.sep}node_modules${path.sep}`));
|
|
10
10
|
const runtimeUsed = new Set();
|
|
11
11
|
const devUsed = new Set();
|
|
12
|
-
for (const
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
for (const entry of fileEntries) {
|
|
13
|
+
if (!SOURCE_FILE_PATTERN.test(entry.path)) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const imports = extractImportedPackages(entry.content);
|
|
17
|
+
const filePath = entry.path;
|
|
15
18
|
const isDevOnlyFile = isDevelopmentOnlyFile(rootDir, filePath);
|
|
16
19
|
const target = isDevOnlyFile ? devUsed : runtimeUsed;
|
|
17
20
|
for (const importedPackage of imports) {
|
|
@@ -25,8 +28,7 @@ export async function findUnusedDependencies(rootDir, graph) {
|
|
|
25
28
|
devUsed.add(referencedBinary);
|
|
26
29
|
}
|
|
27
30
|
const hasTypeScriptSources = projectFiles.some((filePath) => /\.(c|m)?tsx?$/.test(filePath));
|
|
28
|
-
|
|
29
|
-
if (hasTypeScriptConfig) {
|
|
31
|
+
if (options.hasTypeScriptConfig) {
|
|
30
32
|
devUsed.add("typescript");
|
|
31
33
|
}
|
|
32
34
|
const unusedDependencies = Object.keys(graph.dependencies)
|
|
@@ -34,10 +36,26 @@ export async function findUnusedDependencies(rootDir, graph) {
|
|
|
34
36
|
.map((name) => ({ name, section: "dependencies" }));
|
|
35
37
|
const unusedDevDependencies = Object.keys(graph.devDependencies)
|
|
36
38
|
.filter((name) => !devUsed.has(name) && !runtimeUsed.has(name))
|
|
37
|
-
.filter((name) => !isImplicitlyUsedDevDependency(name, hasTypeScriptSources, hasTypeScriptConfig))
|
|
39
|
+
.filter((name) => !isImplicitlyUsedDevDependency(name, hasTypeScriptSources, options.hasTypeScriptConfig))
|
|
38
40
|
.map((name) => ({ name, section: "devDependencies" }));
|
|
39
41
|
return [...unusedDependencies, ...unusedDevDependencies].sort((left, right) => left.name.localeCompare(right.name));
|
|
40
42
|
}
|
|
43
|
+
export async function runUnusedCheck(context) {
|
|
44
|
+
const unused = await findUnusedDependencies(context.rootDir, context.graph, context.fileEntries, { hasTypeScriptConfig: context.hasTypeScriptConfig });
|
|
45
|
+
return {
|
|
46
|
+
name: "unused",
|
|
47
|
+
summary: `${unused.length} unused dependencies found`,
|
|
48
|
+
issues: unused.map((item) => ({
|
|
49
|
+
id: `unused:${item.section}:${item.name}`,
|
|
50
|
+
message: `${item.name} appears unused`,
|
|
51
|
+
severity: "warning",
|
|
52
|
+
meta: {
|
|
53
|
+
name: item.name,
|
|
54
|
+
section: item.section
|
|
55
|
+
}
|
|
56
|
+
}))
|
|
57
|
+
};
|
|
58
|
+
}
|
|
41
59
|
function extractImportedPackages(content) {
|
|
42
60
|
const imports = new Set();
|
|
43
61
|
const patterns = [
|
|
@@ -109,12 +127,3 @@ function isImplicitlyUsedDevDependency(name, hasTypeScriptSources, hasTypeScript
|
|
|
109
127
|
}
|
|
110
128
|
return false;
|
|
111
129
|
}
|
|
112
|
-
async function hasFile(rootDir, fileName) {
|
|
113
|
-
try {
|
|
114
|
-
await readTextFile(path.join(rootDir, fileName));
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
}
|
package/dist/cli.js
CHANGED
|
@@ -39,6 +39,30 @@ async function main() {
|
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
if (command !== "analyze") {
|
|
42
|
+
if (command === "report") {
|
|
43
|
+
const fromPath = optionValues.get("--from") ?? positionals[0];
|
|
44
|
+
if (!fromPath) {
|
|
45
|
+
console.error("Missing --from <file> for report");
|
|
46
|
+
printHelp();
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const raw = await fs.readFile(fromPath, "utf8");
|
|
52
|
+
const reportData = JSON.parse(raw);
|
|
53
|
+
const output = flags.has("--json")
|
|
54
|
+
? JSON.stringify(reportData, null, 2)
|
|
55
|
+
: renderMarkdownReport(reportData);
|
|
56
|
+
await writeOutput(output, optionValues.get("--out"));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.error("Failed to render report.");
|
|
61
|
+
console.error(error);
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
42
66
|
if (command === "config") {
|
|
43
67
|
if (!(await hasPackageJson(targetPath))) {
|
|
44
68
|
console.error(`No package.json found at ${targetPath}`);
|
|
@@ -78,21 +102,21 @@ async function main() {
|
|
|
78
102
|
configPath: optionValues.get("--config"),
|
|
79
103
|
config: cliConfig
|
|
80
104
|
});
|
|
105
|
+
let output;
|
|
81
106
|
if (flags.has("--json")) {
|
|
82
|
-
|
|
107
|
+
output = renderJsonReport(result);
|
|
83
108
|
}
|
|
84
109
|
else if (flags.has("--md")) {
|
|
85
|
-
|
|
110
|
+
output = renderMarkdownReport(result);
|
|
86
111
|
}
|
|
87
112
|
else {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
process.stdout.write(`${output}\n`);
|
|
94
|
-
}
|
|
113
|
+
const consoleOutput = renderConsoleReport(result);
|
|
114
|
+
output =
|
|
115
|
+
!consoleOutput || consoleOutput.trim().length === 0
|
|
116
|
+
? renderJsonReport(result)
|
|
117
|
+
: consoleOutput;
|
|
95
118
|
}
|
|
119
|
+
await writeOutput(output, optionValues.get("--out"));
|
|
96
120
|
if (!result.policy.passed) {
|
|
97
121
|
process.exitCode = 1;
|
|
98
122
|
}
|
|
@@ -148,7 +172,8 @@ function printHelp() {
|
|
|
148
172
|
console.log("Dependency Brain");
|
|
149
173
|
console.log("");
|
|
150
174
|
console.log("Usage:");
|
|
151
|
-
console.log(" dep-brain analyze [path] [--json] [--md] [--config path] [--min-score n] [--fail-on-risks] [--fail-on-outdated] [--fail-on-unused] [--fail-on-duplicates]");
|
|
175
|
+
console.log(" dep-brain analyze [path] [--json] [--md] [--out path] [--config path] [--min-score n] [--fail-on-risks] [--fail-on-outdated] [--fail-on-unused] [--fail-on-duplicates]");
|
|
176
|
+
console.log(" dep-brain report --from <file> [--md] [--json] [--out path]");
|
|
152
177
|
console.log(" dep-brain config [path] [--config path]");
|
|
153
178
|
console.log(" dep-brain help");
|
|
154
179
|
console.log(" dep-brain --version");
|
|
@@ -157,6 +182,8 @@ function printHelp() {
|
|
|
157
182
|
console.log(" --json Output JSON for analysis");
|
|
158
183
|
console.log(" --md Output Markdown report");
|
|
159
184
|
console.log(" --config <path> Path to depbrain.config.json");
|
|
185
|
+
console.log(" --from <file> Read analysis JSON from file");
|
|
186
|
+
console.log(" --out <path> Write output to a file");
|
|
160
187
|
console.log(" --min-score <n> Minimum score required to pass");
|
|
161
188
|
console.log(" --fail-on-risks Fail when risky dependencies exist");
|
|
162
189
|
console.log(" --fail-on-outdated Fail when outdated dependencies exist");
|
|
@@ -176,3 +203,10 @@ async function loadPackageVersion() {
|
|
|
176
203
|
return null;
|
|
177
204
|
}
|
|
178
205
|
}
|
|
206
|
+
async function writeOutput(output, outPath) {
|
|
207
|
+
if (outPath) {
|
|
208
|
+
await fs.writeFile(outPath, `${output}\n`, "utf8");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
process.stdout.write(`${output}\n`);
|
|
212
|
+
}
|
package/dist/core/analyzer.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { runDuplicateCheck } from "../checks/duplicate.js";
|
|
3
|
+
import { runOutdatedCheck } from "../checks/outdated.js";
|
|
4
|
+
import { runRiskCheck } from "../checks/risk.js";
|
|
5
|
+
import { runUnusedCheck } from "../checks/unused.js";
|
|
6
6
|
import { loadDepBrainConfig } from "../utils/config.js";
|
|
7
7
|
import { findWorkspacePackages } from "../utils/workspaces.js";
|
|
8
8
|
import { buildDependencyGraph } from "./graph-builder.js";
|
|
9
9
|
import { calculateHealthScore } from "./scorer.js";
|
|
10
|
+
import { buildAnalysisContext } from "./context.js";
|
|
10
11
|
export const OUTPUT_VERSION = "1.0";
|
|
11
12
|
export async function analyzeProject(options = {}) {
|
|
12
13
|
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
@@ -17,8 +18,9 @@ export async function analyzeProject(options = {}) {
|
|
|
17
18
|
return analyzeSingleProject(rootDir, config);
|
|
18
19
|
}
|
|
19
20
|
const rootGraph = await buildDependencyGraph(rootDir);
|
|
20
|
-
const
|
|
21
|
-
const
|
|
21
|
+
const duplicateCheck = await runDuplicateCheck(rootGraph);
|
|
22
|
+
const filteredDuplicateIssues = filterIssues(duplicateCheck.issues, "duplicates", config);
|
|
23
|
+
const duplicates = mapDuplicateIssues(filteredDuplicateIssues);
|
|
22
24
|
const packages = [];
|
|
23
25
|
for (const workspace of workspaces) {
|
|
24
26
|
const result = await analyzeSingleProject(workspace.rootDir, config, {
|
|
@@ -81,7 +83,9 @@ function mergeConfig(base, overrides) {
|
|
|
81
83
|
duplicates: overrides.ignore?.duplicates ?? base.ignore.duplicates,
|
|
82
84
|
outdated: overrides.ignore?.outdated ?? base.ignore.outdated,
|
|
83
85
|
risks: overrides.ignore?.risks ?? base.ignore.risks,
|
|
84
|
-
unused: overrides.ignore?.unused ?? base.ignore.unused
|
|
86
|
+
unused: overrides.ignore?.unused ?? base.ignore.unused,
|
|
87
|
+
prefixes: overrides.ignore?.prefixes ?? base.ignore.prefixes,
|
|
88
|
+
patterns: overrides.ignore?.patterns ?? base.ignore.patterns
|
|
85
89
|
},
|
|
86
90
|
policy: {
|
|
87
91
|
minScore: overrides.policy?.minScore ?? base.policy.minScore,
|
|
@@ -98,6 +102,9 @@ function mergeConfig(base, overrides) {
|
|
|
98
102
|
outdatedWeight: overrides.scoring?.outdatedWeight ?? base.scoring.outdatedWeight,
|
|
99
103
|
unusedWeight: overrides.scoring?.unusedWeight ?? base.scoring.unusedWeight,
|
|
100
104
|
riskWeight: overrides.scoring?.riskWeight ?? base.scoring.riskWeight
|
|
105
|
+
},
|
|
106
|
+
scan: {
|
|
107
|
+
excludePaths: overrides.scan?.excludePaths ?? base.scan.excludePaths
|
|
101
108
|
}
|
|
102
109
|
};
|
|
103
110
|
}
|
|
@@ -124,18 +131,13 @@ function evaluatePolicy(summary, config) {
|
|
|
124
131
|
};
|
|
125
132
|
}
|
|
126
133
|
async function analyzeSingleProject(rootDir, config, options = {}) {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const duplicates = rawDuplicates.filter((item) => !config.ignore.duplicates.includes(item.name));
|
|
135
|
-
const unused = rawUnused.filter((item) => !config.ignore.unused.includes(item.name) &&
|
|
136
|
-
!config.ignore[item.section].includes(item.name));
|
|
137
|
-
const outdated = rawOutdated.filter((item) => !config.ignore.outdated.includes(item.name));
|
|
138
|
-
const risks = rawRisks.filter((item) => !config.ignore.risks.includes(item.name));
|
|
134
|
+
const context = await buildAnalysisContext(rootDir, config);
|
|
135
|
+
const results = await runChecks(context);
|
|
136
|
+
const issueGroups = normalizeIssues(results, config);
|
|
137
|
+
const duplicates = mapDuplicateIssues(issueGroups.duplicates);
|
|
138
|
+
const unused = mapUnusedIssues(issueGroups.unused);
|
|
139
|
+
const outdated = mapOutdatedIssues(issueGroups.outdated);
|
|
140
|
+
const risks = mapRiskIssues(issueGroups.risks);
|
|
139
141
|
const score = calculateHealthScore({
|
|
140
142
|
duplicates: duplicates.length,
|
|
141
143
|
unused: unused.length,
|
|
@@ -187,6 +189,106 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
|
|
|
187
189
|
config
|
|
188
190
|
};
|
|
189
191
|
}
|
|
192
|
+
function shouldIgnorePackage(name, bucket, config) {
|
|
193
|
+
if (config.ignore[bucket].includes(name)) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (config.ignore.prefixes.some((prefix) => name.startsWith(prefix))) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
return config.ignore.patterns.some((pattern) => {
|
|
200
|
+
try {
|
|
201
|
+
const regex = new RegExp(pattern);
|
|
202
|
+
return regex.test(name);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async function runChecks(context) {
|
|
210
|
+
const checks = [
|
|
211
|
+
{
|
|
212
|
+
name: "duplicate",
|
|
213
|
+
run: () => runDuplicateCheck(context.graph)
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "unused",
|
|
217
|
+
run: () => runUnusedCheck(context)
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "outdated",
|
|
221
|
+
run: () => runOutdatedCheck(context.graph)
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "risk",
|
|
225
|
+
run: () => runRiskCheck(context.graph)
|
|
226
|
+
}
|
|
227
|
+
];
|
|
228
|
+
const results = [];
|
|
229
|
+
for (const check of checks) {
|
|
230
|
+
results.push(await check.run());
|
|
231
|
+
}
|
|
232
|
+
return results;
|
|
233
|
+
}
|
|
234
|
+
function normalizeIssues(results, config) {
|
|
235
|
+
const map = new Map();
|
|
236
|
+
for (const result of results) {
|
|
237
|
+
map.set(result.name, result.issues);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
duplicates: filterIssues(map.get("duplicate") ?? [], "duplicates", config),
|
|
241
|
+
unused: filterIssues(map.get("unused") ?? [], "unused", config),
|
|
242
|
+
outdated: filterIssues(map.get("outdated") ?? [], "outdated", config),
|
|
243
|
+
risks: filterIssues(map.get("risk") ?? [], "risks", config)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function filterIssues(issues, bucket, config) {
|
|
247
|
+
return issues.filter((issue) => {
|
|
248
|
+
const name = typeof issue.meta?.name === "string" ? issue.meta.name : issue.package ?? "";
|
|
249
|
+
if (!name) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (bucket === "unused") {
|
|
253
|
+
const section = issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies";
|
|
254
|
+
if (shouldIgnorePackage(name, section, config)) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return !shouldIgnorePackage(name, bucket, config);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function mapDuplicateIssues(issues) {
|
|
262
|
+
return issues.map((issue) => ({
|
|
263
|
+
name: String(issue.meta?.name ?? issue.package ?? "unknown"),
|
|
264
|
+
versions: Array.isArray(issue.meta?.versions) ? issue.meta?.versions : [],
|
|
265
|
+
instances: Array.isArray(issue.meta?.instances)
|
|
266
|
+
? issue.meta?.instances
|
|
267
|
+
: []
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
function mapUnusedIssues(issues) {
|
|
271
|
+
return issues.map((issue) => ({
|
|
272
|
+
name: String(issue.meta?.name ?? issue.package ?? "unknown"),
|
|
273
|
+
section: issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies"
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
function mapOutdatedIssues(issues) {
|
|
277
|
+
return issues.map((issue) => ({
|
|
278
|
+
name: String(issue.meta?.name ?? issue.package ?? "unknown"),
|
|
279
|
+
current: String(issue.meta?.current ?? ""),
|
|
280
|
+
latest: String(issue.meta?.latest ?? ""),
|
|
281
|
+
updateType: issue.meta?.updateType === "major" || issue.meta?.updateType === "minor" || issue.meta?.updateType === "patch"
|
|
282
|
+
? issue.meta.updateType
|
|
283
|
+
: "unknown"
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
function mapRiskIssues(issues) {
|
|
287
|
+
return issues.map((issue) => ({
|
|
288
|
+
name: String(issue.meta?.name ?? issue.package ?? "unknown"),
|
|
289
|
+
reasons: Array.isArray(issue.meta?.reasons) ? issue.meta?.reasons : []
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
190
292
|
function buildScoreBreakdown(counts, config) {
|
|
191
293
|
return {
|
|
192
294
|
baseScore: 100,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { buildDependencyGraph } from "./graph-builder.js";
|
|
3
|
+
import { collectProjectFiles, readTextFile } from "../utils/file-parser.js";
|
|
4
|
+
const SOURCE_FILE_PATTERN = /\.(c|m)?(t|j)sx?$/;
|
|
5
|
+
export async function buildAnalysisContext(rootDir, config) {
|
|
6
|
+
const graph = await buildDependencyGraph(rootDir);
|
|
7
|
+
const projectFiles = await collectProjectFiles(rootDir, SOURCE_FILE_PATTERN, config.scan.excludePaths);
|
|
8
|
+
const fileEntries = await Promise.all(projectFiles.map(async (filePath) => ({
|
|
9
|
+
path: filePath,
|
|
10
|
+
content: await readTextFile(filePath)
|
|
11
|
+
})));
|
|
12
|
+
const sourceText = fileEntries.map((entry) => entry.content).join("\n");
|
|
13
|
+
const hasTypeScriptConfig = await hasFile(rootDir, "tsconfig.json");
|
|
14
|
+
return {
|
|
15
|
+
rootDir,
|
|
16
|
+
graph,
|
|
17
|
+
sourceText,
|
|
18
|
+
projectFiles,
|
|
19
|
+
fileEntries,
|
|
20
|
+
hasTypeScriptConfig
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function hasFile(rootDir, fileName) {
|
|
24
|
+
try {
|
|
25
|
+
await readTextFile(path.join(rootDir, fileName));
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type IssueSeverity = "info" | "warning" | "critical";
|
|
2
|
+
export type Issue = {
|
|
3
|
+
id: string;
|
|
4
|
+
message: string;
|
|
5
|
+
package?: string;
|
|
6
|
+
severity: IssueSeverity;
|
|
7
|
+
meta?: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
export type CheckResult = {
|
|
10
|
+
name: string;
|
|
11
|
+
issues: Issue[];
|
|
12
|
+
summary: string;
|
|
13
|
+
};
|
|
14
|
+
export type AnalysisContext = {
|
|
15
|
+
rootDir: string;
|
|
16
|
+
graph: import("./graph-builder.js").DependencyGraph;
|
|
17
|
+
sourceText: string;
|
|
18
|
+
projectFiles: string[];
|
|
19
|
+
fileEntries: {
|
|
20
|
+
path: string;
|
|
21
|
+
content: string;
|
|
22
|
+
}[];
|
|
23
|
+
hasTypeScriptConfig: boolean;
|
|
24
|
+
};
|
|
25
|
+
export type CheckRunner = {
|
|
26
|
+
name: string;
|
|
27
|
+
run: (context: AnalysisContext) => Promise<CheckResult>;
|
|
28
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { analyzeProject } from "./core/analyzer.js";
|
|
2
2
|
export type { AnalysisOptions, AnalysisResult, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, ScoreBreakdown, RiskDependency, UnusedDependency } from "./core/analyzer.js";
|
|
3
3
|
export { OUTPUT_VERSION } from "./core/analyzer.js";
|
|
4
|
+
export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
|
|
4
5
|
export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
|
|
5
6
|
export type { WorkspacePackage } from "./utils/workspaces.js";
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface DepBrainConfig {
|
|
|
6
6
|
outdated: string[];
|
|
7
7
|
risks: string[];
|
|
8
8
|
unused: string[];
|
|
9
|
+
prefixes: string[];
|
|
10
|
+
patterns: string[];
|
|
9
11
|
};
|
|
10
12
|
policy: {
|
|
11
13
|
minScore: number;
|
|
@@ -23,12 +25,16 @@ export interface DepBrainConfig {
|
|
|
23
25
|
unusedWeight: number;
|
|
24
26
|
riskWeight: number;
|
|
25
27
|
};
|
|
28
|
+
scan: {
|
|
29
|
+
excludePaths: string[];
|
|
30
|
+
};
|
|
26
31
|
}
|
|
27
32
|
export interface DepBrainConfigOverrides {
|
|
28
33
|
ignore?: Partial<DepBrainConfig["ignore"]>;
|
|
29
34
|
policy?: Partial<DepBrainConfig["policy"]>;
|
|
30
35
|
report?: Partial<DepBrainConfig["report"]>;
|
|
31
36
|
scoring?: Partial<DepBrainConfig["scoring"]>;
|
|
37
|
+
scan?: Partial<DepBrainConfig["scan"]>;
|
|
32
38
|
}
|
|
33
39
|
export declare const defaultConfig: DepBrainConfig;
|
|
34
40
|
export declare function loadDepBrainConfig(rootDir: string, configPath?: string): Promise<DepBrainConfig>;
|
package/dist/utils/config.js
CHANGED
|
@@ -7,7 +7,9 @@ export const defaultConfig = {
|
|
|
7
7
|
duplicates: [],
|
|
8
8
|
outdated: [],
|
|
9
9
|
risks: [],
|
|
10
|
-
unused: []
|
|
10
|
+
unused: [],
|
|
11
|
+
prefixes: [],
|
|
12
|
+
patterns: []
|
|
11
13
|
},
|
|
12
14
|
policy: {
|
|
13
15
|
minScore: 0,
|
|
@@ -24,6 +26,9 @@ export const defaultConfig = {
|
|
|
24
26
|
outdatedWeight: 3,
|
|
25
27
|
unusedWeight: 4,
|
|
26
28
|
riskWeight: 10
|
|
29
|
+
},
|
|
30
|
+
scan: {
|
|
31
|
+
excludePaths: ["node_modules", "dist", "build", "coverage", ".git"]
|
|
27
32
|
}
|
|
28
33
|
};
|
|
29
34
|
export async function loadDepBrainConfig(rootDir, configPath) {
|
|
@@ -44,7 +49,9 @@ function normalizeConfig(loaded) {
|
|
|
44
49
|
duplicates: normalizeStringArray(loaded.ignore?.duplicates, defaultConfig.ignore.duplicates),
|
|
45
50
|
outdated: normalizeStringArray(loaded.ignore?.outdated, defaultConfig.ignore.outdated),
|
|
46
51
|
risks: normalizeStringArray(loaded.ignore?.risks, defaultConfig.ignore.risks),
|
|
47
|
-
unused: normalizeStringArray(loaded.ignore?.unused, defaultConfig.ignore.unused)
|
|
52
|
+
unused: normalizeStringArray(loaded.ignore?.unused, defaultConfig.ignore.unused),
|
|
53
|
+
prefixes: normalizeStringArray(loaded.ignore?.prefixes, defaultConfig.ignore.prefixes),
|
|
54
|
+
patterns: normalizeStringArray(loaded.ignore?.patterns, defaultConfig.ignore.patterns)
|
|
48
55
|
},
|
|
49
56
|
policy: {
|
|
50
57
|
minScore: normalizeNumber(loaded.policy?.minScore, defaultConfig.policy.minScore),
|
|
@@ -61,6 +68,9 @@ function normalizeConfig(loaded) {
|
|
|
61
68
|
outdatedWeight: normalizeNumber(loaded.scoring?.outdatedWeight, defaultConfig.scoring.outdatedWeight),
|
|
62
69
|
unusedWeight: normalizeNumber(loaded.scoring?.unusedWeight, defaultConfig.scoring.unusedWeight),
|
|
63
70
|
riskWeight: normalizeNumber(loaded.scoring?.riskWeight, defaultConfig.scoring.riskWeight)
|
|
71
|
+
},
|
|
72
|
+
scan: {
|
|
73
|
+
excludePaths: normalizeStringArray(loaded.scan?.excludePaths, defaultConfig.scan.excludePaths)
|
|
64
74
|
}
|
|
65
75
|
};
|
|
66
76
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export declare function readJsonFile<T>(filePath: string): Promise<T>;
|
|
2
2
|
export declare function readTextFile(filePath: string): Promise<string>;
|
|
3
|
-
export declare function collectProjectFiles(rootDir: string, pattern: RegExp): Promise<string[]>;
|
|
3
|
+
export declare function collectProjectFiles(rootDir: string, pattern: RegExp, excludePaths?: string[]): Promise<string[]>;
|
|
@@ -7,16 +7,23 @@ export async function readJsonFile(filePath) {
|
|
|
7
7
|
export async function readTextFile(filePath) {
|
|
8
8
|
return fs.readFile(filePath, "utf8");
|
|
9
9
|
}
|
|
10
|
-
export async function collectProjectFiles(rootDir, pattern) {
|
|
11
|
-
|
|
10
|
+
export async function collectProjectFiles(rootDir, pattern, excludePaths = []) {
|
|
11
|
+
return collectProjectFilesInternal(rootDir, rootDir, pattern, excludePaths);
|
|
12
|
+
}
|
|
13
|
+
async function collectProjectFilesInternal(currentDir, baseDir, pattern, excludePaths) {
|
|
14
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
12
15
|
const files = [];
|
|
13
16
|
for (const entry of entries) {
|
|
14
|
-
const fullPath = path.join(
|
|
17
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
18
|
+
const relPath = path.relative(baseDir, fullPath);
|
|
19
|
+
if (matchesAnyPattern(relPath, excludePaths)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
15
22
|
if (entry.isDirectory()) {
|
|
16
23
|
if (entry.name === ".git") {
|
|
17
24
|
continue;
|
|
18
25
|
}
|
|
19
|
-
files.push(...(await
|
|
26
|
+
files.push(...(await collectProjectFilesInternal(fullPath, baseDir, pattern, excludePaths)));
|
|
20
27
|
continue;
|
|
21
28
|
}
|
|
22
29
|
if (pattern.test(entry.name)) {
|
|
@@ -25,3 +32,26 @@ export async function collectProjectFiles(rootDir, pattern) {
|
|
|
25
32
|
}
|
|
26
33
|
return files;
|
|
27
34
|
}
|
|
35
|
+
function matchesAnyPattern(value, patterns) {
|
|
36
|
+
if (patterns.length === 0) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const normalized = normalizePath(value);
|
|
40
|
+
return patterns.some((pattern) => {
|
|
41
|
+
const regex = globToRegExp(pattern);
|
|
42
|
+
return regex.test(normalized);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function globToRegExp(pattern) {
|
|
46
|
+
const normalized = normalizePath(pattern)
|
|
47
|
+
.replace(/\/+$/, "")
|
|
48
|
+
.replace(/^\//, "")
|
|
49
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
50
|
+
.replace(/\*\*/g, "___DEPBRAIN_GLOBSTAR___")
|
|
51
|
+
.replace(/\*/g, "[^/]*")
|
|
52
|
+
.replace(/___DEPBRAIN_GLOBSTAR___/g, ".*");
|
|
53
|
+
return new RegExp(`(^|.*/)${normalized}($|/.*)`);
|
|
54
|
+
}
|
|
55
|
+
function normalizePath(value) {
|
|
56
|
+
return value.split(path.sep).join("/");
|
|
57
|
+
}
|