Skip to content

Commit ce967de

Browse files
committed
module: CJS extension searching for folder exports
1 parent f399cfd commit ce967de

File tree

8 files changed

+92
-43
lines changed

8 files changed

+92
-43
lines changed

doc/api/modules.md

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -210,26 +210,32 @@ LOAD_SELF_REFERENCE(X, START)
210210
3. If the `package.json` has no "exports", return.
211211
4. If the name in `package.json` isn't a prefix of X, throw "not found".
212212
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`.
213+
was loaded via `LOAD_NODE_MODULES` with a name in `package.json`.
214214
215215
LOAD_PACKAGE_EXPORTS(DIR, X)
216216
1. Try to interpret X as a combination of name and subpath where the name
217217
may have a @scope/ prefix and the subpath begins with a slash (`/`).
218-
2. If X matches this pattern and DIR/name/package.json is a file:
219-
a. Parse DIR/name/package.json, and look for "exports" field.
220-
b. If "exports" is null or undefined, return.
221-
c. If "exports" is an object with some keys starting with "." and some keys
222-
not starting with ".", throw "invalid config".
223-
d. If "exports" is a string, or object with no keys starting with ".", treat
224-
it as having that value as its "." object property.
225-
e. If subpath is "." and "exports" does not have a "." entry, return.
226-
f. Find the longest key in "exports" that the subpath starts with.
227-
g. If no such key can be found, throw "not found".
228-
h. let RESOLVED_URL =
229-
PACKAGE_EXPORTS_TARGET_RESOLVE(pathToFileURL(DIR/name), exports[key],
230-
subpath.slice(key.length), ["node", "require"]), as defined in the ESM
231-
resolver.
232-
i. Load fileURLToPath(RESOLVED_URL) as its file extension format. STOP
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"
233239
```
234240

235241
## Caching

lib/internal/modules/cjs/loader.js

Lines changed: 32 additions & 10 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) {
@@ -410,7 +412,7 @@ function trySelf(parentPath, request) {
410412
if (fromExports) {
411413
return tryFile(fromExports, false);
412414
}
413-
assert(false);
415+
assert(fromExports !== false);
414416
}
415417

416418
function isConditionalDotExportSugar(exports, basePath) {
@@ -466,8 +468,24 @@ function applyExports(basePath, expansion) {
466468
if (dirMatch !== '') {
467469
const mapping = pkgExports[dirMatch];
468470
const subpath = StringPrototypeSlice(mappingKey, dirMatch.length);
469-
return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping,
470-
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;
471489
}
472490
}
473491

@@ -488,10 +506,10 @@ function resolveExports(nmPath, request) {
488506

489507
const basePath = path.resolve(nmPath, name);
490508
const fromExports = applyExports(basePath, expansion);
491-
if (!fromExports) {
492-
return false;
509+
if (fromExports) {
510+
return tryFile(fromExports, false);
493511
}
494-
return tryFile(fromExports, false);
512+
return fromExports;
495513
}
496514

497515
function isArrayIndex(p) {
@@ -582,6 +600,7 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) {
582600
StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey, subpath, target);
583601
}
584602

603+
const trailingSlashRegex = /(?:^|\/)\.?\.$/;
585604
Module._findPath = function(request, paths, isMain) {
586605
const absoluteRequest = path.isAbsolute(request);
587606
if (absoluteRequest) {
@@ -600,7 +619,7 @@ Module._findPath = function(request, paths, isMain) {
600619
let trailingSlash = request.length > 0 &&
601620
request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH;
602621
if (!trailingSlash) {
603-
trailingSlash = /(?:^|\/)\.?\.$/.test(request);
622+
trailingSlash = RegExpPrototypeTest(trailingSlashRegex, request);
604623
}
605624

606625
// For each path
@@ -611,6 +630,9 @@ Module._findPath = function(request, paths, isMain) {
611630

612631
if (!absoluteRequest) {
613632
const exportsResolved = resolveExports(curPath, request);
633+
// Undefined means not found, false means no exports
634+
if (exportsResolved === undefined)
635+
break;
614636
if (exportsResolved) {
615637
return exportsResolved;
616638
}

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

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
3535
['pkgexports-sugar', { default: 'main' }],
3636
]);
3737

38+
if (isRequire) {
39+
validSpecifiers.set('pkgexports/subpath/file', { default: 'file' });
40+
validSpecifiers.set('pkgexports/subpath/dir1', { default: 'main' });
41+
validSpecifiers.set('pkgexports/subpath/dir1/', { default: 'main' });
42+
validSpecifiers.set('pkgexports/subpath/dir2', { default: 'index' });
43+
validSpecifiers.set('pkgexports/subpath/dir2/', { default: 'index' });
44+
}
45+
3846
for (const [validSpecifier, expected] of validSpecifiers) {
3947
if (validSpecifier === null) continue;
4048

@@ -118,23 +126,28 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
118126
}));
119127
}
120128

121-
// Non-existing file
122-
loadFixture('pkgexports/sub/not-a-file.js').catch(mustCall((err) => {
123-
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
124-
// ESM returns a full file path
125-
assertStartsWith(err.message, isRequire ?
126-
'Cannot find module \'pkgexports/sub/not-a-file.js\'' :
127-
'Cannot find module');
128-
}));
129+
const notFoundExports = new Map([
130+
// Non-existing file
131+
['pkgexports/sub/not-a-file.js', 'pkgexports/sub/not-a-file.js'],
132+
// No extension lookups
133+
['pkgexports/no-ext', 'pkgexports/no-ext'],
134+
]);
129135

130-
// No extension lookups
131-
loadFixture('pkgexports/no-ext').catch(mustCall((err) => {
132-
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
133-
// ESM returns a full file path
134-
assertStartsWith(err.message, isRequire ?
135-
'Cannot find module \'pkgexports/no-ext\'' :
136-
'Cannot find module');
137-
}));
136+
if (!isRequire) {
137+
notFoundExports.set('pkgexports/subpath/file', 'pkgexports/subpath/file');
138+
notFoundExports.set('pkgexports/subpath/dir1', 'pkgexports/subpath/dir1');
139+
notFoundExports.set('pkgexports/subpath/dir2', 'pkgexports/subpath/dir2');
140+
}
141+
142+
for (const [specifier, request] of notFoundExports) {
143+
loadFixture(specifier).catch(mustCall((err) => {
144+
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
145+
// ESM returns a full file path
146+
assertStartsWith(err.message, isRequire ?
147+
`Cannot find module '${request}'` :
148+
'Cannot find module');
149+
}));
150+
}
138151

139152
// The use of %2F escapes in paths fails loading
140153
loadFixture('pkgexports/sub/..%2F..%2Fbar.js').catch(mustCall((err) => {

test/fixtures/node_modules/pkgexports/package.json

Lines changed: 2 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.

test/fixtures/node_modules/pkgexports/subpath/dir1/package.json

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

test/fixtures/node_modules/pkgexports/subpath/dir2/index.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.

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

Lines changed: 2 additions & 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)