Skip to content

Commit

Permalink
[New] add support for the exports package.json attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
bgotink committed Sep 29, 2020
1 parent 7c26483 commit 016c79e
Show file tree
Hide file tree
Showing 23 changed files with 759 additions and 37 deletions.
119 changes: 103 additions & 16 deletions lib/async.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* eslint-disable max-lines */
var fs = require('fs');
var path = require('path');
var caller = require('./caller.js');
var nodeModulesPaths = require('./node-modules-paths.js');
var normalizeOptions = require('./normalize-options.js');
var isCore = require('is-core-module');
var resolveExports = require('./resolve-imports-exports.js');

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

Expand Down Expand Up @@ -75,6 +77,7 @@ module.exports = function resolve(x, options, callback) {
var extensions = opts.extensions || ['.js'];
var basedir = opts.basedir || path.dirname(caller());
var parent = opts.filename || basedir;
var conditions = opts.ignoreExportsField === false ? ['require', 'node'] : [];

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

Expand Down Expand Up @@ -278,35 +281,119 @@ module.exports = function resolve(x, options, callback) {
});
}

function processDirs(cb, dirs) {
function loadManifestInDir(dir, cb) {
maybeRealpath(realpath, dir, opts, function (err, pkgdir) {
if (err) return cb(null);

var pkgfile = path.join(pkgdir, 'package.json');
isFile(pkgfile, function (err, ex) {
// on err, ex is false
if (!ex) return cb(null);

readFile(pkgfile, function (err, body) {
if (err) cb(err);
try { var pkg = JSON.parse(body); } catch (jsonErr) {}

if (pkg && opts.packageFilter) {
pkg = opts.packageFilter(pkg, pkgfile, dir);
}
cb(pkg);
});
});
});
}

function processDirs(cb, dirs, subpath, endsWithSubpath) {
if (dirs.length === 0) return cb(null, undefined);
var dir = dirs[0];

isDirectory(path.dirname(dir), isdir);

function isdir(err, isdir) {
if (err) return cb(err);
if (!isdir) return processDirs(cb, dirs.slice(1));
loadAsFile(dir, opts.package, onfile);
if (conditions.length > 0 && endsWithSubpath(dir)) {
var pkgDir = dir.slice(0, dir.length - subpath.length);
loadManifestInDir(pkgDir, onmanifestWithExports);
} else {
onmanifest(false);
}

function onfile(err, m, pkg) {
if (err) return cb(err);
if (m) return cb(null, m, pkg);
loadAsDirectory(dir, opts.package, ondir);
function onmanifestWithExports(pkg) {
if (!pkg || pkg.exports === null || pkg.exports === undefined) {
return onmanifest(false);
}

var resolvedExport;
try {
resolvedExport = resolveExports(pkgDir, parent, subpath, pkg.exports, conditions);
} catch (resolveErr) {
return cb(resolveErr);
}

if (resolvedExport.exact) {
isFile(resolvedExport.resolved, function (err, ex) {
if (ex) {
cb(null, resolvedExport.resolved, pkg);
} else {
cb(null, undefined);
}
});
} else {
dir = resolvedExport.resolved;
onmanifest(true);
}
}

function ondir(err, n, pkg) {
if (err) return cb(err);
if (n) return cb(null, n, pkg);
processDirs(cb, dirs.slice(1));
function onmanifest(stop) {
isDirectory(path.dirname(dir), isdir);

function isdir(err, isdir) {
if (err) return cb(err);
if (!isdir) return next();
loadAsFile(dir, opts.package, onfile);
}

function onfile(err, m, pkg) {
if (err) return cb(err);
if (m) return cb(null, m, pkg);
loadAsDirectory(dir, opts.package, ondir);
}

function ondir(err, n, pkg) {
if (err) return cb(err);
if (n) return cb(null, n, pkg);
next();
}

function next() {
if (stop) {
cb(null, undefined);
} else {
processDirs(cb, dirs.slice(1), subpath, endsWithSubpath);
}
}
}

}
function loadNodeModules(x, start, cb) {
var subpathIndex = x.indexOf('/');
if (x[0] === '@') {
subpathIndex = x.indexOf('/', subpathIndex + 1);
}
var subpath;
if (subpathIndex === -1) {
subpath = '';
} else {
subpath = x.slice(subpathIndex);
}

var thunk = function () { return getPackageCandidates(x, start, opts); };
var endsWithSubpath = function (dir) {
var endOfDir = dir.slice(dir.length - subpath.length);

return endOfDir === subpath || endOfDir.replace(/\\/g, '/') === subpath;
};
processDirs(
cb,
packageIterator ? packageIterator(x, start, thunk, opts) : thunk()
packageIterator ? packageIterator(x, start, thunk, opts) : thunk(),
subpath,
endsWithSubpath
);
}
};
185 changes: 185 additions & 0 deletions lib/resolve-imports-exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
var path = require('path');
var startsWith = require('string.prototype.startswith');

function makeError(code, message) {
var error = new Error(message);
error.code = code;
return error;
}

function validateExports(exports, basePath) {
var isConditional = true;

if (typeof exports === 'object' && !Array.isArray(exports)) {
var exportKeys = Object.keys(exports);

for (var i = 0; i < exportKeys.length; i++) {
var isKeyConditional = exportKeys[i][0] !== '.';
if (i === 0) {
isConditional = isKeyConditional;
} else if (isKeyConditional !== isConditional) {
throw makeError('ERR_INVALID_PACKAGE_CONFIG', 'Invalid package config ' + basePath + path.sep + 'package.json, '
+ '"exports" cannot contain some keys starting with \'.\' and some not. '
+ 'The exports object must either be an object of package subpath keys '
+ 'or an object of main entry condition name keys only.');
}
}
}

if (isConditional) {
return { '.': exports };
} else {
return exports;
}
}

function validateConditions(names) {
// TODO If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error.
return names;
}

// eslint-disable-next-line max-lines-per-function
function resolvePackageTarget(packagePath, parent, key, target, subpath, internal, conditions) {
if (typeof target === 'string') {
var resolvedTarget = path.resolve(packagePath, target);
var invalidTarget = false;

if (!startsWith(target, './')) {
if (!internal) {
invalidTarget = true;
} else if (!startsWith(target, '../') && !startsWith(target, '/')) {
invalidTarget = true;
} else {
// TODO: imports need call package_resolve here
}
}

var targetParts = target.split(/[\\/]/).slice(1); // slice to strip the leading '.'
if (invalidTarget || targetParts.indexOf('node_modules') !== -1 || targetParts.indexOf('.') !== -1 || targetParts.indexOf('..') !== -1) {
throw makeError('ERR_INVALID_PACKAGE_TARGET', 'Invalid "exports" target ' + JSON.stringify(target)
+ ' defined for ' + key + ' in ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
}

if (subpath !== '' && target[target.length - 1] !== '/') {
throw makeError('ERR_INVALID_MODULE_SPECIFIER', 'Package subpath "' + subpath + '" is not a valid module request for '
+ 'the "exports" resolution of ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
}

var resolved = path.normalize(resolvedTarget + subpath);
var subpathParts = subpath.split(/[\\/]/);
if (!startsWith(resolved, resolvedTarget) || subpathParts.indexOf('node_modules') !== -1 || subpathParts.indexOf('.') !== -1 || subpathParts.indexOf('..') !== -1) {
throw makeError('ERR_INVALID_MODULE_SPECIFIER', 'Package subpath "' + subpath + '" is not a valid module request for '
+ 'the "exports" resolution of ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
}

return resolved;
}

if (Array.isArray(target)) {
if (target.length === 0) {
throw makeError('ERR_PACKAGE_PATH_NOT_EXPORTED', key === '.'
? 'No "exports" main resolved in ' + packagePath + path.sep + 'package.json.'
: 'Package subpath ' + key + ' is not defined by "exports" in ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
}

var lastError;
for (var i = 0; i < target.length; i++) {
try {
return resolvePackageTarget(packagePath, parent, key, target[i], subpath, internal, conditions);
} catch (e) {
if (e && (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || e.code === 'ERR_INVALID_PACKAGE_TARGET')) {
lastError = e;
} else {
throw e;
}
}
}
throw lastError;
}

if (target === null) {
throw makeError('ERR_PACKAGE_PATH_NOT_EXPORTED', key === '.'
? 'No "exports" main resolved in ' + packagePath + path.sep + 'package.json.'
: 'Package subpath ' + key + ' is not defined by "exports" in ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
}

if (typeof target !== 'object') {
throw makeError('ERR_INVALID_PACKAGE_TARGET', 'Invalid "exports" target ' + JSON.stringify(target)
+ ' defined for ' + key + ' in ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
}

var exportedConditions = validateConditions(Object.keys(target));

for (i = 0; i < exportedConditions.length; i++) {
var exportedCondition = exportedConditions[i];
if (exportedCondition === 'default' || conditions.indexOf(exportedCondition) !== -1) {
try {
return resolvePackageTarget(
packagePath,
parent,
key,
target[exportedCondition],
subpath,
internal,
conditions
);
} catch (e) {
if (!e || e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
throw e;
}
}
}
}

throw makeError('ERR_PACKAGE_PATH_NOT_EXPORTED', key === '.'
? 'No "exports" main resolved in ' + packagePath + path.sep + 'package.json.'
: 'Package subpath ' + key + ' is not defined by "exports" in ' + packagePath + path.sep + 'package.json imported from ' + parent + '.');
}

function resolveImportExport(packagePath, parent, matchObj, matchKey, isImports, conditions) {
if (Object.prototype.hasOwnProperty.call(matchObj, matchKey) && matchKey[matchKey.length - 1] !== '*') {
return {
resolved: resolvePackageTarget(packagePath, parent, matchKey, matchObj[matchKey], '', isImports, conditions),
exact: true
};
}

var longestMatchingExport = '';
var exportedPaths = Object.keys(matchObj);

for (var i = 0; i < exportedPaths.length; i++) {
var exportedPath = exportedPaths[i];
if (exportedPath[exportedPath.length - 1] === '/' && startsWith(matchKey, exportedPath) && exportedPath.length > longestMatchingExport.length) {
longestMatchingExport = exportedPath;
}
}

if (longestMatchingExport === '') {
throw makeError('ERR_PACKAGE_PATH_NOT_EXPORTED', 'Package subpath ' + matchKey + ' is not defined by "exports" in '
+ packagePath + path.sep + 'package.json imported from ' + parent + '.');
}

return {
resolved: resolvePackageTarget(
packagePath,
parent,
longestMatchingExport,
matchObj[longestMatchingExport],
matchKey.substring(longestMatchingExport.length - 1),
isImports,
conditions
),
exact: false
};
}

module.exports = function resolveExports(packagePath, parent, subpath, exports, conditions) {
return resolveImportExport(
packagePath,
parent,
validateExports(exports, packagePath),
'.' + subpath,
false,
conditions
);
};
Loading

0 comments on commit 016c79e

Please sign in to comment.