Skip to content

Commit 50d64ed

Browse files
guybedfordtargos
authored andcommitted
esm: refactor responseURL handling
PR-URL: #43164 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Minwoo Jung <nodecorelab@gmail.com> Reviewed-By: Jacob Smith <jacob@frende.me>
1 parent 254efd9 commit 50d64ed

9 files changed

+97
-154
lines changed

lib/internal/modules/cjs/loader.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -1030,8 +1030,7 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10301030
displayErrors: true,
10311031
importModuleDynamically: async (specifier, _, importAssertions) => {
10321032
const loader = asyncESM.esmLoader;
1033-
return loader.import(specifier,
1034-
loader.getBaseURL(normalizeReferrerURL(filename)),
1033+
return loader.import(specifier, normalizeReferrerURL(filename),
10351034
importAssertions);
10361035
},
10371036
});
@@ -1047,8 +1046,7 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10471046
filename,
10481047
importModuleDynamically(specifier, _, importAssertions) {
10491048
const loader = asyncESM.esmLoader;
1050-
return loader.import(specifier,
1051-
loader.getBaseURL(normalizeReferrerURL(filename)),
1049+
return loader.import(specifier, normalizeReferrerURL(filename),
10521050
importAssertions);
10531051
},
10541052
});

lib/internal/modules/esm/fetch_module.js

-11
Original file line numberDiff line numberDiff line change
@@ -238,17 +238,6 @@ 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-
251241
module.exports = {
252242
fetchModule,
253-
inFetchCache,
254243
};

lib/internal/modules/esm/get_source.js

-60
This file was deleted.

lib/internal/modules/esm/initialize_import_meta.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,13 @@ function createImportMetaResolve(defaultParentUrl) {
2626
* @param {{url: string}} context
2727
*/
2828
function initializeImportMeta(meta, context) {
29-
let url = context.url;
29+
const { url } = context;
3030

3131
// Alphabetical
3232
if (experimentalImportMetaResolve) {
3333
meta.resolve = createImportMetaResolve(url);
3434
}
3535

36-
url = asyncESM.esmLoader.getBaseURL(url);
37-
3836
meta.url = url;
3937
}
4038

lib/internal/modules/esm/load.js

+63-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,67 @@
11
'use strict';
22

3+
const {
4+
ArrayPrototypePush,
5+
RegExpPrototypeExec,
6+
decodeURIComponent,
7+
} = primordials;
8+
39
const { defaultGetFormat } = require('internal/modules/esm/get_format');
4-
const { defaultGetSource } = require('internal/modules/esm/get_source');
510
const { validateAssertions } = require('internal/modules/esm/assert');
11+
const { getOptionValue } = require('internal/options');
12+
const { fetchModule } = require('internal/modules/esm/fetch_module');
13+
14+
// Do not eagerly grab .manifest, it may be in TDZ
15+
const policy = getOptionValue('--experimental-policy') ?
16+
require('internal/process/policy') :
17+
null;
18+
const experimentalNetworkImports =
19+
getOptionValue('--experimental-network-imports');
20+
21+
const { Buffer: { from: BufferFrom } } = require('buffer');
22+
23+
const { readFile: readFileAsync } = require('internal/fs/promises').exports;
24+
const { URL } = require('internal/url');
25+
const {
26+
ERR_INVALID_URL,
27+
ERR_UNSUPPORTED_ESM_URL_SCHEME,
28+
} = require('internal/errors').codes;
29+
30+
const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/;
31+
32+
async function getSource(url, context) {
33+
const parsed = new URL(url);
34+
let responseURL = url;
35+
let source;
36+
if (parsed.protocol === 'file:') {
37+
source = await readFileAsync(parsed);
38+
} else if (parsed.protocol === 'data:') {
39+
const match = RegExpPrototypeExec(DATA_URL_PATTERN, parsed.pathname);
40+
if (!match) {
41+
throw new ERR_INVALID_URL(url);
42+
}
43+
const { 1: base64, 2: body } = match;
44+
source = BufferFrom(decodeURIComponent(body), base64 ? 'base64' : 'utf8');
45+
} else if (experimentalNetworkImports && (
46+
parsed.protocol === 'https:' ||
47+
parsed.protocol === 'http:'
48+
)) {
49+
const res = await fetchModule(parsed, context);
50+
source = await res.body;
51+
responseURL = res.resolvedHREF;
52+
} else {
53+
const supportedSchemes = ['file', 'data'];
54+
if (experimentalNetworkImports) {
55+
ArrayPrototypePush(supportedSchemes, 'http', 'https');
56+
}
57+
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, supportedSchemes);
58+
}
59+
if (policy?.manifest) {
60+
policy.manifest.assertIntegrity(parsed, source);
61+
}
62+
return { responseURL, source };
63+
}
64+
665

766
/**
867
* Node.js default load hook.
@@ -11,6 +70,7 @@ const { validateAssertions } = require('internal/modules/esm/assert');
1170
* @returns {object}
1271
*/
1372
async function defaultLoad(url, context) {
73+
let responseURL = url;
1474
const { importAssertions } = context;
1575
let {
1676
format,
@@ -29,11 +89,12 @@ async function defaultLoad(url, context) {
2989
) {
3090
source = null;
3191
} else if (source == null) {
32-
source = await defaultGetSource(url, context);
92+
({ responseURL, source } = await getSource(url, context));
3393
}
3494

3595
return {
3696
format,
97+
responseURL,
3798
source,
3899
};
39100
}

lib/internal/modules/esm/loader.js

+28-61
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@ const {
1717
RegExpPrototypeExec,
1818
SafeArrayIterator,
1919
SafeWeakMap,
20-
StringPrototypeStartsWith,
2120
globalThis,
2221
} = primordials;
2322
const { MessageChannel } = require('internal/worker/io');
2423

2524
const {
2625
ERR_LOADER_CHAIN_INCOMPLETE,
27-
ERR_INTERNAL_ASSERTION,
2826
ERR_INVALID_ARG_TYPE,
2927
ERR_INVALID_ARG_VALUE,
3028
ERR_INVALID_RETURN_PROPERTY_VALUE,
@@ -55,11 +53,6 @@ const { defaultLoad } = require('internal/modules/esm/load');
5553
const { translators } = require(
5654
'internal/modules/esm/translators');
5755
const { getOptionValue } = require('internal/options');
58-
const {
59-
fetchModule,
60-
inFetchCache,
61-
} = require('internal/modules/esm/fetch_module');
62-
6356

6457
/**
6558
* @typedef {object} ExportedHooks
@@ -306,9 +299,7 @@ class ESMLoader {
306299
const module = new ModuleWrap(url, undefined, source, 0, 0);
307300
callbackMap.set(module, {
308301
importModuleDynamically: (specifier, { url }, importAssertions) => {
309-
return this.import(specifier,
310-
this.getBaseURL(url),
311-
importAssertions);
302+
return this.import(specifier, url, importAssertions);
312303
}
313304
});
314305

@@ -324,55 +315,6 @@ class ESMLoader {
324315
};
325316
}
326317

327-
/**
328-
* Returns the url to use for the resolution of a given cache key url
329-
* These are not guaranteed to be the same.
330-
*
331-
* In WHATWG HTTP spec for ESM the cache key is the non-I/O bound
332-
* synchronous resolution using only string operations
333-
* ~= resolveImportMap(new URL(specifier, importerHREF))
334-
*
335-
* The url used for subsequent resolution is the response URL after
336-
* all redirects have been resolved.
337-
*
338-
* https://example.com/foo redirecting to https://example.com/bar
339-
* would have a cache key of https://example.com/foo and baseURL
340-
* of https://example.com/bar
341-
*
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']>}
346-
*/
347-
getBaseURL(url) {
348-
if (getOptionValue('--experimental-network-imports') && (
349-
StringPrototypeStartsWith(url, 'http:') ||
350-
StringPrototypeStartsWith(url, 'https:')
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
354-
// immediately and synchronously
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;
371-
}
372-
}
373-
return url;
374-
}
375-
376318
/**
377319
* Get a (possibly still pending) module job from the cache,
378320
* or create one and return its Promise.
@@ -431,6 +373,7 @@ class ESMLoader {
431373
const moduleProvider = async (url, isMain) => {
432374
const {
433375
format: finalFormat,
376+
responseURL,
434377
source,
435378
} = await this.load(url, {
436379
format,
@@ -440,10 +383,10 @@ class ESMLoader {
440383
const translator = translators.get(finalFormat);
441384

442385
if (!translator) {
443-
throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, url);
386+
throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL);
444387
}
445388

446-
return FunctionPrototypeCall(translator, this, url, source, isMain);
389+
return FunctionPrototypeCall(translator, this, responseURL, source, isMain);
447390
};
448391

449392
const inspectBrk = (
@@ -607,6 +550,29 @@ class ESMLoader {
607550
format,
608551
source,
609552
} = loaded;
553+
let responseURL = loaded.responseURL;
554+
555+
if (responseURL === undefined) {
556+
responseURL = url;
557+
}
558+
559+
let responseURLObj;
560+
if (typeof responseURL === 'string') {
561+
try {
562+
responseURLObj = new URL(responseURL);
563+
} catch {
564+
// responseURLObj not defined will throw in next branch.
565+
}
566+
}
567+
568+
if (responseURLObj?.href !== responseURL) {
569+
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
570+
'undefined or a fully resolved URL string',
571+
hookErrIdentifier,
572+
'responseURL',
573+
responseURL,
574+
);
575+
}
610576

611577
if (format == null) {
612578
const dataUrl = RegExpPrototypeExec(
@@ -644,6 +610,7 @@ class ESMLoader {
644610

645611
return {
646612
format,
613+
responseURL,
647614
source,
648615
};
649616
}

lib/internal/modules/esm/module_job.js

+1-6
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,7 @@ class ModuleJob {
7676
// these `link` callbacks depending on each other.
7777
const dependencyJobs = [];
7878
const promises = this.module.link(async (specifier, assertions) => {
79-
const base = await this.loader.getBaseURL(url);
80-
const baseURL = typeof base === 'string' ?
81-
base :
82-
base.resolvedHREF;
83-
84-
const jobPromise = this.loader.getModuleJob(specifier, baseURL, assertions);
79+
const jobPromise = this.loader.getModuleJob(specifier, url, assertions);
8580
ArrayPrototypePush(dependencyJobs, jobPromise);
8681
const job = await jobPromise;
8782
return job.modulePromise;

lib/internal/modules/esm/translators.js

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

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

111109
// Strategy for loading a standard JavaScript module.
@@ -116,9 +114,7 @@ translators.set('module', async function moduleStrategy(url, source, isMain) {
116114
debug(`Translating StandardModule ${url}`);
117115
const module = new ModuleWrap(url, undefined, source, 0, 0);
118116
moduleWrap.callbackMap.set(module, {
119-
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, {
120-
url: wrap.url
121-
}),
117+
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
122118
importModuleDynamically,
123119
});
124120
return module;

0 commit comments

Comments
 (0)