Skip to content

feat(unstable): support TC39 import defer proposal - #32360

Merged
bartlomieju merged 12 commits into
denoland:mainfrom
bartlomieju:feat/import-defer-eval
Apr 29, 2026
Merged

bartlomieju merged 12 commits into
denoland:mainfrom
bartlomieju:feat/import-defer-eval

Conversation

@bartlomieju

@bartlomieju bartlomieju commented Feb 27, 2026 •

Copy link
Copy Markdown
Member

This commit adds an experimental support for import defer ... syntax,
for https://github.com/tc39/proposal-defer-import-eval proposal.

Closes #30053

Adds support for the TC39 "Deferred Module Evaluation" proposal
(https://github.com/tc39/proposal-defer-import-eval, Stage 3).

Changes:
- Enable V8 `--js-defer-import-eval` flag for parser/runtime support
- Separate `ModuleImportPhase::Defer` handling from `Evaluation` phase:
  deferred modules are instantiated but not evaluated, letting V8 create
  a deferred namespace that triggers evaluation on first property access
- Add integration test validating deferred evaluation semantics

Ported from denoland/deno_core#1280

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bartlomieju bartlomieju changed the title feat(core): support TC39 import defer proposal feat(unstable): support TC39 import defer proposal Mar 3, 2026
@bartlomieju

Copy link
Copy Markdown
Member Author

Waiting on v8/v8@f2016e1 in V8 14.7.

@bartlomieju bartlomieju added the upstream Changes in upstream are required to solve these issues label Mar 3, 2026
@bartlomieju bartlomieju added this to the 2.8.0 milestone Apr 17, 2026
Upgrade rusty_v8 to 147.3.0 which includes the
Module::EvaluateForImportDefer and GetModuleNamespace with phase APIs
needed for dynamic import.defer() support.

Fix merge conflict in map.rs: restore separate Defer arm in dynamic
import handling (must NOT call dynamic_import_module_evaluate since
defer imports should not be evaluated until namespace access).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bartlomieju bartlomieju removed the upstream Changes in upstream are required to solve these issues label Apr 19, 2026
bartlomieju and others added 7 commits April 19, 2026 16:02
Point to local deno_graph (with ImportDefer variants) and deno_doc.
Add ImportDefer handling to unfurl.rs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use the new V8 Module::evaluate_for_import_defer API for dynamic
import.defer() to properly gather async transitive dependencies before
resolving. Get the deferred namespace via get_module_namespace_with_phase
(kDefer) so V8 creates a namespace that triggers evaluation on first
property access.

Handles three promise states:
- Fulfilled: all async deps ready, resolve immediately with deferred ns
- Rejected: propagate error to dynamic import reject
- Pending: store for later resolution via pending_dyn_mod_evaluations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Filter TS18060 diagnostic ("Deferred imports are only supported when
  module is esnext or preserve") since Deno handles module resolution
  natively and supports import defer.
- Add test for running import defer from TypeScript files.
- Add test for type-checking import defer TypeScript files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace local path overrides with published crate versions that
include import defer support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bartlomieju
bartlomieju merged commit 4df8b35 into denoland:main Apr 29, 2026
136 checks passed
@bartlomieju
bartlomieju deleted the feat/import-defer-eval branch April 29, 2026 07:39
Jarred-Sumner pushed a commit to oven-sh/bun that referenced this pull request May 19, 2026
Implements the static form of the Stage 3 [Deferred Module
Evaluation](https://github.com/tc39/proposal-defer-import-eval)
proposal:

```js
import defer * as ns from "./mod.js";
// mod.js is fetched and linked now, but NOT evaluated.

console.log("before");
console.log(ns.value);   // <-- mod.js evaluates here, synchronously
```

JavaScriptCore already carries the full runtime for this (landed
upstream in
[WebKit/WebKit#64053](WebKit/WebKit#64053) and
present in Bun's pinned WebKit `3167a44fb9`). This PR threads the
`ModulePhase::Defer` flag through Bun's own parser → printer →
`JSModuleRecord` bridge so JSC actually receives it, and flips on
`useImportDefer`.

> Supersedes #30015, which was written before the WebKit bump and gated
everything behind `#ifdef JSC_HAS_IMPORT_DEFER` pending a separate
`oven-sh/WebKit` patch. That indirection is no longer needed. This PR is
also narrower in scope — it intentionally does **not** implement dynamic
`import.defer()`.

### What changed

**Parser / AST** — `src/js_parser/parse/parse_stmt.rs`,
`src/ast/{s,import_record}.rs`
- `defer` is a contextual phase keyword only when immediately followed
by `*`. In every other position (`import defer from "x"`, `import defer,
{a} from "x"`, `import { defer } from "x"`) it remains an ordinary
identifier.
- New `S::Import.phase_defer` and `ImportRecord::Flags::PHASE_DEFER`.

**Printer / ModuleInfo** — `src/js_printer/lib.rs`
- Emits `import defer` so JSC's own parser sees it during bytecode
generation.
- New `RecordKind::ImportInfoNamespaceDefer`; `requested_modules` now
dedups by `(specifier, phase)` — matching JSC's
`ModuleAnalyzer::appendRequestedModule` — and serialises a parallel `u8`
phase array. Transpiler-cache version bumped (20 → 21) for the format
change.
- `finalize()`'s local→indirect export rewrite intentionally ignores
deferred namespace imports so `import defer * as ns; export { ns }`
stays a **local** export, matching `ModuleAnalyzer::exportVariable`.

**JSC bridge** — `src/bundler/analyze_transpiled_module.rs`,
`src/bundler_jsc/analyze_jsc.rs`,
`src/jsc/bindings/BunAnalyzeTranspiledModule.cpp`
- `appendRequestedModule(..., ModulePhase::Defer)` and `addImportEntry({
Namespace, phase: Defer, ... })` for deferred requests.
- `dumpRecordInfo` includes phase so the `BUN_DEBUG` fallback-parse diff
stays in agreement.

**Runtime** — `src/jsc/bindings/ZigGlobalObject.cpp`
- `JSC::Options::useImportDefer() = true`.

**Unrelated build fix** — `src/jsc/bindings/wtf-bindings.cpp`
- `wtf/Int128.h` dropped its transitive `<cassert>` in the latest WebKit
bump, which was the only thing declaring `assert()` for
`uv__tty_make_raw()` in that unified-build unit. Added the include
directly.

### Tests (`test/js/bun/resolve/import-defer.test.ts`, 16 cases)

- Evaluation deferred until first non-symbol property access; no re-eval
on subsequent access
- `ns[Symbol.toStringTag] === "Deferred Module"` without triggering
evaluation
- `in` / `Object.keys` trigger evaluation
- Throwing module re-throws the same error on every access
- Same specifier imported at both Evaluation and Defer phase
- Transitive TLA dep evaluated eagerly
(`GatherAsynchronousTransitiveDependencies`) so on-access eval stays
synchronous
- `import defer * as ns; export { ns }` exports the deferred namespace
as a local binding
- `import defer * as d from "./x.json" with { type: "json" }`
- Works in `.ts`
- `defer` still a valid identifier (`import defer from`, `import defer,
{x} from`, `import { defer } from`)
- `Bun.Transpiler` round-trips `import defer`
- `import defer { x } from` / `import defer x from` are syntax errors

### Out of scope

- Dynamic `import.defer(...)` (left for a follow-up; JSC's upstream PR
also excluded it).
- Bundler-side lazy-wrapping: when `bun build` inlines a deferred
dependency into the same chunk the defer semantics are lost (module body
runs at chunk load). External imports (`--external`) correctly preserve
`import defer` in the output.

### References

- Proposal: https://github.com/tc39/proposal-defer-import-eval
- Spec: https://tc39.es/proposal-defer-import-eval/
- WebKit runtime: WebKit/WebKit#64053
- Deno: denoland/deno#32360

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
liooil pushed a commit to liooil/poly that referenced this pull request Aug 7, 2026
Implements the static form of the Stage 3 [Deferred Module
Evaluation](https://github.com/tc39/proposal-defer-import-eval)
proposal:

```js
import defer * as ns from "./mod.js";
// mod.js is fetched and linked now, but NOT evaluated.

console.log("before");
console.log(ns.value);   // <-- mod.js evaluates here, synchronously
```

JavaScriptCore already carries the full runtime for this (landed
upstream in
[WebKit/WebKit#64053](WebKit/WebKit#64053) and
present in Bun's pinned WebKit `3167a44fb9`). This PR threads the
`ModulePhase::Defer` flag through Bun's own parser → printer →
`JSModuleRecord` bridge so JSC actually receives it, and flips on
`useImportDefer`.

> Supersedes #30015, which was written before the WebKit bump and gated
everything behind `#ifdef JSC_HAS_IMPORT_DEFER` pending a separate
`oven-sh/WebKit` patch. That indirection is no longer needed. This PR is
also narrower in scope — it intentionally does **not** implement dynamic
`import.defer()`.

### What changed

**Parser / AST** — `src/js_parser/parse/parse_stmt.rs`,
`src/ast/{s,import_record}.rs`
- `defer` is a contextual phase keyword only when immediately followed
by `*`. In every other position (`import defer from "x"`, `import defer,
{a} from "x"`, `import { defer } from "x"`) it remains an ordinary
identifier.
- New `S::Import.phase_defer` and `ImportRecord::Flags::PHASE_DEFER`.

**Printer / ModuleInfo** — `src/js_printer/lib.rs`
- Emits `import defer` so JSC's own parser sees it during bytecode
generation.
- New `RecordKind::ImportInfoNamespaceDefer`; `requested_modules` now
dedups by `(specifier, phase)` — matching JSC's
`ModuleAnalyzer::appendRequestedModule` — and serialises a parallel `u8`
phase array. Transpiler-cache version bumped (20 → 21) for the format
change.
- `finalize()`'s local→indirect export rewrite intentionally ignores
deferred namespace imports so `import defer * as ns; export { ns }`
stays a **local** export, matching `ModuleAnalyzer::exportVariable`.

**JSC bridge** — `src/bundler/analyze_transpiled_module.rs`,
`src/bundler_jsc/analyze_jsc.rs`,
`src/jsc/bindings/BunAnalyzeTranspiledModule.cpp`
- `appendRequestedModule(..., ModulePhase::Defer)` and `addImportEntry({
Namespace, phase: Defer, ... })` for deferred requests.
- `dumpRecordInfo` includes phase so the `BUN_DEBUG` fallback-parse diff
stays in agreement.

**Runtime** — `src/jsc/bindings/ZigGlobalObject.cpp`
- `JSC::Options::useImportDefer() = true`.

**Unrelated build fix** — `src/jsc/bindings/wtf-bindings.cpp`
- `wtf/Int128.h` dropped its transitive `<cassert>` in the latest WebKit
bump, which was the only thing declaring `assert()` for
`uv__tty_make_raw()` in that unified-build unit. Added the include
directly.

### Tests (`test/js/bun/resolve/import-defer.test.ts`, 16 cases)

- Evaluation deferred until first non-symbol property access; no re-eval
on subsequent access
- `ns[Symbol.toStringTag] === "Deferred Module"` without triggering
evaluation
- `in` / `Object.keys` trigger evaluation
- Throwing module re-throws the same error on every access
- Same specifier imported at both Evaluation and Defer phase
- Transitive TLA dep evaluated eagerly
(`GatherAsynchronousTransitiveDependencies`) so on-access eval stays
synchronous
- `import defer * as ns; export { ns }` exports the deferred namespace
as a local binding
- `import defer * as d from "./x.json" with { type: "json" }`
- Works in `.ts`
- `defer` still a valid identifier (`import defer from`, `import defer,
{x} from`, `import { defer } from`)
- `Bun.Transpiler` round-trips `import defer`
- `import defer { x } from` / `import defer x from` are syntax errors

### Out of scope

- Dynamic `import.defer(...)` (left for a follow-up; JSC's upstream PR
also excluded it).
- Bundler-side lazy-wrapping: when `bun build` inlines a deferred
dependency into the same chunk the defer semantics are lost (module body
runs at chunk load). External imports (`--external`) correctly preserve
`import defer` in the output.

### References

- Proposal: https://github.com/tc39/proposal-defer-import-eval
- Spec: https://tc39.es/proposal-defer-import-eval/
- WebKit runtime: WebKit/WebKit#64053
- Deno: denoland/deno#32360

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: implement import-defer

2 participants