Skip to content

Commit 254efd9

Browse files
JakobJingleheimertargos
authored andcommitted
esm: fix http(s) import via custom loader
PR-URL: #43130 Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent da61e23 commit 254efd9

File tree

5 files changed

+154
-14
lines changed

5 files changed

+154
-14
lines changed

lib/internal/modules/esm/fetch_module.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,17 @@ function fetchModule(parsed, { parentURL }) {
238238
return fetchWithRedirects(parsed);
239239
}
240240

241+
/**
242+
* Checks if the given canonical URL exists in the fetch cache
243+
*
244+
* @param {string} key
245+
* @returns {boolean}
246+
*/
247+
function inFetchCache(key) {
248+
return cacheForGET.has(key);
249+
}
250+
241251
module.exports = {
242-
fetchModule: fetchModule,
252+
fetchModule,
253+
inFetchCache,
243254
};

lib/internal/modules/esm/loader.js

+25-12
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const { translators } = require(
5757
const { getOptionValue } = require('internal/options');
5858
const {
5959
fetchModule,
60+
inFetchCache,
6061
} = require('internal/modules/esm/fetch_module');
6162

6263

@@ -338,23 +339,35 @@ class ESMLoader {
338339
* would have a cache key of https://example.com/foo and baseURL
339340
* of https://example.com/bar
340341
*
341-
* MUST BE SYNCHRONOUS for import.meta initialization
342-
* MUST BE CALLED AFTER receiving the url body due to I/O
343-
* @param {string} url
344-
* @returns {string}
342+
* ! MUST BE SYNCHRONOUS for import.meta initialization
343+
* ! MUST BE CALLED AFTER receiving the url body due to I/O
344+
* @param {URL['href']} url
345+
* @returns {string|Promise<URL['href']>}
345346
*/
346347
getBaseURL(url) {
347-
if (
348+
if (getOptionValue('--experimental-network-imports') && (
348349
StringPrototypeStartsWith(url, 'http:') ||
349350
StringPrototypeStartsWith(url, 'https:')
350-
) {
351-
// The request & response have already settled, so they are in
352-
// fetchModule's cache, in which case, fetchModule returns
351+
)) {
352+
// When using network-imports, the request & response have already settled
353+
// so they are in fetchModule's cache, in which case, fetchModule returns
353354
// immediately and synchronously
354-
url = fetchModule(new URL(url), { parentURL: url }).resolvedHREF;
355-
// This should only occur if the module hasn't been fetched yet
356-
if (typeof url !== 'string') { // [2]
357-
throw new ERR_INTERNAL_ASSERTION(`Base url for module ${url} not loaded.`);
355+
// Unless a custom loader bypassed the fetch cache, in which case we just
356+
// use the original url
357+
if (inFetchCache(url)) {
358+
const module = fetchModule(new URL(url), { parentURL: url });
359+
if (typeof module?.resolvedHREF === 'string') {
360+
return module.resolvedHREF;
361+
}
362+
// Internal error
363+
throw new ERR_INTERNAL_ASSERTION(
364+
`Base url for module ${url} not loaded.`
365+
);
366+
} else {
367+
// A custom loader was used instead of network-imports.
368+
// Adding support for a response URL resolve return in custom loaders is
369+
// pending.
370+
return url;
358371
}
359372
}
360373
return url;

lib/internal/modules/esm/module_job.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ class ModuleJob {
7676
// these `link` callbacks depending on each other.
7777
const dependencyJobs = [];
7878
const promises = this.module.link(async (specifier, assertions) => {
79-
const baseURL = this.loader.getBaseURL(url);
79+
const base = await this.loader.getBaseURL(url);
80+
const baseURL = typeof base === 'string' ?
81+
base :
82+
base.resolvedHREF;
83+
8084
const jobPromise = this.loader.getModuleJob(specifier, baseURL, assertions);
8185
ArrayPrototypePush(dependencyJobs, jobPromise);
8286
const job = await jobPromise;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { mustCall } from '../common/index.mjs';
2+
import fixtures from '../common/fixtures.js';
3+
import { strictEqual } from 'node:assert';
4+
import { spawn } from 'node:child_process';
5+
import http from 'node:http';
6+
import path from 'node:path';
7+
import { promisify } from 'node:util';
8+
9+
10+
const files = {
11+
'main.mjs': 'export * from "./lib.mjs";',
12+
'lib.mjs': 'export { sum } from "./sum.mjs";',
13+
'sum.mjs': 'export function sum(a, b) { return a + b }',
14+
};
15+
16+
const requestListener = ({ url }, rsp) => {
17+
const filename = path.basename(url);
18+
const content = files[filename];
19+
20+
if (content) {
21+
return rsp
22+
.writeHead(200, { 'Content-Type': 'application/javascript' })
23+
.end(content);
24+
}
25+
26+
return rsp
27+
.writeHead(404)
28+
.end();
29+
};
30+
31+
const server = http.createServer(requestListener);
32+
33+
await promisify(server.listen.bind(server))({
34+
host: '127.0.0.1',
35+
port: 0,
36+
});
37+
38+
const {
39+
address: host,
40+
port,
41+
} = server.address();
42+
43+
{ // Verify nested HTTP imports work
44+
const child = spawn( // ! `spawn` MUST be used (vs `spawnSync`) to avoid blocking the event loop
45+
process.execPath,
46+
[
47+
'--no-warnings',
48+
'--loader',
49+
fixtures.fileURL('es-module-loaders', 'http-loader.mjs'),
50+
'--input-type=module',
51+
'--eval',
52+
`import * as main from 'http://${host}:${port}/main.mjs'; console.log(main)`,
53+
]
54+
);
55+
56+
let stderr = '';
57+
let stdout = '';
58+
59+
child.stderr.setEncoding('utf8');
60+
child.stderr.on('data', (data) => stderr += data);
61+
child.stdout.setEncoding('utf8');
62+
child.stdout.on('data', (data) => stdout += data);
63+
64+
child.on('close', mustCall((code, signal) => {
65+
strictEqual(stderr, '');
66+
strictEqual(stdout, '[Module: null prototype] { sum: [Function: sum] }\n');
67+
strictEqual(code, 0);
68+
strictEqual(signal, null);
69+
70+
server.close();
71+
}));
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { get } from 'http';
2+
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, context);
19+
}
20+
21+
export function load(url, context, nextLoad) {
22+
if (url.startsWith('http://')) {
23+
return new Promise((resolve, reject) => {
24+
get(url, (rsp) => {
25+
let data = '';
26+
rsp.on('data', (chunk) => data += chunk);
27+
rsp.on('end', () => {
28+
resolve({
29+
format: 'module',
30+
shortCircuit: true,
31+
source: data,
32+
});
33+
});
34+
})
35+
.on('error', reject);
36+
});
37+
}
38+
39+
return nextLoad(url, context);
40+
}

0 commit comments

Comments
 (0)