Skip to content

Commit 8dd4878

Browse files
bmecksxa
authored andcommitted
esm: fix base URL for network imports
PR-URL: #42131 Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent ceb47d1 commit 8dd4878

File tree

7 files changed

+79
-16
lines changed

7 files changed

+79
-16
lines changed

lib/internal/modules/cjs/helpers.js

+5
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ function addBuiltinLibsToObject(object, dummyModuleName) {
193193
});
194194
}
195195

196+
/**
197+
*
198+
* @param {string | URL} referrer
199+
* @returns {string}
200+
*/
196201
function normalizeReferrerURL(referrer) {
197202
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
198203
return pathToFileURL(referrer).href;

lib/internal/modules/cjs/loader.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -1017,7 +1017,8 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10171017
displayErrors: true,
10181018
importModuleDynamically: async (specifier, _, importAssertions) => {
10191019
const loader = asyncESM.esmLoader;
1020-
return loader.import(specifier, normalizeReferrerURL(filename),
1020+
return loader.import(specifier,
1021+
loader.getBaseURL(normalizeReferrerURL(filename)),
10211022
importAssertions);
10221023
},
10231024
});
@@ -1033,7 +1034,8 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10331034
filename,
10341035
importModuleDynamically(specifier, _, importAssertions) {
10351036
const loader = asyncESM.esmLoader;
1036-
return loader.import(specifier, normalizeReferrerURL(filename),
1037+
return loader.import(specifier,
1038+
loader.getBaseURL(normalizeReferrerURL(filename)),
10371039
importAssertions);
10381040
},
10391041
});

lib/internal/modules/esm/initialize_import_meta.js

+5-11
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33
const { getOptionValue } = require('internal/options');
44
const experimentalImportMetaResolve =
55
getOptionValue('--experimental-import-meta-resolve');
6-
const { fetchModule } = require('internal/modules/esm/fetch_module');
7-
const { URL } = require('internal/url');
86
const {
97
PromisePrototypeThen,
108
PromiseReject,
11-
StringPrototypeStartsWith,
129
} = primordials;
1310
const asyncESM = require('internal/process/esm_loader');
1411

@@ -24,6 +21,10 @@ function createImportMetaResolve(defaultParentUrl) {
2421
};
2522
}
2623

24+
/**
25+
* @param {object} meta
26+
* @param {{url: string}} context
27+
*/
2728
function initializeImportMeta(meta, context) {
2829
let url = context.url;
2930

@@ -32,14 +33,7 @@ function initializeImportMeta(meta, context) {
3233
meta.resolve = createImportMetaResolve(url);
3334
}
3435

35-
if (
36-
StringPrototypeStartsWith(url, 'http:') ||
37-
StringPrototypeStartsWith(url, 'https:')
38-
) {
39-
// The request & response have already settled, so they are in fetchModule's
40-
// cache, in which case, fetchModule returns immediately and synchronously
41-
url = fetchModule(new URL(url), context).resolvedHREF;
42-
}
36+
url = asyncESM.esmLoader.getBaseURL(url);
4337

4438
meta.url = url;
4539
}

lib/internal/modules/esm/loader.js

+45-1
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ const {
1717
RegExpPrototypeExec,
1818
SafeArrayIterator,
1919
SafeWeakMap,
20+
StringPrototypeStartsWith,
2021
globalThis,
2122
} = primordials;
2223
const { MessageChannel } = require('internal/worker/io');
2324

2425
const {
26+
ERR_INTERNAL_ASSERTION,
2527
ERR_INVALID_ARG_TYPE,
2628
ERR_INVALID_ARG_VALUE,
2729
ERR_INVALID_RETURN_PROPERTY_VALUE,
@@ -47,6 +49,9 @@ const { defaultLoad } = require('internal/modules/esm/load');
4749
const { translators } = require(
4850
'internal/modules/esm/translators');
4951
const { getOptionValue } = require('internal/options');
52+
const {
53+
fetchModule,
54+
} = require('internal/modules/esm/fetch_module');
5055

5156
/**
5257
* An ESMLoader instance is used as the main entry point for loading ES modules.
@@ -209,7 +214,9 @@ class ESMLoader {
209214
const module = new ModuleWrap(url, undefined, source, 0, 0);
210215
callbackMap.set(module, {
211216
importModuleDynamically: (specifier, { url }, importAssertions) => {
212-
return this.import(specifier, url, importAssertions);
217+
return this.import(specifier,
218+
this.getBaseURL(url),
219+
importAssertions);
213220
}
214221
});
215222

@@ -225,6 +232,43 @@ class ESMLoader {
225232
};
226233
}
227234

235+
/**
236+
* Returns the url to use for the resolution of a given cache key url
237+
* These are not guaranteed to be the same.
238+
*
239+
* In WHATWG HTTP spec for ESM the cache key is the non-I/O bound
240+
* synchronous resolution using only string operations
241+
* ~= resolveImportMap(new URL(specifier, importerHREF))
242+
*
243+
* The url used for subsequent resolution is the response URL after
244+
* all redirects have been resolved.
245+
*
246+
* https://example.com/foo redirecting to https://example.com/bar
247+
* would have a cache key of https://example.com/foo and baseURL
248+
* of https://example.com/bar
249+
*
250+
* MUST BE SYNCHRONOUS for import.meta initialization
251+
* MUST BE CALLED AFTER receiving the url body due to I/O
252+
* @param {string} url
253+
* @returns {string}
254+
*/
255+
getBaseURL(url) {
256+
if (
257+
StringPrototypeStartsWith(url, 'http:') ||
258+
StringPrototypeStartsWith(url, 'https:')
259+
) {
260+
// The request & response have already settled, so they are in
261+
// fetchModule's cache, in which case, fetchModule returns
262+
// immediately and synchronously
263+
url = fetchModule(new URL(url), { parentURL: url }).resolvedHREF;
264+
// This should only occur if the module hasn't been fetched yet
265+
if (typeof url !== 'string') {
266+
throw new ERR_INTERNAL_ASSERTION(`Base url for module ${url} not loaded.`);
267+
}
268+
}
269+
return url;
270+
}
271+
228272
/**
229273
* Get a (possibly still pending) module job from the cache,
230274
* or create one and return its Promise.

lib/internal/modules/esm/module_job.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ class ModuleJob {
7676
// these `link` callbacks depending on each other.
7777
const dependencyJobs = [];
7878
const promises = this.module.link(async (specifier, assertions) => {
79-
const jobPromise = this.loader.getModuleJob(specifier, url, assertions);
79+
const baseURL = this.loader.getBaseURL(url);
80+
const jobPromise = this.loader.getModuleJob(specifier, baseURL, assertions);
8081
ArrayPrototypePush(dependencyJobs, jobPromise);
8182
const job = await jobPromise;
8283
return job.modulePromise;

lib/internal/modules/esm/translators.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ function errPath(url) {
103103
}
104104

105105
async function importModuleDynamically(specifier, { url }, assertions) {
106-
return asyncESM.esmLoader.import(specifier, url, assertions);
106+
return asyncESM.esmLoader.import(specifier,
107+
asyncESM.esmLoader.getBaseURL(url),
108+
assertions);
107109
}
108110

109111
// Strategy for loading a standard JavaScript module.

test/es-module/test-http-imports.mjs

+15
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,21 @@ for (const { protocol, createServer } of [
116116
assert.strict.notEqual(redirectedNS.default, ns.default);
117117
assert.strict.equal(redirectedNS.url, url.href);
118118

119+
// Redirects have the same import.meta.url but different cache
120+
// entry on Web
121+
const relativeAfterRedirect = new URL(url.href + 'foo/index.js');
122+
const redirected = new URL(url.href + 'bar/index.js');
123+
redirected.searchParams.set('body', 'export let relativeDepURL = (await import("./baz.js")).url');
124+
relativeAfterRedirect.searchParams.set('redirect', JSON.stringify({
125+
status: 302,
126+
location: redirected.href
127+
}));
128+
const relativeAfterRedirectedNS = await import(relativeAfterRedirect.href);
129+
assert.strict.equal(
130+
relativeAfterRedirectedNS.relativeDepURL,
131+
url.href + 'bar/baz.js'
132+
);
133+
119134
const crossProtocolRedirect = new URL(url.href);
120135
crossProtocolRedirect.searchParams.set('redirect', JSON.stringify({
121136
status: 302,

0 commit comments

Comments
 (0)