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.
@@ -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;