dep-brain 1.2.0 → 1.4.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 +24 -0
- package/README.md +42 -28
- package/depbrain.config.json +6 -1
- package/depbrain.config.schema.json +6 -1
- package/depbrain.output.schema.json +22 -3
- package/dist/checks/risk.d.ts +3 -1
- package/dist/checks/risk.js +205 -35
- package/dist/cli.js +11 -3
- package/dist/core/analyzer.d.ts +12 -1
- package/dist/core/analyzer.js +56 -11
- package/dist/core/graph-builder.d.ts +1 -0
- package/dist/core/graph-builder.js +153 -58
- package/dist/core/plugin-manager.d.ts +9 -0
- package/dist/core/plugin-manager.js +142 -18
- package/dist/index.d.ts +2 -2
- package/dist/reporters/console.js +9 -2
- package/dist/reporters/dashboard.d.ts +2 -0
- package/dist/reporters/dashboard.js +123 -0
- package/dist/reporters/markdown.js +9 -2
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.js +12 -2
- package/package.json +1 -1
package/dist/core/analyzer.js
CHANGED
|
@@ -9,7 +9,7 @@ import { buildDependencyGraph } from "./graph-builder.js";
|
|
|
9
9
|
import { PluginManager } from "./plugin-manager.js";
|
|
10
10
|
import { calculateHealthScore, calculateScoreDeductions } from "./scorer.js";
|
|
11
11
|
import { buildAnalysisContext } from "./context.js";
|
|
12
|
-
export const OUTPUT_VERSION = "1.
|
|
12
|
+
export const OUTPUT_VERSION = "1.5";
|
|
13
13
|
export async function analyzeProject(options = {}) {
|
|
14
14
|
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
15
15
|
const loadedConfig = await loadDepBrainConfig(rootDir, options.configPath);
|
|
@@ -143,7 +143,12 @@ function mergeConfig(base, overrides) {
|
|
|
143
143
|
},
|
|
144
144
|
risk: {
|
|
145
145
|
transitiveBloatThreshold: overrides.risk?.transitiveBloatThreshold ?? base.risk.transitiveBloatThreshold,
|
|
146
|
-
typosquattingDistanceThreshold: overrides.risk?.typosquattingDistanceThreshold ?? base.risk.typosquattingDistanceThreshold
|
|
146
|
+
typosquattingDistanceThreshold: overrides.risk?.typosquattingDistanceThreshold ?? base.risk.typosquattingDistanceThreshold,
|
|
147
|
+
staleReleaseDays: overrides.risk?.staleReleaseDays ?? base.risk.staleReleaseDays,
|
|
148
|
+
agingReleaseDays: overrides.risk?.agingReleaseDays ?? base.risk.agingReleaseDays,
|
|
149
|
+
lowDownloadThreshold: overrides.risk?.lowDownloadThreshold ?? base.risk.lowDownloadThreshold,
|
|
150
|
+
lowTrustWeightThreshold: overrides.risk?.lowTrustWeightThreshold ?? base.risk.lowTrustWeightThreshold,
|
|
151
|
+
mediumTrustWeightThreshold: overrides.risk?.mediumTrustWeightThreshold ?? base.risk.mediumTrustWeightThreshold
|
|
147
152
|
},
|
|
148
153
|
dashboard: {
|
|
149
154
|
outputPath: overrides.dashboard?.outputPath ?? base.dashboard.outputPath
|
|
@@ -187,7 +192,7 @@ function evaluatePolicy(summary, config) {
|
|
|
187
192
|
}
|
|
188
193
|
async function analyzeSingleProject(rootDir, config, options = {}) {
|
|
189
194
|
const context = await buildAnalysisContext(rootDir, config);
|
|
190
|
-
const results = await runChecks(context, options.focus ?? "all");
|
|
195
|
+
const results = await runChecks(context, options.focus ?? "all", config);
|
|
191
196
|
const issueGroups = normalizeIssues(results, config);
|
|
192
197
|
const duplicates = mapDuplicateIssues(issueGroups.duplicates);
|
|
193
198
|
const unused = mapUnusedIssues(issueGroups.unused);
|
|
@@ -280,7 +285,7 @@ function shouldIgnorePackage(name, bucket, config) {
|
|
|
280
285
|
}
|
|
281
286
|
});
|
|
282
287
|
}
|
|
283
|
-
async function runChecks(context, focus) {
|
|
288
|
+
async function runChecks(context, focus, config) {
|
|
284
289
|
const checks = [
|
|
285
290
|
{
|
|
286
291
|
name: "duplicate",
|
|
@@ -296,7 +301,7 @@ async function runChecks(context, focus) {
|
|
|
296
301
|
},
|
|
297
302
|
{
|
|
298
303
|
name: "risk",
|
|
299
|
-
run: () => runRiskCheck(context.graph)
|
|
304
|
+
run: () => runRiskCheck(context.graph, { thresholds: config.risk })
|
|
300
305
|
}
|
|
301
306
|
];
|
|
302
307
|
const results = [];
|
|
@@ -391,6 +396,10 @@ function mapRiskIssues(issues) {
|
|
|
391
396
|
explanation: normalizeStringArray(issue.explanation),
|
|
392
397
|
trustScore: normalizeTrustScore(issue.meta?.trustScore),
|
|
393
398
|
riskFactors: normalizeRiskFactors(issue.meta?.riskFactors),
|
|
399
|
+
transitiveRiskScore: typeof issue.meta?.transitiveRiskScore === "number"
|
|
400
|
+
? issue.meta.transitiveRiskScore
|
|
401
|
+
: 0,
|
|
402
|
+
riskyTransitiveDeps: normalizeRiskTransitiveDependencies(issue.meta?.riskyTransitiveDeps),
|
|
394
403
|
recommendation: buildRiskRecommendation(issue)
|
|
395
404
|
}));
|
|
396
405
|
}
|
|
@@ -454,13 +463,18 @@ function buildRiskRecommendation(issue) {
|
|
|
454
463
|
const reasons = normalizeStringArray(issue.explanation);
|
|
455
464
|
const confidence = normalizeConfidence(issue.confidence);
|
|
456
465
|
const trustScore = normalizeTrustScore(issue.meta?.trustScore);
|
|
466
|
+
const riskyTransitiveDeps = normalizeRiskTransitiveDependencies(issue.meta?.riskyTransitiveDeps);
|
|
457
467
|
return {
|
|
458
468
|
action: "review",
|
|
459
|
-
priority: trustScore === "low" || confidence >= 0.79
|
|
469
|
+
priority: trustScore === "low" || confidence >= 0.79 || riskyTransitiveDeps.length >= 2
|
|
470
|
+
? "high"
|
|
471
|
+
: "medium",
|
|
460
472
|
safety: "caution",
|
|
461
|
-
summary:
|
|
462
|
-
? "
|
|
463
|
-
:
|
|
473
|
+
summary: riskyTransitiveDeps.length > 0
|
|
474
|
+
? "Review this direct dependency and its transitive chain before upgrading or keeping it."
|
|
475
|
+
: trustScore === "low"
|
|
476
|
+
? "Low trust package; review whether to replace, pin, or monitor it closely."
|
|
477
|
+
: "Review package trust signals and decide whether to keep, replace, or monitor it.",
|
|
464
478
|
reasons
|
|
465
479
|
};
|
|
466
480
|
}
|
|
@@ -599,7 +613,9 @@ function normalizeRiskFactors(value) {
|
|
|
599
613
|
versionCount: null,
|
|
600
614
|
recentReleaseCount: null,
|
|
601
615
|
hasRepository: false,
|
|
602
|
-
dependencyType: "unknown"
|
|
616
|
+
dependencyType: "unknown",
|
|
617
|
+
transitiveDependencyCount: 0,
|
|
618
|
+
riskyTransitiveCount: 0
|
|
603
619
|
};
|
|
604
620
|
}
|
|
605
621
|
const factors = value;
|
|
@@ -612,9 +628,38 @@ function normalizeRiskFactors(value) {
|
|
|
612
628
|
hasRepository: factors.hasRepository === true,
|
|
613
629
|
dependencyType: factors.dependencyType === "dependencies" || factors.dependencyType === "devDependencies"
|
|
614
630
|
? factors.dependencyType
|
|
615
|
-
: "unknown"
|
|
631
|
+
: "unknown",
|
|
632
|
+
transitiveDependencyCount: typeof factors.transitiveDependencyCount === "number" ? factors.transitiveDependencyCount : 0,
|
|
633
|
+
riskyTransitiveCount: typeof factors.riskyTransitiveCount === "number" ? factors.riskyTransitiveCount : 0
|
|
616
634
|
};
|
|
617
635
|
}
|
|
636
|
+
function normalizeRiskTransitiveDependencies(value) {
|
|
637
|
+
if (!Array.isArray(value)) {
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
640
|
+
return value
|
|
641
|
+
.map((entry) => {
|
|
642
|
+
if (!entry || typeof entry !== "object") {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
const item = entry;
|
|
646
|
+
if (typeof item.name !== "string" ||
|
|
647
|
+
(item.trustScore !== "high" && item.trustScore !== "medium" && item.trustScore !== "low") ||
|
|
648
|
+
typeof item.confidence !== "number" ||
|
|
649
|
+
!Array.isArray(item.reasons) ||
|
|
650
|
+
!Array.isArray(item.introducedByPaths)) {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
name: item.name,
|
|
655
|
+
trustScore: item.trustScore,
|
|
656
|
+
confidence: normalizeConfidence(item.confidence),
|
|
657
|
+
reasons: item.reasons.filter((reason) => typeof reason === "string"),
|
|
658
|
+
introducedByPaths: item.introducedByPaths.filter((trace) => typeof trace === "string")
|
|
659
|
+
};
|
|
660
|
+
})
|
|
661
|
+
.filter((entry) => entry !== null);
|
|
662
|
+
}
|
|
618
663
|
function normalizeWorkspaceUsage(value) {
|
|
619
664
|
if (!Array.isArray(value)) {
|
|
620
665
|
return [];
|
|
@@ -11,5 +11,6 @@ export interface DependencyGraph {
|
|
|
11
11
|
overrides: Record<string, unknown>;
|
|
12
12
|
scripts: Record<string, string>;
|
|
13
13
|
lockPackages: Record<string, LockPackageInstance[]>;
|
|
14
|
+
lockDependencies: Record<string, string[]>;
|
|
14
15
|
}
|
|
15
16
|
export declare function buildDependencyGraph(rootDir: string): Promise<DependencyGraph>;
|
|
@@ -7,32 +7,29 @@ export async function buildDependencyGraph(rootDir) {
|
|
|
7
7
|
const pnpmLockfilePath = path.join(rootDir, "pnpm-lock.yaml");
|
|
8
8
|
const yarnLockfilePath = path.join(rootDir, "yarn.lock");
|
|
9
9
|
const packageJson = await readJsonFile(packageJsonPath);
|
|
10
|
-
const lockPackages = new Map();
|
|
11
10
|
try {
|
|
12
11
|
const packageLock = await readJsonFile(lockfilePath);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const instances = lockPackages.get(name) ?? new Map();
|
|
29
|
-
const normalizedPath = `node_modules/${name}`;
|
|
30
|
-
instances.set(normalizedPath, { path: normalizedPath, version: details.version });
|
|
31
|
-
lockPackages.set(name, instances);
|
|
32
|
-
}
|
|
12
|
+
const parsed = parseNpmLockfile(packageLock, {
|
|
13
|
+
...packageJson.dependencies,
|
|
14
|
+
...packageJson.devDependencies
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
rootDir,
|
|
18
|
+
packageJsonPath,
|
|
19
|
+
lockfilePath,
|
|
20
|
+
dependencies: packageJson.dependencies ?? {},
|
|
21
|
+
devDependencies: packageJson.devDependencies ?? {},
|
|
22
|
+
overrides: packageJson.overrides ?? {},
|
|
23
|
+
scripts: packageJson.scripts ?? {},
|
|
24
|
+
lockPackages: parsed.lockPackages,
|
|
25
|
+
lockDependencies: parsed.lockDependencies
|
|
26
|
+
};
|
|
33
27
|
}
|
|
34
28
|
catch {
|
|
35
|
-
const fallbackLockfile = await readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath
|
|
29
|
+
const fallbackLockfile = await readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath, {
|
|
30
|
+
...packageJson.dependencies,
|
|
31
|
+
...packageJson.devDependencies
|
|
32
|
+
});
|
|
36
33
|
return {
|
|
37
34
|
rootDir,
|
|
38
35
|
packageJsonPath,
|
|
@@ -41,29 +38,19 @@ export async function buildDependencyGraph(rootDir) {
|
|
|
41
38
|
devDependencies: packageJson.devDependencies ?? {},
|
|
42
39
|
overrides: packageJson.overrides ?? {},
|
|
43
40
|
scripts: packageJson.scripts ?? {},
|
|
44
|
-
lockPackages: fallbackLockfile.lockPackages
|
|
41
|
+
lockPackages: fallbackLockfile.lockPackages,
|
|
42
|
+
lockDependencies: fallbackLockfile.lockDependencies
|
|
45
43
|
};
|
|
46
44
|
}
|
|
47
|
-
return {
|
|
48
|
-
rootDir,
|
|
49
|
-
packageJsonPath,
|
|
50
|
-
lockfilePath,
|
|
51
|
-
dependencies: packageJson.dependencies ?? {},
|
|
52
|
-
devDependencies: packageJson.devDependencies ?? {},
|
|
53
|
-
overrides: packageJson.overrides ?? {},
|
|
54
|
-
scripts: packageJson.scripts ?? {},
|
|
55
|
-
lockPackages: Object.fromEntries(Array.from(lockPackages.entries()).map(([name, instances]) => [
|
|
56
|
-
name,
|
|
57
|
-
Array.from(instances.values()).sort((left, right) => left.path.localeCompare(right.path))
|
|
58
|
-
]))
|
|
59
|
-
};
|
|
60
45
|
}
|
|
61
|
-
async function readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath) {
|
|
46
|
+
async function readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath, rootDependencies) {
|
|
62
47
|
try {
|
|
63
48
|
const content = await fs.readFile(pnpmLockfilePath, "utf8");
|
|
49
|
+
const parsed = parsePnpmLockfile(content, rootDependencies);
|
|
64
50
|
return {
|
|
65
51
|
lockfilePath: pnpmLockfilePath,
|
|
66
|
-
lockPackages:
|
|
52
|
+
lockPackages: parsed.lockPackages,
|
|
53
|
+
lockDependencies: parsed.lockDependencies
|
|
67
54
|
};
|
|
68
55
|
}
|
|
69
56
|
catch {
|
|
@@ -71,58 +58,150 @@ async function readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath) {
|
|
|
71
58
|
}
|
|
72
59
|
try {
|
|
73
60
|
const content = await fs.readFile(yarnLockfilePath, "utf8");
|
|
61
|
+
const parsed = parseYarnLockfile(content, rootDependencies);
|
|
74
62
|
return {
|
|
75
63
|
lockfilePath: yarnLockfilePath,
|
|
76
|
-
lockPackages:
|
|
64
|
+
lockPackages: parsed.lockPackages,
|
|
65
|
+
lockDependencies: parsed.lockDependencies
|
|
77
66
|
};
|
|
78
67
|
}
|
|
79
68
|
catch {
|
|
80
69
|
return {
|
|
81
|
-
lockPackages: {}
|
|
70
|
+
lockPackages: {},
|
|
71
|
+
lockDependencies: {}
|
|
82
72
|
};
|
|
83
73
|
}
|
|
84
74
|
}
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
75
|
+
function parseNpmLockfile(packageLock, rootDependencies) {
|
|
76
|
+
const lockPackages = new Map();
|
|
77
|
+
const lockDependencies = new Map();
|
|
78
|
+
for (const [packagePath, details] of Object.entries(packageLock.packages ?? {})) {
|
|
79
|
+
const name = details.name ?? extractPackageName(packagePath);
|
|
80
|
+
const version = details.version;
|
|
81
|
+
if (name && version) {
|
|
82
|
+
const instances = lockPackages.get(name) ?? new Map();
|
|
83
|
+
const normalizedPath = packagePath || "node_modules/" + name;
|
|
84
|
+
instances.set(normalizedPath, { path: normalizedPath, version });
|
|
85
|
+
lockPackages.set(name, instances);
|
|
86
|
+
}
|
|
87
|
+
if (name) {
|
|
88
|
+
addDependencyNames(lockDependencies, name, Object.keys(details.dependencies ?? {}));
|
|
89
|
+
}
|
|
88
90
|
}
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
for (const [name, details] of Object.entries(packageLock.dependencies ?? {})) {
|
|
92
|
+
if (details.version) {
|
|
93
|
+
const instances = lockPackages.get(name) ?? new Map();
|
|
94
|
+
const normalizedPath = `node_modules/${name}`;
|
|
95
|
+
instances.set(normalizedPath, { path: normalizedPath, version: details.version });
|
|
96
|
+
lockPackages.set(name, instances);
|
|
97
|
+
}
|
|
98
|
+
addDependencyNames(lockDependencies, name, Object.keys(details.requires ?? {}));
|
|
92
99
|
}
|
|
93
|
-
|
|
100
|
+
addDependencyNames(lockDependencies, "__root__", Object.keys(rootDependencies));
|
|
101
|
+
return {
|
|
102
|
+
lockPackages: toLockPackageRecord(lockPackages),
|
|
103
|
+
lockDependencies: toDependencyRecord(lockDependencies)
|
|
104
|
+
};
|
|
94
105
|
}
|
|
95
|
-
function parsePnpmLockfile(content) {
|
|
106
|
+
function parsePnpmLockfile(content, rootDependencies) {
|
|
96
107
|
const lockPackages = new Map();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
const lockDependencies = new Map();
|
|
109
|
+
const lines = content.split(/\r?\n/);
|
|
110
|
+
let currentName = null;
|
|
111
|
+
let currentVersion = null;
|
|
112
|
+
let inDependenciesBlock = false;
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
const packageMatch = line.match(/^\s{2}(?:'|")?\/((?:@[^/]+\/)?[^/@'"]+)@([^('":]+)[^:]*:(?:'|")?\s*$/);
|
|
115
|
+
if (packageMatch) {
|
|
116
|
+
currentName = packageMatch[1];
|
|
117
|
+
currentVersion = packageMatch[2];
|
|
118
|
+
inDependenciesBlock = false;
|
|
119
|
+
addLockPackage(lockPackages, currentName, `pnpm:${currentName}@${currentVersion}`, currentVersion);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!currentName) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (/^\s{4}(?:dependencies|optionalDependencies):\s*$/.test(line)) {
|
|
126
|
+
inDependenciesBlock = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (/^\s{4}\S/.test(line) && !/^\s{4}(?:dependencies|optionalDependencies):\s*$/.test(line)) {
|
|
130
|
+
inDependenciesBlock = false;
|
|
131
|
+
}
|
|
132
|
+
if (!inDependenciesBlock) {
|
|
100
133
|
continue;
|
|
101
134
|
}
|
|
102
|
-
|
|
135
|
+
const dependencyMatch = line.match(/^\s{6}((?:@[^/]+\/)?[^:\s]+):\s*(.+)?$/);
|
|
136
|
+
if (!dependencyMatch) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
addDependencyNames(lockDependencies, currentName, [dependencyMatch[1]]);
|
|
103
140
|
}
|
|
104
|
-
|
|
141
|
+
addDependencyNames(lockDependencies, "__root__", Object.keys(rootDependencies));
|
|
142
|
+
return {
|
|
143
|
+
lockPackages: toLockPackageRecord(lockPackages),
|
|
144
|
+
lockDependencies: toDependencyRecord(lockDependencies)
|
|
145
|
+
};
|
|
105
146
|
}
|
|
106
|
-
function parseYarnLockfile(content) {
|
|
147
|
+
function parseYarnLockfile(content, rootDependencies) {
|
|
107
148
|
const lockPackages = new Map();
|
|
149
|
+
const lockDependencies = new Map();
|
|
150
|
+
const lines = content.split(/\r?\n/);
|
|
108
151
|
let currentNames = [];
|
|
109
|
-
|
|
152
|
+
let currentVersion = null;
|
|
153
|
+
let inDependenciesBlock = false;
|
|
154
|
+
for (const line of lines) {
|
|
110
155
|
if (line.trim().length === 0 || line.startsWith("#")) {
|
|
111
156
|
continue;
|
|
112
157
|
}
|
|
113
158
|
if (!line.startsWith(" ") && line.endsWith(":")) {
|
|
114
159
|
currentNames = extractYarnEntryNames(line.slice(0, -1));
|
|
160
|
+
currentVersion = null;
|
|
161
|
+
inDependenciesBlock = false;
|
|
115
162
|
continue;
|
|
116
163
|
}
|
|
117
164
|
const versionMatch = line.match(/^\s+version\s+"?([^"\s]+)"?\s*$/);
|
|
118
|
-
if (
|
|
165
|
+
if (versionMatch) {
|
|
166
|
+
currentVersion = versionMatch[1];
|
|
167
|
+
for (const name of currentNames) {
|
|
168
|
+
addLockPackage(lockPackages, name, `yarn:${name}@${currentVersion}`, currentVersion);
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (/^\s{2}dependencies:\s*$/.test(line)) {
|
|
173
|
+
inDependenciesBlock = true;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (/^\s{2}\S/.test(line) && !/^\s{2}dependencies:\s*$/.test(line)) {
|
|
177
|
+
inDependenciesBlock = false;
|
|
178
|
+
}
|
|
179
|
+
if (!inDependenciesBlock) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const dependencyMatch = line.match(/^\s{4}((?:@[^/]+\/)?[^"\s]+)\s+/);
|
|
183
|
+
if (!dependencyMatch) {
|
|
119
184
|
continue;
|
|
120
185
|
}
|
|
121
186
|
for (const name of currentNames) {
|
|
122
|
-
|
|
187
|
+
addDependencyNames(lockDependencies, name, [dependencyMatch[1]]);
|
|
123
188
|
}
|
|
124
189
|
}
|
|
125
|
-
|
|
190
|
+
addDependencyNames(lockDependencies, "__root__", Object.keys(rootDependencies));
|
|
191
|
+
return {
|
|
192
|
+
lockPackages: toLockPackageRecord(lockPackages),
|
|
193
|
+
lockDependencies: toDependencyRecord(lockDependencies)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function extractPackageName(packagePath) {
|
|
197
|
+
if (!packagePath) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const match = packagePath.match(/(?:^|\/)node_modules\/(.+)$/);
|
|
201
|
+
if (!match) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return match[1];
|
|
126
205
|
}
|
|
127
206
|
function extractYarnEntryNames(entry) {
|
|
128
207
|
const names = new Set();
|
|
@@ -149,9 +228,25 @@ function addLockPackage(lockPackages, name, packagePath, version) {
|
|
|
149
228
|
instances.set(packagePath, { path: packagePath, version });
|
|
150
229
|
lockPackages.set(name, instances);
|
|
151
230
|
}
|
|
231
|
+
function addDependencyNames(lockDependencies, name, dependencies) {
|
|
232
|
+
if (dependencies.length === 0) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const entry = lockDependencies.get(name) ?? new Set();
|
|
236
|
+
for (const dependency of dependencies) {
|
|
237
|
+
entry.add(dependency);
|
|
238
|
+
}
|
|
239
|
+
lockDependencies.set(name, entry);
|
|
240
|
+
}
|
|
152
241
|
function toLockPackageRecord(lockPackages) {
|
|
153
242
|
return Object.fromEntries(Array.from(lockPackages.entries()).map(([name, instances]) => [
|
|
154
243
|
name,
|
|
155
244
|
Array.from(instances.values()).sort((left, right) => left.path.localeCompare(right.path))
|
|
156
245
|
]));
|
|
157
246
|
}
|
|
247
|
+
function toDependencyRecord(lockDependencies) {
|
|
248
|
+
return Object.fromEntries(Array.from(lockDependencies.entries()).map(([name, dependencies]) => [
|
|
249
|
+
name,
|
|
250
|
+
Array.from(dependencies).sort((left, right) => left.localeCompare(right))
|
|
251
|
+
]));
|
|
252
|
+
}
|
|
@@ -11,10 +11,19 @@ export interface DepBrainPlugin {
|
|
|
11
11
|
reportHook?: (result: AnalysisResult) => Promise<Record<string, unknown> | void> | Record<string, unknown> | void;
|
|
12
12
|
cliCommands?: (cli: unknown) => void;
|
|
13
13
|
}
|
|
14
|
+
export interface PluginDiagnostic {
|
|
15
|
+
spec: string;
|
|
16
|
+
code: "load_failed" | "invalid_plugin" | "hook_failed";
|
|
17
|
+
message: string;
|
|
18
|
+
plugin?: string;
|
|
19
|
+
hook?: "preScan" | "postScan" | "reportHook";
|
|
20
|
+
}
|
|
14
21
|
export declare class PluginManager {
|
|
15
22
|
private readonly plugins;
|
|
23
|
+
private readonly diagnostics;
|
|
16
24
|
private constructor();
|
|
17
25
|
static load(rootDir: string, config: DepBrainConfig): Promise<PluginManager>;
|
|
18
26
|
runPreScan(context: ProjectContext): Promise<void>;
|
|
19
27
|
runPostScan(result: AnalysisResult): Promise<AnalysisResult>;
|
|
28
|
+
private attachDiagnostics;
|
|
20
29
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { pathToFileURL } from "node:url";
|
|
3
4
|
export class PluginManager {
|
|
4
5
|
plugins;
|
|
5
|
-
|
|
6
|
+
diagnostics;
|
|
7
|
+
constructor(plugins, diagnostics) {
|
|
6
8
|
this.plugins = plugins;
|
|
9
|
+
this.diagnostics = diagnostics;
|
|
7
10
|
}
|
|
8
11
|
static async load(rootDir, config) {
|
|
9
12
|
const specs = [
|
|
@@ -11,38 +14,71 @@ export class PluginManager {
|
|
|
11
14
|
...config.plugins.paths
|
|
12
15
|
];
|
|
13
16
|
const plugins = [];
|
|
17
|
+
const diagnostics = [];
|
|
14
18
|
for (const spec of specs) {
|
|
15
|
-
const
|
|
16
|
-
if (plugin) {
|
|
17
|
-
plugins.push(plugin);
|
|
19
|
+
const result = await loadPlugin(rootDir, spec);
|
|
20
|
+
if (result.plugin) {
|
|
21
|
+
plugins.push(result.plugin);
|
|
22
|
+
}
|
|
23
|
+
if (result.diagnostic) {
|
|
24
|
+
diagnostics.push(result.diagnostic);
|
|
18
25
|
}
|
|
19
26
|
}
|
|
20
|
-
return new PluginManager(plugins);
|
|
27
|
+
return new PluginManager(plugins, diagnostics);
|
|
21
28
|
}
|
|
22
29
|
async runPreScan(context) {
|
|
23
30
|
for (const plugin of this.plugins) {
|
|
24
|
-
|
|
31
|
+
try {
|
|
32
|
+
await plugin.preScan?.(context);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
this.diagnostics.push(buildHookDiagnostic(plugin.name, "preScan", error));
|
|
36
|
+
}
|
|
25
37
|
}
|
|
26
38
|
}
|
|
27
39
|
async runPostScan(result) {
|
|
28
40
|
let current = result;
|
|
29
41
|
for (const plugin of this.plugins) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
try {
|
|
43
|
+
const next = await plugin.postScan?.(current);
|
|
44
|
+
if (next) {
|
|
45
|
+
current = next;
|
|
46
|
+
}
|
|
33
47
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
current.extensions[plugin.name] = {
|
|
37
|
-
...(asRecord(current.extensions[plugin.name]) ?? {}),
|
|
38
|
-
...reportSection
|
|
39
|
-
};
|
|
48
|
+
catch (error) {
|
|
49
|
+
this.diagnostics.push(buildHookDiagnostic(plugin.name, "postScan", error));
|
|
40
50
|
}
|
|
51
|
+
try {
|
|
52
|
+
const reportSection = await plugin.reportHook?.(current);
|
|
53
|
+
if (reportSection) {
|
|
54
|
+
current.extensions[plugin.name] = {
|
|
55
|
+
...(asRecord(current.extensions[plugin.name]) ?? {}),
|
|
56
|
+
...reportSection
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
this.diagnostics.push(buildHookDiagnostic(plugin.name, "reportHook", error));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return this.attachDiagnostics(current);
|
|
65
|
+
}
|
|
66
|
+
attachDiagnostics(result) {
|
|
67
|
+
if (this.diagnostics.length === 0) {
|
|
68
|
+
return result;
|
|
41
69
|
}
|
|
42
|
-
|
|
70
|
+
result.extensions.depBrain = {
|
|
71
|
+
...(asRecord(result.extensions.depBrain) ?? {}),
|
|
72
|
+
plugins: this.diagnostics
|
|
73
|
+
};
|
|
74
|
+
return result;
|
|
43
75
|
}
|
|
44
76
|
}
|
|
45
77
|
async function loadPlugin(rootDir, spec) {
|
|
78
|
+
const builtIn = getBuiltInPlugin(spec);
|
|
79
|
+
if (builtIn) {
|
|
80
|
+
return { plugin: builtIn };
|
|
81
|
+
}
|
|
46
82
|
try {
|
|
47
83
|
const resolved = spec.startsWith(".") || path.isAbsolute(spec)
|
|
48
84
|
? path.resolve(rootDir, spec)
|
|
@@ -51,11 +87,99 @@ async function loadPlugin(rootDir, spec) {
|
|
|
51
87
|
const mod = await import(moduleUrl);
|
|
52
88
|
const exported = mod.default ?? mod.plugin ?? mod;
|
|
53
89
|
const candidate = typeof exported === "function" ? new exported() : exported;
|
|
54
|
-
|
|
90
|
+
if (isPlugin(candidate)) {
|
|
91
|
+
return { plugin: candidate };
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
plugin: null,
|
|
95
|
+
diagnostic: {
|
|
96
|
+
spec,
|
|
97
|
+
code: "invalid_plugin",
|
|
98
|
+
message: "Plugin must export an object with a string name."
|
|
99
|
+
}
|
|
100
|
+
};
|
|
55
101
|
}
|
|
56
|
-
catch {
|
|
102
|
+
catch (error) {
|
|
103
|
+
return {
|
|
104
|
+
plugin: null,
|
|
105
|
+
diagnostic: {
|
|
106
|
+
spec,
|
|
107
|
+
code: "load_failed",
|
|
108
|
+
message: error instanceof Error ? error.message : String(error)
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function getBuiltInPlugin(spec) {
|
|
114
|
+
if (spec !== "license" && spec !== "dep-brain-plugin-license") {
|
|
57
115
|
return null;
|
|
58
116
|
}
|
|
117
|
+
return {
|
|
118
|
+
name: "license",
|
|
119
|
+
reportHook: async (result) => {
|
|
120
|
+
const packages = await collectLicensePackages(result.rootDir);
|
|
121
|
+
const licenses = packages.reduce((acc, item) => {
|
|
122
|
+
acc[item.license] = (acc[item.license] ?? 0) + 1;
|
|
123
|
+
return acc;
|
|
124
|
+
}, {});
|
|
125
|
+
return {
|
|
126
|
+
summary: {
|
|
127
|
+
total: packages.length,
|
|
128
|
+
unknown: packages.filter((item) => item.license === "UNKNOWN").length,
|
|
129
|
+
licenses
|
|
130
|
+
},
|
|
131
|
+
packages
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async function collectLicensePackages(rootDir) {
|
|
137
|
+
const raw = await fs.readFile(path.join(rootDir, "package.json"), "utf8");
|
|
138
|
+
const pkg = JSON.parse(raw);
|
|
139
|
+
const names = Object.keys({
|
|
140
|
+
...(pkg.dependencies ?? {}),
|
|
141
|
+
...(pkg.devDependencies ?? {})
|
|
142
|
+
}).sort();
|
|
143
|
+
return Promise.all(names.map(async (name) => ({
|
|
144
|
+
name,
|
|
145
|
+
license: await readPackageLicense(rootDir, name)
|
|
146
|
+
})));
|
|
147
|
+
}
|
|
148
|
+
async function readPackageLicense(rootDir, name) {
|
|
149
|
+
try {
|
|
150
|
+
const raw = await fs.readFile(path.join(rootDir, "node_modules", name, "package.json"), "utf8");
|
|
151
|
+
const pkg = JSON.parse(raw);
|
|
152
|
+
if (typeof pkg.license === "string" && pkg.license.trim().length > 0) {
|
|
153
|
+
return pkg.license;
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(pkg.licenses) && pkg.licenses.length > 0) {
|
|
156
|
+
const licenses = pkg.licenses
|
|
157
|
+
.map((item) => {
|
|
158
|
+
if (typeof item === "string") {
|
|
159
|
+
return item;
|
|
160
|
+
}
|
|
161
|
+
if (item && typeof item === "object" && typeof item.type === "string") {
|
|
162
|
+
return item.type;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
})
|
|
166
|
+
.filter((item) => Boolean(item));
|
|
167
|
+
return licenses.length > 0 ? licenses.join(", ") : "UNKNOWN";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return "UNKNOWN";
|
|
172
|
+
}
|
|
173
|
+
return "UNKNOWN";
|
|
174
|
+
}
|
|
175
|
+
function buildHookDiagnostic(plugin, hook, error) {
|
|
176
|
+
return {
|
|
177
|
+
spec: plugin,
|
|
178
|
+
plugin,
|
|
179
|
+
hook,
|
|
180
|
+
code: "hook_failed",
|
|
181
|
+
message: error instanceof Error ? error.message : String(error)
|
|
182
|
+
};
|
|
59
183
|
}
|
|
60
184
|
function isPlugin(value) {
|
|
61
185
|
return Boolean(value &&
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { analyzeProject } from "./core/analyzer.js";
|
|
2
|
-
export type { AnalysisOptions, AnalysisFocus, AnalysisResult, DepBrainBaseline, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } from "./core/analyzer.js";
|
|
2
|
+
export type { AnalysisOptions, AnalysisFocus, AnalysisResult, DepBrainBaseline, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, RiskTransitiveDependency, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } from "./core/analyzer.js";
|
|
3
3
|
export { OUTPUT_VERSION } from "./core/analyzer.js";
|
|
4
4
|
export { PluginManager } from "./core/plugin-manager.js";
|
|
5
|
-
export type { DepBrainPlugin, ProjectContext } from "./core/plugin-manager.js";
|
|
5
|
+
export type { DepBrainPlugin, PluginDiagnostic, ProjectContext } from "./core/plugin-manager.js";
|
|
6
6
|
export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
|
|
7
7
|
export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
|
|
8
8
|
export type { WorkspacePackage } from "./utils/workspaces.js";
|