Add `--target esm` to enable cross-target WASM sharing
Summary
This PR unifies the web target's WASM import interface with bundler and experimental-nodejs-module, enabling the same WASM file to be shared across these targets.
Background
Currently, wasm-bindgen generates nearly identical WASM files for different targets, but with different import entry points. This prevents sharing the same WASM file across multiple environments. See #3790 for the full discussion.
Composable Target Plan
This is PR 1 of a multi-step plan:
-
PR 1 (this PR): Align
webtarget's WASM interface - PR 2: Align
nodejsanddenoWASM interface - PR 3: Update wasm-pack to read
mainfield frompackage.json - PR 4: Rename entry files to allow coexistence
- PR 5: Composable targets (
--target web,deno,nodejs)
Changes in this PR
Web target now generates dual-file structure:
pkg/
├── {name}.js # Main entry: init/initSync + re-export
├── {name}_bg.js # Bindings + __wbg_set_wasm + glue functions
├── {name}_bg.wasm # WASM (imports from _bg.js)
└── {name}.d.ts
Key changes:
-
WASM imports: Changed from inline
wbgobject to./xxx_bg.jsmodule import -
_bg.jsfile: Contains bindings code,__wbg_set_wasm(exports, module), and glue functions as independent exports -
Main entry
{name}.js: ContainsinitSync,__wbg_init(async), and re-exports from_bg.js -
__wbg_set_wasmsignature: Now uses(exports, module)to matchexperimental-nodejs-module -
package.json: Now includesmainfield pointing to entry file
Before (old web target):
// {name}.js - everything in one file
let wasm;
// ... inline glue code ...
export function initSync(module) { ... }
export default async function __wbg_init(input) { ... }
After (new web target):
// {name}.js - main entry
import * as import0 from "./xxx_bg.js";
import { __wbg_set_wasm } from "./xxx_bg.js";
export * from "./xxx_bg.js";
export function initSync(module) { ... }
export default async function __wbg_init(input) { ... }
// {name}_bg.js - bindings (same as bundler)
let wasm;
export function __wbg_set_wasm(exports, module) { wasm = exports; }
export function exported() { return wasm.exported(); }
export function __wbg_xxx() { ... } // glue functions
Benefits
-
web,bundler, andexperimental-nodejs-modulenow produce identical_bg.wasmand_bg.jsfiles - Paves the way for composable targets in future PRs
Test Plan
- [x] All 41 reference tests pass
- [x] Verified
package.jsongeneration withmainfield - [x] Manual testing with
--target web
Checklist
- [x] Verified changelog requirement
Update: Backward Compatibility Fix
Issue
The above changes introduced a breaking change for wasm-pack, which hardcodes the assumption that --target web does NOT produce _bg.js files.
Solution
To maintain backward compatibility while still enabling WASM sharing:
-
Added new
--target esm: Provides bundler-compatible WASM interface (generates_bg.jsand_bg.wasm) -
Restored original
--target web: Single JS file with inline init functions (wasm-pack compatible) -
Updated WASM import namespace: Changed from
wbgto./{name}_bg.jsfor bothwebandno-modulestargets
CodSpeed Performance Report
Merging #4850 will not alter performance
Comparing magic-akari:refactor/cli-web (80e34cf) with main (315cfa1)
Summary
✅ 4 untouched
Thank you for this PR - modernizing the targets is a major goal currently, and we've been needing this for a while.
I want to review this properly, and will make sure to do that next week, thanks for your patience.
I agree with all of the motivations here. The alignment is a huge step forward.
Currently we have the source phase form supported under --target module, which is already an "ESM Target" just a different type of ESM target.
We had the discussion when we landed that target whether to have --target source / --target instance or have a single --target module with a modifier for supporting the instance phase, and the decision was made then that a modifier would be preferred since both source and instances phase are part of the same ESM integration proposal.
Therefore I would like to suggest that we change --target esm in this PR to instead be --target module --instance as a modifier on the module target, which could be implemented as Module(InstancePhase) as opposed to Module(SourcePhase) as the default.
Let me know if that can work for you here and if that can fit into how you see this coming together?
Then as for future composable targets, I wonder if we could separate the concept of the target from the environment and start treating environment hints / feature support as separate fine-grained thing to support to modify the output. In this framing --target deno becomes --target module --env deno say, where deno implies some facts about the environment that might be specific to deno. We can continue this discussion when we get there of course!
I really appreciate your pushing this forward, will be great to land this with the small adjustment soon.
Thank you for the thoughtful feedback and for planning to review this properly! I'm really glad to see that we are aligned on the goal of modernizing the targets - this has been a pain point for many users and I'm eager to help move it forward.
I completely agree with the idea of separating target from environment. The environment-specific outputs (node, deno) exist primarily to provide convenience for runtimes with filesystem access - they can auto-initialize the WASM module without requiring users to manually pass the path. This is indeed an orthogonal concern from the core module format.
The --env modifier approach makes a lot of sense for this: --target module --env node,deno clearly expresses "module format + environment-specific conveniences".
Regarding --target module --instance vs --target esm:
I do have a slight concern here. As I understand it, the current --target module (SourcePhase) is more forward-looking and aligned with the ESM integration proposal, while what this PR adds is the InstancePhase variant that works in browsers, node, deno and bundlers today.
If we make SourcePhase the default for --target module, there might be some friction:
- Most users today want "ESM that works now", which is InstancePhase
- Requiring
--instancefor the common case feels a bit counterintuitive - The SourcePhase isn't yet widely supported
A few possible paths forward:
Option 1: Keep separate target names for now
-
--target modulefor SourcePhase (current behavior) -
--target esmfor InstancePhase (this PR) - We can unify them later when SourcePhase becomes more mainstream
Option 2: Make --instance the default
-
--target module→ InstancePhase (common case, simpler) -
--target module --source→ SourcePhase (explicit opt-in for future-looking users)
Option 3: Align SourcePhase to bundler/node-module first
- I could pivot this PR to modify the existing
--target module(SourcePhase), aligning its WASM output to match--target bundlerand--target experimental-nodejs-module - This would mean removing the new
--target esmadditions and applying the same WASM alignment changes to the current SourcePhase module target instead - Defer the InstancePhase variant discussion to a future PR when we have clearer requirements and broader ecosystem support
Option 4: Implement exactly as suggested
-
--target moduleremains SourcePhase (default) -
--target module --instanceenables the InstancePhase (this PR) - And I will change the SourcePhase module output in this PR as well to match bundler/node-module behavior
I'm open to any of these approaches - what do you think would work best for the project's direction?
Just following up on my previous comment about Option 3 - I'm planning to start implementing that approach soon (aligning the SourcePhase WASM outputs) since it seems like a good incremental step forward. I'm still waiting for your thoughts on this path vs the others, so I'll hold off until I hear from you.
No rush at all - whenever you have a moment to share your preference, that would be really helpful. Thanks again for your guidance and insights on.
I think option 3 makes sense with the current plans, especially given that it is becoming stable across tools and we expect browsers to start supporting that form soon natively.
I'm happy to include the instance case here too, but if you want to treat that as a follow-on that is fine by me as well.
I'm assuming this PR depends on the others, but please re-request a review if this is ready.
This PR is ready as well.