diff --git a/lib/async.js b/lib/async.js index 111820d7..a58dc465 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-module'); +var resolveExports = require('./resolve-imports-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 conditions = opts.ignoreExportsField === false ? ['require', 'node'] : []; opts.paths = opts.paths || []; @@ -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 ); } }; diff --git a/lib/resolve-imports-exports.js b/lib/resolve-imports-exports.js new file mode 100644 index 00000000..5548f220 --- /dev/null +++ b/lib/resolve-imports-exports.js @@ -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 + ); +}; diff --git a/lib/sync.js b/lib/sync.js index 15d61724..2ddc92f5 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-imports-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 conditions = opts.ignoreExportsField === false ? ['require', 'node'] : []; opts.paths = opts.paths || []; @@ -88,7 +90,7 @@ module.exports = function resolveSync(x, options) { } else if (isCore(x)) { return x; } else { - var n = loadNodeModulesSync(x, absoluteStart); + var n = (conditions.length > 0 ? loadNodeModulesWithExportsSync : loadNodeModulesSync)(x, absoluteStart); if (n) return maybeRealpathSync(realpathSync, n, opts); } @@ -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,22 +159,30 @@ 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')); @@ -192,4 +202,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(pkgDir, parent, subpath, pkg.exports, conditions); + } + } + + 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; + } + } + } }; diff --git a/package.json b/package.json index 888729e8..03a4307b 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ }, "dependencies": { "is-core-module": "^2.0.0", - "path-parse": "^1.0.6" + "path-parse": "^1.0.6", + "string.prototype.startswith": "^1.0.0" } } diff --git a/readme.markdown b/readme.markdown index f4490e2b..17a97346 100644 --- a/readme.markdown +++ b/readme.markdown @@ -95,6 +95,8 @@ options are: * opts.preserveSymlinks - if true, doesn't resolve `basedir` to real path before resolving. This is the way Node resolves dependencies when executed with the [--preserve-symlinks](https://nodejs.org/api/all.html#cli_preserve_symlinks) flag. +* opts.ignoreExportsField - if false, take package exports into account + default `opts` values: ```js @@ -129,7 +131,8 @@ default `opts` values: }); }, moduleDirectory: 'node_modules', - preserveSymlinks: false + preserveSymlinks: false, + ignoreExportsField: true } ``` @@ -165,7 +168,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 @@ -182,6 +185,8 @@ options are: * opts.preserveSymlinks - if true, doesn't resolve `basedir` to real path before resolving. This is the way Node resolves dependencies when executed with the [--preserve-symlinks](https://nodejs.org/api/all.html#cli_preserve_symlinks) flag. +* opts.ignoreExportsField - if false, take package exports into account + default `opts` values: ```js @@ -220,7 +225,8 @@ default `opts` values: return file; }, moduleDirectory: 'node_modules', - preserveSymlinks: false + preserveSymlinks: false, + ignoreExportsField: true } ``` diff --git a/test/exports.js b/test/exports.js new file mode 100644 index 00000000..1cd5c812 --- /dev/null +++ b/test/exports.js @@ -0,0 +1,160 @@ +var path = require('path'); +var test = require('tape'); +var resolve = require('../'); + +test('exports', function (t) { + t.plan(32); + var dir = path.join(__dirname, '/exports'); + + resolve('mix-conditionals', { basedir: dir, ignoreExportsField: false }, 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, ignoreExportsField: false }, 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, ignoreExportsField: false }, 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, ignoreExportsField: false }, 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, ignoreExportsField: false }, 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, ignoreExportsField: false }, 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, ignoreExportsField: false }, 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, ignoreExportsField: false }, 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, ignoreExportsField: false }, 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, ignoreExportsField: false }, 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, ignoreExportsField: false }, 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, ignoreExportsField: false, 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, ignoreExportsField: false, 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, ignoreExportsField: false }); + 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, ignoreExportsField: false }); + 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, ignoreExportsField: false }); + 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, ignoreExportsField: false }); + 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, ignoreExportsField: false }), path.join(dir, 'node_modules/valid-config/exists.js')); + + try { + resolve.sync('valid-config/package.json', { basedir: dir, ignoreExportsField: false }); + 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, ignoreExportsField: false }), path.join(dir, 'node_modules/valid-config/exists.js')); + + t.equal(resolve.sync('valid-config/remapped/exists.js', { basedir: dir, ignoreExportsField: false }), path.join(dir, 'node_modules/valid-config/exists.js')); + + try { + resolve.sync('valid-config/remapped/doesnt-exist.js', { basedir: dir, ignoreExportsField: false }); + 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, ignoreExportsField: false }), path.join(dir, 'node_modules/valid-config/exists.js')); + + t.equal(resolve.sync('valid-config/with-env', { basedir: dir, ignoreExportsField: false }), 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, ignoreExportsField: false, 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, ignoreExportsField: false, packageIterator: iterateModifyingSubpath }), path.join(dir, 'other_modules/other-module-dir/index.js')); + + 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/index.js b/test/exports/node_modules/mix-conditionals/index.js new file mode 100644 index 00000000..e69de29b 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/main.js b/test/exports/node_modules/valid-config/main.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..a5c0f1e3 --- /dev/null +++ b/test/exports/node_modules/valid-config/package.json @@ -0,0 +1,19 @@ +{ + "name": "valid-config", + "main": "main.js", + "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 diff --git a/test/exports/other_modules/other-module-dir/exported.js b/test/exports/other_modules/other-module-dir/exported.js new file mode 100644 index 00000000..e69de29b diff --git a/test/exports/other_modules/other-module-dir/index.js b/test/exports/other_modules/other-module-dir/index.js new file mode 100644 index 00000000..e69de29b diff --git a/test/exports/other_modules/other-module-dir/package.json b/test/exports/other_modules/other-module-dir/package.json new file mode 100644 index 00000000..aed7d4e0 --- /dev/null +++ b/test/exports/other_modules/other-module-dir/package.json @@ -0,0 +1,6 @@ +{ + "name": "other-module-dir", + "exports": { + ".": "./exported.js" + } +} \ No newline at end of file diff --git a/test/exports_disabled.js b/test/exports_disabled.js new file mode 100644 index 00000000..e3313f78 --- /dev/null +++ b/test/exports_disabled.js @@ -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, ignoreExportsField: true }, 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, ignoreExportsField: true }, 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, ignoreExportsField: true }, 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, ignoreExportsField: true }, 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, ignoreExportsField: true }, 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, ignoreExportsField: true }, 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, ignoreExportsField: true }, 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, ignoreExportsField: true }, 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, ignoreExportsField: true }, 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, ignoreExportsField: true }, 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, ignoreExportsField: true }, 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, ignoreExportsField: true, 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, ignoreExportsField: true, 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, ignoreExportsField: true }), path.join(dir, 'node_modules/mix-conditionals/index.js')); + + try { + resolve.sync('invalid-config/with-node_modules', { basedir: dir, ignoreExportsField: true }); + 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, ignoreExportsField: true }); + 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, ignoreExportsField: true }); + 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, ignoreExportsField: true }), path.join(dir, 'node_modules/valid-config/main.js')); + + t.equal(resolve.sync('valid-config/package.json', { basedir: dir, ignoreExportsField: true }), path.join(dir, 'node_modules/valid-config/package.json')); + + try { + resolve.sync('valid-config/remapped', { basedir: dir, ignoreExportsField: true }); + 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, ignoreExportsField: true }); + 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, ignoreExportsField: true }); + 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, ignoreExportsField: true }); + 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, ignoreExportsField: true }); + 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, ignoreExportsField: true, 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, ignoreExportsField: true, packageIterator: iterateModifyingSubpath }), path.join(dir, 'other_modules/other-module-dir/index.js')); + + t.end(); +}); +