Skip to content

Commit 5fedf30

Browse files
committed
loader: fix package resolution for edge case
this commit solves a regression introduced with PR-40980. if a resolve call results in a script with .mjs extension the is automatically set to . This avoids the case where an additional in the same directory as the .mjs file would declare the to commonjs PR-URL: nodejs#41218 Refs: nodejs#40980 Refs: yargs/yargs#2068 Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: James M Snell <[email protected]> Backport-PR-URL: nodejs#41752
1 parent 5fe75b0 commit 5fedf30

File tree

4 files changed

+241
-171
lines changed

4 files changed

+241
-171
lines changed

lib/internal/modules/esm/get_format.js

+42-24
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const legacyExtensionFormatMap = {
3232
'.node': 'commonjs'
3333
};
3434

35+
let experimentalSpecifierResolutionWarned = false;
36+
3537
if (experimentalWasmModules)
3638
extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm';
3739

@@ -53,41 +55,57 @@ const protocolHandlers = ObjectAssign(ObjectCreate(null), {
5355

5456
return format;
5557
},
56-
'file:'(parsed, url) {
57-
const ext = extname(parsed.pathname);
58-
let format;
59-
60-
if (ext === '.js') {
61-
format = getPackageType(parsed.href) === 'module' ? 'module' : 'commonjs';
62-
} else {
63-
format = extensionFormatMap[ext];
64-
}
65-
if (!format) {
66-
if (experimentalSpecifierResolution === 'node') {
67-
process.emitWarning(
68-
'The Node.js specifier resolution in ESM is experimental.',
69-
'ExperimentalWarning');
70-
format = legacyExtensionFormatMap[ext];
71-
} else {
72-
throw new ERR_UNKNOWN_FILE_EXTENSION(ext, fileURLToPath(url));
73-
}
74-
}
75-
76-
return format || null;
77-
},
58+
'file:': getFileProtocolModuleFormat,
7859
'node:'() { return 'builtin'; },
7960
});
8061

81-
function defaultGetFormat(url, context) {
62+
function getLegacyExtensionFormat(ext) {
63+
if (
64+
experimentalSpecifierResolution === 'node' &&
65+
!experimentalSpecifierResolutionWarned
66+
) {
67+
process.emitWarning(
68+
'The Node.js specifier resolution in ESM is experimental.',
69+
'ExperimentalWarning');
70+
experimentalSpecifierResolutionWarned = true;
71+
}
72+
return legacyExtensionFormatMap[ext];
73+
}
74+
75+
function getFileProtocolModuleFormat(url, ignoreErrors) {
76+
const ext = extname(url.pathname);
77+
if (ext === '.js') {
78+
return getPackageType(url) === 'module' ? 'module' : 'commonjs';
79+
}
80+
81+
const format = extensionFormatMap[ext];
82+
if (format) return format;
83+
if (experimentalSpecifierResolution !== 'node') {
84+
// Explicit undefined return indicates load hook should rerun format check
85+
if (ignoreErrors)
86+
return undefined;
87+
throw new ERR_UNKNOWN_FILE_EXTENSION(ext, fileURLToPath(url));
88+
}
89+
return getLegacyExtensionFormat(ext) ?? null;
90+
}
91+
92+
function defaultGetFormatWithoutErrors(url, context) {
8293
const parsed = new URL(url);
94+
if (!ObjectPrototypeHasOwnProperty(protocolHandlers, parsed.protocol))
95+
return null;
96+
return protocolHandlers[parsed.protocol](parsed, true);
97+
}
8398

99+
function defaultGetFormat(url, context) {
100+
const parsed = new URL(url);
84101
return ObjectPrototypeHasOwnProperty(protocolHandlers, parsed.protocol) ?
85-
protocolHandlers[parsed.protocol](parsed, url) :
102+
protocolHandlers[parsed.protocol](parsed, false) :
86103
null;
87104
}
88105

89106
module.exports = {
90107
defaultGetFormat,
108+
defaultGetFormatWithoutErrors,
91109
extensionFormatMap,
92110
legacyExtensionFormatMap,
93111
};

lib/internal/modules/esm/load.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22

33
const { defaultGetFormat } = require('internal/modules/esm/get_format');
44
const { defaultGetSource } = require('internal/modules/esm/get_source');
5-
const { translators } = require('internal/modules/esm/translators');
65

76
async function defaultLoad(url, context) {
87
let {
98
format,
109
source,
1110
} = context;
1211

13-
if (!translators.has(format)) format = defaultGetFormat(url);
12+
if (format == null) {
13+
format = defaultGetFormat(url);
14+
}
1415

1516
if (
1617
format === 'builtin' ||

lib/internal/modules/esm/resolve.js

+48-63
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ function emitTrailingSlashPatternDeprecation(match, pjsonUrl, isExports, base) {
130130
* @returns
131131
*/
132132
function emitLegacyIndexDeprecation(url, packageJSONUrl, base, main) {
133-
const format = defaultGetFormat(url);
133+
const format = defaultGetFormatWithoutErrors(url);
134134
if (format !== 'module')
135135
return;
136136
const path = fileURLToPath(url);
@@ -474,22 +474,6 @@ const patternRegEx = /\*/g;
474474
function resolvePackageTargetString(
475475
target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) {
476476

477-
const composeResult = (resolved) => {
478-
let format;
479-
try {
480-
format = getPackageType(resolved);
481-
} catch (err) {
482-
if (err.code === 'ERR_INVALID_FILE_URL_PATH') {
483-
const invalidModuleErr = new ERR_INVALID_MODULE_SPECIFIER(
484-
resolved, 'must not include encoded "/" or "\\" characters', base);
485-
invalidModuleErr.cause = err;
486-
throw invalidModuleErr;
487-
}
488-
throw err;
489-
}
490-
return { resolved, ...(format !== 'none') && { format } };
491-
};
492-
493477
if (subpath !== '' && !pattern && target[target.length - 1] !== '/')
494478
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
495479

@@ -522,18 +506,22 @@ function resolvePackageTargetString(
522506
if (!StringPrototypeStartsWith(resolvedPath, packagePath))
523507
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
524508

525-
if (subpath === '') return composeResult(resolved);
509+
if (subpath === '') return resolved;
526510

527511
if (RegExpPrototypeTest(invalidSegmentRegEx, subpath))
528512
throwInvalidSubpath(match + subpath, packageJSONUrl, internal, base);
529513

530514
if (pattern) {
531-
return composeResult(new URL(RegExpPrototypeSymbolReplace(patternRegEx,
532-
resolved.href,
533-
() => subpath)));
515+
return new URL(
516+
RegExpPrototypeSymbolReplace(
517+
patternRegEx,
518+
resolved.href,
519+
() => subpath
520+
)
521+
);
534522
}
535523

536-
return composeResult(new URL(subpath, resolved));
524+
return new URL(subpath, resolved);
537525
}
538526

539527
/**
@@ -659,15 +647,15 @@ function packageExportsResolve(
659647
!StringPrototypeIncludes(packageSubpath, '*') &&
660648
!StringPrototypeEndsWith(packageSubpath, '/')) {
661649
const target = exports[packageSubpath];
662-
const resolveResult = resolvePackageTarget(
650+
const resolved = resolvePackageTarget(
663651
packageJSONUrl, target, '', packageSubpath, base, false, false, conditions
664652
);
665653

666-
if (resolveResult == null) {
654+
if (resolved == null) {
667655
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
668656
}
669657

670-
return { ...resolveResult, exact: true };
658+
return { resolved, exact: true };
671659
}
672660

673661
let bestMatch = '';
@@ -703,7 +691,7 @@ function packageExportsResolve(
703691
if (bestMatch) {
704692
const target = exports[bestMatch];
705693
const pattern = StringPrototypeIncludes(bestMatch, '*');
706-
const resolveResult = resolvePackageTarget(
694+
const resolved = resolvePackageTarget(
707695
packageJSONUrl,
708696
target,
709697
bestMatchSubpath,
@@ -713,15 +701,15 @@ function packageExportsResolve(
713701
false,
714702
conditions);
715703

716-
if (resolveResult == null) {
704+
if (resolved == null) {
717705
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
718706
}
719707

720708
if (!pattern) {
721709
emitFolderMapDeprecation(bestMatch, packageJSONUrl, true, base);
722710
}
723711

724-
return { ...resolveResult, exact: pattern };
712+
return { resolved, exact: pattern };
725713
}
726714

727715
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
@@ -761,11 +749,11 @@ function packageImportsResolve(name, base, conditions) {
761749
if (ObjectPrototypeHasOwnProperty(imports, name) &&
762750
!StringPrototypeIncludes(name, '*') &&
763751
!StringPrototypeEndsWith(name, '/')) {
764-
const resolveResult = resolvePackageTarget(
752+
const resolved = resolvePackageTarget(
765753
packageJSONUrl, imports[name], '', name, base, false, true, conditions
766754
);
767-
if (resolveResult != null) {
768-
return { resolved: resolveResult.resolved, exact: true };
755+
if (resolved != null) {
756+
return { resolved, exact: true };
769757
}
770758
} else {
771759
let bestMatch = '';
@@ -798,15 +786,15 @@ function packageImportsResolve(name, base, conditions) {
798786
if (bestMatch) {
799787
const target = imports[bestMatch];
800788
const pattern = StringPrototypeIncludes(bestMatch, '*');
801-
const resolveResult = resolvePackageTarget(
789+
const resolved = resolvePackageTarget(
802790
packageJSONUrl, target,
803791
bestMatchSubpath, bestMatch,
804792
base, pattern, true,
805793
conditions);
806-
if (resolveResult !== null) {
794+
if (resolved !== null) {
807795
if (!pattern)
808796
emitFolderMapDeprecation(bestMatch, packageJSONUrl, false, base);
809-
return { resolved: resolveResult.resolved, exact: pattern };
797+
return { resolved, exact: pattern };
810798
}
811799
}
812800
}
@@ -883,7 +871,8 @@ function packageResolve(specifier, base, conditions) {
883871
if (packageConfig.name === packageName &&
884872
packageConfig.exports !== undefined && packageConfig.exports !== null) {
885873
return packageExportsResolve(
886-
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
874+
packageJSONUrl, packageSubpath, packageConfig, base, conditions
875+
).resolved;
887876
}
888877
}
889878

@@ -907,24 +896,19 @@ function packageResolve(specifier, base, conditions) {
907896
const packageConfig = getPackageConfig(packageJSONPath, specifier, base);
908897
if (packageConfig.exports !== undefined && packageConfig.exports !== null) {
909898
return packageExportsResolve(
910-
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
899+
packageJSONUrl, packageSubpath, packageConfig, base, conditions
900+
).resolved;
911901
}
912902

913903
if (packageSubpath === '.') {
914-
return {
915-
resolved: legacyMainResolve(
916-
packageJSONUrl,
917-
packageConfig,
918-
base),
919-
...(packageConfig.type !== 'none') && { format: packageConfig.type }
920-
};
904+
return legacyMainResolve(
905+
packageJSONUrl,
906+
packageConfig,
907+
base
908+
);
921909
}
922910

923-
return {
924-
resolved: new URL(packageSubpath, packageJSONUrl),
925-
...(packageConfig.type !== 'none') && { format: packageConfig.type }
926-
};
927-
911+
return new URL(packageSubpath, packageJSONUrl);
928912
// Cross-platform root check.
929913
} while (packageJSONPath.length !== lastPath.length);
930914

@@ -967,7 +951,6 @@ function moduleResolve(specifier, base, conditions) {
967951
// Order swapped from spec for minor perf gain.
968952
// Ok since relative URLs cannot parse as URLs.
969953
let resolved;
970-
let format;
971954
if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
972955
resolved = new URL(specifier, base);
973956
} else if (specifier[0] === '#') {
@@ -976,13 +959,10 @@ function moduleResolve(specifier, base, conditions) {
976959
try {
977960
resolved = new URL(specifier);
978961
} catch {
979-
({ resolved, format } = packageResolve(specifier, base, conditions));
962+
resolved = packageResolve(specifier, base, conditions);
980963
}
981964
}
982-
return {
983-
url: finalizeResolution(resolved, base),
984-
...(format != null) && { format }
985-
};
965+
return finalizeResolution(resolved, base);
986966
}
987967

988968
/**
@@ -1031,6 +1011,13 @@ function resolveAsCommonJS(specifier, parentURL) {
10311011
}
10321012
}
10331013

1014+
function throwIfUnsupportedURLProtocol(url) {
1015+
if (url.protocol !== 'file:' && url.protocol !== 'data:' &&
1016+
url.protocol !== 'node:') {
1017+
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url);
1018+
}
1019+
}
1020+
10341021
function defaultResolve(specifier, context = {}, defaultResolveUnused) {
10351022
let { parentURL, conditions } = context;
10361023
if (parentURL && policy?.manifest) {
@@ -1093,14 +1080,8 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
10931080

10941081
conditions = getConditionsSet(conditions);
10951082
let url;
1096-
let format;
10971083
try {
1098-
({ url, format } =
1099-
moduleResolve(
1100-
specifier,
1101-
parentURL,
1102-
conditions
1103-
));
1084+
url = moduleResolve(specifier, parentURL, conditions);
11041085
} catch (error) {
11051086
// Try to give the user a hint of what would have been the
11061087
// resolved CommonJS module
@@ -1136,9 +1117,11 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
11361117
url.hash = old.hash;
11371118
}
11381119

1120+
throwIfUnsupportedURLProtocol(url);
1121+
11391122
return {
11401123
url: `${url}`,
1141-
...(format != null) && { format }
1124+
format: defaultGetFormatWithoutErrors(url),
11421125
};
11431126
}
11441127

@@ -1153,4 +1136,6 @@ module.exports = {
11531136
};
11541137

11551138
// cycle
1156-
const { defaultGetFormat } = require('internal/modules/esm/get_format');
1139+
const {
1140+
defaultGetFormatWithoutErrors,
1141+
} = require('internal/modules/esm/get_format');

0 commit comments

Comments
 (0)