module: path-only CJS exports extension searching · nodejs/node@b2e7700 · GitHub
Skip to content

Commit b2e7700

Browse files
guybedfordBethGriggs
authored andcommitted
module: path-only CJS exports extension searching
PR-URL: #32351 Reviewed-By: Geoffrey Booth <webmaster@geoffreybooth.com> Reviewed-By: Bradley Farias <bradley.meck@gmail.com> Reviewed-By: Jan Krems <jan.krems@gmail.com> Reviewed-By: Myles Borins <myles.borins@gmail.com>
1 parent 75ee5b2 commit b2e7700

9 files changed

Lines changed: 122 additions & 129 deletions

File tree

doc/api/esm.md

Lines changed: 1 addition & 1 deletion

doc/api/modules.md

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ require(X) from module at path Y
165165
6. THROW "not found"
166166
167167
LOAD_AS_FILE(X)
168-
1. If X is a file, load X as JavaScript text. STOP
168+
1. If X is a file, load X as its file extension format. STOP
169169
2. If X.js is a file, load X.js as JavaScript text. STOP
170170
3. If X.json is a file, parse X.json to a JavaScript Object. STOP
171171
4. If X.node is a file, load X.node as binary addon. STOP
@@ -189,8 +189,9 @@ LOAD_AS_DIRECTORY(X)
189189
LOAD_NODE_MODULES(X, START)
190190
1. let DIRS = NODE_MODULES_PATHS(START)
191191
2. for each DIR in DIRS:
192-
a. LOAD_AS_FILE(DIR/X)
193-
b. LOAD_AS_DIRECTORY(DIR/X)
192+
a. LOAD_PACKAGE_EXPORTS(DIR, X)
193+
b. LOAD_AS_FILE(DIR/X)
194+
c. LOAD_AS_DIRECTORY(DIR/X)
194195
195196
NODE_MODULES_PATHS(START)
196197
1. let PARTS = path split(START)
@@ -208,50 +209,35 @@ LOAD_SELF_REFERENCE(X, START)
208209
2. If no scope was found, return.
209210
3. If the `package.json` has no "exports", return.
210211
4. If the name in `package.json` isn't a prefix of X, throw "not found".
211-
5. Otherwise, resolve the remainder of X relative to this package as if it
212-
was loaded via `LOAD_NODE_MODULES` with a name in `package.json`.
213-
```
214-
215-
Node.js allows packages loaded via
216-
`LOAD_NODE_MODULES` to explicitly declare which file paths to expose and how
217-
they should be interpreted. This expands on the control packages already had
218-
using the `main` field.
219-
220-
With this feature enabled, the `LOAD_NODE_MODULES` changes are:
212+
5. Otherwise, load the remainder of X relative to this package as if it
213+
was loaded via `LOAD_NODE_MODULES` with a name in `package.json`.
221214
222-
```txt
223-
LOAD_NODE_MODULES(X, START)
224-
1. let DIRS = NODE_MODULES_PATHS(START)
225-
2. for each DIR in DIRS:
226-
a. let FILE_PATH = RESOLVE_BARE_SPECIFIER(DIR, X)
227-
b. LOAD_AS_FILE(FILE_PATH)
228-
c. LOAD_AS_DIRECTORY(FILE_PATH)
229-
230-
RESOLVE_BARE_SPECIFIER(DIR, X)
215+
LOAD_PACKAGE_EXPORTS(DIR, X)
231216
1. Try to interpret X as a combination of name and subpath where the name
232217
may have a @scope/ prefix and the subpath begins with a slash (`/`).
233-
2. If X matches this pattern and DIR/name/package.json is a file:
234-
a. Parse DIR/name/package.json, and look for "exports" field.
235-
b. If "exports" is null or undefined, GOTO 3.
236-
c. If "exports" is an object with some keys starting with "." and some keys
237-
not starting with ".", throw "invalid config".
238-
d. If "exports" is a string, or object with no keys starting with ".", treat
239-
it as having that value as its "." object property.
240-
e. If subpath is "." and "exports" does not have a "." entry, GOTO 3.
241-
f. Find the longest key in "exports" that the subpath starts with.
242-
g. If no such key can be found, throw "not found".
243-
h. let RESOLVED_URL =
244-
PACKAGE_EXPORTS_TARGET_RESOLVE(pathToFileURL(DIR/name), exports[key],
245-
subpath.slice(key.length), ["node", "require"]), as defined in the ESM
246-
resolver.
247-
i. return fileURLToPath(RESOLVED_URL)
248-
3. return DIR/X
218+
2. If X does not match this pattern or DIR/name/package.json is not a file,
219+
return.
220+
3. Parse DIR/name/package.json, and look for "exports" field.
221+
4. If "exports" is null or undefined, return.
222+
5. If "exports" is an object with some keys starting with "." and some keys
223+
not starting with ".", throw "invalid config".
224+
6. If "exports" is a string, or object with no keys starting with ".", treat
225+
it as having that value as its "." object property.
226+
7. If subpath is "." and "exports" does not have a "." entry, return.
227+
8. Find the longest key in "exports" that the subpath starts with.
228+
9. If no such key can be found, throw "not found".
229+
10. let RESOLVED =
230+
fileURLToPath(PACKAGE_EXPORTS_TARGET_RESOLVE(pathToFileURL(DIR/name),
231+
exports[key], subpath.slice(key.length), ["node", "require"])), as defined
232+
in the ESM resolver.
233+
11. If key ends with "/":
234+
a. LOAD_AS_FILE(RESOLVED)
235+
b. LOAD_AS_DIRECTORY(RESOLVED)
236+
12. Otherwise
237+
a. If RESOLVED is a file, load it as its file extension format. STOP
238+
13. Throw "not found"
249239
```
250240

251-
`"exports"` is only honored when loading a package "name" as defined above. Any
252-
`"exports"` values within nested directories and packages must be declared by
253-
the `package.json` responsible for the "name".
254-
255241
## Caching
256242

257243
<!--type=misc-->

lib/internal/modules/cjs/loader.js

Lines changed: 53 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const {
3737
ObjectPrototypeHasOwnProperty,
3838
ObjectSetPrototypeOf,
3939
ReflectSet,
40+
RegExpPrototypeTest,
4041
SafeMap,
4142
String,
4243
StringPrototypeIndexOf,
@@ -125,7 +126,7 @@ function enrichCJSError(err) {
125126
after a comment block and/or after a variable definition.
126127
*/
127128
if (err.message.startsWith('Unexpected token \'export\'') ||
128-
(/^\s*import(?=[ {'"*])\s*(?![ (])/).test(lineWithErr)) {
129+
(RegExpPrototypeTest(/^\s*import(?=[ {'"*])\s*(?![ (])/, lineWithErr))) {
129130
// Emit the warning synchronously because we are in the middle of handling
130131
// a SyntaxError that will throw and likely terminate the process before an
131132
// asynchronous warning would be emitted.
@@ -352,10 +353,11 @@ const realpathCache = new Map();
352353
// absolute realpath.
353354
function tryFile(requestPath, isMain) {
354355
const rc = stat(requestPath);
356+
if (rc !== 0) return;
355357
if (preserveSymlinks && !isMain) {
356-
return rc === 0 && path.resolve(requestPath);
358+
return path.resolve(requestPath);
357359
}
358-
return rc === 0 && toRealPath(requestPath);
360+
return toRealPath(requestPath);
359361
}
360362

361363
function toRealPath(requestPath) {
@@ -392,52 +394,7 @@ function findLongestRegisteredExtension(filename) {
392394
return '.js';
393395
}
394396

395-
function resolveBasePath(basePath, exts, isMain, trailingSlash, request) {
396-
let filename;
397-
398-
const rc = stat(basePath);
399-
if (!trailingSlash) {
400-
if (rc === 0) { // File.
401-
if (!isMain) {
402-
if (preserveSymlinks) {
403-
filename = path.resolve(basePath);
404-
} else {
405-
filename = toRealPath(basePath);
406-
}
407-
} else if (preserveSymlinksMain) {
408-
// For the main module, we use the preserveSymlinksMain flag instead
409-
// mainly for backward compatibility, as the preserveSymlinks flag
410-
// historically has not applied to the main module. Most likely this
411-
// was intended to keep .bin/ binaries working, as following those
412-
// symlinks is usually required for the imports in the corresponding
413-
// files to resolve; that said, in some use cases following symlinks
414-
// causes bigger problems which is why the preserveSymlinksMain option
415-
// is needed.
416-
filename = path.resolve(basePath);
417-
} else {
418-
filename = toRealPath(basePath);
419-
}
420-
}
421-
422-
if (!filename) {
423-
// Try it with each of the extensions
424-
if (exts === undefined)
425-
exts = ObjectKeys(Module._extensions);
426-
filename = tryExtensions(basePath, exts, isMain);
427-
}
428-
}
429-
430-
if (!filename && rc === 1) { // Directory.
431-
// try it with each of the extensions at "index"
432-
if (exts === undefined)
433-
exts = ObjectKeys(Module._extensions);
434-
filename = tryPackage(basePath, exts, isMain, request);
435-
}
436-
437-
return filename;
438-
}
439-
440-
function trySelf(parentPath, isMain, request) {
397+
function trySelf(parentPath, request) {
441398
const { data: pkg, path: basePath } = readPackageScope(parentPath) || {};
442399
if (!pkg || pkg.exports === undefined) return false;
443400
if (typeof pkg.name !== 'string') return false;
@@ -451,20 +408,11 @@ function trySelf(parentPath, isMain, request) {
451408
return false;
452409
}
453410

454-
const exts = ObjectKeys(Module._extensions);
455411
const fromExports = applyExports(basePath, expansion);
456-
// Use exports
457412
if (fromExports) {
458-
let trailingSlash = request.length > 0 &&
459-
request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH;
460-
if (!trailingSlash) {
461-
trailingSlash = /(?:^|\/)\.?\.$/.test(request);
462-
}
463-
return resolveBasePath(fromExports, exts, isMain, trailingSlash, request);
464-
} else {
465-
// Use main field
466-
return tryPackage(basePath, exts, isMain, request);
413+
return tryFile(fromExports, false);
467414
}
415+
assert(fromExports !== false);
468416
}
469417

470418
function isConditionalDotExportSugar(exports, basePath) {
@@ -496,7 +444,7 @@ function applyExports(basePath, expansion) {
496444

497445
let pkgExports = readPackageExports(basePath);
498446
if (pkgExports === undefined || pkgExports === null)
499-
return path.resolve(basePath, mappingKey);
447+
return false;
500448

501449
if (isConditionalDotExportSugar(pkgExports, basePath))
502450
pkgExports = { '.': pkgExports };
@@ -520,8 +468,24 @@ function applyExports(basePath, expansion) {
520468
if (dirMatch !== '') {
521469
const mapping = pkgExports[dirMatch];
522470
const subpath = StringPrototypeSlice(mappingKey, dirMatch.length);
523-
return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping,
524-
subpath, mappingKey);
471+
const resolved = resolveExportsTarget(pathToFileURL(basePath + '/'),
472+
mapping, subpath, mappingKey);
473+
// Extension searching for folder exports only
474+
const rc = stat(resolved);
475+
if (rc === 0) return resolved;
476+
if (!(RegExpPrototypeTest(trailingSlashRegex, resolved))) {
477+
const exts = ObjectKeys(Module._extensions);
478+
const filename = tryExtensions(resolved, exts, false);
479+
if (filename) return filename;
480+
}
481+
if (rc === 1) {
482+
const exts = ObjectKeys(Module._extensions);
483+
const filename = tryPackage(resolved, exts, false,
484+
basePath + expansion);
485+
if (filename) return filename;
486+
}
487+
// Undefined means not found
488+
return;
525489
}
526490
}
527491

@@ -532,20 +496,20 @@ function applyExports(basePath, expansion) {
532496
// 1. name/.*
533497
// 2. @scope/name/.*
534498
const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/;
535-
function resolveExports(nmPath, request, absoluteRequest) {
499+
function resolveExports(nmPath, request) {
536500
// The implementation's behavior is meant to mirror resolution in ESM.
537-
if (!absoluteRequest) {
538-
const [, name, expansion = ''] =
539-
StringPrototypeMatch(request, EXPORTS_PATTERN) || [];
540-
if (!name) {
541-
return path.resolve(nmPath, request);
542-
}
543-
544-
const basePath = path.resolve(nmPath, name);
545-
return applyExports(basePath, expansion);
501+
const [, name, expansion = ''] =
502+
StringPrototypeMatch(request, EXPORTS_PATTERN) || [];
503+
if (!name) {
504+
return false;
546505
}
547506

548-
return path.resolve(nmPath, request);
507+
const basePath = path.resolve(nmPath, name);
508+
const fromExports = applyExports(basePath, expansion);
509+
if (fromExports) {
510+
return tryFile(fromExports, false);
511+
}
512+
return fromExports;
549513
}
550514

551515
function isArrayIndex(p) {
@@ -636,6 +600,7 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) {
636600
StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey, subpath, target);
637601
}
638602

603+
const trailingSlashRegex = /(?:^|\/)\.?\.$/;
639604
Module._findPath = function(request, paths, isMain) {
640605
const absoluteRequest = path.isAbsolute(request);
641606
if (absoluteRequest) {
@@ -654,15 +619,26 @@ Module._findPath = function(request, paths, isMain) {
654619
let trailingSlash = request.length > 0 &&
655620
request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH;
656621
if (!trailingSlash) {
657-
trailingSlash = /(?:^|\/)\.?\.$/.test(request);
622+
trailingSlash = RegExpPrototypeTest(trailingSlashRegex, request);
658623
}
659624

660625
// For each path
661626
for (let i = 0; i < paths.length; i++) {
662627
// Don't search further if path doesn't exist
663628
const curPath = paths[i];
664629
if (curPath && stat(curPath) < 1) continue;
665-
const basePath = resolveExports(curPath, request, absoluteRequest);
630+
631+
if (!absoluteRequest) {
632+
const exportsResolved = resolveExports(curPath, request);
633+
// Undefined means not found, false means no exports
634+
if (exportsResolved === undefined)
635+
break;
636+
if (exportsResolved) {
637+
return exportsResolved;
638+
}
639+
}
640+
641+
const basePath = path.resolve(curPath, request);
666642
let filename;
667643

668644
const rc = stat(basePath);
@@ -1005,7 +981,7 @@ Module._resolveFilename = function(request, parent, isMain, options) {
1005981
}
1006982

1007983
if (parent && parent.filename) {
1008-
const filename = trySelf(parent.filename, isMain, request);
984+
const filename = trySelf(parent.filename, request);
1009985
if (filename) {
1010986
const cacheKey = request + '\x00' +
1011987
(paths.length === 1 ? paths[0] : paths.join('\x00'));

test/es-module/test-esm-exports.mjs

Lines changed: 30 additions & 8 deletions

test/fixtures/node_modules/pkgexports/package.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/node_modules/pkgexports/subpath/dir1/dir1.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)