Skip to content

Commit d907bc8

Browse files
committed
[New] add support for the exports package.json attribute
1 parent 46cafa1 commit d907bc8

File tree

23 files changed

+759
-37
lines changed

23 files changed

+759
-37
lines changed

Diff for: lib/async.js

+103-16
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-imports-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 conditions = opts.ignoreExportsField === false ? ['require', 'node'] : [];
7881

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

@@ -278,35 +281,119 @@ 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, endsWithSubpath) {
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 (conditions.length > 0 && endsWithSubpath(dir)) {
311+
var pkgDir = dir.slice(0, dir.length - subpath.length);
312+
loadManifestInDir(pkgDir, onmanifestWithExports);
313+
} else {
314+
onmanifest(false);
291315
}
292316

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);
317+
function onmanifestWithExports(pkg) {
318+
if (!pkg || pkg.exports === null || pkg.exports === undefined) {
319+
return onmanifest(false);
320+
}
321+
322+
var resolvedExport;
323+
try {
324+
resolvedExport = resolveExports(pkgDir, parent, subpath, pkg.exports, conditions);
325+
} catch (resolveErr) {
326+
return cb(resolveErr);
327+
}
328+
329+
if (resolvedExport.exact) {
330+
isFile(resolvedExport.resolved, function (err, ex) {
331+
if (ex) {
332+
cb(null, resolvedExport.resolved, pkg);
333+
} else {
334+
cb(null, undefined);
335+
}
336+
});
337+
} else {
338+
dir = resolvedExport.resolved;
339+
onmanifest(true);
340+
}
297341
}
298342

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));
343+
function onmanifest(stop) {
344+
isDirectory(path.dirname(dir), isdir);
345+
346+
function isdir(err, isdir) {
347+
if (err) return cb(err);
348+
if (!isdir) return next();
349+
loadAsFile(dir, opts.package, onfile);
350+
}
351+
352+
function onfile(err, m, pkg) {
353+
if (err) return cb(err);
354+
if (m) return cb(null, m, pkg);
355+
loadAsDirectory(dir, opts.package, ondir);
356+
}
357+
358+
function ondir(err, n, pkg) {
359+
if (err) return cb(err);
360+
if (n) return cb(null, n, pkg);
361+
next();
362+
}
363+
364+
function next() {
365+
if (stop) {
366+
cb(null, undefined);
367+
} else {
368+
processDirs(cb, dirs.slice(1), subpath, endsWithSubpath);
369+
}
370+
}
303371
}
372+
304373
}
305374
function loadNodeModules(x, start, cb) {
375+
var subpathIndex = x.indexOf('/');
376+
if (x[0] === '@') {
377+
subpathIndex = x.indexOf('/', subpathIndex + 1);
378+
}
379+
var subpath;
380+
if (subpathIndex === -1) {
381+
subpath = '';
382+
} else {
383+
subpath = x.slice(subpathIndex);
384+
}
385+
306386
var thunk = function () { return getPackageCandidates(x, start, opts); };
387+
var endsWithSubpath = function (dir) {
388+
var endOfDir = dir.slice(dir.length - subpath.length);
389+
390+
return endOfDir === subpath || endOfDir.replace(/\\/g, '/') === subpath;
391+
};
307392
processDirs(
308393
cb,
309-
packageIterator ? packageIterator(x, start, thunk, opts) : thunk()
394+
packageIterator ? packageIterator(x, start, thunk, opts) : thunk(),
395+
subpath,
396+
endsWithSubpath
310397
);
311398
}
312399
};

Diff for: lib/resolve-imports-exports.js

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

0 commit comments

Comments
 (0)