dependency-radar 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -39
- package/dist/aggregator.js +386 -45
- package/dist/cli.js +399 -78
- package/dist/cta.js +11 -0
- package/dist/report-assets.js +2 -2
- package/dist/report.js +203 -151
- package/dist/runners/npmLs.js +120 -12
- package/dist/runners/npmOutdated.js +34 -18
- package/dist/utils.js +15 -1
- package/package.json +13 -22
package/dist/aggregator.js
CHANGED
|
@@ -69,9 +69,235 @@ function formatProjectDir(projectPath) {
|
|
|
69
69
|
}
|
|
70
70
|
return projectPath;
|
|
71
71
|
}
|
|
72
|
+
function asTrimmedString(value) {
|
|
73
|
+
if (typeof value !== 'string')
|
|
74
|
+
return undefined;
|
|
75
|
+
const trimmed = value.trim();
|
|
76
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
77
|
+
}
|
|
78
|
+
function normalizeStringList(value) {
|
|
79
|
+
if (typeof value === 'string') {
|
|
80
|
+
const single = value.trim();
|
|
81
|
+
return single ? [single] : undefined;
|
|
82
|
+
}
|
|
83
|
+
if (!Array.isArray(value))
|
|
84
|
+
return undefined;
|
|
85
|
+
const items = value
|
|
86
|
+
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
if (items.length === 0)
|
|
89
|
+
return undefined;
|
|
90
|
+
return Array.from(new Set(items));
|
|
91
|
+
}
|
|
92
|
+
function toObjectRecord(value) {
|
|
93
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
94
|
+
return undefined;
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function hasKeys(value) {
|
|
98
|
+
return Boolean(value && Object.keys(value).length > 0);
|
|
99
|
+
}
|
|
100
|
+
function mergeRecordObjects(...sources) {
|
|
101
|
+
const merged = {};
|
|
102
|
+
for (const source of sources) {
|
|
103
|
+
if (!source)
|
|
104
|
+
continue;
|
|
105
|
+
for (const [key, val] of Object.entries(source)) {
|
|
106
|
+
merged[key] = val;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
110
|
+
}
|
|
111
|
+
function normalizeRepository(repository) {
|
|
112
|
+
const direct = asTrimmedString(repository);
|
|
113
|
+
if (direct)
|
|
114
|
+
return direct;
|
|
115
|
+
const asObject = toObjectRecord(repository);
|
|
116
|
+
if (!asObject)
|
|
117
|
+
return undefined;
|
|
118
|
+
const url = asTrimmedString(asObject.url);
|
|
119
|
+
if (url)
|
|
120
|
+
return url;
|
|
121
|
+
const type = asTrimmedString(asObject.type);
|
|
122
|
+
const directory = asTrimmedString(asObject.directory);
|
|
123
|
+
if (type && directory)
|
|
124
|
+
return `${type} (${directory})`;
|
|
125
|
+
return type;
|
|
126
|
+
}
|
|
127
|
+
function extractPackageNameFromSelector(selector) {
|
|
128
|
+
let token = selector.trim();
|
|
129
|
+
if (!token)
|
|
130
|
+
return undefined;
|
|
131
|
+
if (token.includes('>')) {
|
|
132
|
+
const parts = token.split('>').map((part) => part.trim()).filter(Boolean);
|
|
133
|
+
if (parts.length > 0)
|
|
134
|
+
token = parts[parts.length - 1];
|
|
135
|
+
}
|
|
136
|
+
if (token.startsWith('npm:')) {
|
|
137
|
+
token = token.slice(4).trim();
|
|
138
|
+
}
|
|
139
|
+
if (!token)
|
|
140
|
+
return undefined;
|
|
141
|
+
if (token.startsWith('@')) {
|
|
142
|
+
const scopedMatch = token.match(/^@[^/\s]+\/[^@\s]+/);
|
|
143
|
+
return scopedMatch ? scopedMatch[0] : undefined;
|
|
144
|
+
}
|
|
145
|
+
const atIndex = token.indexOf('@');
|
|
146
|
+
const unversioned = atIndex > 0 ? token.slice(0, atIndex) : token;
|
|
147
|
+
const match = unversioned.match(/^[^@\s]+/);
|
|
148
|
+
return match ? match[0] : undefined;
|
|
149
|
+
}
|
|
150
|
+
function collectPolicyPackageNames(entries) {
|
|
151
|
+
if (!entries)
|
|
152
|
+
return undefined;
|
|
153
|
+
const names = new Set();
|
|
154
|
+
for (const selector of Object.keys(entries)) {
|
|
155
|
+
const pkgName = extractPackageNameFromSelector(selector);
|
|
156
|
+
if (pkgName)
|
|
157
|
+
names.add(pkgName);
|
|
158
|
+
}
|
|
159
|
+
if (names.size === 0)
|
|
160
|
+
return undefined;
|
|
161
|
+
return Array.from(names).sort();
|
|
162
|
+
}
|
|
163
|
+
function buildProjectDependencyPolicy(projectPkg, inputPolicy) {
|
|
164
|
+
var _a;
|
|
165
|
+
const projectPkgOverrides = toObjectRecord(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.overrides);
|
|
166
|
+
const projectPkgPnpm = toObjectRecord(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.pnpm);
|
|
167
|
+
const projectPkgPnpmOverrides = toObjectRecord(projectPkgPnpm === null || projectPkgPnpm === void 0 ? void 0 : projectPkgPnpm.overrides);
|
|
168
|
+
const projectPkgResolutions = toObjectRecord(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.resolutions);
|
|
169
|
+
const overrides = mergeRecordObjects(projectPkgOverrides, projectPkgPnpmOverrides, inputPolicy === null || inputPolicy === void 0 ? void 0 : inputPolicy.overrides);
|
|
170
|
+
const resolutions = mergeRecordObjects(projectPkgResolutions, inputPolicy === null || inputPolicy === void 0 ? void 0 : inputPolicy.resolutions);
|
|
171
|
+
const sources = new Set();
|
|
172
|
+
if (hasKeys(projectPkgOverrides))
|
|
173
|
+
sources.add('package.json#overrides');
|
|
174
|
+
if (hasKeys(projectPkgPnpmOverrides))
|
|
175
|
+
sources.add('package.json#pnpm.overrides');
|
|
176
|
+
if (hasKeys(projectPkgResolutions))
|
|
177
|
+
sources.add('package.json#resolutions');
|
|
178
|
+
(_a = inputPolicy === null || inputPolicy === void 0 ? void 0 : inputPolicy.sources) === null || _a === void 0 ? void 0 : _a.forEach((source) => {
|
|
179
|
+
const normalized = source.trim();
|
|
180
|
+
if (normalized)
|
|
181
|
+
sources.add(normalized);
|
|
182
|
+
});
|
|
183
|
+
const policy = {
|
|
184
|
+
...(overrides ? { overrides } : {}),
|
|
185
|
+
...(resolutions ? { resolutions } : {})
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
...(hasKeys(policy.overrides) || hasKeys(policy.resolutions) ? { policy } : {}),
|
|
189
|
+
...(sources.size > 0 ? { sources: Array.from(sources).sort() } : {})
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function buildProjectDependencyPolicySummary(policy, sources) {
|
|
193
|
+
const overrideCount = (policy === null || policy === void 0 ? void 0 : policy.overrides) ? Object.keys(policy.overrides).length : 0;
|
|
194
|
+
const resolutionCount = (policy === null || policy === void 0 ? void 0 : policy.resolutions) ? Object.keys(policy.resolutions).length : 0;
|
|
195
|
+
if (overrideCount === 0 && resolutionCount === 0)
|
|
196
|
+
return undefined;
|
|
197
|
+
const overriddenPackageNames = collectPolicyPackageNames(policy === null || policy === void 0 ? void 0 : policy.overrides);
|
|
198
|
+
const resolvedPackageNames = collectPolicyPackageNames(policy === null || policy === void 0 ? void 0 : policy.resolutions);
|
|
199
|
+
return {
|
|
200
|
+
hasOverrides: overrideCount > 0,
|
|
201
|
+
overrideCount,
|
|
202
|
+
...(overriddenPackageNames ? { overriddenPackageNames } : {}),
|
|
203
|
+
hasResolutions: resolutionCount > 0,
|
|
204
|
+
resolutionCount,
|
|
205
|
+
...(resolvedPackageNames ? { resolvedPackageNames } : {}),
|
|
206
|
+
...(sources && sources.length > 0 ? { sources } : {})
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function buildProjectMetadata(projectPath, projectPkg, inputPolicy) {
|
|
210
|
+
var _a, _b;
|
|
211
|
+
const { policy, sources } = buildProjectDependencyPolicy(projectPkg, inputPolicy);
|
|
212
|
+
const policySummary = buildProjectDependencyPolicySummary(policy, sources);
|
|
213
|
+
const constraints = {
|
|
214
|
+
...(normalizeStringList(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.os) ? { os: normalizeStringList(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.os) } : {}),
|
|
215
|
+
...(normalizeStringList(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.cpu) ? { cpu: normalizeStringList(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.cpu) } : {}),
|
|
216
|
+
...(asTrimmedString((_a = projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.engines) === null || _a === void 0 ? void 0 : _a.node) ? { enginesNode: asTrimmedString((_b = projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.engines) === null || _b === void 0 ? void 0 : _b.node) } : {})
|
|
217
|
+
};
|
|
218
|
+
const hasConstraints = Object.keys(constraints).length > 0;
|
|
219
|
+
return {
|
|
220
|
+
projectDir: formatProjectDir(projectPath),
|
|
221
|
+
...(asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.name) ? { name: asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.name) } : {}),
|
|
222
|
+
...(asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.version) ? { version: asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.version) } : {}),
|
|
223
|
+
...(asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.description) ? { description: asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.description) } : {}),
|
|
224
|
+
...(asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.license) ? { license: asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.license) } : {}),
|
|
225
|
+
...(normalizeStringList(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.keywords) ? { keywords: normalizeStringList(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.keywords) } : {}),
|
|
226
|
+
...(asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.homepage) ? { homepage: asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.homepage) } : {}),
|
|
227
|
+
...(normalizeRepository(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.repository) ? { repository: normalizeRepository(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.repository) } : {}),
|
|
228
|
+
...(hasConstraints ? { constraints } : {}),
|
|
229
|
+
...(policy ? { dependencyPolicy: policy } : {}),
|
|
230
|
+
...(policySummary ? { dependencyPolicySummary: policySummary } : {})
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function getRiskRank(risk) {
|
|
234
|
+
if (risk === 'red')
|
|
235
|
+
return 2;
|
|
236
|
+
if (risk === 'amber')
|
|
237
|
+
return 1;
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
function maxRisk(...risks) {
|
|
241
|
+
let highest = 'green';
|
|
242
|
+
for (const risk of risks) {
|
|
243
|
+
if (getRiskRank(risk) > getRiskRank(highest))
|
|
244
|
+
highest = risk;
|
|
245
|
+
}
|
|
246
|
+
return highest;
|
|
247
|
+
}
|
|
248
|
+
function resolveLicenseRisk(licenseInfo) {
|
|
249
|
+
const declaredOrPrimaryRisk = (0, license_1.pickLicenseRisk)(licenseInfo.licenseIds);
|
|
250
|
+
if (licenseInfo.record.status !== 'mismatch')
|
|
251
|
+
return declaredOrPrimaryRisk;
|
|
252
|
+
const inferredRisk = licenseInfo.record.inferred
|
|
253
|
+
? (0, license_1.pickLicenseRisk)([licenseInfo.record.inferred.spdxId])
|
|
254
|
+
: 'green';
|
|
255
|
+
return maxRisk(declaredOrPrimaryRisk, inferredRisk, 'amber');
|
|
256
|
+
}
|
|
257
|
+
function isWorkspaceLocalVersion(version) {
|
|
258
|
+
const normalized = version.trim().toLowerCase();
|
|
259
|
+
return normalized.startsWith('workspace:') || normalized.startsWith('link:') || normalized.startsWith('file:');
|
|
260
|
+
}
|
|
261
|
+
function isPathWithin(basePath, candidatePath) {
|
|
262
|
+
const normalizedBase = path_1.default.resolve(basePath);
|
|
263
|
+
const normalizedCandidate = path_1.default.resolve(candidatePath);
|
|
264
|
+
return (normalizedCandidate === normalizedBase ||
|
|
265
|
+
normalizedCandidate.startsWith(`${normalizedBase}${path_1.default.sep}`));
|
|
266
|
+
}
|
|
267
|
+
function isWorkspacePackageNode(node, input) {
|
|
268
|
+
var _a, _b, _c;
|
|
269
|
+
if (!input.workspaceEnabled)
|
|
270
|
+
return false;
|
|
271
|
+
if ((_a = input.workspacePackageIds) === null || _a === void 0 ? void 0 : _a.has(node.key))
|
|
272
|
+
return true;
|
|
273
|
+
if (isWorkspaceLocalVersion(node.version))
|
|
274
|
+
return true;
|
|
275
|
+
if ((_b = input.workspaceLocalDependencyNames) === null || _b === void 0 ? void 0 : _b.has(node.name))
|
|
276
|
+
return true;
|
|
277
|
+
if (node.path && input.workspacePackagePaths && input.workspacePackagePaths.size > 0) {
|
|
278
|
+
for (const workspacePath of input.workspacePackagePaths) {
|
|
279
|
+
if (isPathWithin(workspacePath, node.path))
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (((_c = input.workspacePackageNames) === null || _c === void 0 ? void 0 : _c.has(node.name)) && node.depth <= 1) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
72
288
|
async function aggregateData(input) {
|
|
73
289
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
74
290
|
const pkg = input.pkgOverride || (await (0, utils_1.readPackageJson)(input.projectPath));
|
|
291
|
+
let projectPkg = input.projectPackageJson;
|
|
292
|
+
if (!projectPkg) {
|
|
293
|
+
try {
|
|
294
|
+
projectPkg = await (0, utils_1.readPackageJson)(input.projectPath);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
projectPkg = {};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const project = buildProjectMetadata(input.projectPath, projectPkg, input.projectDependencyPolicy);
|
|
75
301
|
// Get git branch
|
|
76
302
|
const gitBranch = await getGitBranch(input.projectPath);
|
|
77
303
|
const nodeMap = buildNodeMap((_a = input.npmLsResult) === null || _a === void 0 ? void 0 : _a.data);
|
|
@@ -88,7 +314,7 @@ async function aggregateData(input) {
|
|
|
88
314
|
const dependencies = {};
|
|
89
315
|
const licenseCache = new Map();
|
|
90
316
|
const nodeEngineRanges = [];
|
|
91
|
-
const nodes = Array.from(nodeMap.values());
|
|
317
|
+
const nodes = Array.from(nodeMap.values()).filter((node) => !isWorkspacePackageNode(node, input));
|
|
92
318
|
let directCount = 0;
|
|
93
319
|
const MAX_TOP_ROOT_PACKAGES = 10; // cap to keep payload size predictable
|
|
94
320
|
const MAX_TOP_PARENT_PACKAGES = 5; // cap for direct parents to keep payload size predictable
|
|
@@ -108,7 +334,7 @@ async function aggregateData(input) {
|
|
|
108
334
|
}
|
|
109
335
|
const vulnerabilities = vulnMap.get(node.name) || emptyVulnSummary();
|
|
110
336
|
const licenseInfo = buildLicenseInfo(licenseSource.license, licenseSource.licenseText);
|
|
111
|
-
const licenseRisk = (
|
|
337
|
+
const licenseRisk = resolveLicenseRisk(licenseInfo);
|
|
112
338
|
// Calculate root causes (direct dependencies that cause this to be installed)
|
|
113
339
|
const rootCauses = findRootCauses(node, nodeMap, pkg);
|
|
114
340
|
const packageInsights = await gatherPackageInsights(node.name, node.version, resolvePaths, packageMetaCache, packageStatCache);
|
|
@@ -118,7 +344,7 @@ async function aggregateData(input) {
|
|
|
118
344
|
const scope = determineScope(node.name, direct, rootCauses, pkg);
|
|
119
345
|
const importUsage = usageResult.summary.get(node.name);
|
|
120
346
|
const runtimeImpact = usageResult.runtimeImpact.get(node.name);
|
|
121
|
-
const introduction = determineIntroduction(direct, rootCauses, runtimeImpact);
|
|
347
|
+
const introduction = determineIntroduction(direct, scope, rootCauses, runtimeImpact);
|
|
122
348
|
const parentIds = Array.from(node.parents).sort();
|
|
123
349
|
const origins = buildOrigins(rootCauses, parentIds, (_e = input.workspaceUsage) === null || _e === void 0 ? void 0 : _e.get(node.name), input.workspaceEnabled, MAX_TOP_ROOT_PACKAGES, MAX_TOP_PARENT_PACKAGES);
|
|
124
350
|
const execution = packageInsights.execution;
|
|
@@ -141,6 +367,8 @@ async function aggregateData(input) {
|
|
|
141
367
|
name: node.name,
|
|
142
368
|
version: node.version,
|
|
143
369
|
...(packageInsights.description ? { description: packageInsights.description } : {}),
|
|
370
|
+
...(typeof packageInsights.fileCount === 'number' ? { fileCount: packageInsights.fileCount } : {}),
|
|
371
|
+
...(packageInsights.hasBin ? { hasBin: true } : {}),
|
|
144
372
|
deprecated: packageInsights.deprecated,
|
|
145
373
|
links: {
|
|
146
374
|
npm: `https://www.npmjs.com/package/${node.name}`,
|
|
@@ -189,15 +417,13 @@ async function aggregateData(input) {
|
|
|
189
417
|
const dependencyCount = nodes.length;
|
|
190
418
|
const transitiveCount = dependencyCount - directCount;
|
|
191
419
|
return {
|
|
192
|
-
schemaVersion: '1.
|
|
420
|
+
schemaVersion: '1.3',
|
|
193
421
|
generatedAt: new Date().toISOString(),
|
|
194
422
|
dependencyRadarVersion,
|
|
195
423
|
git: {
|
|
196
424
|
branch: gitBranch || ''
|
|
197
425
|
},
|
|
198
|
-
project
|
|
199
|
-
projectDir: formatProjectDir(input.projectPath)
|
|
200
|
-
},
|
|
426
|
+
project,
|
|
201
427
|
environment: {
|
|
202
428
|
nodeVersion,
|
|
203
429
|
runtimeVersion,
|
|
@@ -215,7 +441,8 @@ async function aggregateData(input) {
|
|
|
215
441
|
...(input.workspaceType ? { type: input.workspaceType } : {}),
|
|
216
442
|
...(typeof input.workspacePackageCount === 'number'
|
|
217
443
|
? { packageCount: input.workspacePackageCount }
|
|
218
|
-
: {})
|
|
444
|
+
: {}),
|
|
445
|
+
...(input.workspacePackages ? { workspacePackages: input.workspacePackages } : {})
|
|
219
446
|
},
|
|
220
447
|
summary: {
|
|
221
448
|
dependencyCount,
|
|
@@ -662,12 +889,14 @@ function buildUsageSummary(graph, projectPath) {
|
|
|
662
889
|
file,
|
|
663
890
|
count,
|
|
664
891
|
depth: file.split('/').length,
|
|
665
|
-
|
|
892
|
+
category: classifyFileCategory(file)
|
|
666
893
|
}));
|
|
667
|
-
// Rank: prefer non-
|
|
894
|
+
// Rank: prefer non-testing files, then higher import counts, then closer to root.
|
|
668
895
|
entries.sort((a, b) => {
|
|
669
|
-
|
|
670
|
-
|
|
896
|
+
const aIsTest = a.category === 'testing';
|
|
897
|
+
const bIsTest = b.category === 'testing';
|
|
898
|
+
if (aIsTest !== bIsTest)
|
|
899
|
+
return aIsTest ? 1 : -1;
|
|
671
900
|
if (b.count !== a.count)
|
|
672
901
|
return b.count - a.count;
|
|
673
902
|
if (a.depth !== b.depth)
|
|
@@ -678,14 +907,15 @@ function buildUsageSummary(graph, projectPath) {
|
|
|
678
907
|
fileCount: fileMap.size,
|
|
679
908
|
topFiles: entries.slice(0, 5).map((entry) => entry.file)
|
|
680
909
|
});
|
|
681
|
-
runtimeImpact.set(dep, determineRuntimeImpactFromFiles(
|
|
910
|
+
runtimeImpact.set(dep, determineRuntimeImpactFromFiles(entries));
|
|
682
911
|
}
|
|
683
912
|
return { summary, runtimeImpact };
|
|
684
913
|
}
|
|
685
914
|
function isDirectDependency(name, pkg) {
|
|
686
915
|
return Boolean((pkg.dependencies && pkg.dependencies[name]) ||
|
|
687
916
|
(pkg.devDependencies && pkg.devDependencies[name]) ||
|
|
688
|
-
(pkg.optionalDependencies && pkg.optionalDependencies[name])
|
|
917
|
+
(pkg.optionalDependencies && pkg.optionalDependencies[name]) ||
|
|
918
|
+
(pkg.peerDependencies && pkg.peerDependencies[name]));
|
|
689
919
|
}
|
|
690
920
|
function directScopeFromPackage(name, pkg) {
|
|
691
921
|
if (pkg.dependencies && pkg.dependencies[name])
|
|
@@ -756,35 +986,65 @@ function buildOrigins(rootCauses, parentIds, workspaceList, workspaceEnabled, ma
|
|
|
756
986
|
return origins;
|
|
757
987
|
}
|
|
758
988
|
function isTestFile(file) {
|
|
759
|
-
return /(^|\/)(__tests__|__mocks__|test|tests)(\/|$)/.test(file) ||
|
|
989
|
+
return (/(^|\/)(__tests__|__mocks__|test|tests|testing|e2e|cypress|playwright|__snapshots__)(\/|$)/.test(file) ||
|
|
990
|
+
/\.(test|spec|e2e)\./.test(file) ||
|
|
991
|
+
/(^|\/)(jest|vitest|playwright|cypress)\.config\./.test(file) ||
|
|
992
|
+
/(^|\/)\.(jest|vitest|playwright|cypress)(rc|\.config)?(\..*)?$/.test(file));
|
|
760
993
|
}
|
|
761
994
|
function isToolingFile(file) {
|
|
762
|
-
return /(^|\/)(eslint|prettier|stylelint|commitlint|lint-staged|husky)[^\/]*\./.test(file)
|
|
995
|
+
return (/(^|\/)(eslint|prettier|stylelint|commitlint|lint-staged|husky|renovate|semantic-release|release-it|lefthook|dependabot)[^\/]*\./.test(file) ||
|
|
996
|
+
/(^|\/)\.(eslint|eslintrc|prettier|prettierrc|stylelint|stylelintrc|commitlint|commitlintrc|lint-staged|lintstagedrc|husky|huskyrc|renovate|semantic-release|release-it|lefthook|dependabot)(rc|\.config)?(\..*)?$/.test(file));
|
|
763
997
|
}
|
|
764
998
|
function isBuildFile(file) {
|
|
765
|
-
return /(^|\/)(webpack|rollup|vite|tsconfig|babel|swc|esbuild|parcel|gulpfile|gruntfile|postcss|tailwind)[^\/]*\./.test(file)
|
|
999
|
+
return (/(^|\/)(webpack|rollup|vite|tsconfig|babel|swc|esbuild|parcel|gulpfile|gruntfile|postcss|tailwind|storybook|rspack|turbo|nx|metro)[^\/]*\./.test(file) ||
|
|
1000
|
+
/(^|\/)scripts\/(build|bundle|compile|release|deploy)(\/|\.|$)/.test(file) ||
|
|
1001
|
+
/(^|\/)\.(webpack|rollup|vite|tsconfig|babel|babelrc|swc|swcrc|esbuild|parcel|postcss|postcssrc|tailwind|storybook|rspack|turbo|nx|metro)(rc|\.config)?(\..*)?$/.test(file));
|
|
1002
|
+
}
|
|
1003
|
+
function classifyFileCategory(file) {
|
|
1004
|
+
if (isTestFile(file))
|
|
1005
|
+
return 'testing';
|
|
1006
|
+
if (isToolingFile(file))
|
|
1007
|
+
return 'tooling';
|
|
1008
|
+
if (isBuildFile(file))
|
|
1009
|
+
return 'build';
|
|
1010
|
+
return 'runtime';
|
|
766
1011
|
}
|
|
767
1012
|
function determineRuntimeImpactFromFiles(files) {
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
if (categories.size === 0)
|
|
1013
|
+
const weights = {
|
|
1014
|
+
runtime: 0,
|
|
1015
|
+
build: 0,
|
|
1016
|
+
testing: 0,
|
|
1017
|
+
tooling: 0
|
|
1018
|
+
};
|
|
1019
|
+
let total = 0;
|
|
1020
|
+
for (const entry of files) {
|
|
1021
|
+
const category = classifyFileCategory(entry.file);
|
|
1022
|
+
const weight = Number.isFinite(entry.count) && entry.count > 0 ? entry.count : 1;
|
|
1023
|
+
weights[category] += weight;
|
|
1024
|
+
total += weight;
|
|
1025
|
+
}
|
|
1026
|
+
if (total <= 0)
|
|
784
1027
|
return 'runtime';
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1028
|
+
const ranked = Object.entries(weights)
|
|
1029
|
+
.filter(([, weight]) => weight > 0)
|
|
1030
|
+
.sort((a, b) => b[1] - a[1]);
|
|
1031
|
+
if (ranked.length === 0)
|
|
1032
|
+
return 'runtime';
|
|
1033
|
+
if (ranked.length === 1)
|
|
1034
|
+
return ranked[0][0];
|
|
1035
|
+
const [top, second] = ranked;
|
|
1036
|
+
const topRatio = top[1] / total;
|
|
1037
|
+
const secondRatio = second ? second[1] / total : 0;
|
|
1038
|
+
// Strong dominance: classify as a single category instead of defaulting to mixed.
|
|
1039
|
+
if (topRatio >= 0.7)
|
|
1040
|
+
return top[0];
|
|
1041
|
+
// Runtime is common; tolerate a small amount of non-runtime usage before calling it mixed.
|
|
1042
|
+
if (top[0] === 'runtime' && topRatio >= 0.6 && secondRatio <= 0.25)
|
|
1043
|
+
return 'runtime';
|
|
1044
|
+
// Testing-heavy dependencies often leak a small runtime footprint (helpers in fixtures).
|
|
1045
|
+
if (top[0] === 'testing' && topRatio >= 0.6 && secondRatio <= 0.3)
|
|
1046
|
+
return 'testing';
|
|
1047
|
+
return 'mixed';
|
|
788
1048
|
}
|
|
789
1049
|
function buildLicenseInfo(declaredRaw, licenseText) {
|
|
790
1050
|
const declaredValue = typeof declaredRaw === 'string' ? declaredRaw.trim() : '';
|
|
@@ -880,12 +1140,18 @@ function isFrameworkPackage(name) {
|
|
|
880
1140
|
return FRAMEWORK_PACKAGES.has(name);
|
|
881
1141
|
}
|
|
882
1142
|
// Heuristic-only classification for why a dependency exists. Kept deterministic and bounded.
|
|
883
|
-
function determineIntroduction(direct, rootCauses, runtimeImpact) {
|
|
1143
|
+
function determineIntroduction(direct, scope, rootCauses, runtimeImpact) {
|
|
884
1144
|
const rootNames = rootCauses.map((root) => root.name);
|
|
885
1145
|
if (direct)
|
|
886
1146
|
return 'direct';
|
|
887
1147
|
if (runtimeImpact === 'testing')
|
|
888
1148
|
return 'testing';
|
|
1149
|
+
if (runtimeImpact === 'tooling' || runtimeImpact === 'build')
|
|
1150
|
+
return 'tooling';
|
|
1151
|
+
if (scope === 'dev')
|
|
1152
|
+
return 'tooling';
|
|
1153
|
+
if (scope === 'peer' && runtimeImpact !== 'runtime')
|
|
1154
|
+
return 'tooling';
|
|
889
1155
|
if (rootNames.length > 0 && rootNames.every((root) => isToolingPackage(root)))
|
|
890
1156
|
return 'tooling';
|
|
891
1157
|
if (rootNames.some((root) => isFrameworkPackage(root)))
|
|
@@ -894,23 +1160,61 @@ function determineIntroduction(direct, rootCauses, runtimeImpact) {
|
|
|
894
1160
|
return 'transitive';
|
|
895
1161
|
return 'unknown';
|
|
896
1162
|
}
|
|
897
|
-
|
|
1163
|
+
function isPermissiveNodeEngineRange(range) {
|
|
1164
|
+
const compact = range.trim().toLowerCase().replace(/\s+/g, '');
|
|
1165
|
+
return compact === '*' || compact === 'x' || compact === '>=0' || compact === '>=0.0' || compact === '>=0.0.0';
|
|
1166
|
+
}
|
|
1167
|
+
function hasNodeEngineUpgradeBlocker(nodeEngine) {
|
|
1168
|
+
if (!nodeEngine || !nodeEngine.trim())
|
|
1169
|
+
return false;
|
|
1170
|
+
if (isPermissiveNodeEngineRange(nodeEngine))
|
|
1171
|
+
return false;
|
|
1172
|
+
const clauses = nodeEngine.split('||').map((clause) => clause.trim()).filter(Boolean);
|
|
1173
|
+
if (clauses.length > 0 && clauses.every((clause) => isPermissiveNodeEngineRange(clause))) {
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
const minMajor = parseMinMajorFromRange(nodeEngine);
|
|
1177
|
+
if (minMajor !== undefined) {
|
|
1178
|
+
if (minMajor > 0)
|
|
1179
|
+
return true;
|
|
1180
|
+
return /<\s*\d/.test(nodeEngine);
|
|
1181
|
+
}
|
|
1182
|
+
if (/<\s*\d/.test(nodeEngine))
|
|
1183
|
+
return true;
|
|
1184
|
+
const majors = Array.from(nodeEngine.matchAll(/v?(\d+)/g))
|
|
1185
|
+
.map((match) => Number.parseInt(match[1], 10))
|
|
1186
|
+
.filter((major) => Number.isFinite(major));
|
|
1187
|
+
return majors.some((major) => major > 0);
|
|
1188
|
+
}
|
|
1189
|
+
function hasInstallScriptUpgradeBlocker(execution) {
|
|
1190
|
+
var _a;
|
|
1191
|
+
const hooks = (_a = execution === null || execution === void 0 ? void 0 : execution.scripts) === null || _a === void 0 ? void 0 : _a.hooks;
|
|
1192
|
+
if (!hooks || hooks.length === 0)
|
|
1193
|
+
return false;
|
|
1194
|
+
return hooks.includes('preinstall') || hooks.includes('install') || hooks.includes('postinstall');
|
|
1195
|
+
}
|
|
898
1196
|
function buildUpgradeBlock(insights) {
|
|
899
1197
|
var _a;
|
|
900
1198
|
const blockers = [];
|
|
901
|
-
|
|
1199
|
+
const hasNodeEngineBlocker = hasNodeEngineUpgradeBlocker(insights.nodeEngine);
|
|
1200
|
+
const hasNativeBindingsBlocker = Boolean((_a = insights.execution) === null || _a === void 0 ? void 0 : _a.native);
|
|
1201
|
+
const hasInstallScriptBlocker = hasInstallScriptUpgradeBlocker(insights.execution);
|
|
1202
|
+
if (hasNodeEngineBlocker)
|
|
902
1203
|
blockers.push('nodeEngine');
|
|
903
|
-
if (
|
|
1204
|
+
if (insights.requiredPeerDependencies > 0)
|
|
904
1205
|
blockers.push('peerDependency');
|
|
905
|
-
if (
|
|
1206
|
+
if (hasNativeBindingsBlocker)
|
|
906
1207
|
blockers.push('nativeBindings');
|
|
1208
|
+
if (hasInstallScriptBlocker)
|
|
1209
|
+
blockers.push('installScripts');
|
|
907
1210
|
if (insights.deprecated)
|
|
908
1211
|
blockers.push('deprecated');
|
|
909
1212
|
if (blockers.length === 0)
|
|
910
1213
|
return undefined;
|
|
1214
|
+
const blocksNodeMajor = hasNodeEngineBlocker || hasNativeBindingsBlocker || hasInstallScriptBlocker;
|
|
911
1215
|
return {
|
|
912
|
-
|
|
913
|
-
|
|
1216
|
+
blockers,
|
|
1217
|
+
...(blocksNodeMajor ? { blocksNodeMajor: true } : {})
|
|
914
1218
|
};
|
|
915
1219
|
}
|
|
916
1220
|
async function gatherPackageInsights(name, version, resolvePaths, metaCache, statCache) {
|
|
@@ -920,6 +1224,8 @@ async function gatherPackageInsights(name, version, resolvePaths, metaCache, sta
|
|
|
920
1224
|
return {
|
|
921
1225
|
deprecated: false,
|
|
922
1226
|
nodeEngine: null,
|
|
1227
|
+
requiredPeerDependencies: 0,
|
|
1228
|
+
hasBin: false,
|
|
923
1229
|
declaredDependencies: { dep: {}, dev: {}, peer: {}, opt: {} },
|
|
924
1230
|
tsTypes: 'unknown'
|
|
925
1231
|
};
|
|
@@ -927,18 +1233,21 @@ async function gatherPackageInsights(name, version, resolvePaths, metaCache, sta
|
|
|
927
1233
|
const pkg = (meta === null || meta === void 0 ? void 0 : meta.pkg) || {};
|
|
928
1234
|
const dir = meta === null || meta === void 0 ? void 0 : meta.dir;
|
|
929
1235
|
const stats = dir ? await calculatePackageStats(dir, statCache) : undefined;
|
|
1236
|
+
const peerDependencies = normalizeDeclaredDeps(pkg.peerDependencies);
|
|
930
1237
|
const declaredDependencies = {
|
|
931
1238
|
dep: normalizeDeclaredDeps(pkg.dependencies),
|
|
932
1239
|
dev: normalizeDeclaredDeps(pkg.devDependencies),
|
|
933
|
-
peer:
|
|
1240
|
+
peer: peerDependencies,
|
|
934
1241
|
opt: normalizeDeclaredDeps(pkg.optionalDependencies)
|
|
935
1242
|
};
|
|
1243
|
+
const requiredPeerDependencies = countRequiredPeerDependencies(peerDependencies, pkg.peerDependenciesMeta);
|
|
936
1244
|
const scripts = pkg.scripts || {};
|
|
937
1245
|
const deprecated = Boolean(pkg.deprecated);
|
|
938
1246
|
const nodeEngine = typeof ((_a = pkg.engines) === null || _a === void 0 ? void 0 : _a.node) === 'string' ? pkg.engines.node : null;
|
|
939
1247
|
const description = typeof pkg.description === 'string' && pkg.description.trim()
|
|
940
1248
|
? pkg.description.trim()
|
|
941
1249
|
: undefined;
|
|
1250
|
+
const hasBin = hasPackageBin(pkg.bin);
|
|
942
1251
|
const hasDefinitelyTyped = await hasDefinitelyTypedPackage(name, resolvePaths, metaCache);
|
|
943
1252
|
const tsTypes = determineTypes(pkg, (stats === null || stats === void 0 ? void 0 : stats.hasDts) || false, hasDefinitelyTyped);
|
|
944
1253
|
const links = extractPackageLinks(pkg);
|
|
@@ -946,7 +1255,10 @@ async function gatherPackageInsights(name, version, resolvePaths, metaCache, sta
|
|
|
946
1255
|
return {
|
|
947
1256
|
deprecated,
|
|
948
1257
|
nodeEngine,
|
|
1258
|
+
requiredPeerDependencies,
|
|
949
1259
|
description,
|
|
1260
|
+
...(typeof (stats === null || stats === void 0 ? void 0 : stats.fileCount) === 'number' ? { fileCount: stats.fileCount } : {}),
|
|
1261
|
+
hasBin,
|
|
950
1262
|
declaredDependencies,
|
|
951
1263
|
links,
|
|
952
1264
|
execution,
|
|
@@ -966,6 +1278,30 @@ function normalizeDeclaredDeps(source) {
|
|
|
966
1278
|
}
|
|
967
1279
|
return out;
|
|
968
1280
|
}
|
|
1281
|
+
function countRequiredPeerDependencies(peerDependencies, peerDependenciesMeta) {
|
|
1282
|
+
if (!peerDependencies || Object.keys(peerDependencies).length === 0)
|
|
1283
|
+
return 0;
|
|
1284
|
+
const meta = peerDependenciesMeta && typeof peerDependenciesMeta === 'object'
|
|
1285
|
+
? peerDependenciesMeta
|
|
1286
|
+
: {};
|
|
1287
|
+
let count = 0;
|
|
1288
|
+
for (const peerName of Object.keys(peerDependencies)) {
|
|
1289
|
+
const peerMeta = meta[peerName];
|
|
1290
|
+
const optional = Boolean(peerMeta &&
|
|
1291
|
+
typeof peerMeta === 'object' &&
|
|
1292
|
+
peerMeta.optional === true);
|
|
1293
|
+
if (!optional)
|
|
1294
|
+
count += 1;
|
|
1295
|
+
}
|
|
1296
|
+
return count;
|
|
1297
|
+
}
|
|
1298
|
+
function hasPackageBin(binField) {
|
|
1299
|
+
if (typeof binField === 'string')
|
|
1300
|
+
return binField.trim().length > 0;
|
|
1301
|
+
if (!binField || typeof binField !== 'object')
|
|
1302
|
+
return false;
|
|
1303
|
+
return Object.values(binField).some((value) => typeof value === 'string' && value.trim().length > 0);
|
|
1304
|
+
}
|
|
969
1305
|
async function loadPackageMeta(name, resolvePaths, cache, version) {
|
|
970
1306
|
const cacheKey = version ? `${name}@${version}` : name;
|
|
971
1307
|
if (cache.has(cacheKey))
|
|
@@ -1010,6 +1346,7 @@ async function calculatePackageStats(dir, cache) {
|
|
|
1010
1346
|
let hasDts = false;
|
|
1011
1347
|
let hasNativeBinary = false;
|
|
1012
1348
|
let hasBindingGyp = false;
|
|
1349
|
+
let fileCount = 0;
|
|
1013
1350
|
async function walk(current) {
|
|
1014
1351
|
const entries = await promises_1.default.readdir(current, { withFileTypes: true });
|
|
1015
1352
|
for (const entry of entries) {
|
|
@@ -1017,9 +1354,13 @@ async function calculatePackageStats(dir, cache) {
|
|
|
1017
1354
|
if (entry.isSymbolicLink())
|
|
1018
1355
|
continue;
|
|
1019
1356
|
if (entry.isDirectory()) {
|
|
1357
|
+
// Ignore nested dependency stores to keep package-level stats bounded and comparable.
|
|
1358
|
+
if (entry.name === 'node_modules' || entry.name === '.git')
|
|
1359
|
+
continue;
|
|
1020
1360
|
await walk(full);
|
|
1021
1361
|
}
|
|
1022
1362
|
else if (entry.isFile()) {
|
|
1363
|
+
fileCount += 1;
|
|
1023
1364
|
if (entry.name.endsWith('.d.ts'))
|
|
1024
1365
|
hasDts = true;
|
|
1025
1366
|
if (entry.name.endsWith('.node'))
|
|
@@ -1035,7 +1376,7 @@ async function calculatePackageStats(dir, cache) {
|
|
|
1035
1376
|
catch (err) {
|
|
1036
1377
|
// best-effort; ignore inaccessible paths
|
|
1037
1378
|
}
|
|
1038
|
-
const result = { hasDts, hasNativeBinary, hasBindingGyp };
|
|
1379
|
+
const result = { hasDts, hasNativeBinary, hasBindingGyp, fileCount };
|
|
1039
1380
|
cache.set(dir, result);
|
|
1040
1381
|
return result;
|
|
1041
1382
|
}
|