dep-brain 1.3.0 → 1.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/CHANGELOG.md +16 -0
- package/README.md +16 -1
- package/depbrain.output.schema.json +41 -4
- package/dist/checks/outdated.d.ts +4 -1
- package/dist/checks/outdated.js +180 -28
- package/dist/checks/risk.js +179 -25
- package/dist/cli.js +40 -8
- package/dist/core/analyzer.d.ts +22 -1
- package/dist/core/analyzer.js +107 -16
- package/dist/core/graph-builder.d.ts +1 -0
- package/dist/core/graph-builder.js +153 -58
- package/dist/index.d.ts +1 -1
- package/dist/reporters/console.js +20 -4
- package/dist/reporters/dashboard.js +74 -5
- package/dist/reporters/markdown.js +20 -4
- package/dist/utils/npm-api.d.ts +2 -0
- package/dist/utils/npm-api.js +3 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
5
|
export type { DepBrainPlugin, PluginDiagnostic, ProjectContext } from "./core/plugin-manager.js";
|
|
@@ -28,11 +28,11 @@ export function renderConsoleReport(result) {
|
|
|
28
28
|
? `${item.name} (${item.section}) [${item.package}]`
|
|
29
29
|
: `${item.name} (${item.section})`, item.confidence, item.explanation, item.recommendation)));
|
|
30
30
|
appendSection(lines, "Outdated dependencies", result.outdated.map((item) => formatEntry(item.package
|
|
31
|
-
? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
|
|
32
|
-
: `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
|
|
31
|
+
? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]${formatOutdatedAdviceSuffix(item)}`
|
|
32
|
+
: `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]${formatOutdatedAdviceSuffix(item)}`, item.confidence, item.explanation, item.recommendation)));
|
|
33
33
|
appendSection(lines, "Risky dependencies", result.risks.map((item) => formatEntry(item.package
|
|
34
|
-
? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]`
|
|
35
|
-
: `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]`, item.confidence, item.explanation, item.recommendation)));
|
|
34
|
+
? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`
|
|
35
|
+
: `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`, item.confidence, item.explanation, item.recommendation)));
|
|
36
36
|
appendSection(lines, "Policy reasons", result.policy.reasons);
|
|
37
37
|
if (result.suggestions.length > 0) {
|
|
38
38
|
lines.push("");
|
|
@@ -43,6 +43,13 @@ export function renderConsoleReport(result) {
|
|
|
43
43
|
}
|
|
44
44
|
return lines.join("\n");
|
|
45
45
|
}
|
|
46
|
+
function formatTransitiveRiskSuffix(item) {
|
|
47
|
+
if (item.riskyTransitiveDeps.length === 0) {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
const names = item.riskyTransitiveDeps.slice(0, 3).map((entry) => entry.name).join(", ");
|
|
51
|
+
return ` [transitive score ${item.transitiveRiskScore}] [via ${names}]`;
|
|
52
|
+
}
|
|
46
53
|
function summaryLine(label, count) {
|
|
47
54
|
const indicator = count === 0 ? "OK" : "WARN";
|
|
48
55
|
return `${indicator} ${label}: ${count}`;
|
|
@@ -64,3 +71,12 @@ function formatEntry(label, confidence, explanation, recommendation) {
|
|
|
64
71
|
: "";
|
|
65
72
|
return `${label} | confidence ${Math.round(confidence * 100)}%${recommendationSummary}${reasonSummary}`;
|
|
66
73
|
}
|
|
74
|
+
function formatOutdatedAdviceSuffix(item) {
|
|
75
|
+
if (!item.advice.recommendedTarget) {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
const steps = item.advice.intermediateSteps.length > 1
|
|
79
|
+
? ` steps ${item.advice.intermediateSteps.join(" -> ")}`
|
|
80
|
+
: "";
|
|
81
|
+
return ` [advice ${item.advice.risk.toUpperCase()} target ${item.advice.recommendedTarget}${steps}]`;
|
|
82
|
+
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
export function renderDashboardReport(result) {
|
|
2
2
|
const topIssues = result.topIssues.map(renderTopIssue).join("");
|
|
3
3
|
const suggestions = result.suggestions.map((item) => `<li>${escapeHtml(item)}</li>`).join("");
|
|
4
|
+
const transitiveHotspots = result.risks
|
|
5
|
+
.filter((item) => item.riskyTransitiveDeps.length > 0)
|
|
6
|
+
.sort((left, right) => right.transitiveRiskScore - left.transitiveRiskScore)
|
|
7
|
+
.map(renderTransitiveHotspot)
|
|
8
|
+
.join("");
|
|
9
|
+
const upgradeAdvice = result.outdated
|
|
10
|
+
.filter((item) => item.advice.risk !== "low" || item.updateType === "major")
|
|
11
|
+
.sort((left, right) => compareAdviceRisk(right.advice.risk, left.advice.risk) ||
|
|
12
|
+
left.name.localeCompare(right.name))
|
|
13
|
+
.map(renderUpgradeAdvice)
|
|
14
|
+
.join("");
|
|
4
15
|
return [
|
|
5
16
|
"<!doctype html>",
|
|
6
17
|
'<html lang="en">',
|
|
@@ -14,9 +25,11 @@ export function renderDashboardReport(result) {
|
|
|
14
25
|
"header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}",
|
|
15
26
|
"h1{font-size:28px;margin:0 0 8px}",
|
|
16
27
|
"h2{font-size:18px;margin:0 0 12px}",
|
|
28
|
+
"h3{font-size:15px;margin:0 0 8px}",
|
|
17
29
|
".muted{color:#637083;font-size:13px}",
|
|
18
30
|
".score{font-size:48px;font-weight:700}",
|
|
19
31
|
".grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:20px}",
|
|
32
|
+
".split{display:grid;grid-template-columns:1.2fr .8fr;gap:16px;margin-top:16px}",
|
|
20
33
|
".panel{background:#fff;border:1px solid #dce3ee;border-radius:8px;padding:16px}",
|
|
21
34
|
".metric{font-size:28px;font-weight:700;margin-top:4px}",
|
|
22
35
|
".pass{color:#167a43}.fail{color:#b42318}",
|
|
@@ -24,7 +37,9 @@ export function renderDashboardReport(result) {
|
|
|
24
37
|
"li{margin:8px 0}",
|
|
25
38
|
".issue{margin-bottom:10px}",
|
|
26
39
|
".kind{font-size:12px;text-transform:uppercase;color:#637083}",
|
|
27
|
-
"
|
|
40
|
+
".hotspot{border-top:1px solid #e7ecf4;padding-top:12px;margin-top:12px}",
|
|
41
|
+
".path{font-family:Consolas,monospace;font-size:12px;background:#f3f6fb;border-radius:6px;padding:6px 8px;margin:6px 0}",
|
|
42
|
+
"@media(max-width:760px){main{padding:20px}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.split{grid-template-columns:1fr}header{display:block}.score{font-size:40px}}",
|
|
28
43
|
"</style>",
|
|
29
44
|
"</head>",
|
|
30
45
|
"<body>",
|
|
@@ -42,20 +57,42 @@ export function renderDashboardReport(result) {
|
|
|
42
57
|
renderMetric("Outdated", result.outdated.length),
|
|
43
58
|
renderMetric("Risks", result.risks.length),
|
|
44
59
|
"</section>",
|
|
45
|
-
'<section class="
|
|
60
|
+
'<section class="split">',
|
|
61
|
+
'<div class="panel">',
|
|
46
62
|
"<h2>Policy</h2>",
|
|
47
63
|
`<p class="${result.policy.passed ? "pass" : "fail"}">${result.policy.passed ? "Passed" : "Failed"}</p>`,
|
|
48
64
|
result.policy.reasons.length > 0
|
|
49
65
|
? `<ul>${result.policy.reasons.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`
|
|
50
66
|
: '<p class="muted">No policy failures.</p>',
|
|
67
|
+
"</div>",
|
|
68
|
+
'<div class="panel">',
|
|
69
|
+
"<h2>Risk Snapshot</h2>",
|
|
70
|
+
`<div class="muted">${result.risks.filter((item) => item.riskyTransitiveDeps.length > 0).length} direct dependencies carry transitive risk.</div>`,
|
|
71
|
+
`<div class="metric">${result.risks.reduce((total, item) => total + item.transitiveRiskScore, 0)}</div>`,
|
|
72
|
+
'<div class="muted">Total transitive risk score</div>',
|
|
73
|
+
"</div>",
|
|
51
74
|
"</section>",
|
|
52
|
-
'<section class="
|
|
75
|
+
'<section class="split">',
|
|
76
|
+
'<div class="panel">',
|
|
53
77
|
"<h2>Top Issues</h2>",
|
|
54
78
|
topIssues.length > 0 ? `<ol>${topIssues}</ol>` : '<p class="muted">No actionable issues found.</p>',
|
|
55
|
-
"</
|
|
56
|
-
'<
|
|
79
|
+
"</div>",
|
|
80
|
+
'<div class="panel">',
|
|
57
81
|
"<h2>Suggestions</h2>",
|
|
58
82
|
suggestions.length > 0 ? `<ul>${suggestions}</ul>` : '<p class="muted">No suggestions.</p>',
|
|
83
|
+
"</div>",
|
|
84
|
+
"</section>",
|
|
85
|
+
'<section class="panel">',
|
|
86
|
+
"<h2>Upgrade Priorities</h2>",
|
|
87
|
+
upgradeAdvice.length > 0
|
|
88
|
+
? `<ul>${upgradeAdvice}</ul>`
|
|
89
|
+
: '<p class="muted">No high-risk upgrades found.</p>',
|
|
90
|
+
"</section>",
|
|
91
|
+
'<section class="panel">',
|
|
92
|
+
"<h2>Transitive Risk Hotspots</h2>",
|
|
93
|
+
transitiveHotspots.length > 0
|
|
94
|
+
? transitiveHotspots
|
|
95
|
+
: '<p class="muted">No transitive risk hotspots found.</p>',
|
|
59
96
|
"</section>",
|
|
60
97
|
"</main>",
|
|
61
98
|
"</body>",
|
|
@@ -74,6 +111,38 @@ function renderTopIssue(item) {
|
|
|
74
111
|
"</li>"
|
|
75
112
|
].join("");
|
|
76
113
|
}
|
|
114
|
+
function renderTransitiveHotspot(item) {
|
|
115
|
+
return [
|
|
116
|
+
'<div class="hotspot">',
|
|
117
|
+
`<h3>${escapeHtml(item.name)} <span class="muted">score ${item.transitiveRiskScore}</span></h3>`,
|
|
118
|
+
`<div class="muted">${item.riskFactors.transitiveDependencyCount} transitive dependencies, ${item.riskyTransitiveDeps.length} risky transitive dependencies</div>`,
|
|
119
|
+
"<ul>",
|
|
120
|
+
item.riskyTransitiveDeps
|
|
121
|
+
.slice(0, 4)
|
|
122
|
+
.map((entry) => `<li><strong>${escapeHtml(entry.name)}</strong> [${escapeHtml(entry.trustScore.toUpperCase())}]<div>${escapeHtml(entry.reasons.join("; "))}</div>${entry.introducedByPaths.map((trace) => `<div class="path">${escapeHtml(trace)}</div>`).join("")}</li>`)
|
|
123
|
+
.join(""),
|
|
124
|
+
"</ul>",
|
|
125
|
+
"</div>"
|
|
126
|
+
].join("");
|
|
127
|
+
}
|
|
128
|
+
function renderUpgradeAdvice(item) {
|
|
129
|
+
return [
|
|
130
|
+
"<li>",
|
|
131
|
+
`<strong>${escapeHtml(item.name)}</strong> <span class="muted">[${escapeHtml(item.advice.risk.toUpperCase())}]</span>`,
|
|
132
|
+
`<div>${escapeHtml(item.current)} -> ${escapeHtml(item.latest)} | target ${escapeHtml(item.advice.recommendedTarget)}</div>`,
|
|
133
|
+
item.advice.intermediateSteps.length > 1
|
|
134
|
+
? `<div class="path">${escapeHtml(item.advice.intermediateSteps.join(" -> "))}</div>`
|
|
135
|
+
: "",
|
|
136
|
+
item.advice.releaseNotes[0]
|
|
137
|
+
? `<div class="muted">${escapeHtml(item.advice.releaseNotes[0])}</div>`
|
|
138
|
+
: "",
|
|
139
|
+
"</li>"
|
|
140
|
+
].join("");
|
|
141
|
+
}
|
|
142
|
+
function compareAdviceRisk(left, right) {
|
|
143
|
+
const rank = { high: 3, medium: 2, low: 1 };
|
|
144
|
+
return rank[left] - rank[right];
|
|
145
|
+
}
|
|
77
146
|
function escapeHtml(value) {
|
|
78
147
|
return value
|
|
79
148
|
.replace(/&/g, "&")
|
|
@@ -32,11 +32,11 @@ export function renderMarkdownReport(result) {
|
|
|
32
32
|
? `${item.name} (${item.section}) [${item.package}]`
|
|
33
33
|
: `${item.name} (${item.section})`, item.confidence, item.explanation, item.recommendation)));
|
|
34
34
|
appendSection(lines, "Outdated dependencies", result.outdated.map((item) => formatEntry(item.package
|
|
35
|
-
? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
|
|
36
|
-
: `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
|
|
35
|
+
? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]${formatOutdatedAdviceSuffix(item)}`
|
|
36
|
+
: `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]${formatOutdatedAdviceSuffix(item)}`, item.confidence, item.explanation, item.recommendation)));
|
|
37
37
|
appendSection(lines, "Risky dependencies", result.risks.map((item) => formatEntry(item.package
|
|
38
|
-
? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]`
|
|
39
|
-
: `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]`, item.confidence, item.explanation, item.recommendation)));
|
|
38
|
+
? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`
|
|
39
|
+
: `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`, item.confidence, item.explanation, item.recommendation)));
|
|
40
40
|
appendSection(lines, "Policy reasons", result.policy.reasons);
|
|
41
41
|
if (result.suggestions.length > 0) {
|
|
42
42
|
lines.push("## Suggestions");
|
|
@@ -47,6 +47,13 @@ export function renderMarkdownReport(result) {
|
|
|
47
47
|
}
|
|
48
48
|
return lines.join("\n");
|
|
49
49
|
}
|
|
50
|
+
function formatTransitiveRiskSuffix(item) {
|
|
51
|
+
if (item.riskyTransitiveDeps.length === 0) {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
const names = item.riskyTransitiveDeps.slice(0, 3).map((entry) => entry.name).join(", ");
|
|
55
|
+
return ` [transitive score ${item.transitiveRiskScore}] [via ${names}]`;
|
|
56
|
+
}
|
|
50
57
|
function appendSection(lines, title, items) {
|
|
51
58
|
if (items.length === 0) {
|
|
52
59
|
return;
|
|
@@ -64,3 +71,12 @@ function formatEntry(label, confidence, explanation, recommendation) {
|
|
|
64
71
|
: "";
|
|
65
72
|
return `${label} | confidence ${Math.round(confidence * 100)}%${recommendationSummary}${reasonSummary}`;
|
|
66
73
|
}
|
|
74
|
+
function formatOutdatedAdviceSuffix(item) {
|
|
75
|
+
if (!item.advice.recommendedTarget) {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
const steps = item.advice.intermediateSteps.length > 1
|
|
79
|
+
? ` steps ${item.advice.intermediateSteps.join(" -> ")}`
|
|
80
|
+
: "";
|
|
81
|
+
return ` [advice ${item.advice.risk.toUpperCase()} target ${item.advice.recommendedTarget}${steps}]`;
|
|
82
|
+
}
|
package/dist/utils/npm-api.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export interface PackageMetadata {
|
|
2
2
|
latestVersion: string | null;
|
|
3
3
|
repository: string | null;
|
|
4
|
+
homepage: string | null;
|
|
4
5
|
downloads: number | null;
|
|
5
6
|
daysSincePublish: number | null;
|
|
6
7
|
maintainersCount: number | null;
|
|
7
8
|
versionCount: number | null;
|
|
8
9
|
recentReleaseCount: number | null;
|
|
10
|
+
versions: string[];
|
|
9
11
|
}
|
|
10
12
|
export declare function getLatestVersion(name: string): Promise<string | null>;
|
|
11
13
|
export declare function getPackageMetadata(name: string): Promise<PackageMetadata | null>;
|
package/dist/utils/npm-api.js
CHANGED
|
@@ -49,11 +49,13 @@ async function fetchPackageMetadata(name) {
|
|
|
49
49
|
return {
|
|
50
50
|
latestVersion,
|
|
51
51
|
repository,
|
|
52
|
+
homepage: typeof packageJson.homepage === "string" ? packageJson.homepage : null,
|
|
52
53
|
downloads: downloadsJson.downloads ?? null,
|
|
53
54
|
daysSincePublish,
|
|
54
55
|
maintainersCount,
|
|
55
56
|
versionCount,
|
|
56
|
-
recentReleaseCount
|
|
57
|
+
recentReleaseCount,
|
|
58
|
+
versions: Object.keys(packageJson.versions ?? {})
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
61
|
catch {
|