Skip to content

module: enable existing machinery for deferred imports - #63712

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
MayaLekova:import-defer-01
Jun 16, 2026
Merged

module: enable existing machinery for deferred imports#63712
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
MayaLekova:import-defer-01

Conversation

@MayaLekova

Copy link
Copy Markdown
Contributor

This commit enables deferred module imports by using the corresponding functionality in V8. It also adds a single test, but more test coverage should be added.

Refs: https://github.com/tc39/proposal-defer-import-eval

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders
  • @nodejs/vm

@MayaLekova MayaLekova changed the title module: enable existing machinery for import defer module: enable existing machinery for deferred imports Jun 2, 2026
@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. labels Jun 2, 2026

@joyeecheung joyeecheung left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the commit message needs to clarify that this only makes it work for static import, but not dynamic import yet?

Comment thread test/fixtures/es-modules/module-deferred-eval.mjs
@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 90.35%. Comparing base (6813080) to head (6bb2db9).
⚠️ Report is 24 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main   #63712    +/-   ##
========================================
  Coverage   90.34%   90.35%            
========================================
  Files         732      732            
  Lines      236580   236709   +129     
  Branches    44545    44585    +40     
========================================
+ Hits       213748   213870   +122     
- Misses      14536    14565    +29     
+ Partials     8296     8274    -22     
Files with missing lines Coverage Ξ”
src/module_wrap.cc 74.41% <100.00%> (+0.04%) ⬆️
src/module_wrap.h 52.94% <ΓΈ> (ΓΈ)

... and 53 files with indirect coverage changes

πŸš€ New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • πŸ“¦ JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@MayaLekova

Copy link
Copy Markdown
Contributor Author

Thank you, I moved the test, but should I add more extensive test coverage, even for static imports?

@joyeecheung

joyeecheung commented Jun 4, 2026

Copy link
Copy Markdown
Member

I think a smoke test is enough. The rest should be covered by test262 in the upstream.

Can you rewrap the commit message to 72 chars to make the linter happy? Also this needs adding a eslint plugin

2026-06-03T13:01:48.8039114Z /home/runner/work/node/node/test/es-module/test-defer-import-eval.mjs
2026-06-03T13:01:48.8044324Z 10:7 error Parsing error: This experimental syntax requires enabling the parser plugin: "deferredImportEvaluation". (10:7)

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing!

@MayaLekova
MayaLekova force-pushed the import-defer-01 branch 3 times, most recently from 2915765 to bd9968c Compare June 8, 2026 15:08
Comment thread src/module_wrap.h

@guybedford guybedford left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works perfectly thank you!

@joyeecheung joyeecheung added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 9, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 9, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment thread test/es-module/test-defer-import-eval.mjs Outdated
Comment thread test/es-module/test-defer-import-with-module-tree.mjs Outdated
@MayaLekova

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews! Do you mind checking the updated version? (minor changes in tests only)
@joyeecheung @guybedford

@guybedford guybedford left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test looks good. It would be great to see a Wasm test too (import defer * as foo from './foo.wasm'), but that also isn't necessary either.

@legendecas legendecas added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 10, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 10, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

This commit enables deferred import for statically imported modules,
by using the corresponding functionality in V8. It adds two tests:
 - smoke test exercising the basic syntax
 - test for deferred import of module with its own imports

More test coverage should be added for mixing ESM and CJS modules,
as well as TypeScript modules.

Refs: https://github.com/tc39/proposal-defer-import-eval
Signed-off-by: Maya Lekova <maya@igalia.com>
@joyeecheung joyeecheung added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 13, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 13, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@guybedford guybedford added author ready PRs that have at least one approval, no outstanding review comments, and a CI started. commit-queue Add this label to land a pull request using GitHub Actions. labels Jun 16, 2026
@nodejs-github-bot nodejs-github-bot removed the commit-queue Add this label to land a pull request using GitHub Actions. label Jun 16, 2026
@nodejs-github-bot
nodejs-github-bot merged commit b09155d into nodejs:main Jun 16, 2026
73 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in b09155d

@MayaLekova

Copy link
Copy Markdown
Contributor Author

Test looks good. It would be great to see a Wasm test too (import defer * as foo from './foo.wasm'), but that also isn't necessary either.

Thank you for helping me land this PR!

I'll add more tests in the next commits, including some with Wasm modules.

aduh95 pushed a commit that referenced this pull request Jun 18, 2026
This commit enables deferred import for statically imported modules,
by using the corresponding functionality in V8. It adds two tests:
 - smoke test exercising the basic syntax
 - test for deferred import of module with its own imports

More test coverage should be added for mixing ESM and CJS modules,
as well as TypeScript modules.

Refs: https://github.com/tc39/proposal-defer-import-eval
Signed-off-by: Maya Lekova <maya@igalia.com>
PR-URL: #63712
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
@aduh95 aduh95 added the backport-requested-v24.x PRs awaiting manual backport to the v24.x-staging branch. label Aug 6, 2026
@aduh95

aduh95 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

This doesn't land cleanly on v24.x-staging, it would require a manual backport PR if we want it on Node.js 24

colinhacks added a commit to nubjs/nub that referenced this pull request Aug 20, 2026
* node: auto-unflag `import defer` on Node 26.4+ via an argv-only mitigation

`import defer * as ns from "m"` needs V8's `--js-defer-import-eval`. Node refuses
that flag in NODE_OPTIONS in both polarities and omits it from
`process.allowedNodeEnvironmentFlags`, so it cannot use the existing `Unflag`
shape: `compute_inject_flags` would drop it in the Stage-4 accepted-flag
intersection whenever that probe succeeds, and would feed it to the
NODE_OPTIONS-bound script-runner path whenever the probe fails.

Add a `Mitigation::UnflagArgv` shape for a V8 flag Node accepts only on the
command line, plus an `import-defer` row banded to 26.4+. The floor is a Node
floor, not a V8-flag one: V8 has carried the flag since 26.0, but Node only wired
the defer phase through in nodejs/node#63712, first released in 26.4.0. Verified
against real binaries β€” 25.9 rejects the flag outright, 26.0-26.3 accept it and
then abort in to_phase_constant, 26.4 and 26.5 defer correctly.

Injection happens at the argv-bearing call sites (spawn, watch, the compiled
launcher) alongside user `v8Flags`, never through NODE_OPTIONS. That is what keeps
it clear of the Electron snapshot-hash crash in #246: the flag does enter V8's
flag hash, but an argv flag reaches only the process nub spawns, never a
downstream Electron binary spawned by absolute path.

* docs: note that an absolute-path subprocess does not inherit the defer flag

Argv delivery does not propagate to a child spawned through Node's own
executable path. A child launched as `node` still gets it through the shim.
Verified with a spawnSync fixture on Node 26.5.

* node: add a removal backstop and an end-to-end test for the defer unflag

Addresses four review findings on #770.

An open-ended `UnflagArgv` band had no analog to `compute_inject_flags`' Stage-4
intersection, and could not reuse it: a command-line-only V8 flag is absent from
`process.allowedNodeEnvironmentFlags` by construction, so that probe reads false
even where the flag works. Without a backstop, V8 eventually removing the flag
would abort EVERY augmented invocation on that Node β€” an unknown `--js-*` is a
hard "bad option", verified β€” including from binaries already shipped. Add
`discovery::accepts_argv_flag`, which spawns the binary once per (binary, flag)
and caches the verdict by (path, mtime); `argv_inject_flags` now intersects
against it. Below the band's floor nothing is probed, so an out-of-band Node pays
no extra spawn. Verified by reporting the flag as unaccepted: the injection is
dropped and the program fails with a clean SyntaxError instead of aborting.

Record the dynamic form as an ACCEPTED ADDITIVITY EXCEPTION rather than charging
it upstream. Measured against the real user baseline (`node app.js`, no flag),
bare Node raises a catchable SyntaxError on `import.defer(spec)` while nub dies
on an uncatchable V8 fatal error. Saying it "reproduces on plain Node" was true
only of `node --js-defer-import-eval`, which no user runs. The docs callout now
states that delta plainly.

Strengthen the NODE_OPTIONS-leak test to assert against `compute_inject_flags`,
whose output actually becomes that payload. Comparing the two matrix accessors
was near-vacuous β€” they match disjoint variants β€” and missed routes that bypass
the table, `ALWAYS_INJECT` above all.

Add `import_defer_actually_defers_evaluation`, the only test covering the wiring
rather than the table: the previous tests would all pass with every
`argv_inject_flags` call site deleted. It asserts ORDERING, so a non-deferring
implementation fails, and uses a .ts entry importing a .ts module β€” the shape the
docs advertise, previously uncovered.

* node: hide nub's argv-only V8 flags from process.execArgv

Fixes a real regression this branch introduced: a Next.js 16 + Turbopack build
died under nub on Node 26.4+ with

  ERR_WORKER_INVALID_EXEC_ARGV: Initiated Worker with invalid NODE_OPTIONS env
  variable: --js-defer-import-eval is not allowed in NODE_OPTIONS

nub injects that flag on argv precisely BECAUSE Node refuses it in NODE_OPTIONS,
but the flag still lands in `process.execArgv` β€” and a great deal of real tooling
forwards `process.execArgv` into a Worker or into a child's NODE_OPTIONS. Node
then rejects nub's own flag and kills the build. Every other flag nub injects is
NODE_OPTIONS-legal, so this hazard is specific to the new UnflagArgv shape.

The spawn, watch and compiled-launcher paths now pass the injected argv-only flags
to the preload through `__NUB_ARGV_ONLY_FLAGS`, and the preload removes them from
`process.execArgv` before any user code runs, then deletes the variable. V8 parses
these flags at startup, so the feature stays on: verified that `import defer`
still defers on the main thread AND inside a worker that received the filtered
execArgv. Only flags NUB injected are hidden; a user's own `v8Flags` stay visible.

This restores the execArgv a plain-Node user would have seen, rather than changing
Node behavior β€” the same reasoning as worker-polyfill.mjs stripping `--harmony-*`
for the identical error.

Covered by `injected_argv_only_flags_are_hidden_from_exec_argv`, which asserts both
halves: nothing left in execArgv is NODE_OPTIONS-illegal, and deferral still works
inside a worker handed that execArgv. The bug escaped the earlier round because
the manual worker check used a bare `new Worker(...)` with no execArgv option.

* node: only cache a genuine rejection from the argv-flag probe

`accepts_argv_flag` treated every non-zero exit as "Node rejected this flag" and
persisted that verdict by (path, mtime). One transient failure β€” a signal under
memory pressure, a sandbox denial, a half-written install β€” would therefore have
disabled the feature for that binary until Node was reinstalled or the cache file
deleted by hand, with no diagnostic. Its sibling `accepted_env_flags` returns
`None` on a failed probe and writes nothing, so a transient failure there costs a
single re-probe.

The false negative was also the LIKELY case rather than the rare one: this probe
only runs at or above a band floor, where the flag is expected to work, while the
rejection it exists to catch is a future flag removal.

Narrow the durable verdict to the exact signal, measured on real binaries: Node
25.9.0 rejects with exit 9 and `node: bad option: --js-defer-import-eval` on
stderr, and 26.5.0 accepts with exit 0 and empty stderr. Anything else returns
`None`, falling back to version-band gating. Covered by
`argv_flag_probe_caches_a_rejection_but_not_a_transient_failure`, which drives both
paths through fake binaries.

Also stop probing a Node that nub provisioned or embedded itself: `argv_inject_flags`
now takes an optional path, and the compiled launcher passes `None` for a managed
Node. That mirrors why `accepted_env_flags` is already skipped there β€” its accepted
flags follow from its version, and the launcher is the hot path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs that have at least one approval, no outstanding review comments, and a CI started. backport-requested-v24.x PRs awaiting manual backport to the v24.x-staging branch. c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants