Skip to content

feat: support source phase import for WebAssembly modules#20364

Merged
alexander-akait merged 12 commits intowebpack:mainfrom
magic-akari:feat/source-phase-import-wasm
Apr 8, 2026
Merged

feat: support source phase import for WebAssembly modules#20364
alexander-akait merged 12 commits intowebpack:mainfrom
magic-akari:feat/source-phase-import-wasm

Conversation

@magic-akari
Copy link
Copy Markdown
Contributor

Summary

This PR implements support for the TC39 source-phase-imports proposal for WebAssembly modules, enabling developers to import WebAssembly modules at the source phase using import.source() syntax.

Motivation:

Source phase imports allow importing a WebAssembly module as a WebAssembly.Module object rather than an instantiated module. This enables several important use cases:

  • Multiple instantiation: The same compiled WASM module can be instantiated multiple times with different imports
  • Worker sharing: Compiled modules can be efficiently shared across Web Workers using postMessage

Example usage:

// Import as compiled WebAssembly.Module
const wasmModule = await import.source("./module.wasm");

// Instantiate multiple times with different imports
const instance1 = await WebAssembly.instantiate(wasmModule, imports1);
const instance2 = await WebAssembly.instantiate(wasmModule, imports2);

// Share with workers
worker.postMessage(wasmModule);

Implementation details:

  • Extended the existing ImportPhase infrastructure to support Source phase alongside Evaluation and Defer
  • Added new runtime global RuntimeGlobals.compileWasm for source phase compilation
  • Created AsyncWasmCompileRuntimeModule to generate runtime code for compiling WASM modules
  • Optimized parser to skip full WASM decoding for source phase imports (only validates magic header)
  • Source phase modules export the WebAssembly.Module as default export
  • Gated behind experiments.importSource flag for safety

What kind of change does this PR introduce?

Feature (feat) - Adds support for experimental TC39 source phase imports for WebAssembly modules.

Did you add tests for your changes?

Yes. Added test case in test/configCases/wasm/source-phase-basic/ covering:

  • Basic source phase import functionality
  • Verification that imported value is a WebAssembly.Module instance
  • Module instantiation from source phase import
  • Module caching (multiple imports return the same module instance)

Does this PR introduce a breaking change?

No. This feature is:

  • Opt-in via experiments.importSource configuration flag (defaults to false)
  • Only affects code explicitly using import.source() syntax
  • Backward compatible with existing WASM import patterns

If relevant, what needs to be documented once your changes are merged or what have you already documented?

Documentation needed:

  1. Configuration documentation:

    • Add experiments.importSource flag to webpack configuration docs
    • Note that this requires the experimental flag to be enabled
  2. Feature documentation:

    • Usage guide for source phase imports with WebAssembly modules
    • Use cases and examples (worker sharing, multiple instantiation)
    • Comparison with normal WASM imports
  3. TypeScript types:

    • Already documented in declarations/WebpackOptions.d.ts with JSDoc
    • Types exported in types.d.ts

Related:

Copilot AI review requested due to automatic review settings January 27, 2026 17:41
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 27, 2026

🦋 Changeset detected

Latest commit: ecbbe50

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces experimental support for TC39 source-phase imports for WebAssembly modules in webpack, allowing import.source() of .wasm/.wat to yield a WebAssembly.Module object instead of an instantiated instance. It wires the feature through parser options, experiments config, runtime globals, wasm async loading/compilation, and adds tests and schema/type updates.

Changes:

  • Add experiments.importSource and associated JavaScript parser options (parser.javascript*.importSource) to configuration schemas, defaults, CLI snapshots, and TypeScript declaration files.
  • Extend the import phase infrastructure to support a new Source phase, integrate it with both ESM and dynamic import parsers, and plumb a sourcePhase flag through NormalModule and the async WebAssembly module pipeline.
  • Introduce RuntimeGlobals.compileWasm and a new AsyncWasmCompileRuntimeModule, along with generator/parser changes so async WebAssembly modules imported via import.source() compile to and export a cached WebAssembly.Module default, plus config-case tests for basic behavior and caching.

Reviewed changes

Copilot reviewed 23 out of 26 changed files in this pull request and generated no comments.

Show a summary per file
File Description
types.d.ts Adds experiments.importSource, parser importSource options, ImportExpressionJavascriptParser.phase support for "source", NormalModule.sourcePhase, and RuntimeGlobals.compileWasm to the public type surface.
declarations/WebpackOptions.d.ts Mirrors new importSource experiment and parser options in the exported TS configuration interfaces.
schemas/WebpackOptions.json Extends the JSON schema with experiments.importSource and module.parser.javascript*.importSource boolean options so config validation/CLI help know about the new feature.
lib/config/defaults.js Wires experiments.importSource into defaults, passes it through module/parser option defaults, and ensures JS parser importSource defaults from the experiment flag.
lib/WebpackOptionsApply.js Enables acorn-import-phases whenever deferImport or importSource is enabled and configures it to recognize source-phase syntax when experiments.importSource is true.
lib/javascript/JavascriptParser.js Updates the ImportExpression typedef so the AST node can carry `phase: "defer"
lib/dependencies/ImportPhase.js Introduces ImportPhase.Source and ImportPhaseUtils.isSource, and updates createGetImportPhase to classify both defer and source syntax when enabled.
lib/dependencies/ImportParserPlugin.js Switches import-phase detection to be gated by either deferImport or importSource parser options for dynamic imports.
lib/dependencies/HarmonyImportDependencyParserPlugin.js Same as above for static import/export (harmony) parsing.
lib/dependencies/ImportDependency.js Uses ImportPhaseUtils and, for source-phase imports, unwraps the default export so import.source() resolves to the underlying value (the WebAssembly.Module) instead of the namespace object.
lib/dependencies/HarmonyImportDependency.js Extends debugging/serialization output to reflect `
lib/RuntimeGlobals.js Registers a new runtime global compileWasm (__webpack_require__.vs) with documentation indicating it compiles a wasm module from id and hash to WebAssembly.Module.
lib/WebpackOptionsApply.js Ensures that when experiments.importSource is enabled, the parser is extended with source-phase import support, without affecting builds where the experiment is off.
lib/NormalModule.js Adds a sourcePhase flag to NormalModule construction data, stores it on instances, and includes it in serialization/deserialization for caching.
lib/wasm-async/AsyncWebAssemblyModulesPlugin.js Detects when an async wasm module is reached via a source-phase dependency and marks its create data as sourcePhase so downstream parser/generator behavior can switch.
lib/wasm-async/AsyncWebAssemblyParser.js For NormalModule.sourcePhase wasm modules, validates only the wasm magic header, marks buildMeta.sourcePhase, declares a single default export, and skips full wasm decoding; otherwise keeps existing async wasm parsing.
lib/wasm-async/AsyncWebAssemblyJavascriptGenerator.js Routes source generation for buildMeta.sourcePhase modules to a new _generateSourcePhase code path that defines an async module using RuntimeGlobals.compileWasm and exports the resulting WebAssembly.Module as default.
lib/wasm-async/AsyncWasmCompileRuntimeModule.js New runtime module that defines RuntimeGlobals.compileWasm as a function taking (wasmModuleId, wasmModuleHash), loading the wasm asset (via target-specific loader code), using WebAssembly.compile / compileStreaming with a MIME-type-sensitive fallback, and returning a Promise<WebAssembly.Module>.
lib/web/FetchCompileAsyncWasmPlugin.js Hooks the web/fetch wasm loading pipeline to also provide the new compile runtime using AsyncWasmCompileRuntimeModule when async wasm modules are present, sharing the same generateLoadBinaryCode as instantiation.
lib/node/ReadFileCompileAsyncWasmPlugin.js Similarly wires the node/async-node wasm pipeline to add AsyncWasmCompileRuntimeModule (non-streaming, using fs/url loader) when async wasm modules are in the chunk.
test/configCases/wasm/source-phase-basic/webpack.config.js Adds an async-node test configuration enabling experiments.importSource and async wasm via a .wat loader.
test/configCases/wasm/source-phase-basic/wasm.wat Simple WAT module exporting an add(i32, i32) function to be used in source-phase import tests.
test/configCases/wasm/source-phase-basic/test.filter.js Skips the new wasm test config when the environment lacks WebAssembly support.
test/configCases/wasm/source-phase-basic/index.js Test cases verifying import.source("./wasm.wat") yields a WebAssembly.Module, that it can be instantiated and invoked, and that multiple imports share the same compiled module instance.
test/__snapshots__/Cli.basictest.js.snap Updates CLI option snapshots to include experiments.importSource and all module.parser.javascript*.importSource flags with consistent descriptions.
test/Defaults.unittest.js Adjusts defaults snapshots to reflect experiments.importSource: false and parser importSource: false by default.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/configCases/wasm/source-phase-basic/index.js
Comment thread lib/config/defaults.js Outdated
deferImport:
/** @type {NonNullable<ExperimentsNormalized["deferImport"]>} */
(options.experiments.deferImport),
importSource:
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.

Maybe sourceImport? To be align with deferImport

@alexander-akait
Copy link
Copy Markdown
Member

Do you want to solve it const mod = await import.source("./module.js"); in separate PR? (also any ideas about implementation?)

From spec - https://github.com/tc39/proposal-source-phase-imports?tab=readme-ov-file#js-module-source

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Jan 27, 2026

Merging this PR will degrade performance by 31.43%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 3 improved benchmarks
❌ 3 regressed benchmarks
✅ 138 untouched benchmarks

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "devtool-eval-source-map", scenario '{"name":"mode-development","mode":"development"}' 968.6 KB 1,396.4 KB -30.64%
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-production","mode":"production"}' 9.5 MB 7 MB +35.92%
Memory benchmark "wasm-modules-async", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 1,420.5 KB 916.3 KB +55.04%
Memory benchmark "react", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 644.2 KB 864.4 KB -25.48%
Memory benchmark "devtool-source-map", scenario '{"name":"mode-development","mode":"development"}' 1.4 MB 1 MB +32.44%
Memory benchmark "asset-modules-source", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 267.4 KB 390 KB -31.43%

Comparing magic-akari:feat/source-phase-import-wasm (ecbbe50) with main (0b60f1c)

Open in CodSpeed

@magic-akari magic-akari marked this pull request as draft January 28, 2026 12:31
@magic-akari magic-akari force-pushed the feat/source-phase-import-wasm branch from f462aa6 to 32352c3 Compare January 28, 2026 13:42
@alexander-akait
Copy link
Copy Markdown
Member

alexander-akait commented Jan 28, 2026

Thanks for example and other things, there remains to be added:

  1. Passing phase to NormalModuleFactory in this.ruleSet.exec(), so developer can register an own loader for phases (also apply different resolving and etc).
  2. Adding test cases for "node", "web" and ["node", "web"] (universal target) targets for CommonJS and ECMA modules outputs.

@magic-akari
Copy link
Copy Markdown
Contributor Author

  1. Passing phase to NormalModuleFactory in this.ruleSet.exec(), so developer can register an own loader for phases (also apply different resolving and etc).

Done

  1. Adding test cases for "node", "web" and ["node", "web"] (universal target) targets for CommonJS and ECMA modules outputs.

I’ve derived the current test cases from the existing Wasm tests.
However, I ran into some issues while trying to add CJS output for the universal target (["node", "web"]). I might be misinterpreting the expected behavior here. Could you provide a bit more detail or a specific example of the structure you're looking for?

Do you want to solve it const mod = await import.source("./module.js"); in separate PR? (also any ideas about implementation?)

From spec - https://github.com/tc39/proposal-source-phase-imports?tab=readme-ov-file#js-module-source

That’s a very interesting use case! However, since the spec is still in its early stages and many details remain unclear, I think it’s best to handle this in a separate PR. For now, introducing the phase rule in Webpack to allow for custom loaders is a solid foundation and a great place to start.

@alexander-akait
Copy link
Copy Markdown
Member

However, I ran into some issues while trying to add CJS output for the universal target (["node", "web"]). I might be misinterpreting the expected behavior here. Could you provide a bit more detail or a specific example of the structure you're looking for?

Universal target (["node", "web"]) is only for ES modules, so you don't need to tests it, we need: CommonJS ("web" and "node") and ECMA modules ("web", "node", ["web", "node"])

@magic-akari
Copy link
Copy Markdown
Contributor Author

Universal target (["node", "web"]) is only for ES modules, so you don't need to tests it, we need: CommonJS ("web" and "node") and ECMA modules ("web", "node", ["web", "node"])

Thanks for the clarification. You can find the implementation and new test cases under:

  • test/configCases/wasm/source-phase-async-node
  • test/configCases/wasm/source-phase-fetch

@alexander-akait
Copy link
Copy Markdown
Member

Thanks, code looks good, we will review this deeply soon (maybe will be included in the next release)

@magic-akari magic-akari marked this pull request as ready for review January 28, 2026 18:45
@magic-akari magic-akari force-pushed the feat/source-phase-import-wasm branch 2 times, most recently from 6a3d326 to 8f95cdb Compare February 17, 2026 15:08
@magic-akari magic-akari marked this pull request as draft February 17, 2026 15:38
@magic-akari
Copy link
Copy Markdown
Contributor Author

I'm working on a rebase and have tried several approaches without success. Are there any guidelines regarding __importPhasesExtended ?

@alexander-akait
Copy link
Copy Markdown
Member

alexander-akait commented Feb 17, 2026

@magic-akari I think we need refactor this place, ideally we need:

JavascriptParser.extend(
  importPhases({ 
    source: options.experiments.sourceImport,
    defer: options.experiments.defer
  })
);

And use WeakMap<OptionsToImportPhase, JavascriptParser> to cache them, the main problem here when we run multiple time webpack(options, (stats) => {}) we extend acorn parser many times (memory leaking + possible infinity calls), so we added __importPhasesExtended to prevent it, but in your case we have different options, so when we firstly run webpack with defer we extends with only defer support, when you run with source we are using old parser which supports only defer, also any improvement welcome

@magic-akari magic-akari force-pushed the feat/source-phase-import-wasm branch from 8f95cdb to 63d9223 Compare February 18, 2026 03:42
@magic-akari magic-akari marked this pull request as ready for review February 18, 2026 04:35
@magic-akari
Copy link
Copy Markdown
Contributor Author

@magic-akari I think we need refactor this place, ideally we need:

JavascriptParser.extend(

  importPhases({ 

    source: options.experiments.sourceImport,

    defer: options.experiments.defer

  })

);

And use WeakMap<OptionsToImportPhase, JavascriptParser> to cache them, the main problem here when we run multiple time webpack(options, (stats) => {}) we extend acorn parser many times (memory leaking + possible infinity calls), so we added __importPhasesExtended to prevent it, but in your case we have different options, so when we firstly run webpack with defer we extends with only defer support, when you run with source we are using old parser which supports only defer, also any improvement welcome

Thanks for the guidance! Everything is now set and this PR is ready for review.

@magic-akari magic-akari force-pushed the feat/source-phase-import-wasm branch from 63d9223 to 9667819 Compare March 15, 2026 03:59
@alexander-akait
Copy link
Copy Markdown
Member

@magic-akari Thanks for rebase, will review and merge soon (will be in the next minor release)

@alexander-akait
Copy link
Copy Markdown
Member

@magic-akari can you make an extra one rebase and I will focus on this improvement, thanks, sorry for that

@magic-akari magic-akari force-pushed the feat/source-phase-import-wasm branch from 9667819 to 68a6124 Compare March 24, 2026 14:12
@magic-akari
Copy link
Copy Markdown
Contributor Author

@magic-akari can you make an extra one rebase and I will focus on this improvement, thanks, sorry for that

Done

@alexander-akait alexander-akait force-pushed the feat/source-phase-import-wasm branch 2 times, most recently from 08699f7 to aa0de2c Compare April 7, 2026 23:52
@alexander-akait
Copy link
Copy Markdown
Member

@magic-akari Looks like we need a new one rebase and we can merge, still can't understand why github doesn't provide ability to rebase branches in forks when you are working on them like a contributor 😞 ...

@magic-akari magic-akari force-pushed the feat/source-phase-import-wasm branch from aa0de2c to de2d586 Compare April 8, 2026 02:01
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 8, 2026

Codecov Report

❌ Patch coverage is 91.66667% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.40%. Comparing base (0b60f1c) to head (ecbbe50).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/dependencies/ImportPhase.js 78.78% 7 Missing ⚠️
lib/wasm-async/AsyncWasmCompileRuntimeModule.js 87.87% 4 Missing ⚠️
lib/node/ReadFileCompileAsyncWasmPlugin.js 87.50% 1 Missing ⚠️
lib/wasm-async/AsyncWebAssemblyParser.js 90.90% 1 Missing ⚠️
lib/wasm-async/UniversalCompileAsyncWasmPlugin.js 88.88% 1 Missing ⚠️
lib/web/FetchCompileAsyncWasmPlugin.js 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #20364      +/-   ##
==========================================
+ Coverage   90.45%   91.40%   +0.94%     
==========================================
  Files         557      560       +3     
  Lines       55135    55295     +160     
  Branches    14554    14593      +39     
==========================================
+ Hits        49875    50545     +670     
+ Misses       5260     4750     -510     
Flag Coverage Δ
integration 90.42% <91.66%> (-0.01%) ⬇️
test262 ?
unit 36.16% <26.86%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 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.

@alexander-akait alexander-akait merged commit 7cdc173 into webpack:main Apr 8, 2026
53 of 55 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 8, 2026

This PR is packaged and the instant preview is available (7cdc173).

Install it locally:

  • npm
npm i -D webpack@https://pkg.pr.new/webpack@7cdc173
  • yarn
yarn add -D webpack@https://pkg.pr.new/webpack@7cdc173
  • pnpm
pnpm add -D webpack@https://pkg.pr.new/webpack@7cdc173

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.

4 participants