Skip to content

Commit

Permalink
esm: fix base URL for network imports
Browse files Browse the repository at this point in the history
PR-URL: nodejs#42131
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
  • Loading branch information
bmeck authored and danielleadams committed Apr 21, 2022
1 parent 1b6426c commit 1beda65
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 16 deletions.
5 changes: 5 additions & 0 deletions lib/internal/modules/cjs/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ function addBuiltinLibsToObject(object, dummyModuleName) {
});
}

/**
*
* @param {string | URL} referrer
* @returns {string}
*/
function normalizeReferrerURL(referrer) {
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
return pathToFileURL(referrer).href;
Expand Down
6 changes: 4 additions & 2 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,8 @@ function wrapSafe(filename, content, cjsModuleInstance) {
displayErrors: true,
importModuleDynamically: async (specifier, _, importAssertions) => {
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename),
return loader.import(specifier,
loader.getBaseURL(normalizeReferrerURL(filename)),
importAssertions);
},
});
Expand All @@ -1039,7 +1040,8 @@ function wrapSafe(filename, content, cjsModuleInstance) {
filename,
importModuleDynamically(specifier, _, importAssertions) {
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename),
return loader.import(specifier,
loader.getBaseURL(normalizeReferrerURL(filename)),
importAssertions);
},
});
Expand Down
16 changes: 5 additions & 11 deletions lib/internal/modules/esm/initialize_import_meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@
const { getOptionValue } = require('internal/options');
const experimentalImportMetaResolve =
getOptionValue('--experimental-import-meta-resolve');
const { fetchModule } = require('internal/modules/esm/fetch_module');
const { URL } = require('internal/url');
const {
PromisePrototypeThen,
PromiseReject,
StringPrototypeStartsWith,
} = primordials;
const asyncESM = require('internal/process/esm_loader');

Expand All @@ -24,6 +21,10 @@ function createImportMetaResolve(defaultParentUrl) {
};
}

/**
* @param {object} meta
* @param {{url: string}} context
*/
function initializeImportMeta(meta, context) {
let url = context.url;

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

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

meta.url = url;
}
Expand Down
46 changes: 45 additions & 1 deletion lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ const {
RegExpPrototypeExec,
SafeArrayIterator,
SafeWeakMap,
StringPrototypeStartsWith,
globalThis,
} = primordials;
const { MessageChannel } = require('internal/worker/io');

const {
ERR_INTERNAL_ASSERTION,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_RETURN_PROPERTY_VALUE,
Expand All @@ -47,6 +49,9 @@ const { defaultLoad } = require('internal/modules/esm/load');
const { translators } = require(
'internal/modules/esm/translators');
const { getOptionValue } = require('internal/options');
const {
fetchModule,
} = require('internal/modules/esm/fetch_module');

/**
* An ESMLoader instance is used as the main entry point for loading ES modules.
Expand Down Expand Up @@ -209,7 +214,9 @@ class ESMLoader {
const module = new ModuleWrap(url, undefined, source, 0, 0);
callbackMap.set(module, {
importModuleDynamically: (specifier, { url }, importAssertions) => {
return this.import(specifier, url, importAssertions);
return this.import(specifier,
this.getBaseURL(url),
importAssertions);
}
});

Expand All @@ -225,6 +232,43 @@ class ESMLoader {
};
}

/**
* Returns the url to use for the resolution of a given cache key url
* These are not guaranteed to be the same.
*
* In WHATWG HTTP spec for ESM the cache key is the non-I/O bound
* synchronous resolution using only string operations
* ~= resolveImportMap(new URL(specifier, importerHREF))
*
* The url used for subsequent resolution is the response URL after
* all redirects have been resolved.
*
* https://example.com/foo redirecting to https://example.com/bar
* would have a cache key of https://example.com/foo and baseURL
* of https://example.com/bar
*
* MUST BE SYNCHRONOUS for import.meta initialization
* MUST BE CALLED AFTER receiving the url body due to I/O
* @param {string} url
* @returns {string}
*/
getBaseURL(url) {
if (
StringPrototypeStartsWith(url, 'http:') ||
StringPrototypeStartsWith(url, 'https:')
) {
// The request & response have already settled, so they are in
// fetchModule's cache, in which case, fetchModule returns
// immediately and synchronously
url = fetchModule(new URL(url), { parentURL: url }).resolvedHREF;
// This should only occur if the module hasn't been fetched yet
if (typeof url !== 'string') {
throw new ERR_INTERNAL_ASSERTION(`Base url for module ${url} not loaded.`);
}
}
return url;
}

/**
* Get a (possibly still pending) module job from the cache,
* or create one and return its Promise.
Expand Down
3 changes: 2 additions & 1 deletion lib/internal/modules/esm/module_job.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ class ModuleJob {
// these `link` callbacks depending on each other.
const dependencyJobs = [];
const promises = this.module.link(async (specifier, assertions) => {
const jobPromise = this.loader.getModuleJob(specifier, url, assertions);
const baseURL = this.loader.getBaseURL(url);
const jobPromise = this.loader.getModuleJob(specifier, baseURL, assertions);
ArrayPrototypePush(dependencyJobs, jobPromise);
const job = await jobPromise;
return job.modulePromise;
Expand Down
4 changes: 3 additions & 1 deletion lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ function errPath(url) {
}

async function importModuleDynamically(specifier, { url }, assertions) {
return asyncESM.esmLoader.import(specifier, url, assertions);
return asyncESM.esmLoader.import(specifier,
asyncESM.esmLoader.getBaseURL(url),
assertions);
}

// Strategy for loading a standard JavaScript module.
Expand Down
15 changes: 15 additions & 0 deletions test/es-module/test-http-imports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,21 @@ for (const { protocol, createServer } of [
assert.strict.notEqual(redirectedNS.default, ns.default);
assert.strict.equal(redirectedNS.url, url.href);

// Redirects have the same import.meta.url but different cache
// entry on Web
const relativeAfterRedirect = new URL(url.href + 'foo/index.js');
const redirected = new URL(url.href + 'bar/index.js');
redirected.searchParams.set('body', 'export let relativeDepURL = (await import("./baz.js")).url');
relativeAfterRedirect.searchParams.set('redirect', JSON.stringify({
status: 302,
location: redirected.href
}));
const relativeAfterRedirectedNS = await import(relativeAfterRedirect.href);
assert.strict.equal(
relativeAfterRedirectedNS.relativeDepURL,
url.href + 'bar/baz.js'
);

const crossProtocolRedirect = new URL(url.href);
crossProtocolRedirect.searchParams.set('redirect', JSON.stringify({
status: 302,
Expand Down

0 comments on commit 1beda65

Please sign in to comment.