dependency-radar 0.5.0 → 0.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 +214 -119
- package/dist/cli.js +402 -50
- package/dist/failOn.js +177 -0
- package/dist/report-assets.js +2 -2
- package/dist/report.js +70 -3
- package/dist/runners/importGraphRunner.js +28 -5
- package/dist/runners/lockfileGraph.js +81 -25
- package/dist/runners/lockfileParsers.js +434 -0
- package/dist/runners/npmAudit.js +28 -11
- package/dist/runners/npmLs.js +58 -18
- package/dist/runners/npmOutdated.js +37 -16
- package/dist/utils.js +36 -1
- package/package.json +4 -8
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseYamlLike = parseYamlLike;
|
|
4
|
+
exports.parseYarnV1Lockfile = parseYarnV1Lockfile;
|
|
5
|
+
exports.splitSelectorList = splitSelectorList;
|
|
6
|
+
/**
|
|
7
|
+
* Parse a YAML-like string into a plain object supporting only the subset used by lockfiles.
|
|
8
|
+
*
|
|
9
|
+
* The function is tolerant of comments and indentation but intentionally limits supported YAML
|
|
10
|
+
* features; on malformed input or when the top-level result is not a mapping, it returns an empty object.
|
|
11
|
+
*
|
|
12
|
+
* @returns A plain object representing the parsed mapping, or an empty object if parsing fails or no top-level mapping is present.
|
|
13
|
+
*/
|
|
14
|
+
function parseYamlLike(raw) {
|
|
15
|
+
var _a, _b;
|
|
16
|
+
try {
|
|
17
|
+
const lines = [];
|
|
18
|
+
for (const rawLine of raw.split(/\r?\n/)) {
|
|
19
|
+
const noComment = stripYamlInlineComment(rawLine).replace(/\s+$/, '');
|
|
20
|
+
if (!noComment.trim())
|
|
21
|
+
continue;
|
|
22
|
+
const indent = (_b = (_a = noComment.match(/^(\s*)/)) === null || _a === void 0 ? void 0 : _a[1].length) !== null && _b !== void 0 ? _b : 0;
|
|
23
|
+
lines.push({
|
|
24
|
+
indent,
|
|
25
|
+
content: noComment.trim()
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
let index = 0;
|
|
29
|
+
/**
|
|
30
|
+
* Parse either a mapping or sequence node at the current cursor position.
|
|
31
|
+
*/
|
|
32
|
+
const parseNode = (indentLevel) => {
|
|
33
|
+
if (index >= lines.length)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (lines[index].indent < indentLevel)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (lines[index].indent === indentLevel && lines[index].content.startsWith('- ')) {
|
|
38
|
+
return parseSequence(indentLevel);
|
|
39
|
+
}
|
|
40
|
+
return parseMapping(indentLevel);
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Parse an indentation-scoped YAML mapping.
|
|
44
|
+
*/
|
|
45
|
+
const parseMapping = (indentLevel) => {
|
|
46
|
+
const out = {};
|
|
47
|
+
while (index < lines.length) {
|
|
48
|
+
const line = lines[index];
|
|
49
|
+
if (line.indent < indentLevel)
|
|
50
|
+
break;
|
|
51
|
+
if (line.indent > indentLevel) {
|
|
52
|
+
index += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (line.content.startsWith('- '))
|
|
56
|
+
break;
|
|
57
|
+
const colonIndex = findYamlMapSeparator(line.content);
|
|
58
|
+
if (colonIndex <= 0) {
|
|
59
|
+
index += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const key = unwrapOuterQuotes(line.content.slice(0, colonIndex));
|
|
63
|
+
const valueToken = line.content.slice(colonIndex + 1).trim();
|
|
64
|
+
index += 1;
|
|
65
|
+
if (valueToken) {
|
|
66
|
+
out[key] = parseYamlScalar(valueToken);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (index < lines.length && lines[index].indent > indentLevel) {
|
|
70
|
+
out[key] = parseNode(lines[index].indent);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
out[key] = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Parse an indentation-scoped YAML sequence.
|
|
80
|
+
*/
|
|
81
|
+
const parseSequence = (indentLevel) => {
|
|
82
|
+
const values = [];
|
|
83
|
+
while (index < lines.length) {
|
|
84
|
+
const line = lines[index];
|
|
85
|
+
if (line.indent < indentLevel)
|
|
86
|
+
break;
|
|
87
|
+
if (line.indent !== indentLevel || !line.content.startsWith('- '))
|
|
88
|
+
break;
|
|
89
|
+
const valueToken = line.content.slice(2).trim();
|
|
90
|
+
index += 1;
|
|
91
|
+
if (valueToken) {
|
|
92
|
+
values.push(parseYamlScalar(valueToken));
|
|
93
|
+
if (index < lines.length && lines[index].indent > indentLevel) {
|
|
94
|
+
parseNode(lines[index].indent);
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (index < lines.length && lines[index].indent > indentLevel) {
|
|
99
|
+
values.push(parseNode(lines[index].indent));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
values.push(null);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return values;
|
|
106
|
+
};
|
|
107
|
+
const parsed = parseNode(0);
|
|
108
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
return parsed;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Parses Yarn v1 lockfile content into selector-to-entry mappings.
|
|
119
|
+
*
|
|
120
|
+
* @param raw - The raw text content of a Yarn v1 lockfile
|
|
121
|
+
* @returns A Map from selector string to `YarnLockEntry`, or `undefined` if no entries could be parsed
|
|
122
|
+
*/
|
|
123
|
+
function parseYarnV1Lockfile(raw) {
|
|
124
|
+
var _a, _b;
|
|
125
|
+
try {
|
|
126
|
+
const map = new Map();
|
|
127
|
+
const lines = raw.split(/\r?\n/);
|
|
128
|
+
let currentSelectors = [];
|
|
129
|
+
let currentEntry;
|
|
130
|
+
let currentSection;
|
|
131
|
+
// Persist the currently parsed entry into every selector alias that points to it.
|
|
132
|
+
const flushEntry = () => {
|
|
133
|
+
if (!currentEntry || currentSelectors.length === 0)
|
|
134
|
+
return;
|
|
135
|
+
const entry = {};
|
|
136
|
+
if (currentEntry.version)
|
|
137
|
+
entry.version = currentEntry.version;
|
|
138
|
+
if (currentEntry.dependencies && Object.keys(currentEntry.dependencies).length > 0) {
|
|
139
|
+
entry.dependencies = { ...currentEntry.dependencies };
|
|
140
|
+
}
|
|
141
|
+
if (currentEntry.optionalDependencies && Object.keys(currentEntry.optionalDependencies).length > 0) {
|
|
142
|
+
entry.optionalDependencies = { ...currentEntry.optionalDependencies };
|
|
143
|
+
}
|
|
144
|
+
for (const selector of currentSelectors) {
|
|
145
|
+
if (!map.has(selector)) {
|
|
146
|
+
map.set(selector, entry);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
for (const rawLine of lines) {
|
|
151
|
+
const line = rawLine.replace(/\s+$/, '');
|
|
152
|
+
const trimmed = line.trim();
|
|
153
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
154
|
+
continue;
|
|
155
|
+
const indent = (_b = (_a = line.match(/^(\s*)/)) === null || _a === void 0 ? void 0 : _a[1].length) !== null && _b !== void 0 ? _b : 0;
|
|
156
|
+
if (indent === 0 && trimmed.endsWith(':')) {
|
|
157
|
+
flushEntry();
|
|
158
|
+
currentSelectors = splitSelectorList(trimmed.slice(0, -1));
|
|
159
|
+
currentEntry = {};
|
|
160
|
+
currentSection = undefined;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!currentEntry)
|
|
164
|
+
continue;
|
|
165
|
+
if (indent === 2) {
|
|
166
|
+
if (trimmed === 'dependencies:') {
|
|
167
|
+
currentSection = 'dependencies';
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (trimmed === 'optionalDependencies:') {
|
|
171
|
+
currentSection = 'optionalDependencies';
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
currentSection = undefined;
|
|
175
|
+
const pair = parseYarnTokenPair(trimmed);
|
|
176
|
+
if (!pair)
|
|
177
|
+
continue;
|
|
178
|
+
if (pair[0] === 'version' && pair[1]) {
|
|
179
|
+
currentEntry.version = pair[1];
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (indent >= 4 && currentSection) {
|
|
184
|
+
const pair = parseYarnTokenPair(trimmed);
|
|
185
|
+
if (!pair)
|
|
186
|
+
continue;
|
|
187
|
+
const [name, spec] = pair;
|
|
188
|
+
if (!name || !spec)
|
|
189
|
+
continue;
|
|
190
|
+
if (currentSection === 'dependencies') {
|
|
191
|
+
if (!currentEntry.dependencies)
|
|
192
|
+
currentEntry.dependencies = {};
|
|
193
|
+
currentEntry.dependencies[name] = spec;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
if (!currentEntry.optionalDependencies)
|
|
197
|
+
currentEntry.optionalDependencies = {};
|
|
198
|
+
currentEntry.optionalDependencies[name] = spec;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
flushEntry();
|
|
203
|
+
return map.size > 0 ? map : undefined;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Split a comma-separated selector list into individual selector strings, respecting quotes and escapes.
|
|
211
|
+
*
|
|
212
|
+
* Handles quoted tokens (single and double quotes), preserves escaped characters inside double-quoted tokens,
|
|
213
|
+
* trims whitespace and unwraps optional outer quotes from each selector, and ignores empty tokens.
|
|
214
|
+
*
|
|
215
|
+
* @param selectorKey - The raw selector list (e.g. `"a@1", b@2`) to split and normalize
|
|
216
|
+
* @returns An array of normalized selector strings in order of appearance
|
|
217
|
+
*/
|
|
218
|
+
function splitSelectorList(selectorKey) {
|
|
219
|
+
const out = tokenizeSelectorParts(selectorKey.trim())
|
|
220
|
+
.map(normalizeSelectorToken)
|
|
221
|
+
.filter(Boolean);
|
|
222
|
+
// Yarn Berry often stores the *entire* selector list as one quoted scalar.
|
|
223
|
+
// If that happens, split the unwrapped scalar again to recover aliases.
|
|
224
|
+
if (out.length === 1 && out[0].includes(',')) {
|
|
225
|
+
return tokenizeSelectorParts(out[0])
|
|
226
|
+
.map(normalizeSelectorToken)
|
|
227
|
+
.filter(Boolean);
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Tokenize a selector list by commas that are outside quote scopes.
|
|
233
|
+
*
|
|
234
|
+
* @param value - Raw selector list text.
|
|
235
|
+
* @returns Raw selector tokens in source order.
|
|
236
|
+
*/
|
|
237
|
+
function tokenizeSelectorParts(value) {
|
|
238
|
+
const out = [];
|
|
239
|
+
let current = '';
|
|
240
|
+
let inSingle = false;
|
|
241
|
+
let inDouble = false;
|
|
242
|
+
let escaped = false;
|
|
243
|
+
for (const ch of value) {
|
|
244
|
+
if (inDouble && ch === '\\' && !escaped) {
|
|
245
|
+
escaped = true;
|
|
246
|
+
current += ch;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (ch === "'" && !inDouble) {
|
|
250
|
+
inSingle = !inSingle;
|
|
251
|
+
current += ch;
|
|
252
|
+
escaped = false;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (ch === '"' && !inSingle && !escaped) {
|
|
256
|
+
inDouble = !inDouble;
|
|
257
|
+
current += ch;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (ch === ',' && !inSingle && !inDouble) {
|
|
261
|
+
out.push(current);
|
|
262
|
+
current = '';
|
|
263
|
+
escaped = false;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
current += ch;
|
|
267
|
+
escaped = false;
|
|
268
|
+
}
|
|
269
|
+
out.push(current);
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Normalize a selector token by trimming surrounding whitespace and removing optional outer quotes.
|
|
274
|
+
*
|
|
275
|
+
* @param value - The selector token to normalize
|
|
276
|
+
* @returns The normalized selector string without outer quotes
|
|
277
|
+
*/
|
|
278
|
+
function normalizeSelectorToken(value) {
|
|
279
|
+
return unwrapOuterQuotes(value.trim());
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Parse a single Yarn v1 lockfile token pair from a line containing a name and a spec.
|
|
283
|
+
*
|
|
284
|
+
* @param value - The line to parse, containing a name token followed by a spec token; tokens may be quoted.
|
|
285
|
+
* @returns A two-element tuple `[name, spec]` with outer quotes removed, or `undefined` if the line does not contain a valid pair.
|
|
286
|
+
*/
|
|
287
|
+
function parseYarnTokenPair(value) {
|
|
288
|
+
const first = readQuotedOrBareToken(value, 0);
|
|
289
|
+
if (!first)
|
|
290
|
+
return undefined;
|
|
291
|
+
const second = value.slice(first.next).trim();
|
|
292
|
+
if (!second)
|
|
293
|
+
return undefined;
|
|
294
|
+
return [unwrapOuterQuotes(first.token.trim()), unwrapOuterQuotes(second)];
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Reads the next token from a string, supporting quoted (single or double) and bare tokens.
|
|
298
|
+
*
|
|
299
|
+
* @param value - The input string to read a token from.
|
|
300
|
+
* @param start - The index at which to begin scanning; leading whitespace is skipped.
|
|
301
|
+
* @returns An object `{ token, next }` where `token` is the raw token (quoted tokens include their surrounding quotes and any escape sequences) and `next` is the index immediately after the token, or `undefined` if no token is found or a quoted token is unterminated.
|
|
302
|
+
*/
|
|
303
|
+
function readQuotedOrBareToken(value, start) {
|
|
304
|
+
let index = start;
|
|
305
|
+
while (index < value.length && /\s/.test(value[index]))
|
|
306
|
+
index += 1;
|
|
307
|
+
if (index >= value.length)
|
|
308
|
+
return undefined;
|
|
309
|
+
const firstChar = value[index];
|
|
310
|
+
if (firstChar === '"' || firstChar === "'") {
|
|
311
|
+
const quote = firstChar;
|
|
312
|
+
let token = quote;
|
|
313
|
+
index += 1;
|
|
314
|
+
let escaped = false;
|
|
315
|
+
while (index < value.length) {
|
|
316
|
+
const ch = value[index];
|
|
317
|
+
token += ch;
|
|
318
|
+
if (quote === '"' && ch === '\\' && !escaped) {
|
|
319
|
+
escaped = true;
|
|
320
|
+
index += 1;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (ch === quote && !escaped) {
|
|
324
|
+
return { token, next: index + 1 };
|
|
325
|
+
}
|
|
326
|
+
escaped = false;
|
|
327
|
+
index += 1;
|
|
328
|
+
}
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
let end = index;
|
|
332
|
+
while (end < value.length && !/\s/.test(value[end]))
|
|
333
|
+
end += 1;
|
|
334
|
+
return { token: value.slice(index, end), next: end };
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Strip an inline YAML-style comment from a single line while preserving content inside quotes.
|
|
338
|
+
*
|
|
339
|
+
* @param rawLine - A single line of YAML-like text that may contain an inline `#` comment
|
|
340
|
+
* @returns The substring up to (but not including) the first `#` character that is not inside single or double quotes; returns the original line if no such comment is found
|
|
341
|
+
*/
|
|
342
|
+
function stripYamlInlineComment(rawLine) {
|
|
343
|
+
let inSingle = false;
|
|
344
|
+
let inDouble = false;
|
|
345
|
+
for (let i = 0; i < rawLine.length; i += 1) {
|
|
346
|
+
const ch = rawLine[i];
|
|
347
|
+
const prev = i > 0 ? rawLine[i - 1] : '';
|
|
348
|
+
if (ch === "'" && !inDouble) {
|
|
349
|
+
inSingle = !inSingle;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (ch === '"' && !inSingle && prev !== '\\') {
|
|
353
|
+
inDouble = !inDouble;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (ch === '#' && !inSingle && !inDouble) {
|
|
357
|
+
return rawLine.slice(0, i);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return rawLine;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Locate the colon that separates a YAML mapping key from its value, ignoring colons inside quoted strings.
|
|
364
|
+
*
|
|
365
|
+
* @returns The index of the separating colon in `content`, or `-1` if none is found. A colon is considered a separator only if it is not inside single-quoted or double-quoted strings (double quotes may use backslash to escape) and is followed by whitespace or the end of the line.
|
|
366
|
+
*/
|
|
367
|
+
function findYamlMapSeparator(content) {
|
|
368
|
+
let inSingle = false;
|
|
369
|
+
let inDouble = false;
|
|
370
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
371
|
+
const ch = content[i];
|
|
372
|
+
const prev = i > 0 ? content[i - 1] : '';
|
|
373
|
+
if (ch === "'" && !inDouble) {
|
|
374
|
+
inSingle = !inSingle;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (ch === '"' && !inSingle && prev !== '\\') {
|
|
378
|
+
inDouble = !inDouble;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (ch !== ':' || inSingle || inDouble)
|
|
382
|
+
continue;
|
|
383
|
+
const next = content[i + 1];
|
|
384
|
+
if (next === undefined || next === ' ' || next === '\t') {
|
|
385
|
+
return i;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return -1;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Convert a YAML-like scalar token into its corresponding JavaScript value for the lockfile parser.
|
|
392
|
+
*
|
|
393
|
+
* @param value - The scalar token to parse (may be quoted or a special literal)
|
|
394
|
+
* @returns The parsed value: `''` for empty input, `{}` for `'{}'`, `[]` for `'[]'`, `null` for `'null'` or `'~'`, `true` for `'true'`, `false` for `'false'`, or the unquoted string otherwise.
|
|
395
|
+
*/
|
|
396
|
+
function parseYamlScalar(value) {
|
|
397
|
+
const normalized = value.trim();
|
|
398
|
+
if (!normalized)
|
|
399
|
+
return '';
|
|
400
|
+
if (normalized === '{}')
|
|
401
|
+
return {};
|
|
402
|
+
if (normalized === '[]')
|
|
403
|
+
return [];
|
|
404
|
+
if (normalized === 'null' || normalized === '~')
|
|
405
|
+
return null;
|
|
406
|
+
if (normalized === 'true')
|
|
407
|
+
return true;
|
|
408
|
+
if (normalized === 'false')
|
|
409
|
+
return false;
|
|
410
|
+
return unwrapOuterQuotes(normalized);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Remove matching outer single or double quotes from a string and unescape their common escape sequences.
|
|
414
|
+
*
|
|
415
|
+
* @param value - The input string that may be wrapped in matching quotes
|
|
416
|
+
* @returns The trimmed string with outer matching quotes removed; for double-quoted input, unescapes `\"` to `"` and `\\` to `\`; for single-quoted input, collapses doubled single quotes `''` to `'`. If the input is not wrapped in matching quotes, returns the trimmed input unchanged.
|
|
417
|
+
*/
|
|
418
|
+
function unwrapOuterQuotes(value) {
|
|
419
|
+
const trimmed = value.trim();
|
|
420
|
+
if (trimmed.length < 2)
|
|
421
|
+
return trimmed;
|
|
422
|
+
const first = trimmed[0];
|
|
423
|
+
const last = trimmed[trimmed.length - 1];
|
|
424
|
+
if (first !== last || (first !== '"' && first !== "'")) {
|
|
425
|
+
return trimmed;
|
|
426
|
+
}
|
|
427
|
+
const inner = trimmed.slice(1, -1);
|
|
428
|
+
if (first === '"') {
|
|
429
|
+
return inner
|
|
430
|
+
.replace(/\\"/g, '"')
|
|
431
|
+
.replace(/\\\\/g, '\\');
|
|
432
|
+
}
|
|
433
|
+
return inner.replace(/''/g, "'");
|
|
434
|
+
}
|
package/dist/runners/npmAudit.js
CHANGED
|
@@ -61,7 +61,18 @@ function buildAuditCommand(tool, yarnVersion) {
|
|
|
61
61
|
lockFiles: ["package-lock.json", "npm-shrinkwrap.json"],
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Run a dependency audit using the specified package manager, normalize the audit output, and optionally write the result to disk.
|
|
66
|
+
*
|
|
67
|
+
* @param projectPath - Path to the project whose dependencies should be audited
|
|
68
|
+
* @param tempDir - Directory where the audit output file may be written
|
|
69
|
+
* @param tool - Package manager to use: `"npm"`, `"pnpm"`, or `"yarn"`
|
|
70
|
+
* @param yarnVersion - Optional Yarn version string used to select the correct Yarn audit command
|
|
71
|
+
* @param options - Optional persistence settings; when `options.persistToDisk` is omitted or `true`, write audit output/diagnostics to `${tempDir}/${tool}-audit.json` and include `file` in the result
|
|
72
|
+
* @returns An object with `ok: true` and `data` containing the normalized audit output (and `file` when persisted) on success; otherwise `ok: false` and `error` with an optional `file` when persisted
|
|
73
|
+
*/
|
|
74
|
+
async function runPackageAudit(projectPath, tempDir, tool, yarnVersion, options = {}) {
|
|
75
|
+
const persistToDisk = options.persistToDisk !== false;
|
|
65
76
|
const targetFile = path_1.default.join(tempDir, `${tool}-audit.json`);
|
|
66
77
|
try {
|
|
67
78
|
const { cmd, args, lockFiles } = buildAuditCommand(tool, yarnVersion);
|
|
@@ -71,26 +82,32 @@ async function runPackageAudit(projectPath, tempDir, tool, yarnVersion) {
|
|
|
71
82
|
const parsed = (0, utils_1.parseJsonOutput)(result.stdout);
|
|
72
83
|
const normalized = normalizeAuditOutput(tool, parsed);
|
|
73
84
|
if (normalized) {
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
if (persistToDisk) {
|
|
86
|
+
await (0, utils_1.writeJsonFile)(targetFile, normalized);
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, data: normalized, ...(persistToDisk ? { file: targetFile } : {}) };
|
|
89
|
+
}
|
|
90
|
+
if (persistToDisk) {
|
|
91
|
+
await (0, utils_1.writeJsonFile)(targetFile, {
|
|
92
|
+
stdout: result.stdout,
|
|
93
|
+
stderr: result.stderr,
|
|
94
|
+
code: result.code,
|
|
95
|
+
});
|
|
76
96
|
}
|
|
77
|
-
await (0, utils_1.writeJsonFile)(targetFile, {
|
|
78
|
-
stdout: result.stdout,
|
|
79
|
-
stderr: result.stderr,
|
|
80
|
-
code: result.code,
|
|
81
|
-
});
|
|
82
97
|
return {
|
|
83
98
|
ok: false,
|
|
84
99
|
error: `Failed to parse ${tool} audit output`,
|
|
85
|
-
file: targetFile,
|
|
100
|
+
...(persistToDisk ? { file: targetFile } : {}),
|
|
86
101
|
};
|
|
87
102
|
}
|
|
88
103
|
catch (err) {
|
|
89
|
-
|
|
104
|
+
if (persistToDisk) {
|
|
105
|
+
await (0, utils_1.writeJsonFile)(targetFile, { error: String(err) });
|
|
106
|
+
}
|
|
90
107
|
return {
|
|
91
108
|
ok: false,
|
|
92
109
|
error: `${tool} audit failed: ${String(err)}`,
|
|
93
|
-
file: targetFile,
|
|
110
|
+
...(persistToDisk ? { file: targetFile } : {}),
|
|
94
111
|
};
|
|
95
112
|
}
|
|
96
113
|
}
|
package/dist/runners/npmLs.js
CHANGED
|
@@ -17,15 +17,18 @@ const PNPM_MAX_OLD_SPACE_SIZE_MB = '8192';
|
|
|
17
17
|
* @param tempDir - Directory where the resulting JSON file and any diagnostics will be written
|
|
18
18
|
* @param tool - Package manager to use (`npm`, `pnpm`, or `yarn`)
|
|
19
19
|
* @param options - Optional progress callbacks and context; if `lockfileSearchRoot` is provided it will be used as the root when searching for a lockfile
|
|
20
|
-
* @returns The tool result. On success, `data` is the normalized dependency tree and `file` is the path of the written JSON; on failure, `error` contains a message suitable for users and `file` points to
|
|
20
|
+
* @returns The tool result. On success, `data` is the normalized dependency tree and `file` is the path of the written JSON when `options.persistToDisk !== false` (omitted/undefined when `options.persistToDisk === false`); on failure, `error` contains a message suitable for users and `file` points to diagnostics only when persistence is enabled.
|
|
21
21
|
*/
|
|
22
22
|
async function runNpmLs(projectPath, tempDir, tool = 'npm', options = {}) {
|
|
23
|
+
const persistToDisk = options.persistToDisk !== false;
|
|
23
24
|
const targetFile = path_1.default.join(tempDir, `${tool}-ls.json`);
|
|
24
25
|
try {
|
|
25
26
|
const lockfileTree = await (0, lockfileGraph_1.tryBuildDependencyTreeFromLockfile)(projectPath, tool, options.lockfileSearchRoot);
|
|
26
27
|
if (lockfileTree) {
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
if (persistToDisk) {
|
|
29
|
+
await (0, utils_1.writeJsonFile)(targetFile, lockfileTree.data);
|
|
30
|
+
}
|
|
31
|
+
return { ok: true, data: lockfileTree.data, ...(persistToDisk ? { file: targetFile } : {}) };
|
|
29
32
|
}
|
|
30
33
|
if (tool === 'pnpm') {
|
|
31
34
|
return await runPnpmLsWithFallback(projectPath, targetFile, options);
|
|
@@ -35,18 +38,34 @@ async function runNpmLs(projectPath, tempDir, tool = 'npm', options = {}) {
|
|
|
35
38
|
const parsed = parseJsonOutput(result.stdout);
|
|
36
39
|
const normalized = normalize(parsed);
|
|
37
40
|
if (normalized) {
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
if (persistToDisk) {
|
|
42
|
+
await (0, utils_1.writeJsonFile)(targetFile, normalized);
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, data: normalized, ...(persistToDisk ? { file: targetFile } : {}) };
|
|
45
|
+
}
|
|
46
|
+
if (persistToDisk) {
|
|
47
|
+
await (0, utils_1.writeJsonFile)(targetFile, { stdout: result.stdout, stderr: result.stderr, code: result.code });
|
|
40
48
|
}
|
|
41
|
-
await (0, utils_1.writeJsonFile)(targetFile, { stdout: result.stdout, stderr: result.stderr, code: result.code });
|
|
42
49
|
const error = buildLsFailureMessage(tool, result.code, result.stderr);
|
|
43
|
-
return { ok: false, error, file: targetFile };
|
|
50
|
+
return { ok: false, error, ...(persistToDisk ? { file: targetFile } : {}) };
|
|
44
51
|
}
|
|
45
52
|
catch (err) {
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
if (persistToDisk) {
|
|
54
|
+
await (0, utils_1.writeJsonFile)(targetFile, { error: String(err) });
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: `${tool} ls failed: ${String(err)}`,
|
|
59
|
+
...(persistToDisk ? { file: targetFile } : {})
|
|
60
|
+
};
|
|
48
61
|
}
|
|
49
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Selects the package-manager-specific list command arguments and the corresponding normalizer.
|
|
65
|
+
*
|
|
66
|
+
* @param tool - The package manager identifier ('npm', 'pnpm', or 'yarn') used to choose arguments and normalizer.
|
|
67
|
+
* @returns An object with `args`, the CLI arguments to run the tool's list command, and `normalize`, a function that converts the tool's parsed output into a `ResolvedTree` or `undefined` when parsing/normalization fails.
|
|
68
|
+
*/
|
|
50
69
|
function buildLsCommand(tool) {
|
|
51
70
|
if (tool === 'yarn') {
|
|
52
71
|
return {
|
|
@@ -59,7 +78,18 @@ function buildLsCommand(tool) {
|
|
|
59
78
|
normalize: normalizeNpmTree
|
|
60
79
|
};
|
|
61
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Attempt to build a normalized dependency tree for a pnpm workspace by running `pnpm list` with progressively lower depths until a parseable result is produced.
|
|
83
|
+
*
|
|
84
|
+
* Tries multiple depth levels, detects out-of-memory conditions, and optionally persists the normalized tree or diagnostic JSON to `targetFile` when `options.persistToDisk` is not explicitly `false`.
|
|
85
|
+
*
|
|
86
|
+
* @param projectPath - Filesystem path of the project/workspace to inspect
|
|
87
|
+
* @param targetFile - Path where the normalized tree or diagnostics will be written when persistence is enabled
|
|
88
|
+
* @param options - Progress and persistence options; when `options.persistToDisk` is omitted or `true`, successful results include `file: targetFile` and diagnostic output is written on failure
|
|
89
|
+
* @returns On success: an object with `ok: true` and `data` containing the normalized dependency tree (and `file` when persisted). On failure: an object with `ok: false` and an `error` message describing the failure (and `file` when diagnostics were persisted).
|
|
90
|
+
*/
|
|
62
91
|
async function runPnpmLsWithFallback(projectPath, targetFile, options) {
|
|
92
|
+
const persistToDisk = options.persistToDisk !== false;
|
|
63
93
|
const installState = createPnpmInstallState(projectPath);
|
|
64
94
|
const attempts = [];
|
|
65
95
|
const env = {
|
|
@@ -85,8 +115,10 @@ async function runPnpmLsWithFallback(projectPath, targetFile, options) {
|
|
|
85
115
|
if (index > 0) {
|
|
86
116
|
progress(options, `✔ PNPM ls recovered for workspace: ${formatContextLabel(options)} (depth=${depth})`);
|
|
87
117
|
}
|
|
88
|
-
|
|
89
|
-
|
|
118
|
+
if (persistToDisk) {
|
|
119
|
+
await (0, utils_1.writeJsonFile)(targetFile, normalized);
|
|
120
|
+
}
|
|
121
|
+
return { ok: true, data: normalized, ...(persistToDisk ? { file: targetFile } : {}) };
|
|
90
122
|
}
|
|
91
123
|
const reason = describeAttemptFailure(result.code, result.stderr);
|
|
92
124
|
progress(options, `✖ Failed pnpm ls for workspace: ${formatContextLabel(options)} (depth=${depth}; ${reason})`);
|
|
@@ -95,18 +127,20 @@ async function runPnpmLsWithFallback(projectPath, targetFile, options) {
|
|
|
95
127
|
progress(options, `✔ Retrying pnpm ls for workspace: ${formatContextLabel(options)} (depth=${nextDepth})`);
|
|
96
128
|
}
|
|
97
129
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
130
|
+
if (persistToDisk) {
|
|
131
|
+
await (0, utils_1.writeJsonFile)(targetFile, {
|
|
132
|
+
error: 'pnpm ls retries exhausted',
|
|
133
|
+
nodeOptions: env.NODE_OPTIONS,
|
|
134
|
+
attempts
|
|
135
|
+
});
|
|
136
|
+
}
|
|
103
137
|
const sawOom = attempts.some((attempt) => attempt.outOfMemory);
|
|
104
138
|
const lastAttempt = attempts[attempts.length - 1];
|
|
105
139
|
if (sawOom) {
|
|
106
140
|
return {
|
|
107
141
|
ok: false,
|
|
108
142
|
error: 'pnpm ls ran out of memory while building the dependency tree (retried with lower depths).',
|
|
109
|
-
file: targetFile
|
|
143
|
+
...(persistToDisk ? { file: targetFile } : {})
|
|
110
144
|
};
|
|
111
145
|
}
|
|
112
146
|
const suffix = lastAttempt && typeof lastAttempt.code === 'number'
|
|
@@ -115,7 +149,7 @@ async function runPnpmLsWithFallback(projectPath, targetFile, options) {
|
|
|
115
149
|
return {
|
|
116
150
|
ok: false,
|
|
117
151
|
error: `Failed to parse pnpm ls output after retries.${suffix}`,
|
|
118
|
-
file: targetFile
|
|
152
|
+
...(persistToDisk ? { file: targetFile } : {})
|
|
119
153
|
};
|
|
120
154
|
}
|
|
121
155
|
function progress(options, line) {
|
|
@@ -446,6 +480,12 @@ function normalizeYarnNode(node) {
|
|
|
446
480
|
}
|
|
447
481
|
return { name: parsed.name, node: out };
|
|
448
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Parse a Yarn node label into a package name and version.
|
|
485
|
+
*
|
|
486
|
+
* @param label - The Yarn label, typically in the form `name@version` (the version may be prefixed with `npm:`).
|
|
487
|
+
* @returns An object with `name` containing the package name and `version` containing the package version; if the label or either part is missing, that field is `"unknown"`.
|
|
488
|
+
*/
|
|
449
489
|
function splitYarnLabel(label) {
|
|
450
490
|
if (!label)
|
|
451
491
|
return { name: 'unknown', version: 'unknown' };
|