diff --git a/Cargo.lock b/Cargo.lock index 22650a0756303c..ba7fbb98713b4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2292,9 +2292,9 @@ dependencies = [ [[package]] name = "deno_doc" -version = "0.197.0" +version = "0.198.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72868ff531723780688c97c23a9b78d9a7790ee329b1869deba3ad0f121a1c27" +checksum = "2d44b93e82f85f644b9e8595d0fc0d9beeae5ed636d95d54b1edfcf30f8f3399" dependencies = [ "anyhow", "cfg-if", @@ -2454,9 +2454,9 @@ dependencies = [ [[package]] name = "deno_graph" -version = "0.107.1" +version = "0.107.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06518571e56fef9ae4441b93445754f0b3c608404607b8143541ab672e589bf6" +checksum = "0f8f640f8f0fa2af60bc1c7ed82d6e6f9f658a5869473deaa77cb5b470b77fec" dependencies = [ "async-trait", "boxed_error", diff --git a/Cargo.toml b/Cargo.toml index 8ec33029b586b7..5481989fee4cd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,9 +89,9 @@ repository = "https://github.com/denoland/deno" [workspace.dependencies] deno_ast = { version = "=0.53.1", features = ["transpiling"] } deno_core_icudata = "0.77.0" -deno_doc = "=0.197.0" +deno_doc = "=0.198.0" deno_error = "=0.7.1" -deno_graph = { version = "=0.107.1", default-features = false } +deno_graph = { version = "=0.107.2", default-features = false } deno_lint = "=0.83.0" deno_media_type = { version = "=0.4.0", features = ["module_specifier"] } deno_native_certs = "0.3.0" diff --git a/cli/tools/publish/unfurl.rs b/cli/tools/publish/unfurl.rs index d25cdb9760cdcf..7bfdd2dda5d2a9 100644 --- a/cli/tools/publish/unfurl.rs +++ b/cli/tools/publish/unfurl.rs @@ -918,6 +918,7 @@ impl SpecifierUnfurler { match dep.kind { StaticDependencyKind::Export | StaticDependencyKind::Import + | StaticDependencyKind::ImportDefer | StaticDependencyKind::ImportSource | StaticDependencyKind::ExportEquals | StaticDependencyKind::ImportEquals => { diff --git a/cli/tsc/mod.rs b/cli/tsc/mod.rs index 9ff8730dd6a1f8..67c9fb8cebec02 100644 --- a/cli/tsc/mod.rs +++ b/cli/tsc/mod.rs @@ -1279,6 +1279,10 @@ pub static IGNORED_DIAGNOSTIC_CODES: LazyLock> = // implicitly has an 'any' type. This is due to `allowJs` being off by // default but importing of a JavaScript module. 7016, + // TS18060: Deferred imports are only supported when the '--module' flag + // is set to 'esnext' or 'preserve'. Deno uses its own module resolution + // and supports import defer natively. + 18060, ] .into_iter() .collect() diff --git a/libs/core/modules/map.rs b/libs/core/modules/map.rs index 447bd9851ee6ab..ea3d5d1765d1fc 100644 --- a/libs/core/modules/map.rs +++ b/libs/core/modules/map.rs @@ -2018,7 +2018,7 @@ impl ModuleMap { .unwrap() .clone(); match state.phase { - ModuleImportPhase::Defer | ModuleImportPhase::Evaluation => { + ModuleImportPhase::Evaluation => { let module_id = load.root_module_id().expect("Root module should be loaded"); let result = self.instantiate_module(scope, module_id); @@ -2032,6 +2032,95 @@ impl ModuleMap { state, )?; } + ModuleImportPhase::Defer => { + // For defer phase imports, the module is instantiated but NOT + // eagerly evaluated. We call evaluate_for_import_defer which + // gathers and evaluates async transitive dependencies, then + // resolve with a deferred namespace that triggers evaluation + // on first property access. + let module_id = + load.root_module_id().expect("Root module should be loaded"); + let result = self.instantiate_module(scope, module_id); + if let Err(exception) = result { + self.dynamic_import_reject(scope, dyn_import_id, exception); + continue; + } + let module_handle = + self.get_handle(module_id).expect("ModuleInfo not found"); + + v8::tc_scope!(let tc_scope, scope); + + let cped = v8::Local::new(tc_scope, state.cped.clone()); + tc_scope.set_continuation_preserved_embedder_data(cped); + + let module = v8::Local::new(tc_scope, &module_handle); + + // Gather async transitive dependencies. Returns a promise + // that resolves when all async deps are ready. + let maybe_promise = module.evaluate_for_import_defer(tc_scope); + + let Some(promise_val) = maybe_promise else { + let exception = tc_scope.exception().unwrap(); + let exception = v8::Global::new(tc_scope, exception); + self.dynamic_import_reject(tc_scope, dyn_import_id, exception); + continue; + }; + + // Get the deferred namespace — this triggers evaluation on + // first property access. + let module_namespace = module + .get_module_namespace_with_phase(v8::ModuleImportPhase::kDefer); + + let promise = v8::Local::::try_from(promise_val) + .expect("evaluate_for_import_defer should return a promise"); + + match promise.state() { + v8::PromiseState::Fulfilled => { + // All async deps are ready, resolve immediately. + let resolver_handle = self + .dynamic_import_map + .borrow_mut() + .remove(&dyn_import_id) + .expect("Invalid dynamic import id") + .resolver; + let resolver = resolver_handle.open(tc_scope); + resolver.resolve(tc_scope, module_namespace).unwrap(); + tc_scope.perform_microtask_checkpoint(); + } + v8::PromiseState::Rejected => { + let err = promise.result(tc_scope); + let err = v8::Global::new(tc_scope, err); + self.dynamic_import_reject(tc_scope, dyn_import_id, err); + } + v8::PromiseState::Pending => { + // Async deps still loading. Store for later resolution. + // The module_waker will wake us when the promise settles. + fn wake_module( + scope: &mut v8::PinScope<'_, '_>, + _args: v8::FunctionCallbackArguments<'_>, + _rv: v8::ReturnValue, + ) { + let module_map = JsRealm::module_map_from(scope); + module_map.module_waker.wake(); + } + + let wake_module_cb = + v8::Function::builder(wake_module).build(tc_scope); + if let Some(wake_module_cb) = wake_module_cb { + promise.then2(tc_scope, wake_module_cb, wake_module_cb); + } + + let dyn_import_mod_evaluate = DynImportModEvaluate { + load_id: dyn_import_id, + module_id, + promise: v8::Global::new(tc_scope, promise), + }; + self + .pending_dyn_mod_evaluations + .push(dyn_import_mod_evaluate); + } + } + } ModuleImportPhase::Source => { let module_reference = load.root_module_reference().expect( "Root module reference had to have been resolved to get here.", diff --git a/libs/core/runtime/setup.rs b/libs/core/runtime/setup.rs index 5b99453ef52502..0dc35a29ffd354 100644 --- a/libs/core/runtime/setup.rs +++ b/libs/core/runtime/setup.rs @@ -154,7 +154,8 @@ fn v8_init( " --harmony-temporal", " --js-float16array", " --js-explicit-resource-management", - " --js-source-phase-imports" + " --js-source-phase-imports", + " --js-defer-import-eval" ); let snapshot_flags = "--predictable --random-seed=42"; let expose_natives_flags = "--expose_gc --allow_natives_syntax"; diff --git a/libs/core_testing/integration/import_defer/deferred.js b/libs/core_testing/integration/import_defer/deferred.js new file mode 100644 index 00000000000000..5f799e65960ce7 --- /dev/null +++ b/libs/core_testing/integration/import_defer/deferred.js @@ -0,0 +1,6 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +console.log("deferred module evaluated"); +export const value = 42; +export function add(a, b) { + return a + b; +} diff --git a/libs/core_testing/integration/import_defer/deferred2.js b/libs/core_testing/integration/import_defer/deferred2.js new file mode 100644 index 00000000000000..58db69f69b5b57 --- /dev/null +++ b/libs/core_testing/integration/import_defer/deferred2.js @@ -0,0 +1,3 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +console.log("deferred2 module evaluated"); +export const value = 99; diff --git a/libs/core_testing/integration/import_defer/import_defer.js b/libs/core_testing/integration/import_defer/import_defer.js new file mode 100644 index 00000000000000..ebb52dd99298cf --- /dev/null +++ b/libs/core_testing/integration/import_defer/import_defer.js @@ -0,0 +1,35 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +// Test for TC39 proposal: Deferred Module Evaluation +// https://github.com/tc39/proposal-defer-import-eval +// +// The `import defer` syntax allows loading a module without immediately +// executing it. The module is executed synchronously when any property +// on the namespace is first accessed. +// +console.log("before import defer"); + +// Static import defer syntax - module is loaded but not executed +import defer * as deferred from "./deferred.js"; + +console.log("after import defer, before access"); + +// First property access triggers module evaluation +console.log(`value: ${deferred.value}`); + +console.log("after first access"); + +// Subsequent accesses use the already-evaluated module +console.log(`add: ${deferred.add(1, 2)}`); + +console.log("after first access, before dynamic import defer"); + +// Dynamic import.defer syntax +const deferred2 = await import.defer("./deferred2.js"); + +console.log("after dynamic import defer, before access"); + +// First property access triggers evaluation +console.log(`deferred2 value: ${deferred2.value}`); + +console.log("done"); diff --git a/libs/core_testing/integration/import_defer/import_defer.out b/libs/core_testing/integration/import_defer/import_defer.out new file mode 100644 index 00000000000000..94921b707b2e50 --- /dev/null +++ b/libs/core_testing/integration/import_defer/import_defer.out @@ -0,0 +1,11 @@ +before import defer +after import defer, before access +deferred module evaluated +value: 42 +after first access +add: 3 +after first access, before dynamic import defer +after dynamic import defer, before access +deferred2 module evaluated +deferred2 value: 99 +done diff --git a/libs/core_testing/lib.rs b/libs/core_testing/lib.rs index 6b0a96f57b2dc1..f17af37b45aeb0 100644 --- a/libs/core_testing/lib.rs +++ b/libs/core_testing/lib.rs @@ -70,6 +70,7 @@ integration_test!( dyn_import_op, dyn_import_no_hang, dyn_import_pending_tla, + import_defer, error_async_stack, error_callsite, error_non_existent_eval_source, diff --git a/tests/specs/run/import_defer/__test__.jsonc b/tests/specs/run/import_defer/__test__.jsonc new file mode 100644 index 00000000000000..9431e889292c5b --- /dev/null +++ b/tests/specs/run/import_defer/__test__.jsonc @@ -0,0 +1,20 @@ +{ + "tests": { + "static_defer": { + "args": "run --allow-read main.js", + "output": "main.out" + }, + "dynamic_defer": { + "args": "run --allow-read dynamic.js", + "output": "dynamic.out" + }, + "static_defer_ts": { + "args": "run --allow-read main_ts.ts", + "output": "main.out" + }, + "check_defer_ts": { + "args": "check main_ts.ts", + "output": "check.out" + } + } +} diff --git a/tests/specs/run/import_defer/check.out b/tests/specs/run/import_defer/check.out new file mode 100644 index 00000000000000..fce3b7fe72143d --- /dev/null +++ b/tests/specs/run/import_defer/check.out @@ -0,0 +1 @@ +Check [WILDCARD]main_ts.ts diff --git a/tests/specs/run/import_defer/deferred.js b/tests/specs/run/import_defer/deferred.js new file mode 100644 index 00000000000000..2d7671f1ba6f80 --- /dev/null +++ b/tests/specs/run/import_defer/deferred.js @@ -0,0 +1,5 @@ +console.log("deferred module evaluated"); +export const value = 42; +export function add(a, b) { + return a + b; +} diff --git a/tests/specs/run/import_defer/dynamic.js b/tests/specs/run/import_defer/dynamic.js new file mode 100644 index 00000000000000..589f99a637ce24 --- /dev/null +++ b/tests/specs/run/import_defer/dynamic.js @@ -0,0 +1,14 @@ +console.log("before dynamic import defer"); + +const deferred = await import.defer("./deferred.js"); + +console.log("after dynamic import defer, before access"); + +// First property access triggers module evaluation +console.log(`value: ${deferred.value}`); + +console.log("after first access"); + +console.log(`add: ${deferred.add(1, 2)}`); + +console.log("done"); diff --git a/tests/specs/run/import_defer/dynamic.out b/tests/specs/run/import_defer/dynamic.out new file mode 100644 index 00000000000000..14f0b0524337ff --- /dev/null +++ b/tests/specs/run/import_defer/dynamic.out @@ -0,0 +1,7 @@ +before dynamic import defer +after dynamic import defer, before access +deferred module evaluated +value: 42 +after first access +add: 3 +done diff --git a/tests/specs/run/import_defer/main.js b/tests/specs/run/import_defer/main.js new file mode 100644 index 00000000000000..a3155bb42c6736 --- /dev/null +++ b/tests/specs/run/import_defer/main.js @@ -0,0 +1,13 @@ +import defer * as deferred from "./deferred.js"; + +console.log("before access"); + +// First property access triggers module evaluation +console.log(`value: ${deferred.value}`); + +console.log("after first access"); + +// Subsequent accesses use the already-evaluated module +console.log(`add: ${deferred.add(1, 2)}`); + +console.log("done"); diff --git a/tests/specs/run/import_defer/main.out b/tests/specs/run/import_defer/main.out new file mode 100644 index 00000000000000..e6700a8b070283 --- /dev/null +++ b/tests/specs/run/import_defer/main.out @@ -0,0 +1,6 @@ +before access +deferred module evaluated +value: 42 +after first access +add: 3 +done diff --git a/tests/specs/run/import_defer/main_ts.ts b/tests/specs/run/import_defer/main_ts.ts new file mode 100644 index 00000000000000..29160c7b860481 --- /dev/null +++ b/tests/specs/run/import_defer/main_ts.ts @@ -0,0 +1,7 @@ +import defer * as deferred from "./deferred.js"; + +console.log("before access"); +console.log(`value: ${deferred.value}`); +console.log("after first access"); +console.log(`add: ${deferred.add(1, 2)}`); +console.log("done");