Skip to content

Commit

Permalink
module: centralize SourceTextModule compilation for builtin loader
Browse files Browse the repository at this point in the history
This refactors the code that compiles SourceTextModule for the
built-in ESM loader to use a common routine so that it's easier
to customize cache handling for the ESM loader. In addition
this introduces a common symbol for import.meta and import()
so that we don't need to create additional closures as handlers,
since we can get all the information we need from the V8 callback
already. This should reduce the memory footprint of ESM as well.

PR-URL: #52291
Refs: #47472
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
  • Loading branch information
joyeecheung committed Apr 3, 2024
1 parent 4df34cf commit d6b57f6
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 91 deletions.
5 changes: 2 additions & 3 deletions lib/internal/modules/esm/create_dynamic_module.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ ${ArrayPrototypeJoin(ArrayPrototypeMap(imports, createImport), '\n')}
${ArrayPrototypeJoin(ArrayPrototypeMap(exports, createExport), '\n')}
import.meta.done();
`;
const { ModuleWrap } = internalBinding('module_wrap');
const m = new ModuleWrap(`${url}`, undefined, source, 0, 0);
const { registerModule, compileSourceTextModule } = require('internal/modules/esm/utils');
const m = compileSourceTextModule(`${url}`, source);

const readyfns = new SafeSet();
/** @type {DynamicModuleReflect} */
Expand All @@ -68,7 +68,6 @@ import.meta.done();
if (imports.length) {
reflect.imports = { __proto__: null };
}
const { registerModule } = require('internal/modules/esm/utils');
registerModule(m, {
__proto__: null,
initializeImportMeta: (meta, wrap) => {
Expand Down
34 changes: 3 additions & 31 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,10 @@ const { getOptionValue } = require('internal/options');
const { isURL, pathToFileURL, URL } = require('internal/url');
const { emitExperimentalWarning, kEmptyObject } = require('internal/util');
const {
registerModule,
compileSourceTextModule,
getDefaultConditions,
} = require('internal/modules/esm/utils');
const { kImplicitAssertType } = require('internal/modules/esm/assert');
const {
maybeCacheSourceMap,
} = require('internal/source_map/source_map_cache');
const { canParse } = internalBinding('url');
const { ModuleWrap } = internalBinding('module_wrap');
let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer;
Expand Down Expand Up @@ -193,16 +190,7 @@ class ModuleLoader {

async eval(source, url, isEntryPoint = false) {
const evalInstance = (url) => {
const module = new ModuleWrap(url, undefined, source, 0, 0);
registerModule(module, {
__proto__: null,
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
importModuleDynamically: (specifier, { url }, importAttributes) => {
return this.import(specifier, url, importAttributes);
},
});

return module;
return compileSourceTextModule(url, source, this);
};
const { ModuleJob } = require('internal/modules/esm/module_job');
const job = new ModuleJob(
Expand Down Expand Up @@ -273,26 +261,10 @@ class ModuleLoader {
if (job !== undefined) {
return job.module.getNamespaceSync();
}

// TODO(joyeecheung): refactor this so that we pre-parse in C++ and hit the
// cache here, or use a carrier object to carry the compiled module script
// into the constructor to ensure cache hit.
const wrap = new ModuleWrap(url, undefined, source, 0, 0);
// Cache the source map for the module if present.
if (wrap.sourceMapURL) {
maybeCacheSourceMap(url, source, null, false, undefined, wrap.sourceMapURL);
}
const { registerModule } = require('internal/modules/esm/utils');
// TODO(joyeecheung): refactor so that the default options are shared across
// the built-in loaders.
registerModule(wrap, {
__proto__: null,
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
importModuleDynamically: (specifier, wrap, importAttributes) => {
return this.import(specifier, url, importAttributes);
},
});

const wrap = compileSourceTextModule(url, source, this);
const inspectBrk = (isMain && getOptionValue('--inspect-brk'));

const { ModuleJobSync } = require('internal/modules/esm/module_job');
Expand Down
26 changes: 2 additions & 24 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,35 +156,13 @@ function errPath(url) {
return url;
}

/**
* Dynamically imports a module using the ESM loader.
* @param {string} specifier - The module specifier to import.
* @param {object} options - An object containing options for the import.
* @param {string} options.url - The URL of the module requesting the import.
* @param {Record<string, string>} [attributes] - An object containing attributes for the import.
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} The imported module.
*/
async function importModuleDynamically(specifier, { url }, attributes) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.import(specifier, url, attributes);
}

// Strategy for loading a standard JavaScript module.
translators.set('module', function moduleStrategy(url, source, isMain) {
assertBufferSource(source, true, 'load');
source = stringify(source);
debug(`Translating StandardModule ${url}`);
const module = new ModuleWrap(url, undefined, source, 0, 0);
// Cache the source map for the module if present.
if (module.sourceMapURL) {
maybeCacheSourceMap(url, source, null, false, undefined, module.sourceMapURL);
}
const { registerModule } = require('internal/modules/esm/utils');
registerModule(module, {
__proto__: null,
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
importModuleDynamically,
});
const { compileSourceTextModule } = require('internal/modules/esm/utils');
const module = compileSourceTextModule(url, source, this);
return module;
});

Expand Down
81 changes: 71 additions & 10 deletions lib/internal/modules/esm/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ const {
},
} = internalBinding('util');
const {
source_text_module_default_hdo,
vm_dynamic_import_default_internal,
vm_dynamic_import_main_context_default,
vm_dynamic_import_missing_flag,
vm_dynamic_import_no_callback,
} = internalBinding('symbols');

const { ModuleWrap } = internalBinding('module_wrap');
const {
maybeCacheSourceMap,
} = require('internal/source_map/source_map_cache');

const {
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG,
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
Expand Down Expand Up @@ -167,28 +173,55 @@ function registerModule(referrer, registry) {
moduleRegistries.set(idSymbol, registry);
}

/**
* Proxy the import meta handling to the default loader for source text modules.
* @param {Record<string, string | Function>} meta - The import.meta object to initialize.
* @param {ModuleWrap} wrap - The ModuleWrap of the SourceTextModule where `import.meta` is referenced.
*/
function defaultInitializeImportMetaForModule(meta, wrap) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.importMetaInitialize(meta, { url: wrap.url });
}

/**
* Defines the `import.meta` object for a given module.
* @param {symbol} symbol - Reference to the module.
* @param {Record<string, string | Function>} meta - The import.meta object to initialize.
* @param {ModuleWrap} wrap - The ModuleWrap of the SourceTextModule where `import.meta` is referenced.
*/
function initializeImportMetaObject(symbol, meta) {
if (moduleRegistries.has(symbol)) {
const { initializeImportMeta, callbackReferrer } = moduleRegistries.get(symbol);
if (initializeImportMeta !== undefined) {
meta = initializeImportMeta(meta, callbackReferrer);
}
function initializeImportMetaObject(symbol, meta, wrap) {
if (symbol === source_text_module_default_hdo) {
defaultInitializeImportMetaForModule(meta, wrap);
return;
}
const data = moduleRegistries.get(symbol);
assert(data, `import.meta registry not found for ${wrap.url}`);
const { initializeImportMeta, callbackReferrer } = data;
if (initializeImportMeta !== undefined) {
meta = initializeImportMeta(meta, callbackReferrer);
}
}

/**
* Proxy the dynamic import to the default loader.
* Proxy the dynamic import handling to the default loader for source text modules.
* @param {string} specifier - The module specifier string.
* @param {Record<string, string>} attributes - The import attributes object.
* @param {string|null|undefined} referrerName - name of the referrer.
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
*/
function defaultImportModuleDynamicallyForModule(specifier, attributes, referrerName) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.import(specifier, referrerName, attributes);
}

/**
* Proxy the dynamic import to the default loader for classic scripts.
* @param {string} specifier - The module specifier string.
* @param {Record<string, string>} attributes - The import attributes object.
* @param {string|null|undefined} referrerName - name of the referrer.
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
*/
function defaultImportModuleDynamically(specifier, attributes, referrerName) {
function defaultImportModuleDynamicallyForScript(specifier, attributes, referrerName) {
const parentURL = normalizeReferrerURL(referrerName);
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.import(specifier, parentURL, attributes);
Expand All @@ -208,12 +241,16 @@ async function importModuleDynamicallyCallback(referrerSymbol, specifier, attrib
// and fall back to the default loader.
if (referrerSymbol === vm_dynamic_import_main_context_default) {
emitExperimentalWarning('vm.USE_MAIN_CONTEXT_DEFAULT_LOADER');
return defaultImportModuleDynamically(specifier, attributes, referrerName);
return defaultImportModuleDynamicallyForScript(specifier, attributes, referrerName);
}
// For script compiled internally that should use the default loader to handle dynamic
// import, proxy the request to the default loader without the warning.
if (referrerSymbol === vm_dynamic_import_default_internal) {
return defaultImportModuleDynamically(specifier, attributes, referrerName);
return defaultImportModuleDynamicallyForScript(specifier, attributes, referrerName);
}
// For SourceTextModules compiled internally, proxy the request to the default loader.
if (referrerSymbol === source_text_module_default_hdo) {
return defaultImportModuleDynamicallyForModule(specifier, attributes, referrerName);
}

if (moduleRegistries.has(referrerSymbol)) {
Expand Down Expand Up @@ -286,6 +323,29 @@ async function initializeHooks() {
return hooks;
}

/**
* Compile a SourceTextModule for the built-in ESM loader. Register it for default
* source map and import.meta and dynamic import() handling if cascadedLoader is provided.
* @param {string} url URL of the module.
* @param {string} source Source code of the module.
* @param {typeof import('./loader.js').ModuleLoader|undefined} cascadedLoader If provided,
* register the module for default handling.
* @returns {ModuleWrap}
*/
function compileSourceTextModule(url, source, cascadedLoader) {
const hostDefinedOption = cascadedLoader ? source_text_module_default_hdo : undefined;
const wrap = new ModuleWrap(url, undefined, source, 0, 0, hostDefinedOption);

if (!cascadedLoader) {
return wrap;
}
// Cache the source map for the module if present.
if (wrap.sourceMapURL) {
maybeCacheSourceMap(url, source, null, false, undefined, wrap.sourceMapURL);
}
return wrap;
}

module.exports = {
registerModule,
initializeESM,
Expand All @@ -294,4 +354,5 @@ module.exports = {
getConditionsSet,
loaderWorkerId: 'internal/modules/esm/worker',
forceDefaultLoader,
compileSourceTextModule,
};
11 changes: 5 additions & 6 deletions lib/internal/vm/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,19 +141,18 @@ class Module {
importModuleDynamicallyWrap(options.importModuleDynamically) :
undefined,
};
// This will take precedence over the referrer as the object being
// passed into the callbacks.
registry.callbackReferrer = this;
const { registerModule } = require('internal/modules/esm/utils');
registerModule(this[kWrap], registry);
} else {
assert(syntheticEvaluationSteps);
this[kWrap] = new ModuleWrap(identifier, context,
syntheticExportNames,
syntheticEvaluationSteps);
}

// This will take precedence over the referrer as the object being
// passed into the callbacks.
registry.callbackReferrer = this;
const { registerModule } = require('internal/modules/esm/utils');
registerModule(this[kWrap], registry);

this[kContext] = context;
}

Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
V(onpskexchange_symbol, "onpskexchange") \
V(resource_symbol, "resource_symbol") \
V(trigger_async_id_symbol, "trigger_async_id_symbol") \
V(source_text_module_default_hdo, "source_text_module_default_hdo") \
V(vm_dynamic_import_default_internal, "vm_dynamic_import_default_internal") \
V(vm_dynamic_import_main_context_default, \
"vm_dynamic_import_main_context_default") \
Expand Down
42 changes: 25 additions & 17 deletions src/module_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,10 @@ v8::Maybe<bool> ModuleWrap::CheckUnsettledTopLevelAwait() {
return v8::Just(false);
}

// new ModuleWrap(url, context, source, lineOffset, columnOffset)
// new ModuleWrap(url, context, exportNames, syntheticExecutionFunction)
// new ModuleWrap(url, context, source, lineOffset, columnOffset, cachedData)
// new ModuleWrap(url, context, source, lineOffset, columOffset,
// hostDefinedOption) new ModuleWrap(url, context, exportNames,
// syntheticExecutionFunction)
void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
CHECK(args.IsConstructCall());
CHECK_GE(args.Length(), 3);
Expand Down Expand Up @@ -172,22 +174,36 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
int column_offset = 0;

bool synthetic = args[2]->IsArray();

Local<PrimitiveArray> host_defined_options =
PrimitiveArray::New(isolate, HostDefinedOptions::kLength);
Local<Symbol> id_symbol;
if (synthetic) {
// new ModuleWrap(url, context, exportNames, syntheticExecutionFunction)
CHECK(args[3]->IsFunction());
} else {
// new ModuleWrap(url, context, source, lineOffset, columOffset, cachedData)
// new ModuleWrap(url, context, source, lineOffset, columOffset,
// hostDefinedOption)
CHECK(args[2]->IsString());
CHECK(args[3]->IsNumber());
line_offset = args[3].As<Int32>()->Value();
CHECK(args[4]->IsNumber());
column_offset = args[4].As<Int32>()->Value();
}
if (args[5]->IsSymbol()) {
id_symbol = args[5].As<Symbol>();
} else {
id_symbol = Symbol::New(isolate, url);
}
host_defined_options->Set(isolate, HostDefinedOptions::kID, id_symbol);

Local<PrimitiveArray> host_defined_options =
PrimitiveArray::New(isolate, HostDefinedOptions::kLength);
Local<Symbol> id_symbol = Symbol::New(isolate, url);
host_defined_options->Set(isolate, HostDefinedOptions::kID, id_symbol);
if (that->SetPrivate(context,
realm->isolate_data()->host_defined_option_symbol(),
id_symbol)
.IsNothing()) {
return;
}
}

ShouldNotAbortOnUncaughtScope no_abort_scope(realm->env());
TryCatchScope try_catch(realm->env());
Expand Down Expand Up @@ -215,8 +231,7 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
isolate, url, span, SyntheticModuleEvaluationStepsCallback);
} else {
ScriptCompiler::CachedData* cached_data = nullptr;
if (!args[5]->IsUndefined()) {
CHECK(args[5]->IsArrayBufferView());
if (args[5]->IsArrayBufferView()) {
Local<ArrayBufferView> cached_data_buf = args[5].As<ArrayBufferView>();
uint8_t* data =
static_cast<uint8_t*>(cached_data_buf->Buffer()->Data());
Expand Down Expand Up @@ -279,13 +294,6 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
return;
}

if (that->SetPrivate(context,
realm->isolate_data()->host_defined_option_symbol(),
id_symbol)
.IsNothing()) {
return;
}

// Use the extras object as an object whose GetCreationContext() will be the
// original `context`, since the `Context` itself strictly speaking cannot
// be stored in an internal field.
Expand Down Expand Up @@ -878,7 +886,7 @@ void ModuleWrap::HostInitializeImportMetaObjectCallback(
return;
}
DCHECK(id->IsSymbol());
Local<Value> args[] = {id, meta};
Local<Value> args[] = {id, meta, wrap};
TryCatchScope try_catch(env);
USE(callback->Call(
context, Undefined(realm->isolate()), arraysize(args), args));
Expand Down

0 comments on commit d6b57f6

Please sign in to comment.