@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
package/src/schema.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/schema.js
|
|
2
|
+
// The Dependably suite's shared finding JSON schema (v1). Every tool's
|
|
3
|
+
// `--format json` emits the SAME top-level envelope so an AI or CI consumer can
|
|
4
|
+
// parse any of the five tools identically; tool-specific data only ever rides
|
|
5
|
+
// under `extra` (never as a new top-level key). See the suite schema spec.
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
export const TOOL_NAME = 'npm-check';
|
|
11
|
+
export const SCHEMA_VERSION = '1.0';
|
|
12
|
+
|
|
13
|
+
// The ONE severity ladder, most-severe first.
|
|
14
|
+
export const SEVERITY_LADDER = ['critical', 'high', 'moderate', 'low', 'info'];
|
|
15
|
+
|
|
16
|
+
let cachedVersion = null;
|
|
17
|
+
|
|
18
|
+
// Resolve the tool's own semver from package.json (cached). Mirrors bin/cli.js's
|
|
19
|
+
// getVersion so the envelope's toolVersion always matches `--version`.
|
|
20
|
+
export function toolVersion() {
|
|
21
|
+
if (cachedVersion) return cachedVersion;
|
|
22
|
+
try {
|
|
23
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(here, '../package.json'), 'utf8'));
|
|
25
|
+
cachedVersion = pkg.version || 'unknown';
|
|
26
|
+
} catch {
|
|
27
|
+
cachedVersion = 'unknown';
|
|
28
|
+
}
|
|
29
|
+
return cachedVersion;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// A zeroed bySeverity histogram with every ladder key present.
|
|
33
|
+
export function emptyBySeverity() {
|
|
34
|
+
return { critical: 0, high: 0, moderate: 0, low: 0, info: 0 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Count findings into the ladder histogram (ignores any off-ladder severity).
|
|
38
|
+
export function tallyBySeverity(findings) {
|
|
39
|
+
const by = emptyBySeverity();
|
|
40
|
+
for (const f of findings) {
|
|
41
|
+
if (f && Object.prototype.hasOwnProperty.call(by, f.severity)) by[f.severity]++;
|
|
42
|
+
}
|
|
43
|
+
return by;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Assemble the shared envelope. `findings` MUST be the complete, schema-conformant
|
|
48
|
+
* list (never truncated); `summary.findings` is derived from it and `summary.exitCode`
|
|
49
|
+
* MUST equal the process exit code the run will return.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} args
|
|
52
|
+
* @param {string} args.target - path/manifest scanned, as given
|
|
53
|
+
* @param {number} args.scanned - packages examined
|
|
54
|
+
* @param {Array} args.findings - schema Finding objects
|
|
55
|
+
* @param {number} args.exitCode - the real process exit code (0/1/2)
|
|
56
|
+
* @param {object} [args.extra] - the ONE sanctioned tool-specific escape hatch
|
|
57
|
+
* @returns {object} the envelope
|
|
58
|
+
*/
|
|
59
|
+
export function buildEnvelope({ target, scanned, findings, exitCode, extra }) {
|
|
60
|
+
const list = Array.isArray(findings) ? findings : [];
|
|
61
|
+
const envelope = {
|
|
62
|
+
tool: TOOL_NAME,
|
|
63
|
+
toolVersion: toolVersion(),
|
|
64
|
+
schemaVersion: SCHEMA_VERSION,
|
|
65
|
+
target,
|
|
66
|
+
summary: {
|
|
67
|
+
scanned,
|
|
68
|
+
findings: list.length,
|
|
69
|
+
bySeverity: tallyBySeverity(list),
|
|
70
|
+
exitCode
|
|
71
|
+
},
|
|
72
|
+
findings: list
|
|
73
|
+
};
|
|
74
|
+
if (extra !== undefined) envelope.extra = extra;
|
|
75
|
+
return envelope;
|
|
76
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffered parser for large package-lock.json files.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: despite the "streaming" name, these helpers read the file in chunks (so
|
|
5
|
+
* progress can be reported) but parse the fully-buffered content with the shared
|
|
6
|
+
* `parseLockfile` from format-library — they do NOT parse incrementally. A true
|
|
7
|
+
* incremental JSON parser is future work. The buffered approach is honest about
|
|
8
|
+
* its result: it returns the actual parsed lockfile (or throws), never an empty
|
|
9
|
+
* skeleton that would make every downstream integrity/vuln/license check pass
|
|
10
|
+
* vacuously.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
import { parseLockfile as parseLockfileFromFormat } from './format-library.js';
|
|
16
|
+
|
|
17
|
+
// Record one incremental `package` event into the accumulating lockfile: the
|
|
18
|
+
// root ('') merges into rootMetadata, others land in the packages map.
|
|
19
|
+
function recordStreamPackage(path, pkg, lockfile, rootMetadata, options) {
|
|
20
|
+
if (path === '') {
|
|
21
|
+
Object.assign(rootMetadata, pkg); // root package → root metadata
|
|
22
|
+
} else {
|
|
23
|
+
lockfile.packages[path] = pkg;
|
|
24
|
+
}
|
|
25
|
+
if (options.onPackage) options.onPackage(path, pkg);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Assemble the final result from the buffered `complete` event. The parsed result
|
|
29
|
+
// is authoritative; anything the (currently no-op) incremental events collected is
|
|
30
|
+
// layered on WITHOUT injecting empty skeleton keys — a v3 file must not gain a
|
|
31
|
+
// spurious `dependencies: {}`, nor a v1 file a `packages: {}`, which a write-back
|
|
32
|
+
// would then persist.
|
|
33
|
+
function assembleStreamResult(result, lockfile, rootMetadata) {
|
|
34
|
+
const parsed = (result && typeof result === 'object') ? result : {};
|
|
35
|
+
const merged = { ...parsed };
|
|
36
|
+
for (const [k, v] of Object.entries(rootMetadata)) {
|
|
37
|
+
if (!(k in merged)) merged[k] = v;
|
|
38
|
+
}
|
|
39
|
+
if (Object.keys(lockfile.packages).length > 0) {
|
|
40
|
+
merged.packages = { ...(parsed.packages || {}), ...lockfile.packages };
|
|
41
|
+
}
|
|
42
|
+
return merged;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Streaming parser for package-lock.json files
|
|
47
|
+
* Handles large files by parsing incrementally
|
|
48
|
+
*/
|
|
49
|
+
export class StreamingParser extends EventEmitter {
|
|
50
|
+
/**
|
|
51
|
+
* Create a streaming parser
|
|
52
|
+
* @param {Object} options - Parser options
|
|
53
|
+
* @param {Function} options.onPackage - Callback when package is parsed (path, pkg)
|
|
54
|
+
* @param {Function} options.onProgress - Progress callback (bytesRead, totalBytes)
|
|
55
|
+
* @param {number} options.chunkSize - Read chunk size in bytes (default: 64KB)
|
|
56
|
+
*/
|
|
57
|
+
constructor(options = {}) {
|
|
58
|
+
super();
|
|
59
|
+
this.onPackageCallback = options.onPackage || null;
|
|
60
|
+
this.onProgressCallback = options.onProgress || null;
|
|
61
|
+
this.chunkSize = options.chunkSize || 64 * 1024;
|
|
62
|
+
this.buffer = '';
|
|
63
|
+
this.state = 'initial';
|
|
64
|
+
this.depth = 0;
|
|
65
|
+
this.currentPath = null;
|
|
66
|
+
this.currentPackage = null;
|
|
67
|
+
this.inString = false;
|
|
68
|
+
this.escapeNext = false;
|
|
69
|
+
this.bytesRead = 0;
|
|
70
|
+
this.totalBytes = 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse a lockfile from file path using streaming
|
|
75
|
+
* @param {string} filePath - Path to lockfile
|
|
76
|
+
* @param {Object} options - Parser options
|
|
77
|
+
* @returns {Promise<Object>} Parsed lockfile object
|
|
78
|
+
*/
|
|
79
|
+
static async parseLockfileStream(filePath, options = {}) {
|
|
80
|
+
const stats = fs.statSync(filePath);
|
|
81
|
+
const totalBytes = stats.size;
|
|
82
|
+
|
|
83
|
+
const parser = new StreamingParser({
|
|
84
|
+
...options,
|
|
85
|
+
onProgress: (bytesRead) => {
|
|
86
|
+
if (options.onProgress) {
|
|
87
|
+
options.onProgress(bytesRead, totalBytes);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
parser.totalBytes = totalBytes;
|
|
93
|
+
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const lockfile = {
|
|
96
|
+
packages: {},
|
|
97
|
+
dependencies: {}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Collect root metadata
|
|
101
|
+
let rootMetadata = {};
|
|
102
|
+
|
|
103
|
+
parser.on('package', (path, pkg) => recordStreamPackage(path, pkg, lockfile, rootMetadata, options));
|
|
104
|
+
parser.on('metadata', (key, value) => {
|
|
105
|
+
rootMetadata[key] = value;
|
|
106
|
+
});
|
|
107
|
+
parser.on('error', reject);
|
|
108
|
+
parser.on('complete', (result) => resolve(assembleStreamResult(result, lockfile, rootMetadata)));
|
|
109
|
+
|
|
110
|
+
const stream = fs.createReadStream(filePath, {
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
highWaterMark: parser.chunkSize
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
stream.on('data', (chunk) => {
|
|
116
|
+
parser.processChunk(chunk);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
stream.on('end', () => {
|
|
120
|
+
parser.finish();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
stream.on('error', reject);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Process a chunk of data
|
|
129
|
+
* @param {string} chunk - Data chunk
|
|
130
|
+
*/
|
|
131
|
+
processChunk(chunk) {
|
|
132
|
+
this.buffer += chunk;
|
|
133
|
+
this.bytesRead += Buffer.byteLength(chunk, 'utf8');
|
|
134
|
+
|
|
135
|
+
if (this.onProgressCallback) {
|
|
136
|
+
this.onProgressCallback(this.bytesRead);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Simple approach: For very large files, fall back to standard parsing
|
|
140
|
+
// but do it in a way that doesn't block. For now, we'll use a hybrid approach.
|
|
141
|
+
// If buffer gets too large, parse what we have and continue.
|
|
142
|
+
|
|
143
|
+
// For package-lock.json, the structure is predictable enough that we can
|
|
144
|
+
// use a simpler incremental approach: read the file in chunks and parse
|
|
145
|
+
// the packages map incrementally.
|
|
146
|
+
|
|
147
|
+
// Since full streaming JSON parsing is complex, we'll use a pragmatic approach:
|
|
148
|
+
// For files under a certain size, use standard parsing.
|
|
149
|
+
// For larger files, we'll implement a simplified streaming parser that
|
|
150
|
+
// extracts packages one by one.
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Finish parsing
|
|
155
|
+
*/
|
|
156
|
+
finish() {
|
|
157
|
+
if (this.buffer.trim()) {
|
|
158
|
+
try {
|
|
159
|
+
// Parse the fully-buffered content through the shared format-library
|
|
160
|
+
// parser (consistent error messages; not raw JSON.parse).
|
|
161
|
+
const parsed = parseLockfileFromFormat(this.buffer);
|
|
162
|
+
this.emit('complete', parsed);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.emit('error', error);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// An empty stream is not a valid lockfile. Fail loudly rather than
|
|
168
|
+
// resolving an empty (vacuously "clean") lockfile.
|
|
169
|
+
this.emit('error', new Error('Cannot parse lockfile: stream produced no data'));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Parse lockfile using streaming approach for large files
|
|
176
|
+
* Falls back to standard parsing for smaller files
|
|
177
|
+
* @param {string} filePath - Path to lockfile
|
|
178
|
+
* @param {Object} options - Options
|
|
179
|
+
* @param {Function} options.onPackage - Callback when package parsed
|
|
180
|
+
* @param {Function} options.onProgress - Progress callback
|
|
181
|
+
* @param {number} options.streamingThreshold - File size threshold in bytes for streaming (default: 10MB)
|
|
182
|
+
* @returns {Promise<Object>} Parsed lockfile
|
|
183
|
+
*/
|
|
184
|
+
export async function parseLockfileStream(filePath, options = {}) {
|
|
185
|
+
const stats = fs.statSync(filePath);
|
|
186
|
+
const fileSize = stats.size;
|
|
187
|
+
const threshold = options.streamingThreshold || 10 * 1024 * 1024; // 10MB default
|
|
188
|
+
|
|
189
|
+
// For files smaller than threshold, use standard parsing
|
|
190
|
+
if (fileSize < threshold) {
|
|
191
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
192
|
+
return parseLockfileFromFormat(content);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// For larger files, read the file in chunks (so we can report progress) and then
|
|
196
|
+
// parse the fully-buffered content. This is buffered, not truly incremental, but
|
|
197
|
+
// it returns the ACTUAL parsed lockfile (or rejects) — never an empty skeleton.
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
|
200
|
+
let buffer = '';
|
|
201
|
+
let bytesRead = 0;
|
|
202
|
+
|
|
203
|
+
stream.on('data', (chunk) => {
|
|
204
|
+
buffer += chunk;
|
|
205
|
+
bytesRead += Buffer.byteLength(chunk, 'utf8');
|
|
206
|
+
|
|
207
|
+
if (options.onProgress) {
|
|
208
|
+
options.onProgress(bytesRead, fileSize);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
stream.on('end', () => {
|
|
213
|
+
try {
|
|
214
|
+
resolve(parseLockfileFromFormat(buffer));
|
|
215
|
+
} catch (error) {
|
|
216
|
+
reject(error);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
stream.on('error', reject);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Synchronous version with callbacks (for compatibility)
|
|
226
|
+
* @param {string} filePath - Path to lockfile
|
|
227
|
+
* @param {Object} options - Options
|
|
228
|
+
* @returns {Object} Parsed lockfile
|
|
229
|
+
*/
|
|
230
|
+
export function parseLockfileStreamSync(filePath, options = {}) {
|
|
231
|
+
const stats = fs.statSync(filePath);
|
|
232
|
+
const fileSize = stats.size;
|
|
233
|
+
const threshold = options.streamingThreshold || 10 * 1024 * 1024;
|
|
234
|
+
|
|
235
|
+
// For smaller files, use standard parsing
|
|
236
|
+
if (fileSize < threshold) {
|
|
237
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
238
|
+
return parseLockfileFromFormat(content);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// For larger files, read in chunks but still parse at once
|
|
242
|
+
// This is a compromise - true streaming would require async or more complex parsing
|
|
243
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
244
|
+
return parseLockfileFromFormat(content);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export default {
|
|
248
|
+
StreamingParser,
|
|
249
|
+
parseLockfileStream,
|
|
250
|
+
parseLockfileStreamSync
|
|
251
|
+
};
|
package/src/updater.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Updater module for package-lock.json files
|
|
3
|
+
* Optimized implementations for handling large lockfiles efficiently.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { detectLockfileVersion, hasPackagesMap, resolvePackageName } from './format-library.js';
|
|
7
|
+
import {
|
|
8
|
+
shallowCopyLockfile,
|
|
9
|
+
filterPackagesLazy,
|
|
10
|
+
isLargeLockfile
|
|
11
|
+
} from './performance.js';
|
|
12
|
+
import { parallelUpgradeIntegrityHashes as parallelUpgrade, parallelDeduplicatePackages as parallelDedupe } from './parallel-processor.js';
|
|
13
|
+
|
|
14
|
+
// Dependency sections within a package entry whose integrity hashes are
|
|
15
|
+
// upgraded recursively alongside the entry's own integrity.
|
|
16
|
+
const DEPENDENCY_SECTIONS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
17
|
+
|
|
18
|
+
// Emit a progress update for the integrity-hash upgrade stage.
|
|
19
|
+
function reportUpgradeProgress(onProgress, processed, total) {
|
|
20
|
+
if (!onProgress) return;
|
|
21
|
+
onProgress({ current: processed, total, percentage: Math.round((processed / total) * 100), stage: 'Upgrading integrity hashes' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Promote a single integrity hash to sha512. sha1 hashes are always promoted;
|
|
25
|
+
// other non-sha512 hashes are promoted only when `all` is set.
|
|
26
|
+
function upgradeHash(hash, all) {
|
|
27
|
+
if (!hash) return hash;
|
|
28
|
+
if (hash.startsWith('sha1-')) {
|
|
29
|
+
return 'sha512-' + hash.slice(5);
|
|
30
|
+
}
|
|
31
|
+
if (all && !hash.startsWith('sha512-')) {
|
|
32
|
+
return 'sha512-' + hash.slice(hash.indexOf('-') + 1);
|
|
33
|
+
}
|
|
34
|
+
return hash;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Recursively upgrade integrity hashes in a dependency tree, preserving
|
|
38
|
+
// non-object entries untouched.
|
|
39
|
+
function upgradeDependencies(deps, all) {
|
|
40
|
+
if (!deps || typeof deps !== 'object') return deps;
|
|
41
|
+
|
|
42
|
+
const upgraded = {};
|
|
43
|
+
for (const [depName, dep] of Object.entries(deps)) {
|
|
44
|
+
if (!dep || typeof dep !== 'object') {
|
|
45
|
+
upgraded[depName] = dep;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Upgrade this dependency and recursively upgrade nested ones
|
|
50
|
+
upgraded[depName] = {
|
|
51
|
+
...dep,
|
|
52
|
+
...(dep.integrity && { integrity: upgradeHash(dep.integrity, all) }),
|
|
53
|
+
...(dep.dependencies && { dependencies: upgradeDependencies(dep.dependencies, all) })
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return upgraded;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Upgrade a single package entry's own integrity plus each of its nested
|
|
60
|
+
// dependency sections, returning a shallow copy.
|
|
61
|
+
function upgradePackageEntry(pkg, all) {
|
|
62
|
+
const upgradedPkg = { ...pkg };
|
|
63
|
+
|
|
64
|
+
if (upgradedPkg.integrity) {
|
|
65
|
+
upgradedPkg.integrity = upgradeHash(upgradedPkg.integrity, all);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
69
|
+
if (upgradedPkg[section] && typeof upgradedPkg[section] === 'object') {
|
|
70
|
+
upgradedPkg[section] = upgradeDependencies(upgradedPkg[section], all);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return upgradedPkg;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Upgrade integrity hashes in a lockfile
|
|
79
|
+
* @param {object} lockfileData - Lockfile data
|
|
80
|
+
* @param {object} options - Options for upgrade
|
|
81
|
+
* @param {boolean} options.all - Upgrade all hashes (default: false, only sha1)
|
|
82
|
+
* @param {Function} options.onProgress - Progress callback function(progressInfo)
|
|
83
|
+
* @param {boolean} options.parallel - Use parallel processing for large files (default: false)
|
|
84
|
+
* @returns {object|Promise<object>} Updated lockfile data (immutable - returns Promise if parallel)
|
|
85
|
+
*/
|
|
86
|
+
export function upgradeIntegrityHashes(lockfileData, options = {}) {
|
|
87
|
+
const { all = false, onProgress = null, parallel = false } = options;
|
|
88
|
+
|
|
89
|
+
// Use parallel processing if requested and file is large
|
|
90
|
+
if (parallel && isLargeLockfile(lockfileData, 10)) {
|
|
91
|
+
return parallelUpgrade(lockfileData, {
|
|
92
|
+
all,
|
|
93
|
+
onProgress,
|
|
94
|
+
...options
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const version = detectLockfileVersion(lockfileData);
|
|
98
|
+
|
|
99
|
+
if (!hasPackagesMap(version)) {
|
|
100
|
+
return lockfileData;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Use shallow copy to avoid deep cloning
|
|
104
|
+
const result = shallowCopyLockfile(lockfileData);
|
|
105
|
+
|
|
106
|
+
// Process packages directly without deep recursion
|
|
107
|
+
const packageEntries = Object.entries(result.packages || {});
|
|
108
|
+
const total = packageEntries.length;
|
|
109
|
+
const upgradedPackages = {};
|
|
110
|
+
let processed = 0;
|
|
111
|
+
|
|
112
|
+
for (const [path, pkg] of packageEntries) {
|
|
113
|
+
upgradedPackages[path] = pkg ? upgradePackageEntry(pkg, all) : pkg;
|
|
114
|
+
processed++;
|
|
115
|
+
|
|
116
|
+
// Null entries report every step; real entries report every 100th.
|
|
117
|
+
if (!pkg || processed % 100 === 0) {
|
|
118
|
+
reportUpgradeProgress(onProgress, processed, total);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
result.packages = upgradedPackages;
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Deduplicate packages in a lockfile
|
|
128
|
+
* Uses Map-based deduplication for O(1) lookups
|
|
129
|
+
* @param {object} lockfileData - Lockfile data
|
|
130
|
+
* @param {object} options - Options for deduplication
|
|
131
|
+
* @param {boolean} options.keepLatest - Keep only latest version of duplicates (default: false)
|
|
132
|
+
* @param {Function} options.onProgress - Progress callback function(progressInfo)
|
|
133
|
+
* @param {boolean} options.parallel - Use parallel processing for large files (default: false)
|
|
134
|
+
* @returns {object|Promise<object>} Updated lockfile data (immutable - returns Promise if parallel)
|
|
135
|
+
*/
|
|
136
|
+
export function deduplicatePackages(lockfileData, options = {}) {
|
|
137
|
+
const { keepLatest = false, onProgress = null, parallel = false } = options;
|
|
138
|
+
|
|
139
|
+
// Use parallel processing if requested and file is large
|
|
140
|
+
if (parallel && isLargeLockfile(lockfileData, 10)) {
|
|
141
|
+
return parallelDedupe(lockfileData, {
|
|
142
|
+
keepLatest,
|
|
143
|
+
onProgress,
|
|
144
|
+
...options
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const version = detectLockfileVersion(lockfileData);
|
|
148
|
+
|
|
149
|
+
const result = shallowCopyLockfile(lockfileData);
|
|
150
|
+
|
|
151
|
+
// Packages map (v2/v3): keyed by *install path*. Every entry is a distinct,
|
|
152
|
+
// required node — the package name is encoded in the path, so most entries
|
|
153
|
+
// carry no `.name` field at all. There is no safe way to "remove duplicates"
|
|
154
|
+
// here: two entries that share a name@version live at different paths because
|
|
155
|
+
// npm could not hoist them to one location (conflicting dependents), and
|
|
156
|
+
// collapsing them produces an un-installable lockfile. Real npm deduplication
|
|
157
|
+
// is tree hoisting, which requires full re-resolution and is out of scope.
|
|
158
|
+
//
|
|
159
|
+
// So we PRESERVE every path entry. (The previous implementation keyed a map by
|
|
160
|
+
// `name#version` and rebuilt from it, which silently dropped every entry that
|
|
161
|
+
// lacked a `.name` field — i.e. effectively the entire packages map — gutting
|
|
162
|
+
// the lockfile down to the root. See deduplicatePackages preserve tests.)
|
|
163
|
+
// Use `prune` to remove genuinely orphaned (unreachable) entries.
|
|
164
|
+
if (hasPackagesMap(version) && result.packages) {
|
|
165
|
+
const total = Object.keys(result.packages).length;
|
|
166
|
+
if (onProgress) {
|
|
167
|
+
onProgress({ current: total, total, percentage: 100, stage: 'Deduplication complete' });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// v1 dependencies tree: top-level keys are unique by JS object construction,
|
|
172
|
+
// so a Set-based filter at this level is a no-op. Real v1 "deduplication" is
|
|
173
|
+
// tree hoisting from nested sub-dependency objects — that requires full
|
|
174
|
+
// re-resolution and is out of scope here (use `npm install --prefer-dedupe`).
|
|
175
|
+
// We preserve the tree as-is, consistent with the packages-map preserve-only
|
|
176
|
+
// policy above. No entries are removed.
|
|
177
|
+
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Find packages matching a predicate without full copy (lazy evaluation)
|
|
183
|
+
* Useful for filtering operations on large lockfiles
|
|
184
|
+
* @param {object} lockfileData - Lockfile data
|
|
185
|
+
* @param {Function} predicate - Function(path, pkg) => boolean
|
|
186
|
+
* @returns {object} Filtered packages object
|
|
187
|
+
*/
|
|
188
|
+
export function findPackagesMatching(lockfileData, predicate) {
|
|
189
|
+
return filterPackagesLazy(lockfileData.packages || {}, predicate);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Count unique packages in lockfile (efficient for large files)
|
|
194
|
+
* @param {object} lockfileData - Lockfile data
|
|
195
|
+
* @returns {number} Number of unique packages
|
|
196
|
+
*/
|
|
197
|
+
export function countUniquePackages(lockfileData) {
|
|
198
|
+
if (!lockfileData.packages || typeof lockfileData.packages !== 'object') {
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const uniqueNames = new Set();
|
|
203
|
+
for (const [path, pkg] of Object.entries(lockfileData.packages)) {
|
|
204
|
+
if (!pkg || typeof pkg !== 'object') continue;
|
|
205
|
+
// Derive the name from the path — v2/v3 entries usually have no `.name` field,
|
|
206
|
+
// so reading `pkg.name` alone would miss nearly every real dependency.
|
|
207
|
+
const name = resolvePackageName(path, pkg);
|
|
208
|
+
if (name && name !== '(root)') uniqueNames.add(name);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return uniqueNames.size;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Find duplicate packages efficiently
|
|
216
|
+
* @param {object} lockfileData - Lockfile data
|
|
217
|
+
* @returns {Map} Map of package names to array of {path, version} objects
|
|
218
|
+
*/
|
|
219
|
+
export function findDuplicatePackages(lockfileData) {
|
|
220
|
+
const duplicates = new Map();
|
|
221
|
+
|
|
222
|
+
for (const [path, pkg] of Object.entries(lockfileData.packages || {})) {
|
|
223
|
+
if (!pkg || typeof pkg !== 'object') continue;
|
|
224
|
+
// Derive the name from the path — v2/v3 entries usually have no `.name` field.
|
|
225
|
+
const name = resolvePackageName(path, pkg);
|
|
226
|
+
if (!name || name === '(root)') continue;
|
|
227
|
+
|
|
228
|
+
if (!duplicates.has(name)) {
|
|
229
|
+
duplicates.set(name, []);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
duplicates.get(name).push({ path, version: pkg.version });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Keep only actual duplicates
|
|
236
|
+
for (const [name, entries] of duplicates.entries()) {
|
|
237
|
+
if (entries.length === 1) {
|
|
238
|
+
duplicates.delete(name);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return duplicates;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export default {
|
|
246
|
+
upgradeIntegrityHashes,
|
|
247
|
+
deduplicatePackages,
|
|
248
|
+
findPackagesMatching,
|
|
249
|
+
countUniquePackages,
|
|
250
|
+
findDuplicatePackages
|
|
251
|
+
};
|