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 Aug 23, 2020
1 parent 0db50df commit b36b16e
Show file tree
Hide file tree
Showing 19 changed files with 540 additions and 36 deletions.
98 changes: 82 additions & 16 deletions lib/async.js
@@ -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');
var resolveExports = require('./resolve-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 env = opts.ignoreExportsField === false ? ['require', 'node'] : [];

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

Expand Down Expand Up @@ -278,35 +281,98 @@ 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) {
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 (env.length > 0 && dir.substring(dir.length - subpath.length) === subpath) {
var pkgDir = dir.substring(0, dir.length - subpath.length);
loadManifestInDir(pkgDir, onmanifestWithExports);
} else {
onmanifest(true);
}

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) {
var tryLoadAsDirectory = true;
if (pkg && pkg.exports) {
try {
var resolvedExport = resolveExports(pkgDir, parent, subpath, pkg.exports, env);
} catch (resolveErr) {
return cb(resolveErr);
}

if (resolvedExport) {
dir = resolvedExport.path;
tryLoadAsDirectory = resolvedExport.tryLoadAsDirectory;
}
}

onmanifest(tryLoadAsDirectory);
}

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

function isdir(err, isdir) {
if (err) return cb(err);
if (!isdir) return processDirs(cb, dirs.slice(1), subpath);
loadAsFile(dir, opts.package, onfile);
}

function onfile(err, m, pkg) {
if (err) return cb(err);
if (m) return cb(null, m, pkg);
if (!tryLoadAsDirectory) return processDirs(cb, dirs.slice(1), subpath);
loadAsDirectory(dir, opts.package, ondir);
}

function ondir(err, n, pkg) {
if (err) return cb(err);
if (n) return cb(null, n, pkg);
processDirs(cb, dirs.slice(1), subpath);
}
}

}
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.substring(subpathIndex);
}

var thunk = function () { return getPackageCandidates(x, start, opts); };
processDirs(
cb,
packageIterator ? packageIterator(x, start, thunk, opts) : thunk()
packageIterator ? packageIterator(x, start, thunk, opts) : thunk(),
subpath
);
}
};
178 changes: 178 additions & 0 deletions lib/resolve-exports.js
@@ -0,0 +1,178 @@
var path = require('path');

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 validateEnvNames(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;
}

var startsWith;

if (typeof String.prototype.startsWith === 'function') {
startsWith = function (path, exportedPath) {
return path.startsWith(exportedPath);
};
} else {
startsWith = function (path, exportedPath) {
for (var i = 0; i < exportedPath.length; i++) {
if (path[i] !== exportedPath[i]) {
return false;
}
}

return true;
};
}

function resolveExportsTarget(packagePath, parent, key, target, subpath, env) {
if (typeof target === 'string') {
var resolvedTarget = path.resolve(packagePath, target);
if (!(/^\.\//).test(target) || resolvedTarget.indexOf('/node_modules/', packagePath.length - 1) !== -1 || !startsWith(resolvedTarget, packagePath)) {
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);

if (!startsWith(resolved, resolvedTarget)) {
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 resolveExportsTarget(packagePath, parent, key, target[i], subpath, env);
} 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 exportedEnvs = validateEnvNames(Object.keys(target));

for (i = 0; i < exportedEnvs.length; i++) {
var exportedEnv = exportedEnvs[i];
if (exportedEnv === 'default' || env.indexOf(exportedEnv) !== -1) {
try {
return resolveExportsTarget(packagePath, parent, key, target[exportedEnv], subpath, env);
} 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 + '.');
}

module.exports = function resolveExports(packagePath, parent, subpath, exports, env) {
exports = validateExports(exports, packagePath); // eslint-disable-line no-param-reassign
subpath = '.' + subpath; // eslint-disable-line no-param-reassign

if (subpath === '.' && exports['.'] === undefined) {
return;
}

var resolved;
var isDirectoryExport;
if (Object.prototype.hasOwnProperty.call(exports, subpath)) {
resolved = resolveExportsTarget(packagePath, parent, subpath, exports[subpath], '', env);
isDirectoryExport = false;
} else {
var longestMatchingExport = '';
var exportedPaths = Object.keys(exports);

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

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

resolved = resolveExportsTarget(
packagePath,
parent,
longestMatchingExport,
exports[longestMatchingExport],
subpath.substring(longestMatchingExport.length - 1),
env
);
isDirectoryExport = true;
}

return {
path: resolved,
tryLoadAsDirectory: isDirectoryExport && subpath !== './'
};
};

0 comments on commit b36b16e

Please sign in to comment.