@dependably/npm-check 1.7.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/LICENSE +201 -0
- package/README.md +71 -0
- package/bin/cli.js +1577 -0
- package/package.json +103 -0
- package/src/audit-config.js +331 -0
- package/src/audit.js +805 -0
- package/src/backup.js +235 -0
- package/src/checker.js +771 -0
- package/src/checksum-fixer.js +419 -0
- package/src/deprecation.js +325 -0
- package/src/fixer.js +207 -0
- package/src/format-library.js +132 -0
- package/src/index.js +138 -0
- package/src/integrity.js +519 -0
- package/src/migrator.js +263 -0
- package/src/npmrc-validator.js +205 -0
- package/src/overrides.js +71 -0
- package/src/package-json-validator.js +356 -0
- package/src/parallel-processor.js +374 -0
- package/src/parser.js +133 -0
- package/src/performance.js +283 -0
- package/src/pinner.js +248 -0
- package/src/pnpm-format.js +229 -0
- package/src/pnpm-workspace-validator.js +121 -0
- package/src/progress-reporter.js +245 -0
- package/src/pruner.js +177 -0
- package/src/remediate.js +389 -0
- package/src/report.js +663 -0
- package/src/schema.js +76 -0
- package/src/streaming-parser.js +251 -0
- package/src/updater.js +251 -0
- package/src/usage-scanner.js +215 -0
- package/src/validator.js +282 -0
- package/src/vuln.js +618 -0
- package/src/workers/dedupe-worker.js +20 -0
- package/src/workers/hash-upgrade-worker.js +20 -0
- package/src/workers/migration-worker.js +21 -0
- package/src/workers/validation-worker.js +20 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel processing utilities using worker threads.
|
|
3
|
+
* Distributes CPU-bound operations across multiple cores.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Worker } from 'worker_threads';
|
|
7
|
+
import { cpus } from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { chunkLockfile, mergeLockfileChunks, isLargeLockfile } from './performance.js';
|
|
11
|
+
import { migrateToVersion } from './migrator.js';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Worker pool for managing worker threads
|
|
18
|
+
*/
|
|
19
|
+
export class WorkerPool {
|
|
20
|
+
/**
|
|
21
|
+
* Create a worker pool
|
|
22
|
+
* @param {number} size - Number of workers (default: CPU count - 1)
|
|
23
|
+
* @param {string} workerScript - Path to worker script
|
|
24
|
+
*/
|
|
25
|
+
constructor(size = null, workerScript = null) {
|
|
26
|
+
this.size = size || Math.max(1, cpus().length - 1);
|
|
27
|
+
this.workerScript = workerScript;
|
|
28
|
+
this.workers = []; // [{ worker, busy, index, current }]
|
|
29
|
+
this.queue = []; // [{ task, resolve, reject }]
|
|
30
|
+
this.active = 0;
|
|
31
|
+
this.terminated = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Initialize workers
|
|
36
|
+
*/
|
|
37
|
+
init() {
|
|
38
|
+
if (this.workers.length > 0) {
|
|
39
|
+
return; // Already initialized
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.terminated = false;
|
|
43
|
+
for (let i = 0; i < this.size; i++) {
|
|
44
|
+
this.workers.push(this._spawnWorker(i));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Spawn a single worker and register its persistent lifecycle handlers.
|
|
50
|
+
* The 'error'/'exit' listeners are registered ONCE per worker (not per task),
|
|
51
|
+
* so they never accumulate. Per-task handlers are added in _assign and always
|
|
52
|
+
* removed in the settle path, preventing the listener leak.
|
|
53
|
+
* @param {number} index - Worker id
|
|
54
|
+
* @returns {Object} Worker info record
|
|
55
|
+
*/
|
|
56
|
+
_spawnWorker(index) {
|
|
57
|
+
const worker = new Worker(this.workerScript, {
|
|
58
|
+
workerData: { workerId: index }
|
|
59
|
+
});
|
|
60
|
+
const info = { worker, busy: false, index, current: null, dead: false };
|
|
61
|
+
|
|
62
|
+
// A worker 'error' means an uncaught exception in the thread — the thread has
|
|
63
|
+
// exited and must not be reused. Reject its in-flight task and replace it.
|
|
64
|
+
worker.on('error', (error) => this._handleWorkerFailure(info, error));
|
|
65
|
+
worker.on('exit', (code) => {
|
|
66
|
+
if (this.terminated) return;
|
|
67
|
+
// Any exit that orphans an in-flight task must reject it and respawn —
|
|
68
|
+
// even a clean (code 0) exit mid-task, which would otherwise leave the
|
|
69
|
+
// task's promise forever unsettled and (at pool size 1) wedge the pool.
|
|
70
|
+
if (info.current || code !== 0) {
|
|
71
|
+
this._handleWorkerFailure(info, new Error(`Worker ${index} exited with code ${code}`));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return info;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Execute a task on the pool. Every task is enqueued and a single dispatcher
|
|
80
|
+
* assigns queued tasks to idle workers, so queued tasks always drain (no
|
|
81
|
+
* deadlock when tasks > workers).
|
|
82
|
+
* @param {Object} task - Task data
|
|
83
|
+
* @returns {Promise} Promise that resolves with the worker result
|
|
84
|
+
*/
|
|
85
|
+
execute(task) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
if (this.terminated) {
|
|
88
|
+
reject(new Error('WorkerPool has been terminated'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.queue.push({ task, resolve, reject });
|
|
92
|
+
this._dispatch();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Assign as many queued tasks as there are idle workers. Called on every
|
|
98
|
+
* enqueue and on every task completion/failure so the queue never stalls.
|
|
99
|
+
*/
|
|
100
|
+
_dispatch() {
|
|
101
|
+
if (this.terminated) return;
|
|
102
|
+
|
|
103
|
+
while (this.queue.length > 0) {
|
|
104
|
+
const workerInfo = this.workers.find(w => !w.busy);
|
|
105
|
+
if (!workerInfo) return; // all workers busy; retry on next completion
|
|
106
|
+
const job = this.queue.shift();
|
|
107
|
+
this._assign(workerInfo, job);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Run one job on one worker, with a per-task 'message' handler that is always
|
|
113
|
+
* removed once it fires (via once + explicit settle), so no handlers leak.
|
|
114
|
+
* @param {Object} workerInfo - Worker record
|
|
115
|
+
* @param {Object} job - { task, resolve, reject }
|
|
116
|
+
*/
|
|
117
|
+
_assign(workerInfo, job) {
|
|
118
|
+
workerInfo.busy = true;
|
|
119
|
+
workerInfo.current = job;
|
|
120
|
+
this.active++;
|
|
121
|
+
|
|
122
|
+
const onMessage = (result) => {
|
|
123
|
+
// Settle this worker before dispatching queued work.
|
|
124
|
+
workerInfo.busy = false;
|
|
125
|
+
workerInfo.current = null;
|
|
126
|
+
this.active--;
|
|
127
|
+
if (result && result.success === false) {
|
|
128
|
+
job.reject(new Error(result.error || 'Worker task failed'));
|
|
129
|
+
} else {
|
|
130
|
+
job.resolve(result);
|
|
131
|
+
}
|
|
132
|
+
this._dispatch();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// `once` auto-removes the message listener when it fires. Task failures are
|
|
136
|
+
// handled by the persistent 'error'/'exit' listeners (_handleWorkerFailure),
|
|
137
|
+
// so no per-task error listener is registered — the source of the old leak.
|
|
138
|
+
workerInfo.worker.once('message', onMessage);
|
|
139
|
+
workerInfo.worker.postMessage(job.task);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Handle a worker that has crashed/exited: reject its in-flight task, remove
|
|
144
|
+
* the dead worker from the pool (so nothing posts to a dead thread), respawn a
|
|
145
|
+
* replacement to keep capacity, and resume draining the queue.
|
|
146
|
+
* @param {Object} info - Worker record
|
|
147
|
+
* @param {Error} error - Failure cause
|
|
148
|
+
*/
|
|
149
|
+
_handleWorkerFailure(info, error) {
|
|
150
|
+
if (info.dead) return; // guard against error+exit firing for the same failure
|
|
151
|
+
info.dead = true;
|
|
152
|
+
|
|
153
|
+
// The thread is gone; drop any pending per-task message listener.
|
|
154
|
+
info.worker.removeAllListeners('message');
|
|
155
|
+
|
|
156
|
+
if (info.current) {
|
|
157
|
+
const job = info.current;
|
|
158
|
+
info.current = null;
|
|
159
|
+
this.active--;
|
|
160
|
+
job.reject(error);
|
|
161
|
+
}
|
|
162
|
+
info.busy = false;
|
|
163
|
+
|
|
164
|
+
const idx = this.workers.indexOf(info);
|
|
165
|
+
if (idx !== -1) {
|
|
166
|
+
this.workers.splice(idx, 1);
|
|
167
|
+
// Replace the dead worker so the pool keeps its configured capacity.
|
|
168
|
+
if (!this.terminated) {
|
|
169
|
+
this.workers.push(this._spawnWorker(info.index));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this._dispatch();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Terminate all workers
|
|
178
|
+
*/
|
|
179
|
+
async terminate() {
|
|
180
|
+
this.terminated = true;
|
|
181
|
+
const workers = this.workers.slice();
|
|
182
|
+
this.workers = [];
|
|
183
|
+
// Reject any tasks still queued so callers don't hang.
|
|
184
|
+
const pending = this.queue.splice(0, this.queue.length);
|
|
185
|
+
for (const job of pending) {
|
|
186
|
+
job.reject(new Error('WorkerPool has been terminated'));
|
|
187
|
+
}
|
|
188
|
+
this.active = 0;
|
|
189
|
+
await Promise.all(workers.map(({ worker }) => worker.terminate()));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Process lockfile chunks in parallel
|
|
195
|
+
* @param {Object} lockfile - Lockfile to process
|
|
196
|
+
* @param {string} operation - Operation name (hash-upgrade, dedupe, migration, validation)
|
|
197
|
+
* @param {Object} options - Options
|
|
198
|
+
* @param {number} options.workerCount - Number of workers (default: CPU count - 1)
|
|
199
|
+
* @param {number} options.chunkSize - Packages per chunk (default: 5000)
|
|
200
|
+
* @param {Function} options.onProgress - Progress callback
|
|
201
|
+
* @param {Object} options.operationOptions - Options specific to operation
|
|
202
|
+
* @returns {Promise<Object>} Processed lockfile
|
|
203
|
+
*/
|
|
204
|
+
export async function processInParallel(lockfile, operation, options = {}) {
|
|
205
|
+
const {
|
|
206
|
+
workerCount = Math.max(1, cpus().length - 1),
|
|
207
|
+
chunkSize = 5000,
|
|
208
|
+
onProgress = null,
|
|
209
|
+
operationOptions = {}
|
|
210
|
+
} = options;
|
|
211
|
+
|
|
212
|
+
// Only use parallel processing for large files
|
|
213
|
+
if (!isLargeLockfile(lockfile, 10)) {
|
|
214
|
+
// For smaller files, parallel overhead isn't worth it
|
|
215
|
+
throw new Error('Parallel processing is only recommended for large lockfiles (>10MB)');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Migration between lockfile formats reconstructs the dependency tree from the
|
|
219
|
+
// ENTIRE package graph, so it cannot be split across chunks: each chunk would see
|
|
220
|
+
// only a slice of `packages`, and merging the results keeps only one chunk's
|
|
221
|
+
// reconstructed `dependencies` tree (silent data loss). Run migration on the whole
|
|
222
|
+
// lockfile via parallelMigrate() / migrateToVersion() instead.
|
|
223
|
+
if (operation === 'migration') {
|
|
224
|
+
throw new Error(
|
|
225
|
+
'Migration is not chunk-parallelizable (tree reconstruction needs the whole graph); ' +
|
|
226
|
+
'use migrateToVersion() on the whole lockfile'
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Chunk the lockfile
|
|
231
|
+
const chunks = chunkLockfile(lockfile, chunkSize);
|
|
232
|
+
|
|
233
|
+
if (chunks.length === 1 && operation !== 'validation') {
|
|
234
|
+
// Single chunk, no need for parallel processing. Validation is excluded: it
|
|
235
|
+
// must still run through the worker so it returns the merged
|
|
236
|
+
// `{ valid, errors, warnings, info }` shape rather than a raw lockfile chunk.
|
|
237
|
+
return chunks[0];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Determine worker script based on operation
|
|
241
|
+
const workerScripts = {
|
|
242
|
+
'hash-upgrade': path.join(__dirname, 'workers', 'hash-upgrade-worker.js'),
|
|
243
|
+
'dedupe': path.join(__dirname, 'workers', 'dedupe-worker.js'),
|
|
244
|
+
'migration': path.join(__dirname, 'workers', 'migration-worker.js'),
|
|
245
|
+
'validation': path.join(__dirname, 'workers', 'validation-worker.js')
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const workerScript = workerScripts[operation];
|
|
249
|
+
if (!workerScript) {
|
|
250
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Create worker pool
|
|
254
|
+
const pool = new WorkerPool(workerCount, workerScript);
|
|
255
|
+
pool.init();
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
// Process chunks in parallel
|
|
259
|
+
const tasks = chunks.map((chunk, index) => ({
|
|
260
|
+
chunk,
|
|
261
|
+
operation,
|
|
262
|
+
options: operationOptions,
|
|
263
|
+
chunkIndex: index
|
|
264
|
+
}));
|
|
265
|
+
|
|
266
|
+
let completed = 0;
|
|
267
|
+
const results = await Promise.all(
|
|
268
|
+
tasks.map(async (task) => {
|
|
269
|
+
const response = await pool.execute(task);
|
|
270
|
+
completed++;
|
|
271
|
+
|
|
272
|
+
if (onProgress) {
|
|
273
|
+
onProgress({
|
|
274
|
+
current: completed,
|
|
275
|
+
total: tasks.length,
|
|
276
|
+
percentage: Math.round((completed / tasks.length) * 100),
|
|
277
|
+
stage: `Processing chunk ${completed}/${tasks.length}`
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Extract result from worker response
|
|
282
|
+
return response.result || response;
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Merge results with an operation-appropriate aggregator. Validation produces
|
|
287
|
+
// `{ valid, errors, warnings, info }` result objects, NOT lockfile chunks, so
|
|
288
|
+
// merging them as lockfiles (via mergeLockfileChunks) is nonsensical.
|
|
289
|
+
if (operation === 'validation') {
|
|
290
|
+
return mergeValidationResults(results);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// hash-upgrade / dedupe operate per-entry on the packages map, so merging the
|
|
294
|
+
// packages maps back together is correct.
|
|
295
|
+
const merged = mergeLockfileChunks(results);
|
|
296
|
+
return merged;
|
|
297
|
+
} finally {
|
|
298
|
+
await pool.terminate();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Aggregate per-chunk validation results into a single validation report.
|
|
304
|
+
* @param {Array<Object>} results - Array of `{ valid, errors, warnings, info }`
|
|
305
|
+
* @returns {Object} Combined validation result
|
|
306
|
+
*/
|
|
307
|
+
export function mergeValidationResults(results) {
|
|
308
|
+
const merged = { valid: true, errors: [], warnings: [], info: [] };
|
|
309
|
+
|
|
310
|
+
for (const result of results) {
|
|
311
|
+
if (!result || typeof result !== 'object') continue;
|
|
312
|
+
if (result.valid === false) merged.valid = false;
|
|
313
|
+
if (Array.isArray(result.errors)) merged.errors.push(...result.errors);
|
|
314
|
+
if (Array.isArray(result.warnings)) merged.warnings.push(...result.warnings);
|
|
315
|
+
if (Array.isArray(result.info)) merged.info.push(...result.info);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return merged;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Parallel hash upgrade
|
|
323
|
+
* @param {Object} lockfile - Lockfile to process
|
|
324
|
+
* @param {Object} options - Options
|
|
325
|
+
* @returns {Promise<Object>} Processed lockfile
|
|
326
|
+
*/
|
|
327
|
+
export async function parallelUpgradeIntegrityHashes(lockfile, options = {}) {
|
|
328
|
+
return processInParallel(lockfile, 'hash-upgrade', {
|
|
329
|
+
...options,
|
|
330
|
+
operationOptions: {
|
|
331
|
+
all: options.all || false
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Parallel deduplication
|
|
338
|
+
* @param {Object} lockfile - Lockfile to process
|
|
339
|
+
* @param {Object} options - Options
|
|
340
|
+
* @returns {Promise<Object>} Processed lockfile
|
|
341
|
+
*/
|
|
342
|
+
export async function parallelDeduplicatePackages(lockfile, options = {}) {
|
|
343
|
+
return processInParallel(lockfile, 'dedupe', {
|
|
344
|
+
...options,
|
|
345
|
+
operationOptions: {
|
|
346
|
+
keepLatest: options.keepLatest || false
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Parallel migration
|
|
353
|
+
* @param {Object} lockfile - Lockfile to process
|
|
354
|
+
* @param {number} targetVersion - Target version
|
|
355
|
+
* @param {Object} options - Options
|
|
356
|
+
* @returns {Promise<Object>} Processed lockfile
|
|
357
|
+
*/
|
|
358
|
+
// eslint-disable-next-line no-unused-vars
|
|
359
|
+
export async function parallelMigrate(lockfile, targetVersion, options = {}) {
|
|
360
|
+
// Migration is NOT chunk-parallelizable: reconstructing the dependency tree
|
|
361
|
+
// (e.g. v3 -> v2/v1) requires the whole package graph, and the old chunked
|
|
362
|
+
// implementation silently dropped every chunk's tree but the first, corrupting
|
|
363
|
+
// the output. Run the migration on the whole lockfile instead. It stays async
|
|
364
|
+
// for API compatibility (callers `await` it).
|
|
365
|
+
return migrateToVersion(lockfile, targetVersion);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export default {
|
|
369
|
+
WorkerPool,
|
|
370
|
+
processInParallel,
|
|
371
|
+
parallelUpgradeIntegrityHashes,
|
|
372
|
+
parallelDeduplicatePackages,
|
|
373
|
+
parallelMigrate
|
|
374
|
+
};
|
package/src/parser.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// src/parser.js
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import { parseLockfile as parseLockfileFromFormat, stringifyLockfile } from './format-library.js';
|
|
6
|
+
import { parseNpmrc } from './npmrc-validator.js';
|
|
7
|
+
import { DEFAULT_REGISTRY } from './integrity.js';
|
|
8
|
+
import { BackupError } from './backup.js';
|
|
9
|
+
import { parseLockfileStreamSync } from './streaming-parser.js';
|
|
10
|
+
|
|
11
|
+
// `yaml` is the one runtime dependency, and it is pulled in LAZILY (and
|
|
12
|
+
// synchronously, via createRequire) only when a pnpm-lock.yaml is actually
|
|
13
|
+
// parsed — the npm JSON path never loads it, preserving the zero-dependency
|
|
14
|
+
// core for npm-only users.
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
let _yaml = null;
|
|
17
|
+
function loadYaml() {
|
|
18
|
+
if (!_yaml) _yaml = require('yaml');
|
|
19
|
+
return _yaml;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// A lockfile is pnpm's when it's a YAML file (pnpm-lock.yaml / *.yaml / *.yml).
|
|
23
|
+
function isPnpmLockfilePath(filePath) {
|
|
24
|
+
const base = path.basename(filePath).toLowerCase();
|
|
25
|
+
return base === 'pnpm-lock.yaml' || base.endsWith('.yaml') || base.endsWith('.yml');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build the registry config for a pnpm lockfile from its sibling .npmrc — pnpm
|
|
30
|
+
* reads .npmrc ONLY for registry + auth, so this is the authoritative source for
|
|
31
|
+
* the per-package registry base (there is no `resolved` URL to parse).
|
|
32
|
+
* @param {string} filePath - Path to the pnpm lockfile
|
|
33
|
+
* @returns {{ registry: string, scopedRegistries: object }}
|
|
34
|
+
*/
|
|
35
|
+
function buildPnpmRegistryConfig(filePath) {
|
|
36
|
+
const npmrcPath = path.join(path.dirname(filePath), '.npmrc');
|
|
37
|
+
let registry = DEFAULT_REGISTRY;
|
|
38
|
+
const scopedRegistries = {};
|
|
39
|
+
if (fs.existsSync(npmrcPath)) {
|
|
40
|
+
try {
|
|
41
|
+
for (const { key, value } of parseNpmrc(fs.readFileSync(npmrcPath, 'utf8'))) {
|
|
42
|
+
if (key === 'registry') registry = value;
|
|
43
|
+
else if (key && key.startsWith('@') && key.endsWith(':registry')) {
|
|
44
|
+
scopedRegistries[key.slice(0, key.indexOf(':'))] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// A malformed .npmrc shouldn't break lockfile parsing — fall back to defaults.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { registry, scopedRegistries };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse a pnpm-lock.yaml into an object stamped with a non-enumerable
|
|
56
|
+
* `__npmCheckMeta` ({ flavor, lockfileVersion, registry, scopedRegistries }).
|
|
57
|
+
* The meta is non-enumerable so it never leaks into JSON output or stringify.
|
|
58
|
+
* @param {string} filePath - Path to the pnpm lockfile
|
|
59
|
+
* @returns {object} Parsed pnpm lockfile
|
|
60
|
+
*/
|
|
61
|
+
function parsePnpmLockfile(filePath) {
|
|
62
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
63
|
+
const data = loadYaml().parse(content);
|
|
64
|
+
if (!data || typeof data !== 'object') {
|
|
65
|
+
throw new Error(`Invalid pnpm-lock.yaml: ${filePath}`);
|
|
66
|
+
}
|
|
67
|
+
Object.defineProperty(data, '__npmCheckMeta', {
|
|
68
|
+
value: {
|
|
69
|
+
flavor: 'pnpm',
|
|
70
|
+
lockfileVersion: data.lockfileVersion,
|
|
71
|
+
...buildPnpmRegistryConfig(filePath)
|
|
72
|
+
},
|
|
73
|
+
enumerable: false,
|
|
74
|
+
writable: true,
|
|
75
|
+
configurable: true
|
|
76
|
+
});
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse a lockfile from file path. Dispatches by flavor: pnpm-lock.yaml is parsed
|
|
82
|
+
* as YAML (with the lazy `yaml` dependency); npm package-lock.json is parsed as
|
|
83
|
+
* JSON, using the streaming parser for large files.
|
|
84
|
+
* @param {string} filePath - Path to lockfile
|
|
85
|
+
* @param {Object} options - Options
|
|
86
|
+
* @param {boolean} options.useStreaming - Force streaming parser (default: auto-detect, npm only)
|
|
87
|
+
* @param {number} options.streamingThreshold - File size threshold in bytes for streaming (default: 10MB)
|
|
88
|
+
* @param {Function} options.onProgress - Progress callback for streaming
|
|
89
|
+
* @returns {Object} Parsed lockfile object
|
|
90
|
+
*/
|
|
91
|
+
export function parseLockfile(filePath, options = {}) {
|
|
92
|
+
// pnpm lockfiles are YAML and rarely huge — parse directly, no streaming.
|
|
93
|
+
if (isPnpmLockfilePath(filePath)) {
|
|
94
|
+
return parsePnpmLockfile(filePath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const {
|
|
98
|
+
useStreaming = null, // null = auto-detect
|
|
99
|
+
streamingThreshold = 10 * 1024 * 1024, // 10MB
|
|
100
|
+
onProgress = null
|
|
101
|
+
} = options;
|
|
102
|
+
|
|
103
|
+
// Check file size
|
|
104
|
+
let shouldUseStreaming = useStreaming;
|
|
105
|
+
if (shouldUseStreaming === null) {
|
|
106
|
+
try {
|
|
107
|
+
const stats = fs.statSync(filePath);
|
|
108
|
+
shouldUseStreaming = stats.size >= streamingThreshold;
|
|
109
|
+
} catch {
|
|
110
|
+
// If we can't get stats, fall back to standard parsing
|
|
111
|
+
shouldUseStreaming = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (shouldUseStreaming) {
|
|
116
|
+
return parseLockfileStreamSync(filePath, {
|
|
117
|
+
streamingThreshold,
|
|
118
|
+
onProgress
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Standard parsing for smaller files
|
|
123
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
124
|
+
return parseLockfileFromFormat(content);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function serializeLockfile(filePath, data, overwrite = false) {
|
|
128
|
+
if (!overwrite && fs.existsSync(filePath)) {
|
|
129
|
+
throw new BackupError(`File ${filePath} already exists`);
|
|
130
|
+
}
|
|
131
|
+
const content = stringifyLockfile(data);
|
|
132
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
133
|
+
}
|