From f399cfd2883bd0407c7c6d6e93c623f2d6c319e2 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 19 Mar 2020 01:49:49 +0200 Subject: [PATCH] module: fix no extension searching for exports --- doc/api/esm.md | 2 +- doc/api/modules.md | 38 ++------ lib/internal/modules/cjs/loader.js | 96 +++++-------------- test/es-module/test-esm-exports.mjs | 11 ++- .../node_modules/pkgexports/package.json | 1 + 5 files changed, 46 insertions(+), 102 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 78caf93e74332a..df30f341c6325c 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -1635,7 +1635,7 @@ The resolver can throw the following errors: > 1. If _exports_ contains any index property keys, as defined in ECMA-262 > [6.1.7 Array Index][], throw an _Invalid Package Configuration_ error. > 1. For each property _p_ of _target_, in object insertion order as, -> 1. If _env_ contains an entry for _p_, then +> 1. If _p_ equals _"default"_ or _env_ contains an entry for _p_, then > 1. Let _targetValue_ be the value of the _p_ property in _target_. > 1. Return the result of **PACKAGE_EXPORTS_TARGET_RESOLVE**( > _packageURL_, _targetValue_, _subpath_, _env_), continuing the diff --git a/doc/api/modules.md b/doc/api/modules.md index 9587e2ef0d38e5..5de7c9bbe00b5c 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -165,7 +165,7 @@ require(X) from module at path Y 6. THROW "not found" LOAD_AS_FILE(X) -1. If X is a file, load X as JavaScript text. STOP +1. If X is a file, load X as its file extension format. STOP 2. If X.js is a file, load X.js as JavaScript text. STOP 3. If X.json is a file, parse X.json to a JavaScript Object. STOP 4. If X.node is a file, load X.node as binary addon. STOP @@ -189,8 +189,9 @@ LOAD_AS_DIRECTORY(X) LOAD_NODE_MODULES(X, START) 1. let DIRS = NODE_MODULES_PATHS(START) 2. for each DIR in DIRS: - a. LOAD_AS_FILE(DIR/X) - b. LOAD_AS_DIRECTORY(DIR/X) + a. LOAD_PACKAGE_EXPORTS(DIR, X) + b. LOAD_AS_FILE(DIR/X) + c. LOAD_AS_DIRECTORY(DIR/X) NODE_MODULES_PATHS(START) 1. let PARTS = path split(START) @@ -208,50 +209,29 @@ LOAD_SELF_REFERENCE(X, START) 2. If no scope was found, return. 3. If the `package.json` has no "exports", return. 4. If the name in `package.json` isn't a prefix of X, throw "not found". -5. Otherwise, resolve the remainder of X relative to this package as if it +5. Otherwise, load the remainder of X relative to this package as if it was loaded via `LOAD_NODE_MODULES` with a name in `package.json`. -``` - -Node.js allows packages loaded via -`LOAD_NODE_MODULES` to explicitly declare which file paths to expose and how -they should be interpreted. This expands on the control packages already had -using the `main` field. - -With this feature enabled, the `LOAD_NODE_MODULES` changes are: -```txt -LOAD_NODE_MODULES(X, START) -1. let DIRS = NODE_MODULES_PATHS(START) -2. for each DIR in DIRS: - a. let FILE_PATH = RESOLVE_BARE_SPECIFIER(DIR, X) - b. LOAD_AS_FILE(FILE_PATH) - c. LOAD_AS_DIRECTORY(FILE_PATH) - -RESOLVE_BARE_SPECIFIER(DIR, X) +LOAD_PACKAGE_EXPORTS(DIR, X) 1. Try to interpret X as a combination of name and subpath where the name may have a @scope/ prefix and the subpath begins with a slash (`/`). 2. If X matches this pattern and DIR/name/package.json is a file: a. Parse DIR/name/package.json, and look for "exports" field. - b. If "exports" is null or undefined, GOTO 3. + b. If "exports" is null or undefined, return. c. If "exports" is an object with some keys starting with "." and some keys not starting with ".", throw "invalid config". d. If "exports" is a string, or object with no keys starting with ".", treat it as having that value as its "." object property. - e. If subpath is "." and "exports" does not have a "." entry, GOTO 3. + e. If subpath is "." and "exports" does not have a "." entry, return. f. Find the longest key in "exports" that the subpath starts with. g. If no such key can be found, throw "not found". h. let RESOLVED_URL = PACKAGE_EXPORTS_TARGET_RESOLVE(pathToFileURL(DIR/name), exports[key], subpath.slice(key.length), ["node", "require"]), as defined in the ESM resolver. - i. return fileURLToPath(RESOLVED_URL) -3. return DIR/X + i. Load fileURLToPath(RESOLVED_URL) as its file extension format. STOP ``` -`"exports"` is only honored when loading a package "name" as defined above. Any -`"exports"` values within nested directories and packages must be declared by -the `package.json` responsible for the "name". - ## Caching diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index a73101aa1feaf1..d39ef012d22b94 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -392,52 +392,7 @@ function findLongestRegisteredExtension(filename) { return '.js'; } -function resolveBasePath(basePath, exts, isMain, trailingSlash, request) { - let filename; - - const rc = stat(basePath); - if (!trailingSlash) { - if (rc === 0) { // File. - if (!isMain) { - if (preserveSymlinks) { - filename = path.resolve(basePath); - } else { - filename = toRealPath(basePath); - } - } else if (preserveSymlinksMain) { - // For the main module, we use the preserveSymlinksMain flag instead - // mainly for backward compatibility, as the preserveSymlinks flag - // historically has not applied to the main module. Most likely this - // was intended to keep .bin/ binaries working, as following those - // symlinks is usually required for the imports in the corresponding - // files to resolve; that said, in some use cases following symlinks - // causes bigger problems which is why the preserveSymlinksMain option - // is needed. - filename = path.resolve(basePath); - } else { - filename = toRealPath(basePath); - } - } - - if (!filename) { - // Try it with each of the extensions - if (exts === undefined) - exts = ObjectKeys(Module._extensions); - filename = tryExtensions(basePath, exts, isMain); - } - } - - if (!filename && rc === 1) { // Directory. - // try it with each of the extensions at "index" - if (exts === undefined) - exts = ObjectKeys(Module._extensions); - filename = tryPackage(basePath, exts, isMain, request); - } - - return filename; -} - -function trySelf(parentPath, isMain, request) { +function trySelf(parentPath, request) { const { data: pkg, path: basePath } = readPackageScope(parentPath) || {}; if (!pkg || pkg.exports === undefined) return false; if (typeof pkg.name !== 'string') return false; @@ -451,20 +406,11 @@ function trySelf(parentPath, isMain, request) { return false; } - const exts = ObjectKeys(Module._extensions); const fromExports = applyExports(basePath, expansion); - // Use exports if (fromExports) { - let trailingSlash = request.length > 0 && - request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH; - if (!trailingSlash) { - trailingSlash = /(?:^|\/)\.?\.$/.test(request); - } - return resolveBasePath(fromExports, exts, isMain, trailingSlash, request); - } else { - // Use main field - return tryPackage(basePath, exts, isMain, request); + return tryFile(fromExports, false); } + assert(false); } function isConditionalDotExportSugar(exports, basePath) { @@ -496,7 +442,7 @@ function applyExports(basePath, expansion) { let pkgExports = readPackageExports(basePath); if (pkgExports === undefined || pkgExports === null) - return path.resolve(basePath, mappingKey); + return false; if (isConditionalDotExportSugar(pkgExports, basePath)) pkgExports = { '.': pkgExports }; @@ -532,20 +478,20 @@ function applyExports(basePath, expansion) { // 1. name/.* // 2. @scope/name/.* const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/; -function resolveExports(nmPath, request, absoluteRequest) { +function resolveExports(nmPath, request) { // The implementation's behavior is meant to mirror resolution in ESM. - if (!absoluteRequest) { - const [, name, expansion = ''] = - StringPrototypeMatch(request, EXPORTS_PATTERN) || []; - if (!name) { - return path.resolve(nmPath, request); - } - - const basePath = path.resolve(nmPath, name); - return applyExports(basePath, expansion); + const [, name, expansion = ''] = + StringPrototypeMatch(request, EXPORTS_PATTERN) || []; + if (!name) { + return false; } - return path.resolve(nmPath, request); + const basePath = path.resolve(nmPath, name); + const fromExports = applyExports(basePath, expansion); + if (!fromExports) { + return false; + } + return tryFile(fromExports, false); } function isArrayIndex(p) { @@ -662,7 +608,15 @@ Module._findPath = function(request, paths, isMain) { // Don't search further if path doesn't exist const curPath = paths[i]; if (curPath && stat(curPath) < 1) continue; - const basePath = resolveExports(curPath, request, absoluteRequest); + + if (!absoluteRequest) { + const exportsResolved = resolveExports(curPath, request); + if (exportsResolved) { + return exportsResolved; + } + } + + const basePath = path.resolve(curPath, request); let filename; const rc = stat(basePath); @@ -1005,7 +959,7 @@ Module._resolveFilename = function(request, parent, isMain, options) { } if (parent && parent.filename) { - const filename = trySelf(parent.filename, isMain, request); + const filename = trySelf(parent.filename, request); if (filename) { const cacheKey = request + '\x00' + (paths.length === 1 ? paths[0] : paths.join('\x00')); diff --git a/test/es-module/test-esm-exports.mjs b/test/es-module/test-esm-exports.mjs index 9003c119bc9447..a75ec01a0d2704 100644 --- a/test/es-module/test-esm-exports.mjs +++ b/test/es-module/test-esm-exports.mjs @@ -118,7 +118,7 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; })); } - // Covering out bases - not a file is still not a file after dir mapping. + // Non-existing file loadFixture('pkgexports/sub/not-a-file.js').catch(mustCall((err) => { strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND'); // ESM returns a full file path @@ -127,6 +127,15 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; 'Cannot find module'); })); + // No extension lookups + loadFixture('pkgexports/no-ext').catch(mustCall((err) => { + strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND'); + // ESM returns a full file path + assertStartsWith(err.message, isRequire ? + 'Cannot find module \'pkgexports/no-ext\'' : + 'Cannot find module'); + })); + // The use of %2F escapes in paths fails loading loadFixture('pkgexports/sub/..%2F..%2Fbar.js').catch(mustCall((err) => { strictEqual(err.code, 'ERR_INVALID_FILE_URL_PATH'); diff --git a/test/fixtures/node_modules/pkgexports/package.json b/test/fixtures/node_modules/pkgexports/package.json index 7f417ad5457bfc..1384f8454e844e 100644 --- a/test/fixtures/node_modules/pkgexports/package.json +++ b/test/fixtures/node_modules/pkgexports/package.json @@ -32,6 +32,7 @@ "nomatch": "./nothing.js" } }], + "./no-ext": "./asdf", "./resolve-self": { "require": "./resolve-self.js", "import": "./resolve-self.mjs"