Skip to content

Commit

Permalink
module: fix no extension searching for exports
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford committed Mar 25, 2020
1 parent 2fa74e3 commit f399cfd
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 102 deletions.
2 changes: 1 addition & 1 deletion doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 9 additions & 29 deletions doc/api/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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

<!--type=misc-->
Expand Down
96 changes: 25 additions & 71 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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'));
Expand Down
11 changes: 10 additions & 1 deletion test/es-module/test-esm-exports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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');
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/node_modules/pkgexports/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f399cfd

Please sign in to comment.