Skip to content

Commit 63a7425

Browse files
committed
change default resolver to not throw on unknown scheme
Fixes nodejs/loaders#138
1 parent ab434d2 commit 63a7425

File tree

9 files changed

+136
-98
lines changed

9 files changed

+136
-98
lines changed

doc/api/esm.md

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,28 +1029,6 @@ and there is no security.
10291029
// https-loader.mjs
10301030
import { get } from 'node:https';
10311031
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-
10541032
export function load(url, context, nextLoad) {
10551033
// For JavaScript to be loaded over the network, we need to fetch and
10561034
// return it.
@@ -1091,9 +1069,7 @@ prints the current version of CoffeeScript per the module at the URL in
10911069
#### Transpiler loader
10921070

10931071
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.
1072+
JavaScript using the [`load` hook][load hook].
10971073
10981074
This is less performant than transpiling source files before running
10991075
Node.js; a transpiler loader should only be used for development and testing
@@ -1109,25 +1085,6 @@ import CoffeeScript from 'coffeescript';
11091085
11101086
const baseURL = pathToFileURL(`${cwd()}/`).href;
11111087
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-
11311088
export async function load(url, context, nextLoad) {
11321089
if (extensionsRegex.test(url)) {
11331090
// Now that we patched resolve to let CoffeeScript URLs through, we need to
@@ -1220,6 +1177,49 @@ loaded from disk but before Node.js executes it; and so on for any `.coffee`,
12201177
`.litcoffee` or `.coffee.md` files referenced via `import` statements of any
12211178
loaded file.
12221179
1180+
#### Overriding loader
1181+
1182+
The above two loaders hooked into the "load" phase of the module loader.
1183+
This loader hooks into the "resolution" phase. This loader reads an
1184+
`overrides.json` file that specifies which specifiers to override to another
1185+
url.
1186+
1187+
```js
1188+
// overriding-loader.js
1189+
import fs from 'node:fs/promises'
1190+
1191+
const overrides = JSON.parse(await fs.readFile('overrides.json'))
1192+
1193+
export async function resolve(specifier, context, nextResolve) {
1194+
if (specifier in overrides) {
1195+
return nextResolve(overrides[specifier], context)
1196+
}
1197+
1198+
return nextResolve(specifier, context)
1199+
}
1200+
```
1201+
1202+
Let's assume we have these files:
1203+
1204+
```js
1205+
// main.js
1206+
import 'a-module-to-override'
1207+
```
1208+
```json
1209+
// overrides.json
1210+
{
1211+
"a-module-to-override": "./module-override.js"
1212+
}
1213+
```
1214+
1215+
```js
1216+
// module-override.js
1217+
console.log('module overridden!')
1218+
```
1219+
1220+
If you run `node --experimental-loader ./overriding-loader.js main.js` the output will be
1221+
`module overriden!`.
1222+
12231223
## Resolution algorithm
12241224
12251225
### Features
@@ -1506,9 +1506,9 @@ _isImports_, _conditions_)
15061506
> 7. If _pjson?.type_ exists and is _"module"_, then
15071507
> 1. If _url_ ends in _".js"_, then
15081508
> 1. Return _"module"_.
1509-
> 2. Throw an _Unsupported File Extension_ error.
1509+
> 2. return **undefined**.
15101510
> 8. Otherwise,
1511-
> 1. Throw an _Unsupported File Extension_ error.
1511+
> 1. return **undefined**.
15121512
15131513
**LOOKUP\_PACKAGE\_SCOPE**(_url_)
15141514

lib/internal/modules/esm/load.js

Lines changed: 32 additions & 0 deletions
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,36 @@ 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+
*
111+
* @param {URL} parsed
112+
* @param {boolean} experimentalNetworkImports
113+
*/
114+
function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) {
115+
// Avoid accessing the `protocol` property due to the lazy getters.
116+
const protocol = parsed?.protocol;
117+
if (
118+
protocol &&
119+
protocol !== 'file:' &&
120+
protocol !== 'data:' &&
121+
protocol !== 'node:' &&
122+
(
123+
!experimentalNetworkImports ||
124+
(
125+
protocol !== 'https:' &&
126+
protocol !== 'http:'
127+
)
128+
)
129+
) {
130+
const schemes = ['file', 'data', 'node'];
131+
if (experimentalNetworkImports) {
132+
ArrayPrototypePush(schemes, 'https', 'http');
133+
}
134+
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, schemes);
135+
}
136+
}
105137

106138
/**
107139
* For a falsy `format` returned from `load`, throw an error.

lib/internal/modules/esm/resolve.js

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -941,37 +941,6 @@ function throwIfInvalidParentURL(parentURL) {
941941
}
942942
}
943943

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-
}
975944

976945
function defaultResolve(specifier, context = {}) {
977946
let { parentURL, conditions } = context;
@@ -1048,7 +1017,6 @@ function defaultResolve(specifier, context = {}) {
10481017
// This must come after checkIfDisallowedImport
10491018
if (parsed && parsed.protocol === 'node:') return { __proto__: null, url: specifier };
10501019

1051-
throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports);
10521020

10531021
const isMain = parentURL === undefined;
10541022
if (isMain) {
@@ -1095,8 +1063,6 @@ function defaultResolve(specifier, context = {}) {
10951063
throw error;
10961064
}
10971065

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

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ assert.strictEqual(
3030
code: 'ERR_INVALID_ARG_TYPE',
3131
})
3232
);
33+
assert.equal(import.meta.resolve('http://some-absolute/url'), 'http://some-absolute/url')
34+
assert.equal(import.meta.resolve('some://weird/protocol'), 'some://weird/protocol')
3335
assert.strictEqual(import.meta.resolve('baz/', fixtures),
3436
fixtures + 'node_modules/baz/');
3537

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { spawnPromisified } from '../common/index.mjs';
2+
import * as fixtures from '../common/fixtures.mjs';
3+
import assert from 'node:assert';
4+
import { execPath } from 'node:process';
5+
import {describe, it} from 'node:test'
6+
7+
describe('default resolver', () => {
8+
it('should accept foreign schemas without exception (e.g. uyyt://something/or-other', async () => {
9+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
10+
'--no-warnings',
11+
'--experimental-loader',
12+
fixtures.fileURL('/es-module-loaders/uyyt-dummy-loader.mjs'),
13+
fixtures.path('/es-module-loaders/uyyt-dummy-loader-main.mjs'),
14+
]);
15+
assert.strictEqual(code, 0);
16+
assert.strictEqual(stdout.trim(), 'index.mjs!');
17+
assert.strictEqual(stderr, '');
18+
})
19+
it('should resolve foreign schemas by doing regular url absolutization', async () => {
20+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
21+
'--no-warnings',
22+
'--experimental-loader',
23+
fixtures.fileURL('/es-module-loaders/uyyt-dummy-loader.mjs'),
24+
fixtures.path('/es-module-loaders/uyyt-dummy-loader-main2.mjs'),
25+
]);
26+
assert.strictEqual(code, 0);
27+
assert.strictEqual(stdout.trim(), '42');
28+
assert.strictEqual(stderr, '');
29+
})
30+
})

test/fixtures/es-module-loaders/http-loader.mjs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,5 @@
11
import { get } from 'http';
22

3-
export function resolve(specifier, context, nextResolve) {
4-
const { parentURL = null } = context;
5-
6-
if (specifier.startsWith('http://')) {
7-
return {
8-
shortCircuit: true,
9-
url: specifier,
10-
};
11-
} else if (parentURL?.startsWith('http://')) {
12-
return {
13-
shortCircuit: true,
14-
url: new URL(specifier, parentURL).href,
15-
};
16-
}
17-
18-
return nextResolve(specifier);
19-
}
20-
213
export function load(url, context, nextLoad) {
224
if (url.startsWith('http://')) {
235
return new Promise((resolve, reject) => {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import 'uyyt://1/index.mjs';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import 'uyyt://1/index2.mjs';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export function load(url, context, nextLoad) {
2+
switch (url) {
3+
case 'uyyt://1/index.mjs':
4+
return {
5+
source: 'console.log("index.mjs!")',
6+
format: 'module',
7+
shortCircuit: true,
8+
};
9+
case 'uyyt://1/index2.mjs':
10+
return {
11+
source: 'import c from "./sub.mjs"; console.log(c);',
12+
format: 'module',
13+
shortCircuit: true,
14+
};
15+
case 'uyyt://1/sub.mjs':
16+
return {
17+
source: 'export default 42',
18+
format: 'module',
19+
shortCircuit: true,
20+
};
21+
default:
22+
return nextLoad(url, context);
23+
}
24+
}

0 commit comments

Comments
 (0)