Skip to content

Commit 4df8b35

Browse files
authored
feat(unstable): support TC39 import defer proposal (#32360)
This commit adds an experimental support for `import defer ...` syntax, for https://github.com/tc39/proposal-defer-import-eval proposal. Closes #30053
1 parent 54a12b8 commit 4df8b35

19 files changed

Lines changed: 232 additions & 8 deletions

File tree

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ repository = "https://github.com/denoland/deno"
8989
[workspace.dependencies]
9090
deno_ast = { version = "=0.53.1", features = ["transpiling"] }
9191
deno_core_icudata = "0.77.0"
92-
deno_doc = "=0.197.0"
92+
deno_doc = "=0.198.0"
9393
deno_error = "=0.7.1"
94-
deno_graph = { version = "=0.107.1", default-features = false }
94+
deno_graph = { version = "=0.107.2", default-features = false }
9595
deno_lint = "=0.83.0"
9696
deno_media_type = { version = "=0.4.0", features = ["module_specifier"] }
9797
deno_native_certs = "0.3.0"

cli/tools/publish/unfurl.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,7 @@ impl<TSys: SpecifierUnfurlerSys> SpecifierUnfurler<TSys> {
918918
match dep.kind {
919919
StaticDependencyKind::Export
920920
| StaticDependencyKind::Import
921+
| StaticDependencyKind::ImportDefer
921922
| StaticDependencyKind::ImportSource
922923
| StaticDependencyKind::ExportEquals
923924
| StaticDependencyKind::ImportEquals => {

cli/tsc/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,10 @@ pub static IGNORED_DIAGNOSTIC_CODES: LazyLock<HashSet<u64>> =
12791279
// implicitly has an 'any' type. This is due to `allowJs` being off by
12801280
// default but importing of a JavaScript module.
12811281
7016,
1282+
// TS18060: Deferred imports are only supported when the '--module' flag
1283+
// is set to 'esnext' or 'preserve'. Deno uses its own module resolution
1284+
// and supports import defer natively.
1285+
18060,
12821286
]
12831287
.into_iter()
12841288
.collect()

libs/core/modules/map.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2018,7 +2018,7 @@ impl ModuleMap {
20182018
.unwrap()
20192019
.clone();
20202020
match state.phase {
2021-
ModuleImportPhase::Defer | ModuleImportPhase::Evaluation => {
2021+
ModuleImportPhase::Evaluation => {
20222022
let module_id =
20232023
load.root_module_id().expect("Root module should be loaded");
20242024
let result = self.instantiate_module(scope, module_id);
@@ -2032,6 +2032,95 @@ impl ModuleMap {
20322032
state,
20332033
)?;
20342034
}
2035+
ModuleImportPhase::Defer => {
2036+
// For defer phase imports, the module is instantiated but NOT
2037+
// eagerly evaluated. We call evaluate_for_import_defer which
2038+
// gathers and evaluates async transitive dependencies, then
2039+
// resolve with a deferred namespace that triggers evaluation
2040+
// on first property access.
2041+
let module_id =
2042+
load.root_module_id().expect("Root module should be loaded");
2043+
let result = self.instantiate_module(scope, module_id);
2044+
if let Err(exception) = result {
2045+
self.dynamic_import_reject(scope, dyn_import_id, exception);
2046+
continue;
2047+
}
2048+
let module_handle =
2049+
self.get_handle(module_id).expect("ModuleInfo not found");
2050+
2051+
v8::tc_scope!(let tc_scope, scope);
2052+
2053+
let cped = v8::Local::new(tc_scope, state.cped.clone());
2054+
tc_scope.set_continuation_preserved_embedder_data(cped);
2055+
2056+
let module = v8::Local::new(tc_scope, &module_handle);
2057+
2058+
// Gather async transitive dependencies. Returns a promise
2059+
// that resolves when all async deps are ready.
2060+
let maybe_promise = module.evaluate_for_import_defer(tc_scope);
2061+
2062+
let Some(promise_val) = maybe_promise else {
2063+
let exception = tc_scope.exception().unwrap();
2064+
let exception = v8::Global::new(tc_scope, exception);
2065+
self.dynamic_import_reject(tc_scope, dyn_import_id, exception);
2066+
continue;
2067+
};
2068+
2069+
// Get the deferred namespace — this triggers evaluation on
2070+
// first property access.
2071+
let module_namespace = module
2072+
.get_module_namespace_with_phase(v8::ModuleImportPhase::kDefer);
2073+
2074+
let promise = v8::Local::<v8::Promise>::try_from(promise_val)
2075+
.expect("evaluate_for_import_defer should return a promise");
2076+
2077+
match promise.state() {
2078+
v8::PromiseState::Fulfilled => {
2079+
// All async deps are ready, resolve immediately.
2080+
let resolver_handle = self
2081+
.dynamic_import_map
2082+
.borrow_mut()
2083+
.remove(&dyn_import_id)
2084+
.expect("Invalid dynamic import id")
2085+
.resolver;
2086+
let resolver = resolver_handle.open(tc_scope);
2087+
resolver.resolve(tc_scope, module_namespace).unwrap();
2088+
tc_scope.perform_microtask_checkpoint();
2089+
}
2090+
v8::PromiseState::Rejected => {
2091+
let err = promise.result(tc_scope);
2092+
let err = v8::Global::new(tc_scope, err);
2093+
self.dynamic_import_reject(tc_scope, dyn_import_id, err);
2094+
}
2095+
v8::PromiseState::Pending => {
2096+
// Async deps still loading. Store for later resolution.
2097+
// The module_waker will wake us when the promise settles.
2098+
fn wake_module(
2099+
scope: &mut v8::PinScope<'_, '_>,
2100+
_args: v8::FunctionCallbackArguments<'_>,
2101+
_rv: v8::ReturnValue,
2102+
) {
2103+
let module_map = JsRealm::module_map_from(scope);
2104+
module_map.module_waker.wake();
2105+
}
2106+
2107+
let wake_module_cb =
2108+
v8::Function::builder(wake_module).build(tc_scope);
2109+
if let Some(wake_module_cb) = wake_module_cb {
2110+
promise.then2(tc_scope, wake_module_cb, wake_module_cb);
2111+
}
2112+
2113+
let dyn_import_mod_evaluate = DynImportModEvaluate {
2114+
load_id: dyn_import_id,
2115+
module_id,
2116+
promise: v8::Global::new(tc_scope, promise),
2117+
};
2118+
self
2119+
.pending_dyn_mod_evaluations
2120+
.push(dyn_import_mod_evaluate);
2121+
}
2122+
}
2123+
}
20352124
ModuleImportPhase::Source => {
20362125
let module_reference = load.root_module_reference().expect(
20372126
"Root module reference had to have been resolved to get here.",

libs/core/runtime/setup.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ fn v8_init(
154154
" --harmony-temporal",
155155
" --js-float16array",
156156
" --js-explicit-resource-management",
157-
" --js-source-phase-imports"
157+
" --js-source-phase-imports",
158+
" --js-defer-import-eval"
158159
);
159160
let snapshot_flags = "--predictable --random-seed=42";
160161
let expose_natives_flags = "--expose_gc --allow_natives_syntax";
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
console.log("deferred module evaluated");
3+
export const value = 42;
4+
export function add(a, b) {
5+
return a + b;
6+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
console.log("deferred2 module evaluated");
3+
export const value = 99;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
3+
// Test for TC39 proposal: Deferred Module Evaluation
4+
// https://github.com/tc39/proposal-defer-import-eval
5+
//
6+
// The `import defer` syntax allows loading a module without immediately
7+
// executing it. The module is executed synchronously when any property
8+
// on the namespace is first accessed.
9+
//
10+
console.log("before import defer");
11+
12+
// Static import defer syntax - module is loaded but not executed
13+
import defer * as deferred from "./deferred.js";
14+
15+
console.log("after import defer, before access");
16+
17+
// First property access triggers module evaluation
18+
console.log(`value: ${deferred.value}`);
19+
20+
console.log("after first access");
21+
22+
// Subsequent accesses use the already-evaluated module
23+
console.log(`add: ${deferred.add(1, 2)}`);
24+
25+
console.log("after first access, before dynamic import defer");
26+
27+
// Dynamic import.defer syntax
28+
const deferred2 = await import.defer("./deferred2.js");
29+
30+
console.log("after dynamic import defer, before access");
31+
32+
// First property access triggers evaluation
33+
console.log(`deferred2 value: ${deferred2.value}`);
34+
35+
console.log("done");
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
before import defer
2+
after import defer, before access
3+
deferred module evaluated
4+
value: 42
5+
after first access
6+
add: 3
7+
after first access, before dynamic import defer
8+
after dynamic import defer, before access
9+
deferred2 module evaluated
10+
deferred2 value: 99
11+
done

0 commit comments

Comments
 (0)