From eb00fccde4461270d429d675d5f499a749a14c35 Mon Sep 17 00:00:00 2001 From: Bram Gotink Date: Sat, 6 Jun 2020 00:16:56 +0200 Subject: [PATCH] [New] add support for the exports package.json attribute --- lib/async.js | 105 +++++++++-- lib/resolve-exports.js | 178 ++++++++++++++++++ lib/sync.js | 84 ++++++--- test/exports.js | 176 +++++++++++++++++ test/exports/.gitignore | 1 + .../invalid-config/node_modules/foo/index.js | 0 .../invalid-config/not-a-folder/index.js | 0 .../node_modules/invalid-config/package.json | 8 + .../mix-conditionals/package.json | 7 + .../node_modules/valid-config/exists.js | 0 .../node_modules/valid-config/package.json | 18 ++ .../valid-config/with-env/custom.js | 0 .../valid-config/with-env/default.js | 0 .../valid-config/with-env/node.js | 0 .../valid-config/with-env/require.js | 0 15 files changed, 538 insertions(+), 39 deletions(-) create mode 100644 lib/resolve-exports.js create mode 100644 test/exports.js create mode 100644 test/exports/.gitignore create mode 100644 test/exports/node_modules/invalid-config/node_modules/foo/index.js create mode 100644 test/exports/node_modules/invalid-config/not-a-folder/index.js create mode 100644 test/exports/node_modules/invalid-config/package.json create mode 100644 test/exports/node_modules/mix-conditionals/package.json create mode 100644 test/exports/node_modules/valid-config/exists.js create mode 100644 test/exports/node_modules/valid-config/package.json create mode 100644 test/exports/node_modules/valid-config/with-env/custom.js create mode 100644 test/exports/node_modules/valid-config/with-env/default.js create mode 100644 test/exports/node_modules/valid-config/with-env/node.js create mode 100644 test/exports/node_modules/valid-config/with-env/require.js diff --git a/lib/async.js b/lib/async.js index ad4b36b7..17d9925d 100644 --- a/lib/async.js +++ b/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; @@ -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.env || []; opts.paths = opts.paths || []; @@ -278,35 +281,101 @@ 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) { + loadManifestInDir(dir, onmanifest); + } else { + onmanifest(); } - function onfile(err, m, pkg) { - if (err) return cb(err); - if (m) return cb(null, m, pkg); - loadAsDirectory(dir, opts.package, ondir); - } + function onmanifest(pkg) { + var resolvedX; + var tryLoadAsDirectory = true; - function ondir(err, n, pkg) { - if (err) return cb(err); - if (n) return cb(null, n, pkg); - processDirs(cb, dirs.slice(1)); + if (pkg && pkg.exports) { + try { + var resolvedExport = resolveExports(dir, parent, subpath, pkg.exports, env); + } catch (resolveErr) { + return cb(resolveErr); + } + + if (resolvedExport) { + resolvedX = resolvedExport.path; + tryLoadAsDirectory = resolvedExport.isDirectoryExport; + } + } + + if (!resolvedX) { + resolvedX = dir + subpath; + } + + isDirectory(path.dirname(resolvedX), isdir); + + function isdir(err, isdir) { + if (err) return cb(err); + if (!isdir) return processDirs(cb, dirs.slice(1), subpath); + loadAsFile(resolvedX, 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(resolvedX, 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 thunk = function () { return getPackageCandidates(x, start, opts); }; + var subpathIndex = x.indexOf('/'); + if (x[0] === '@') { + subpathIndex = x.indexOf('/', subpathIndex + 1); + } + var packageName, subpath; + if (subpathIndex === -1) { + packageName = x; + subpath = ''; + } else { + packageName = x.substring(0, subpathIndex); + subpath = x.substring(subpathIndex); + } + + var thunk = function () { return getPackageCandidates(packageName, start, opts); }; processDirs( cb, - packageIterator ? packageIterator(x, start, thunk, opts) : thunk() + packageIterator ? packageIterator(x, start, thunk, opts) : thunk(), + subpath ); } }; diff --git a/lib/resolve-exports.js b/lib/resolve-exports.js new file mode 100644 index 00000000..9ee76948 --- /dev/null +++ b/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, + isDirectoryExport: isDirectoryExport + }; +}; diff --git a/lib/sync.js b/lib/sync.js index 08378087..aa64f6a5 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -4,6 +4,7 @@ var path = require('path'); var caller = require('./caller.js'); var nodeModulesPaths = require('./node-modules-paths.js'); var normalizeOptions = require('./normalize-options.js'); +var resolveExports = require('./resolve-exports.js'); var realpathFS = fs.realpathSync && typeof fs.realpathSync.native === 'function' ? fs.realpathSync.native : fs.realpathSync; @@ -68,6 +69,7 @@ module.exports = function resolveSync(x, options) { var extensions = opts.extensions || ['.js']; var basedir = opts.basedir || path.dirname(caller()); var parent = opts.filename || basedir; + var env = opts.env || []; opts.paths = opts.paths || []; @@ -145,7 +147,7 @@ module.exports = function resolveSync(x, options) { return { pkg: pkg, dir: dir }; } - function loadAsDirectorySync(x) { + function loadManifestInDir(x) { var pkgfile = path.join(isDirectory(x) ? maybeRealpathSync(realpathSync, x, opts) : x, '/package.json'); if (isFile(pkgfile)) { try { @@ -157,38 +159,78 @@ module.exports = function resolveSync(x, options) { pkg = opts.packageFilter(pkg, pkgfile, 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 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')); } function loadNodeModulesSync(x, start) { - var thunk = function () { return getPackageCandidates(x, start, opts); }; + var subpathIndex = x.indexOf('/'); + if (x[0] === '@') { + subpathIndex = x.indexOf('/', subpathIndex + 1); + } + var packageName, subpath; + if (subpathIndex === -1) { + packageName = x; + subpath = ''; + } else { + packageName = x.substring(0, subpathIndex); + subpath = x.substring(subpathIndex); + } + + var thunk = function () { return getPackageCandidates(packageName, start, opts); }; var dirs = packageIterator ? packageIterator(x, start, thunk, opts) : thunk(); for (var i = 0; i < dirs.length; i++) { var dir = dirs[i]; - if (isDirectory(path.dirname(dir))) { - var m = loadAsFileSync(dir); + + var pkg; + var resolvedX; + var tryLoadAsDirectory = true; + if (env.length > 0 && (pkg = loadManifestInDir(dir)) && pkg.exports) { + var resolvedExport = resolveExports(dir, parent, subpath, pkg.exports, env); + + if (resolvedExport) { + resolvedX = resolvedExport.path; + tryLoadAsDirectory = resolvedExport.isDirectoryExport; + } + } + + if (!resolvedX) { + resolvedX = dir + subpath; + } + + if (isDirectory(path.dirname(resolvedX))) { + var m = loadAsFileSync(resolvedX); if (m) return m; - var n = loadAsDirectorySync(dir); - if (n) return n; + if (tryLoadAsDirectory) { + var n = loadAsDirectorySync(resolvedX); + if (n) return n; + } } } } diff --git a/test/exports.js b/test/exports.js new file mode 100644 index 00000000..2d218d9d --- /dev/null +++ b/test/exports.js @@ -0,0 +1,176 @@ +var path = require('path'); +var test = require('tape'); +var resolve = require('../'); + +test('exports', function (t) { + t.plan(39); + var dir = path.join(__dirname, '/exports'); + + resolve('mix-conditionals', { basedir: dir, env: ['node', 'require'] }, 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, env: ['node', 'require'] }, 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, env: ['node', 'require'] }, 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, env: ['node', 'require'] }, 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('valid-config', { basedir: dir, env: ['node', 'require'] }, 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, env: ['node', 'require'] }, 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, env: ['node', 'require'] }, 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, env: ['node', 'require'] }, 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, env: ['node', 'require'] }, 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, env: ['node', 'require'] }, 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, env: ['node', 'require'] }, function (err, res, pkg) { + t.ifErr(err); + t.equal(res, path.join(dir, 'node_modules/valid-config/with-env/require.js')); + }); + + resolve('valid-config/with-env', { basedir: dir, env: ['node', 'import'] }, function (err, res, pkg) { + t.ifErr(err); + t.equal(res, path.join(dir, 'node_modules/valid-config/with-env/node.js')); + }); + + resolve('valid-config/with-env', { basedir: dir, env: ['node', 'require', 'custom'] }, function (err, res, pkg) { + t.ifErr(err); + t.equal(res, path.join(dir, 'node_modules/valid-config/with-env/custom.js')); + }); + + resolve('valid-config/with-env', { basedir: dir, env: ['not-specified'] }, function (err, res, pkg) { + t.ifErr(err); + t.equal(res, path.join(dir, 'node_modules/valid-config/with-env/default.js')); + }); + + resolve('valid-config/remapped/foo/../exists.js', { basedir: dir, env: ['not-specified'] }, function (err, res, pkg) { + t.ifErr(err); + t.equal(res, path.join(dir, 'node_modules/valid-config/exists.js')); + }); + + resolve('valid-config/remapped/../exists.js', { basedir: dir, env: ['not-specified'] }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'ERR_INVALID_MODULE_SPECIFIER'); + t.match(err && err.message, /Package subpath "\/\.\.\/exists\.js" is not a valid module request/); + }); +}); + +test('exports sync', function (t) { + var dir = path.join(__dirname, '/exports'); + + try { + resolve.sync('mix-conditionals', { basedir: dir, env: ['node', 'require'] }); + 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, env: ['node', 'require'] }); + 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, env: ['node', 'require'] }); + 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, env: ['node', 'require'] }); + t.fail(); + } catch (err) { + t.equal(err.code, 'ERR_INVALID_PACKAGE_TARGET'); + t.match(err.message, /Invalid "exports" target "package\.json"/); + } + + t.equal(resolve.sync('valid-config', { basedir: dir, env: ['node', 'require'] }), path.join(dir, 'node_modules/valid-config/exists.js')); + + try { + resolve.sync('valid-config/package.json', { basedir: dir, env: ['node', 'require'] }); + 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, env: ['node', 'require'] }), path.join(dir, 'node_modules/valid-config/exists.js')); + + t.equal(resolve.sync('valid-config/remapped/exists.js', { basedir: dir, env: ['node', 'require'] }), path.join(dir, 'node_modules/valid-config/exists.js')); + + try { + resolve.sync('valid-config/remapped/doesnt-exist.js', { basedir: dir, env: ['node', 'require'] }); + 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, env: ['node', 'require'] }), path.join(dir, 'node_modules/valid-config/exists.js')); + + t.equal(resolve.sync('valid-config/with-env', { basedir: dir, env: ['node', 'require'] }), path.join(dir, 'node_modules/valid-config/with-env/require.js')); + + t.equal(resolve.sync('valid-config/with-env', { basedir: dir, env: ['node', 'import'] }), path.join(dir, 'node_modules/valid-config/with-env/node.js')); + + t.equal(resolve.sync('valid-config/with-env', { basedir: dir, env: ['node', 'require', 'custom'] }), path.join(dir, 'node_modules/valid-config/with-env/custom.js')); + + t.equal(resolve.sync('valid-config/with-env', { basedir: dir, env: ['not-specified'] }), path.join(dir, 'node_modules/valid-config/with-env/default.js')); + + t.equal(resolve.sync('valid-config/remapped/foo/../exists.js', { basedir: dir, env: ['not-specified'] }), path.join(dir, 'node_modules/valid-config/exists.js')); + + try { + resolve.sync('valid-config/remapped/../exists.js', { basedir: dir, env: ['not-specified'] }); + t.fail(); + } catch (err) { + t.equal(err.code, 'ERR_INVALID_MODULE_SPECIFIER'); + t.match(err.message, /Package subpath "\/\.\.\/exists\.js" is not a valid module request/); + } + + t.end(); +}); + diff --git a/test/exports/.gitignore b/test/exports/.gitignore new file mode 100644 index 00000000..736e8ae5 --- /dev/null +++ b/test/exports/.gitignore @@ -0,0 +1 @@ +!node_modules \ No newline at end of file diff --git a/test/exports/node_modules/invalid-config/node_modules/foo/index.js b/test/exports/node_modules/invalid-config/node_modules/foo/index.js new file mode 100644 index 00000000..e69de29b diff --git a/test/exports/node_modules/invalid-config/not-a-folder/index.js b/test/exports/node_modules/invalid-config/not-a-folder/index.js new file mode 100644 index 00000000..e69de29b diff --git a/test/exports/node_modules/invalid-config/package.json b/test/exports/node_modules/invalid-config/package.json new file mode 100644 index 00000000..0f697592 --- /dev/null +++ b/test/exports/node_modules/invalid-config/package.json @@ -0,0 +1,8 @@ +{ + "name": "invalid-config", + "exports": { + "./with-node_modules": "./node_modules/foo/index.js", + "./outside-package": "./../mix-conditionals/package.json", + "./not-with-dot": "package.json" + } +} \ No newline at end of file diff --git a/test/exports/node_modules/mix-conditionals/package.json b/test/exports/node_modules/mix-conditionals/package.json new file mode 100644 index 00000000..01aa6ce2 --- /dev/null +++ b/test/exports/node_modules/mix-conditionals/package.json @@ -0,0 +1,7 @@ +{ + "name": "mix-conditionals", + "exports": { + "./package.json": "./package.json", + "default": "./package.json" + } +} \ No newline at end of file diff --git a/test/exports/node_modules/valid-config/exists.js b/test/exports/node_modules/valid-config/exists.js new file mode 100644 index 00000000..e69de29b diff --git a/test/exports/node_modules/valid-config/package.json b/test/exports/node_modules/valid-config/package.json new file mode 100644 index 00000000..239f1458 --- /dev/null +++ b/test/exports/node_modules/valid-config/package.json @@ -0,0 +1,18 @@ +{ + "name": "valid-config", + "exports": { + ".": "./exists.js", + "./remapped": "./exists.js", + "./remapped/": "./", + "./array": [ + "invalid:syntax", + "./exists.js" + ], + "./with-env": { + "custom": "./with-env/custom.js", + "require": "./with-env/require.js", + "node": "./with-env/node.js", + "default": "./with-env/default.js" + } + } +} \ No newline at end of file diff --git a/test/exports/node_modules/valid-config/with-env/custom.js b/test/exports/node_modules/valid-config/with-env/custom.js new file mode 100644 index 00000000..e69de29b diff --git a/test/exports/node_modules/valid-config/with-env/default.js b/test/exports/node_modules/valid-config/with-env/default.js new file mode 100644 index 00000000..e69de29b diff --git a/test/exports/node_modules/valid-config/with-env/node.js b/test/exports/node_modules/valid-config/with-env/node.js new file mode 100644 index 00000000..e69de29b diff --git a/test/exports/node_modules/valid-config/with-env/require.js b/test/exports/node_modules/valid-config/with-env/require.js new file mode 100644 index 00000000..e69de29b