Skip to content
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
32 changes: 32 additions & 0 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,37 @@ a second argument:
* `parent` {string|URL} An optional absolute parent module URL to resolve from.
**Default:** `import.meta.url`

### `import.meta.sync(specifier)`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development

* `specifier` {string} The module specifier to synchronously import.
* Returns: {Object} The module namespace object.

`import.meta.sync()` provides a way to synchronously import ES modules,
offering feature parity with `require()` for conditional synchronous imports
in ES modules. This is particularly useful when migrating from CommonJS to ES modules
while maintaining the ability to conditionally load dependencies in synchronous code paths.

The primary use case is conditional synchronous importing:

```js
let mod;
try {
mod = import.meta.sync('./module-a.js');
} catch {
mod = import.meta.sync('./module-b.js');
}
```

If the imported module or any of its
dependencies use top-level `await`, `import.meta.sync()` will throw an
[`ERR_REQUIRE_ASYNC_MODULE`][] error.

## Interoperability with CommonJS

### `import` statements
Expand Down Expand Up @@ -1304,6 +1335,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
[`"exports"`]: packages.md#exports
[`"type"`]: packages.md#type
[`--input-type`]: cli.md#--input-typetype
[`ERR_REQUIRE_ASYNC_MODULE`]: #err_require_async_module
[`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
[`import()`]: #import-expressions
Expand Down
38 changes: 35 additions & 3 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const {
} = require('internal/errors').codes;
const { getOptionValue } = require('internal/options');
const { isURL, pathToFileURL } = require('internal/url');
const { kEmptyObject } = require('internal/util');
const { kEmptyObject, emitExperimentalWarning } = require('internal/util');
const {
compileSourceTextModule,
getDefaultConditions,
Expand All @@ -52,6 +52,7 @@ const {
kSourcePhase,
throwIfPromiseRejected,
setImportMetaResolveInitializer,
setImportMetaSyncInitializer,
} = internalBinding('module_wrap');
const {
urlToFilename,
Expand Down Expand Up @@ -908,6 +909,36 @@ function createImportMetaResolve(loader, moduleURL) {
};
}

/**
* @param {ModuleLoader} loader The cascaded loader.
* @param {string} moduleURL URL of the module accessing import.meta
* @returns {function(string, ImportAttributes|undefined): ModuleNamespace} The import.meta.sync function
*/
function createImportMetaSync(loader, moduleURL) {
/**
* @param {string} specifier The module specifier to synchronously import.
* @param {Record<string, any>} [importAttributes] Optional import attributes object. Accepts
* the same shape as dynamic import(): { with: { type: 'json' } } etc.
* @returns {ModuleNamespace} The module namespace object
*/
return function sync(specifier, importAttributes = kEmptyObject) {
emitExperimentalWarning('import.meta.sync');
let normalizedAttributes = importAttributes;
if (importAttributes && typeof importAttributes === 'object' && importAttributes.with) {
const withBag = importAttributes.with;
if (withBag && typeof withBag === 'object') {
normalizedAttributes = { ...withBag, __proto__: null };
}
}
const request = { specifier, phase: kEvaluationPhase, attributes: normalizedAttributes, __proto__: null };
const job = loader.getOrCreateModuleJob(moduleURL, request, kImportInImportedESM);

// This will throw if the module has TLA or is async
const result = job.runSync();
return result.namespace;
};
}

let cascadedLoader;
/**
* This is a singleton ESM loader that integrates the loader hooks, if any.
Expand All @@ -924,12 +955,13 @@ let cascadedLoader;
function getOrInitializeCascadedLoader(asyncLoaderHooks) {
if (!cascadedLoader) {
cascadedLoader = createModuleLoader(asyncLoaderHooks);
// import.meta.resolve is not allowed in the async loader hook worker thread.
// So only set up the import.meta.resolve initializer when we are initializing
// import.meta.resolve and import.meta.sync are not allowed in the async loader hook worker thread.
// So only set up the initializers when we are initializing
// the non-loader-hook-thread cascaded loader. When the native land doesn't see it,
// it knows the loader is running on the loader hook thread.
if (!(asyncLoaderHooks?.isForAsyncLoaderHookWorker)) {
setImportMetaResolveInitializer(createImportMetaResolve.bind(null, cascadedLoader));
setImportMetaSyncInitializer(createImportMetaSync.bind(null, cascadedLoader));
}
}
return cascadedLoader;
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@
V(get_source_map_error_source, v8::Function) \
V(host_import_module_dynamically_callback, v8::Function) \
V(host_import_meta_resolve_initializer, v8::Function) \
V(host_import_meta_sync_initializer, v8::Function) \
V(host_initialize_import_meta_object_callback, v8::Function) \
V(http2session_on_altsvc_function, v8::Function) \
V(http2session_on_error_function, v8::Function) \
Expand Down
65 changes: 65 additions & 0 deletions src/module_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,18 @@ void ModuleWrap::SetImportMetaResolveInitializer(
realm->set_host_import_meta_resolve_initializer(initializer);
}

void ModuleWrap::SetImportMetaSyncInitializer(
const v8::FunctionCallbackInfo<v8::Value>& args) {
Isolate* isolate = args.GetIsolate();
Realm* realm = Realm::GetCurrent(args);
HandleScope handle_scope(isolate);

CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsFunction());
Local<Function> initializer = args[0].As<Function>();
realm->set_host_import_meta_sync_initializer(initializer);
}

static void ImportMetaResolveLazyGetter(
Local<v8::Name> name, const PropertyCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
Expand Down Expand Up @@ -1256,6 +1268,42 @@ static void ImportMetaResolveLazyGetter(
info.GetReturnValue().Set(ret);
}

static void ImportMetaSyncLazyGetter(Local<v8::Name> name,
const PropertyCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
Local<Value> receiver_val = info.This();
if (!receiver_val->IsObject()) {
THROW_ERR_INVALID_INVOCATION(isolate);
return;
}
Local<Object> receiver = receiver_val.As<Object>();
Local<Context> context;
if (!receiver->GetCreationContext().ToLocal(&context)) {
THROW_ERR_INVALID_INVOCATION(isolate);
return;
}
Realm* realm = Realm::GetCurrent(context);
if (realm == nullptr) {
THROW_ERR_INVALID_INVOCATION(isolate);
}
Local<Function> initializer = realm->host_import_meta_sync_initializer();
if (initializer.IsEmpty()) {
THROW_ERR_INVALID_INVOCATION(isolate);
return;
}

// This should be createImportMetaSync(). The loader argument is already
// bound at initialization time.
Local<Value> args[] = {info.Data()};
Local<Value> ret;
if (!initializer
->Call(context, Undefined(realm->isolate()), arraysize(args), args)
.ToLocal(&ret)) {
return;
}
info.GetReturnValue().Set(ret);
}

static void PathHelpersLazyGetter(Local<v8::Name> name,
const PropertyCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
Expand Down Expand Up @@ -1361,6 +1409,18 @@ static Maybe<void> DefaultImportMetaObjectInitializer(Realm* realm,
return Nothing<void>();
}

// Set a lazy getter of import.meta.sync
Local<Function> import_meta_sync_initializer =
realm->host_import_meta_sync_initializer();
if (!import_meta_sync_initializer.IsEmpty() &&
meta->SetLazyDataProperty(context,
FIXED_ONE_BYTE_STRING(isolate, "sync"),
ImportMetaSyncLazyGetter,
url)
.IsNothing()) {
return Nothing<void>();
}

// Set import.meta.url = moduleWrap.url
if (meta->Set(context, env->url_string(), url).IsEmpty()) {
return Nothing<void>();
Expand Down Expand Up @@ -1629,6 +1689,10 @@ void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data,
target,
"setImportMetaResolveInitializer",
SetImportMetaResolveInitializer);
SetMethod(isolate,
target,
"setImportMetaSyncInitializer",
SetImportMetaSyncInitializer);
SetMethod(isolate,
target,
"createRequiredModuleFacade",
Expand Down Expand Up @@ -1683,6 +1747,7 @@ void ModuleWrap::RegisterExternalReferences(
registry->Register(SetImportModuleDynamicallyCallback);
registry->Register(SetInitializeImportMetaObjectCallback);
registry->Register(SetImportMetaResolveInitializer);
registry->Register(SetImportMetaSyncInitializer);
registry->Register(ThrowIfPromiseRejected);
}
} // namespace loader
Expand Down
2 changes: 2 additions & 0 deletions src/module_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ class ModuleWrap : public BaseObject {
const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetImportMetaResolveInitializer(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetImportMetaSyncInitializer(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetInitializeImportMetaObjectCallback(
const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::MaybeLocal<v8::Value> SyntheticModuleEvaluationStepsCallback(
Expand Down
Loading