Skip to content

Commit ccada8b

Browse files
authored
module: change default resolver to not throw on unknown scheme
Fixes nodejs/loaders#138 PR-URL: #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 adcbfce commit ccada8b

File tree

8 files changed

+202
-112
lines changed

8 files changed

+202
-112
lines changed

doc/api/esm.md

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

146-
Bare specifier resolutions are handled by the [Node.js module resolution
147-
algorithm][]. All other specifier resolutions are always only resolved with
146+
Bare specifier resolutions are handled by the [Node.js module
147+
resolution and loading algorithm][].
148+
All other specifier resolutions are always only resolved with
148149
the standard relative [URL][] resolution semantics.
149150

150151
Like in CommonJS, module files within packages can be accessed by appending a
@@ -1029,28 +1030,6 @@ and there is no security.
10291030
// https-loader.mjs
10301031
import { get } from 'node:https';
10311032
1032-
export function resolve(specifier, context, nextResolve) {
1033-
const { parentURL = null } = context;
1034-
1035-
// Normally Node.js would error on specifiers starting with 'https://', so
1036-
// this hook intercepts them and converts them into absolute URLs to be
1037-
// passed along to the later hooks below.
1038-
if (specifier.startsWith('https://')) {
1039-
return {
1040-
shortCircuit: true,
1041-
url: specifier,
1042-
};
1043-
} else if (parentURL && parentURL.startsWith('https://')) {
1044-
return {
1045-
shortCircuit: true,
1046-
url: new URL(specifier, parentURL).href,
1047-
};
1048-
}
1049-
1050-
// Let Node.js handle all other specifiers.
1051-
return nextResolve(specifier);
1052-
}
1053-
10541033
export function load(url, context, nextLoad) {
10551034
// For JavaScript to be loaded over the network, we need to fetch and
10561035
// return it.
@@ -1091,9 +1070,7 @@ prints the current version of CoffeeScript per the module at the URL in
10911070
#### Transpiler loader
10921071

10931072
Sources that are in formats Node.js doesn't understand can be converted into
1094-
JavaScript using the [`load` hook][load hook]. Before that hook gets called,
1095-
however, a [`resolve` hook][resolve hook] needs to tell Node.js not to
1096-
throw an error on unknown file types.
1073+
JavaScript using the [`load` hook][load hook].
10971074
10981075
This is less performant than transpiling source files before running
10991076
Node.js; a transpiler loader should only be used for development and testing
@@ -1109,25 +1086,6 @@ import CoffeeScript from 'coffeescript';
11091086
11101087
const baseURL = pathToFileURL(`${cwd()}/`).href;
11111088
1112-
// CoffeeScript files end in .coffee, .litcoffee, or .coffee.md.
1113-
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
1114-
1115-
export function resolve(specifier, context, nextResolve) {
1116-
if (extensionsRegex.test(specifier)) {
1117-
const { parentURL = baseURL } = context;
1118-
1119-
// Node.js normally errors on unknown file extensions, so return a URL for
1120-
// specifiers ending in the CoffeeScript file extensions.
1121-
return {
1122-
shortCircuit: true,
1123-
url: new URL(specifier, parentURL).href,
1124-
};
1125-
}
1126-
1127-
// Let Node.js handle all other specifiers.
1128-
return nextResolve(specifier);
1129-
}
1130-
11311089
export async function load(url, context, nextLoad) {
11321090
if (extensionsRegex.test(url)) {
11331091
// Now that we patched resolve to let CoffeeScript URLs through, we need to
@@ -1220,27 +1178,99 @@ loaded from disk but before Node.js executes it; and so on for any `.coffee`,
12201178
`.litcoffee` or `.coffee.md` files referenced via `import` statements of any
12211179
loaded file.
12221180
1223-
## Resolution algorithm
1181+
#### "import map" loader
1182+
1183+
The previous two loaders defined `load` hooks. This is an example of a loader
1184+
that does its work via the `resolve` hook. This loader reads an
1185+
`import-map.json` file that specifies which specifiers to override to another
1186+
URL (this is a very simplistic implemenation of a small subset of the
1187+
"import maps" specification).
1188+
1189+
```js
1190+
// import-map-loader.js
1191+
import fs from 'node:fs/promises';
1192+
1193+
const { imports } = JSON.parse(await fs.readFile('import-map.json'));
1194+
1195+
export async function resolve(specifier, context, nextResolve) {
1196+
if (Object.hasOwn(imports, specifier)) {
1197+
return nextResolve(imports[specifier], context);
1198+
}
1199+
1200+
return nextResolve(specifier, context);
1201+
}
1202+
```
1203+
1204+
Let's assume we have these files:
1205+
1206+
```js
1207+
// main.js
1208+
import 'a-module';
1209+
```
1210+
1211+
```json
1212+
// import-map.json
1213+
{
1214+
"imports": {
1215+
"a-module": "./some-module.js"
1216+
}
1217+
}
1218+
```
1219+
1220+
```js
1221+
// some-module.js
1222+
console.log('some module!');
1223+
```
1224+
1225+
If you run `node --experimental-loader ./import-map-loader.js main.js`
1226+
the output will be `some module!`.
1227+
1228+
## Resolution and loading algorithm
12241229
12251230
### Features
12261231
1227-
The resolver has the following properties:
1232+
The default resolver has the following properties:
12281233
12291234
* FileURL-based resolution as is used by ES modules
1230-
* Support for builtin module loading
12311235
* Relative and absolute URL resolution
12321236
* No default extensions
12331237
* No folder mains
12341238
* Bare specifier package resolution lookup through node\_modules
1239+
* Does not fail on unknown extensions or protocols
1240+
* Can optionally provide a hint of the format to the loading phase
1241+
1242+
The default loader has the following properties
12351243
1236-
### Resolver algorithm
1244+
* Support for builtin module loading via `node:` URLs
1245+
* Support for "inline" module loading via `data:` URLs
1246+
* Support for `file:` module loading
1247+
* Fails on any other URL protocol
1248+
* Fails on unknown extensions for `file:` loading
1249+
(supports only `.cjs`, `.js`, and `.mjs`)
1250+
1251+
### Resolution algorithm
12371252
12381253
The algorithm to load an ES module specifier is given through the
12391254
**ESM\_RESOLVE** method below. It returns the resolved URL for a
12401255
module specifier relative to a parentURL.
12411256
1257+
The resolution algorithm determines the full resolved URL for a module
1258+
load, along with its suggested module format. The resolution algorithm
1259+
does not determine whether the resolved URL protocol can be loaded,
1260+
or whether the file extensions are permitted, instead these validations
1261+
are applied by Node.js during the load phase
1262+
(for example, if it was asked to load a URL that has a protocol that is
1263+
not `file:`, `data:`, `node:`, or if `--experimental-network-imports`
1264+
is enabled, `https:`).
1265+
1266+
The algorithm also tries to determine the format of the file based
1267+
on the extension (see `ESM_FILE_FORMAT` algorithm below). If it does
1268+
not recognize the file extension (eg if it is not `.mjs`, `.cjs`, or
1269+
`.json`), then a format of `undefined` is returned,
1270+
which will throw during the load phase.
1271+
12421272
The algorithm to determine the module format of a resolved URL is
1243-
provided by **ESM\_FORMAT**, which returns the unique module
1273+
provided by **ESM\_FILE\_FORMAT**, which returns the unique module
12441274
format for any file. The _"module"_ format is returned for an ECMAScript
12451275
Module, while the _"commonjs"_ format is used to indicate loading through the
12461276
legacy CommonJS loader. Additional formats such as _"addon"_ can be extended in
@@ -1267,7 +1297,7 @@ The resolver can throw the following errors:
12671297
* _Unsupported Directory Import_: The resolved path corresponds to a directory,
12681298
which is not a supported target for module imports.
12691299
1270-
### Resolver Algorithm Specification
1300+
### Resolution Algorithm Specification
12711301
12721302
**ESM\_RESOLVE**(_specifier_, _parentURL_)
12731303
@@ -1301,7 +1331,7 @@ The resolver can throw the following errors:
13011331
> 8. Otherwise,
13021332
> 1. Set _format_ the module format of the content type associated with the
13031333
> URL _resolved_.
1304-
> 9. Load _resolved_ as module format, _format_.
1334+
> 9. Return _format_ and _resolved_ to the loading phase
13051335
13061336
**PACKAGE\_RESOLVE**(_packageSpecifier_, _parentURL_)
13071337
@@ -1506,9 +1536,9 @@ _isImports_, _conditions_)
15061536
> 7. If _pjson?.type_ exists and is _"module"_, then
15071537
> 1. If _url_ ends in _".js"_, then
15081538
> 1. Return _"module"_.
1509-
> 2. Throw an _Unsupported File Extension_ error.
1539+
> 2. Return **undefined**.
15101540
> 8. Otherwise,
1511-
> 1. Throw an _Unsupported File Extension_ error.
1541+
> 1. Return **undefined**.
15121542
15131543
**LOOKUP\_PACKAGE\_SCOPE**(_url_)
15141544
@@ -1552,7 +1582,7 @@ for ESM specifiers is [commonjs-extension-resolution-loader][].
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
@@ -1581,7 +1611,6 @@ for ESM specifiers is [commonjs-extension-resolution-loader][].
15811611
[custom https loader]: #https-loader
15821612
[load hook]: #loadurl-context-nextload
15831613
[percent-encoded]: url.md#percent-encoding-in-urls
1584-
[resolve hook]: #resolvespecifier-context-nextresolve
15851614
[special scheme]: https://url.spec.whatwg.org/#special-scheme
15861615
[status code]: process.md#exit-codes
15871616
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules

lib/internal/modules/esm/load.js

+31
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ async function defaultLoad(url, context = kEmptyObject) {
7979
source,
8080
} = context;
8181

82+
throwIfUnsupportedURLScheme(new URL(url), experimentalNetworkImports);
83+
8284
if (format == null) {
8385
format = await defaultGetFormat(url, context);
8486
}
@@ -102,6 +104,35 @@ async function defaultLoad(url, context = kEmptyObject) {
102104
};
103105
}
104106

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

106137
/**
107138
* For a falsy `format` returned from `load`, throw an error.

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
JSONStringify,
98
ObjectGetOwnPropertyNames,
@@ -51,7 +50,6 @@ const {
5150
ERR_PACKAGE_PATH_NOT_EXPORTED,
5251
ERR_UNSUPPORTED_DIR_IMPORT,
5352
ERR_NETWORK_IMPORT_DISALLOWED,
54-
ERR_UNSUPPORTED_ESM_URL_SCHEME,
5553
} = require('internal/errors').codes;
5654

5755
const { Module: CJSModule } = require('internal/modules/cjs/loader');
@@ -941,37 +939,6 @@ function throwIfInvalidParentURL(parentURL) {
941939
}
942940
}
943941

944-
function throwIfUnsupportedURLProtocol(url) {
945-
// Avoid accessing the `protocol` property due to the lazy getters.
946-
const protocol = url.protocol;
947-
if (protocol !== 'file:' && protocol !== 'data:' &&
948-
protocol !== 'node:') {
949-
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url);
950-
}
951-
}
952-
953-
function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) {
954-
// Avoid accessing the `protocol` property due to the lazy getters.
955-
const protocol = parsed?.protocol;
956-
if (
957-
protocol &&
958-
protocol !== 'file:' &&
959-
protocol !== 'data:' &&
960-
(
961-
!experimentalNetworkImports ||
962-
(
963-
protocol !== 'https:' &&
964-
protocol !== 'http:'
965-
)
966-
)
967-
) {
968-
const schemes = ['file', 'data'];
969-
if (experimentalNetworkImports) {
970-
ArrayPrototypePush(schemes, 'https', 'http');
971-
}
972-
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, schemes);
973-
}
974-
}
975942

976943
function defaultResolve(specifier, context = {}) {
977944
let { parentURL, conditions } = context;
@@ -1048,7 +1015,6 @@ function defaultResolve(specifier, context = {}) {
10481015
// This must come after checkIfDisallowedImport
10491016
if (parsed && parsed.protocol === 'node:') return { __proto__: null, url: specifier };
10501017

1051-
throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports);
10521018

10531019
const isMain = parentURL === undefined;
10541020
if (isMain) {
@@ -1095,8 +1061,6 @@ function defaultResolve(specifier, context = {}) {
10951061
throw error;
10961062
}
10971063

1098-
throwIfUnsupportedURLProtocol(url);
1099-
11001064
return {
11011065
__proto__: null,
11021066
// Do NOT cast `url` to a string: that will work even when there are real

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
@@ -30,6 +30,8 @@ assert.strictEqual(
3030
code: 'ERR_INVALID_ARG_TYPE',
3131
})
3232
);
33+
assert.strictEqual(import.meta.resolve('http://some-absolute/url'), 'http://some-absolute/url');
34+
assert.strictEqual(import.meta.resolve('some://weird/protocol'), 'some://weird/protocol');
3335
assert.strictEqual(import.meta.resolve('baz/', fixtures),
3436
fixtures + 'node_modules/baz/');
3537

0 commit comments

Comments
 (0)