@getcodesentinel/codesentinel 1.5.0 → 1.6.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 +3 -0
- package/dist/index.js +1860 -1237
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,1428 +2,2018 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command, Option } from "commander";
|
|
5
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
6
|
-
import { dirname, resolve as resolve3 } from "path";
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
8
5
|
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
external: summary.external.available ? {
|
|
22
|
-
available: true,
|
|
23
|
-
metrics: summary.external.metrics,
|
|
24
|
-
highRiskDependenciesTop: summary.external.highRiskDependencies.slice(0, 10),
|
|
25
|
-
highRiskDevelopmentDependenciesTop: summary.external.highRiskDevelopmentDependencies.slice(
|
|
26
|
-
0,
|
|
27
|
-
10
|
|
28
|
-
),
|
|
29
|
-
transitiveExposureDependenciesTop: summary.external.transitiveExposureDependencies.slice(0, 10)
|
|
30
|
-
} : {
|
|
31
|
-
available: false,
|
|
32
|
-
reason: summary.external.reason
|
|
33
|
-
},
|
|
34
|
-
risk: {
|
|
35
|
-
repositoryScore: summary.risk.repositoryScore,
|
|
36
|
-
normalizedScore: summary.risk.normalizedScore,
|
|
37
|
-
hotspotsTop: summary.risk.hotspots.slice(0, 5).map((hotspot) => ({
|
|
38
|
-
file: hotspot.file,
|
|
39
|
-
score: hotspot.score
|
|
40
|
-
})),
|
|
41
|
-
fragileClusterCount: summary.risk.fragileClusters.length,
|
|
42
|
-
dependencyAmplificationZoneCount: summary.risk.dependencyAmplificationZones.length
|
|
6
|
+
// ../dependency-firewall/dist/index.js
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
10
|
+
var round4 = (value) => Number(value.toFixed(4));
|
|
11
|
+
var normalizeNodes = (nodes) => {
|
|
12
|
+
const byName = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const node of nodes) {
|
|
14
|
+
const bucket = byName.get(node.name) ?? [];
|
|
15
|
+
bucket.push(node);
|
|
16
|
+
byName.set(node.name, bucket);
|
|
43
17
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return false;
|
|
18
|
+
const normalized = [];
|
|
19
|
+
for (const [name, candidates] of byName.entries()) {
|
|
20
|
+
if (candidates.length === 0) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
candidates.sort((a, b) => b.version.localeCompare(a.version));
|
|
24
|
+
const selected = candidates[0];
|
|
25
|
+
if (selected === void 0) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const deps = selected.dependencies.map((dep) => {
|
|
29
|
+
const at = dep.lastIndexOf("@");
|
|
30
|
+
return at <= 0 ? dep : dep.slice(0, at);
|
|
31
|
+
}).filter((depName) => depName.length > 0).sort((a, b) => a.localeCompare(b));
|
|
32
|
+
normalized.push({
|
|
33
|
+
key: `${name}@${selected.version}`,
|
|
34
|
+
name,
|
|
35
|
+
version: selected.version,
|
|
36
|
+
dependencies: deps
|
|
37
|
+
});
|
|
65
38
|
}
|
|
66
|
-
return
|
|
67
|
-
};
|
|
68
|
-
var write = (messageLevel, message) => {
|
|
69
|
-
process.stderr.write(`[codesentinel] ${messageLevel.toUpperCase()} ${message}
|
|
70
|
-
`);
|
|
39
|
+
return normalized.sort((a, b) => a.name.localeCompare(b.name));
|
|
71
40
|
};
|
|
72
|
-
var
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
|
|
41
|
+
var computeDepths = (nodeByName, directNames) => {
|
|
42
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
43
|
+
const depthByName = /* @__PURE__ */ new Map();
|
|
44
|
+
const compute = (name) => {
|
|
45
|
+
const known = depthByName.get(name);
|
|
46
|
+
if (known !== void 0) {
|
|
47
|
+
return known;
|
|
48
|
+
}
|
|
49
|
+
if (visiting.has(name)) {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
visiting.add(name);
|
|
53
|
+
const node = nodeByName.get(name);
|
|
54
|
+
if (node === void 0) {
|
|
55
|
+
visiting.delete(name);
|
|
56
|
+
depthByName.set(name, 0);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
let maxChildDepth = 0;
|
|
60
|
+
for (const dependencyName of node.dependencies) {
|
|
61
|
+
const childDepth = compute(dependencyName);
|
|
62
|
+
if (childDepth > maxChildDepth) {
|
|
63
|
+
maxChildDepth = childDepth;
|
|
95
64
|
}
|
|
96
65
|
}
|
|
66
|
+
visiting.delete(name);
|
|
67
|
+
const ownDepth = directNames.has(name) ? 0 : maxChildDepth + 1;
|
|
68
|
+
depthByName.set(name, ownDepth);
|
|
69
|
+
return ownDepth;
|
|
97
70
|
};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return value;
|
|
107
|
-
default:
|
|
108
|
-
return "info";
|
|
71
|
+
for (const name of nodeByName.keys()) {
|
|
72
|
+
compute(name);
|
|
73
|
+
}
|
|
74
|
+
let maxDepth = 0;
|
|
75
|
+
for (const depth of depthByName.values()) {
|
|
76
|
+
if (depth > maxDepth) {
|
|
77
|
+
maxDepth = depth;
|
|
78
|
+
}
|
|
109
79
|
}
|
|
80
|
+
return { depthByName, maxDepth };
|
|
110
81
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
var
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
82
|
+
var rankCentrality = (nodes, dependentsByName, directNames, topN) => [...nodes].map((node) => ({
|
|
83
|
+
name: node.name,
|
|
84
|
+
dependents: dependentsByName.get(node.name) ?? 0,
|
|
85
|
+
fanOut: node.dependencies.length,
|
|
86
|
+
direct: directNames.has(node.name)
|
|
87
|
+
})).sort(
|
|
88
|
+
(a, b) => b.dependents - a.dependents || b.fanOut - a.fanOut || a.name.localeCompare(b.name)
|
|
89
|
+
).slice(0, topN);
|
|
90
|
+
var canPropagateSignal = (signal) => signal === "abandoned" || signal === "high_centrality" || signal === "deep_chain" || signal === "high_fanout";
|
|
91
|
+
var collectTransitiveDependencies = (rootName, nodeByName) => {
|
|
92
|
+
const seen = /* @__PURE__ */ new Set();
|
|
93
|
+
const stack = [...nodeByName.get(rootName)?.dependencies ?? []];
|
|
94
|
+
while (stack.length > 0) {
|
|
95
|
+
const current = stack.pop();
|
|
96
|
+
if (current === void 0 || seen.has(current) || current === rootName) {
|
|
125
97
|
continue;
|
|
126
98
|
}
|
|
127
|
-
|
|
99
|
+
seen.add(current);
|
|
100
|
+
const currentNode = nodeByName.get(current);
|
|
101
|
+
if (currentNode === void 0) {
|
|
128
102
|
continue;
|
|
129
103
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (fromCompare !== 0) {
|
|
135
|
-
return fromCompare;
|
|
104
|
+
for (const next of currentNode.dependencies) {
|
|
105
|
+
if (!seen.has(next)) {
|
|
106
|
+
stack.push(next);
|
|
107
|
+
}
|
|
136
108
|
}
|
|
137
|
-
return a.to.localeCompare(b.to);
|
|
138
|
-
});
|
|
139
|
-
const adjacency = /* @__PURE__ */ new Map();
|
|
140
|
-
for (const node of sortedNodes) {
|
|
141
|
-
adjacency.set(node.id, []);
|
|
142
109
|
}
|
|
143
|
-
|
|
144
|
-
|
|
110
|
+
return [...seen].sort((a, b) => a.localeCompare(b));
|
|
111
|
+
};
|
|
112
|
+
var buildExternalAnalysisSummary = (targetPath, extraction, metadataByKey, config) => {
|
|
113
|
+
const nodes = normalizeNodes(extraction.nodes);
|
|
114
|
+
const directNames = new Set(extraction.directDependencies.map((dep) => dep.name));
|
|
115
|
+
const directSpecByName = new Map(extraction.directDependencies.map((dep) => [dep.name, dep]));
|
|
116
|
+
const nodeByName = new Map(nodes.map((node) => [node.name, node]));
|
|
117
|
+
const dependentsByName = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const node of nodes) {
|
|
119
|
+
dependentsByName.set(node.name, dependentsByName.get(node.name) ?? 0);
|
|
145
120
|
}
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
121
|
+
for (const node of nodes) {
|
|
122
|
+
for (const dependencyName of node.dependencies) {
|
|
123
|
+
if (!nodeByName.has(dependencyName)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
dependentsByName.set(dependencyName, (dependentsByName.get(dependencyName) ?? 0) + 1);
|
|
127
|
+
}
|
|
149
128
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const onStack = /* @__PURE__ */ new Set();
|
|
162
|
-
const components = [];
|
|
163
|
-
const strongConnect = (nodeId) => {
|
|
164
|
-
indices.set(nodeId, index);
|
|
165
|
-
lowLink.set(nodeId, index);
|
|
166
|
-
index += 1;
|
|
167
|
-
stack.push(nodeId);
|
|
168
|
-
onStack.add(nodeId);
|
|
169
|
-
const neighbors = adjacencyById.get(nodeId) ?? [];
|
|
170
|
-
for (const nextId of neighbors) {
|
|
171
|
-
if (!indices.has(nextId)) {
|
|
172
|
-
strongConnect(nextId);
|
|
173
|
-
const nodeLowLink2 = lowLink.get(nodeId);
|
|
174
|
-
const nextLowLink = lowLink.get(nextId);
|
|
175
|
-
if (nodeLowLink2 !== void 0 && nextLowLink !== void 0 && nextLowLink < nodeLowLink2) {
|
|
176
|
-
lowLink.set(nodeId, nextLowLink);
|
|
177
|
-
}
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (onStack.has(nextId)) {
|
|
181
|
-
const nodeLowLink2 = lowLink.get(nodeId);
|
|
182
|
-
const nextIndex = indices.get(nextId);
|
|
183
|
-
if (nodeLowLink2 !== void 0 && nextIndex !== void 0 && nextIndex < nodeLowLink2) {
|
|
184
|
-
lowLink.set(nodeId, nextIndex);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const nodeLowLink = lowLink.get(nodeId);
|
|
189
|
-
const nodeIndex = indices.get(nodeId);
|
|
190
|
-
if (nodeLowLink === void 0 || nodeIndex === void 0 || nodeLowLink !== nodeIndex) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
const component = [];
|
|
194
|
-
for (; ; ) {
|
|
195
|
-
const popped = stack.pop();
|
|
196
|
-
if (popped === void 0) {
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
onStack.delete(popped);
|
|
200
|
-
component.push(popped);
|
|
201
|
-
if (popped === nodeId) {
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
component.sort((a, b) => a.localeCompare(b));
|
|
206
|
-
components.push(component);
|
|
207
|
-
};
|
|
208
|
-
const nodeIds = [...adjacencyById.keys()].sort((a, b) => a.localeCompare(b));
|
|
209
|
-
for (const nodeId of nodeIds) {
|
|
210
|
-
if (!indices.has(nodeId)) {
|
|
211
|
-
strongConnect(nodeId);
|
|
129
|
+
const { depthByName, maxDepth } = computeDepths(nodeByName, directNames);
|
|
130
|
+
const centralityRanking = rankCentrality(nodes, dependentsByName, directNames, config.centralityTopN);
|
|
131
|
+
const topCentralNames = new Set(
|
|
132
|
+
centralityRanking.slice(0, Math.max(1, Math.ceil(centralityRanking.length * 0.25))).map((entry) => entry.name)
|
|
133
|
+
);
|
|
134
|
+
const allDependencies = [];
|
|
135
|
+
let metadataAvailableCount = 0;
|
|
136
|
+
for (const node of nodes) {
|
|
137
|
+
const metadata = metadataByKey.get(node.key) ?? null;
|
|
138
|
+
if (metadata !== null) {
|
|
139
|
+
metadataAvailableCount += 1;
|
|
212
140
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
});
|
|
219
|
-
return { components };
|
|
220
|
-
};
|
|
221
|
-
var hasSelfLoop = (nodeId, adjacencyById) => {
|
|
222
|
-
const targets = adjacencyById.get(nodeId) ?? [];
|
|
223
|
-
return targets.includes(nodeId);
|
|
224
|
-
};
|
|
225
|
-
var computeCyclesAndDepth = (graph) => {
|
|
226
|
-
const { components } = runTarjanScc(graph.adjacencyById);
|
|
227
|
-
const cycles = [];
|
|
228
|
-
const componentByNodeId = /* @__PURE__ */ new Map();
|
|
229
|
-
components.forEach((component, index) => {
|
|
230
|
-
for (const nodeId of component) {
|
|
231
|
-
componentByNodeId.set(nodeId, index);
|
|
141
|
+
const dependencyDepth = depthByName.get(node.name) ?? 0;
|
|
142
|
+
const dependents = dependentsByName.get(node.name) ?? 0;
|
|
143
|
+
const riskSignals = [];
|
|
144
|
+
if ((metadata?.maintainerCount ?? 0) === 1) {
|
|
145
|
+
riskSignals.push("single_maintainer");
|
|
232
146
|
}
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
return;
|
|
147
|
+
if ((metadata?.daysSinceLastRelease ?? 0) >= config.abandonedDaysThreshold) {
|
|
148
|
+
riskSignals.push("abandoned");
|
|
236
149
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
cycles.push({ nodes: [...component] });
|
|
150
|
+
if (topCentralNames.has(node.name) && dependents > 0) {
|
|
151
|
+
riskSignals.push("high_centrality");
|
|
240
152
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
244
|
-
for (let i = 0; i < components.length; i += 1) {
|
|
245
|
-
dagOutgoing.set(i, /* @__PURE__ */ new Set());
|
|
246
|
-
inDegree.set(i, 0);
|
|
247
|
-
}
|
|
248
|
-
for (const edge of graph.edges) {
|
|
249
|
-
const fromComponent = componentByNodeId.get(edge.from);
|
|
250
|
-
const toComponent = componentByNodeId.get(edge.to);
|
|
251
|
-
if (fromComponent === void 0 || toComponent === void 0 || fromComponent === toComponent) {
|
|
252
|
-
continue;
|
|
153
|
+
if (dependencyDepth >= config.deepChainThreshold) {
|
|
154
|
+
riskSignals.push("deep_chain");
|
|
253
155
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
continue;
|
|
156
|
+
if (node.dependencies.length >= config.fanOutHighThreshold) {
|
|
157
|
+
riskSignals.push("high_fanout");
|
|
257
158
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
const queue = [];
|
|
262
|
-
const depthByComponent = /* @__PURE__ */ new Map();
|
|
263
|
-
for (let i = 0; i < components.length; i += 1) {
|
|
264
|
-
if ((inDegree.get(i) ?? 0) === 0) {
|
|
265
|
-
queue.push(i);
|
|
266
|
-
depthByComponent.set(i, 0);
|
|
159
|
+
if (metadata === null) {
|
|
160
|
+
riskSignals.push("metadata_unavailable");
|
|
267
161
|
}
|
|
162
|
+
allDependencies.push({
|
|
163
|
+
name: node.name,
|
|
164
|
+
direct: directNames.has(node.name),
|
|
165
|
+
dependencyScope: directSpecByName.get(node.name)?.scope ?? "prod",
|
|
166
|
+
requestedRange: directSpecByName.get(node.name)?.requestedRange ?? null,
|
|
167
|
+
resolvedVersion: node.version,
|
|
168
|
+
transitiveDependencies: [],
|
|
169
|
+
weeklyDownloads: metadata?.weeklyDownloads ?? null,
|
|
170
|
+
dependencyDepth,
|
|
171
|
+
fanOut: node.dependencies.length,
|
|
172
|
+
dependents,
|
|
173
|
+
maintainerCount: metadata?.maintainerCount ?? null,
|
|
174
|
+
releaseFrequencyDays: metadata?.releaseFrequencyDays ?? null,
|
|
175
|
+
daysSinceLastRelease: metadata?.daysSinceLastRelease ?? null,
|
|
176
|
+
repositoryActivity30d: metadata?.repositoryActivity30d ?? null,
|
|
177
|
+
busFactor: metadata?.busFactor ?? null,
|
|
178
|
+
ownRiskSignals: [...riskSignals].sort((a, b) => a.localeCompare(b)),
|
|
179
|
+
inheritedRiskSignals: [],
|
|
180
|
+
riskSignals
|
|
181
|
+
});
|
|
268
182
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const nextDepth = depthByComponent.get(nextComponent) ?? 0;
|
|
280
|
-
if (currentDepth + 1 > nextDepth) {
|
|
281
|
-
depthByComponent.set(nextComponent, currentDepth + 1);
|
|
183
|
+
allDependencies.sort((a, b) => a.name.localeCompare(b.name));
|
|
184
|
+
const allByName = new Map(allDependencies.map((dep) => [dep.name, dep]));
|
|
185
|
+
const dependencies = allDependencies.filter((dep) => dep.direct).map((dep) => {
|
|
186
|
+
const transitiveDependencies = collectTransitiveDependencies(dep.name, nodeByName);
|
|
187
|
+
const inheritedSignals = /* @__PURE__ */ new Set();
|
|
188
|
+
const allSignals = new Set(dep.ownRiskSignals);
|
|
189
|
+
for (const transitiveName of transitiveDependencies) {
|
|
190
|
+
const transitive = allByName.get(transitiveName);
|
|
191
|
+
if (transitive === void 0) {
|
|
192
|
+
continue;
|
|
282
193
|
}
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
194
|
+
for (const signal of transitive.riskSignals) {
|
|
195
|
+
if (canPropagateSignal(signal)) {
|
|
196
|
+
inheritedSignals.add(signal);
|
|
197
|
+
allSignals.add(signal);
|
|
198
|
+
}
|
|
287
199
|
}
|
|
288
200
|
}
|
|
289
|
-
}
|
|
290
|
-
const depthByNodeId = /* @__PURE__ */ new Map();
|
|
291
|
-
let graphDepth = 0;
|
|
292
|
-
components.forEach((component, componentId) => {
|
|
293
|
-
const componentDepth = depthByComponent.get(componentId) ?? 0;
|
|
294
|
-
if (componentDepth > graphDepth) {
|
|
295
|
-
graphDepth = componentDepth;
|
|
296
|
-
}
|
|
297
|
-
for (const nodeId of component) {
|
|
298
|
-
depthByNodeId.set(nodeId, componentDepth);
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
cycles.sort((a, b) => {
|
|
302
|
-
const firstA = a.nodes[0] ?? "";
|
|
303
|
-
const firstB = b.nodes[0] ?? "";
|
|
304
|
-
return firstA.localeCompare(firstB);
|
|
305
|
-
});
|
|
306
|
-
return {
|
|
307
|
-
depthByNodeId,
|
|
308
|
-
graphDepth,
|
|
309
|
-
cycles
|
|
310
|
-
};
|
|
311
|
-
};
|
|
312
|
-
var createGraphAnalysisSummary = (targetPath, graph) => {
|
|
313
|
-
const fanInById = /* @__PURE__ */ new Map();
|
|
314
|
-
const fanOutById = /* @__PURE__ */ new Map();
|
|
315
|
-
for (const node of graph.nodes) {
|
|
316
|
-
fanInById.set(node.id, 0);
|
|
317
|
-
fanOutById.set(node.id, graph.adjacencyById.get(node.id)?.length ?? 0);
|
|
318
|
-
}
|
|
319
|
-
for (const edge of graph.edges) {
|
|
320
|
-
fanInById.set(edge.to, (fanInById.get(edge.to) ?? 0) + 1);
|
|
321
|
-
}
|
|
322
|
-
const { cycles, depthByNodeId, graphDepth } = computeCyclesAndDepth(graph);
|
|
323
|
-
let maxFanIn = 0;
|
|
324
|
-
let maxFanOut = 0;
|
|
325
|
-
const files = graph.nodes.map((node) => {
|
|
326
|
-
const fanIn = fanInById.get(node.id) ?? 0;
|
|
327
|
-
const fanOut = fanOutById.get(node.id) ?? 0;
|
|
328
|
-
if (fanIn > maxFanIn) {
|
|
329
|
-
maxFanIn = fanIn;
|
|
330
|
-
}
|
|
331
|
-
if (fanOut > maxFanOut) {
|
|
332
|
-
maxFanOut = fanOut;
|
|
333
|
-
}
|
|
334
201
|
return {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
fanOut,
|
|
340
|
-
depth: depthByNodeId.get(node.id) ?? 0
|
|
202
|
+
...dep,
|
|
203
|
+
transitiveDependencies,
|
|
204
|
+
inheritedRiskSignals: [...inheritedSignals].sort((a, b) => a.localeCompare(b)),
|
|
205
|
+
riskSignals: [...allSignals].sort((a, b) => a.localeCompare(b))
|
|
341
206
|
};
|
|
342
|
-
});
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
207
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
208
|
+
const highRiskDependencies = dependencies.filter(
|
|
209
|
+
(dep) => dep.ownRiskSignals.includes("abandoned") || dep.ownRiskSignals.filter(
|
|
210
|
+
(signal) => signal === "high_centrality" || signal === "deep_chain" || signal === "high_fanout"
|
|
211
|
+
).length >= 2 || dep.ownRiskSignals.includes("single_maintainer") && ((dep.daysSinceLastRelease ?? 0) >= config.abandonedDaysThreshold / 2 || (dep.repositoryActivity30d ?? 1) <= 0)
|
|
212
|
+
).filter((dep) => dep.dependencyScope === "prod").sort(
|
|
213
|
+
(a, b) => b.ownRiskSignals.length - a.ownRiskSignals.length || a.name.localeCompare(b.name)
|
|
214
|
+
).slice(0, config.maxHighRiskDependencies).map((dep) => dep.name);
|
|
215
|
+
const highRiskDevelopmentDependencies = dependencies.filter(
|
|
216
|
+
(dep) => dep.dependencyScope === "dev" && (dep.ownRiskSignals.includes("abandoned") || dep.ownRiskSignals.filter(
|
|
217
|
+
(signal) => signal === "high_centrality" || signal === "deep_chain" || signal === "high_fanout"
|
|
218
|
+
).length >= 2 || dep.ownRiskSignals.includes("single_maintainer") && ((dep.daysSinceLastRelease ?? 0) >= config.abandonedDaysThreshold / 2 || (dep.repositoryActivity30d ?? 1) <= 0))
|
|
219
|
+
).sort(
|
|
220
|
+
(a, b) => b.ownRiskSignals.length - a.ownRiskSignals.length || a.name.localeCompare(b.name)
|
|
221
|
+
).slice(0, config.maxHighRiskDependencies).map((dep) => dep.name);
|
|
222
|
+
const transitiveExposureDependencies = dependencies.filter((dep) => dep.inheritedRiskSignals.length > 0).sort(
|
|
223
|
+
(a, b) => b.inheritedRiskSignals.length - a.inheritedRiskSignals.length || a.name.localeCompare(b.name)
|
|
224
|
+
).map((dep) => dep.name);
|
|
225
|
+
const singleMaintainerDependencies = dependencies.filter((dep) => dep.ownRiskSignals.includes("single_maintainer")).map((dep) => dep.name).sort((a, b) => a.localeCompare(b));
|
|
226
|
+
const abandonedDependencies = dependencies.filter((dep) => dep.ownRiskSignals.includes("abandoned")).map((dep) => dep.name).sort((a, b) => a.localeCompare(b));
|
|
351
227
|
return {
|
|
352
228
|
targetPath,
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
229
|
+
available: true,
|
|
230
|
+
metrics: {
|
|
231
|
+
totalDependencies: allDependencies.length,
|
|
232
|
+
directDependencies: dependencies.length,
|
|
233
|
+
directProductionDependencies: dependencies.filter((dependency) => dependency.dependencyScope === "prod").length,
|
|
234
|
+
directDevelopmentDependencies: dependencies.filter((dependency) => dependency.dependencyScope === "dev").length,
|
|
235
|
+
transitiveDependencies: allDependencies.length - dependencies.length,
|
|
236
|
+
dependencyDepth: maxDepth,
|
|
237
|
+
lockfileKind: extraction.kind,
|
|
238
|
+
metadataCoverage: allDependencies.length === 0 ? 0 : round4(metadataAvailableCount / allDependencies.length)
|
|
239
|
+
},
|
|
240
|
+
dependencies,
|
|
241
|
+
highRiskDependencies,
|
|
242
|
+
highRiskDevelopmentDependencies,
|
|
243
|
+
transitiveExposureDependencies,
|
|
244
|
+
singleMaintainerDependencies,
|
|
245
|
+
abandonedDependencies,
|
|
246
|
+
centralityRanking
|
|
358
247
|
};
|
|
359
248
|
};
|
|
360
|
-
var
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
"
|
|
370
|
-
"
|
|
249
|
+
var DEFAULT_EXTERNAL_ANALYSIS_CONFIG = {
|
|
250
|
+
abandonedDaysThreshold: 540,
|
|
251
|
+
deepChainThreshold: 6,
|
|
252
|
+
fanOutHighThreshold: 25,
|
|
253
|
+
centralityTopN: 20,
|
|
254
|
+
maxHighRiskDependencies: 100,
|
|
255
|
+
metadataRequestConcurrency: 8
|
|
256
|
+
};
|
|
257
|
+
var LOCKFILE_CANDIDATES = [
|
|
258
|
+
{ fileName: "pnpm-lock.yaml", kind: "pnpm" },
|
|
259
|
+
{ fileName: "package-lock.json", kind: "npm" },
|
|
260
|
+
{ fileName: "npm-shrinkwrap.json", kind: "npm-shrinkwrap" },
|
|
261
|
+
{ fileName: "yarn.lock", kind: "yarn" },
|
|
262
|
+
{ fileName: "bun.lock", kind: "bun" },
|
|
263
|
+
{ fileName: "bun.lockb", kind: "bun" }
|
|
371
264
|
];
|
|
372
|
-
var
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
"dist",
|
|
377
|
-
"build",
|
|
378
|
-
".next",
|
|
379
|
-
"coverage",
|
|
380
|
-
".turbo",
|
|
381
|
-
".cache",
|
|
382
|
-
"out"
|
|
383
|
-
]);
|
|
384
|
-
var normalizePath = (pathValue) => pathValue.replaceAll("\\", "/");
|
|
385
|
-
var isProjectSourceFile = (filePath, projectRoot) => {
|
|
386
|
-
const extension = extname(filePath);
|
|
387
|
-
if (!SOURCE_EXTENSIONS.has(extension)) {
|
|
388
|
-
return false;
|
|
389
|
-
}
|
|
390
|
-
const relativePath = relative(projectRoot, filePath);
|
|
391
|
-
if (relativePath.startsWith("..")) {
|
|
392
|
-
return false;
|
|
265
|
+
var loadPackageJson = (repositoryPath) => {
|
|
266
|
+
const packageJsonPath2 = join(repositoryPath, "package.json");
|
|
267
|
+
if (!existsSync(packageJsonPath2)) {
|
|
268
|
+
return null;
|
|
393
269
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
};
|
|
398
|
-
var discoverSourceFilesByScan = (projectRoot) => {
|
|
399
|
-
const files = ts.sys.readDirectory(
|
|
400
|
-
projectRoot,
|
|
401
|
-
[...SOURCE_EXTENSIONS],
|
|
402
|
-
SCAN_EXCLUDES,
|
|
403
|
-
SCAN_INCLUDES
|
|
404
|
-
);
|
|
405
|
-
return files.map((filePath) => resolve(filePath));
|
|
270
|
+
return {
|
|
271
|
+
path: packageJsonPath2,
|
|
272
|
+
raw: readFileSync(packageJsonPath2, "utf8")
|
|
273
|
+
};
|
|
406
274
|
};
|
|
407
|
-
var
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
{
|
|
411
|
-
|
|
412
|
-
...ts.sys,
|
|
413
|
-
onUnRecoverableConfigFileDiagnostic: () => {
|
|
414
|
-
throw new Error(`Failed to parse TypeScript configuration at ${configPath}`);
|
|
415
|
-
}
|
|
275
|
+
var selectLockfile = (repositoryPath) => {
|
|
276
|
+
for (const candidate of LOCKFILE_CANDIDATES) {
|
|
277
|
+
const absolutePath = join(repositoryPath, candidate.fileName);
|
|
278
|
+
if (!existsSync(absolutePath)) {
|
|
279
|
+
continue;
|
|
416
280
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
281
|
+
return {
|
|
282
|
+
path: absolutePath,
|
|
283
|
+
kind: candidate.kind,
|
|
284
|
+
raw: readFileSync(absolutePath, "utf8")
|
|
285
|
+
};
|
|
420
286
|
}
|
|
421
|
-
return
|
|
287
|
+
return null;
|
|
422
288
|
};
|
|
423
|
-
var
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const visitedConfigPaths = /* @__PURE__ */ new Set();
|
|
429
|
-
const collectedFiles = /* @__PURE__ */ new Set();
|
|
430
|
-
let rootOptions = null;
|
|
431
|
-
const visitConfig = (configPath) => {
|
|
432
|
-
const absoluteConfigPath = resolve(configPath);
|
|
433
|
-
if (visitedConfigPaths.has(absoluteConfigPath)) {
|
|
289
|
+
var parsePackageJson = (raw) => {
|
|
290
|
+
const parsed = JSON.parse(raw);
|
|
291
|
+
const merged = /* @__PURE__ */ new Map();
|
|
292
|
+
const addBlock = (block, scope) => {
|
|
293
|
+
if (block === void 0) {
|
|
434
294
|
return;
|
|
435
295
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
for (const filePath of parsed.fileNames) {
|
|
442
|
-
collectedFiles.add(resolve(filePath));
|
|
443
|
-
}
|
|
444
|
-
for (const reference of parsed.projectReferences ?? []) {
|
|
445
|
-
const referencePath = resolve(reference.path);
|
|
446
|
-
const referenceConfigPath = ts.sys.directoryExists(referencePath) ? ts.findConfigFile(referencePath, ts.sys.fileExists, "tsconfig.json") : referencePath;
|
|
447
|
-
if (referenceConfigPath !== void 0 && ts.sys.fileExists(referenceConfigPath)) {
|
|
448
|
-
visitConfig(referenceConfigPath);
|
|
296
|
+
for (const [name, versionRange] of Object.entries(block)) {
|
|
297
|
+
const existing = merged.get(name);
|
|
298
|
+
if (existing?.scope === "prod" && scope === "dev") {
|
|
299
|
+
continue;
|
|
449
300
|
}
|
|
301
|
+
merged.set(name, { name, requestedRange: versionRange, scope });
|
|
450
302
|
}
|
|
451
303
|
};
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
},
|
|
458
|
-
visitedConfigCount: visitedConfigPaths.size
|
|
459
|
-
};
|
|
304
|
+
addBlock(parsed.dependencies, "prod");
|
|
305
|
+
addBlock(parsed.optionalDependencies, "prod");
|
|
306
|
+
addBlock(parsed.peerDependencies, "prod");
|
|
307
|
+
addBlock(parsed.devDependencies, "dev");
|
|
308
|
+
return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
460
309
|
};
|
|
461
|
-
var
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
310
|
+
var parsePackageLock = (raw, directSpecs) => {
|
|
311
|
+
const parsed = JSON.parse(raw);
|
|
312
|
+
const nodes = [];
|
|
313
|
+
if (parsed.packages !== void 0) {
|
|
314
|
+
for (const [packagePath, packageData] of Object.entries(parsed.packages)) {
|
|
315
|
+
if (packagePath.length === 0 || packageData.version === void 0) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const segments = packagePath.split("node_modules/");
|
|
319
|
+
const name = segments[segments.length - 1] ?? "";
|
|
320
|
+
if (name.length === 0) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const dependencies = Object.entries(packageData.dependencies ?? {}).map(([depName, depRange]) => `${depName}@${String(depRange)}`).sort((a, b) => a.localeCompare(b));
|
|
324
|
+
nodes.push({
|
|
325
|
+
name,
|
|
326
|
+
version: packageData.version,
|
|
327
|
+
dependencies
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
} else if (parsed.dependencies !== void 0) {
|
|
331
|
+
for (const [name, dep] of Object.entries(parsed.dependencies)) {
|
|
332
|
+
if (dep.version === void 0) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const dependencies = Object.entries(dep.dependencies ?? {}).map(([depName, depVersion]) => `${depName}@${String(depVersion)}`).sort((a, b) => a.localeCompare(b));
|
|
336
|
+
nodes.push({
|
|
337
|
+
name,
|
|
338
|
+
version: dep.version,
|
|
339
|
+
dependencies
|
|
340
|
+
});
|
|
341
|
+
}
|
|
483
342
|
}
|
|
343
|
+
nodes.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
484
344
|
return {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
usedFallbackScan: false
|
|
345
|
+
kind: "npm",
|
|
346
|
+
directDependencies: directSpecs,
|
|
347
|
+
nodes
|
|
489
348
|
};
|
|
490
349
|
};
|
|
491
|
-
var
|
|
492
|
-
|
|
493
|
-
|
|
350
|
+
var sanitizeValue = (value) => value.replace(/^['"]|['"]$/g, "").trim();
|
|
351
|
+
var parsePackageKey = (rawKey) => {
|
|
352
|
+
const key = sanitizeValue(rawKey.replace(/:$/, ""));
|
|
353
|
+
const withoutSlash = key.startsWith("/") ? key.slice(1) : key;
|
|
354
|
+
const lastAt = withoutSlash.lastIndexOf("@");
|
|
355
|
+
if (lastAt <= 0) {
|
|
356
|
+
return null;
|
|
494
357
|
}
|
|
495
|
-
|
|
496
|
-
|
|
358
|
+
const name = withoutSlash.slice(0, lastAt);
|
|
359
|
+
const versionWithPeers = withoutSlash.slice(lastAt + 1);
|
|
360
|
+
const version2 = versionWithPeers.split("(")[0] ?? versionWithPeers;
|
|
361
|
+
if (name.length === 0 || version2.length === 0) {
|
|
362
|
+
return null;
|
|
497
363
|
}
|
|
498
|
-
return
|
|
364
|
+
return { name, version: version2 };
|
|
499
365
|
};
|
|
500
|
-
var
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
return true;
|
|
510
|
-
}
|
|
511
|
-
const namedBindings = importClause.namedBindings;
|
|
512
|
-
if (namedBindings === void 0) {
|
|
513
|
-
return false;
|
|
514
|
-
}
|
|
515
|
-
if (ts.isNamespaceImport(namedBindings)) {
|
|
516
|
-
return true;
|
|
517
|
-
}
|
|
518
|
-
if (namedBindings.elements.length === 0) {
|
|
519
|
-
return true;
|
|
520
|
-
}
|
|
521
|
-
return namedBindings.elements.some((element) => !element.isTypeOnly);
|
|
522
|
-
};
|
|
523
|
-
var extractModuleSpecifiers = (sourceFile) => {
|
|
524
|
-
const specifiers = /* @__PURE__ */ new Set();
|
|
525
|
-
const visit = (node) => {
|
|
526
|
-
if (ts.isImportDeclaration(node)) {
|
|
527
|
-
if (hasRuntimeImport(node) && node.moduleSpecifier !== void 0) {
|
|
528
|
-
const specifier = getSpecifierFromExpression(node.moduleSpecifier);
|
|
529
|
-
if (specifier !== void 0) {
|
|
530
|
-
specifiers.add(specifier);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
return;
|
|
366
|
+
var parsePnpmLockfile = (raw, directSpecs) => {
|
|
367
|
+
const lines = raw.split("\n");
|
|
368
|
+
let state = "root";
|
|
369
|
+
let currentPackage = null;
|
|
370
|
+
let currentDependencyName = null;
|
|
371
|
+
const dependenciesByNode = /* @__PURE__ */ new Map();
|
|
372
|
+
for (const line of lines) {
|
|
373
|
+
if (line.trim().length === 0 || line.trimStart().startsWith("#")) {
|
|
374
|
+
continue;
|
|
534
375
|
}
|
|
535
|
-
if (
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
376
|
+
if (line.startsWith("importers:")) {
|
|
377
|
+
state = "importers";
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (line.startsWith("packages:")) {
|
|
381
|
+
state = "packages";
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (state === "packages" || state === "packageDeps") {
|
|
385
|
+
const packageMatch = line.match(/^\s{2}([^\s].+):\s*$/);
|
|
386
|
+
if (packageMatch !== null) {
|
|
387
|
+
const parsedKey = parsePackageKey(packageMatch[1] ?? "");
|
|
388
|
+
if (parsedKey !== null) {
|
|
389
|
+
currentPackage = `${parsedKey.name}@${parsedKey.version}`;
|
|
390
|
+
dependenciesByNode.set(currentPackage, /* @__PURE__ */ new Set());
|
|
391
|
+
state = "packageDeps";
|
|
392
|
+
currentDependencyName = null;
|
|
540
393
|
}
|
|
394
|
+
continue;
|
|
541
395
|
}
|
|
542
|
-
return;
|
|
543
396
|
}
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
397
|
+
if (state === "packageDeps" && currentPackage !== null) {
|
|
398
|
+
const depLine = line.match(/^\s{6}([^:\s]+):\s*(.+)$/);
|
|
399
|
+
if (depLine !== null) {
|
|
400
|
+
const depName = sanitizeValue(depLine[1] ?? "");
|
|
401
|
+
const depRef = sanitizeValue(depLine[2] ?? "");
|
|
402
|
+
const depVersion = depRef.split("(")[0] ?? depRef;
|
|
403
|
+
if (depName.length > 0 && depVersion.length > 0) {
|
|
404
|
+
dependenciesByNode.get(currentPackage)?.add(`${depName}@${depVersion}`);
|
|
552
405
|
}
|
|
406
|
+
currentDependencyName = null;
|
|
407
|
+
continue;
|
|
553
408
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
409
|
+
const depBlockLine = line.match(/^\s{6}([^:\s]+):\s*$/);
|
|
410
|
+
if (depBlockLine !== null) {
|
|
411
|
+
currentDependencyName = sanitizeValue(depBlockLine[1] ?? "");
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const depVersionLine = line.match(/^\s{8}version:\s*(.+)$/);
|
|
415
|
+
if (depVersionLine !== null && currentDependencyName !== null) {
|
|
416
|
+
const depRef = sanitizeValue(depVersionLine[1] ?? "");
|
|
417
|
+
const depVersion = depRef.split("(")[0] ?? depRef;
|
|
418
|
+
if (depVersion.length > 0) {
|
|
419
|
+
dependenciesByNode.get(currentPackage)?.add(`${currentDependencyName}@${depVersion}`);
|
|
561
420
|
}
|
|
421
|
+
currentDependencyName = null;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (line.match(/^\s{4}(dependencies|optionalDependencies):\s*$/) !== null) {
|
|
425
|
+
continue;
|
|
562
426
|
}
|
|
563
427
|
}
|
|
564
|
-
|
|
428
|
+
}
|
|
429
|
+
const nodes = [...dependenciesByNode.entries()].map(([nodeId, deps]) => {
|
|
430
|
+
const at = nodeId.lastIndexOf("@");
|
|
431
|
+
return {
|
|
432
|
+
name: nodeId.slice(0, at),
|
|
433
|
+
version: nodeId.slice(at + 1),
|
|
434
|
+
dependencies: [...deps].sort((a, b) => a.localeCompare(b))
|
|
435
|
+
};
|
|
436
|
+
}).sort(
|
|
437
|
+
(a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version)
|
|
438
|
+
);
|
|
439
|
+
return {
|
|
440
|
+
kind: "pnpm",
|
|
441
|
+
directDependencies: directSpecs,
|
|
442
|
+
nodes
|
|
565
443
|
};
|
|
566
|
-
visit(sourceFile);
|
|
567
|
-
return [...specifiers];
|
|
568
444
|
};
|
|
569
|
-
var
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const uniqueSourceFilePaths = [...new Set(sourceFilePaths)].sort((a, b) => a.localeCompare(b));
|
|
575
|
-
const sourceFilePathSet = new Set(uniqueSourceFilePaths);
|
|
576
|
-
onProgress?.({ stage: "files_discovered", totalSourceFiles: uniqueSourceFilePaths.length });
|
|
577
|
-
const program2 = ts.createProgram({
|
|
578
|
-
rootNames: uniqueSourceFilePaths,
|
|
579
|
-
options
|
|
580
|
-
});
|
|
581
|
-
onProgress?.({ stage: "program_created", totalSourceFiles: uniqueSourceFilePaths.length });
|
|
582
|
-
const nodeByAbsolutePath = /* @__PURE__ */ new Map();
|
|
583
|
-
for (const sourcePath of uniqueSourceFilePaths) {
|
|
584
|
-
const relativePath = normalizePath(relative(projectRoot, sourcePath));
|
|
585
|
-
const nodeId = relativePath;
|
|
586
|
-
nodeByAbsolutePath.set(sourcePath, {
|
|
587
|
-
id: nodeId,
|
|
588
|
-
absolutePath: sourcePath,
|
|
589
|
-
relativePath
|
|
590
|
-
});
|
|
445
|
+
var stripQuotes = (value) => value.replace(/^['"]|['"]$/g, "");
|
|
446
|
+
var parseVersionSelector = (selector) => {
|
|
447
|
+
const npmIndex = selector.lastIndexOf("@npm:");
|
|
448
|
+
if (npmIndex >= 0) {
|
|
449
|
+
return selector.slice(npmIndex + 5);
|
|
591
450
|
}
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
451
|
+
const lastAt = selector.lastIndexOf("@");
|
|
452
|
+
if (lastAt <= 0) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
return selector.slice(lastAt + 1);
|
|
456
|
+
};
|
|
457
|
+
var parseYarnLock = (raw, directSpecs) => {
|
|
458
|
+
const lines = raw.split("\n");
|
|
459
|
+
const nodes = [];
|
|
460
|
+
let selectors = [];
|
|
461
|
+
let version2 = null;
|
|
462
|
+
let readingDependencies = false;
|
|
463
|
+
let dependencies = [];
|
|
464
|
+
const flushEntry = () => {
|
|
465
|
+
if (selectors.length === 0 || version2 === null) {
|
|
466
|
+
selectors = [];
|
|
467
|
+
version2 = null;
|
|
468
|
+
dependencies = [];
|
|
469
|
+
readingDependencies = false;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
for (const selector of selectors) {
|
|
473
|
+
const parsedVersion = parseVersionSelector(selector);
|
|
474
|
+
const at = selector.lastIndexOf("@");
|
|
475
|
+
const name = at <= 0 ? selector : selector.slice(0, at);
|
|
476
|
+
if (name.length === 0) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
nodes.push({
|
|
480
|
+
name,
|
|
481
|
+
version: version2,
|
|
482
|
+
dependencies: [...dependencies].sort((a, b) => a.localeCompare(b))
|
|
483
|
+
});
|
|
484
|
+
if (parsedVersion !== null) {
|
|
485
|
+
nodes.push({
|
|
486
|
+
name,
|
|
487
|
+
version: parsedVersion,
|
|
488
|
+
dependencies: [...dependencies].sort((a, b) => a.localeCompare(b))
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
selectors = [];
|
|
493
|
+
version2 = null;
|
|
494
|
+
dependencies = [];
|
|
495
|
+
readingDependencies = false;
|
|
496
|
+
};
|
|
497
|
+
for (const line of lines) {
|
|
498
|
+
if (line.trim().length === 0) {
|
|
597
499
|
continue;
|
|
598
500
|
}
|
|
599
|
-
|
|
600
|
-
|
|
501
|
+
if (!line.startsWith(" ") && line.endsWith(":")) {
|
|
502
|
+
flushEntry();
|
|
503
|
+
const keyText = line.slice(0, -1);
|
|
504
|
+
selectors = keyText.split(",").map((part) => stripQuotes(part.trim())).filter((part) => part.length > 0);
|
|
601
505
|
continue;
|
|
602
506
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const toNode = nodeByAbsolutePath.get(resolvedPath);
|
|
618
|
-
if (toNode === void 0) {
|
|
507
|
+
if (line.match(/^\s{2}version\s+/) !== null) {
|
|
508
|
+
const value = line.replace(/^\s{2}version\s+/, "").trim();
|
|
509
|
+
version2 = stripQuotes(value);
|
|
510
|
+
readingDependencies = false;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (line.match(/^\s{2}dependencies:\s*$/) !== null) {
|
|
514
|
+
readingDependencies = true;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (readingDependencies && line.match(/^\s{4}[^\s].+$/) !== null) {
|
|
518
|
+
const depLine = line.trim();
|
|
519
|
+
const firstSpace = depLine.indexOf(" ");
|
|
520
|
+
if (firstSpace <= 0) {
|
|
619
521
|
continue;
|
|
620
522
|
}
|
|
621
|
-
|
|
523
|
+
const depName = stripQuotes(depLine.slice(0, firstSpace));
|
|
524
|
+
const depRef = stripQuotes(depLine.slice(firstSpace + 1).trim());
|
|
525
|
+
const depVersion = parseVersionSelector(depRef) ?? depRef;
|
|
526
|
+
dependencies.push(`${depName}@${depVersion}`);
|
|
527
|
+
continue;
|
|
622
528
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
529
|
+
readingDependencies = false;
|
|
530
|
+
}
|
|
531
|
+
flushEntry();
|
|
532
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
533
|
+
for (const node of nodes) {
|
|
534
|
+
const key = `${node.name}@${node.version}`;
|
|
535
|
+
if (!deduped.has(key)) {
|
|
536
|
+
deduped.set(key, node);
|
|
631
537
|
}
|
|
632
538
|
}
|
|
633
|
-
onProgress?.({ stage: "edges_resolved", totalEdges: edges.length });
|
|
634
539
|
return {
|
|
635
|
-
|
|
636
|
-
|
|
540
|
+
kind: "yarn",
|
|
541
|
+
directDependencies: directSpecs,
|
|
542
|
+
nodes: [...deduped.values()].sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version))
|
|
637
543
|
};
|
|
638
544
|
};
|
|
639
|
-
var
|
|
640
|
-
|
|
641
|
-
const graphData = createGraphData(parsedProject.nodes, parsedProject.edges);
|
|
642
|
-
return createGraphAnalysisSummary(input.projectPath, graphData);
|
|
545
|
+
var parseBunLock = (_raw, _directSpecs) => {
|
|
546
|
+
throw new Error("unsupported_lockfile_format");
|
|
643
547
|
};
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
import { join } from "path";
|
|
648
|
-
import { setTimeout as sleep } from "timers/promises";
|
|
649
|
-
var round4 = (value) => Number(value.toFixed(4));
|
|
650
|
-
var normalizeNodes = (nodes) => {
|
|
651
|
-
const byName = /* @__PURE__ */ new Map();
|
|
652
|
-
for (const node of nodes) {
|
|
653
|
-
const bucket = byName.get(node.name) ?? [];
|
|
654
|
-
bucket.push(node);
|
|
655
|
-
byName.set(node.name, bucket);
|
|
548
|
+
var parseRetryAfterMs = (value) => {
|
|
549
|
+
if (value === null) {
|
|
550
|
+
return null;
|
|
656
551
|
}
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
552
|
+
const seconds = Number.parseInt(value, 10);
|
|
553
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
return seconds * 1e3;
|
|
557
|
+
};
|
|
558
|
+
var shouldRetryStatus = (status) => status === 429 || status >= 500;
|
|
559
|
+
var fetchJsonWithRetry = async (url, options) => {
|
|
560
|
+
for (let attempt = 0; attempt <= options.retries; attempt += 1) {
|
|
561
|
+
const response = await fetch(url);
|
|
562
|
+
if (response.ok) {
|
|
563
|
+
return await response.json();
|
|
661
564
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (selected === void 0) {
|
|
665
|
-
continue;
|
|
565
|
+
if (!shouldRetryStatus(response.status) || attempt === options.retries) {
|
|
566
|
+
return null;
|
|
666
567
|
}
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}).filter((depName) => depName.length > 0).sort((a, b) => a.localeCompare(b));
|
|
671
|
-
normalized.push({
|
|
672
|
-
key: `${name}@${selected.version}`,
|
|
673
|
-
name,
|
|
674
|
-
version: selected.version,
|
|
675
|
-
dependencies: deps
|
|
676
|
-
});
|
|
568
|
+
const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
569
|
+
const backoffMs = retryAfterMs ?? options.baseDelayMs * 2 ** attempt;
|
|
570
|
+
await sleep(backoffMs);
|
|
677
571
|
}
|
|
678
|
-
return
|
|
572
|
+
return null;
|
|
679
573
|
};
|
|
680
|
-
var
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
574
|
+
var MAX_RETRIES = 3;
|
|
575
|
+
var RETRY_BASE_DELAY_MS = 500;
|
|
576
|
+
var parsePrerelease = (value) => {
|
|
577
|
+
if (value === void 0 || value.length === 0) {
|
|
578
|
+
return [];
|
|
579
|
+
}
|
|
580
|
+
return value.split(".").map((part) => {
|
|
581
|
+
const asNumber = Number.parseInt(part, 10);
|
|
582
|
+
if (!Number.isNaN(asNumber) && `${asNumber}` === part) {
|
|
583
|
+
return asNumber;
|
|
687
584
|
}
|
|
688
|
-
|
|
585
|
+
return part;
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
var parseSemver = (value) => {
|
|
589
|
+
const trimmed = value.trim();
|
|
590
|
+
const semverMatch = trimmed.match(
|
|
591
|
+
/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
|
|
592
|
+
);
|
|
593
|
+
if (semverMatch === null) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
const major = Number.parseInt(semverMatch[1] ?? "", 10);
|
|
597
|
+
const minor = Number.parseInt(semverMatch[2] ?? "", 10);
|
|
598
|
+
const patch = Number.parseInt(semverMatch[3] ?? "", 10);
|
|
599
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor) || !Number.isFinite(patch)) {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
major,
|
|
604
|
+
minor,
|
|
605
|
+
patch,
|
|
606
|
+
prerelease: parsePrerelease(semverMatch[4])
|
|
607
|
+
};
|
|
608
|
+
};
|
|
609
|
+
var compareIdentifier = (left, right) => {
|
|
610
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
611
|
+
return left - right;
|
|
612
|
+
}
|
|
613
|
+
if (typeof left === "number") {
|
|
614
|
+
return -1;
|
|
615
|
+
}
|
|
616
|
+
if (typeof right === "number") {
|
|
617
|
+
return 1;
|
|
618
|
+
}
|
|
619
|
+
return left.localeCompare(right);
|
|
620
|
+
};
|
|
621
|
+
var compareSemver = (left, right) => {
|
|
622
|
+
if (left.major !== right.major) {
|
|
623
|
+
return left.major - right.major;
|
|
624
|
+
}
|
|
625
|
+
if (left.minor !== right.minor) {
|
|
626
|
+
return left.minor - right.minor;
|
|
627
|
+
}
|
|
628
|
+
if (left.patch !== right.patch) {
|
|
629
|
+
return left.patch - right.patch;
|
|
630
|
+
}
|
|
631
|
+
if (left.prerelease.length === 0 && right.prerelease.length === 0) {
|
|
632
|
+
return 0;
|
|
633
|
+
}
|
|
634
|
+
if (left.prerelease.length === 0) {
|
|
635
|
+
return 1;
|
|
636
|
+
}
|
|
637
|
+
if (right.prerelease.length === 0) {
|
|
638
|
+
return -1;
|
|
639
|
+
}
|
|
640
|
+
const maxLength = Math.max(left.prerelease.length, right.prerelease.length);
|
|
641
|
+
for (let i = 0; i < maxLength; i += 1) {
|
|
642
|
+
const leftPart = left.prerelease[i];
|
|
643
|
+
const rightPart = right.prerelease[i];
|
|
644
|
+
if (leftPart === void 0 && rightPart === void 0) {
|
|
689
645
|
return 0;
|
|
690
646
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if (node === void 0) {
|
|
694
|
-
visiting.delete(name);
|
|
695
|
-
depthByName.set(name, 0);
|
|
696
|
-
return 0;
|
|
647
|
+
if (leftPart === void 0) {
|
|
648
|
+
return -1;
|
|
697
649
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const childDepth = compute(dependencyName);
|
|
701
|
-
if (childDepth > maxChildDepth) {
|
|
702
|
-
maxChildDepth = childDepth;
|
|
703
|
-
}
|
|
650
|
+
if (rightPart === void 0) {
|
|
651
|
+
return 1;
|
|
704
652
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
return ownDepth;
|
|
709
|
-
};
|
|
710
|
-
for (const name of nodeByName.keys()) {
|
|
711
|
-
compute(name);
|
|
712
|
-
}
|
|
713
|
-
let maxDepth = 0;
|
|
714
|
-
for (const depth of depthByName.values()) {
|
|
715
|
-
if (depth > maxDepth) {
|
|
716
|
-
maxDepth = depth;
|
|
653
|
+
const diff = compareIdentifier(leftPart, rightPart);
|
|
654
|
+
if (diff !== 0) {
|
|
655
|
+
return diff;
|
|
717
656
|
}
|
|
718
657
|
}
|
|
719
|
-
return
|
|
658
|
+
return 0;
|
|
720
659
|
};
|
|
721
|
-
var
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const stack = [...nodeByName.get(rootName)?.dependencies ?? []];
|
|
733
|
-
while (stack.length > 0) {
|
|
734
|
-
const current = stack.pop();
|
|
735
|
-
if (current === void 0 || seen.has(current) || current === rootName) {
|
|
736
|
-
continue;
|
|
660
|
+
var isWildcardPart = (value) => value === void 0 || value === "*" || value.toLowerCase() === "x";
|
|
661
|
+
var matchesPartialVersion = (version2, token) => {
|
|
662
|
+
const normalized = token.trim().replace(/^v/, "");
|
|
663
|
+
if (normalized === "*" || normalized.length === 0) {
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
const [majorPart, minorPart, patchPart] = normalized.split(".");
|
|
667
|
+
if (majorPart !== void 0 && !isWildcardPart(majorPart)) {
|
|
668
|
+
const major = Number.parseInt(majorPart, 10);
|
|
669
|
+
if (!Number.isFinite(major) || major !== version2.major) {
|
|
670
|
+
return false;
|
|
737
671
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
672
|
+
}
|
|
673
|
+
if (minorPart !== void 0 && !isWildcardPart(minorPart)) {
|
|
674
|
+
const minor = Number.parseInt(minorPart, 10);
|
|
675
|
+
if (!Number.isFinite(minor) || minor !== version2.minor) {
|
|
676
|
+
return false;
|
|
742
677
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
678
|
+
}
|
|
679
|
+
if (patchPart !== void 0 && !isWildcardPart(patchPart)) {
|
|
680
|
+
const patch = Number.parseInt(patchPart, 10);
|
|
681
|
+
if (!Number.isFinite(patch) || patch !== version2.patch) {
|
|
682
|
+
return false;
|
|
747
683
|
}
|
|
748
684
|
}
|
|
749
|
-
return
|
|
685
|
+
return true;
|
|
750
686
|
};
|
|
751
|
-
var
|
|
752
|
-
const
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
687
|
+
var parseComparatorToken = (token) => {
|
|
688
|
+
const operators = [">=", "<=", ">", "<", "="];
|
|
689
|
+
for (const operator of operators) {
|
|
690
|
+
if (token.startsWith(operator)) {
|
|
691
|
+
return {
|
|
692
|
+
operator,
|
|
693
|
+
versionToken: token.slice(operator.length).trim()
|
|
694
|
+
};
|
|
695
|
+
}
|
|
759
696
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
697
|
+
return {
|
|
698
|
+
operator: "=",
|
|
699
|
+
versionToken: token.trim()
|
|
700
|
+
};
|
|
701
|
+
};
|
|
702
|
+
var satisfiesComparator = (version2, token) => {
|
|
703
|
+
if (token.length === 0 || token === "*") {
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
if (token.startsWith("^")) {
|
|
707
|
+
const base = parseSemver(token.slice(1));
|
|
708
|
+
if (base === null) {
|
|
709
|
+
return null;
|
|
766
710
|
}
|
|
711
|
+
let upper;
|
|
712
|
+
if (base.major > 0) {
|
|
713
|
+
upper = { major: base.major + 1, minor: 0, patch: 0, prerelease: [] };
|
|
714
|
+
} else if (base.minor > 0) {
|
|
715
|
+
upper = { major: 0, minor: base.minor + 1, patch: 0, prerelease: [] };
|
|
716
|
+
} else {
|
|
717
|
+
upper = { major: 0, minor: 0, patch: base.patch + 1, prerelease: [] };
|
|
718
|
+
}
|
|
719
|
+
return compareSemver(version2, base) >= 0 && compareSemver(version2, upper) < 0;
|
|
767
720
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
);
|
|
773
|
-
const allDependencies = [];
|
|
774
|
-
let metadataAvailableCount = 0;
|
|
775
|
-
for (const node of nodes) {
|
|
776
|
-
const metadata = metadataByKey.get(node.key) ?? null;
|
|
777
|
-
if (metadata !== null) {
|
|
778
|
-
metadataAvailableCount += 1;
|
|
721
|
+
if (token.startsWith("~")) {
|
|
722
|
+
const base = parseSemver(token.slice(1));
|
|
723
|
+
if (base === null) {
|
|
724
|
+
return null;
|
|
779
725
|
}
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
726
|
+
const upper = {
|
|
727
|
+
major: base.major,
|
|
728
|
+
minor: base.minor + 1,
|
|
729
|
+
patch: 0,
|
|
730
|
+
prerelease: []
|
|
731
|
+
};
|
|
732
|
+
return compareSemver(version2, base) >= 0 && compareSemver(version2, upper) < 0;
|
|
733
|
+
}
|
|
734
|
+
const parsedComparator = parseComparatorToken(token);
|
|
735
|
+
const hasWildcard = /(^|[.])(?:x|X|\*)($|[.])/.test(parsedComparator.versionToken);
|
|
736
|
+
if (hasWildcard) {
|
|
737
|
+
if (parsedComparator.operator !== "=") {
|
|
738
|
+
return null;
|
|
785
739
|
}
|
|
786
|
-
|
|
787
|
-
|
|
740
|
+
return matchesPartialVersion(version2, parsedComparator.versionToken);
|
|
741
|
+
}
|
|
742
|
+
const parsedVersion = parseSemver(parsedComparator.versionToken);
|
|
743
|
+
if (parsedVersion === null) {
|
|
744
|
+
if (parsedComparator.operator !== "=") {
|
|
745
|
+
return null;
|
|
788
746
|
}
|
|
789
|
-
|
|
790
|
-
|
|
747
|
+
return matchesPartialVersion(version2, parsedComparator.versionToken);
|
|
748
|
+
}
|
|
749
|
+
const comparison = compareSemver(version2, parsedVersion);
|
|
750
|
+
switch (parsedComparator.operator) {
|
|
751
|
+
case ">":
|
|
752
|
+
return comparison > 0;
|
|
753
|
+
case ">=":
|
|
754
|
+
return comparison >= 0;
|
|
755
|
+
case "<":
|
|
756
|
+
return comparison < 0;
|
|
757
|
+
case "<=":
|
|
758
|
+
return comparison <= 0;
|
|
759
|
+
case "=":
|
|
760
|
+
return comparison === 0;
|
|
761
|
+
default:
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
var satisfiesRangeClause = (version2, clause) => {
|
|
766
|
+
const hyphenMatch = clause.match(/^\s*(.+?)\s+-\s+(.+?)\s*$/);
|
|
767
|
+
if (hyphenMatch !== null) {
|
|
768
|
+
const lower = hyphenMatch[1];
|
|
769
|
+
const upper = hyphenMatch[2];
|
|
770
|
+
if (lower === void 0 || upper === void 0) {
|
|
771
|
+
return null;
|
|
791
772
|
}
|
|
792
|
-
|
|
793
|
-
|
|
773
|
+
const lowerResult = satisfiesComparator(version2, `>=${lower}`);
|
|
774
|
+
const upperResult = satisfiesComparator(version2, `<=${upper}`);
|
|
775
|
+
if (lowerResult === null || upperResult === null) {
|
|
776
|
+
return null;
|
|
794
777
|
}
|
|
795
|
-
|
|
796
|
-
|
|
778
|
+
return lowerResult && upperResult;
|
|
779
|
+
}
|
|
780
|
+
const tokens = clause.split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
|
|
781
|
+
if (tokens.length === 0) {
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
for (const token of tokens) {
|
|
785
|
+
const matched = satisfiesComparator(version2, token);
|
|
786
|
+
if (matched === null) {
|
|
787
|
+
return null;
|
|
797
788
|
}
|
|
798
|
-
if (
|
|
799
|
-
|
|
789
|
+
if (!matched) {
|
|
790
|
+
return false;
|
|
800
791
|
}
|
|
801
|
-
allDependencies.push({
|
|
802
|
-
name: node.name,
|
|
803
|
-
direct: directNames.has(node.name),
|
|
804
|
-
dependencyScope: directSpecByName.get(node.name)?.scope ?? "prod",
|
|
805
|
-
requestedRange: directSpecByName.get(node.name)?.requestedRange ?? null,
|
|
806
|
-
resolvedVersion: node.version,
|
|
807
|
-
transitiveDependencies: [],
|
|
808
|
-
weeklyDownloads: metadata?.weeklyDownloads ?? null,
|
|
809
|
-
dependencyDepth,
|
|
810
|
-
fanOut: node.dependencies.length,
|
|
811
|
-
dependents,
|
|
812
|
-
maintainerCount: metadata?.maintainerCount ?? null,
|
|
813
|
-
releaseFrequencyDays: metadata?.releaseFrequencyDays ?? null,
|
|
814
|
-
daysSinceLastRelease: metadata?.daysSinceLastRelease ?? null,
|
|
815
|
-
repositoryActivity30d: metadata?.repositoryActivity30d ?? null,
|
|
816
|
-
busFactor: metadata?.busFactor ?? null,
|
|
817
|
-
ownRiskSignals: [...riskSignals].sort((a, b) => a.localeCompare(b)),
|
|
818
|
-
inheritedRiskSignals: [],
|
|
819
|
-
riskSignals
|
|
820
|
-
});
|
|
821
792
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
793
|
+
return true;
|
|
794
|
+
};
|
|
795
|
+
var resolveRangeVersion = (versions, requested) => {
|
|
796
|
+
const clauses = requested.split("||").map((clause) => clause.trim()).filter((clause) => clause.length > 0);
|
|
797
|
+
if (clauses.length === 0) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
const parsedVersions = versions.map((version2) => ({ version: version2, parsed: parseSemver(version2) })).filter((candidate) => candidate.parsed !== null).sort((a, b) => compareSemver(b.parsed, a.parsed));
|
|
801
|
+
for (const candidate of parsedVersions) {
|
|
802
|
+
let clauseMatched = false;
|
|
803
|
+
let clauseUnsupported = false;
|
|
804
|
+
for (const clause of clauses) {
|
|
805
|
+
const matched = satisfiesRangeClause(candidate.parsed, clause);
|
|
806
|
+
if (matched === null) {
|
|
807
|
+
clauseUnsupported = true;
|
|
831
808
|
continue;
|
|
832
809
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
allSignals.add(signal);
|
|
837
|
-
}
|
|
810
|
+
if (matched) {
|
|
811
|
+
clauseMatched = true;
|
|
812
|
+
break;
|
|
838
813
|
}
|
|
839
814
|
}
|
|
815
|
+
if (clauseMatched) {
|
|
816
|
+
return candidate.version;
|
|
817
|
+
}
|
|
818
|
+
if (clauseUnsupported && clauses.length === 1) {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return null;
|
|
823
|
+
};
|
|
824
|
+
var fetchPackument = async (name) => {
|
|
825
|
+
const encodedName = encodeURIComponent(name);
|
|
826
|
+
try {
|
|
827
|
+
return await fetchJsonWithRetry(`https://registry.npmjs.org/${encodedName}`, {
|
|
828
|
+
retries: MAX_RETRIES,
|
|
829
|
+
baseDelayMs: RETRY_BASE_DELAY_MS
|
|
830
|
+
});
|
|
831
|
+
} catch {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
var resolveRequestedVersion = (packument, requested) => {
|
|
836
|
+
const versions = packument.versions ?? {};
|
|
837
|
+
const versionKeys = Object.keys(versions);
|
|
838
|
+
const tags = packument["dist-tags"] ?? {};
|
|
839
|
+
const latest = tags["latest"];
|
|
840
|
+
if (requested !== null && versions[requested] !== void 0) {
|
|
840
841
|
return {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
riskSignals: [...allSignals].sort((a, b) => a.localeCompare(b))
|
|
842
|
+
version: requested,
|
|
843
|
+
resolution: "exact",
|
|
844
|
+
fallbackUsed: false
|
|
845
845
|
};
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
(
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
846
|
+
}
|
|
847
|
+
if (requested !== null) {
|
|
848
|
+
const tagged = tags[requested];
|
|
849
|
+
if (tagged !== void 0 && versions[tagged] !== void 0) {
|
|
850
|
+
return {
|
|
851
|
+
version: tagged,
|
|
852
|
+
resolution: "tag",
|
|
853
|
+
fallbackUsed: false
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (requested !== null) {
|
|
858
|
+
const matched = resolveRangeVersion(versionKeys, requested);
|
|
859
|
+
if (matched !== null && versions[matched] !== void 0) {
|
|
860
|
+
return {
|
|
861
|
+
version: matched,
|
|
862
|
+
resolution: "range",
|
|
863
|
+
fallbackUsed: false
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (latest !== void 0 && versions[latest] !== void 0) {
|
|
868
|
+
return {
|
|
869
|
+
version: latest,
|
|
870
|
+
resolution: "latest",
|
|
871
|
+
fallbackUsed: requested !== null
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
const semverSorted = versionKeys.map((version2) => ({ version: version2, parsed: parseSemver(version2) })).filter((candidate) => candidate.parsed !== null).sort((a, b) => compareSemver(b.parsed, a.parsed)).map((candidate) => candidate.version);
|
|
875
|
+
const fallbackVersion = semverSorted[0] ?? versionKeys.sort((a, b) => b.localeCompare(a))[0];
|
|
876
|
+
if (fallbackVersion === void 0) {
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
version: fallbackVersion,
|
|
881
|
+
resolution: "latest",
|
|
882
|
+
fallbackUsed: requested !== null
|
|
883
|
+
};
|
|
884
|
+
};
|
|
885
|
+
var resolveRegistryGraphFromDirectSpecs = async (directSpecs, options) => {
|
|
886
|
+
const maxNodes = Math.max(1, options.maxNodes);
|
|
887
|
+
const maxDepth = Math.max(0, options.maxDepth);
|
|
888
|
+
const queue = directSpecs.map((spec) => ({
|
|
889
|
+
name: spec.name,
|
|
890
|
+
requested: spec.requestedRange,
|
|
891
|
+
depth: 0
|
|
892
|
+
}));
|
|
893
|
+
const scopeByName = new Map(directSpecs.map((spec) => [spec.name, spec.scope]));
|
|
894
|
+
const requestedByName = new Map(directSpecs.map((spec) => [spec.name, spec.requestedRange]));
|
|
895
|
+
const packumentByName = /* @__PURE__ */ new Map();
|
|
896
|
+
const nodesByKey = /* @__PURE__ */ new Map();
|
|
897
|
+
const directByName = /* @__PURE__ */ new Map();
|
|
898
|
+
const assumptions = /* @__PURE__ */ new Set();
|
|
899
|
+
let truncated = false;
|
|
900
|
+
while (queue.length > 0) {
|
|
901
|
+
if (nodesByKey.size >= maxNodes) {
|
|
902
|
+
truncated = true;
|
|
903
|
+
assumptions.add(`Dependency graph truncated at ${maxNodes} nodes.`);
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
const item = queue.shift();
|
|
907
|
+
if (item === void 0) {
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
let packument = packumentByName.get(item.name) ?? null;
|
|
911
|
+
if (!packumentByName.has(item.name)) {
|
|
912
|
+
packument = await fetchPackument(item.name);
|
|
913
|
+
packumentByName.set(item.name, packument);
|
|
914
|
+
}
|
|
915
|
+
if (packument === null) {
|
|
916
|
+
if (scopeByName.has(item.name)) {
|
|
917
|
+
assumptions.add(`Could not resolve direct dependency from registry: ${item.name}.`);
|
|
918
|
+
}
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
const resolved = resolveRequestedVersion(packument, item.requested);
|
|
922
|
+
if (resolved === null) {
|
|
923
|
+
if (scopeByName.has(item.name)) {
|
|
924
|
+
assumptions.add(`Could not resolve direct dependency version: ${item.name}.`);
|
|
925
|
+
}
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
if (scopeByName.has(item.name) && !directByName.has(item.name)) {
|
|
929
|
+
directByName.set(item.name, {
|
|
930
|
+
name: item.name,
|
|
931
|
+
requestedRange: requestedByName.get(item.name) ?? "latest",
|
|
932
|
+
resolvedVersion: resolved.version,
|
|
933
|
+
resolution: resolved.resolution,
|
|
934
|
+
scope: scopeByName.get(item.name) ?? "prod"
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
if (resolved.fallbackUsed && item.requested !== null) {
|
|
938
|
+
assumptions.add(
|
|
939
|
+
`Resolved ${item.name}@${item.requested} to latest (${resolved.version}) because exact/tag/range match was unavailable.`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
const nodeKey = `${item.name}@${resolved.version}`;
|
|
943
|
+
if (nodesByKey.has(nodeKey)) {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const manifest = (packument.versions ?? {})[resolved.version] ?? {};
|
|
947
|
+
const dependencies = Object.entries(manifest.dependencies ?? {}).filter(([dependencyName, dependencyRange]) => dependencyName.length > 0 && dependencyRange.length > 0).sort((a, b) => a[0].localeCompare(b[0]));
|
|
948
|
+
nodesByKey.set(nodeKey, {
|
|
949
|
+
name: item.name,
|
|
950
|
+
version: resolved.version,
|
|
951
|
+
dependencies: dependencies.map(([dependencyName, dependencyRange]) => `${dependencyName}@${dependencyRange}`)
|
|
952
|
+
});
|
|
953
|
+
if (item.depth >= maxDepth && dependencies.length > 0) {
|
|
954
|
+
truncated = true;
|
|
955
|
+
assumptions.add(`Dependency graph truncated at depth ${maxDepth}.`);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
for (const [dependencyName, dependencyRange] of dependencies) {
|
|
959
|
+
if (nodesByKey.size + queue.length >= maxNodes) {
|
|
960
|
+
truncated = true;
|
|
961
|
+
assumptions.add(`Dependency graph truncated at ${maxNodes} nodes.`);
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
queue.push({
|
|
965
|
+
name: dependencyName,
|
|
966
|
+
requested: dependencyRange,
|
|
967
|
+
depth: item.depth + 1
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return {
|
|
972
|
+
nodes: [...nodesByKey.values()].sort(
|
|
973
|
+
(a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version)
|
|
974
|
+
),
|
|
975
|
+
directDependencies: [...directByName.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
976
|
+
assumptions: [...assumptions].sort((a, b) => a.localeCompare(b)),
|
|
977
|
+
truncated
|
|
978
|
+
};
|
|
979
|
+
};
|
|
980
|
+
var withDefaults = (overrides) => ({
|
|
981
|
+
...DEFAULT_EXTERNAL_ANALYSIS_CONFIG,
|
|
982
|
+
...overrides
|
|
983
|
+
});
|
|
984
|
+
var parseExtraction = (lockfileKind, lockfileRaw, directSpecs) => {
|
|
985
|
+
switch (lockfileKind) {
|
|
986
|
+
case "pnpm":
|
|
987
|
+
return parsePnpmLockfile(lockfileRaw, directSpecs);
|
|
988
|
+
case "npm":
|
|
989
|
+
case "npm-shrinkwrap":
|
|
990
|
+
return {
|
|
991
|
+
...parsePackageLock(lockfileRaw, directSpecs),
|
|
992
|
+
kind: lockfileKind
|
|
993
|
+
};
|
|
994
|
+
case "yarn":
|
|
995
|
+
return parseYarnLock(lockfileRaw, directSpecs);
|
|
996
|
+
case "bun":
|
|
997
|
+
return parseBunLock(lockfileRaw, directSpecs);
|
|
998
|
+
default:
|
|
999
|
+
throw new Error("unsupported_lockfile_format");
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
var mapWithConcurrency = async (values, limit, handler) => {
|
|
1003
|
+
const effectiveLimit = Math.max(1, limit);
|
|
1004
|
+
const workerCount = Math.min(effectiveLimit, values.length);
|
|
1005
|
+
const results = new Array(values.length);
|
|
1006
|
+
let index = 0;
|
|
1007
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
1008
|
+
while (true) {
|
|
1009
|
+
const current = index;
|
|
1010
|
+
index += 1;
|
|
1011
|
+
if (current >= values.length) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const value = values[current];
|
|
1015
|
+
if (value !== void 0) {
|
|
1016
|
+
results[current] = await handler(value);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
await Promise.all(workers);
|
|
1021
|
+
return results;
|
|
1022
|
+
};
|
|
1023
|
+
var analyzeDependencyExposure = async (input, metadataProvider, onProgress) => {
|
|
1024
|
+
const config = withDefaults(input.config);
|
|
1025
|
+
const packageJson = loadPackageJson(input.repositoryPath);
|
|
1026
|
+
if (packageJson === null) {
|
|
1027
|
+
return {
|
|
1028
|
+
targetPath: input.repositoryPath,
|
|
1029
|
+
available: false,
|
|
1030
|
+
reason: "package_json_not_found"
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
onProgress?.({ stage: "package_json_loaded" });
|
|
1034
|
+
try {
|
|
1035
|
+
const directSpecs = parsePackageJson(packageJson.raw);
|
|
1036
|
+
const lockfile = selectLockfile(input.repositoryPath);
|
|
1037
|
+
let extraction;
|
|
1038
|
+
if (lockfile === null) {
|
|
1039
|
+
const resolvedGraph = await resolveRegistryGraphFromDirectSpecs(directSpecs, {
|
|
1040
|
+
maxNodes: 500,
|
|
1041
|
+
maxDepth: 8
|
|
1042
|
+
});
|
|
1043
|
+
if (resolvedGraph.nodes.length === 0) {
|
|
1044
|
+
return {
|
|
1045
|
+
targetPath: input.repositoryPath,
|
|
1046
|
+
available: false,
|
|
1047
|
+
reason: "lockfile_not_found"
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
extraction = {
|
|
1051
|
+
kind: "npm",
|
|
1052
|
+
directDependencies: resolvedGraph.directDependencies.map((dependency) => ({
|
|
1053
|
+
name: dependency.name,
|
|
1054
|
+
requestedRange: dependency.requestedRange,
|
|
1055
|
+
scope: dependency.scope
|
|
1056
|
+
})),
|
|
1057
|
+
nodes: resolvedGraph.nodes
|
|
1058
|
+
};
|
|
1059
|
+
onProgress?.({ stage: "lockfile_selected", kind: "npm" });
|
|
1060
|
+
} else {
|
|
1061
|
+
extraction = parseExtraction(lockfile.kind, lockfile.raw, directSpecs);
|
|
1062
|
+
onProgress?.({ stage: "lockfile_selected", kind: lockfile.kind });
|
|
1063
|
+
}
|
|
1064
|
+
const directNames = new Set(extraction.directDependencies.map((dependency) => dependency.name));
|
|
1065
|
+
onProgress?.({
|
|
1066
|
+
stage: "lockfile_parsed",
|
|
1067
|
+
dependencyNodes: extraction.nodes.length,
|
|
1068
|
+
directDependencies: extraction.directDependencies.length
|
|
1069
|
+
});
|
|
1070
|
+
onProgress?.({ stage: "metadata_fetch_started", total: extraction.nodes.length });
|
|
1071
|
+
let completed = 0;
|
|
1072
|
+
const metadataEntries = await mapWithConcurrency(
|
|
1073
|
+
extraction.nodes,
|
|
1074
|
+
config.metadataRequestConcurrency,
|
|
1075
|
+
async (node) => {
|
|
1076
|
+
const result = {
|
|
1077
|
+
key: `${node.name}@${node.version}`,
|
|
1078
|
+
metadata: await metadataProvider.getMetadata(node.name, node.version, {
|
|
1079
|
+
directDependency: directNames.has(node.name)
|
|
1080
|
+
})
|
|
1081
|
+
};
|
|
1082
|
+
completed += 1;
|
|
1083
|
+
onProgress?.({
|
|
1084
|
+
stage: "metadata_fetch_progress",
|
|
1085
|
+
completed,
|
|
1086
|
+
total: extraction.nodes.length,
|
|
1087
|
+
packageName: node.name
|
|
1088
|
+
});
|
|
1089
|
+
return result;
|
|
1090
|
+
}
|
|
1091
|
+
);
|
|
1092
|
+
onProgress?.({ stage: "metadata_fetch_completed", total: extraction.nodes.length });
|
|
1093
|
+
const metadataByKey = /* @__PURE__ */ new Map();
|
|
1094
|
+
for (const entry of metadataEntries) {
|
|
1095
|
+
metadataByKey.set(entry.key, entry.metadata);
|
|
1096
|
+
}
|
|
1097
|
+
const summary = buildExternalAnalysisSummary(input.repositoryPath, extraction, metadataByKey, config);
|
|
1098
|
+
if (summary.available) {
|
|
1099
|
+
onProgress?.({
|
|
1100
|
+
stage: "summary_built",
|
|
1101
|
+
totalDependencies: summary.metrics.totalDependencies,
|
|
1102
|
+
directDependencies: summary.metrics.directDependencies
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
return summary;
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
const message = error instanceof Error ? error.message : "unknown";
|
|
1108
|
+
if (message.includes("unsupported_lockfile_format")) {
|
|
1109
|
+
return {
|
|
1110
|
+
targetPath: input.repositoryPath,
|
|
1111
|
+
available: false,
|
|
1112
|
+
reason: "unsupported_lockfile_format"
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
return {
|
|
1116
|
+
targetPath: input.repositoryPath,
|
|
1117
|
+
available: false,
|
|
1118
|
+
reason: "invalid_lockfile"
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
var DEFAULT_MAX_NODES = 250;
|
|
1123
|
+
var DEFAULT_MAX_DEPTH = 6;
|
|
1124
|
+
var withDefaults2 = (overrides) => ({
|
|
1125
|
+
...DEFAULT_EXTERNAL_ANALYSIS_CONFIG,
|
|
1126
|
+
...overrides
|
|
1127
|
+
});
|
|
1128
|
+
var parseDependencySpec = (value) => {
|
|
1129
|
+
const trimmed = value.trim();
|
|
1130
|
+
if (trimmed.length === 0 || /\s/.test(trimmed)) {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
if (trimmed.startsWith("@")) {
|
|
1134
|
+
const splitIndex2 = trimmed.lastIndexOf("@");
|
|
1135
|
+
if (splitIndex2 <= 0) {
|
|
1136
|
+
return { name: trimmed, requested: null };
|
|
1137
|
+
}
|
|
1138
|
+
const name2 = trimmed.slice(0, splitIndex2);
|
|
1139
|
+
const requested2 = trimmed.slice(splitIndex2 + 1);
|
|
1140
|
+
if (name2.length === 0 || requested2.length === 0) {
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
return { name: name2, requested: requested2 };
|
|
1144
|
+
}
|
|
1145
|
+
const splitIndex = trimmed.lastIndexOf("@");
|
|
1146
|
+
if (splitIndex <= 0) {
|
|
1147
|
+
return { name: trimmed, requested: null };
|
|
1148
|
+
}
|
|
1149
|
+
const name = trimmed.slice(0, splitIndex);
|
|
1150
|
+
const requested = trimmed.slice(splitIndex + 1);
|
|
1151
|
+
if (name.length === 0 || requested.length === 0) {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
return { name, requested };
|
|
1155
|
+
};
|
|
1156
|
+
var mapWithConcurrency2 = async (values, limit, handler) => {
|
|
1157
|
+
const effectiveLimit = Math.max(1, limit);
|
|
1158
|
+
const workerCount = Math.min(effectiveLimit, values.length);
|
|
1159
|
+
const results = new Array(values.length);
|
|
1160
|
+
let index = 0;
|
|
1161
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
1162
|
+
while (true) {
|
|
1163
|
+
const current = index;
|
|
1164
|
+
index += 1;
|
|
1165
|
+
if (current >= values.length) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const value = values[current];
|
|
1169
|
+
if (value !== void 0) {
|
|
1170
|
+
results[current] = await handler(value);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
await Promise.all(workers);
|
|
1175
|
+
return results;
|
|
1176
|
+
};
|
|
1177
|
+
var analyzeDependencyCandidate = async (input, metadataProvider) => {
|
|
1178
|
+
const parsed = parseDependencySpec(input.dependency);
|
|
1179
|
+
if (parsed === null) {
|
|
1180
|
+
return {
|
|
1181
|
+
available: false,
|
|
1182
|
+
reason: "invalid_dependency_spec",
|
|
1183
|
+
dependency: input.dependency
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
const maxNodes = Math.max(1, input.maxNodes ?? DEFAULT_MAX_NODES);
|
|
1187
|
+
const maxDepth = Math.max(0, input.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
1188
|
+
const config = withDefaults2(input.config);
|
|
1189
|
+
const graph = await resolveRegistryGraphFromDirectSpecs(
|
|
1190
|
+
[
|
|
1191
|
+
{
|
|
1192
|
+
name: parsed.name,
|
|
1193
|
+
requestedRange: parsed.requested ?? "latest",
|
|
1194
|
+
scope: "prod"
|
|
1195
|
+
}
|
|
1196
|
+
],
|
|
1197
|
+
{ maxNodes, maxDepth }
|
|
1198
|
+
);
|
|
1199
|
+
const direct = graph.directDependencies.find((dependency) => dependency.name === parsed.name);
|
|
1200
|
+
if (direct === void 0 || graph.nodes.length === 0) {
|
|
1201
|
+
return {
|
|
1202
|
+
available: false,
|
|
1203
|
+
reason: "package_not_found",
|
|
1204
|
+
dependency: input.dependency
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
const metadataEntries = await mapWithConcurrency2(
|
|
1208
|
+
graph.nodes,
|
|
1209
|
+
config.metadataRequestConcurrency,
|
|
1210
|
+
async (node) => ({
|
|
1211
|
+
key: `${node.name}@${node.version}`,
|
|
1212
|
+
metadata: await metadataProvider.getMetadata(node.name, node.version, {
|
|
1213
|
+
directDependency: node.name === parsed.name
|
|
1214
|
+
})
|
|
1215
|
+
})
|
|
1216
|
+
);
|
|
1217
|
+
const metadataByKey = /* @__PURE__ */ new Map();
|
|
1218
|
+
for (const entry of metadataEntries) {
|
|
1219
|
+
metadataByKey.set(entry.key, entry.metadata);
|
|
1220
|
+
}
|
|
1221
|
+
const extraction = {
|
|
1222
|
+
kind: "npm",
|
|
1223
|
+
directDependencies: [
|
|
1224
|
+
{
|
|
1225
|
+
name: parsed.name,
|
|
1226
|
+
requestedRange: parsed.requested ?? "latest",
|
|
1227
|
+
scope: "prod"
|
|
1228
|
+
}
|
|
1229
|
+
],
|
|
1230
|
+
nodes: graph.nodes
|
|
1231
|
+
};
|
|
1232
|
+
const external = buildExternalAnalysisSummary(
|
|
1233
|
+
`npm:${parsed.name}`,
|
|
1234
|
+
extraction,
|
|
1235
|
+
metadataByKey,
|
|
1236
|
+
config
|
|
1237
|
+
);
|
|
866
1238
|
return {
|
|
867
|
-
targetPath,
|
|
868
1239
|
available: true,
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
transitiveDependencies: allDependencies.length - dependencies.length,
|
|
875
|
-
dependencyDepth: maxDepth,
|
|
876
|
-
lockfileKind: extraction.kind,
|
|
877
|
-
metadataCoverage: allDependencies.length === 0 ? 0 : round4(metadataAvailableCount / allDependencies.length)
|
|
1240
|
+
dependency: {
|
|
1241
|
+
name: parsed.name,
|
|
1242
|
+
requested: parsed.requested,
|
|
1243
|
+
resolvedVersion: direct.resolvedVersion,
|
|
1244
|
+
resolution: direct.resolution
|
|
878
1245
|
},
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1246
|
+
graph: {
|
|
1247
|
+
nodeCount: graph.nodes.length,
|
|
1248
|
+
truncated: graph.truncated,
|
|
1249
|
+
maxNodes,
|
|
1250
|
+
maxDepth
|
|
1251
|
+
},
|
|
1252
|
+
assumptions: graph.assumptions,
|
|
1253
|
+
external
|
|
1254
|
+
};
|
|
1255
|
+
};
|
|
1256
|
+
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1257
|
+
var MAX_RETRIES2 = 3;
|
|
1258
|
+
var RETRY_BASE_DELAY_MS2 = 500;
|
|
1259
|
+
var round42 = (value) => Number(value.toFixed(4));
|
|
1260
|
+
var parseDate = (iso) => {
|
|
1261
|
+
if (iso === void 0) {
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
const value = Date.parse(iso);
|
|
1265
|
+
return Number.isNaN(value) ? null : value;
|
|
1266
|
+
};
|
|
1267
|
+
var NpmRegistryMetadataProvider = class {
|
|
1268
|
+
cache = /* @__PURE__ */ new Map();
|
|
1269
|
+
async fetchWeeklyDownloads(name) {
|
|
1270
|
+
const encodedName = encodeURIComponent(name);
|
|
1271
|
+
const payload = await fetchJsonWithRetry(
|
|
1272
|
+
`https://api.npmjs.org/downloads/point/last-week/${encodedName}`,
|
|
1273
|
+
{ retries: MAX_RETRIES2, baseDelayMs: RETRY_BASE_DELAY_MS2 }
|
|
1274
|
+
);
|
|
1275
|
+
if (payload === null) {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
const downloads = payload.downloads;
|
|
1279
|
+
if (typeof downloads !== "number" || Number.isNaN(downloads) || downloads < 0) {
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
return Math.floor(downloads);
|
|
1283
|
+
}
|
|
1284
|
+
async getMetadata(name, version2, context) {
|
|
1285
|
+
const key = `${name}@${version2}`;
|
|
1286
|
+
if (this.cache.has(key)) {
|
|
1287
|
+
return this.cache.get(key) ?? null;
|
|
1288
|
+
}
|
|
1289
|
+
try {
|
|
1290
|
+
const encodedName = encodeURIComponent(name);
|
|
1291
|
+
const payload = await fetchJsonWithRetry(
|
|
1292
|
+
`https://registry.npmjs.org/${encodedName}`,
|
|
1293
|
+
{ retries: MAX_RETRIES2, baseDelayMs: RETRY_BASE_DELAY_MS2 }
|
|
1294
|
+
);
|
|
1295
|
+
if (payload === null) {
|
|
1296
|
+
this.cache.set(key, null);
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
const timeEntries = payload.time ?? {};
|
|
1300
|
+
const publishDates = Object.entries(timeEntries).filter(([tag]) => tag !== "created" && tag !== "modified").map(([, date]) => parseDate(date)).filter((value) => value !== null).sort((a, b) => a - b);
|
|
1301
|
+
const modifiedAt = parseDate(timeEntries["modified"]);
|
|
1302
|
+
const now = Date.now();
|
|
1303
|
+
const daysSinceLastRelease = modifiedAt === null ? null : Math.max(0, round42((now - modifiedAt) / ONE_DAY_MS));
|
|
1304
|
+
let releaseFrequencyDays = null;
|
|
1305
|
+
if (publishDates.length >= 2) {
|
|
1306
|
+
const totalIntervals = publishDates.length - 1;
|
|
1307
|
+
let sum = 0;
|
|
1308
|
+
for (let i = 1; i < publishDates.length; i += 1) {
|
|
1309
|
+
const current = publishDates[i];
|
|
1310
|
+
const previous = publishDates[i - 1];
|
|
1311
|
+
if (current !== void 0 && previous !== void 0) {
|
|
1312
|
+
sum += current - previous;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
releaseFrequencyDays = round42(sum / totalIntervals / ONE_DAY_MS);
|
|
1316
|
+
}
|
|
1317
|
+
const maintainers = payload.maintainers ?? [];
|
|
1318
|
+
const maintainerCount = maintainers.length > 0 ? maintainers.length : null;
|
|
1319
|
+
const weeklyDownloads = context.directDependency ? await this.fetchWeeklyDownloads(name).catch(() => null) : null;
|
|
1320
|
+
const metadata = {
|
|
1321
|
+
name,
|
|
1322
|
+
version: version2,
|
|
1323
|
+
weeklyDownloads,
|
|
1324
|
+
maintainerCount,
|
|
1325
|
+
releaseFrequencyDays,
|
|
1326
|
+
daysSinceLastRelease,
|
|
1327
|
+
repositoryActivity30d: null,
|
|
1328
|
+
busFactor: null
|
|
1329
|
+
};
|
|
1330
|
+
this.cache.set(key, metadata);
|
|
1331
|
+
return metadata;
|
|
1332
|
+
} catch {
|
|
1333
|
+
this.cache.set(key, null);
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
var NoopMetadataProvider = class {
|
|
1339
|
+
async getMetadata(_name, _version, _context) {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
var analyzeDependencyExposureFromProject = async (input, onProgress) => {
|
|
1344
|
+
const metadataProvider = process.env["CODESENTINEL_EXTERNAL_METADATA"] === "none" ? new NoopMetadataProvider() : new NpmRegistryMetadataProvider();
|
|
1345
|
+
return analyzeDependencyExposure(input, metadataProvider, onProgress);
|
|
1346
|
+
};
|
|
1347
|
+
var analyzeDependencyCandidateFromRegistry = async (input) => {
|
|
1348
|
+
const metadataProvider = process.env["CODESENTINEL_EXTERNAL_METADATA"] === "none" ? new NoopMetadataProvider() : new NpmRegistryMetadataProvider();
|
|
1349
|
+
return analyzeDependencyCandidate(input, metadataProvider);
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
// src/index.ts
|
|
1353
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1354
|
+
import { dirname, resolve as resolve3 } from "path";
|
|
1355
|
+
import { fileURLToPath } from "url";
|
|
1356
|
+
|
|
1357
|
+
// src/application/format-analyze-output.ts
|
|
1358
|
+
var createSummaryShape = (summary) => ({
|
|
1359
|
+
targetPath: summary.structural.targetPath,
|
|
1360
|
+
structural: summary.structural.metrics,
|
|
1361
|
+
evolution: summary.evolution.available ? {
|
|
1362
|
+
available: true,
|
|
1363
|
+
metrics: summary.evolution.metrics,
|
|
1364
|
+
hotspotsTop: summary.evolution.hotspots.slice(0, 5).map((hotspot) => hotspot.filePath)
|
|
1365
|
+
} : {
|
|
1366
|
+
available: false,
|
|
1367
|
+
reason: summary.evolution.reason
|
|
1368
|
+
},
|
|
1369
|
+
external: summary.external.available ? {
|
|
1370
|
+
available: true,
|
|
1371
|
+
metrics: summary.external.metrics,
|
|
1372
|
+
highRiskDependenciesTop: summary.external.highRiskDependencies.slice(0, 10),
|
|
1373
|
+
highRiskDevelopmentDependenciesTop: summary.external.highRiskDevelopmentDependencies.slice(
|
|
1374
|
+
0,
|
|
1375
|
+
10
|
|
1376
|
+
),
|
|
1377
|
+
transitiveExposureDependenciesTop: summary.external.transitiveExposureDependencies.slice(0, 10)
|
|
1378
|
+
} : {
|
|
1379
|
+
available: false,
|
|
1380
|
+
reason: summary.external.reason
|
|
1381
|
+
},
|
|
1382
|
+
risk: {
|
|
1383
|
+
repositoryScore: summary.risk.repositoryScore,
|
|
1384
|
+
normalizedScore: summary.risk.normalizedScore,
|
|
1385
|
+
hotspotsTop: summary.risk.hotspots.slice(0, 5).map((hotspot) => ({
|
|
1386
|
+
file: hotspot.file,
|
|
1387
|
+
score: hotspot.score
|
|
1388
|
+
})),
|
|
1389
|
+
fragileClusterCount: summary.risk.fragileClusters.length,
|
|
1390
|
+
dependencyAmplificationZoneCount: summary.risk.dependencyAmplificationZones.length
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
var formatAnalyzeOutput = (summary, mode) => mode === "json" ? JSON.stringify(summary, null, 2) : JSON.stringify(createSummaryShape(summary), null, 2);
|
|
1394
|
+
|
|
1395
|
+
// src/application/format-dependency-risk-output.ts
|
|
1396
|
+
var createSummaryShape2 = (result) => {
|
|
1397
|
+
if (!result.available) {
|
|
1398
|
+
return {
|
|
1399
|
+
available: false,
|
|
1400
|
+
reason: result.reason,
|
|
1401
|
+
dependency: result.dependency
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
const direct = result.external.dependencies[0];
|
|
1405
|
+
return {
|
|
1406
|
+
available: true,
|
|
1407
|
+
dependency: result.dependency,
|
|
1408
|
+
graph: result.graph,
|
|
1409
|
+
assumptions: result.assumptions,
|
|
1410
|
+
external: {
|
|
1411
|
+
metrics: result.external.metrics,
|
|
1412
|
+
ownRiskSignals: direct?.ownRiskSignals ?? [],
|
|
1413
|
+
inheritedRiskSignals: direct?.inheritedRiskSignals ?? [],
|
|
1414
|
+
highRiskDependenciesTop: result.external.highRiskDependencies.slice(0, 10),
|
|
1415
|
+
transitiveExposureDependenciesTop: result.external.transitiveExposureDependencies.slice(0, 10)
|
|
1416
|
+
}
|
|
886
1417
|
};
|
|
887
1418
|
};
|
|
888
|
-
var
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1419
|
+
var formatDependencyRiskOutput = (result, mode) => mode === "json" ? JSON.stringify(result, null, 2) : JSON.stringify(createSummaryShape2(result), null, 2);
|
|
1420
|
+
|
|
1421
|
+
// src/application/logger.ts
|
|
1422
|
+
var logLevelRank = {
|
|
1423
|
+
error: 0,
|
|
1424
|
+
warn: 1,
|
|
1425
|
+
info: 2,
|
|
1426
|
+
debug: 3
|
|
895
1427
|
};
|
|
896
|
-
var
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
var
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1428
|
+
var noop = () => {
|
|
1429
|
+
};
|
|
1430
|
+
var createSilentLogger = () => ({
|
|
1431
|
+
error: noop,
|
|
1432
|
+
warn: noop,
|
|
1433
|
+
info: noop,
|
|
1434
|
+
debug: noop
|
|
1435
|
+
});
|
|
1436
|
+
var shouldLog = (configuredLevel, messageLevel) => {
|
|
1437
|
+
if (configuredLevel === "silent") {
|
|
1438
|
+
return false;
|
|
1439
|
+
}
|
|
1440
|
+
return logLevelRank[messageLevel] <= logLevelRank[configuredLevel];
|
|
1441
|
+
};
|
|
1442
|
+
var write = (messageLevel, message) => {
|
|
1443
|
+
process.stderr.write(`[codesentinel] ${messageLevel.toUpperCase()} ${message}
|
|
1444
|
+
`);
|
|
1445
|
+
};
|
|
1446
|
+
var createStderrLogger = (level) => {
|
|
1447
|
+
if (level === "silent") {
|
|
1448
|
+
return createSilentLogger();
|
|
908
1449
|
}
|
|
909
1450
|
return {
|
|
910
|
-
|
|
911
|
-
|
|
1451
|
+
error: (message) => {
|
|
1452
|
+
if (shouldLog(level, "error")) {
|
|
1453
|
+
write("error", message);
|
|
1454
|
+
}
|
|
1455
|
+
},
|
|
1456
|
+
warn: (message) => {
|
|
1457
|
+
if (shouldLog(level, "warn")) {
|
|
1458
|
+
write("warn", message);
|
|
1459
|
+
}
|
|
1460
|
+
},
|
|
1461
|
+
info: (message) => {
|
|
1462
|
+
if (shouldLog(level, "info")) {
|
|
1463
|
+
write("info", message);
|
|
1464
|
+
}
|
|
1465
|
+
},
|
|
1466
|
+
debug: (message) => {
|
|
1467
|
+
if (shouldLog(level, "debug")) {
|
|
1468
|
+
write("debug", message);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
912
1471
|
};
|
|
913
1472
|
};
|
|
914
|
-
var
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1473
|
+
var parseLogLevel = (value) => {
|
|
1474
|
+
switch (value) {
|
|
1475
|
+
case "silent":
|
|
1476
|
+
case "error":
|
|
1477
|
+
case "warn":
|
|
1478
|
+
case "info":
|
|
1479
|
+
case "debug":
|
|
1480
|
+
return value;
|
|
1481
|
+
default:
|
|
1482
|
+
return "info";
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
// src/application/run-analyze-command.ts
|
|
1487
|
+
import { resolve as resolve2 } from "path";
|
|
1488
|
+
|
|
1489
|
+
// ../code-graph/dist/index.js
|
|
1490
|
+
import { extname, isAbsolute, relative, resolve } from "path";
|
|
1491
|
+
import * as ts from "typescript";
|
|
1492
|
+
var edgeKey = (from, to) => `${from}\0${to}`;
|
|
1493
|
+
var createGraphData = (nodes, rawEdges) => {
|
|
1494
|
+
const sortedNodes = [...nodes].sort((a, b) => a.id.localeCompare(b.id));
|
|
1495
|
+
const knownNodeIds = new Set(sortedNodes.map((node) => node.id));
|
|
1496
|
+
const uniqueEdgeMap = /* @__PURE__ */ new Map();
|
|
1497
|
+
for (const edge of rawEdges) {
|
|
1498
|
+
if (edge.from === edge.to) {
|
|
918
1499
|
continue;
|
|
919
1500
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
};
|
|
1501
|
+
if (!knownNodeIds.has(edge.from) || !knownNodeIds.has(edge.to)) {
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
uniqueEdgeMap.set(edgeKey(edge.from, edge.to), edge);
|
|
925
1505
|
}
|
|
926
|
-
|
|
1506
|
+
const sortedEdges = [...uniqueEdgeMap.values()].sort((a, b) => {
|
|
1507
|
+
const fromCompare = a.from.localeCompare(b.from);
|
|
1508
|
+
if (fromCompare !== 0) {
|
|
1509
|
+
return fromCompare;
|
|
1510
|
+
}
|
|
1511
|
+
return a.to.localeCompare(b.to);
|
|
1512
|
+
});
|
|
1513
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
1514
|
+
for (const node of sortedNodes) {
|
|
1515
|
+
adjacency.set(node.id, []);
|
|
1516
|
+
}
|
|
1517
|
+
for (const edge of sortedEdges) {
|
|
1518
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
1519
|
+
}
|
|
1520
|
+
const adjacencyById = /* @__PURE__ */ new Map();
|
|
1521
|
+
for (const [nodeId, targets] of adjacency.entries()) {
|
|
1522
|
+
adjacencyById.set(nodeId, [...targets]);
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
nodes: sortedNodes,
|
|
1526
|
+
edges: sortedEdges,
|
|
1527
|
+
adjacencyById
|
|
1528
|
+
};
|
|
927
1529
|
};
|
|
928
|
-
var
|
|
929
|
-
|
|
930
|
-
const
|
|
931
|
-
const
|
|
932
|
-
|
|
1530
|
+
var runTarjanScc = (adjacencyById) => {
|
|
1531
|
+
let index = 0;
|
|
1532
|
+
const indices = /* @__PURE__ */ new Map();
|
|
1533
|
+
const lowLink = /* @__PURE__ */ new Map();
|
|
1534
|
+
const stack = [];
|
|
1535
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
1536
|
+
const components = [];
|
|
1537
|
+
const strongConnect = (nodeId) => {
|
|
1538
|
+
indices.set(nodeId, index);
|
|
1539
|
+
lowLink.set(nodeId, index);
|
|
1540
|
+
index += 1;
|
|
1541
|
+
stack.push(nodeId);
|
|
1542
|
+
onStack.add(nodeId);
|
|
1543
|
+
const neighbors = adjacencyById.get(nodeId) ?? [];
|
|
1544
|
+
for (const nextId of neighbors) {
|
|
1545
|
+
if (!indices.has(nextId)) {
|
|
1546
|
+
strongConnect(nextId);
|
|
1547
|
+
const nodeLowLink2 = lowLink.get(nodeId);
|
|
1548
|
+
const nextLowLink = lowLink.get(nextId);
|
|
1549
|
+
if (nodeLowLink2 !== void 0 && nextLowLink !== void 0 && nextLowLink < nodeLowLink2) {
|
|
1550
|
+
lowLink.set(nodeId, nextLowLink);
|
|
1551
|
+
}
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
if (onStack.has(nextId)) {
|
|
1555
|
+
const nodeLowLink2 = lowLink.get(nodeId);
|
|
1556
|
+
const nextIndex = indices.get(nextId);
|
|
1557
|
+
if (nodeLowLink2 !== void 0 && nextIndex !== void 0 && nextIndex < nodeLowLink2) {
|
|
1558
|
+
lowLink.set(nodeId, nextIndex);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
const nodeLowLink = lowLink.get(nodeId);
|
|
1563
|
+
const nodeIndex = indices.get(nodeId);
|
|
1564
|
+
if (nodeLowLink === void 0 || nodeIndex === void 0 || nodeLowLink !== nodeIndex) {
|
|
933
1565
|
return;
|
|
934
1566
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1567
|
+
const component = [];
|
|
1568
|
+
for (; ; ) {
|
|
1569
|
+
const popped = stack.pop();
|
|
1570
|
+
if (popped === void 0) {
|
|
1571
|
+
break;
|
|
1572
|
+
}
|
|
1573
|
+
onStack.delete(popped);
|
|
1574
|
+
component.push(popped);
|
|
1575
|
+
if (popped === nodeId) {
|
|
1576
|
+
break;
|
|
939
1577
|
}
|
|
940
|
-
merged.set(name, { name, requestedRange: versionRange, scope });
|
|
941
1578
|
}
|
|
1579
|
+
component.sort((a, b) => a.localeCompare(b));
|
|
1580
|
+
components.push(component);
|
|
942
1581
|
};
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1582
|
+
const nodeIds = [...adjacencyById.keys()].sort((a, b) => a.localeCompare(b));
|
|
1583
|
+
for (const nodeId of nodeIds) {
|
|
1584
|
+
if (!indices.has(nodeId)) {
|
|
1585
|
+
strongConnect(nodeId);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
components.sort((a, b) => {
|
|
1589
|
+
const firstA = a[0] ?? "";
|
|
1590
|
+
const firstB = b[0] ?? "";
|
|
1591
|
+
return firstA.localeCompare(firstB);
|
|
1592
|
+
});
|
|
1593
|
+
return { components };
|
|
948
1594
|
};
|
|
949
|
-
var
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
name,
|
|
965
|
-
version: packageData.version,
|
|
966
|
-
dependencies
|
|
967
|
-
});
|
|
1595
|
+
var hasSelfLoop = (nodeId, adjacencyById) => {
|
|
1596
|
+
const targets = adjacencyById.get(nodeId) ?? [];
|
|
1597
|
+
return targets.includes(nodeId);
|
|
1598
|
+
};
|
|
1599
|
+
var computeCyclesAndDepth = (graph) => {
|
|
1600
|
+
const { components } = runTarjanScc(graph.adjacencyById);
|
|
1601
|
+
const cycles = [];
|
|
1602
|
+
const componentByNodeId = /* @__PURE__ */ new Map();
|
|
1603
|
+
components.forEach((component, index) => {
|
|
1604
|
+
for (const nodeId of component) {
|
|
1605
|
+
componentByNodeId.set(nodeId, index);
|
|
1606
|
+
}
|
|
1607
|
+
if (component.length > 1) {
|
|
1608
|
+
cycles.push({ nodes: [...component] });
|
|
1609
|
+
return;
|
|
968
1610
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
974
|
-
const dependencies = Object.entries(dep.dependencies ?? {}).map(([depName, depVersion]) => `${depName}@${String(depVersion)}`).sort((a, b) => a.localeCompare(b));
|
|
975
|
-
nodes.push({
|
|
976
|
-
name,
|
|
977
|
-
version: dep.version,
|
|
978
|
-
dependencies
|
|
979
|
-
});
|
|
1611
|
+
const onlyNode = component[0];
|
|
1612
|
+
if (onlyNode !== void 0 && hasSelfLoop(onlyNode, graph.adjacencyById)) {
|
|
1613
|
+
cycles.push({ nodes: [...component] });
|
|
980
1614
|
}
|
|
1615
|
+
});
|
|
1616
|
+
const dagOutgoing = /* @__PURE__ */ new Map();
|
|
1617
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
1618
|
+
for (let i = 0; i < components.length; i += 1) {
|
|
1619
|
+
dagOutgoing.set(i, /* @__PURE__ */ new Set());
|
|
1620
|
+
inDegree.set(i, 0);
|
|
981
1621
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
nodes
|
|
987
|
-
};
|
|
988
|
-
};
|
|
989
|
-
var sanitizeValue = (value) => value.replace(/^['"]|['"]$/g, "").trim();
|
|
990
|
-
var parsePackageKey = (rawKey) => {
|
|
991
|
-
const key = sanitizeValue(rawKey.replace(/:$/, ""));
|
|
992
|
-
const withoutSlash = key.startsWith("/") ? key.slice(1) : key;
|
|
993
|
-
const lastAt = withoutSlash.lastIndexOf("@");
|
|
994
|
-
if (lastAt <= 0) {
|
|
995
|
-
return null;
|
|
996
|
-
}
|
|
997
|
-
const name = withoutSlash.slice(0, lastAt);
|
|
998
|
-
const versionWithPeers = withoutSlash.slice(lastAt + 1);
|
|
999
|
-
const version2 = versionWithPeers.split("(")[0] ?? versionWithPeers;
|
|
1000
|
-
if (name.length === 0 || version2.length === 0) {
|
|
1001
|
-
return null;
|
|
1002
|
-
}
|
|
1003
|
-
return { name, version: version2 };
|
|
1004
|
-
};
|
|
1005
|
-
var parsePnpmLockfile = (raw, directSpecs) => {
|
|
1006
|
-
const lines = raw.split("\n");
|
|
1007
|
-
let state = "root";
|
|
1008
|
-
let currentPackage = null;
|
|
1009
|
-
let currentDependencyName = null;
|
|
1010
|
-
const dependenciesByNode = /* @__PURE__ */ new Map();
|
|
1011
|
-
for (const line of lines) {
|
|
1012
|
-
if (line.trim().length === 0 || line.trimStart().startsWith("#")) {
|
|
1622
|
+
for (const edge of graph.edges) {
|
|
1623
|
+
const fromComponent = componentByNodeId.get(edge.from);
|
|
1624
|
+
const toComponent = componentByNodeId.get(edge.to);
|
|
1625
|
+
if (fromComponent === void 0 || toComponent === void 0 || fromComponent === toComponent) {
|
|
1013
1626
|
continue;
|
|
1014
1627
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1628
|
+
const outgoing = dagOutgoing.get(fromComponent);
|
|
1629
|
+
if (outgoing?.has(toComponent) === true) {
|
|
1017
1630
|
continue;
|
|
1018
1631
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1632
|
+
outgoing?.add(toComponent);
|
|
1633
|
+
inDegree.set(toComponent, (inDegree.get(toComponent) ?? 0) + 1);
|
|
1634
|
+
}
|
|
1635
|
+
const queue = [];
|
|
1636
|
+
const depthByComponent = /* @__PURE__ */ new Map();
|
|
1637
|
+
for (let i = 0; i < components.length; i += 1) {
|
|
1638
|
+
if ((inDegree.get(i) ?? 0) === 0) {
|
|
1639
|
+
queue.push(i);
|
|
1640
|
+
depthByComponent.set(i, 0);
|
|
1022
1641
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
state = "packageDeps";
|
|
1031
|
-
currentDependencyName = null;
|
|
1032
|
-
}
|
|
1033
|
-
continue;
|
|
1034
|
-
}
|
|
1642
|
+
}
|
|
1643
|
+
let cursor = 0;
|
|
1644
|
+
while (cursor < queue.length) {
|
|
1645
|
+
const componentId = queue[cursor];
|
|
1646
|
+
cursor += 1;
|
|
1647
|
+
if (componentId === void 0) {
|
|
1648
|
+
continue;
|
|
1035
1649
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (depName.length > 0 && depVersion.length > 0) {
|
|
1043
|
-
dependenciesByNode.get(currentPackage)?.add(`${depName}@${depVersion}`);
|
|
1044
|
-
}
|
|
1045
|
-
currentDependencyName = null;
|
|
1046
|
-
continue;
|
|
1047
|
-
}
|
|
1048
|
-
const depBlockLine = line.match(/^\s{6}([^:\s]+):\s*$/);
|
|
1049
|
-
if (depBlockLine !== null) {
|
|
1050
|
-
currentDependencyName = sanitizeValue(depBlockLine[1] ?? "");
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
const depVersionLine = line.match(/^\s{8}version:\s*(.+)$/);
|
|
1054
|
-
if (depVersionLine !== null && currentDependencyName !== null) {
|
|
1055
|
-
const depRef = sanitizeValue(depVersionLine[1] ?? "");
|
|
1056
|
-
const depVersion = depRef.split("(")[0] ?? depRef;
|
|
1057
|
-
if (depVersion.length > 0) {
|
|
1058
|
-
dependenciesByNode.get(currentPackage)?.add(`${currentDependencyName}@${depVersion}`);
|
|
1059
|
-
}
|
|
1060
|
-
currentDependencyName = null;
|
|
1061
|
-
continue;
|
|
1650
|
+
const currentDepth = depthByComponent.get(componentId) ?? 0;
|
|
1651
|
+
const outgoing = dagOutgoing.get(componentId) ?? /* @__PURE__ */ new Set();
|
|
1652
|
+
for (const nextComponent of outgoing) {
|
|
1653
|
+
const nextDepth = depthByComponent.get(nextComponent) ?? 0;
|
|
1654
|
+
if (currentDepth + 1 > nextDepth) {
|
|
1655
|
+
depthByComponent.set(nextComponent, currentDepth + 1);
|
|
1062
1656
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1657
|
+
const remainingIncoming = (inDegree.get(nextComponent) ?? 0) - 1;
|
|
1658
|
+
inDegree.set(nextComponent, remainingIncoming);
|
|
1659
|
+
if (remainingIncoming === 0) {
|
|
1660
|
+
queue.push(nextComponent);
|
|
1065
1661
|
}
|
|
1066
1662
|
}
|
|
1067
1663
|
}
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1664
|
+
const depthByNodeId = /* @__PURE__ */ new Map();
|
|
1665
|
+
let graphDepth = 0;
|
|
1666
|
+
components.forEach((component, componentId) => {
|
|
1667
|
+
const componentDepth = depthByComponent.get(componentId) ?? 0;
|
|
1668
|
+
if (componentDepth > graphDepth) {
|
|
1669
|
+
graphDepth = componentDepth;
|
|
1670
|
+
}
|
|
1671
|
+
for (const nodeId of component) {
|
|
1672
|
+
depthByNodeId.set(nodeId, componentDepth);
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
cycles.sort((a, b) => {
|
|
1676
|
+
const firstA = a.nodes[0] ?? "";
|
|
1677
|
+
const firstB = b.nodes[0] ?? "";
|
|
1678
|
+
return firstA.localeCompare(firstB);
|
|
1679
|
+
});
|
|
1680
|
+
return {
|
|
1681
|
+
depthByNodeId,
|
|
1682
|
+
graphDepth,
|
|
1683
|
+
cycles
|
|
1684
|
+
};
|
|
1685
|
+
};
|
|
1686
|
+
var createGraphAnalysisSummary = (targetPath, graph) => {
|
|
1687
|
+
const fanInById = /* @__PURE__ */ new Map();
|
|
1688
|
+
const fanOutById = /* @__PURE__ */ new Map();
|
|
1689
|
+
for (const node of graph.nodes) {
|
|
1690
|
+
fanInById.set(node.id, 0);
|
|
1691
|
+
fanOutById.set(node.id, graph.adjacencyById.get(node.id)?.length ?? 0);
|
|
1692
|
+
}
|
|
1693
|
+
for (const edge of graph.edges) {
|
|
1694
|
+
fanInById.set(edge.to, (fanInById.get(edge.to) ?? 0) + 1);
|
|
1695
|
+
}
|
|
1696
|
+
const { cycles, depthByNodeId, graphDepth } = computeCyclesAndDepth(graph);
|
|
1697
|
+
let maxFanIn = 0;
|
|
1698
|
+
let maxFanOut = 0;
|
|
1699
|
+
const files = graph.nodes.map((node) => {
|
|
1700
|
+
const fanIn = fanInById.get(node.id) ?? 0;
|
|
1701
|
+
const fanOut = fanOutById.get(node.id) ?? 0;
|
|
1702
|
+
if (fanIn > maxFanIn) {
|
|
1703
|
+
maxFanIn = fanIn;
|
|
1704
|
+
}
|
|
1705
|
+
if (fanOut > maxFanOut) {
|
|
1706
|
+
maxFanOut = fanOut;
|
|
1707
|
+
}
|
|
1070
1708
|
return {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1709
|
+
id: node.id,
|
|
1710
|
+
relativePath: node.relativePath,
|
|
1711
|
+
directDependencies: graph.adjacencyById.get(node.id) ?? [],
|
|
1712
|
+
fanIn,
|
|
1713
|
+
fanOut,
|
|
1714
|
+
depth: depthByNodeId.get(node.id) ?? 0
|
|
1074
1715
|
};
|
|
1075
|
-
})
|
|
1076
|
-
|
|
1716
|
+
});
|
|
1717
|
+
const metrics = {
|
|
1718
|
+
nodeCount: graph.nodes.length,
|
|
1719
|
+
edgeCount: graph.edges.length,
|
|
1720
|
+
cycleCount: cycles.length,
|
|
1721
|
+
graphDepth,
|
|
1722
|
+
maxFanIn,
|
|
1723
|
+
maxFanOut
|
|
1724
|
+
};
|
|
1725
|
+
return {
|
|
1726
|
+
targetPath,
|
|
1727
|
+
nodes: graph.nodes,
|
|
1728
|
+
edges: graph.edges,
|
|
1729
|
+
cycles,
|
|
1730
|
+
files,
|
|
1731
|
+
metrics
|
|
1732
|
+
};
|
|
1733
|
+
};
|
|
1734
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
1735
|
+
var SCAN_EXCLUDES = [
|
|
1736
|
+
"**/node_modules/**",
|
|
1737
|
+
"**/.git/**",
|
|
1738
|
+
"**/dist/**",
|
|
1739
|
+
"**/build/**",
|
|
1740
|
+
"**/.next/**",
|
|
1741
|
+
"**/coverage/**",
|
|
1742
|
+
"**/.turbo/**",
|
|
1743
|
+
"**/.cache/**",
|
|
1744
|
+
"**/out/**"
|
|
1745
|
+
];
|
|
1746
|
+
var SCAN_INCLUDES = ["**/*"];
|
|
1747
|
+
var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
1748
|
+
"node_modules",
|
|
1749
|
+
".git",
|
|
1750
|
+
"dist",
|
|
1751
|
+
"build",
|
|
1752
|
+
".next",
|
|
1753
|
+
"coverage",
|
|
1754
|
+
".turbo",
|
|
1755
|
+
".cache",
|
|
1756
|
+
"out"
|
|
1757
|
+
]);
|
|
1758
|
+
var normalizePath = (pathValue) => pathValue.replaceAll("\\", "/");
|
|
1759
|
+
var isProjectSourceFile = (filePath, projectRoot) => {
|
|
1760
|
+
const extension = extname(filePath);
|
|
1761
|
+
if (!SOURCE_EXTENSIONS.has(extension)) {
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
const relativePath = relative(projectRoot, filePath);
|
|
1765
|
+
if (relativePath.startsWith("..")) {
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
const normalizedRelativePath = normalizePath(relativePath);
|
|
1769
|
+
const segments = normalizedRelativePath.split("/");
|
|
1770
|
+
return !segments.some((segment) => IGNORED_SEGMENTS.has(segment));
|
|
1771
|
+
};
|
|
1772
|
+
var discoverSourceFilesByScan = (projectRoot) => {
|
|
1773
|
+
const files = ts.sys.readDirectory(
|
|
1774
|
+
projectRoot,
|
|
1775
|
+
[...SOURCE_EXTENSIONS],
|
|
1776
|
+
SCAN_EXCLUDES,
|
|
1777
|
+
SCAN_INCLUDES
|
|
1778
|
+
);
|
|
1779
|
+
return files.map((filePath) => resolve(filePath));
|
|
1780
|
+
};
|
|
1781
|
+
var parseTsConfigFile = (configPath) => {
|
|
1782
|
+
const parsedCommandLine = ts.getParsedCommandLineOfConfigFile(
|
|
1783
|
+
configPath,
|
|
1784
|
+
{},
|
|
1785
|
+
{
|
|
1786
|
+
...ts.sys,
|
|
1787
|
+
onUnRecoverableConfigFileDiagnostic: () => {
|
|
1788
|
+
throw new Error(`Failed to parse TypeScript configuration at ${configPath}`);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1077
1791
|
);
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
directDependencies: directSpecs,
|
|
1081
|
-
nodes
|
|
1082
|
-
};
|
|
1083
|
-
};
|
|
1084
|
-
var stripQuotes = (value) => value.replace(/^['"]|['"]$/g, "");
|
|
1085
|
-
var parseVersionSelector = (selector) => {
|
|
1086
|
-
const npmIndex = selector.lastIndexOf("@npm:");
|
|
1087
|
-
if (npmIndex >= 0) {
|
|
1088
|
-
return selector.slice(npmIndex + 5);
|
|
1792
|
+
if (parsedCommandLine === void 0) {
|
|
1793
|
+
throw new Error(`Failed to parse TypeScript configuration at ${configPath}`);
|
|
1089
1794
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1795
|
+
return parsedCommandLine;
|
|
1796
|
+
};
|
|
1797
|
+
var collectFilesFromTsConfigGraph = (projectRoot) => {
|
|
1798
|
+
const rootConfigPath = ts.findConfigFile(projectRoot, ts.sys.fileExists, "tsconfig.json");
|
|
1799
|
+
if (rootConfigPath === void 0) {
|
|
1092
1800
|
return null;
|
|
1093
1801
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
let version2 = null;
|
|
1101
|
-
let readingDependencies = false;
|
|
1102
|
-
let dependencies = [];
|
|
1103
|
-
const flushEntry = () => {
|
|
1104
|
-
if (selectors.length === 0 || version2 === null) {
|
|
1105
|
-
selectors = [];
|
|
1106
|
-
version2 = null;
|
|
1107
|
-
dependencies = [];
|
|
1108
|
-
readingDependencies = false;
|
|
1802
|
+
const visitedConfigPaths = /* @__PURE__ */ new Set();
|
|
1803
|
+
const collectedFiles = /* @__PURE__ */ new Set();
|
|
1804
|
+
let rootOptions = null;
|
|
1805
|
+
const visitConfig = (configPath) => {
|
|
1806
|
+
const absoluteConfigPath = resolve(configPath);
|
|
1807
|
+
if (visitedConfigPaths.has(absoluteConfigPath)) {
|
|
1109
1808
|
return;
|
|
1110
1809
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
if (name.length === 0) {
|
|
1116
|
-
continue;
|
|
1117
|
-
}
|
|
1118
|
-
nodes.push({
|
|
1119
|
-
name,
|
|
1120
|
-
version: version2,
|
|
1121
|
-
dependencies: [...dependencies].sort((a, b) => a.localeCompare(b))
|
|
1122
|
-
});
|
|
1123
|
-
if (parsedVersion !== null) {
|
|
1124
|
-
nodes.push({
|
|
1125
|
-
name,
|
|
1126
|
-
version: parsedVersion,
|
|
1127
|
-
dependencies: [...dependencies].sort((a, b) => a.localeCompare(b))
|
|
1128
|
-
});
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
selectors = [];
|
|
1132
|
-
version2 = null;
|
|
1133
|
-
dependencies = [];
|
|
1134
|
-
readingDependencies = false;
|
|
1135
|
-
};
|
|
1136
|
-
for (const line of lines) {
|
|
1137
|
-
if (line.trim().length === 0) {
|
|
1138
|
-
continue;
|
|
1139
|
-
}
|
|
1140
|
-
if (!line.startsWith(" ") && line.endsWith(":")) {
|
|
1141
|
-
flushEntry();
|
|
1142
|
-
const keyText = line.slice(0, -1);
|
|
1143
|
-
selectors = keyText.split(",").map((part) => stripQuotes(part.trim())).filter((part) => part.length > 0);
|
|
1144
|
-
continue;
|
|
1145
|
-
}
|
|
1146
|
-
if (line.match(/^\s{2}version\s+/) !== null) {
|
|
1147
|
-
const value = line.replace(/^\s{2}version\s+/, "").trim();
|
|
1148
|
-
version2 = stripQuotes(value);
|
|
1149
|
-
readingDependencies = false;
|
|
1150
|
-
continue;
|
|
1810
|
+
visitedConfigPaths.add(absoluteConfigPath);
|
|
1811
|
+
const parsed = parseTsConfigFile(absoluteConfigPath);
|
|
1812
|
+
if (rootOptions === null) {
|
|
1813
|
+
rootOptions = parsed.options;
|
|
1151
1814
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
continue;
|
|
1815
|
+
for (const filePath of parsed.fileNames) {
|
|
1816
|
+
collectedFiles.add(resolve(filePath));
|
|
1155
1817
|
}
|
|
1156
|
-
|
|
1157
|
-
const
|
|
1158
|
-
const
|
|
1159
|
-
if (
|
|
1160
|
-
|
|
1818
|
+
for (const reference of parsed.projectReferences ?? []) {
|
|
1819
|
+
const referencePath = resolve(reference.path);
|
|
1820
|
+
const referenceConfigPath = ts.sys.directoryExists(referencePath) ? ts.findConfigFile(referencePath, ts.sys.fileExists, "tsconfig.json") : referencePath;
|
|
1821
|
+
if (referenceConfigPath !== void 0 && ts.sys.fileExists(referenceConfigPath)) {
|
|
1822
|
+
visitConfig(referenceConfigPath);
|
|
1161
1823
|
}
|
|
1162
|
-
const depName = stripQuotes(depLine.slice(0, firstSpace));
|
|
1163
|
-
const depRef = stripQuotes(depLine.slice(firstSpace + 1).trim());
|
|
1164
|
-
const depVersion = parseVersionSelector(depRef) ?? depRef;
|
|
1165
|
-
dependencies.push(`${depName}@${depVersion}`);
|
|
1166
|
-
continue;
|
|
1167
|
-
}
|
|
1168
|
-
readingDependencies = false;
|
|
1169
|
-
}
|
|
1170
|
-
flushEntry();
|
|
1171
|
-
const deduped = /* @__PURE__ */ new Map();
|
|
1172
|
-
for (const node of nodes) {
|
|
1173
|
-
const key = `${node.name}@${node.version}`;
|
|
1174
|
-
if (!deduped.has(key)) {
|
|
1175
|
-
deduped.set(key, node);
|
|
1176
1824
|
}
|
|
1177
|
-
}
|
|
1825
|
+
};
|
|
1826
|
+
visitConfig(rootConfigPath);
|
|
1178
1827
|
return {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1828
|
+
fileNames: [...collectedFiles],
|
|
1829
|
+
rootOptions: rootOptions ?? {
|
|
1830
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext
|
|
1831
|
+
},
|
|
1832
|
+
visitedConfigCount: visitedConfigPaths.size
|
|
1182
1833
|
};
|
|
1183
1834
|
};
|
|
1184
|
-
var
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
...DEFAULT_EXTERNAL_ANALYSIS_CONFIG,
|
|
1189
|
-
...overrides
|
|
1835
|
+
var createCompilerOptions = (base) => ({
|
|
1836
|
+
...base,
|
|
1837
|
+
allowJs: true,
|
|
1838
|
+
moduleResolution: base?.moduleResolution ?? ts.ModuleResolutionKind.NodeNext
|
|
1190
1839
|
});
|
|
1191
|
-
var
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
return parsePnpmLockfile(lockfileRaw, directSpecs);
|
|
1195
|
-
case "npm":
|
|
1196
|
-
case "npm-shrinkwrap":
|
|
1197
|
-
return {
|
|
1198
|
-
...parsePackageLock(lockfileRaw, directSpecs),
|
|
1199
|
-
kind: lockfileKind
|
|
1200
|
-
};
|
|
1201
|
-
case "yarn":
|
|
1202
|
-
return parseYarnLock(lockfileRaw, directSpecs);
|
|
1203
|
-
case "bun":
|
|
1204
|
-
return parseBunLock(lockfileRaw, directSpecs);
|
|
1205
|
-
default:
|
|
1206
|
-
throw new Error("unsupported_lockfile_format");
|
|
1207
|
-
}
|
|
1208
|
-
};
|
|
1209
|
-
var mapWithConcurrency = async (values, limit, handler) => {
|
|
1210
|
-
const effectiveLimit = Math.max(1, limit);
|
|
1211
|
-
const results = new Array(values.length);
|
|
1212
|
-
let index = 0;
|
|
1213
|
-
const workers = Array.from({ length: Math.min(effectiveLimit, values.length) }, async () => {
|
|
1214
|
-
for (; ; ) {
|
|
1215
|
-
const current = index;
|
|
1216
|
-
index += 1;
|
|
1217
|
-
if (current >= values.length) {
|
|
1218
|
-
return;
|
|
1219
|
-
}
|
|
1220
|
-
const value = values[current];
|
|
1221
|
-
if (value === void 0) {
|
|
1222
|
-
return;
|
|
1223
|
-
}
|
|
1224
|
-
results[current] = await handler(value);
|
|
1225
|
-
}
|
|
1226
|
-
});
|
|
1227
|
-
await Promise.all(workers);
|
|
1228
|
-
return results;
|
|
1229
|
-
};
|
|
1230
|
-
var analyzeDependencyExposure = async (input, metadataProvider, onProgress) => {
|
|
1231
|
-
const packageJson = loadPackageJson(input.repositoryPath);
|
|
1232
|
-
if (packageJson === null) {
|
|
1233
|
-
return {
|
|
1234
|
-
targetPath: input.repositoryPath,
|
|
1235
|
-
available: false,
|
|
1236
|
-
reason: "package_json_not_found"
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
onProgress?.({ stage: "package_json_loaded" });
|
|
1240
|
-
const lockfile = selectLockfile(input.repositoryPath);
|
|
1241
|
-
if (lockfile === null) {
|
|
1840
|
+
var parseTsConfig = (projectRoot) => {
|
|
1841
|
+
const collected = collectFilesFromTsConfigGraph(projectRoot);
|
|
1842
|
+
if (collected === null) {
|
|
1242
1843
|
return {
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1844
|
+
fileNames: discoverSourceFilesByScan(projectRoot),
|
|
1845
|
+
options: createCompilerOptions(void 0),
|
|
1846
|
+
tsconfigCount: 0,
|
|
1847
|
+
usedFallbackScan: true
|
|
1246
1848
|
};
|
|
1247
1849
|
}
|
|
1248
|
-
|
|
1249
|
-
try {
|
|
1250
|
-
const directSpecs = parsePackageJson(packageJson.raw);
|
|
1251
|
-
const extraction = parseExtraction(lockfile.kind, lockfile.raw, directSpecs);
|
|
1252
|
-
const config = withDefaults(input.config);
|
|
1253
|
-
const directNames = new Set(extraction.directDependencies.map((dependency) => dependency.name));
|
|
1254
|
-
onProgress?.({
|
|
1255
|
-
stage: "lockfile_parsed",
|
|
1256
|
-
dependencyNodes: extraction.nodes.length,
|
|
1257
|
-
directDependencies: extraction.directDependencies.length
|
|
1258
|
-
});
|
|
1259
|
-
onProgress?.({ stage: "metadata_fetch_started", total: extraction.nodes.length });
|
|
1260
|
-
let completed = 0;
|
|
1261
|
-
const metadataEntries = await mapWithConcurrency(
|
|
1262
|
-
extraction.nodes,
|
|
1263
|
-
config.metadataRequestConcurrency,
|
|
1264
|
-
async (node) => {
|
|
1265
|
-
const result = {
|
|
1266
|
-
key: `${node.name}@${node.version}`,
|
|
1267
|
-
metadata: await metadataProvider.getMetadata(node.name, node.version, {
|
|
1268
|
-
directDependency: directNames.has(node.name)
|
|
1269
|
-
})
|
|
1270
|
-
};
|
|
1271
|
-
completed += 1;
|
|
1272
|
-
onProgress?.({
|
|
1273
|
-
stage: "metadata_fetch_progress",
|
|
1274
|
-
completed,
|
|
1275
|
-
total: extraction.nodes.length,
|
|
1276
|
-
packageName: node.name
|
|
1277
|
-
});
|
|
1278
|
-
return result;
|
|
1279
|
-
}
|
|
1280
|
-
);
|
|
1281
|
-
onProgress?.({ stage: "metadata_fetch_completed", total: extraction.nodes.length });
|
|
1282
|
-
const metadataByKey = /* @__PURE__ */ new Map();
|
|
1283
|
-
for (const entry of metadataEntries) {
|
|
1284
|
-
metadataByKey.set(entry.key, entry.metadata);
|
|
1285
|
-
}
|
|
1286
|
-
const summary = buildExternalAnalysisSummary(input.repositoryPath, extraction, metadataByKey, config);
|
|
1287
|
-
if (summary.available) {
|
|
1288
|
-
onProgress?.({
|
|
1289
|
-
stage: "summary_built",
|
|
1290
|
-
totalDependencies: summary.metrics.totalDependencies,
|
|
1291
|
-
directDependencies: summary.metrics.directDependencies
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
return summary;
|
|
1295
|
-
} catch (error) {
|
|
1296
|
-
const message = error instanceof Error ? error.message : "unknown";
|
|
1297
|
-
if (message.includes("unsupported_lockfile_format")) {
|
|
1298
|
-
return {
|
|
1299
|
-
targetPath: input.repositoryPath,
|
|
1300
|
-
available: false,
|
|
1301
|
-
reason: "unsupported_lockfile_format"
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1850
|
+
if (collected.fileNames.length === 0) {
|
|
1304
1851
|
return {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1852
|
+
fileNames: discoverSourceFilesByScan(projectRoot),
|
|
1853
|
+
options: createCompilerOptions(collected.rootOptions),
|
|
1854
|
+
tsconfigCount: collected.visitedConfigCount,
|
|
1855
|
+
usedFallbackScan: true
|
|
1308
1856
|
};
|
|
1309
1857
|
}
|
|
1858
|
+
return {
|
|
1859
|
+
fileNames: collected.fileNames,
|
|
1860
|
+
options: createCompilerOptions(collected.rootOptions),
|
|
1861
|
+
tsconfigCount: collected.visitedConfigCount,
|
|
1862
|
+
usedFallbackScan: false
|
|
1863
|
+
};
|
|
1310
1864
|
};
|
|
1311
|
-
var
|
|
1312
|
-
if (
|
|
1313
|
-
return
|
|
1865
|
+
var getSpecifierFromExpression = (expression) => {
|
|
1866
|
+
if (ts.isStringLiteral(expression)) {
|
|
1867
|
+
return expression.text;
|
|
1314
1868
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
return null;
|
|
1869
|
+
if (ts.isNoSubstitutionTemplateLiteral(expression)) {
|
|
1870
|
+
return expression.text;
|
|
1318
1871
|
}
|
|
1319
|
-
return
|
|
1872
|
+
return void 0;
|
|
1320
1873
|
};
|
|
1321
|
-
var
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
if (response.ok) {
|
|
1326
|
-
return await response.json();
|
|
1327
|
-
}
|
|
1328
|
-
if (!shouldRetryStatus(response.status) || attempt === options.retries) {
|
|
1329
|
-
return null;
|
|
1330
|
-
}
|
|
1331
|
-
const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
1332
|
-
const backoffMs = retryAfterMs ?? options.baseDelayMs * 2 ** attempt;
|
|
1333
|
-
await sleep(backoffMs);
|
|
1874
|
+
var hasRuntimeImport = (importDeclaration) => {
|
|
1875
|
+
const importClause = importDeclaration.importClause;
|
|
1876
|
+
if (importClause === void 0) {
|
|
1877
|
+
return true;
|
|
1334
1878
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1338
|
-
var MAX_RETRIES = 3;
|
|
1339
|
-
var RETRY_BASE_DELAY_MS = 500;
|
|
1340
|
-
var round42 = (value) => Number(value.toFixed(4));
|
|
1341
|
-
var parseDate = (iso) => {
|
|
1342
|
-
if (iso === void 0) {
|
|
1343
|
-
return null;
|
|
1879
|
+
if (importClause.isTypeOnly) {
|
|
1880
|
+
return false;
|
|
1344
1881
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1882
|
+
if (importClause.name !== void 0) {
|
|
1883
|
+
return true;
|
|
1884
|
+
}
|
|
1885
|
+
const namedBindings = importClause.namedBindings;
|
|
1886
|
+
if (namedBindings === void 0) {
|
|
1887
|
+
return false;
|
|
1888
|
+
}
|
|
1889
|
+
if (ts.isNamespaceImport(namedBindings)) {
|
|
1890
|
+
return true;
|
|
1891
|
+
}
|
|
1892
|
+
if (namedBindings.elements.length === 0) {
|
|
1893
|
+
return true;
|
|
1894
|
+
}
|
|
1895
|
+
return namedBindings.elements.some((element) => !element.isTypeOnly);
|
|
1347
1896
|
};
|
|
1348
|
-
var
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
const downloads = payload.downloads;
|
|
1360
|
-
if (typeof downloads !== "number" || Number.isNaN(downloads) || downloads < 0) {
|
|
1361
|
-
return null;
|
|
1897
|
+
var extractModuleSpecifiers = (sourceFile) => {
|
|
1898
|
+
const specifiers = /* @__PURE__ */ new Set();
|
|
1899
|
+
const visit = (node) => {
|
|
1900
|
+
if (ts.isImportDeclaration(node)) {
|
|
1901
|
+
if (hasRuntimeImport(node) && node.moduleSpecifier !== void 0) {
|
|
1902
|
+
const specifier = getSpecifierFromExpression(node.moduleSpecifier);
|
|
1903
|
+
if (specifier !== void 0) {
|
|
1904
|
+
specifiers.add(specifier);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return;
|
|
1362
1908
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1909
|
+
if (ts.isExportDeclaration(node)) {
|
|
1910
|
+
if (!node.isTypeOnly && node.moduleSpecifier !== void 0) {
|
|
1911
|
+
const specifier = getSpecifierFromExpression(node.moduleSpecifier);
|
|
1912
|
+
if (specifier !== void 0) {
|
|
1913
|
+
specifiers.add(specifier);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
return;
|
|
1369
1917
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1918
|
+
if (ts.isCallExpression(node)) {
|
|
1919
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length > 0) {
|
|
1920
|
+
const firstArgument = node.arguments[0];
|
|
1921
|
+
if (firstArgument !== void 0) {
|
|
1922
|
+
const specifier = getSpecifierFromExpression(firstArgument);
|
|
1923
|
+
if (specifier !== void 0) {
|
|
1924
|
+
specifiers.add(specifier);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1379
1927
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
if (publishDates.length >= 2) {
|
|
1387
|
-
const totalIntervals = publishDates.length - 1;
|
|
1388
|
-
let sum = 0;
|
|
1389
|
-
for (let i = 1; i < publishDates.length; i += 1) {
|
|
1390
|
-
const current = publishDates[i];
|
|
1391
|
-
const previous = publishDates[i - 1];
|
|
1392
|
-
if (current !== void 0 && previous !== void 0) {
|
|
1393
|
-
sum += current - previous;
|
|
1928
|
+
if (ts.isIdentifier(node.expression) && node.expression.text === "require" && node.arguments.length > 0) {
|
|
1929
|
+
const firstArgument = node.arguments[0];
|
|
1930
|
+
if (firstArgument !== void 0) {
|
|
1931
|
+
const specifier = getSpecifierFromExpression(firstArgument);
|
|
1932
|
+
if (specifier !== void 0) {
|
|
1933
|
+
specifiers.add(specifier);
|
|
1394
1934
|
}
|
|
1395
1935
|
}
|
|
1396
|
-
releaseFrequencyDays = round42(sum / totalIntervals / ONE_DAY_MS);
|
|
1397
1936
|
}
|
|
1398
|
-
const maintainers = payload.maintainers ?? [];
|
|
1399
|
-
const maintainerCount = maintainers.length > 0 ? maintainers.length : null;
|
|
1400
|
-
const weeklyDownloads = context.directDependency ? await this.fetchWeeklyDownloads(name).catch(() => null) : null;
|
|
1401
|
-
const metadata = {
|
|
1402
|
-
name,
|
|
1403
|
-
version: version2,
|
|
1404
|
-
weeklyDownloads,
|
|
1405
|
-
maintainerCount,
|
|
1406
|
-
releaseFrequencyDays,
|
|
1407
|
-
daysSinceLastRelease,
|
|
1408
|
-
repositoryActivity30d: null,
|
|
1409
|
-
busFactor: null
|
|
1410
|
-
};
|
|
1411
|
-
this.cache.set(key, metadata);
|
|
1412
|
-
return metadata;
|
|
1413
|
-
} catch {
|
|
1414
|
-
this.cache.set(key, null);
|
|
1415
|
-
return null;
|
|
1416
1937
|
}
|
|
1417
|
-
|
|
1938
|
+
ts.forEachChild(node, visit);
|
|
1939
|
+
};
|
|
1940
|
+
visit(sourceFile);
|
|
1941
|
+
return [...specifiers];
|
|
1418
1942
|
};
|
|
1419
|
-
var
|
|
1420
|
-
|
|
1421
|
-
|
|
1943
|
+
var parseTypescriptProject = (projectPath, onProgress) => {
|
|
1944
|
+
const projectRoot = isAbsolute(projectPath) ? projectPath : resolve(projectPath);
|
|
1945
|
+
const { fileNames, options, tsconfigCount, usedFallbackScan } = parseTsConfig(projectRoot);
|
|
1946
|
+
onProgress?.({ stage: "config_resolved", tsconfigCount, usedFallbackScan });
|
|
1947
|
+
const sourceFilePaths = fileNames.filter((filePath) => isProjectSourceFile(filePath, projectRoot)).map((filePath) => normalizePath(resolve(filePath)));
|
|
1948
|
+
const uniqueSourceFilePaths = [...new Set(sourceFilePaths)].sort((a, b) => a.localeCompare(b));
|
|
1949
|
+
const sourceFilePathSet = new Set(uniqueSourceFilePaths);
|
|
1950
|
+
onProgress?.({ stage: "files_discovered", totalSourceFiles: uniqueSourceFilePaths.length });
|
|
1951
|
+
const program2 = ts.createProgram({
|
|
1952
|
+
rootNames: uniqueSourceFilePaths,
|
|
1953
|
+
options
|
|
1954
|
+
});
|
|
1955
|
+
onProgress?.({ stage: "program_created", totalSourceFiles: uniqueSourceFilePaths.length });
|
|
1956
|
+
const nodeByAbsolutePath = /* @__PURE__ */ new Map();
|
|
1957
|
+
for (const sourcePath of uniqueSourceFilePaths) {
|
|
1958
|
+
const relativePath = normalizePath(relative(projectRoot, sourcePath));
|
|
1959
|
+
const nodeId = relativePath;
|
|
1960
|
+
nodeByAbsolutePath.set(sourcePath, {
|
|
1961
|
+
id: nodeId,
|
|
1962
|
+
absolutePath: sourcePath,
|
|
1963
|
+
relativePath
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
const resolverCache = /* @__PURE__ */ new Map();
|
|
1967
|
+
const edges = [];
|
|
1968
|
+
for (const [index, sourcePath] of uniqueSourceFilePaths.entries()) {
|
|
1969
|
+
const sourceFile = program2.getSourceFile(sourcePath);
|
|
1970
|
+
if (sourceFile === void 0) {
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
const fromNode = nodeByAbsolutePath.get(sourcePath);
|
|
1974
|
+
if (fromNode === void 0) {
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
const moduleSpecifiers = extractModuleSpecifiers(sourceFile);
|
|
1978
|
+
for (const specifier of moduleSpecifiers) {
|
|
1979
|
+
const cacheKey = `${sourcePath}\0${specifier}`;
|
|
1980
|
+
let resolvedPath = resolverCache.get(cacheKey);
|
|
1981
|
+
if (resolvedPath === void 0 && !resolverCache.has(cacheKey)) {
|
|
1982
|
+
const resolved = ts.resolveModuleName(specifier, sourcePath, options, ts.sys).resolvedModule;
|
|
1983
|
+
if (resolved !== void 0) {
|
|
1984
|
+
resolvedPath = normalizePath(resolve(resolved.resolvedFileName));
|
|
1985
|
+
}
|
|
1986
|
+
resolverCache.set(cacheKey, resolvedPath);
|
|
1987
|
+
}
|
|
1988
|
+
if (resolvedPath === void 0 || !sourceFilePathSet.has(resolvedPath)) {
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
const toNode = nodeByAbsolutePath.get(resolvedPath);
|
|
1992
|
+
if (toNode === void 0) {
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
edges.push({ from: fromNode.id, to: toNode.id });
|
|
1996
|
+
}
|
|
1997
|
+
const processed = index + 1;
|
|
1998
|
+
if (processed === 1 || processed === uniqueSourceFilePaths.length || processed % 50 === 0) {
|
|
1999
|
+
onProgress?.({
|
|
2000
|
+
stage: "file_processed",
|
|
2001
|
+
processed,
|
|
2002
|
+
total: uniqueSourceFilePaths.length,
|
|
2003
|
+
filePath: fromNode.id
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
1422
2006
|
}
|
|
2007
|
+
onProgress?.({ stage: "edges_resolved", totalEdges: edges.length });
|
|
2008
|
+
return {
|
|
2009
|
+
nodes: [...nodeByAbsolutePath.values()],
|
|
2010
|
+
edges
|
|
2011
|
+
};
|
|
1423
2012
|
};
|
|
1424
|
-
var
|
|
1425
|
-
const
|
|
1426
|
-
|
|
2013
|
+
var buildProjectGraphSummary = (input) => {
|
|
2014
|
+
const parsedProject = parseTypescriptProject(input.projectPath, input.onProgress);
|
|
2015
|
+
const graphData = createGraphData(parsedProject.nodes, parsedProject.edges);
|
|
2016
|
+
return createGraphAnalysisSummary(input.projectPath, graphData);
|
|
1427
2017
|
};
|
|
1428
2018
|
|
|
1429
2019
|
// ../git-analyzer/dist/index.js
|
|
@@ -2750,6 +3340,39 @@ program.command("analyze").argument("[path]", "path to the project to analyze").
|
|
|
2750
3340
|
`);
|
|
2751
3341
|
}
|
|
2752
3342
|
);
|
|
3343
|
+
program.command("dependency-risk").argument("<dependency>", "dependency spec to evaluate (for example: react or react@19.0.0)").addOption(
|
|
3344
|
+
new Option(
|
|
3345
|
+
"--log-level <level>",
|
|
3346
|
+
"log verbosity: silent, error, warn, info, debug (logs are written to stderr)"
|
|
3347
|
+
).choices(["silent", "error", "warn", "info", "debug"]).default(parseLogLevel(process.env["CODESENTINEL_LOG_LEVEL"]))
|
|
3348
|
+
).addOption(
|
|
3349
|
+
new Option(
|
|
3350
|
+
"--output <mode>",
|
|
3351
|
+
"output mode: summary (default) or json (full analysis object)"
|
|
3352
|
+
).choices(["summary", "json"]).default("summary")
|
|
3353
|
+
).option("--json", "shortcut for --output json").option("--max-nodes <count>", "maximum dependency nodes to resolve", "250").option("--max-depth <count>", "maximum dependency depth to traverse", "6").action(
|
|
3354
|
+
async (dependency, options) => {
|
|
3355
|
+
const logger = createStderrLogger(options.logLevel);
|
|
3356
|
+
const maxNodes = Number.parseInt(options.maxNodes, 10);
|
|
3357
|
+
const maxDepth = Number.parseInt(options.maxDepth, 10);
|
|
3358
|
+
logger.info(`analyzing dependency candidate: ${dependency}`);
|
|
3359
|
+
const result = await analyzeDependencyCandidateFromRegistry({
|
|
3360
|
+
dependency,
|
|
3361
|
+
maxNodes: Number.isFinite(maxNodes) ? maxNodes : 250,
|
|
3362
|
+
maxDepth: Number.isFinite(maxDepth) ? maxDepth : 6
|
|
3363
|
+
});
|
|
3364
|
+
if (result.available) {
|
|
3365
|
+
logger.info(
|
|
3366
|
+
`dependency analysis completed (${result.dependency.name}@${result.dependency.resolvedVersion})`
|
|
3367
|
+
);
|
|
3368
|
+
} else {
|
|
3369
|
+
logger.warn(`dependency analysis unavailable: ${result.reason}`);
|
|
3370
|
+
}
|
|
3371
|
+
const outputMode = options.json === true ? "json" : options.output;
|
|
3372
|
+
process.stdout.write(`${formatDependencyRiskOutput(result, outputMode)}
|
|
3373
|
+
`);
|
|
3374
|
+
}
|
|
3375
|
+
);
|
|
2753
3376
|
if (process.argv.length <= 2) {
|
|
2754
3377
|
program.outputHelp();
|
|
2755
3378
|
process.exit(0);
|