Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for the exports package.json attribute #224

Open
wants to merge 4 commits into
base: 1.x
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ block_comment_end = */
[*.yml]
indent_size = 1

[package.json]
[{package.json,.gitmodules}]
indent_style = tab

[CHANGELOG.md]
@@ -27,7 +27,7 @@ indent_size = 2
[{*.json,Makefile}]
max_line_length = off

[test/{dotdot,resolver,module_dir,multirepo,node_path,pathfilter,precedence}/**/*]
[test/{dotdot,exports,list-exports,resolver,module_dir,multirepo,node_path,pathfilter,precedence}/**/*]
indent_style = off
indent_size = off
max_line_length = off
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
test/list-exports/
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@
"object-curly-newline": 0,
"operator-linebreak": [2, "before"],
"sort-keys": 0,
"eqeqeq": [2, "always", {"null": "ignore"}]
},
"overrides": [
{
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -9,3 +9,6 @@ yarn.lock

# symlinked file used in tests
test/resolver/symlinked/_/node_modules/package

# submodule
test/list-exports
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "test/list-exports"]
path = test/list-exports
url = https://github.com/ljharb/list-exports.git
branch = main
117 changes: 101 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');
var nodeModulesPaths = require('./node-modules-paths');
var normalizeOptions = require('./normalize-options');
var isCore = require('is-core-module');
var resolveExports = require('./resolve-imports-exports');

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

@@ -77,6 +79,12 @@ module.exports = function resolve(x, options, callback) {
var basedir = opts.basedir || path.dirname(caller());
var parent = opts.filename || basedir;

if (opts.exportsField == null) {
opts.exportsField = { level: 'ignore' };
} else if (typeof opts.exportsField === 'string') {
opts.exportsField = { level: opts.exportsField };
}

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

// ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory
@@ -265,35 +273,112 @@ 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 (opts.exportsField.level !== 'ignore' && endsWithSubpath(dir, subpath)) {
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) {
return onmanifest(false);
}

var resolvedExport;
try {
resolvedExport = resolveExports(opts.exportsField, pkgDir, parent, subpath, pkg.exports);
} 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);
}
}
}
}

function loadNodeModules(x, start, cb) {
var subpathIndex = x.charAt(0) === '@' ? x.indexOf('/', x.indexOf('/') + 1) : x.indexOf('/');
var subpath = subpathIndex === -1 ? '' : x.slice(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
);
}

function endsWithSubpath(dir, subpath) {
var endOfDir = dir.slice(dir.length - subpath.length);

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

function parseConfig(config) {
var enableConditions = true;

if (config.level !== 'respect') {
if (config.level === 'respect, without conditions') {
enableConditions = false;
} else {
throw new Error('Invalid exportsField level: ' + config.level);
}
}

return {
enableConditions: enableConditions,
conditions: ['require', 'node']
};
}

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) {
var err = new Error('Invalid package config ' + path.join(basePath, '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.');
err.code = 'ERR_INVALID_PACKAGE_CONFIG';
throw err;
}
}
}

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

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

for (var i = 0; i < names.length; i++) {
var name = names[i];
var nameNum = Number(name);

if (String(nameNum) === name && nameNum >= 0 && nameNum < 0xFFFFFFFF) {
var err = new Error('Invalid package config ' + path.join(packagePath, 'package.json') + '. "exports" cannot contain numeric property keys');
err.code = 'ERR_INVALID_PACKAGE_CONFIG';
throw err;
}
}

return names;
}

function resolvePackageTarget(config, packagePath, parent, key, target, subpath, internal) {
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) {
var err = new Error('Invalid "exports" target ' + JSON.stringify(target)
+ ' defined for ' + key + ' in ' + path.join(packagePath, 'package.json') + ' imported from ' + parent + '.');
err.code = 'ERR_INVALID_PACKAGE_TARGET';
throw err;
}

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

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) {
err = new Error('Package subpath "' + subpath + '" is not a valid module request for '
+ 'the "exports" resolution of ' + path.join(packagePath, 'package.json') + ' imported from ' + parent + '.');
err.code = 'ERR_INVALID_MODULE_SPECIFIER';
throw err;
}

return resolved;
}

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

var lastError;
for (var i = 0; i < target.length; i++) {
try {
return resolvePackageTarget(
config,
packagePath,
parent,
key,
target[i],
subpath,
internal
);
} 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) {
err = new Error(key === '.'
? 'No "exports" main resolved in ' + path.join(packagePath, 'package.json') + '.'
: 'Package subpath ' + key + ' is not defined by "exports" in ' + path.join(packagePath, 'package.json') + ' imported from ' + parent + '.');
err.code = 'ERR_PACKAGE_PATH_NOT_EXPORTED';
throw err;
}

if (!config.enableConditions || typeof target !== 'object') {
err = new Error('Invalid "exports" target ' + JSON.stringify(target)
+ ' defined for ' + key + ' in ' + path.join(packagePath, 'package.json') + ' imported from ' + parent + '.');
err.code = 'ERR_INVALID_PACKAGE_TARGET';
throw err;
}

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

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

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

function resolveImportExport(config, packagePath, parent, matchObj, matchKey, isImports) {
if (Object.prototype.hasOwnProperty.call(matchObj, matchKey) && matchKey[matchKey.length - 1] !== '*') {
return {
resolved: resolvePackageTarget(config, packagePath, parent, matchKey, matchObj[matchKey], '', isImports),
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 === '') {
var err = new Error('Package subpath ' + matchKey + ' is not defined by "exports" in '
+ path.join(packagePath, 'package.json') + ' imported from ' + parent + '.');
err.code = 'ERR_PACKAGE_PATH_NOT_EXPORTED';
throw err;
}

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

module.exports = function resolveExports(config, packagePath, parent, subpath, exports) {
return resolveImportExport(
parseConfig(config),
packagePath,
parent,
validateExports(exports, packagePath),
'.' + subpath,
false
);
};
107 changes: 90 additions & 17 deletions lib/sync.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ var path = require('path');
var caller = require('./caller');
var nodeModulesPaths = require('./node-modules-paths');
var normalizeOptions = require('./normalize-options');
var resolveExports = require('./resolve-imports-exports');

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

@@ -70,6 +71,12 @@ module.exports = function resolveSync(x, options) {
var basedir = opts.basedir || path.dirname(caller());
var parent = opts.filename || basedir;

if (opts.exportsField == null) {
opts.exportsField = { level: 'ignore' };
} else if (typeof opts.exportsField === 'string') {
opts.exportsField = { level: opts.exportsField };
}

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

// ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory
@@ -83,7 +90,7 @@ module.exports = function resolveSync(x, options) {
} else if (includeCoreModules && isCore(x)) {
return x;
} else {
var n = loadNodeModulesSync(x, absoluteStart);
var n = (opts.exportsField.level === 'ignore' ? loadNodeModulesSync : loadNodeModulesWithExportsSync)(x, absoluteStart);
if (n) return maybeRealpathSync(realpathSync, n, opts);
}

@@ -141,7 +148,7 @@ module.exports = function resolveSync(x, options) {
return { pkg: pkg, dir: dir };
}

function loadAsDirectorySync(x) {
function loadManifestInDir(x) {
var pkgfile = path.join(maybeRealpathSync(realpathSync, x, opts), '/package.json');
if (isFile(pkgfile)) {
try {
@@ -154,22 +161,30 @@ module.exports = function resolveSync(x, options) {
pkg = opts.packageFilter(pkg, /*pkgfile,*/ x); // eslint-disable-line spaced-comment
}

if (pkg && pkg.main) {
if (typeof pkg.main !== 'string') {
var mainError = new TypeError('package “' + pkg.name + '” `main` must be a string');
mainError.code = 'INVALID_PACKAGE_MAIN';
throw mainError;
}
if (pkg.main === '.' || pkg.main === './') {
pkg.main = 'index';
}
try {
var m = loadAsFileSync(path.resolve(x, pkg.main));
if (m) return m;
var n = loadAsDirectorySync(path.resolve(x, pkg.main));
if (n) return n;
} catch (e) {}
return pkg;
}

return null;
}

function loadAsDirectorySync(x) {
var pkg = loadManifestInDir(x);

if (pkg && pkg.main) {
if (typeof pkg.main !== 'string') {
var mainError = new TypeError('package “' + pkg.name + '” `main` must be a string');
mainError.code = 'INVALID_PACKAGE_MAIN';
throw mainError;
}
if (pkg.main === '.' || pkg.main === './') {
pkg.main = 'index';
}
try {
var m = loadAsFileSync(path.resolve(x, pkg.main));
if (m) return m;
var n = loadAsDirectorySync(path.resolve(x, pkg.main));
if (n) return n;
} catch (e) {}
}

return loadAsFileSync(path.join(x, '/index'));
@@ -189,4 +204,62 @@ module.exports = function resolveSync(x, options) {
}
}
}

function loadNodeModulesWithExportsSync(x, start) {
var thunk = function () { return getPackageCandidates(x, start, opts); };
var dirs = packageIterator ? packageIterator(x, start, thunk, opts) : thunk();

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

var endsWithSubpath = function (dir) {
var endOfDir = dir.slice(dir.length - subpathLength);

return endOfDir === subpath || endOfDir.replace(/\\/g, '/') === subpath;
};

for (var i = 0; i < dirs.length; i++) {
var dir = dirs[i];

var pkg;

var resolvedExport;
if (endsWithSubpath(dir)) {
var pkgDir = dir.slice(0, dir.length - subpathLength);
if ((pkg = loadManifestInDir(pkgDir)) && pkg.exports) {
resolvedExport = resolveExports(opts.exportsField, pkgDir, parent, subpath, pkg.exports);
}
}

if (resolvedExport) {
if (resolvedExport.exact) {
if (isFile(resolvedExport.resolved)) {
return resolvedExport.resolved;
} else {
return;
}
} else {
dir = resolvedExport.resolved;
}
}

if (isDirectory(path.dirname(dir))) {
var m = loadAsFileSync(dir) || loadAsDirectorySync(dir);
if (m) return m;
}

if (resolvedExport) {
return;
}
}
}
};
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -17,7 +17,9 @@
"prepublish": "safe-publish-latest && cp node_modules/is-core-module/core.json ./lib/",
"prelint": "eclint check '**/*'",
"lint": "eslint --ext=js,mjs .",
"pretests-only": "cd ./test/resolver/nested_symlinks && node mylib/sync && node mylib/async",
"test:nested_symlinks": "cd ./test/resolver/nested_symlinks && node mylib/sync && node mylib/async",
"test:fixtures": "git submodule update --init --recursive",
"pretests-only": "npm run test:nested_symlinks && npm run test:fixtures",
"tests-only": "tape test/*.js",
"pretest": "npm run lint",
"test": "npm run --silent tests-only",
@@ -46,6 +48,7 @@
},
"dependencies": {
"is-core-module": "^2.1.0",
"path-parse": "^1.0.6"
"path-parse": "^1.0.6",
"string.prototype.startswith": "^1.0.0"
}
}
20 changes: 16 additions & 4 deletions readme.markdown
Original file line number Diff line number Diff line change
@@ -84,7 +84,7 @@ options are:

* opts.paths - require.paths array to use if nothing is found on the normal `node_modules` recursive walk (probably don't use this)

For advanced users, `paths` can also be a `opts.paths(request, start, opts)` function
For advanced users, `paths` can also be a `opts.paths(request, start, getNodeModulesDirs, opts)` function
* request - the import specifier being resolved
* start - lookup path
* getNodeModulesDirs - a thunk (no-argument function) that returns the paths using standard `node_modules` resolution
@@ -103,6 +103,11 @@ This is the way Node resolves dependencies when executed with the [--preserve-sy
**Note:** this property is currently `true` by default but it will be changed to
`false` in the next major version because *Node's resolution algorithm does not preserve symlinks by default*.

* opts.exportsField - the behavior of the exports field:
* `'respect'`: respect the exports field
* `'respect, without exports'`: respect the exports field without supporting conditional exports
* `'ignore'`: ignore the exports field

default `opts` values:

```js
@@ -138,7 +143,8 @@ default `opts` values:
});
},
moduleDirectory: 'node_modules',
preserveSymlinks: true
preserveSymlinks: true,
exportsField: 'ignore',
}
```

@@ -175,7 +181,7 @@ options are:

* opts.paths - require.paths array to use if nothing is found on the normal `node_modules` recursive walk (probably don't use this)

For advanced users, `paths` can also be a `opts.paths(request, start, opts)` function
For advanced users, `paths` can also be a `opts.paths(request, start, getNodeModulesDirs, opts)` function
* request - the import specifier being resolved
* start - lookup path
* getNodeModulesDirs - a thunk (no-argument function) that returns the paths using standard `node_modules` resolution
@@ -194,6 +200,11 @@ This is the way Node resolves dependencies when executed with the [--preserve-sy
**Note:** this property is currently `true` by default but it will be changed to
`false` in the next major version because *Node's resolution algorithm does not preserve symlinks by default*.

* opts.exportsField - the behavior of the exports field:
* `'respect'`: respect the exports field
* `'respect, without exports'`: respect the exports field without supporting conditional exports
* `'ignore'`: ignore the exports field

default `opts` values:

```js
@@ -233,7 +244,8 @@ default `opts` values:
return file;
},
moduleDirectory: 'node_modules',
preserveSymlinks: true
preserveSymlinks: true,
exportsField: 'ignore',
}
```

200 changes: 200 additions & 0 deletions test/exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
var path = require('path');
var test = require('tape');
var resolve = require('../');

test('exports', function (t) {
t.plan(38);
var dir = path.join(__dirname, '/exports');

resolve('mix-conditionals', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'ERR_INVALID_PACKAGE_CONFIG');
t.match(err && err.message, /"exports" cannot contain some keys starting with '.' and some not./);
});

resolve('invalid-config/with-node_modules', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'ERR_INVALID_PACKAGE_TARGET');
t.match(err && err.message, /Invalid "exports" target "\.\/node_modules\/foo\/index\.js"/);
});

resolve('invalid-config/outside-package', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'ERR_INVALID_PACKAGE_TARGET');
t.match(err && err.message, /Invalid "exports" target "\.\/\.\.\/mix-conditionals\/package\.json"/);
});

resolve('invalid-config/not-with-dot', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'ERR_INVALID_PACKAGE_TARGET');
t.match(err && err.message, /Invalid "exports" target "package\.json"/);
});

resolve('invalid-config/numeric-key-1', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'ERR_INVALID_PACKAGE_CONFIG');
t.match(err && err.message, /"exports" cannot contain numeric property keys/);
});

resolve('invalid-config/numeric-key-2', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'ERR_INVALID_PACKAGE_CONFIG');
t.match(err && err.message, /"exports" cannot contain numeric property keys/);
});

resolve('valid-config', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.ifError(err);
t.equal(res, path.join(dir, 'node_modules/valid-config/exists.js'));
});

resolve('valid-config/package.json', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'ERR_PACKAGE_PATH_NOT_EXPORTED');
t.match(err && err.message, /Package subpath \.\/package\.json is not defined by "exports" in/);
});

resolve('valid-config/remapped', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.ifError(err);
t.equal(res, path.join(dir, 'node_modules/valid-config/exists.js'));
});

resolve('valid-config/remapped/exists.js', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.ifError(err);
t.equal(res, path.join(dir, 'node_modules/valid-config/exists.js'));
});

resolve('valid-config/remapped/doesnt-exist.js', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'MODULE_NOT_FOUND');
t.match(err && err.message, /Cannot find module 'valid-config\/remapped\/doesnt-exist\.js'/);
});

resolve('valid-config/array', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.ifErr(err);
t.equal(res, path.join(dir, 'node_modules/valid-config/exists.js'));
});

resolve('valid-config/with-env', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) {
t.ifErr(err);
t.equal(res, path.join(dir, 'node_modules/valid-config/with-env/require.js'));
});

function iterateWithoutModifyingSubpath(request, start, getNodeModulesDirs, opts) {
return [path.join(opts.basedir, 'other_modules', request)];
}
resolve('other-module-dir', { basedir: dir, exportsField: 'respect', packageIterator: iterateWithoutModifyingSubpath }, function (err, res, pkg) {
t.ifErr(err);
t.equal(res, path.join(dir, 'other_modules/other-module-dir/exported.js'));
});

function iterateModifyingSubpath(request, start, getNodeModulesDirs, opts) {
return [path.join(opts.basedir, 'other_modules', request, 'index')];
}
resolve('other-module-dir', { basedir: dir, exportsField: 'respect', packageIterator: iterateModifyingSubpath }, function (err, res, pkg) {
t.ifErr(err);
t.equal(res, path.join(dir, 'other_modules/other-module-dir/index.js'));
});
});

test('exports sync', function (t) {
var dir = path.join(__dirname, '/exports');

try {
resolve.sync('mix-conditionals', { basedir: dir, exportsField: 'respect' });
t.fail();
} catch (err) {
t.equal(err.code, 'ERR_INVALID_PACKAGE_CONFIG');
t.match(err.message, /"exports" cannot contain some keys starting with '.' and some not./);
}

try {
resolve.sync('invalid-config/with-node_modules', { basedir: dir, exportsField: 'respect' });
t.fail();
} catch (err) {
t.equal(err.code, 'ERR_INVALID_PACKAGE_TARGET');
t.match(err.message, /Invalid "exports" target "\.\/node_modules\/foo\/index\.js"/);
}

try {
resolve.sync('invalid-config/outside-package', { basedir: dir, exportsField: 'respect' });
t.fail();
} catch (err) {
t.equal(err.code, 'ERR_INVALID_PACKAGE_TARGET');
t.match(err.message, /Invalid "exports" target "\.\/\.\.\/mix-conditionals\/package\.json"/);
}

try {
resolve.sync('invalid-config/not-with-dot', { basedir: dir, exportsField: 'respect' });
t.fail();
} catch (err) {
t.equal(err.code, 'ERR_INVALID_PACKAGE_TARGET');
t.match(err.message, /Invalid "exports" target "package\.json"/);
}

try {
resolve.sync('invalid-config/numeric-key-1', { basedir: dir, exportsField: 'respect' });
t.fail();
} catch (err) {
t.equal(err.code, 'ERR_INVALID_PACKAGE_CONFIG');
t.match(err.message, /"exports" cannot contain numeric property keys/);
}

try {
resolve.sync('invalid-config/numeric-key-2', { basedir: dir, exportsField: 'respect' });
t.fail();
} catch (err) {
t.equal(err.code, 'ERR_INVALID_PACKAGE_CONFIG');
t.match(err.message, /"exports" cannot contain numeric property keys/);
}

t.equal(resolve.sync('valid-config', { basedir: dir, exportsField: 'respect' }), path.join(dir, 'node_modules/valid-config/exists.js'));

try {
resolve.sync('valid-config/package.json', { basedir: dir, exportsField: 'respect' });
t.fail();
} catch (err) {
t.equal(err.code, 'ERR_PACKAGE_PATH_NOT_EXPORTED');
t.match(err.message, /Package subpath \.\/package\.json is not defined by "exports" in/);
}

t.equal(resolve.sync('valid-config/remapped', { basedir: dir, exportsField: 'respect' }), path.join(dir, 'node_modules/valid-config/exists.js'));

t.equal(resolve.sync('valid-config/remapped/exists.js', { basedir: dir, exportsField: 'respect' }), path.join(dir, 'node_modules/valid-config/exists.js'));

try {
resolve.sync('valid-config/remapped/doesnt-exist.js', { basedir: dir, exportsField: 'respect' });
t.fail();
} catch (err) {
t.equal(err.code, 'MODULE_NOT_FOUND');
t.match(err.message, /Cannot find module 'valid-config\/remapped\/doesnt-exist\.js'/);
}

t.equal(
resolve.sync('valid-config/array', { basedir: dir, exportsField: 'respect' }),
path.join(dir, 'node_modules/valid-config/exists.js')
);

t.equal(
resolve.sync('valid-config/with-env', { basedir: dir, exportsField: 'respect' }),
path.join(dir, 'node_modules/valid-config/with-env/require.js')
);

function iterateWithoutModifyingSubpath(request, start, getNodeModulesDirs, opts) {
return [path.join(opts.basedir, 'other_modules', request)];
}
t.equal(
resolve.sync('other-module-dir', { basedir: dir, exportsField: 'respect', packageIterator: iterateWithoutModifyingSubpath }),
path.join(dir, 'other_modules/other-module-dir/exported.js')
);

function iterateModifyingSubpath(request, start, getNodeModulesDirs, opts) {
return [path.join(opts.basedir, 'other_modules', request, 'index')];
}
t.equal(
resolve.sync('other-module-dir', { basedir: dir, exportsField: 'respect', packageIterator: iterateModifyingSubpath }),
path.join(dir, 'other_modules/other-module-dir/index.js')
);

t.end();
});

1 change: 1 addition & 0 deletions test/exports/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!node_modules
Empty file.
Empty file.
16 changes: 16 additions & 0 deletions test/exports/node_modules/invalid-config/package.json
Empty file.
7 changes: 7 additions & 0 deletions test/exports/node_modules/mix-conditionals/package.json
Empty file.
Empty file.
21 changes: 21 additions & 0 deletions test/exports/node_modules/valid-config/package.json
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
6 changes: 6 additions & 0 deletions test/exports/other_modules/other-module-dir/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "other-module-dir",
"exports": {
".": "./exported.js"
}
}
174 changes: 174 additions & 0 deletions test/exports_disabled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
var path = require('path');
var test = require('tape');
var resolve = require('../');

test('exports (disabled)', function (t) {
t.plan(34);
var dir = path.join(__dirname, '/exports');

resolve('mix-conditionals', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) {
t.ifErr(err);
t.equal(res, path.join(dir, 'node_modules/mix-conditionals/index.js'));
});

resolve('invalid-config/with-node_modules', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'MODULE_NOT_FOUND');
t.match(err && err.message, /Cannot find module 'invalid-config\/with-node_modules'/);
});

resolve('invalid-config/outside-package', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'MODULE_NOT_FOUND');
t.match(err && err.message, /Cannot find module 'invalid-config\/outside-package'/);
});

resolve('invalid-config/not-with-dot', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'MODULE_NOT_FOUND');
t.match(err && err.message, /Cannot find module 'invalid-config\/not-with-dot'/);
});

resolve('valid-config', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) {
t.ifError(err);
t.equal(res, path.join(dir, 'node_modules/valid-config/main.js'));
});

resolve('valid-config/package.json', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) {
t.ifError(err);
t.equal(res, path.join(dir, 'node_modules/valid-config/package.json'));
});

resolve('valid-config/remapped', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'MODULE_NOT_FOUND');
t.match(err && err.message, /Cannot find module 'valid-config\/remapped'/);
});

resolve('valid-config/remapped/exists.js', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'MODULE_NOT_FOUND');
t.match(err && err.message, /Cannot find module 'valid-config\/remapped\/exists.js'/);
});

resolve('valid-config/remapped/doesnt-exist.js', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'MODULE_NOT_FOUND');
t.match(err && err.message, /Cannot find module 'valid-config\/remapped\/doesnt-exist\.js'/);
});

resolve('valid-config/array', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'MODULE_NOT_FOUND');
t.match(err && err.message, /Cannot find module 'valid-config\/array'/);
});

resolve('valid-config/with-env', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) {
t.notOk(res);
t.equal(err && err.code, 'MODULE_NOT_FOUND');
t.match(err && err.message, /Cannot find module 'valid-config\/with-env'/);
});

function iterateWithoutModifyingSubpath(request, start, getNodeModulesDirs, opts) {
return [path.join(opts.basedir, 'other_modules', request)];
}
resolve('other-module-dir', { basedir: dir, exportsField: 'ignore', packageIterator: iterateWithoutModifyingSubpath }, function (err, res, pkg) {
t.ifErr(err);
t.equal(res, path.join(dir, 'other_modules/other-module-dir/index.js'));
});

function iterateModifyingSubpath(request, start, getNodeModulesDirs, opts) {
return [path.join(opts.basedir, 'other_modules', request, 'index')];
}
resolve('other-module-dir', { basedir: dir, exportsField: 'ignore', packageIterator: iterateModifyingSubpath }, function (err, res, pkg) {
t.ifErr(err);
t.equal(res, path.join(dir, 'other_modules/other-module-dir/index.js'));
});
});

test('exports sync (disabled)', function (t) {
var dir = path.join(__dirname, '/exports');

t.equal(resolve.sync('mix-conditionals', { basedir: dir, exportsField: 'ignore' }), path.join(dir, 'node_modules/mix-conditionals/index.js'));

try {
resolve.sync('invalid-config/with-node_modules', { basedir: dir, exportsField: 'ignore' });
t.fail();
} catch (err) {
t.equal(err.code, 'MODULE_NOT_FOUND');
t.match(err.message, /Cannot find module 'invalid-config\/with-node_modules'/);
}

try {
resolve.sync('invalid-config/outside-package', { basedir: dir, exportsField: 'ignore' });
t.fail();
} catch (err) {
t.equal(err.code, 'MODULE_NOT_FOUND');
t.match(err.message, /Cannot find module 'invalid-config\/outside-package'/);
}

try {
resolve.sync('invalid-config/not-with-dot', { basedir: dir, exportsField: 'ignore' });
t.fail();
} catch (err) {
t.equal(err.code, 'MODULE_NOT_FOUND');
t.match(err.message, /Cannot find module 'invalid-config\/not-with-dot'/);
}

t.equal(resolve.sync('valid-config', { basedir: dir, exportsField: 'ignore' }), path.join(dir, 'node_modules/valid-config/main.js'));

t.equal(resolve.sync('valid-config/package.json', { basedir: dir, exportsField: 'ignore' }), path.join(dir, 'node_modules/valid-config/package.json'));

try {
resolve.sync('valid-config/remapped', { basedir: dir, exportsField: 'ignore' });
t.fail();
} catch (err) {
t.equal(err.code, 'MODULE_NOT_FOUND');
t.match(err.message, /Cannot find module 'valid-config\/remapped'/);
}

try {
resolve.sync('valid-config/remapped/exists.js', { basedir: dir, exportsField: 'ignore' });
t.fail();
} catch (err) {
t.equal(err.code, 'MODULE_NOT_FOUND');
t.match(err.message, /Cannot find module 'valid-config\/remapped\/exists.js'/);
}

try {
resolve.sync('valid-config/remapped/doesnt-exist.js', { basedir: dir, exportsField: 'ignore' });
t.fail();
} catch (err) {
t.equal(err.code, 'MODULE_NOT_FOUND');
t.match(err.message, /Cannot find module 'valid-config\/remapped\/doesnt-exist\.js'/);
}

try {
resolve.sync('valid-config/array', { basedir: dir, exportsField: 'ignore' });
t.fail();
} catch (err) {
t.equal(err.code, 'MODULE_NOT_FOUND');
t.match(err.message, /Cannot find module 'valid-config\/array'/);
}

try {
resolve.sync('valid-config/with-env', { basedir: dir, exportsField: 'ignore' });
t.fail();
} catch (err) {
t.equal(err.code, 'MODULE_NOT_FOUND');
t.match(err.message, /Cannot find module 'valid-config\/with-env'/);
}

function iterateWithoutModifyingSubpath(request, start, getNodeModulesDirs, opts) {
return [path.join(opts.basedir, 'other_modules', request)];
}
t.equal(resolve.sync('other-module-dir', { basedir: dir, exportsField: 'ignore', packageIterator: iterateWithoutModifyingSubpath }), path.join(dir, 'other_modules/other-module-dir/index.js'));

function iterateModifyingSubpath(request, start, getNodeModulesDirs, opts) {
return [path.join(opts.basedir, 'other_modules', request, 'index')];
}
t.equal(resolve.sync('other-module-dir', { basedir: dir, exportsField: 'ignore', packageIterator: iterateModifyingSubpath }), path.join(dir, 'other_modules/other-module-dir/index.js'));

t.end();
});

1 change: 1 addition & 0 deletions test/list-exports
Submodule list-exports added at 84a90d
204 changes: 204 additions & 0 deletions test/list-exports-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
var fs = require('fs');
var path = require('path');
var test = require('tape');
var resolve = require('../');

var fixturesPath = path.join(__dirname, 'list-exports/packages/tests/fixtures');

fs.readdirSync(fixturesPath).forEach(function (fixtureName) {
var fixtureSpec = require(path.join(fixturesPath, fixtureName, 'expected.json'));
var fixtureWithoutConditionsSpec = require(path.join(fixturesPath, fixtureName, 'expected-without-conditions.json'));
var fixturePackagePath = path.join(fixturesPath, fixtureName, 'project');

function packageIterator(identifier) {
var slashIdx = identifier.indexOf('/');

if (slashIdx === -1) {
return identifier === fixtureSpec.name ? [fixturePackagePath] : null;
}

if (identifier.slice(0, slashIdx) === fixtureSpec.name) {
return [fixturePackagePath + identifier.slice(slashIdx)];
} else {
return null;
}
}

var optsRespect = {
exportsField: 'respect',
packageIterator: packageIterator,
extensions: ['.js', '.json']
};
var optsRespectWithoutConditions = {
exportsField: 'respect, without conditions',
packageIterator: packageIterator,
extensions: ['.js', '.json']
};
var optsIgnore = {
exportsField: 'ignore',
packageIterator: packageIterator,
extensions: ['.js', '.json']
};

if (fixtureName === 'ls-exports' || fixtureName === 'list-exports') {
optsRespect.preserveSymlinks = true;
optsRespectWithoutConditions.preserveSymlinks = true;
optsIgnore.preserveSymlinks = true;
}

test('list-exports-tests fixture ' + fixtureName, function (t) {
/*
* Sanity check: package.json should be resolvable with exports disabled
* All other tests are configured via the expected.json file
*/
resolve(fixtureSpec.name + '/package.json', optsIgnore, function (err, res, pkg) {
t.ifErr(err);
t.equal(path.normalize(res), path.join(fixturePackagePath, 'package.json'), 'sanity check');
});

// with exports enabled

if (fixtureSpec.private) {
t.plan(2);
return;
}

var skipTestWithoutConditions = fixtureName === 'preact';

t.plan(2 * (
1
+ fixtureSpec.require.length
+ fixtureSpec['require (pre-exports)'].length
+ (skipTestWithoutConditions ? 0 : fixtureWithoutConditionsSpec.require.length)
));

fixtureSpec.require.forEach(function (identifier) {
resolve(identifier, optsRespect, function (err, res, pkg) {
t.ifErr(err);
var tree = fixtureSpec.tree[fixtureSpec.name];

var relativeResolvedParts = path.relative(fixturePackagePath, res).split(path.sep);

for (var i = 0; i < relativeResolvedParts.length; i++) {
tree = tree[relativeResolvedParts[i]];

if (!tree) {
t.fail('Unexpected resolved path ' + JSON.stringify(res) + ' for ' + JSON.stringify(identifier));
}
}

t.notEqual(tree.indexOf(identifier), -1, 'resolved path ' + JSON.stringify(res) + ' for ' + JSON.stringify(identifier));
});
});

if (!skipTestWithoutConditions) {
fixtureWithoutConditionsSpec.require.forEach(function (identifier) {
resolve(identifier, optsRespectWithoutConditions, function (err, res, pkg) {
t.ifErr(err);
var tree = fixtureSpec.tree[fixtureSpec.name];

var relativeResolvedParts = path.relative(fixturePackagePath, res).split(path.sep);

for (var i = 0; i < relativeResolvedParts.length; i++) {
tree = tree[relativeResolvedParts[i]];

if (!tree) {
t.fail('Unexpected resolved path ' + JSON.stringify(res) + ' for ' + JSON.stringify(identifier));
}
}

t.notEqual(tree.indexOf(identifier), -1, 'resolved path ' + JSON.stringify(res) + ' for ' + JSON.stringify(identifier));
});
});
}

fixtureSpec['require (pre-exports)'].forEach(function (identifier) {
resolve(identifier, optsIgnore, function (err, res, pkg) {
t.ifErr(err);
var tree = fixtureSpec['tree (pre-exports)'][fixtureSpec.name];

var relativeResolvedParts = path.relative(fixturePackagePath, res).split(path.sep);

for (var i = 0; i < relativeResolvedParts.length; i++) {
tree = tree[relativeResolvedParts[i]];

if (!tree) {
t.fail('Unexpected resolved path ' + JSON.stringify(res) + ' for ' + JSON.stringify(identifier));
}
}

t.notEqual(tree.indexOf(identifier), -1, 'resolved path ' + JSON.stringify(res) + ' for ' + JSON.stringify(identifier));
});
});
});

test('list-exports-tests fixture ' + fixtureName + ' sync', function (t) {
/*
* Sanity check: package.json should be resolvable with exports disabled
* All other tests are configured via the expected.json file
*/
t.equal(path.normalize(resolve.sync(fixtureSpec.name + '/package.json', optsIgnore)), path.join(fixturePackagePath, 'package.json'), 'sanity check');

// with exports enabled

if (fixtureSpec.private) {
t.end();
return;
}

fixtureSpec.require.forEach(function (identifier) {
var resolved = resolve.sync(identifier, optsRespect);
var tree = fixtureSpec.tree[fixtureSpec.name];

var relativeResolvedParts = path.relative(fixturePackagePath, resolved).split(path.sep);

for (var i = 0; i < relativeResolvedParts.length; i++) {
tree = tree[relativeResolvedParts[i]];

if (!tree) {
t.fail('Unexpected resolved path ' + JSON.stringify(resolved) + ' for ' + JSON.stringify(identifier));
}
}

t.notEqual(tree.indexOf(identifier), -1, 'resolved path ' + JSON.stringify(resolved) + ' for ' + JSON.stringify(identifier));
});

if (fixtureName !== 'preact') {
fixtureWithoutConditionsSpec.require.forEach(function (identifier) {
var resolved = resolve.sync(identifier, optsRespectWithoutConditions);
var tree = fixtureSpec.tree[fixtureSpec.name];

var relativeResolvedParts = path.relative(fixturePackagePath, resolved).split(path.sep);

for (var i = 0; i < relativeResolvedParts.length; i++) {
tree = tree[relativeResolvedParts[i]];

if (!tree) {
t.fail('Unexpected resolved path ' + JSON.stringify(resolved) + ' for ' + JSON.stringify(identifier));
}
}

t.notEqual(tree.indexOf(identifier), -1, 'resolved path ' + JSON.stringify(resolved) + ' for ' + JSON.stringify(identifier));
});
}

fixtureSpec['require (pre-exports)'].forEach(function (identifier) {
var resolved = resolve.sync(identifier, optsIgnore);
var tree = fixtureSpec['tree (pre-exports)'][fixtureSpec.name];

var relativeResolvedParts = path.relative(fixturePackagePath, resolved).split(path.sep);

for (var i = 0; i < relativeResolvedParts.length; i++) {
tree = tree[relativeResolvedParts[i]];

if (!tree) {
t.fail('Unexpected resolved path ' + JSON.stringify(resolved) + ' for ' + JSON.stringify(identifier));
}
}

t.notEqual(tree.indexOf(identifier), -1, 'resolved path ' + JSON.stringify(resolved) + ' for ' + JSON.stringify(identifier));
});

t.end();
});
});