From ec4bc59b7fc67ea6dc49095d1f7bfd2eeee1d0db Mon Sep 17 00:00:00 2001 From: Bram Gotink Date: Sun, 23 Aug 2020 14:03:39 +0200 Subject: [PATCH 1/4] [New] add support for the exports package.json attribute --- .editorconfig | 2 +- .eslintrc | 1 + lib/async.js | 112 ++++++++-- lib/resolve-imports-exports.js | 208 ++++++++++++++++++ lib/sync.js | 102 +++++++-- package.json | 3 +- readme.markdown | 14 +- test/exports.js | 200 +++++++++++++++++ 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 | 16 ++ .../node_modules/mix-conditionals/index.js | 0 .../mix-conditionals/package.json | 7 + .../node_modules/valid-config/exists.js | 0 .../exports/node_modules/valid-config/main.js | 0 .../node_modules/valid-config/package.json | 21 ++ .../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 .../other-module-dir/exported.js | 0 .../other_modules/other-module-dir/index.js | 0 .../other-module-dir/package.json | 6 + test/exports_disabled.js | 174 +++++++++++++++ 25 files changed, 828 insertions(+), 39 deletions(-) create mode 100644 lib/resolve-imports-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/index.js 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/main.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 create mode 100644 test/exports/other_modules/other-module-dir/exported.js create mode 100644 test/exports/other_modules/other-module-dir/index.js create mode 100644 test/exports/other_modules/other-module-dir/package.json create mode 100644 test/exports_disabled.js diff --git a/.editorconfig b/.editorconfig index b96fcfbf..ac9da6cc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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,resolver,module_dir,multirepo,node_path,pathfilter,precedence}/**/*] indent_style = off indent_size = off max_line_length = off diff --git a/.eslintrc b/.eslintrc index a22863c8..2896ea01 100644 --- a/.eslintrc +++ b/.eslintrc @@ -27,6 +27,7 @@ "object-curly-newline": 0, "operator-linebreak": [2, "before"], "sort-keys": 0, + "eqeqeq": [2, "always", {"null": "ignore"}] }, "overrides": [ { diff --git a/lib/async.js b/lib/async.js index 29285079..55ade6c4 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'); 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; @@ -76,6 +78,7 @@ module.exports = function resolve(x, options, callback) { var includeCoreModules = opts.includeCoreModules !== false; var basedir = opts.basedir || path.dirname(caller()); var parent = opts.filename || basedir; + var conditions = opts.ignoreExportsField === false ? ['require', 'node'] : []; opts.paths = opts.paths || []; @@ -265,35 +268,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 (conditions.length > 0 && 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(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); + } + } } } + 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; + } }; diff --git a/lib/resolve-imports-exports.js b/lib/resolve-imports-exports.js new file mode 100644 index 00000000..650fe5ef --- /dev/null +++ b/lib/resolve-imports-exports.js @@ -0,0 +1,208 @@ +var path = require('path'); +var startsWith = require('string.prototype.startswith'); + +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(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) { + 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(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) { + 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 (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' || 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; + } + } + } + } + + 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(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 === '') { + 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( + packagePath, + parent, + longestMatchingExport, + matchObj[longestMatchingExport], + matchKey.slice(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 d5308c92..d6325aa5 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -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; @@ -69,6 +70,7 @@ module.exports = function resolveSync(x, options) { var includeCoreModules = opts.includeCoreModules !== false; var basedir = opts.basedir || path.dirname(caller()); var parent = opts.filename || basedir; + var conditions = opts.ignoreExportsField === false ? ['require', 'node'] : []; opts.paths = opts.paths || []; @@ -83,7 +85,7 @@ module.exports = function resolveSync(x, options) { } else if (includeCoreModules && 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); } @@ -141,7 +143,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 +156,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 +199,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 dfcfc497..c014f947 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "is-core-module": "^2.1.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 f742c38d..3131d850 100644 --- a/readme.markdown +++ b/readme.markdown @@ -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,8 @@ 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.ignoreExportsField - if false, take package exports into account + default `opts` values: ```js @@ -138,7 +140,8 @@ default `opts` values: }); }, moduleDirectory: 'node_modules', - preserveSymlinks: true + preserveSymlinks: true, + ignoreExportsField: true } ``` @@ -175,7 +178,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 +197,8 @@ 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.ignoreExportsField - if false, take package exports into account + default `opts` values: ```js @@ -233,7 +238,8 @@ default `opts` values: return file; }, moduleDirectory: 'node_modules', - preserveSymlinks: true + preserveSymlinks: true, + ignoreExportsField: true } ``` diff --git a/test/exports.js b/test/exports.js new file mode 100644 index 00000000..4af06c50 --- /dev/null +++ b/test/exports.js @@ -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, 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('invalid-config/numeric-key-1', { 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 numeric property keys/); + }); + + resolve('invalid-config/numeric-key-2', { 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 numeric property keys/); + }); + + 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"/); + } + + try { + resolve.sync('invalid-config/numeric-key-1', { basedir: dir, ignoreExportsField: false }); + 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, ignoreExportsField: false }); + 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, 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..e5d9691e --- /dev/null +++ b/test/exports/node_modules/invalid-config/package.json @@ -0,0 +1,16 @@ +{ + "name": "invalid-config", + "exports": { + "./with-node_modules": "./node_modules/foo/index.js", + "./outside-package": "./../mix-conditionals/package.json", + "./not-with-dot": "package.json", + "./numeric-key-1": { + "0": "./package.json", + "default": "./package.json" + }, + "./numeric-key-2": { + "586776": "./package.json", + "default": "./package.json" + } + } +} 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..affed776 --- /dev/null +++ b/test/exports/node_modules/valid-config/package.json @@ -0,0 +1,21 @@ +{ + "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", + "4th": "./with-env/default.js", + "596830284857604": "./with-env/default.js", + "default": "./with-env/default.js" + } + } +} 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..2b7dbf38 --- /dev/null +++ b/test/exports/other_modules/other-module-dir/package.json @@ -0,0 +1,6 @@ +{ + "name": "other-module-dir", + "exports": { + ".": "./exported.js" + } +} 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(); +}); + From dc69e83aefae4b13b9ca6c3a193a56d876e56d10 Mon Sep 17 00:00:00 2001 From: Bram Gotink Date: Mon, 7 Sep 2020 12:24:29 +0200 Subject: [PATCH 2/4] [Tests] add list-exports-tests tests --- .editorconfig | 4 +- .eslintignore | 1 + .gitignore | 3 + .gitmodules | 4 ++ package.json | 4 +- test/list-exports | 1 + test/list-exports-tests.js | 142 +++++++++++++++++++++++++++++++++++++ 7 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 .gitmodules create mode 160000 test/list-exports create mode 100644 test/list-exports-tests.js diff --git a/.editorconfig b/.editorconfig index ac9da6cc..2e727efd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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,exports,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 diff --git a/.eslintignore b/.eslintignore index 3c3629e6..a035980e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ node_modules +test/list-exports/ diff --git a/.gitignore b/.gitignore index 52e78ddc..c830970c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ yarn.lock # symlinked file used in tests test/resolver/symlinked/_/node_modules/package + +# submodule +test/list-exports diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..4d5d738a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "test/list-exports"] + path = test/list-exports + url = https://github.com/ljharb/list-exports.git + branch = main diff --git a/package.json b/package.json index c014f947..1b3ba571 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/list-exports b/test/list-exports new file mode 160000 index 00000000..c608373e --- /dev/null +++ b/test/list-exports @@ -0,0 +1 @@ +Subproject commit c608373e393f53e95c580f4a77dc2896f44188bf diff --git a/test/list-exports-tests.js b/test/list-exports-tests.js new file mode 100644 index 00000000..fdd102c3 --- /dev/null +++ b/test/list-exports-tests.js @@ -0,0 +1,142 @@ +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 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 optsWithExports = { packageIterator: packageIterator, ignoreExportsField: false, extensions: ['.js', '.json'] }; + var optsWithoutExports = { packageIterator: packageIterator, ignoreExportsField: true, extensions: ['.js', '.json'] }; + + if (fixtureName === 'ls-exports' || fixtureName === 'list-exports') { + optsWithExports.preserveSymlinks = true; + optsWithoutExports.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', optsWithoutExports, 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; + } + + t.plan(2 * (1 + fixtureSpec.require.length + fixtureSpec['require (pre-exports)'].length)); + + fixtureSpec.require.forEach(function (identifier) { + resolve(identifier, optsWithExports, 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, optsWithoutExports, 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', optsWithoutExports)), 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, optsWithExports); + 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, optsWithoutExports); + 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(); + }); +}); From 5ffcf6a133bd77b7dd430588a1fd9112b3243793 Mon Sep 17 00:00:00 2001 From: Bram Gotink Date: Wed, 7 Oct 2020 23:55:33 +0200 Subject: [PATCH 3/4] [New] rework exports option from boolean to enum --- lib/async.js | 11 +++++-- lib/sync.js | 11 +++++-- readme.markdown | 12 +++++--- test/exports.js | 60 +++++++++++++++++++------------------- test/exports_disabled.js | 52 ++++++++++++++++----------------- test/list-exports-tests.js | 4 +-- 6 files changed, 84 insertions(+), 66 deletions(-) diff --git a/lib/async.js b/lib/async.js index 55ade6c4..fcacd537 100644 --- a/lib/async.js +++ b/lib/async.js @@ -78,7 +78,14 @@ module.exports = function resolve(x, options, callback) { var includeCoreModules = opts.includeCoreModules !== false; var basedir = opts.basedir || path.dirname(caller()); var parent = opts.filename || basedir; - var conditions = opts.ignoreExportsField === false ? ['require', 'node'] : []; + + if (opts.exportsField == null) { + opts.exportsField = 'ignore'; + } else if (opts.exportsField !== 'respect' && opts.exportsField !== 'ignore') { + throw new TypeError('Invalid value for exportsField: ' + opts.exportsField); + } + + var conditions = opts.exportsField === 'respect' ? ['require', 'node'] : []; opts.paths = opts.paths || []; @@ -294,7 +301,7 @@ module.exports = function resolve(x, options, callback) { if (dirs.length === 0) return cb(null, undefined); var dir = dirs[0]; - if (conditions.length > 0 && endsWithSubpath(dir, subpath)) { + if (opts.exportsField !== 'ignore' && endsWithSubpath(dir, subpath)) { var pkgDir = dir.slice(0, dir.length - subpath.length); loadManifestInDir(pkgDir, onmanifestWithExports); } else { diff --git a/lib/sync.js b/lib/sync.js index d6325aa5..49abc166 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -70,7 +70,14 @@ module.exports = function resolveSync(x, options) { var includeCoreModules = opts.includeCoreModules !== false; var basedir = opts.basedir || path.dirname(caller()); var parent = opts.filename || basedir; - var conditions = opts.ignoreExportsField === false ? ['require', 'node'] : []; + + if (opts.exportsField == null) { + opts.exportsField = 'ignore'; + } else if (opts.exportsField !== 'respect' && opts.exportsField !== 'ignore') { + throw new TypeError('Invalid value for exportsField: ' + opts.exportsField); + } + + var conditions = opts.exportsField === 'respect' ? ['require', 'node'] : []; opts.paths = opts.paths || []; @@ -85,7 +92,7 @@ module.exports = function resolveSync(x, options) { } else if (includeCoreModules && isCore(x)) { return x; } else { - var n = (conditions.length > 0 ? loadNodeModulesWithExportsSync : loadNodeModulesSync)(x, absoluteStart); + var n = (opts.exportsField === 'ignore' ? loadNodeModulesSync : loadNodeModulesWithExportsSync)(x, absoluteStart); if (n) return maybeRealpathSync(realpathSync, n, opts); } diff --git a/readme.markdown b/readme.markdown index 3131d850..f891a46b 100644 --- a/readme.markdown +++ b/readme.markdown @@ -103,7 +103,9 @@ 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.ignoreExportsField - if false, take package exports into account +* opts.exportsField - the behavior of the exports field: + * `'respect'`: respect the exports field + * `'ignore'`: ignore the exports field default `opts` values: @@ -141,7 +143,7 @@ default `opts` values: }, moduleDirectory: 'node_modules', preserveSymlinks: true, - ignoreExportsField: true + exportsField: 'ignore', } ``` @@ -197,7 +199,9 @@ 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.ignoreExportsField - if false, take package exports into account +* opts.exportsField - the behavior of the exports field: + * `'respect'`: respect the exports field + * `'ignore'`: ignore the exports field default `opts` values: @@ -239,7 +243,7 @@ default `opts` values: }, moduleDirectory: 'node_modules', preserveSymlinks: true, - ignoreExportsField: true + exportsField: 'ignore', } ``` diff --git a/test/exports.js b/test/exports.js index 4af06c50..75157552 100644 --- a/test/exports.js +++ b/test/exports.js @@ -6,75 +6,75 @@ test('exports', function (t) { t.plan(38); var dir = path.join(__dirname, '/exports'); - resolve('mix-conditionals', { basedir: dir, ignoreExportsField: false }, function (err, res, pkg) { + 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, ignoreExportsField: false }, function (err, res, pkg) { + 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, ignoreExportsField: false }, function (err, res, pkg) { + 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, ignoreExportsField: false }, function (err, res, pkg) { + 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, ignoreExportsField: false }, function (err, res, pkg) { + 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, ignoreExportsField: false }, function (err, res, pkg) { + 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, ignoreExportsField: false }, function (err, res, pkg) { + 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, ignoreExportsField: false }, function (err, res, pkg) { + 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, ignoreExportsField: false }, function (err, res, pkg) { + 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, ignoreExportsField: false }, function (err, res, pkg) { + 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, ignoreExportsField: false }, function (err, res, pkg) { + 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, ignoreExportsField: false }, function (err, res, pkg) { + 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, ignoreExportsField: false }, function (err, res, pkg) { + 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')); }); @@ -82,7 +82,7 @@ test('exports', function (t) { 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) { + 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')); }); @@ -90,7 +90,7 @@ test('exports', function (t) { 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) { + 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')); }); @@ -100,7 +100,7 @@ test('exports sync', function (t) { var dir = path.join(__dirname, '/exports'); try { - resolve.sync('mix-conditionals', { basedir: dir, ignoreExportsField: false }); + resolve.sync('mix-conditionals', { basedir: dir, exportsField: 'respect' }); t.fail(); } catch (err) { t.equal(err.code, 'ERR_INVALID_PACKAGE_CONFIG'); @@ -108,7 +108,7 @@ test('exports sync', function (t) { } try { - resolve.sync('invalid-config/with-node_modules', { basedir: dir, ignoreExportsField: false }); + resolve.sync('invalid-config/with-node_modules', { basedir: dir, exportsField: 'respect' }); t.fail(); } catch (err) { t.equal(err.code, 'ERR_INVALID_PACKAGE_TARGET'); @@ -116,7 +116,7 @@ test('exports sync', function (t) { } try { - resolve.sync('invalid-config/outside-package', { basedir: dir, ignoreExportsField: false }); + resolve.sync('invalid-config/outside-package', { basedir: dir, exportsField: 'respect' }); t.fail(); } catch (err) { t.equal(err.code, 'ERR_INVALID_PACKAGE_TARGET'); @@ -124,7 +124,7 @@ test('exports sync', function (t) { } try { - resolve.sync('invalid-config/not-with-dot', { basedir: dir, ignoreExportsField: false }); + resolve.sync('invalid-config/not-with-dot', { basedir: dir, exportsField: 'respect' }); t.fail(); } catch (err) { t.equal(err.code, 'ERR_INVALID_PACKAGE_TARGET'); @@ -132,7 +132,7 @@ test('exports sync', function (t) { } try { - resolve.sync('invalid-config/numeric-key-1', { basedir: dir, ignoreExportsField: false }); + resolve.sync('invalid-config/numeric-key-1', { basedir: dir, exportsField: 'respect' }); t.fail(); } catch (err) { t.equal(err.code, 'ERR_INVALID_PACKAGE_CONFIG'); @@ -140,29 +140,29 @@ test('exports sync', function (t) { } try { - resolve.sync('invalid-config/numeric-key-2', { basedir: dir, ignoreExportsField: false }); + 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, ignoreExportsField: false }), path.join(dir, 'node_modules/valid-config/exists.js')); + 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, ignoreExportsField: false }); + 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, ignoreExportsField: false }), path.join(dir, 'node_modules/valid-config/exists.js')); + 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, ignoreExportsField: false }), 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, ignoreExportsField: false }); + resolve.sync('valid-config/remapped/doesnt-exist.js', { basedir: dir, exportsField: 'respect' }); t.fail(); } catch (err) { t.equal(err.code, 'MODULE_NOT_FOUND'); @@ -170,12 +170,12 @@ test('exports sync', function (t) { } t.equal( - resolve.sync('valid-config/array', { basedir: dir, ignoreExportsField: false }), + 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, ignoreExportsField: false }), + resolve.sync('valid-config/with-env', { basedir: dir, exportsField: 'respect' }), path.join(dir, 'node_modules/valid-config/with-env/require.js') ); @@ -183,7 +183,7 @@ test('exports sync', function (t) { return [path.join(opts.basedir, 'other_modules', request)]; } t.equal( - resolve.sync('other-module-dir', { basedir: dir, ignoreExportsField: false, packageIterator: iterateWithoutModifyingSubpath }), + resolve.sync('other-module-dir', { basedir: dir, exportsField: 'respect', packageIterator: iterateWithoutModifyingSubpath }), path.join(dir, 'other_modules/other-module-dir/exported.js') ); @@ -191,7 +191,7 @@ test('exports sync', function (t) { return [path.join(opts.basedir, 'other_modules', request, 'index')]; } t.equal( - resolve.sync('other-module-dir', { basedir: dir, ignoreExportsField: false, packageIterator: iterateModifyingSubpath }), + resolve.sync('other-module-dir', { basedir: dir, exportsField: 'respect', packageIterator: iterateModifyingSubpath }), path.join(dir, 'other_modules/other-module-dir/index.js') ); diff --git a/test/exports_disabled.js b/test/exports_disabled.js index e3313f78..e1b40d4f 100644 --- a/test/exports_disabled.js +++ b/test/exports_disabled.js @@ -6,64 +6,64 @@ 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) { + 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, ignoreExportsField: true }, function (err, res, pkg) { + 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, ignoreExportsField: true }, function (err, res, pkg) { + 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, ignoreExportsField: true }, function (err, res, pkg) { + 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, ignoreExportsField: true }, function (err, res, pkg) { + 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, ignoreExportsField: true }, function (err, res, pkg) { + 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, ignoreExportsField: true }, function (err, res, pkg) { + 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, ignoreExportsField: true }, function (err, res, pkg) { + 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, ignoreExportsField: true }, function (err, res, pkg) { + 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, ignoreExportsField: true }, function (err, res, pkg) { + 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, ignoreExportsField: true }, function (err, res, pkg) { + 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'/); @@ -72,7 +72,7 @@ test('exports (disabled)', function (t) { 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) { + 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')); }); @@ -80,7 +80,7 @@ test('exports (disabled)', function (t) { 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) { + 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')); }); @@ -89,10 +89,10 @@ test('exports (disabled)', function (t) { 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')); + 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, ignoreExportsField: true }); + resolve.sync('invalid-config/with-node_modules', { basedir: dir, exportsField: 'ignore' }); t.fail(); } catch (err) { t.equal(err.code, 'MODULE_NOT_FOUND'); @@ -100,7 +100,7 @@ test('exports sync (disabled)', function (t) { } try { - resolve.sync('invalid-config/outside-package', { basedir: dir, ignoreExportsField: true }); + resolve.sync('invalid-config/outside-package', { basedir: dir, exportsField: 'ignore' }); t.fail(); } catch (err) { t.equal(err.code, 'MODULE_NOT_FOUND'); @@ -108,19 +108,19 @@ test('exports sync (disabled)', function (t) { } try { - resolve.sync('invalid-config/not-with-dot', { basedir: dir, ignoreExportsField: true }); + 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, ignoreExportsField: true }), path.join(dir, 'node_modules/valid-config/main.js')); + 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, ignoreExportsField: true }), path.join(dir, 'node_modules/valid-config/package.json')); + 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, ignoreExportsField: true }); + resolve.sync('valid-config/remapped', { basedir: dir, exportsField: 'ignore' }); t.fail(); } catch (err) { t.equal(err.code, 'MODULE_NOT_FOUND'); @@ -128,7 +128,7 @@ test('exports sync (disabled)', function (t) { } try { - resolve.sync('valid-config/remapped/exists.js', { basedir: dir, ignoreExportsField: true }); + resolve.sync('valid-config/remapped/exists.js', { basedir: dir, exportsField: 'ignore' }); t.fail(); } catch (err) { t.equal(err.code, 'MODULE_NOT_FOUND'); @@ -136,7 +136,7 @@ test('exports sync (disabled)', function (t) { } try { - resolve.sync('valid-config/remapped/doesnt-exist.js', { basedir: dir, ignoreExportsField: true }); + resolve.sync('valid-config/remapped/doesnt-exist.js', { basedir: dir, exportsField: 'ignore' }); t.fail(); } catch (err) { t.equal(err.code, 'MODULE_NOT_FOUND'); @@ -144,7 +144,7 @@ test('exports sync (disabled)', function (t) { } try { - resolve.sync('valid-config/array', { basedir: dir, ignoreExportsField: true }); + resolve.sync('valid-config/array', { basedir: dir, exportsField: 'ignore' }); t.fail(); } catch (err) { t.equal(err.code, 'MODULE_NOT_FOUND'); @@ -152,7 +152,7 @@ test('exports sync (disabled)', function (t) { } try { - resolve.sync('valid-config/with-env', { basedir: dir, ignoreExportsField: true }); + resolve.sync('valid-config/with-env', { basedir: dir, exportsField: 'ignore' }); t.fail(); } catch (err) { t.equal(err.code, 'MODULE_NOT_FOUND'); @@ -162,12 +162,12 @@ test('exports sync (disabled)', function (t) { 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')); + 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, ignoreExportsField: true, packageIterator: iterateModifyingSubpath }), path.join(dir, 'other_modules/other-module-dir/index.js')); + 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(); }); diff --git a/test/list-exports-tests.js b/test/list-exports-tests.js index fdd102c3..24dab80a 100644 --- a/test/list-exports-tests.js +++ b/test/list-exports-tests.js @@ -23,8 +23,8 @@ fs.readdirSync(fixturesPath).forEach(function (fixtureName) { } } - var optsWithExports = { packageIterator: packageIterator, ignoreExportsField: false, extensions: ['.js', '.json'] }; - var optsWithoutExports = { packageIterator: packageIterator, ignoreExportsField: true, extensions: ['.js', '.json'] }; + var optsWithExports = { packageIterator: packageIterator, exportsField: 'respect', extensions: ['.js', '.json'] }; + var optsWithoutExports = { packageIterator: packageIterator, exportsField: 'ignore', extensions: ['.js', '.json'] }; if (fixtureName === 'ls-exports' || fixtureName === 'list-exports') { optsWithExports.preserveSymlinks = true; From b02aceb673e7b3a800a0bfd587e0e692a5c50ebc Mon Sep 17 00:00:00 2001 From: Bram Gotink Date: Sat, 7 Nov 2020 14:34:37 +0100 Subject: [PATCH 4/4] [New] add support for exports without conditions --- lib/async.js | 12 ++--- lib/resolve-imports-exports.js | 51 +++++++++++++++------ lib/sync.js | 12 ++--- readme.markdown | 2 + test/list-exports | 2 +- test/list-exports-tests.js | 84 +++++++++++++++++++++++++++++----- 6 files changed, 124 insertions(+), 39 deletions(-) diff --git a/lib/async.js b/lib/async.js index fcacd537..98d5c0a2 100644 --- a/lib/async.js +++ b/lib/async.js @@ -80,13 +80,11 @@ module.exports = function resolve(x, options, callback) { var parent = opts.filename || basedir; if (opts.exportsField == null) { - opts.exportsField = 'ignore'; - } else if (opts.exportsField !== 'respect' && opts.exportsField !== 'ignore') { - throw new TypeError('Invalid value for exportsField: ' + opts.exportsField); + opts.exportsField = { level: 'ignore' }; + } else if (typeof opts.exportsField === 'string') { + opts.exportsField = { level: opts.exportsField }; } - var conditions = opts.exportsField === 'respect' ? ['require', 'node'] : []; - opts.paths = opts.paths || []; // ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory @@ -301,7 +299,7 @@ module.exports = function resolve(x, options, callback) { if (dirs.length === 0) return cb(null, undefined); var dir = dirs[0]; - if (opts.exportsField !== 'ignore' && endsWithSubpath(dir, subpath)) { + if (opts.exportsField.level !== 'ignore' && endsWithSubpath(dir, subpath)) { var pkgDir = dir.slice(0, dir.length - subpath.length); loadManifestInDir(pkgDir, onmanifestWithExports); } else { @@ -315,7 +313,7 @@ module.exports = function resolve(x, options, callback) { var resolvedExport; try { - resolvedExport = resolveExports(pkgDir, parent, subpath, pkg.exports, conditions); + resolvedExport = resolveExports(opts.exportsField, pkgDir, parent, subpath, pkg.exports); } catch (resolveErr) { return cb(resolveErr); } diff --git a/lib/resolve-imports-exports.js b/lib/resolve-imports-exports.js index 650fe5ef..92a1fb7b 100644 --- a/lib/resolve-imports-exports.js +++ b/lib/resolve-imports-exports.js @@ -1,6 +1,23 @@ 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; @@ -46,7 +63,7 @@ function validateConditions(names, packagePath) { return names; } -function resolvePackageTarget(packagePath, parent, key, target, subpath, internal, conditions) { +function resolvePackageTarget(config, packagePath, parent, key, target, subpath, internal) { if (typeof target === 'string') { var resolvedTarget = path.resolve(packagePath, target); var invalidTarget = false; @@ -100,7 +117,15 @@ function resolvePackageTarget(packagePath, parent, key, target, subpath, interna var lastError; for (var i = 0; i < target.length; i++) { try { - return resolvePackageTarget(packagePath, parent, key, target[i], subpath, internal, conditions); + 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; @@ -120,7 +145,7 @@ function resolvePackageTarget(packagePath, parent, key, target, subpath, interna throw err; } - if (typeof target !== 'object') { + 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'; @@ -131,16 +156,16 @@ function resolvePackageTarget(packagePath, parent, key, target, subpath, interna for (i = 0; i < exportedConditions.length; i++) { var exportedCondition = exportedConditions[i]; - if (exportedCondition === 'default' || conditions.indexOf(exportedCondition) !== -1) { + if (exportedCondition === 'default' || config.conditions.indexOf(exportedCondition) !== -1) { try { return resolvePackageTarget( + config, packagePath, parent, key, target[exportedCondition], subpath, - internal, - conditions + internal ); } catch (e) { if (!e || e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') { @@ -157,10 +182,10 @@ function resolvePackageTarget(packagePath, parent, key, target, subpath, interna throw err; } -function resolveImportExport(packagePath, parent, matchObj, matchKey, isImports, conditions) { +function resolveImportExport(config, packagePath, parent, matchObj, matchKey, isImports) { if (Object.prototype.hasOwnProperty.call(matchObj, matchKey) && matchKey[matchKey.length - 1] !== '*') { return { - resolved: resolvePackageTarget(packagePath, parent, matchKey, matchObj[matchKey], '', isImports, conditions), + resolved: resolvePackageTarget(config, packagePath, parent, matchKey, matchObj[matchKey], '', isImports), exact: true }; } @@ -184,25 +209,25 @@ function resolveImportExport(packagePath, parent, matchObj, matchKey, isImports, return { resolved: resolvePackageTarget( + config, packagePath, parent, longestMatchingExport, matchObj[longestMatchingExport], matchKey.slice(longestMatchingExport.length - 1), - isImports, - conditions + isImports ), exact: false }; } -module.exports = function resolveExports(packagePath, parent, subpath, exports, conditions) { +module.exports = function resolveExports(config, packagePath, parent, subpath, exports) { return resolveImportExport( + parseConfig(config), packagePath, parent, validateExports(exports, packagePath), '.' + subpath, - false, - conditions + false ); }; diff --git a/lib/sync.js b/lib/sync.js index 49abc166..0a4cf857 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -72,13 +72,11 @@ module.exports = function resolveSync(x, options) { var parent = opts.filename || basedir; if (opts.exportsField == null) { - opts.exportsField = 'ignore'; - } else if (opts.exportsField !== 'respect' && opts.exportsField !== 'ignore') { - throw new TypeError('Invalid value for exportsField: ' + opts.exportsField); + opts.exportsField = { level: 'ignore' }; + } else if (typeof opts.exportsField === 'string') { + opts.exportsField = { level: opts.exportsField }; } - var conditions = opts.exportsField === 'respect' ? ['require', 'node'] : []; - opts.paths = opts.paths || []; // ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory @@ -92,7 +90,7 @@ module.exports = function resolveSync(x, options) { } else if (includeCoreModules && isCore(x)) { return x; } else { - var n = (opts.exportsField === 'ignore' ? loadNodeModulesSync : loadNodeModulesWithExportsSync)(x, absoluteStart); + var n = (opts.exportsField.level === 'ignore' ? loadNodeModulesSync : loadNodeModulesWithExportsSync)(x, absoluteStart); if (n) return maybeRealpathSync(realpathSync, n, opts); } @@ -238,7 +236,7 @@ module.exports = function resolveSync(x, options) { 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); + resolvedExport = resolveExports(opts.exportsField, pkgDir, parent, subpath, pkg.exports); } } diff --git a/readme.markdown b/readme.markdown index f891a46b..e59a8ad7 100644 --- a/readme.markdown +++ b/readme.markdown @@ -105,6 +105,7 @@ This is the way Node resolves dependencies when executed with the [--preserve-sy * 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: @@ -201,6 +202,7 @@ This is the way Node resolves dependencies when executed with the [--preserve-sy * 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: diff --git a/test/list-exports b/test/list-exports index c608373e..84a90dcc 160000 --- a/test/list-exports +++ b/test/list-exports @@ -1 +1 @@ -Subproject commit c608373e393f53e95c580f4a77dc2896f44188bf +Subproject commit 84a90dcc33bf85b32ef37732e2017ffd1a2146ff diff --git a/test/list-exports-tests.js b/test/list-exports-tests.js index 24dab80a..181748a4 100644 --- a/test/list-exports-tests.js +++ b/test/list-exports-tests.js @@ -7,6 +7,7 @@ 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) { @@ -23,12 +24,26 @@ fs.readdirSync(fixturesPath).forEach(function (fixtureName) { } } - var optsWithExports = { packageIterator: packageIterator, exportsField: 'respect', extensions: ['.js', '.json'] }; - var optsWithoutExports = { packageIterator: packageIterator, exportsField: 'ignore', extensions: ['.js', '.json'] }; + 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') { - optsWithExports.preserveSymlinks = true; - optsWithoutExports.preserveSymlinks = true; + optsRespect.preserveSymlinks = true; + optsRespectWithoutConditions.preserveSymlinks = true; + optsIgnore.preserveSymlinks = true; } test('list-exports-tests fixture ' + fixtureName, function (t) { @@ -36,7 +51,7 @@ fs.readdirSync(fixturesPath).forEach(function (fixtureName) { * 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', optsWithoutExports, function (err, res, pkg) { + 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'); }); @@ -48,10 +63,17 @@ fs.readdirSync(fixturesPath).forEach(function (fixtureName) { return; } - t.plan(2 * (1 + fixtureSpec.require.length + fixtureSpec['require (pre-exports)'].length)); + 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, optsWithExports, function (err, res, pkg) { + resolve(identifier, optsRespect, function (err, res, pkg) { t.ifErr(err); var tree = fixtureSpec.tree[fixtureSpec.name]; @@ -69,8 +91,29 @@ fs.readdirSync(fixturesPath).forEach(function (fixtureName) { }); }); + 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, optsWithoutExports, function (err, res, pkg) { + resolve(identifier, optsIgnore, function (err, res, pkg) { t.ifErr(err); var tree = fixtureSpec['tree (pre-exports)'][fixtureSpec.name]; @@ -94,7 +137,7 @@ fs.readdirSync(fixturesPath).forEach(function (fixtureName) { * 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', optsWithoutExports)), path.join(fixturePackagePath, 'package.json'), 'sanity check'); + t.equal(path.normalize(resolve.sync(fixtureSpec.name + '/package.json', optsIgnore)), path.join(fixturePackagePath, 'package.json'), 'sanity check'); // with exports enabled @@ -104,7 +147,7 @@ fs.readdirSync(fixturesPath).forEach(function (fixtureName) { } fixtureSpec.require.forEach(function (identifier) { - var resolved = resolve.sync(identifier, optsWithExports); + var resolved = resolve.sync(identifier, optsRespect); var tree = fixtureSpec.tree[fixtureSpec.name]; var relativeResolvedParts = path.relative(fixturePackagePath, resolved).split(path.sep); @@ -120,8 +163,27 @@ fs.readdirSync(fixturesPath).forEach(function (fixtureName) { 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, optsWithoutExports); + var resolved = resolve.sync(identifier, optsIgnore); var tree = fixtureSpec['tree (pre-exports)'][fixtureSpec.name]; var relativeResolvedParts = path.relative(fixturePackagePath, resolved).split(path.sep);