Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

esm: merge and simplify loader hooks #35524

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions lib/internal/modules/esm/get_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const { Buffer } = require('buffer');
const fs = require('fs');
const { URL } = require('url');
const { promisify } = require('internal/util');
const { defaultGetFormat } = require('internal/modules/esm/get_format');
const {
ERR_INVALID_URL,
ERR_INVALID_URL_SCHEME,
Expand All @@ -19,7 +20,13 @@ const readFileAsync = promisify(fs.readFile);

const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/;

async function defaultGetSource(url, { format } = {}, defaultGetSource) {
async function defaultGetSource(url, context, defaultGetSource) {
const { format } = defaultGetFormat(url);

if (format === 'builtin' || format === 'commonjs') {
return { format, source: null };
}

const parsed = new URL(url);
let source;
if (parsed.protocol === 'file:') {
Expand All @@ -37,6 +44,6 @@ async function defaultGetSource(url, { format } = {}, defaultGetSource) {
if (policy?.manifest) {
policy.manifest.assertIntegrity(parsed, source);
}
return { source };
return { format, source };
}
exports.defaultGetSource = defaultGetSource;
115 changes: 27 additions & 88 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,8 @@ const {
defaultResolve,
DEFAULT_CONDITIONS,
} = require('internal/modules/esm/resolve');
const { defaultGetFormat } = require('internal/modules/esm/get_format');
const { defaultGetSource } = require(
'internal/modules/esm/get_source');
const { defaultTransformSource } = require(
'internal/modules/esm/transform_source');
const { translators } = require(
'internal/modules/esm/translators');
const { defaultGetSource } = require('internal/modules/esm/get_source');
const { translators } = require('internal/modules/esm/translators');
const { getOptionValue } = require('internal/options');

/* A Loader instance is used as the main entry point for loading ES modules.
Expand All @@ -57,22 +52,10 @@ class Loader {
// running any preload code.
// The preload code runs as soon as the hook module has finished evaluating.
this._getGlobalPreloadCode = null;
// The resolver has the signature
// (specifier : string, parentURL : string, defaultResolve)
// -> Promise<{ url : string }>
// where defaultResolve is ModuleRequest.resolve (having the same
// signature itself).
this._resolve = defaultResolve;
// This hook is called after the module is resolved but before a translator
// is chosen to load it; the format returned by this function is the name
// of a translator.
this._getFormat = defaultGetFormat;
// This hook is called just before the source code of an ES module file
// is loaded.
this._getSource = defaultGetSource;
// This hook is called just after the source code of an ES module file
// is loaded, but before anything is done with the string.
this._transformSource = defaultTransformSource;

this._resolveToURL = defaultResolve;
this._loadFromURL = defaultGetSource;

// The index for assigning unique URLs to anonymous module evaluation
this.evalIndex = 0;
}
Expand All @@ -82,7 +65,7 @@ class Loader {
if (!isMain)
validateString(parentURL, 'parentURL');

const resolveResponse = await this._resolve(
const resolveResponse = await this._resolveToURL(
specifier, { parentURL, conditions: DEFAULT_CONDITIONS }, defaultResolve);
if (typeof resolveResponse !== 'object') {
throw new ERR_INVALID_RETURN_VALUE(
Expand All @@ -97,46 +80,6 @@ class Loader {
return url;
}

async getFormat(url) {
const getFormatResponse = await this._getFormat(
url, {}, defaultGetFormat);
if (typeof getFormatResponse !== 'object') {
throw new ERR_INVALID_RETURN_VALUE(
'object', 'loader getFormat', getFormatResponse);
}

const { format } = getFormatResponse;
if (typeof format !== 'string') {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'string', 'loader getFormat', 'format', format);
}

if (format === 'builtin') {
return format;
}

if (this._resolve !== defaultResolve) {
try {
new URL(url);
} catch {
throw new ERR_INVALID_RETURN_PROPERTY(
'url', 'loader resolve', 'url', url
);
}
}

if (this._resolve === defaultResolve &&
!url.startsWith('file:') &&
!url.startsWith('data:')
) {
throw new ERR_INVALID_RETURN_PROPERTY(
'file: or data: url', 'loader resolve', 'url', url
);
}

return format;
}

async eval(
source,
url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href
Expand Down Expand Up @@ -168,29 +111,24 @@ class Loader {

hook(hooks) {
const {
resolve,
dynamicInstantiate,
getFormat,
getSource,
transformSource,
resolveToURL,
loadFromURL,
getGlobalPreloadCode,

// previous hooks:
dynamicInstantiate,
} = hooks;

// Use .bind() to avoid giving access to the Loader instance when called.
if (resolve !== undefined)
this._resolve = FunctionPrototypeBind(resolve, null);
if (dynamicInstantiate !== undefined) {
process.emitWarning(
'The dynamicInstantiate loader hook has been removed.');
}
if (getFormat !== undefined) {
this._getFormat = FunctionPrototypeBind(getFormat, null);
}
if (getSource !== undefined) {
this._getSource = FunctionPrototypeBind(getSource, null);
}
if (transformSource !== undefined) {
this._transformSource = FunctionPrototypeBind(transformSource, null);

// Use .bind() to avoid giving access to the Loader instance when called.
if (resolveToURL !== undefined)
this._resolveToURL = FunctionPrototypeBind(resolveToURL, null);
if (loadFromURL !== undefined) {
this._loadFromURL = FunctionPrototypeBind(loadFromURL, null);
}
if (getGlobalPreloadCode !== undefined) {
this._getGlobalPreloadCode =
Expand Down Expand Up @@ -227,22 +165,23 @@ class Loader {

async getModuleJob(specifier, parentURL) {
const url = await this.resolve(specifier, parentURL);
const format = await this.getFormat(url);
let job = this.moduleMap.get(url);
// CommonJS will set functions for lazy job evaluation.
if (typeof job === 'function')
this.moduleMap.set(url, job = job());
if (job !== undefined)
return job;

if (!translators.has(format))
throw new ERR_UNKNOWN_MODULE_FORMAT(format);

const loaderInstance = translators.get(format);
const moduleProvider = async (url, isMain) => {
const { source, format } = await this._loadFromURL(url, {}, defaultGetSource);
if (!translators.has(format))
throw new ERR_UNKNOWN_MODULE_FORMAT(format);
const translator = translators.get(format);
return translator.call(this, url, source, isMain);
}

const inspectBrk = parentURL === undefined &&
format === 'module' && getOptionValue('--inspect-brk');
job = new ModuleJob(this, url, loaderInstance, parentURL === undefined,
const inspectBrk = parentURL === undefined && getOptionValue('--inspect-brk');
job = new ModuleJob(this, url, moduleProvider, parentURL === undefined,
inspectBrk);
this.moduleMap.set(url, job);
return job;
Expand Down
8 changes: 1 addition & 7 deletions lib/internal/modules/esm/transform_source.js
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
'use strict';

function defaultTransformSource(source, { url, format } = {},
defaultTransformSource) {
return { source };
}
exports.defaultTransformSource = defaultTransformSource;
// no longer needed.
21 changes: 4 additions & 17 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,8 @@ function initializeImportMeta(meta, { url }) {
}

// Strategy for loading a standard JavaScript module.
translators.set('module', async function moduleStrategy(url) {
let { source } = await this._getSource(
url, { format: 'module' }, defaultGetSource);
translators.set('module', async function moduleStrategy(url, source) {
assertBufferSource(source, true, 'getSource');
({ source } = await this._transformSource(
source, { url, format: 'module' }, defaultTransformSource));
source = stringify(source);
maybeCacheSourceMap(url, source);
debug(`Translating StandardModule ${url}`);
Expand Down Expand Up @@ -157,7 +153,7 @@ function enrichCJSError(err) {
// Strategy for loading a node-style CommonJS module
const isWindows = process.platform === 'win32';
const winSepRegEx = /\//g;
translators.set('commonjs', async function commonjsStrategy(url, isMain) {
translators.set('commonjs', async function commonjsStrategy(url, source, isMain) {
debug(`Translating CJSModule ${url}`);

let filename = internalURLModule.fileURLToPath(new URL(url));
Expand Down Expand Up @@ -270,7 +266,7 @@ translators.set('builtin', async function builtinStrategy(url) {
});

// Strategy for loading a JSON file
translators.set('json', async function jsonStrategy(url) {
translators.set('json', async function jsonStrategy(url, source) {
emitExperimentalWarning('Importing JSON modules');
debug(`Translating JSONModule ${url}`);
debug(`Loading JSONModule ${url}`);
Expand All @@ -288,11 +284,7 @@ translators.set('json', async function jsonStrategy(url) {
});
}
}
let { source } = await this._getSource(
url, { format: 'json' }, defaultGetSource);
assertBufferSource(source, true, 'getSource');
({ source } = await this._transformSource(
source, { url, format: 'json' }, defaultTransformSource));
source = stringify(source);
if (pathname) {
// A require call could have been called on the same file during loading and
Expand Down Expand Up @@ -330,13 +322,8 @@ translators.set('json', async function jsonStrategy(url) {
});

// Strategy for loading a wasm module
translators.set('wasm', async function(url) {
translators.set('wasm', async function(url, source) {
emitExperimentalWarning('Importing Web Assembly modules');
let { source } = await this._getSource(
url, { format: 'wasm' }, defaultGetSource);
assertBufferSource(source, false, 'getSource');
({ source } = await this._transformSource(
source, { url, format: 'wasm' }, defaultTransformSource));
assertBufferSource(source, false, 'transformSource');
debug(`Translating WASMModule ${url}`);
let compiled;
Expand Down
5 changes: 3 additions & 2 deletions test/fixtures/es-module-loaders/get-source.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
export async function getSource(url, { format }, defaultGetSource) {
export async function loadFromURL(url, context, defaultLoadFromURL) {
if (url.endsWith('fixtures/es-modules/message.mjs')) {
// Oh, I’ve got that one in my cache!
return {
format: 'module',
source: `export const message = 'Woohoo!'.toUpperCase();`
}
} else {
return defaultGetSource(url, {format}, defaultGetSource);
return defaultLoadFromURL(url, context, defaultLoadFromURL);
}
}
8 changes: 4 additions & 4 deletions test/fixtures/es-module-loaders/transform-source.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
export async function transformSource(
source, { url, format }, defaultTransformSource) {
export async function loadFromURL(url, context, defaultLoadFromURL) {
let {format, source} = await defaultLoadFromURL(url, context);
if (format === 'module') {
if (typeof source !== 'string') {
source = new TextDecoder().decode(source);
}
return {
format,
source: source.replace(`'A message';`, `'A message'.toUpperCase();`)
};
} else { // source could be a buffer, e.g. for WASM
return defaultTransformSource(
source, {url, format}, defaultTransformSource);
return {format, source};
}
}