diff --git a/.editorconfig b/.editorconfig index b96fcfbf..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,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/.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/.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/lib/async.js b/lib/async.js index 29285079..98d5c0a2 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; @@ -77,6 +79,12 @@ module.exports = function resolve(x, options, callback) { var basedir = opts.basedir || path.dirname(caller()); var parent = opts.filename || basedir; + if (opts.exportsField == null) { + opts.exportsField = { level: 'ignore' }; + } else if (typeof opts.exportsField === 'string') { + opts.exportsField = { level: opts.exportsField }; + } + opts.paths = opts.paths || []; // ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory @@ -265,35 +273,112 @@ module.exports = function resolve(x, options, callback) { }); } - function processDirs(cb, dirs) { + function loadManifestInDir(dir, cb) { + maybeRealpath(realpath, dir, opts, function (err, pkgdir) { + if (err) return cb(null); + + var pkgfile = path.join(pkgdir, 'package.json'); + isFile(pkgfile, function (err, ex) { + // on err, ex is false + if (!ex) return cb(null); + + readFile(pkgfile, function (err, body) { + if (err) cb(err); + try { var pkg = JSON.parse(body); } catch (jsonErr) {} + + if (pkg && opts.packageFilter) { + pkg = opts.packageFilter(pkg, pkgfile, dir); + } + cb(pkg); + }); + }); + }); + } + + function processDirs(cb, dirs, subpath) { if (dirs.length === 0) return cb(null, undefined); var dir = dirs[0]; - isDirectory(path.dirname(dir), isdir); - - function isdir(err, isdir) { - if (err) return cb(err); - if (!isdir) return processDirs(cb, dirs.slice(1)); - loadAsFile(dir, opts.package, onfile); + if (opts.exportsField.level !== 'ignore' && endsWithSubpath(dir, subpath)) { + var pkgDir = dir.slice(0, dir.length - subpath.length); + loadManifestInDir(pkgDir, onmanifestWithExports); + } else { + onmanifest(false); } - function onfile(err, m, pkg) { - if (err) return cb(err); - if (m) return cb(null, m, pkg); - loadAsDirectory(dir, opts.package, ondir); + function onmanifestWithExports(pkg) { + if (!pkg || pkg.exports == null) { + return onmanifest(false); + } + + var resolvedExport; + try { + resolvedExport = resolveExports(opts.exportsField, pkgDir, parent, subpath, pkg.exports); + } catch (resolveErr) { + return cb(resolveErr); + } + + if (resolvedExport.exact) { + isFile(resolvedExport.resolved, function (err, ex) { + if (ex) { + cb(null, resolvedExport.resolved, pkg); + } else { + cb(null, undefined); + } + }); + } else { + dir = resolvedExport.resolved; + onmanifest(true); + } } - function ondir(err, n, pkg) { - if (err) return cb(err); - if (n) return cb(null, n, pkg); - processDirs(cb, dirs.slice(1)); + function onmanifest(stop) { + isDirectory(path.dirname(dir), isdir); + + function isdir(err, isdir) { + if (err) return cb(err); + if (!isdir) return next(); + loadAsFile(dir, opts.package, onfile); + } + + function onfile(err, m, pkg) { + if (err) return cb(err); + if (m) return cb(null, m, pkg); + loadAsDirectory(dir, opts.package, ondir); + } + + function ondir(err, n, pkg) { + if (err) return cb(err); + if (n) return cb(null, n, pkg); + next(); + } + + function next() { + if (stop) { + cb(null, undefined); + } else { + processDirs(cb, dirs.slice(1), subpath); + } + } } } + function loadNodeModules(x, start, cb) { + var subpathIndex = x.charAt(0) === '@' ? x.indexOf('/', x.indexOf('/') + 1) : x.indexOf('/'); + var subpath = subpathIndex === -1 ? '' : x.slice(subpathIndex); + var thunk = function () { return getPackageCandidates(x, start, opts); }; + processDirs( cb, - packageIterator ? packageIterator(x, start, thunk, opts) : thunk() + packageIterator ? packageIterator(x, start, thunk, opts) : thunk(), + subpath ); } + + function endsWithSubpath(dir, subpath) { + var endOfDir = dir.slice(dir.length - subpath.length); + + return endOfDir === subpath || endOfDir.replace(/\\/g, '/') === subpath; + } }; diff --git a/lib/resolve-imports-exports.js b/lib/resolve-imports-exports.js new file mode 100644 index 00000000..92a1fb7b --- /dev/null +++ b/lib/resolve-imports-exports.js @@ -0,0 +1,233 @@ +var path = require('path'); +var startsWith = require('string.prototype.startswith'); + +function parseConfig(config) { + var enableConditions = true; + + if (config.level !== 'respect') { + if (config.level === 'respect, without conditions') { + enableConditions = false; + } else { + throw new Error('Invalid exportsField level: ' + config.level); + } + } + + return { + enableConditions: enableConditions, + conditions: ['require', 'node'] + }; +} + +function validateExports(exports, basePath) { + var isConditional = true; + + if (typeof exports === 'object' && !Array.isArray(exports)) { + var exportKeys = Object.keys(exports); + + for (var i = 0; i < exportKeys.length; i++) { + var isKeyConditional = exportKeys[i][0] !== '.'; + if (i === 0) { + isConditional = isKeyConditional; + } else if (isKeyConditional !== isConditional) { + var err = new Error('Invalid package config ' + path.join(basePath, 'package.json') + ', ' + + '"exports" cannot contain some keys starting with \'.\' and some not. ' + + 'The exports object must either be an object of package subpath keys ' + + 'or an object of main entry condition name keys only.'); + err.code = 'ERR_INVALID_PACKAGE_CONFIG'; + throw err; + } + } + } + + if (isConditional) { + return { '.': exports }; + } else { + return exports; + } +} + +function validateConditions(names, packagePath) { + // If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error. + + for (var i = 0; i < names.length; i++) { + var name = names[i]; + var nameNum = Number(name); + + if (String(nameNum) === name && nameNum >= 0 && nameNum < 0xFFFFFFFF) { + var err = new Error('Invalid package config ' + path.join(packagePath, 'package.json') + '. "exports" cannot contain numeric property keys'); + err.code = 'ERR_INVALID_PACKAGE_CONFIG'; + throw err; + } + } + + return names; +} + +function resolvePackageTarget(config, packagePath, parent, key, target, subpath, internal) { + if (typeof target === 'string') { + var resolvedTarget = path.resolve(packagePath, target); + var invalidTarget = false; + + if (!startsWith(target, './')) { + if (!internal) { + invalidTarget = true; + } else if (!startsWith(target, '../') && !startsWith(target, '/')) { + invalidTarget = true; + } else { + // TODO: imports need call package_resolve here + } + } + + var targetParts = target.split(/[\\/]/).slice(1); // slice to strip the leading '.' + if (invalidTarget || targetParts.indexOf('node_modules') !== -1 || targetParts.indexOf('.') !== -1 || targetParts.indexOf('..') !== -1) { + var err = new Error('Invalid "exports" target ' + JSON.stringify(target) + + ' defined for ' + key + ' in ' + path.join(packagePath, 'package.json') + ' imported from ' + parent + '.'); + err.code = 'ERR_INVALID_PACKAGE_TARGET'; + throw err; + } + + if (subpath !== '' && target[target.length - 1] !== '/') { + err = new Error('Package subpath "' + subpath + '" is not a valid module request for ' + + 'the "exports" resolution of ' + path.join(packagePath, 'package.json') + ' imported from ' + parent + '.'); + err.code = 'ERR_INVALID_MODULE_SPECIFIER'; + throw err; + } + + var resolved = path.normalize(resolvedTarget + subpath); + var subpathParts = subpath.split(/[\\/]/); + if (!startsWith(resolved, resolvedTarget) || subpathParts.indexOf('node_modules') !== -1 || subpathParts.indexOf('.') !== -1 || subpathParts.indexOf('..') !== -1) { + err = new Error('Package subpath "' + subpath + '" is not a valid module request for ' + + 'the "exports" resolution of ' + path.join(packagePath, 'package.json') + ' imported from ' + parent + '.'); + err.code = 'ERR_INVALID_MODULE_SPECIFIER'; + throw err; + } + + return resolved; + } + + if (Array.isArray(target)) { + if (target.length === 0) { + err = new Error(key === '.' + ? 'No "exports" main resolved in ' + path.join(packagePath, 'package.json') + '.' + : 'Package subpath ' + key + ' is not defined by "exports" in ' + path.join(packagePath, 'package.json') + ' imported from ' + parent + '.'); + err.code = 'ERR_PACKAGE_PATH_NOT_EXPORTED'; + throw err; + } + + var lastError; + for (var i = 0; i < target.length; i++) { + try { + return resolvePackageTarget( + config, + packagePath, + parent, + key, + target[i], + subpath, + internal + ); + } catch (e) { + if (e && (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || e.code === 'ERR_INVALID_PACKAGE_TARGET')) { + lastError = e; + } else { + throw e; + } + } + } + throw lastError; + } + + if (target === null) { + err = new Error(key === '.' + ? 'No "exports" main resolved in ' + path.join(packagePath, 'package.json') + '.' + : 'Package subpath ' + key + ' is not defined by "exports" in ' + path.join(packagePath, 'package.json') + ' imported from ' + parent + '.'); + err.code = 'ERR_PACKAGE_PATH_NOT_EXPORTED'; + throw err; + } + + if (!config.enableConditions || typeof target !== 'object') { + err = new Error('Invalid "exports" target ' + JSON.stringify(target) + + ' defined for ' + key + ' in ' + path.join(packagePath, 'package.json') + ' imported from ' + parent + '.'); + err.code = 'ERR_INVALID_PACKAGE_TARGET'; + throw err; + } + + var exportedConditions = validateConditions(Object.keys(target), packagePath); + + for (i = 0; i < exportedConditions.length; i++) { + var exportedCondition = exportedConditions[i]; + if (exportedCondition === 'default' || config.conditions.indexOf(exportedCondition) !== -1) { + try { + return resolvePackageTarget( + config, + packagePath, + parent, + key, + target[exportedCondition], + subpath, + internal + ); + } catch (e) { + if (!e || e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') { + throw e; + } + } + } + } + + err = new Error(key === '.' + ? 'No "exports" main resolved in ' + path.join(packagePath, 'package.json') + '.' + : 'Package subpath ' + key + ' is not defined by "exports" in ' + path.join(packagePath, 'package.json') + ' imported from ' + parent + '.'); + err.code = 'ERR_PACKAGE_PATH_NOT_EXPORTED'; + throw err; +} + +function resolveImportExport(config, packagePath, parent, matchObj, matchKey, isImports) { + if (Object.prototype.hasOwnProperty.call(matchObj, matchKey) && matchKey[matchKey.length - 1] !== '*') { + return { + resolved: resolvePackageTarget(config, packagePath, parent, matchKey, matchObj[matchKey], '', isImports), + exact: true + }; + } + + var longestMatchingExport = ''; + var exportedPaths = Object.keys(matchObj); + + for (var i = 0; i < exportedPaths.length; i++) { + var exportedPath = exportedPaths[i]; + if (exportedPath[exportedPath.length - 1] === '/' && startsWith(matchKey, exportedPath) && exportedPath.length > longestMatchingExport.length) { + longestMatchingExport = exportedPath; + } + } + + if (longestMatchingExport === '') { + var err = new Error('Package subpath ' + matchKey + ' is not defined by "exports" in ' + + path.join(packagePath, 'package.json') + ' imported from ' + parent + '.'); + err.code = 'ERR_PACKAGE_PATH_NOT_EXPORTED'; + throw err; + } + + return { + resolved: resolvePackageTarget( + config, + packagePath, + parent, + longestMatchingExport, + matchObj[longestMatchingExport], + matchKey.slice(longestMatchingExport.length - 1), + isImports + ), + exact: false + }; +} + +module.exports = function resolveExports(config, packagePath, parent, subpath, exports) { + return resolveImportExport( + parseConfig(config), + packagePath, + parent, + validateExports(exports, packagePath), + '.' + subpath, + false + ); +}; diff --git a/lib/sync.js b/lib/sync.js index d5308c92..0a4cf857 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; @@ -70,6 +71,12 @@ module.exports = function resolveSync(x, options) { var basedir = opts.basedir || path.dirname(caller()); var parent = opts.filename || basedir; + if (opts.exportsField == null) { + opts.exportsField = { level: 'ignore' }; + } else if (typeof opts.exportsField === 'string') { + opts.exportsField = { level: opts.exportsField }; + } + opts.paths = opts.paths || []; // ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory @@ -83,7 +90,7 @@ module.exports = function resolveSync(x, options) { } else if (includeCoreModules && isCore(x)) { return x; } else { - var n = loadNodeModulesSync(x, absoluteStart); + var n = (opts.exportsField.level === 'ignore' ? loadNodeModulesSync : loadNodeModulesWithExportsSync)(x, absoluteStart); if (n) return maybeRealpathSync(realpathSync, n, opts); } @@ -141,7 +148,7 @@ module.exports = function resolveSync(x, options) { return { pkg: pkg, dir: dir }; } - function loadAsDirectorySync(x) { + function loadManifestInDir(x) { var pkgfile = path.join(maybeRealpathSync(realpathSync, x, opts), '/package.json'); if (isFile(pkgfile)) { try { @@ -154,22 +161,30 @@ module.exports = function resolveSync(x, options) { pkg = opts.packageFilter(pkg, /*pkgfile,*/ x); // eslint-disable-line spaced-comment } - if (pkg && pkg.main) { - if (typeof pkg.main !== 'string') { - var mainError = new TypeError('package “' + pkg.name + '” `main` must be a string'); - mainError.code = 'INVALID_PACKAGE_MAIN'; - throw mainError; - } - if (pkg.main === '.' || pkg.main === './') { - pkg.main = 'index'; - } - try { - var m = loadAsFileSync(path.resolve(x, pkg.main)); - if (m) return m; - var n = loadAsDirectorySync(path.resolve(x, pkg.main)); - if (n) return n; - } catch (e) {} + return pkg; + } + + return null; + } + + function loadAsDirectorySync(x) { + var pkg = loadManifestInDir(x); + + if (pkg && pkg.main) { + if (typeof pkg.main !== 'string') { + var mainError = new TypeError('package “' + pkg.name + '” `main` must be a string'); + mainError.code = 'INVALID_PACKAGE_MAIN'; + throw mainError; } + if (pkg.main === '.' || pkg.main === './') { + pkg.main = 'index'; + } + try { + var m = loadAsFileSync(path.resolve(x, pkg.main)); + if (m) return m; + var n = loadAsDirectorySync(path.resolve(x, pkg.main)); + if (n) return n; + } catch (e) {} } return loadAsFileSync(path.join(x, '/index')); @@ -189,4 +204,62 @@ module.exports = function resolveSync(x, options) { } } } + + function loadNodeModulesWithExportsSync(x, start) { + var thunk = function () { return getPackageCandidates(x, start, opts); }; + var dirs = packageIterator ? packageIterator(x, start, thunk, opts) : thunk(); + + var subpathIndex = x.indexOf('/'); + if (x[0] === '@') { + subpathIndex = x.indexOf('/', subpathIndex + 1); + } + var subpath; + if (subpathIndex === -1) { + subpath = ''; + } else { + subpath = x.slice(subpathIndex); + } + var subpathLength = subpath.length; + + var endsWithSubpath = function (dir) { + var endOfDir = dir.slice(dir.length - subpathLength); + + return endOfDir === subpath || endOfDir.replace(/\\/g, '/') === subpath; + }; + + for (var i = 0; i < dirs.length; i++) { + var dir = dirs[i]; + + var pkg; + + var resolvedExport; + if (endsWithSubpath(dir)) { + var pkgDir = dir.slice(0, dir.length - subpathLength); + if ((pkg = loadManifestInDir(pkgDir)) && pkg.exports) { + resolvedExport = resolveExports(opts.exportsField, pkgDir, parent, subpath, pkg.exports); + } + } + + if (resolvedExport) { + if (resolvedExport.exact) { + if (isFile(resolvedExport.resolved)) { + return resolvedExport.resolved; + } else { + return; + } + } else { + dir = resolvedExport.resolved; + } + } + + if (isDirectory(path.dirname(dir))) { + var m = loadAsFileSync(dir) || loadAsDirectorySync(dir); + if (m) return m; + } + + if (resolvedExport) { + return; + } + } + } }; diff --git a/package.json b/package.json index dfcfc497..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", @@ -46,6 +48,7 @@ }, "dependencies": { "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" + "path-parse": "^1.0.6", + "string.prototype.startswith": "^1.0.0" } } diff --git a/readme.markdown b/readme.markdown index f742c38d..e59a8ad7 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,11 @@ This is the way Node resolves dependencies when executed with the [--preserve-sy **Note:** this property is currently `true` by default but it will be changed to `false` in the next major version because *Node's resolution algorithm does not preserve symlinks by default*. +* opts.exportsField - the behavior of the exports field: + * `'respect'`: respect the exports field + * `'respect, without exports'`: respect the exports field without supporting conditional exports + * `'ignore'`: ignore the exports field + default `opts` values: ```js @@ -138,7 +143,8 @@ default `opts` values: }); }, moduleDirectory: 'node_modules', - preserveSymlinks: true + preserveSymlinks: true, + exportsField: 'ignore', } ``` @@ -175,7 +181,7 @@ options are: * opts.paths - require.paths array to use if nothing is found on the normal `node_modules` recursive walk (probably don't use this) - For advanced users, `paths` can also be a `opts.paths(request, start, opts)` function + For advanced users, `paths` can also be a `opts.paths(request, start, getNodeModulesDirs, opts)` function * request - the import specifier being resolved * start - lookup path * getNodeModulesDirs - a thunk (no-argument function) that returns the paths using standard `node_modules` resolution @@ -194,6 +200,11 @@ This is the way Node resolves dependencies when executed with the [--preserve-sy **Note:** this property is currently `true` by default but it will be changed to `false` in the next major version because *Node's resolution algorithm does not preserve symlinks by default*. +* opts.exportsField - the behavior of the exports field: + * `'respect'`: respect the exports field + * `'respect, without exports'`: respect the exports field without supporting conditional exports + * `'ignore'`: ignore the exports field + default `opts` values: ```js @@ -233,7 +244,8 @@ default `opts` values: return file; }, moduleDirectory: 'node_modules', - preserveSymlinks: true + preserveSymlinks: true, + exportsField: 'ignore', } ``` diff --git a/test/exports.js b/test/exports.js new file mode 100644 index 00000000..75157552 --- /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, exportsField: 'respect' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'ERR_INVALID_PACKAGE_CONFIG'); + t.match(err && err.message, /"exports" cannot contain some keys starting with '.' and some not./); + }); + + resolve('invalid-config/with-node_modules', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'ERR_INVALID_PACKAGE_TARGET'); + t.match(err && err.message, /Invalid "exports" target "\.\/node_modules\/foo\/index\.js"/); + }); + + resolve('invalid-config/outside-package', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'ERR_INVALID_PACKAGE_TARGET'); + t.match(err && err.message, /Invalid "exports" target "\.\/\.\.\/mix-conditionals\/package\.json"/); + }); + + resolve('invalid-config/not-with-dot', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'ERR_INVALID_PACKAGE_TARGET'); + t.match(err && err.message, /Invalid "exports" target "package\.json"/); + }); + + resolve('invalid-config/numeric-key-1', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'ERR_INVALID_PACKAGE_CONFIG'); + t.match(err && err.message, /"exports" cannot contain numeric property keys/); + }); + + resolve('invalid-config/numeric-key-2', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'ERR_INVALID_PACKAGE_CONFIG'); + t.match(err && err.message, /"exports" cannot contain numeric property keys/); + }); + + resolve('valid-config', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) { + t.ifError(err); + t.equal(res, path.join(dir, 'node_modules/valid-config/exists.js')); + }); + + resolve('valid-config/package.json', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'ERR_PACKAGE_PATH_NOT_EXPORTED'); + t.match(err && err.message, /Package subpath \.\/package\.json is not defined by "exports" in/); + }); + + resolve('valid-config/remapped', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) { + t.ifError(err); + t.equal(res, path.join(dir, 'node_modules/valid-config/exists.js')); + }); + + resolve('valid-config/remapped/exists.js', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) { + t.ifError(err); + t.equal(res, path.join(dir, 'node_modules/valid-config/exists.js')); + }); + + resolve('valid-config/remapped/doesnt-exist.js', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'MODULE_NOT_FOUND'); + t.match(err && err.message, /Cannot find module 'valid-config\/remapped\/doesnt-exist\.js'/); + }); + + resolve('valid-config/array', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) { + t.ifErr(err); + t.equal(res, path.join(dir, 'node_modules/valid-config/exists.js')); + }); + + resolve('valid-config/with-env', { basedir: dir, exportsField: 'respect' }, function (err, res, pkg) { + t.ifErr(err); + t.equal(res, path.join(dir, 'node_modules/valid-config/with-env/require.js')); + }); + + function iterateWithoutModifyingSubpath(request, start, getNodeModulesDirs, opts) { + return [path.join(opts.basedir, 'other_modules', request)]; + } + resolve('other-module-dir', { basedir: dir, exportsField: 'respect', packageIterator: iterateWithoutModifyingSubpath }, function (err, res, pkg) { + t.ifErr(err); + t.equal(res, path.join(dir, 'other_modules/other-module-dir/exported.js')); + }); + + function iterateModifyingSubpath(request, start, getNodeModulesDirs, opts) { + return [path.join(opts.basedir, 'other_modules', request, 'index')]; + } + resolve('other-module-dir', { basedir: dir, exportsField: 'respect', packageIterator: iterateModifyingSubpath }, function (err, res, pkg) { + t.ifErr(err); + t.equal(res, path.join(dir, 'other_modules/other-module-dir/index.js')); + }); +}); + +test('exports sync', function (t) { + var dir = path.join(__dirname, '/exports'); + + try { + resolve.sync('mix-conditionals', { basedir: dir, exportsField: 'respect' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'ERR_INVALID_PACKAGE_CONFIG'); + t.match(err.message, /"exports" cannot contain some keys starting with '.' and some not./); + } + + try { + resolve.sync('invalid-config/with-node_modules', { basedir: dir, exportsField: 'respect' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'ERR_INVALID_PACKAGE_TARGET'); + t.match(err.message, /Invalid "exports" target "\.\/node_modules\/foo\/index\.js"/); + } + + try { + resolve.sync('invalid-config/outside-package', { basedir: dir, exportsField: 'respect' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'ERR_INVALID_PACKAGE_TARGET'); + t.match(err.message, /Invalid "exports" target "\.\/\.\.\/mix-conditionals\/package\.json"/); + } + + try { + resolve.sync('invalid-config/not-with-dot', { basedir: dir, exportsField: 'respect' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'ERR_INVALID_PACKAGE_TARGET'); + t.match(err.message, /Invalid "exports" target "package\.json"/); + } + + try { + resolve.sync('invalid-config/numeric-key-1', { basedir: dir, exportsField: 'respect' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'ERR_INVALID_PACKAGE_CONFIG'); + t.match(err.message, /"exports" cannot contain numeric property keys/); + } + + try { + resolve.sync('invalid-config/numeric-key-2', { basedir: dir, exportsField: 'respect' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'ERR_INVALID_PACKAGE_CONFIG'); + t.match(err.message, /"exports" cannot contain numeric property keys/); + } + + t.equal(resolve.sync('valid-config', { basedir: dir, exportsField: 'respect' }), path.join(dir, 'node_modules/valid-config/exists.js')); + + try { + resolve.sync('valid-config/package.json', { basedir: dir, exportsField: 'respect' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'ERR_PACKAGE_PATH_NOT_EXPORTED'); + t.match(err.message, /Package subpath \.\/package\.json is not defined by "exports" in/); + } + + t.equal(resolve.sync('valid-config/remapped', { basedir: dir, exportsField: 'respect' }), path.join(dir, 'node_modules/valid-config/exists.js')); + + t.equal(resolve.sync('valid-config/remapped/exists.js', { basedir: dir, exportsField: 'respect' }), path.join(dir, 'node_modules/valid-config/exists.js')); + + try { + resolve.sync('valid-config/remapped/doesnt-exist.js', { basedir: dir, exportsField: 'respect' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'MODULE_NOT_FOUND'); + t.match(err.message, /Cannot find module 'valid-config\/remapped\/doesnt-exist\.js'/); + } + + t.equal( + resolve.sync('valid-config/array', { basedir: dir, exportsField: 'respect' }), + path.join(dir, 'node_modules/valid-config/exists.js') + ); + + t.equal( + resolve.sync('valid-config/with-env', { basedir: dir, exportsField: 'respect' }), + path.join(dir, 'node_modules/valid-config/with-env/require.js') + ); + + function iterateWithoutModifyingSubpath(request, start, getNodeModulesDirs, opts) { + return [path.join(opts.basedir, 'other_modules', request)]; + } + t.equal( + resolve.sync('other-module-dir', { basedir: dir, exportsField: 'respect', packageIterator: iterateWithoutModifyingSubpath }), + path.join(dir, 'other_modules/other-module-dir/exported.js') + ); + + function iterateModifyingSubpath(request, start, getNodeModulesDirs, opts) { + return [path.join(opts.basedir, 'other_modules', request, 'index')]; + } + t.equal( + resolve.sync('other-module-dir', { basedir: dir, exportsField: 'respect', packageIterator: iterateModifyingSubpath }), + path.join(dir, 'other_modules/other-module-dir/index.js') + ); + + t.end(); +}); + 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..e1b40d4f --- /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, exportsField: 'ignore' }, function (err, res, pkg) { + t.ifErr(err); + t.equal(res, path.join(dir, 'node_modules/mix-conditionals/index.js')); + }); + + resolve('invalid-config/with-node_modules', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'MODULE_NOT_FOUND'); + t.match(err && err.message, /Cannot find module 'invalid-config\/with-node_modules'/); + }); + + resolve('invalid-config/outside-package', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'MODULE_NOT_FOUND'); + t.match(err && err.message, /Cannot find module 'invalid-config\/outside-package'/); + }); + + resolve('invalid-config/not-with-dot', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'MODULE_NOT_FOUND'); + t.match(err && err.message, /Cannot find module 'invalid-config\/not-with-dot'/); + }); + + resolve('valid-config', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) { + t.ifError(err); + t.equal(res, path.join(dir, 'node_modules/valid-config/main.js')); + }); + + resolve('valid-config/package.json', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) { + t.ifError(err); + t.equal(res, path.join(dir, 'node_modules/valid-config/package.json')); + }); + + resolve('valid-config/remapped', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'MODULE_NOT_FOUND'); + t.match(err && err.message, /Cannot find module 'valid-config\/remapped'/); + }); + + resolve('valid-config/remapped/exists.js', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'MODULE_NOT_FOUND'); + t.match(err && err.message, /Cannot find module 'valid-config\/remapped\/exists.js'/); + }); + + resolve('valid-config/remapped/doesnt-exist.js', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'MODULE_NOT_FOUND'); + t.match(err && err.message, /Cannot find module 'valid-config\/remapped\/doesnt-exist\.js'/); + }); + + resolve('valid-config/array', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'MODULE_NOT_FOUND'); + t.match(err && err.message, /Cannot find module 'valid-config\/array'/); + }); + + resolve('valid-config/with-env', { basedir: dir, exportsField: 'ignore' }, function (err, res, pkg) { + t.notOk(res); + t.equal(err && err.code, 'MODULE_NOT_FOUND'); + t.match(err && err.message, /Cannot find module 'valid-config\/with-env'/); + }); + + function iterateWithoutModifyingSubpath(request, start, getNodeModulesDirs, opts) { + return [path.join(opts.basedir, 'other_modules', request)]; + } + resolve('other-module-dir', { basedir: dir, exportsField: 'ignore', packageIterator: iterateWithoutModifyingSubpath }, function (err, res, pkg) { + t.ifErr(err); + t.equal(res, path.join(dir, 'other_modules/other-module-dir/index.js')); + }); + + function iterateModifyingSubpath(request, start, getNodeModulesDirs, opts) { + return [path.join(opts.basedir, 'other_modules', request, 'index')]; + } + resolve('other-module-dir', { basedir: dir, exportsField: 'ignore', packageIterator: iterateModifyingSubpath }, function (err, res, pkg) { + t.ifErr(err); + t.equal(res, path.join(dir, 'other_modules/other-module-dir/index.js')); + }); +}); + +test('exports sync (disabled)', function (t) { + var dir = path.join(__dirname, '/exports'); + + t.equal(resolve.sync('mix-conditionals', { basedir: dir, exportsField: 'ignore' }), path.join(dir, 'node_modules/mix-conditionals/index.js')); + + try { + resolve.sync('invalid-config/with-node_modules', { basedir: dir, exportsField: 'ignore' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'MODULE_NOT_FOUND'); + t.match(err.message, /Cannot find module 'invalid-config\/with-node_modules'/); + } + + try { + resolve.sync('invalid-config/outside-package', { basedir: dir, exportsField: 'ignore' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'MODULE_NOT_FOUND'); + t.match(err.message, /Cannot find module 'invalid-config\/outside-package'/); + } + + try { + resolve.sync('invalid-config/not-with-dot', { basedir: dir, exportsField: 'ignore' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'MODULE_NOT_FOUND'); + t.match(err.message, /Cannot find module 'invalid-config\/not-with-dot'/); + } + + t.equal(resolve.sync('valid-config', { basedir: dir, exportsField: 'ignore' }), path.join(dir, 'node_modules/valid-config/main.js')); + + t.equal(resolve.sync('valid-config/package.json', { basedir: dir, exportsField: 'ignore' }), path.join(dir, 'node_modules/valid-config/package.json')); + + try { + resolve.sync('valid-config/remapped', { basedir: dir, exportsField: 'ignore' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'MODULE_NOT_FOUND'); + t.match(err.message, /Cannot find module 'valid-config\/remapped'/); + } + + try { + resolve.sync('valid-config/remapped/exists.js', { basedir: dir, exportsField: 'ignore' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'MODULE_NOT_FOUND'); + t.match(err.message, /Cannot find module 'valid-config\/remapped\/exists.js'/); + } + + try { + resolve.sync('valid-config/remapped/doesnt-exist.js', { basedir: dir, exportsField: 'ignore' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'MODULE_NOT_FOUND'); + t.match(err.message, /Cannot find module 'valid-config\/remapped\/doesnt-exist\.js'/); + } + + try { + resolve.sync('valid-config/array', { basedir: dir, exportsField: 'ignore' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'MODULE_NOT_FOUND'); + t.match(err.message, /Cannot find module 'valid-config\/array'/); + } + + try { + resolve.sync('valid-config/with-env', { basedir: dir, exportsField: 'ignore' }); + t.fail(); + } catch (err) { + t.equal(err.code, 'MODULE_NOT_FOUND'); + t.match(err.message, /Cannot find module 'valid-config\/with-env'/); + } + + function iterateWithoutModifyingSubpath(request, start, getNodeModulesDirs, opts) { + return [path.join(opts.basedir, 'other_modules', request)]; + } + t.equal(resolve.sync('other-module-dir', { basedir: dir, exportsField: 'ignore', packageIterator: iterateWithoutModifyingSubpath }), path.join(dir, 'other_modules/other-module-dir/index.js')); + + function iterateModifyingSubpath(request, start, getNodeModulesDirs, opts) { + return [path.join(opts.basedir, 'other_modules', request, 'index')]; + } + t.equal(resolve.sync('other-module-dir', { basedir: dir, exportsField: 'ignore', packageIterator: iterateModifyingSubpath }), path.join(dir, 'other_modules/other-module-dir/index.js')); + + t.end(); +}); + diff --git a/test/list-exports b/test/list-exports new file mode 160000 index 00000000..84a90dcc --- /dev/null +++ b/test/list-exports @@ -0,0 +1 @@ +Subproject commit 84a90dcc33bf85b32ef37732e2017ffd1a2146ff diff --git a/test/list-exports-tests.js b/test/list-exports-tests.js new file mode 100644 index 00000000..181748a4 --- /dev/null +++ b/test/list-exports-tests.js @@ -0,0 +1,204 @@ +var fs = require('fs'); +var path = require('path'); +var test = require('tape'); +var resolve = require('../'); + +var fixturesPath = path.join(__dirname, 'list-exports/packages/tests/fixtures'); + +fs.readdirSync(fixturesPath).forEach(function (fixtureName) { + var fixtureSpec = require(path.join(fixturesPath, fixtureName, 'expected.json')); + var fixtureWithoutConditionsSpec = require(path.join(fixturesPath, fixtureName, 'expected-without-conditions.json')); + var fixturePackagePath = path.join(fixturesPath, fixtureName, 'project'); + + function packageIterator(identifier) { + var slashIdx = identifier.indexOf('/'); + + if (slashIdx === -1) { + return identifier === fixtureSpec.name ? [fixturePackagePath] : null; + } + + if (identifier.slice(0, slashIdx) === fixtureSpec.name) { + return [fixturePackagePath + identifier.slice(slashIdx)]; + } else { + return null; + } + } + + var optsRespect = { + exportsField: 'respect', + packageIterator: packageIterator, + extensions: ['.js', '.json'] + }; + var optsRespectWithoutConditions = { + exportsField: 'respect, without conditions', + packageIterator: packageIterator, + extensions: ['.js', '.json'] + }; + var optsIgnore = { + exportsField: 'ignore', + packageIterator: packageIterator, + extensions: ['.js', '.json'] + }; + + if (fixtureName === 'ls-exports' || fixtureName === 'list-exports') { + optsRespect.preserveSymlinks = true; + optsRespectWithoutConditions.preserveSymlinks = true; + optsIgnore.preserveSymlinks = true; + } + + test('list-exports-tests fixture ' + fixtureName, function (t) { + /* + * Sanity check: package.json should be resolvable with exports disabled + * All other tests are configured via the expected.json file + */ + resolve(fixtureSpec.name + '/package.json', optsIgnore, function (err, res, pkg) { + t.ifErr(err); + t.equal(path.normalize(res), path.join(fixturePackagePath, 'package.json'), 'sanity check'); + }); + + // with exports enabled + + if (fixtureSpec.private) { + t.plan(2); + return; + } + + var skipTestWithoutConditions = fixtureName === 'preact'; + + t.plan(2 * ( + 1 + + fixtureSpec.require.length + + fixtureSpec['require (pre-exports)'].length + + (skipTestWithoutConditions ? 0 : fixtureWithoutConditionsSpec.require.length) + )); + + fixtureSpec.require.forEach(function (identifier) { + resolve(identifier, optsRespect, function (err, res, pkg) { + t.ifErr(err); + var tree = fixtureSpec.tree[fixtureSpec.name]; + + var relativeResolvedParts = path.relative(fixturePackagePath, res).split(path.sep); + + for (var i = 0; i < relativeResolvedParts.length; i++) { + tree = tree[relativeResolvedParts[i]]; + + if (!tree) { + t.fail('Unexpected resolved path ' + JSON.stringify(res) + ' for ' + JSON.stringify(identifier)); + } + } + + t.notEqual(tree.indexOf(identifier), -1, 'resolved path ' + JSON.stringify(res) + ' for ' + JSON.stringify(identifier)); + }); + }); + + if (!skipTestWithoutConditions) { + fixtureWithoutConditionsSpec.require.forEach(function (identifier) { + resolve(identifier, optsRespectWithoutConditions, function (err, res, pkg) { + t.ifErr(err); + var tree = fixtureSpec.tree[fixtureSpec.name]; + + var relativeResolvedParts = path.relative(fixturePackagePath, res).split(path.sep); + + for (var i = 0; i < relativeResolvedParts.length; i++) { + tree = tree[relativeResolvedParts[i]]; + + if (!tree) { + t.fail('Unexpected resolved path ' + JSON.stringify(res) + ' for ' + JSON.stringify(identifier)); + } + } + + t.notEqual(tree.indexOf(identifier), -1, 'resolved path ' + JSON.stringify(res) + ' for ' + JSON.stringify(identifier)); + }); + }); + } + + fixtureSpec['require (pre-exports)'].forEach(function (identifier) { + resolve(identifier, optsIgnore, function (err, res, pkg) { + t.ifErr(err); + var tree = fixtureSpec['tree (pre-exports)'][fixtureSpec.name]; + + var relativeResolvedParts = path.relative(fixturePackagePath, res).split(path.sep); + + for (var i = 0; i < relativeResolvedParts.length; i++) { + tree = tree[relativeResolvedParts[i]]; + + if (!tree) { + t.fail('Unexpected resolved path ' + JSON.stringify(res) + ' for ' + JSON.stringify(identifier)); + } + } + + t.notEqual(tree.indexOf(identifier), -1, 'resolved path ' + JSON.stringify(res) + ' for ' + JSON.stringify(identifier)); + }); + }); + }); + + test('list-exports-tests fixture ' + fixtureName + ' sync', function (t) { + /* + * Sanity check: package.json should be resolvable with exports disabled + * All other tests are configured via the expected.json file + */ + t.equal(path.normalize(resolve.sync(fixtureSpec.name + '/package.json', optsIgnore)), path.join(fixturePackagePath, 'package.json'), 'sanity check'); + + // with exports enabled + + if (fixtureSpec.private) { + t.end(); + return; + } + + fixtureSpec.require.forEach(function (identifier) { + var resolved = resolve.sync(identifier, optsRespect); + var tree = fixtureSpec.tree[fixtureSpec.name]; + + var relativeResolvedParts = path.relative(fixturePackagePath, resolved).split(path.sep); + + for (var i = 0; i < relativeResolvedParts.length; i++) { + tree = tree[relativeResolvedParts[i]]; + + if (!tree) { + t.fail('Unexpected resolved path ' + JSON.stringify(resolved) + ' for ' + JSON.stringify(identifier)); + } + } + + t.notEqual(tree.indexOf(identifier), -1, 'resolved path ' + JSON.stringify(resolved) + ' for ' + JSON.stringify(identifier)); + }); + + if (fixtureName !== 'preact') { + fixtureWithoutConditionsSpec.require.forEach(function (identifier) { + var resolved = resolve.sync(identifier, optsRespectWithoutConditions); + var tree = fixtureSpec.tree[fixtureSpec.name]; + + var relativeResolvedParts = path.relative(fixturePackagePath, resolved).split(path.sep); + + for (var i = 0; i < relativeResolvedParts.length; i++) { + tree = tree[relativeResolvedParts[i]]; + + if (!tree) { + t.fail('Unexpected resolved path ' + JSON.stringify(resolved) + ' for ' + JSON.stringify(identifier)); + } + } + + t.notEqual(tree.indexOf(identifier), -1, 'resolved path ' + JSON.stringify(resolved) + ' for ' + JSON.stringify(identifier)); + }); + } + + fixtureSpec['require (pre-exports)'].forEach(function (identifier) { + var resolved = resolve.sync(identifier, optsIgnore); + var tree = fixtureSpec['tree (pre-exports)'][fixtureSpec.name]; + + var relativeResolvedParts = path.relative(fixturePackagePath, resolved).split(path.sep); + + for (var i = 0; i < relativeResolvedParts.length; i++) { + tree = tree[relativeResolvedParts[i]]; + + if (!tree) { + t.fail('Unexpected resolved path ' + JSON.stringify(resolved) + ' for ' + JSON.stringify(identifier)); + } + } + + t.notEqual(tree.indexOf(identifier), -1, 'resolved path ' + JSON.stringify(resolved) + ' for ' + JSON.stringify(identifier)); + }); + + t.end(); + }); +});