Skip to content

Commit eb00fcc

Browse files
committed
[New] add support for the exports package.json attribute
1 parent 0db50df commit eb00fcc

File tree

15 files changed

+538
-39
lines changed

15 files changed

+538
-39
lines changed

lib/async.js

+87-18
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
/* eslint-disable max-lines */
12
var fs = require('fs');
23
var path = require('path');
34
var caller = require('./caller.js');
45
var nodeModulesPaths = require('./node-modules-paths.js');
56
var normalizeOptions = require('./normalize-options.js');
67
var isCore = require('./is-core');
8+
var resolveExports = require('./resolve-exports.js');
79

810
var realpathFS = fs.realpath && typeof fs.realpath.native === 'function' ? fs.realpath.native : fs.realpath;
911

@@ -75,6 +77,7 @@ module.exports = function resolve(x, options, callback) {
7577
var extensions = opts.extensions || ['.js'];
7678
var basedir = opts.basedir || path.dirname(caller());
7779
var parent = opts.filename || basedir;
80+
var env = opts.env || [];
7881

7982
opts.paths = opts.paths || [];
8083

@@ -278,35 +281,101 @@ module.exports = function resolve(x, options, callback) {
278281
});
279282
}
280283

281-
function processDirs(cb, dirs) {
284+
function loadManifestInDir(dir, cb) {
285+
maybeRealpath(realpath, dir, opts, function (err, pkgdir) {
286+
if (err) return cb(null);
287+
288+
var pkgfile = path.join(pkgdir, 'package.json');
289+
isFile(pkgfile, function (err, ex) {
290+
// on err, ex is false
291+
if (!ex) return cb(null);
292+
293+
readFile(pkgfile, function (err, body) {
294+
if (err) cb(err);
295+
try { var pkg = JSON.parse(body); } catch (jsonErr) {}
296+
297+
if (pkg && opts.packageFilter) {
298+
pkg = opts.packageFilter(pkg, pkgfile, dir);
299+
}
300+
cb(pkg);
301+
});
302+
});
303+
});
304+
}
305+
306+
function processDirs(cb, dirs, subpath) {
282307
if (dirs.length === 0) return cb(null, undefined);
283308
var dir = dirs[0];
284309

285-
isDirectory(path.dirname(dir), isdir);
286-
287-
function isdir(err, isdir) {
288-
if (err) return cb(err);
289-
if (!isdir) return processDirs(cb, dirs.slice(1));
290-
loadAsFile(dir, opts.package, onfile);
310+
if (env.length > 0) {
311+
loadManifestInDir(dir, onmanifest);
312+
} else {
313+
onmanifest();
291314
}
292315

293-
function onfile(err, m, pkg) {
294-
if (err) return cb(err);
295-
if (m) return cb(null, m, pkg);
296-
loadAsDirectory(dir, opts.package, ondir);
297-
}
316+
function onmanifest(pkg) {
317+
var resolvedX;
318+
var tryLoadAsDirectory = true;
298319

299-
function ondir(err, n, pkg) {
300-
if (err) return cb(err);
301-
if (n) return cb(null, n, pkg);
302-
processDirs(cb, dirs.slice(1));
320+
if (pkg && pkg.exports) {
321+
try {
322+
var resolvedExport = resolveExports(dir, parent, subpath, pkg.exports, env);
323+
} catch (resolveErr) {
324+
return cb(resolveErr);
325+
}
326+
327+
if (resolvedExport) {
328+
resolvedX = resolvedExport.path;
329+
tryLoadAsDirectory = resolvedExport.isDirectoryExport;
330+
}
331+
}
332+
333+
if (!resolvedX) {
334+
resolvedX = dir + subpath;
335+
}
336+
337+
isDirectory(path.dirname(resolvedX), isdir);
338+
339+
function isdir(err, isdir) {
340+
if (err) return cb(err);
341+
if (!isdir) return processDirs(cb, dirs.slice(1), subpath);
342+
loadAsFile(resolvedX, opts.package, onfile);
343+
}
344+
345+
function onfile(err, m, pkg) {
346+
if (err) return cb(err);
347+
if (m) return cb(null, m, pkg);
348+
if (!tryLoadAsDirectory) return processDirs(cb, dirs.slice(1), subpath);
349+
loadAsDirectory(resolvedX, opts.package, ondir);
350+
}
351+
352+
function ondir(err, n, pkg) {
353+
if (err) return cb(err);
354+
if (n) return cb(null, n, pkg);
355+
processDirs(cb, dirs.slice(1), subpath);
356+
}
303357
}
358+
304359
}
305360
function loadNodeModules(x, start, cb) {
306-
var thunk = function () { return getPackageCandidates(x, start, opts); };
361+
var subpathIndex = x.indexOf('/');
362+
if (x[0] === '@') {
363+
subpathIndex = x.indexOf('/', subpathIndex + 1);
364+
}
365+
var packageName, subpath;
366+
if (subpathIndex === -1) {
367+
packageName = x;
368+
subpath = '';
369+
} else {
370+
packageName = x.substring(0, subpathIndex);
371+
subpath = x.substring(subpathIndex);
372+
}
373+
374+
var thunk = function () { return getPackageCandidates(packageName, start, opts); };
307375
processDirs(
308376
cb,
309-
packageIterator ? packageIterator(x, start, thunk, opts) : thunk()
377+
packageIterator ? packageIterator(x, start, thunk, opts) : thunk(),
378+
subpath
310379
);
311380
}
312381
};

lib/resolve-exports.js

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
var path = require('path');
2+
3+
function makeError(code, message) {
4+
var error = new Error(message);
5+
error.code = code;
6+
return error;
7+
}
8+
9+
function validateExports(exports, basePath) {
10+
var isConditional = true;
11+
12+
if (typeof exports === 'object' && !Array.isArray(exports)) {
13+
var exportKeys = Object.keys(exports);
14+
15+
for (var i = 0; i < exportKeys.length; i++) {
16+
var isKeyConditional = exportKeys[i][0] !== '.';
17+
if (i === 0) {
18+
isConditional = isKeyConditional;
19+
} else if (isKeyConditional !== isConditional) {
20+
throw makeError('ERR_INVALID_PACKAGE_CONFIG', 'Invalid package config ' + basePath + path.sep + 'package.json, '
21+
+ '"exports" cannot contain some keys starting with \'.\' and some not. '
22+
+ 'The exports object must either be an object of package subpath keys '
23+
+ 'or an object of main entry condition name keys only.');
24+
}
25+
}
26+
}
27+
28+
if (isConditional) {
29+
return { '.': exports };
30+
} else {
31+
return exports;
32+
}
33+
}
34+
35+
function validateEnvNames(names) {
36+
// TODO If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error.
37+
return names;
38+
}
39+
40+
var startsWith;
41+
42+
if (typeof String.prototype.startsWith === 'function') {
43+
startsWith = function (path, exportedPath) {
44+
return path.startsWith(exportedPath);
45+
};
46+
} else {
47+
startsWith = function (path, exportedPath) {
48+
for (var i = 0; i < exportedPath.length; i++) {
49+
if (path[i] !== exportedPath[i]) {
50+
return false;
51+
}
52+
}
53+
54+
return true;
55+
};
56+
}
57+
58+
function resolveExportsTarget(packagePath, parent, key, target, subpath, env) {
59+
if (typeof target === 'string') {
60+
var resolvedTarget = path.resolve(packagePath, target);
61+
if (!(/^\.\//).test(target) || resolvedTarget.indexOf('/node_modules/', packagePath.length - 1) !== -1 || !startsWith(resolvedTarget, packagePath)) {
62+
throw makeError('ERR_INVALID_PACKAGE_TARGET', 'Invalid "exports" target ' + JSON.stringify(target)
63+
+ ' defined for ' + key + ' in ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
64+
}
65+
66+
if (subpath !== '' && target[target.length - 1] !== '/') {
67+
throw makeError('ERR_INVALID_MODULE_SPECIFIER', 'Package subpath "' + subpath + '" is not a valid module request for '
68+
+ 'the "exports" resolution of ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
69+
}
70+
71+
var resolved = path.normalize(resolvedTarget + subpath);
72+
73+
if (!startsWith(resolved, resolvedTarget)) {
74+
throw makeError('ERR_INVALID_MODULE_SPECIFIER', 'Package subpath "' + subpath + '" is not a valid module request for '
75+
+ 'the "exports" resolution of ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
76+
}
77+
78+
return resolved;
79+
}
80+
81+
if (Array.isArray(target)) {
82+
if (target.length === 0) {
83+
throw makeError('ERR_PACKAGE_PATH_NOT_EXPORTED', key === '.'
84+
? 'No "exports" main resolved in ' + packagePath + path.sep + 'package.json.'
85+
: 'Package subpath ' + key + ' is not defined by "exports" in ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
86+
}
87+
88+
var lastError;
89+
for (var i = 0; i < target.length; i++) {
90+
try {
91+
return resolveExportsTarget(packagePath, parent, key, target[i], subpath, env);
92+
} catch (e) {
93+
if (e && (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || e.code === 'ERR_INVALID_PACKAGE_TARGET')) {
94+
lastError = e;
95+
} else {
96+
throw e;
97+
}
98+
}
99+
}
100+
throw lastError;
101+
}
102+
103+
if (target === null) {
104+
throw makeError('ERR_PACKAGE_PATH_NOT_EXPORTED', key === '.'
105+
? 'No "exports" main resolved in ' + packagePath + path.sep + 'package.json.'
106+
: 'Package subpath ' + key + ' is not defined by "exports" in ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
107+
}
108+
109+
if (typeof target !== 'object') {
110+
throw makeError('ERR_INVALID_PACKAGE_TARGET', 'Invalid "exports" target ' + JSON.stringify(target)
111+
+ ' defined for ' + key + ' in ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
112+
}
113+
114+
var exportedEnvs = validateEnvNames(Object.keys(target));
115+
116+
for (i = 0; i < exportedEnvs.length; i++) {
117+
var exportedEnv = exportedEnvs[i];
118+
if (exportedEnv === 'default' || env.indexOf(exportedEnv) === -1) {
119+
try {
120+
return resolveExportsTarget(packagePath, parent, key, target[exportedEnv], subpath, env);
121+
} catch (e) {
122+
if (!e || e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
123+
throw e;
124+
}
125+
}
126+
}
127+
}
128+
129+
throw makeError('ERR_PACKAGE_PATH_NOT_EXPORTED', key === '.'
130+
? 'No "exports" main resolved in ' + packagePath + path.sep + 'package.json.'
131+
: 'Package subpath ' + key + ' is not defined by "exports" in ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
132+
}
133+
134+
module.exports = function resolveExports(packagePath, parent, subpath, exports, env) {
135+
exports = validateExports(exports, packagePath); // eslint-disable-line no-param-reassign
136+
subpath = '.' + subpath; // eslint-disable-line no-param-reassign
137+
138+
if (subpath === '.' && exports['.'] === undefined) {
139+
return;
140+
}
141+
142+
var resolved;
143+
var isDirectoryExport;
144+
if (Object.prototype.hasOwnProperty.call(exports, subpath)) {
145+
resolved = resolveExportsTarget(packagePath, parent, subpath, exports[subpath], '', env);
146+
isDirectoryExport = false;
147+
} else {
148+
var longestMatchingExport = '';
149+
var exportedPaths = Object.keys(exports);
150+
151+
for (var i = 0; i < exportedPaths.length; i++) {
152+
var exportedPath = exportedPaths[i];
153+
if (exportedPath[exportedPath.length - 1] === '/' && startsWith(subpath, exportedPath) && exportedPath.length > longestMatchingExport.length) {
154+
longestMatchingExport = exportedPath;
155+
}
156+
}
157+
158+
if (longestMatchingExport === '') {
159+
throw makeError('ERR_PACKAGE_PATH_NOT_EXPORTED', 'Package subpath ' + subpath + ' is not defined by "exports" in '
160+
+ packagePath + path.sep + 'package.json imported from ' + parent + '.');
161+
}
162+
163+
resolved = resolveExportsTarget(
164+
packagePath,
165+
parent,
166+
longestMatchingExport,
167+
exports[longestMatchingExport],
168+
subpath.substring(longestMatchingExport.length - 1),
169+
env
170+
);
171+
isDirectoryExport = true;
172+
}
173+
174+
return {
175+
path: resolved,
176+
isDirectoryExport: isDirectoryExport
177+
};
178+
};

0 commit comments

Comments
 (0)