Skip to content

Commit c5af5a4

Browse files
giltayarMoLow
authored andcommitted
module: change default resolver to not throw on unknown scheme
Fixes nodejs/loaders#138 PR-URL: nodejs#47824 Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 6acbb23 commit c5af5a4

File tree

8 files changed

+203
-112
lines changed

8 files changed

+203
-112
lines changed

doc/api/esm.md

+86-57
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,9 @@ There are three types of specifiers:
139139
* _Absolute specifiers_ like `'file:///opt/nodejs/config.js'`. They refer
140140
directly and explicitly to a full path.
141141

142-
Bare specifier resolutions are handled by the [Node.js module resolution
143-
algorithm][]. All other specifier resolutions are always only resolved with
142+
Bare specifier resolutions are handled by the [Node.js module
143+
resolution and loading algorithm][].
144+
All other specifier resolutions are always only resolved with
144145
the standard relative [URL][] resolution semantics.
145146

146147
Like in CommonJS, module files within packages can be accessed by appending a
@@ -1007,28 +1008,6 @@ and there is no security.
10071008
// https-loader.mjs
10081009
import { get } from 'node:https';
10091010
1010-
export function resolve(specifier, context, nextResolve) {
1011-
const { parentURL = null } = context;
1012-
1013-
// Normally Node.js would error on specifiers starting with 'https://', so
1014-
// this hook intercepts them and converts them into absolute URLs to be
1015-
// passed along to the later hooks below.
1016-
if (specifier.startsWith('https://')) {
1017-
return {
1018-
shortCircuit: true,
1019-
url: specifier,
1020-
};
1021-
} else if (parentURL && parentURL.startsWith('https://')) {
1022-
return {
1023-
shortCircuit: true,
1024-
url: new URL(specifier, parentURL).href,
1025-
};
1026-
}
1027-
1028-
// Let Node.js handle all other specifiers.
1029-
return nextResolve(specifier);
1030-
}
1031-
10321011
export function load(url, context, nextLoad) {
10331012
// For JavaScript to be loaded over the network, we need to fetch and
10341013
// return it.
@@ -1069,9 +1048,7 @@ prints the current version of CoffeeScript per the module at the URL in
10691048
#### Transpiler loader
10701049

10711050
Sources that are in formats Node.js doesn't understand can be converted into
1072-
JavaScript using the [`load` hook][load hook]. Before that hook gets called,
1073-
however, a [`resolve` hook][resolve hook] needs to tell Node.js not to
1074-
throw an error on unknown file types.
1051+
JavaScript using the [`load` hook][load hook].
10751052
10761053
This is less performant than transpiling source files before running
10771054
Node.js; a transpiler loader should only be used for development and testing
@@ -1087,25 +1064,6 @@ import CoffeeScript from 'coffeescript';
10871064
10881065
const baseURL = pathToFileURL(`${cwd()}/`).href;
10891066
1090-
// CoffeeScript files end in .coffee, .litcoffee, or .coffee.md.
1091-
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
1092-
1093-
export async function resolve(specifier, context, nextResolve) {
1094-
if (extensionsRegex.test(specifier)) {
1095-
const { parentURL = baseURL } = context;
1096-
1097-
// Node.js normally errors on unknown file extensions, so return a URL for
1098-
// specifiers ending in the CoffeeScript file extensions.
1099-
return {
1100-
shortCircuit: true,
1101-
url: new URL(specifier, parentURL).href,
1102-
};
1103-
}
1104-
1105-
// Let Node.js handle all other specifiers.
1106-
return nextResolve(specifier);
1107-
}
1108-
11091067
export async function load(url, context, nextLoad) {
11101068
if (extensionsRegex.test(url)) {
11111069
// Now that we patched resolve to let CoffeeScript URLs through, we need to
@@ -1198,27 +1156,99 @@ loaded from disk but before Node.js executes it; and so on for any `.coffee`,
11981156
`.litcoffee` or `.coffee.md` files referenced via `import` statements of any
11991157
loaded file.
12001158
1201-
## Resolution algorithm
1159+
#### "import map" loader
1160+
1161+
The previous two loaders defined `load` hooks. This is an example of a loader
1162+
that does its work via the `resolve` hook. This loader reads an
1163+
`import-map.json` file that specifies which specifiers to override to another
1164+
URL (this is a very simplistic implemenation of a small subset of the
1165+
"import maps" specification).
1166+
1167+
```js
1168+
// import-map-loader.js
1169+
import fs from 'node:fs/promises';
1170+
1171+
const { imports } = JSON.parse(await fs.readFile('import-map.json'));
1172+
1173+
export async function resolve(specifier, context, nextResolve) {
1174+
if (Object.hasOwn(imports, specifier)) {
1175+
return nextResolve(imports[specifier], context);
1176+
}
1177+
1178+
return nextResolve(specifier, context);
1179+
}
1180+
```
1181+
1182+
Let's assume we have these files:
1183+
1184+
```js
1185+
// main.js
1186+
import 'a-module';
1187+
```
1188+
1189+
```json
1190+
// import-map.json
1191+
{
1192+
"imports": {
1193+
"a-module": "./some-module.js"
1194+
}
1195+
}
1196+
```
1197+
1198+
```js
1199+
// some-module.js
1200+
console.log('some module!');
1201+
```
1202+
1203+
If you run `node --experimental-loader ./import-map-loader.js main.js`
1204+
the output will be `some module!`.
1205+
1206+
## Resolution and loading algorithm
12021207
12031208
### Features
12041209
1205-
The resolver has the following properties:
1210+
The default resolver has the following properties:
12061211
12071212
* FileURL-based resolution as is used by ES modules
1208-
* Support for builtin module loading
12091213
* Relative and absolute URL resolution
12101214
* No default extensions
12111215
* No folder mains
12121216
* Bare specifier package resolution lookup through node\_modules
1217+
* Does not fail on unknown extensions or protocols
1218+
* Can optionally provide a hint of the format to the loading phase
1219+
1220+
The default loader has the following properties
12131221
1214-
### Resolver algorithm
1222+
* Support for builtin module loading via `node:` URLs
1223+
* Support for "inline" module loading via `data:` URLs
1224+
* Support for `file:` module loading
1225+
* Fails on any other URL protocol
1226+
* Fails on unknown extensions for `file:` loading
1227+
(supports only `.cjs`, `.js`, and `.mjs`)
1228+
1229+
### Resolution algorithm
12151230
12161231
The algorithm to load an ES module specifier is given through the
12171232
**ESM\_RESOLVE** method below. It returns the resolved URL for a
12181233
module specifier relative to a parentURL.
12191234
1235+
The resolution algorithm determines the full resolved URL for a module
1236+
load, along with its suggested module format. The resolution algorithm
1237+
does not determine whether the resolved URL protocol can be loaded,
1238+
or whether the file extensions are permitted, instead these validations
1239+
are applied by Node.js during the load phase
1240+
(for example, if it was asked to load a URL that has a protocol that is
1241+
not `file:`, `data:`, `node:`, or if `--experimental-network-imports`
1242+
is enabled, `https:`).
1243+
1244+
The algorithm also tries to determine the format of the file based
1245+
on the extension (see `ESM_FILE_FORMAT` algorithm below). If it does
1246+
not recognize the file extension (eg if it is not `.mjs`, `.cjs`, or
1247+
`.json`), then a format of `undefined` is returned,
1248+
which will throw during the load phase.
1249+
12201250
The algorithm to determine the module format of a resolved URL is
1221-
provided by **ESM\_FORMAT**, which returns the unique module
1251+
provided by **ESM\_FILE\_FORMAT**, which returns the unique module
12221252
format for any file. The _"module"_ format is returned for an ECMAScript
12231253
Module, while the _"commonjs"_ format is used to indicate loading through the
12241254
legacy CommonJS loader. Additional formats such as _"addon"_ can be extended in
@@ -1245,7 +1275,7 @@ The resolver can throw the following errors:
12451275
* _Unsupported Directory Import_: The resolved path corresponds to a directory,
12461276
which is not a supported target for module imports.
12471277
1248-
### Resolver Algorithm Specification
1278+
### Resolution Algorithm Specification
12491279
12501280
**ESM\_RESOLVE**(_specifier_, _parentURL_)
12511281
@@ -1279,7 +1309,7 @@ The resolver can throw the following errors:
12791309
> 8. Otherwise,
12801310
> 1. Set _format_ the module format of the content type associated with the
12811311
> URL _resolved_.
1282-
> 9. Load _resolved_ as module format, _format_.
1312+
> 9. Return _format_ and _resolved_ to the loading phase
12831313
12841314
**PACKAGE\_RESOLVE**(_packageSpecifier_, _parentURL_)
12851315
@@ -1484,9 +1514,9 @@ _isImports_, _conditions_)
14841514
> 7. If _pjson?.type_ exists and is _"module"_, then
14851515
> 1. If _url_ ends in _".js"_, then
14861516
> 1. Return _"module"_.
1487-
> 2. Throw an _Unsupported File Extension_ error.
1517+
> 2. Return **undefined**.
14881518
> 8. Otherwise,
1489-
> 1. Throw an _Unsupported File Extension_ error.
1519+
> 1. Return **undefined**.
14901520
14911521
**LOOKUP\_PACKAGE\_SCOPE**(_url_)
14921522
@@ -1552,7 +1582,7 @@ success!
15521582
[Import Assertions proposal]: https://github.com/tc39/proposal-import-assertions
15531583
[JSON modules]: #json-modules
15541584
[Loaders API]: #loaders
1555-
[Node.js Module Resolution Algorithm]: #resolver-algorithm-specification
1585+
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
15561586
[Terminology]: #terminology
15571587
[URL]: https://url.spec.whatwg.org/
15581588
[`"exports"`]: packages.md#exports
@@ -1580,7 +1610,6 @@ success!
15801610
[custom https loader]: #https-loader
15811611
[load hook]: #loadurl-context-nextload
15821612
[percent-encoded]: url.md#percent-encoding-in-urls
1583-
[resolve hook]: #resolvespecifier-context-nextresolve
15841613
[special scheme]: https://url.spec.whatwg.org/#special-scheme
15851614
[status code]: process.md#exit-codes
15861615
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules

lib/internal/modules/esm/load.js

+32
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ async function defaultLoad(url, context) {
7777
source,
7878
} = context;
7979

80+
throwIfUnsupportedURLScheme(new URL(url), experimentalNetworkImports);
81+
8082
if (format == null) {
8183
format = await defaultGetFormat(url, context);
8284
}
@@ -100,6 +102,36 @@ async function defaultLoad(url, context) {
100102
};
101103
}
102104

105+
/**
106+
* throws an error if the protocol is not one of the protocols
107+
* that can be loaded in the default loader
108+
* @param {URL} parsed
109+
* @param {boolean} experimentalNetworkImports
110+
*/
111+
function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) {
112+
// Avoid accessing the `protocol` property due to the lazy getters.
113+
const protocol = parsed?.protocol;
114+
if (
115+
protocol &&
116+
protocol !== 'file:' &&
117+
protocol !== 'data:' &&
118+
protocol !== 'node:' &&
119+
(
120+
!experimentalNetworkImports ||
121+
(
122+
protocol !== 'https:' &&
123+
protocol !== 'http:'
124+
)
125+
)
126+
) {
127+
const schemes = ['file', 'data', 'node'];
128+
if (experimentalNetworkImports) {
129+
ArrayPrototypePush(schemes, 'https', 'http');
130+
}
131+
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, schemes);
132+
}
133+
}
134+
103135
module.exports = {
104136
defaultLoad,
105137
};

lib/internal/modules/esm/resolve.js

-36
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
const {
44
ArrayIsArray,
55
ArrayPrototypeJoin,
6-
ArrayPrototypePush,
76
ArrayPrototypeShift,
87
JSONParse,
98
JSONStringify,
@@ -57,7 +56,6 @@ const {
5756
ERR_PACKAGE_PATH_NOT_EXPORTED,
5857
ERR_UNSUPPORTED_DIR_IMPORT,
5958
ERR_NETWORK_IMPORT_DISALLOWED,
60-
ERR_UNSUPPORTED_ESM_URL_SCHEME,
6159
} = require('internal/errors').codes;
6260

6361
const { Module: CJSModule } = require('internal/modules/cjs/loader');
@@ -1036,37 +1034,6 @@ function checkIfDisallowedImport(specifier, parsed, parsedParentURL) {
10361034
}
10371035
}
10381036

1039-
function throwIfUnsupportedURLProtocol(url) {
1040-
// Avoid accessing the `protocol` property due to the lazy getters.
1041-
const protocol = url.protocol;
1042-
if (protocol !== 'file:' && protocol !== 'data:' &&
1043-
protocol !== 'node:') {
1044-
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url);
1045-
}
1046-
}
1047-
1048-
function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) {
1049-
// Avoid accessing the `protocol` property due to the lazy getters.
1050-
const protocol = parsed?.protocol;
1051-
if (
1052-
protocol &&
1053-
protocol !== 'file:' &&
1054-
protocol !== 'data:' &&
1055-
(
1056-
!experimentalNetworkImports ||
1057-
(
1058-
protocol !== 'https:' &&
1059-
protocol !== 'http:'
1060-
)
1061-
)
1062-
) {
1063-
const schemes = ['file', 'data'];
1064-
if (experimentalNetworkImports) {
1065-
ArrayPrototypePush(schemes, 'https', 'http');
1066-
}
1067-
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, schemes);
1068-
}
1069-
}
10701037

10711038
async function defaultResolve(specifier, context = {}) {
10721039
let { parentURL, conditions } = context;
@@ -1142,7 +1109,6 @@ async function defaultResolve(specifier, context = {}) {
11421109
// This must come after checkIfDisallowedImport
11431110
if (parsed && parsed.protocol === 'node:') return { url: specifier };
11441111

1145-
throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports);
11461112

11471113
const isMain = parentURL === undefined;
11481114
if (isMain) {
@@ -1189,8 +1155,6 @@ async function defaultResolve(specifier, context = {}) {
11891155
throw error;
11901156
}
11911157

1192-
throwIfUnsupportedURLProtocol(url);
1193-
11941158
return {
11951159
// Do NOT cast `url` to a string: that will work even when there are real
11961160
// problems, silencing them

test/es-module/test-esm-dynamic-import.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ function expectFsNamespace(result) {
5959
'ERR_UNSUPPORTED_ESM_URL_SCHEME');
6060
if (common.isWindows) {
6161
const msg =
62-
'Only URLs with a scheme in: file and data are supported by the default ' +
62+
'Only URLs with a scheme in: file, data, and node are supported by the default ' +
6363
'ESM loader. On Windows, absolute paths must be valid file:// URLs. ' +
6464
"Received protocol 'c:'";
6565
expectModuleError(import('C:\\example\\foo.mjs'),

test/es-module/test-esm-import-meta-resolve.mjs

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const fixtures = dirname.slice(0, dirname.lastIndexOf('/', dirname.length - 2) +
3131
})
3232
)
3333
);
34+
assert.strictEqual(await import.meta.resolve('http://some-absolute/url'), 'http://some-absolute/url');
35+
assert.strictEqual(await import.meta.resolve('some://weird/protocol'), 'some://weird/protocol');
3436
assert.strictEqual(await import.meta.resolve('baz/', fixtures),
3537
fixtures + 'node_modules/baz/');
3638
})().then(mustCall());

0 commit comments

Comments
 (0)