jpm-pkg 1.0.3
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.md +21 -0
- package/README.md +148 -0
- package/bin/jpm.js +252 -0
- package/package.json +52 -0
- package/src/commands/audit.js +56 -0
- package/src/commands/config.js +59 -0
- package/src/commands/info.js +78 -0
- package/src/commands/init.js +88 -0
- package/src/commands/install.js +139 -0
- package/src/commands/list.js +103 -0
- package/src/commands/publish.js +148 -0
- package/src/commands/run.js +72 -0
- package/src/commands/search.js +41 -0
- package/src/commands/uninstall.js +48 -0
- package/src/commands/update.js +63 -0
- package/src/commands/x.js +136 -0
- package/src/core/cache.js +117 -0
- package/src/core/installer.js +316 -0
- package/src/core/lockfile.js +128 -0
- package/src/core/package-json.js +133 -0
- package/src/core/registry.js +166 -0
- package/src/core/resolver.js +248 -0
- package/src/security/audit.js +100 -0
- package/src/security/integrity.js +70 -0
- package/src/utils/config.js +138 -0
- package/src/utils/env.js +31 -0
- package/src/utils/fs.js +154 -0
- package/src/utils/http.js +232 -0
- package/src/utils/logger.js +128 -0
- package/src/utils/lru-cache.js +66 -0
- package/src/utils/progress.js +142 -0
- package/src/utils/semver.js +279 -0
- package/src/utils/system.js +39 -0
- package/src/workspace/workspace.js +126 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { readJSONSafe, writeJSON } = require('../utils/fs');
|
|
5
|
+
const { hashString } = require('../security/integrity');
|
|
6
|
+
|
|
7
|
+
const LOCK_VERSION = 1;
|
|
8
|
+
const LOCK_FILE = 'jpm-lock.json';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* jpm-lock.json structure:
|
|
12
|
+
* {
|
|
13
|
+
* "lockVersion": 1,
|
|
14
|
+
* "packages": {
|
|
15
|
+
* "express@4.18.2": {
|
|
16
|
+
* "name": "express",
|
|
17
|
+
* "version": "4.18.2",
|
|
18
|
+
* "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
|
19
|
+
* "integrity": "sha512-...",
|
|
20
|
+
* "dependencies": { "accepts": "^1.3.8", ... }
|
|
21
|
+
* },
|
|
22
|
+
* ...
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
class Lockfile {
|
|
28
|
+
constructor(projectRoot) {
|
|
29
|
+
this.filePath = path.join(projectRoot, LOCK_FILE);
|
|
30
|
+
this._data = this._load();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_load() {
|
|
34
|
+
const data = readJSONSafe(this.filePath, null);
|
|
35
|
+
if (!data) return { lockVersion: LOCK_VERSION, packages: {} };
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build/update lock file from a resolved package map
|
|
41
|
+
*/
|
|
42
|
+
update(resolvedMap) {
|
|
43
|
+
const packages = {};
|
|
44
|
+
const keys = Array.from(resolvedMap.keys()).sort();
|
|
45
|
+
for (const key of keys) {
|
|
46
|
+
const meta = resolvedMap.get(key);
|
|
47
|
+
packages[key] = {
|
|
48
|
+
name: meta.name,
|
|
49
|
+
version: meta.version,
|
|
50
|
+
resolved: meta.resolved || '',
|
|
51
|
+
integrity: meta.integrity || '',
|
|
52
|
+
shasum: meta.shasum || '',
|
|
53
|
+
dependencies: meta.deps || {},
|
|
54
|
+
engines: meta.engines || {},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const integrity = hashString(JSON.stringify(packages));
|
|
59
|
+
this._data = {
|
|
60
|
+
lockVersion: LOCK_VERSION,
|
|
61
|
+
integrity,
|
|
62
|
+
packages
|
|
63
|
+
};
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Verifies if the lockfile has been tampered with.
|
|
69
|
+
*/
|
|
70
|
+
verify() {
|
|
71
|
+
if (!this._data.integrity || !this._data.packages) return true;
|
|
72
|
+
const actual = hashString(JSON.stringify(this._data.packages));
|
|
73
|
+
return actual === this._data.integrity;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Add or update a single package entry */
|
|
77
|
+
setPackage(name, version, meta) {
|
|
78
|
+
const key = `${name}@${version}`;
|
|
79
|
+
this._data.packages[key] = {
|
|
80
|
+
name,
|
|
81
|
+
version,
|
|
82
|
+
resolved: meta.resolved || '',
|
|
83
|
+
integrity: meta.integrity || '',
|
|
84
|
+
shasum: meta.shasum || '',
|
|
85
|
+
dependencies: meta.dependencies || {},
|
|
86
|
+
};
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Remove a package entry */
|
|
91
|
+
removePackage(name, version) {
|
|
92
|
+
const key = `${name}@${version}`;
|
|
93
|
+
delete this._data.packages[key];
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Look up a specific package in the lock file */
|
|
98
|
+
getPackage(name, version) {
|
|
99
|
+
return this._data.packages[`${name}@${version}`] || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Check whether the lock file has a resolved entry for name@range */
|
|
103
|
+
hasPackage(name, version) {
|
|
104
|
+
return Boolean(this._data.packages[`${name}@${version}`]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Return all locked packages as array */
|
|
108
|
+
allPackages() {
|
|
109
|
+
return Object.values(this._data.packages);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Write lock file to disk */
|
|
113
|
+
save() {
|
|
114
|
+
writeJSON(this.filePath, this._data, 2);
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Raw data */
|
|
119
|
+
get data() { return this._data; }
|
|
120
|
+
|
|
121
|
+
/** Whether a lock file exists */
|
|
122
|
+
exists() {
|
|
123
|
+
const fs = require('node:fs');
|
|
124
|
+
return fs.existsSync(this.filePath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = Lockfile;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const { readJSONSafe, writeJSON, findPackageJson } = require('../utils/fs');
|
|
6
|
+
const logger = require('../utils/logger');
|
|
7
|
+
|
|
8
|
+
const REQUIRED_FIELDS = ['name', 'version'];
|
|
9
|
+
|
|
10
|
+
class PackageJSON {
|
|
11
|
+
constructor(filePath) {
|
|
12
|
+
this.filePath = filePath;
|
|
13
|
+
this._data = this._load();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static fromDir(dir) {
|
|
17
|
+
const f = path.join(dir, 'package.json');
|
|
18
|
+
return new PackageJSON(f);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static find(startDir = process.cwd()) {
|
|
22
|
+
const f = findPackageJson(startDir);
|
|
23
|
+
if (!f) throw new Error('No package.json found in this directory or any parent.');
|
|
24
|
+
return new PackageJSON(f);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_load() {
|
|
28
|
+
const data = readJSONSafe(this.filePath, null);
|
|
29
|
+
if (!data) return this._empty();
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_empty() {
|
|
34
|
+
return {
|
|
35
|
+
name: path.basename(path.dirname(this.filePath || process.cwd())),
|
|
36
|
+
version: '1.0.0',
|
|
37
|
+
description: '',
|
|
38
|
+
main: 'index.js',
|
|
39
|
+
scripts: { test: 'echo "Error: no test specified" && exit 1' },
|
|
40
|
+
keywords: [],
|
|
41
|
+
author: '',
|
|
42
|
+
license: 'MIT',
|
|
43
|
+
dependencies: {},
|
|
44
|
+
devDependencies: {},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Accessors ────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
get name() { return this._data.name; }
|
|
51
|
+
get version() { return this._data.version; }
|
|
52
|
+
get dependencies() { return this._data.dependencies || {}; }
|
|
53
|
+
get devDependencies() { return this._data.devDependencies || {}; }
|
|
54
|
+
get peerDependencies() { return this._data.peerDependencies || {}; }
|
|
55
|
+
get optionalDeps() { return this._data.optionalDependencies || {}; }
|
|
56
|
+
get scripts() { return this._data.scripts || {}; }
|
|
57
|
+
get workspaces() { return this._data.workspaces || []; }
|
|
58
|
+
get main() { return this._data.main || 'index.js'; }
|
|
59
|
+
get bin() { return this._data.bin || {}; }
|
|
60
|
+
get engines() { return this._data.engines || {}; }
|
|
61
|
+
get data() { return this._data; }
|
|
62
|
+
get dir() { return path.dirname(this.filePath); }
|
|
63
|
+
|
|
64
|
+
// ── Mutations ────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
addDependency(name, version, { dev = false, exact = false } = {}) {
|
|
67
|
+
const range = exact ? version : `^${version}`;
|
|
68
|
+
const key = dev ? 'devDependencies' : 'dependencies';
|
|
69
|
+
if (!this._data[key]) this._data[key] = {};
|
|
70
|
+
this._data[key][name] = range;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
removeDependency(name) {
|
|
75
|
+
for (const key of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
76
|
+
if (this._data[key]) delete this._data[key][name];
|
|
77
|
+
}
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setField(key, value) {
|
|
82
|
+
this._data[key] = value;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setVersion(version) {
|
|
87
|
+
this._data.version = version;
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
addScript(name, command) {
|
|
92
|
+
if (!this._data.scripts) this._data.scripts = {};
|
|
93
|
+
this._data.scripts[name] = command;
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Validation ────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
validate() {
|
|
100
|
+
const errors = [];
|
|
101
|
+
for (const field of REQUIRED_FIELDS) {
|
|
102
|
+
if (!this._data[field]) errors.push(`Missing required field: "${field}"`);
|
|
103
|
+
}
|
|
104
|
+
if (this._data.name && !/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(this._data.name)) {
|
|
105
|
+
errors.push(`Invalid package name: "${this._data.name}"`);
|
|
106
|
+
}
|
|
107
|
+
if (this._data.version && !/^\d+\.\d+\.\d+/.test(this._data.version)) {
|
|
108
|
+
errors.push(`Invalid version: "${this._data.version}"`);
|
|
109
|
+
}
|
|
110
|
+
return errors;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Persistence ──────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
save() {
|
|
116
|
+
writeJSON(this.filePath, this._data, 2);
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
exists() {
|
|
121
|
+
return fs.existsSync(this.filePath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
allDeps() {
|
|
125
|
+
return {
|
|
126
|
+
...this.dependencies,
|
|
127
|
+
...this.devDependencies,
|
|
128
|
+
...this.optionalDeps,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = PackageJSON;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getJSON, download } = require('../utils/http');
|
|
4
|
+
const config = require('../utils/config');
|
|
5
|
+
const logger = require('../utils/logger');
|
|
6
|
+
|
|
7
|
+
const REGISTRY = () => config.registry.replace(/\/$/, '');
|
|
8
|
+
|
|
9
|
+
const LRUCache = require('../utils/lru-cache');
|
|
10
|
+
const env = require('../utils/env');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* In-memory metadata cache for registry requests, optimized for resolution speed.
|
|
14
|
+
* @type {LRUCache}
|
|
15
|
+
* @private
|
|
16
|
+
*/
|
|
17
|
+
const metaCache = new LRUCache(1000);
|
|
18
|
+
|
|
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
|
+
}
|
|
38
|
+
|
|
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
|
+
}
|
|
57
|
+
|
|
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
|
+
}
|
|
68
|
+
|
|
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
|
+
}
|
|
79
|
+
|
|
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
|
+
}
|
|
90
|
+
|
|
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
|
+
}
|
|
107
|
+
|
|
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
|
+
}
|
|
122
|
+
|
|
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;
|
|
134
|
+
}
|
|
135
|
+
|
|
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: {} }; }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
getPackument, getVersion, getVersions,
|
|
164
|
+
getLatest, getDistTags,
|
|
165
|
+
downloadTarball, search, fetchAdvisories,
|
|
166
|
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const registry = require('./registry');
|
|
4
|
+
const semver = require('../utils/semver');
|
|
5
|
+
const system = require('../utils/system');
|
|
6
|
+
const logger = require('../utils/logger');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves a full dependency tree given a root package.json dependencies map.
|
|
10
|
+
*
|
|
11
|
+
* Returns a flat map: { "name@version" => { name, version, resolved, integrity, dependencies } }
|
|
12
|
+
* Also detects circular dependencies and performs basic deduplication / hoisting.
|
|
13
|
+
*/
|
|
14
|
+
class Resolver {
|
|
15
|
+
/**
|
|
16
|
+
* Creates an instance of the Resolver.
|
|
17
|
+
* Initializes maps for resolved packages, in-flight requests, and the circular dependency stack.
|
|
18
|
+
*/
|
|
19
|
+
constructor() {
|
|
20
|
+
/** @type {Map<string, object>} Map of "name@version" to package metadata */
|
|
21
|
+
this._resolved = new Map();
|
|
22
|
+
/** @type {Map<string, Promise>} Map of "name@range" to active resolution promises */
|
|
23
|
+
this._inFlight = new Map();
|
|
24
|
+
/** @type {string[]} Stack of package names being resolved to detect cycles */
|
|
25
|
+
this._stack = [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolves a set of dependencies recursively.
|
|
30
|
+
*
|
|
31
|
+
* @param {Object.<string, string>} [deps={}] - Regular dependencies (name to semver range)
|
|
32
|
+
* @param {Object.<string, string>} [devDeps={}] - Development dependencies
|
|
33
|
+
* @param {Object.<string, string>} [peerDeps={}] - Peer dependencies
|
|
34
|
+
* @returns {Promise<Map<string, object>>} A promise that resolves to the flat map of resolved packages
|
|
35
|
+
*/
|
|
36
|
+
async resolve(deps = {}, devDeps = {}, peerDeps = {}, onProgress) {
|
|
37
|
+
const all = { ...deps, ...devDeps };
|
|
38
|
+
this._totalToResolve = Object.keys(all).length;
|
|
39
|
+
this._resolvedCount = 0;
|
|
40
|
+
this._onProgress = onProgress;
|
|
41
|
+
|
|
42
|
+
await Promise.all(
|
|
43
|
+
Object.entries(all).map(([name, range]) => this._resolveOne(name, range))
|
|
44
|
+
);
|
|
45
|
+
return this._resolved;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async _resolveOne(name, range) {
|
|
49
|
+
const key = `${name}@${range}`;
|
|
50
|
+
|
|
51
|
+
// Already in flight? Await the existing promise.
|
|
52
|
+
if (this._inFlight.has(key)) {
|
|
53
|
+
return this._inFlight.get(key);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create a new resolution promise and store it in _inFlight.
|
|
57
|
+
const p = this._doResolve(name, range);
|
|
58
|
+
this._inFlight.set(key, p);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await p;
|
|
62
|
+
this._resolvedCount++;
|
|
63
|
+
this._onProgress?.(this._resolvedCount, this._totalToResolve);
|
|
64
|
+
return p;
|
|
65
|
+
} finally {
|
|
66
|
+
// No longer in flight once the promise settles.
|
|
67
|
+
this._inFlight.delete(key);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Internal resolution logic for a single package and range.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} name - The name of the package as declared in dependencies
|
|
75
|
+
* @param {string} range - The semver range or npm:alias
|
|
76
|
+
* @protected
|
|
77
|
+
*/
|
|
78
|
+
async _doResolve(name, range) {
|
|
79
|
+
// Handle npm: alias protocol (e.g., "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0")
|
|
80
|
+
let targetName = name;
|
|
81
|
+
let targetRange = range === '' || range === '*' || range === 'latest' ? 'latest' : range;
|
|
82
|
+
|
|
83
|
+
if (targetRange.startsWith('npm:')) {
|
|
84
|
+
const parts = targetRange.slice(4).split('@');
|
|
85
|
+
// Handle scoped packages in alias: npm:@scope/pkg@range
|
|
86
|
+
if (targetRange.slice(4).startsWith('@')) {
|
|
87
|
+
targetName = '@' + parts[1];
|
|
88
|
+
targetRange = parts[2] || 'latest';
|
|
89
|
+
} else {
|
|
90
|
+
targetName = parts[0];
|
|
91
|
+
targetRange = parts[1] || 'latest';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// 1. Fetch available versions for the target package
|
|
97
|
+
const versions = await registry.getVersions(targetName);
|
|
98
|
+
|
|
99
|
+
// 2. Select the best matching version candidates
|
|
100
|
+
const chosen = targetRange === 'latest'
|
|
101
|
+
? await registry.getLatest(targetName)
|
|
102
|
+
: semver.maxSatisfying(versions, targetRange);
|
|
103
|
+
|
|
104
|
+
if (!chosen) {
|
|
105
|
+
throw new Error(`No version of ${targetName} satisfies "${targetRange}". Available: ${versions.slice(-5).join(', ')}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The resolved key uses the original dependency name to allow multiple aliases of the same package
|
|
109
|
+
const resolvedKey = `${name}@${chosen}`;
|
|
110
|
+
|
|
111
|
+
// Check if already resolved to avoid redundant work
|
|
112
|
+
if (this._resolved.has(resolvedKey)) return;
|
|
113
|
+
|
|
114
|
+
// Detect circular dependencies on the current resolution path
|
|
115
|
+
if (this._stack.includes(resolvedKey)) return;
|
|
116
|
+
|
|
117
|
+
// 3. Retrieve exhaustive version metadata
|
|
118
|
+
const meta = await registry.getVersion(targetName, chosen);
|
|
119
|
+
|
|
120
|
+
// 4. Map metadata to internal representation
|
|
121
|
+
const metaToStore = {
|
|
122
|
+
name: targetName, // The actual package name for installation
|
|
123
|
+
alias: name !== targetName ? name : undefined, // Alias used in package.json
|
|
124
|
+
version: chosen,
|
|
125
|
+
resolved: meta.dist?.tarball,
|
|
126
|
+
integrity: meta.dist?.integrity || meta.dist?.shasum,
|
|
127
|
+
shasum: meta.dist?.shasum,
|
|
128
|
+
deps: meta.dependencies || {},
|
|
129
|
+
devDeps: meta.devDependencies || {},
|
|
130
|
+
peerDeps: meta.peerDependencies || {},
|
|
131
|
+
optDeps: meta.optionalDependencies || {},
|
|
132
|
+
engines: meta.engines || {},
|
|
133
|
+
bin: meta.bin || {},
|
|
134
|
+
scripts: meta.scripts || {},
|
|
135
|
+
};
|
|
136
|
+
this._resolved.set(resolvedKey, metaToStore);
|
|
137
|
+
|
|
138
|
+
// 5. Recursively resolve transitive dependencies
|
|
139
|
+
this._stack.push(resolvedKey);
|
|
140
|
+
|
|
141
|
+
// Combine normal and optional dependencies for resolution
|
|
142
|
+
const dependencies = {
|
|
143
|
+
...metaToStore.deps,
|
|
144
|
+
...metaToStore.optDeps
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
await Promise.all(
|
|
148
|
+
Object.entries(dependencies).map(async ([depName, depRange]) => {
|
|
149
|
+
// Check if it's an optional dependency and if it's compatible with current system
|
|
150
|
+
if (metaToStore.optDeps[depName]) {
|
|
151
|
+
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
|
+
const packument = await registry.getPackument(depName);
|
|
157
|
+
const version = semver.maxSatisfying(Object.keys(packument.versions || {}), depRange);
|
|
158
|
+
if (version) {
|
|
159
|
+
const depMeta = packument.versions[version];
|
|
160
|
+
if (depMeta && !system.isCompatible(depMeta)) {
|
|
161
|
+
logger.debug(`Skipping optional dependency ${depName}@${version}: incompatible platform`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {
|
|
166
|
+
// If packument fetch fails, we'll let _resolveOne handle it normally
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return this._resolveOne(depName, depRange);
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
this._stack.pop();
|
|
173
|
+
|
|
174
|
+
} catch (err) {
|
|
175
|
+
logger.error(`Failed to resolve ${name}@${range}: ${err.message}`);
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Analysis helpers ────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Identifies package name collisions and suggests resolution to the highest version.
|
|
184
|
+
*
|
|
185
|
+
* @returns {Object[]} List of duplicate packages and their versions
|
|
186
|
+
*/
|
|
187
|
+
deduplicate() {
|
|
188
|
+
const byName = new Map();
|
|
189
|
+
for (const [key, meta] of this._resolved) {
|
|
190
|
+
if (!byName.has(meta.name)) byName.set(meta.name, []);
|
|
191
|
+
byName.get(meta.name).push(meta);
|
|
192
|
+
}
|
|
193
|
+
const dupes = [];
|
|
194
|
+
for (const [name, metas] of byName) {
|
|
195
|
+
if (metas.length > 1) {
|
|
196
|
+
dupes.push({ name, versions: metas.map(m => m.version) });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return dupes;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Traverses the resolved dependency graph to identify circular references.
|
|
204
|
+
*
|
|
205
|
+
* @returns {string[]} An array of strings describing the detected cycles (e.g., "A → B → A")
|
|
206
|
+
*/
|
|
207
|
+
findCircular() {
|
|
208
|
+
const cycles = [];
|
|
209
|
+
const visited = new Set();
|
|
210
|
+
const path = [];
|
|
211
|
+
|
|
212
|
+
const dfs = (key) => {
|
|
213
|
+
if (path.includes(key)) {
|
|
214
|
+
const cycle = path.slice(path.indexOf(key));
|
|
215
|
+
cycles.push(cycle.join(' → ') + ' → ' + key);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (visited.has(key)) return;
|
|
219
|
+
visited.add(key);
|
|
220
|
+
path.push(key);
|
|
221
|
+
|
|
222
|
+
const meta = this._resolved.get(key);
|
|
223
|
+
if (meta) {
|
|
224
|
+
// We must match dependency ranges to their ACTUAL resolved versions in this._resolved
|
|
225
|
+
for (const [depName, depRange] of Object.entries(meta.deps)) {
|
|
226
|
+
// Find the version of depName that was actually resolved
|
|
227
|
+
for (const [resKey, resMeta] of this._resolved) {
|
|
228
|
+
if (resMeta.name === depName && semver.satisfies(resMeta.version, depRange)) {
|
|
229
|
+
dfs(resKey);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
path.pop();
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
for (const [key] of this._resolved) dfs(key);
|
|
238
|
+
return [...new Set(cycles)];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Returns the complete flat map of resolved dependency metadata.
|
|
243
|
+
* @type {Map<string, Object>}
|
|
244
|
+
*/
|
|
245
|
+
get resolved() { return this._resolved; }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = Resolver;
|