jpm-pkg 1.0.3 → 1.0.4
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/bin/jpm.js +7 -1
- package/package.json +1 -1
- package/src/commands/audit.js +70 -41
- package/src/commands/base-command.js +62 -0
- package/src/commands/config.js +94 -51
- package/src/commands/info.js +111 -64
- package/src/commands/init.js +117 -72
- package/src/commands/install.js +121 -112
- package/src/commands/list.js +126 -80
- package/src/commands/publish.js +155 -124
- package/src/commands/run.js +86 -52
- package/src/commands/search.js +56 -32
- package/src/commands/uninstall.js +47 -30
- package/src/commands/update.js +92 -51
- package/src/commands/x.js +134 -96
- package/src/core/cache.js +188 -87
- package/src/core/lockfile.js +73 -26
- package/src/core/package-json.js +119 -1
- package/src/core/registry.js +182 -143
- package/src/core/resolver.js +60 -36
package/src/core/registry.js
CHANGED
|
@@ -3,164 +3,203 @@
|
|
|
3
3
|
const { getJSON, download } = require('../utils/http');
|
|
4
4
|
const config = require('../utils/config');
|
|
5
5
|
const logger = require('../utils/logger');
|
|
6
|
-
|
|
7
|
-
const REGISTRY = () => config.registry.replace(/\/$/, '');
|
|
8
|
-
|
|
9
6
|
const LRUCache = require('../utils/lru-cache');
|
|
10
|
-
const env = require('../utils/env');
|
|
11
7
|
|
|
12
8
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* Registry class handles all interactions with the npm registry.
|
|
10
|
+
* It provides methods for fetching package metadata, versions, and downloading tarballs.
|
|
11
|
+
* Optimized with an in-memory LRU cache for resolution speed.
|
|
16
12
|
*/
|
|
17
|
-
|
|
13
|
+
class Registry {
|
|
14
|
+
/**
|
|
15
|
+
* Creates an instance of the Registry.
|
|
16
|
+
* @param {Object} [options={}] - Configuration options for the registry
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this._config = options.config || config;
|
|
20
|
+
/**
|
|
21
|
+
* In-memory metadata cache for registry requests.
|
|
22
|
+
* @type {LRUCache}
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
this._metaCache = new LRUCache(1000);
|
|
26
|
+
}
|
|
18
27
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (metaCache.has(url)) return metaCache.get(url);
|
|
28
|
-
|
|
29
|
-
logger.verbose(`registry GET ${url}`);
|
|
30
|
-
const doc = await getJSON(url, {
|
|
31
|
-
headers: { Accept: 'application/vnd.npm.install-v1+json, application/json' },
|
|
32
|
-
timeout: config.timeout,
|
|
33
|
-
retries: config.retries,
|
|
34
|
-
});
|
|
35
|
-
metaCache.set(url, doc);
|
|
36
|
-
return doc;
|
|
37
|
-
}
|
|
28
|
+
/**
|
|
29
|
+
* Gets the base registry URL from configuration.
|
|
30
|
+
* @returns {string} The registry URL without trailing slash
|
|
31
|
+
* @private
|
|
32
|
+
*/
|
|
33
|
+
_getRegistryUrl() {
|
|
34
|
+
return this._config.registry.replace(/\/$/, '');
|
|
35
|
+
}
|
|
38
36
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Fetches the full packument for a package from the registry.
|
|
39
|
+
* Includes an internal LRU cache to speed up subsequent requests.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} name - The canonical name of the package (e.g., 'express' or '@types/node')
|
|
42
|
+
* @returns {Promise<Object>} The packument object containing version history and tags
|
|
43
|
+
* @example
|
|
44
|
+
* const packument = await registry.getPackument('lodash');
|
|
45
|
+
*/
|
|
46
|
+
async getPackument(name) {
|
|
47
|
+
const url = `${this._getRegistryUrl()}/${encodeURIComponent(name).replace('%40', '@')}`;
|
|
48
|
+
if (this._metaCache.has(url)) return this._metaCache.get(url);
|
|
49
|
+
|
|
50
|
+
logger.verbose(`registry GET ${url}`);
|
|
51
|
+
const doc = await getJSON(url, {
|
|
52
|
+
headers: { Accept: 'application/vnd.npm.install-v1+json, application/json' },
|
|
53
|
+
timeout: this._config.timeout,
|
|
54
|
+
retries: this._config.retries,
|
|
55
|
+
});
|
|
56
|
+
this._metaCache.set(url, doc);
|
|
57
|
+
return doc;
|
|
58
|
+
}
|
|
57
59
|
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Fetches specific version metadata for a package.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} name - The package name
|
|
64
|
+
* @param {string} version - Specific version (e.g., '1.0.0') or dist-tag (e.g., 'latest')
|
|
65
|
+
* @returns {Promise<Object>} The version manifest containing dependencies and dist info
|
|
66
|
+
* @throws {Error} If the requested version is unavailable in the registry
|
|
67
|
+
* @example
|
|
68
|
+
* const versionData = await registry.getVersion('express', '4.17.1');
|
|
69
|
+
*/
|
|
70
|
+
async getVersion(name, version) {
|
|
71
|
+
const packument = await this.getPackument(name);
|
|
72
|
+
const ver = version === 'latest'
|
|
73
|
+
? packument['dist-tags']?.latest
|
|
74
|
+
: version;
|
|
75
|
+
|
|
76
|
+
const data = packument.versions?.[ver];
|
|
77
|
+
if (!data) throw new Error(`Version ${name}@${ver} not found in registry`);
|
|
78
|
+
return data;
|
|
79
|
+
}
|
|
68
80
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
async
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
81
|
+
/**
|
|
82
|
+
* Retrieves all available version strings for a package.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} name - The package name
|
|
85
|
+
* @returns {Promise<string[]>} Array of version strings sorted by publication time (if provided by registry)
|
|
86
|
+
*/
|
|
87
|
+
async getVersions(name) {
|
|
88
|
+
const packument = await this.getPackument(name);
|
|
89
|
+
return Object.keys(packument.versions || {});
|
|
90
|
+
}
|
|
79
91
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
async
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
92
|
+
/**
|
|
93
|
+
* Retrieves the version string flagged as 'latest' in the registry.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} name - The package name
|
|
96
|
+
* @returns {Promise<string|undefined>} The latest version string
|
|
97
|
+
*/
|
|
98
|
+
async getLatest(name) {
|
|
99
|
+
const packument = await this.getPackument(name);
|
|
100
|
+
return packument['dist-tags']?.latest;
|
|
101
|
+
}
|
|
90
102
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return download(tarballUrl, destStream, {
|
|
102
|
-
timeout: config.timeout * 2,
|
|
103
|
-
retries: config.retries,
|
|
104
|
-
onProgress,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
103
|
+
/**
|
|
104
|
+
* Retrieves all distribution tags associated with a package.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} name - The package name
|
|
107
|
+
* @returns {Promise<Object.<string, string>>} Map of tags to versions (e.g., { latest: '1.0.0', beta: '1.1.0-beta.1' })
|
|
108
|
+
*/
|
|
109
|
+
async getDistTags(name) {
|
|
110
|
+
const packument = await this.getPackument(name);
|
|
111
|
+
return packument['dist-tags'] || {};
|
|
112
|
+
}
|
|
107
113
|
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Downloads a package tarball and writes it to a destination stream.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} tarballUrl - Fully qualified URL to the tarball (usually from version metadata)
|
|
118
|
+
* @param {import('node:stream').Writable} destStream - Target writable stream (e.g., fs.createWriteStream)
|
|
119
|
+
* @param {Function} [onProgress] - Optional heartbeat for download progress (receivedBytes, totalBytes)
|
|
120
|
+
* @returns {Promise<void>}
|
|
121
|
+
*/
|
|
122
|
+
async downloadTarball(tarballUrl, destStream, onProgress) {
|
|
123
|
+
logger.verbose(`tarball GET ${tarballUrl}`);
|
|
124
|
+
return download(tarballUrl, destStream, {
|
|
125
|
+
timeout: this._config.timeout * 2,
|
|
126
|
+
retries: this._config.retries,
|
|
127
|
+
onProgress,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
122
130
|
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Executes a full-text search against the npm registry.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} query - Search term
|
|
135
|
+
* @param {Object} [options] - Pagination options
|
|
136
|
+
* @param {number} [options.size=20] - Number of results to return per page
|
|
137
|
+
* @param {number} [options.from=0] - Offset for results
|
|
138
|
+
* @returns {Promise<Object[]>} List of search result objects containing package info and scores
|
|
139
|
+
*/
|
|
140
|
+
async search(query, { size = 20, from = 0 } = {}) {
|
|
141
|
+
const url = `${this._getRegistryUrl()}/-/v1/search?text=${encodeURIComponent(query)}&size=${size}&from=${from}`;
|
|
142
|
+
const doc = await getJSON(url, { timeout: this._config.timeout });
|
|
143
|
+
return doc.objects || [];
|
|
134
144
|
}
|
|
135
145
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Queries the registry for known security vulnerabilities.
|
|
148
|
+
* Uses the npm quick audit endpoint for efficiency.
|
|
149
|
+
*
|
|
150
|
+
* @param {Object.<string, string[]>} requires - Map of package names to lists of required versions
|
|
151
|
+
* @returns {Promise<Object>} Audit report containing vulnerabilities, advisories, and metadata
|
|
152
|
+
*/
|
|
153
|
+
async fetchAdvisories(requires) {
|
|
154
|
+
const url = `https://registry.npmjs.org/-/npm/v1/security/audits/quick`;
|
|
155
|
+
const packages = {};
|
|
156
|
+
for (const [name, versions] of Object.entries(requires)) {
|
|
157
|
+
packages[name] = versions;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { request } = require('../utils/http');
|
|
161
|
+
const body = JSON.stringify({
|
|
162
|
+
name: 'audit-target',
|
|
163
|
+
version: '1.0.0',
|
|
164
|
+
requires: Object.fromEntries(
|
|
165
|
+
Object.entries(requires).map(([n, vs]) => [n, vs[0] || '*'])
|
|
166
|
+
),
|
|
167
|
+
dependencies: packages,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const res = await request(url, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: {
|
|
173
|
+
'Content-Type': 'application/json',
|
|
174
|
+
'Content-Length': Buffer.byteLength(body),
|
|
175
|
+
},
|
|
176
|
+
body,
|
|
177
|
+
timeout: 30_000,
|
|
178
|
+
retries: 2,
|
|
179
|
+
strict: true, // Security: Always use HTTPS for audits
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(res.body);
|
|
184
|
+
} catch {
|
|
185
|
+
return { advisories: {}, metadata: {} };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
160
188
|
}
|
|
161
189
|
|
|
190
|
+
// Singleton instance for backward compatibility
|
|
191
|
+
const defaultRegistry = new Registry();
|
|
192
|
+
|
|
193
|
+
// Export the class and the singleton members to maintain backward compatibility
|
|
162
194
|
module.exports = {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
195
|
+
Registry,
|
|
196
|
+
getPackument: defaultRegistry.getPackument.bind(defaultRegistry),
|
|
197
|
+
getVersion: defaultRegistry.getVersion.bind(defaultRegistry),
|
|
198
|
+
getVersions: defaultRegistry.getVersions.bind(defaultRegistry),
|
|
199
|
+
getLatest: defaultRegistry.getLatest.bind(defaultRegistry),
|
|
200
|
+
getDistTags: defaultRegistry.getDistTags.bind(defaultRegistry),
|
|
201
|
+
downloadTarball: defaultRegistry.downloadTarball.bind(defaultRegistry),
|
|
202
|
+
search: defaultRegistry.search.bind(defaultRegistry),
|
|
203
|
+
fetchAdvisories: defaultRegistry.fetchAdvisories.bind(defaultRegistry),
|
|
166
204
|
};
|
|
205
|
+
|
package/src/core/resolver.js
CHANGED
|
@@ -6,35 +6,62 @@ const system = require('../utils/system');
|
|
|
6
6
|
const logger = require('../utils/logger');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Resolves a full dependency tree given a root package
|
|
9
|
+
* Resolves a full dependency tree given a root package configuration.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* This class handles recursive dependency resolution, version satisfaction,
|
|
12
|
+
* aliasing (npm: protocol), circular dependency detection, and deduplication.
|
|
13
|
+
*
|
|
14
|
+
* Performance is optimized using parallel resolution and an in-flight request tracker
|
|
15
|
+
* to prevent redundant registry queries for the same package/range pair.
|
|
13
16
|
*/
|
|
14
17
|
class Resolver {
|
|
15
18
|
/**
|
|
16
19
|
* Creates an instance of the Resolver.
|
|
17
|
-
* Initializes
|
|
20
|
+
* Initializes internal state for resolution tracking.
|
|
18
21
|
*/
|
|
19
22
|
constructor() {
|
|
20
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Map of "name@version" to resolved package metadata.
|
|
25
|
+
* @type {Map<string, Object>}
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
21
28
|
this._resolved = new Map();
|
|
22
|
-
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Map of "name@range" to active resolution promises to prevent redundant work.
|
|
32
|
+
* @type {Map<string, Promise>}
|
|
33
|
+
* @private
|
|
34
|
+
*/
|
|
23
35
|
this._inFlight = new Map();
|
|
24
|
-
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Stack of resolved keys ("name@version") being processed in the current recursion path.
|
|
39
|
+
* Used for circular dependency detection.
|
|
40
|
+
* @type {string[]}
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
25
43
|
this._stack = [];
|
|
44
|
+
|
|
45
|
+
/** @type {number} */
|
|
46
|
+
this._totalToResolve = 0;
|
|
47
|
+
/** @type {number} */
|
|
48
|
+
this._resolvedCount = 0;
|
|
26
49
|
}
|
|
27
50
|
|
|
28
51
|
/**
|
|
29
52
|
* Resolves a set of dependencies recursively.
|
|
30
53
|
*
|
|
31
|
-
* @param {Object.<string, string>} [deps={}] -
|
|
54
|
+
* @param {Object.<string, string>} [deps={}] - Production dependencies
|
|
32
55
|
* @param {Object.<string, string>} [devDeps={}] - Development dependencies
|
|
33
56
|
* @param {Object.<string, string>} [peerDeps={}] - Peer dependencies
|
|
34
|
-
* @
|
|
57
|
+
* @param {Function} [onProgress] - Optional progress callback (current, total)
|
|
58
|
+
* @returns {Promise<Map<string, Object>>} A promise that resolves to the flat map of resolved packages
|
|
59
|
+
* @example
|
|
60
|
+
* const resolver = new Resolver();
|
|
61
|
+
* const resolved = await resolver.resolve({ express: '^4.17.1' });
|
|
35
62
|
*/
|
|
36
63
|
async resolve(deps = {}, devDeps = {}, peerDeps = {}, onProgress) {
|
|
37
|
-
const all = { ...deps, ...devDeps };
|
|
64
|
+
const all = { ...deps, ...devDeps, ...peerDeps };
|
|
38
65
|
this._totalToResolve = Object.keys(all).length;
|
|
39
66
|
this._resolvedCount = 0;
|
|
40
67
|
this._onProgress = onProgress;
|
|
@@ -45,15 +72,21 @@ class Resolver {
|
|
|
45
72
|
return this._resolved;
|
|
46
73
|
}
|
|
47
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Initiates or joins an existing resolution for a single package and range.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} name - Package name
|
|
79
|
+
* @param {string} range - Semver range or alias
|
|
80
|
+
* @returns {Promise<void>}
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
48
83
|
async _resolveOne(name, range) {
|
|
49
84
|
const key = `${name}@${range}`;
|
|
50
85
|
|
|
51
|
-
// Already in flight? Await the existing promise.
|
|
52
86
|
if (this._inFlight.has(key)) {
|
|
53
87
|
return this._inFlight.get(key);
|
|
54
88
|
}
|
|
55
89
|
|
|
56
|
-
// Create a new resolution promise and store it in _inFlight.
|
|
57
90
|
const p = this._doResolve(name, range);
|
|
58
91
|
this._inFlight.set(key, p);
|
|
59
92
|
|
|
@@ -63,27 +96,28 @@ class Resolver {
|
|
|
63
96
|
this._onProgress?.(this._resolvedCount, this._totalToResolve);
|
|
64
97
|
return p;
|
|
65
98
|
} finally {
|
|
66
|
-
// No longer in flight once the promise settles.
|
|
67
99
|
this._inFlight.delete(key);
|
|
68
100
|
}
|
|
69
101
|
}
|
|
70
102
|
|
|
71
103
|
/**
|
|
72
104
|
* Internal resolution logic for a single package and range.
|
|
105
|
+
* Handles npm aliases and recursive resolution of transitive dependencies.
|
|
73
106
|
*
|
|
74
|
-
* @param {string} name - The name
|
|
107
|
+
* @param {string} name - The name as declared in dependencies
|
|
75
108
|
* @param {string} range - The semver range or npm:alias
|
|
109
|
+
* @returns {Promise<void>}
|
|
76
110
|
* @protected
|
|
77
111
|
*/
|
|
78
112
|
async _doResolve(name, range) {
|
|
79
|
-
// Handle npm: alias protocol (e.g., "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0")
|
|
80
113
|
let targetName = name;
|
|
81
114
|
let targetRange = range === '' || range === '*' || range === 'latest' ? 'latest' : range;
|
|
82
115
|
|
|
116
|
+
// Handle npm: alias protocol (e.g., "pkg": "npm:real-pkg@^1.0.0")
|
|
83
117
|
if (targetRange.startsWith('npm:')) {
|
|
84
118
|
const parts = targetRange.slice(4).split('@');
|
|
85
|
-
// Handle scoped packages in alias: npm:@scope/pkg@range
|
|
86
119
|
if (targetRange.slice(4).startsWith('@')) {
|
|
120
|
+
// Scoped alias: npm:@scope/pkg@range
|
|
87
121
|
targetName = '@' + parts[1];
|
|
88
122
|
targetRange = parts[2] || 'latest';
|
|
89
123
|
} else {
|
|
@@ -105,13 +139,10 @@ class Resolver {
|
|
|
105
139
|
throw new Error(`No version of ${targetName} satisfies "${targetRange}". Available: ${versions.slice(-5).join(', ')}`);
|
|
106
140
|
}
|
|
107
141
|
|
|
108
|
-
// The resolved key uses the original dependency name to allow multiple aliases of the same package
|
|
109
142
|
const resolvedKey = `${name}@${chosen}`;
|
|
110
143
|
|
|
111
|
-
// Check if already resolved
|
|
144
|
+
// Check if already resolved globally or in current path
|
|
112
145
|
if (this._resolved.has(resolvedKey)) return;
|
|
113
|
-
|
|
114
|
-
// Detect circular dependencies on the current resolution path
|
|
115
146
|
if (this._stack.includes(resolvedKey)) return;
|
|
116
147
|
|
|
117
148
|
// 3. Retrieve exhaustive version metadata
|
|
@@ -119,8 +150,8 @@ class Resolver {
|
|
|
119
150
|
|
|
120
151
|
// 4. Map metadata to internal representation
|
|
121
152
|
const metaToStore = {
|
|
122
|
-
name: targetName,
|
|
123
|
-
alias: name !== targetName ? name : undefined,
|
|
153
|
+
name: targetName,
|
|
154
|
+
alias: name !== targetName ? name : undefined,
|
|
124
155
|
version: chosen,
|
|
125
156
|
resolved: meta.dist?.tarball,
|
|
126
157
|
integrity: meta.dist?.integrity || meta.dist?.shasum,
|
|
@@ -138,7 +169,6 @@ class Resolver {
|
|
|
138
169
|
// 5. Recursively resolve transitive dependencies
|
|
139
170
|
this._stack.push(resolvedKey);
|
|
140
171
|
|
|
141
|
-
// Combine normal and optional dependencies for resolution
|
|
142
172
|
const dependencies = {
|
|
143
173
|
...metaToStore.deps,
|
|
144
174
|
...metaToStore.optDeps
|
|
@@ -146,13 +176,9 @@ class Resolver {
|
|
|
146
176
|
|
|
147
177
|
await Promise.all(
|
|
148
178
|
Object.entries(dependencies).map(async ([depName, depRange]) => {
|
|
149
|
-
//
|
|
179
|
+
// Pre-check for optional dependency compatibility
|
|
150
180
|
if (metaToStore.optDeps[depName]) {
|
|
151
181
|
try {
|
|
152
|
-
// We need the packument to see the OS/CPU of the potential version
|
|
153
|
-
// Actually, a better way is to resolve it first, then check compatibility
|
|
154
|
-
// before resolving its own transitive dependencies.
|
|
155
|
-
// But to be even faster, we can check the packument's version metadata.
|
|
156
182
|
const packument = await registry.getPackument(depName);
|
|
157
183
|
const version = semver.maxSatisfying(Object.keys(packument.versions || {}), depRange);
|
|
158
184
|
if (version) {
|
|
@@ -163,7 +189,7 @@ class Resolver {
|
|
|
163
189
|
}
|
|
164
190
|
}
|
|
165
191
|
} catch (e) {
|
|
166
|
-
// If
|
|
192
|
+
// If check fails, fall through to normal resolution
|
|
167
193
|
}
|
|
168
194
|
}
|
|
169
195
|
return this._resolveOne(depName, depRange);
|
|
@@ -177,10 +203,9 @@ class Resolver {
|
|
|
177
203
|
}
|
|
178
204
|
}
|
|
179
205
|
|
|
180
|
-
// ── Analysis helpers ────────────────────────────────────────────────────────
|
|
181
|
-
|
|
182
206
|
/**
|
|
183
|
-
* Identifies package name collisions and
|
|
207
|
+
* Identifies package name collisions and returns a summary.
|
|
208
|
+
* Used for post-resolution analysis and potential hoisting.
|
|
184
209
|
*
|
|
185
210
|
* @returns {Object[]} List of duplicate packages and their versions
|
|
186
211
|
*/
|
|
@@ -200,9 +225,9 @@ class Resolver {
|
|
|
200
225
|
}
|
|
201
226
|
|
|
202
227
|
/**
|
|
203
|
-
*
|
|
228
|
+
* Performs a Depth-First Search on the resolved graph to detect cycles.
|
|
204
229
|
*
|
|
205
|
-
* @returns {string[]} An array of strings describing
|
|
230
|
+
* @returns {string[]} An array of strings describing detected cycles (e.g., "A → B → A")
|
|
206
231
|
*/
|
|
207
232
|
findCircular() {
|
|
208
233
|
const cycles = [];
|
|
@@ -221,9 +246,7 @@ class Resolver {
|
|
|
221
246
|
|
|
222
247
|
const meta = this._resolved.get(key);
|
|
223
248
|
if (meta) {
|
|
224
|
-
// We must match dependency ranges to their ACTUAL resolved versions in this._resolved
|
|
225
249
|
for (const [depName, depRange] of Object.entries(meta.deps)) {
|
|
226
|
-
// Find the version of depName that was actually resolved
|
|
227
250
|
for (const [resKey, resMeta] of this._resolved) {
|
|
228
251
|
if (resMeta.name === depName && semver.satisfies(resMeta.version, depRange)) {
|
|
229
252
|
dfs(resKey);
|
|
@@ -246,3 +269,4 @@ class Resolver {
|
|
|
246
269
|
}
|
|
247
270
|
|
|
248
271
|
module.exports = Resolver;
|
|
272
|
+
|