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.
@@ -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
- * In-memory metadata cache for registry requests, optimized for resolution speed.
14
- * @type {LRUCache}
15
- * @private
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
- const metaCache = new LRUCache(1000);
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
- * Fetches the full packument for a package from the registry.
21
- *
22
- * @param {string} name - The canonical name of the package
23
- * @returns {Promise<Object>} The packument object containing version history and tags
24
- */
25
- async function getPackument(name) {
26
- const url = `${REGISTRY()}/${encodeURIComponent(name).replace('%40', '@')}`;
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
- * Fetches specific version metadata for a package.
41
- *
42
- * @param {string} name - The package name
43
- * @param {string} version - Specific version or dist-tag (e.g., 'latest')
44
- * @returns {Promise<Object>} The version manifest
45
- * @throws {Error} If the requested version is unavailable in the registry
46
- */
47
- async function getVersion(name, version) {
48
- const packument = await getPackument(name);
49
- const ver = version === 'latest'
50
- ? packument['dist-tags']?.latest
51
- : version;
52
-
53
- const data = packument.versions?.[ver];
54
- if (!data) throw new Error(`Version ${name}@${ver} not found in registry`);
55
- return data;
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
- * Retrieves all available version strings for a package.
60
- *
61
- * @param {string} name - The package name
62
- * @returns {Promise<string[]>} Array of version strings
63
- */
64
- async function getVersions(name) {
65
- const packument = await getPackument(name);
66
- return Object.keys(packument.versions || {});
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
- * Retrieves the version string flagged as 'latest' in the registry.
71
- *
72
- * @param {string} name - The package name
73
- * @returns {Promise<string|undefined>} The latest version string
74
- */
75
- async function getLatest(name) {
76
- const packument = await getPackument(name);
77
- return packument['dist-tags']?.latest;
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
- * Retrieves all distribution tags associated with a package.
82
- *
83
- * @param {string} name - The package name
84
- * @returns {Promise<Object.<string, string>>} Map of tags to versions
85
- */
86
- async function getDistTags(name) {
87
- const packument = await getPackument(name);
88
- return packument['dist-tags'] || {};
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
- * Downloads a package tarball and writes it to a destination stream.
93
- *
94
- * @param {string} tarballUrl - Fully qualified URL to the tarball
95
- * @param {import('node:stream').Writable} destStream - Target writable stream
96
- * @param {Function} [onProgress] - Optional heartbeat for download progress
97
- * @returns {Promise<void>}
98
- */
99
- async function downloadTarball(tarballUrl, destStream, onProgress) {
100
- logger.verbose(`tarball GET ${tarballUrl}`);
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
- * Executes a full-text search against the npm registry.
110
- *
111
- * @param {string} query - Search term
112
- * @param {Object} [options] - Pagination options
113
- * @param {number} [options.size=20] - Number of results to return
114
- * @param {number} [options.from=0] - Offset for results
115
- * @returns {Promise<Object[]>} List of search result objects
116
- */
117
- async function search(query, { size = 20, from = 0 } = {}) {
118
- const url = `${REGISTRY()}/-/v1/search?text=${encodeURIComponent(query)}&size=${size}&from=${from}`;
119
- const doc = await getJSON(url, { timeout: config.timeout });
120
- return doc.objects || [];
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
- * Queries the registry for known security vulnerabilities.
125
- *
126
- * @param {Object.<string, string[]>} requires - Map of package names to lists of required versions
127
- * @returns {Promise<Object>} Audit report containing vulnerabilities and metadata
128
- */
129
- async function fetchAdvisories(requires) {
130
- const url = `https://registry.npmjs.org/-/npm/v1/security/audits/quick`;
131
- const packages = {};
132
- for (const [name, versions] of Object.entries(requires)) {
133
- packages[name] = versions;
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
- const { request } = require('../utils/http');
137
- const body = JSON.stringify({
138
- name: 'audit-target',
139
- version: '1.0.0',
140
- requires: Object.fromEntries(
141
- Object.entries(requires).map(([n, vs]) => [n, vs[0] || '*'])
142
- ),
143
- dependencies: packages,
144
- });
145
-
146
- const res = await request(url, {
147
- method: 'POST',
148
- headers: {
149
- 'Content-Type': 'application/json',
150
- 'Content-Length': Buffer.byteLength(body),
151
- },
152
- body,
153
- timeout: 30_000,
154
- retries: 2,
155
- strict: true, // Security: Always use HTTPS for audits
156
- });
157
-
158
- try { return JSON.parse(res.body); }
159
- catch { return { advisories: {}, metadata: {} }; }
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
- getPackument, getVersion, getVersions,
164
- getLatest, getDistTags,
165
- downloadTarball, search, fetchAdvisories,
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
+
@@ -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.json dependencies map.
9
+ * Resolves a full dependency tree given a root package configuration.
10
10
  *
11
- * Returns a flat map: { "name@version" => { name, version, resolved, integrity, dependencies } }
12
- * Also detects circular dependencies and performs basic deduplication / hoisting.
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 maps for resolved packages, in-flight requests, and the circular dependency stack.
20
+ * Initializes internal state for resolution tracking.
18
21
  */
19
22
  constructor() {
20
- /** @type {Map<string, object>} Map of "name@version" to package metadata */
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
- /** @type {Map<string, Promise>} Map of "name@range" to active resolution promises */
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
- /** @type {string[]} Stack of package names being resolved to detect cycles */
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={}] - Regular dependencies (name to semver range)
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
- * @returns {Promise<Map<string, object>>} A promise that resolves to the flat map of resolved packages
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 of the package as declared in dependencies
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 to avoid redundant work
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, // The actual package name for installation
123
- alias: name !== targetName ? name : undefined, // Alias used in package.json
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
- // Check if it's an optional dependency and if it's compatible with current system
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 packument fetch fails, we'll let _resolveOne handle it normally
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 suggests resolution to the highest version.
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
- * Traverses the resolved dependency graph to identify circular references.
228
+ * Performs a Depth-First Search on the resolved graph to detect cycles.
204
229
  *
205
- * @returns {string[]} An array of strings describing the detected cycles (e.g., "A → B → A")
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
+