Turbopack: Add importModule() support to webpack loaders#89630
Merged
Turbopack: Add importModule() support to webpack loaders#89630
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
Collaborator
Stats from current PR✅ No significant changes detected📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles
Server Middleware
Build DetailsBuild Manifests
📦 WebpackClient Main Bundles
Polyfills
Pages
Server Edge SSR
Middleware
Build DetailsBuild Manifests
Build Cache
🔄 Shared (bundler-independent)Runtimes
📎 Tarball URL |
Collaborator
Tests Passed |
7a28fa8 to
a686d4d
Compare
c3eece4 to
372aa7f
Compare
turbopack/crates/turbopack-node/js/src/transforms/webpack-loaders-runtime.ts
Outdated
Show resolved
Hide resolved
turbopack/crates/turbopack-node/js/src/transforms/webpack-loaders-runtime.ts
Outdated
Show resolved
Hide resolved
21e5326 to
2d5430f
Compare
e86ca83 to
782da5a
Compare
lukesandberg
reviewed
Mar 3, 2026
turbopack/crates/turbopack-node/js/src/transforms/webpack-loaders-runtime.ts
Show resolved
Hide resolved
lukesandberg
requested changes
Mar 5, 2026
Contributor
lukesandberg
left a comment
There was a problem hiding this comment.
see my open question about error semantics
782da5a to
035be1d
Compare
lukesandberg
approved these changes
Mar 16, 2026
a74ad88 to
40989f7
Compare
Test verifies that a custom webpack loader can use `this.importModule()` to compile and execute another module at build time and use its exports to generate loader output. https://claude.ai/code/session_01UiNWDNTAGcLmV49FTipUzt
Turbopack does not support the this.importModule() loader API yet, so wrap the describe block with IS_TURBOPACK_TEST to skip it. https://claude.ai/code/session_01UiNWDNTAGcLmV49FTipUzt
Adds support for the webpack loader API `this.importModule()` in Turbopack's webpack loader compatibility layer. Rust side (turbopack-node): - Add `ImportModule` request/response variants to the loader IPC protocol - Resolve the requested module via Turbopack's resolver - Read the source content and return it as compiled code - Track the file as a dependency for invalidation JS side (webpack-loaders.ts): - Add `importModule` method to the loader context - Execute the returned code in a CommonJS wrapper using Node.js `vm` - Support both callback and Promise API (matching webpack's signature) https://claude.ai/code/session_01UiNWDNTAGcLmV49FTipUzt
Add test coverage for importModule with modules that have their own dependencies: - CJS module (cjs-dep.js) that require()s a JSON file (metadata.json) - ESM module (esm-dep.mjs) with named exports - config-data.js now imports cjs-dep to exercise transitive deps Also update the Turbopack importModule implementation: - Return the resolved file path in the IPC response so the JS side can detect ESM modules by extension - Handle ESM modules (.mjs / export syntax) via native dynamic import() instead of the CommonJS vm wrapper https://claude.ai/code/session_01UiNWDNTAGcLmV49FTipUzt
Convert importModule test dependencies (config-data, cjs-dep) from JavaScript to TypeScript to test proper module compilation. On the Rust side, resolve the requested module using the project's resolve options, then process it through Turbopack's compilation pipeline (SWC for TS, JSON parsing, etc.) via AssetContext::process(). Build a module graph from the resolved module and generate EcmascriptChunkItemContent for each module in the graph. On the JS side, implement a mini Turbopack runtime with module cache and __turbopack_context__ (supporting .r, .i, .s, .v, .n, .x, etc.) to execute the compiled module factories, similar to webpack's executeModule. https://claude.ai/code/session_01UiNWDNTAGcLmV49FTipUzt
- Add esm-dep import to config-data.ts alongside new URL() and WebAssembly usage - Create config-data.mjs as an ESM variant also using URL/WebAssembly APIs - Import config-data.mjs via importModule instead of importing esm-dep directly - Verify all new values in both webpack and Turbopack test modes https://claude.ai/code/session_01UiNWDNTAGcLmV49FTipUzt
… subtype
- Add EcmaScriptModulesReferenceSubType::ImportModule variant for
importModule resolution/processing in webpack loaders
- Add .q() (export URL), .R() (resolve module path), .w() (sync wasm
instantiation), and .a() (async module handler) to the mini Turbopack
runtime in webpack-loaders.ts
- Strip `await` keywords from generated factory code so wasm loader
modules execute synchronously in the importModule context
- Add resolveProjectPath helper to walk up directories and find the
Turbopack project root for path resolution
- Create url-wasm-data.ts/.mjs test fixtures importing add.wasm and
using new URL('./image.png', import.meta.url)
- Test wasm/URL features in Turbopack mode; webpack mode gracefully
falls back since its importModule doesn't support wasm loading
https://claude.ai/code/session_01UiNWDNTAGcLmV49FTipUzt
Replace the await-stripping hack with real async module handling:
- Compute AsyncModuleInfo from the module graph and pass it to
content_with_async_module_info so the code generator emits proper
ctx.a() wrappers for async modules
- Implement ctx.a() with handleAsyncDependencies that tracks async
module promises via a Symbol and awaits them before resolution
- Make ctx.w() async using WebAssembly.instantiate instead of sync
WebAssembly.Module/Instance
- Add ctx.l() for dynamic import support
- Add import("./module.js") to url-wasm-data.ts and url-wasm-data.mjs
test fixtures
https://claude.ai/code/session_01UiNWDNTAGcLmV49FTipUzt
- Extract the mini Turbopack runtime from webpack-loaders.ts into
webpack-loaders-runtime.ts with a single executeModules() entry point
- Deduplicate module ID regex into MODULE_ID_PATH_RE constant and
resolveModuleIdPath() helper
- Extract ensureModule() to eliminate boilerplate in ctx.s/ctx.v/ctx.n
- Remove redundant require('fs') (already imported at top level)
- Run cargo fmt on webpack.rs
https://claude.ai/code/session_01UiNWDNTAGcLmV49FTipUzt
In production mode, Turbopack's code generation produces different patterns than dev mode for async modules, import.meta.url, and dynamic imports. This commit adds the missing pieces: - Send `has_top_level_await` from Rust to JS so the mini runtime can wrap async module code with the ctx.a() handler (mirroring what module_factory() does in item.rs) - Add ctx.P (resolve absolute path) for import.meta.url patterns - Add ctx.A (async loader) for production-mode dynamic imports, with fallback to ctx.l when the loader wrapper module isn't available - Add wasm.d.ts for TypeScript type checking in production builds https://claude.ai/code/session_01UiNWDNTAGcLmV49FTipUzt
- Remove dead `error` field from ModuleObj interface - Move `vm` and `url` to top-level imports (consistent with `fs`/`path`) - Simplify wasm.d.ts to generic wildcard declaration - Clarify doc comment on has_top_level_await field https://claude.ai/code/session_01UiNWDNTAGcLmV49FTipUzt
…s/loader support Thread the module_asset_context through SourceTransform::transform so that webpack loader importModule calls can resolve aliases and apply custom loader rules. Previously importModule used a minimal evaluate_context that lacked the user's resolveAlias and loader configurations. - Add asset_context parameter to SourceTransform trait and all impls - Pass module_asset_context at the call site in process_default_internal - Use asset_context in ImportModule handler's PlainResolveOrigin - Add test fixtures for resolveAlias and custom loader rules
…ntime
The importModule mini-runtime was missing the ctx.U (relativeURL)
constructor needed when UrlRewriteBehavior::Relative is active (default
for App Router server contexts). This caused new URL('./image.png',
import.meta.url) patterns to silently fail in webpack loaders.
Also fixed ctx.q to set module.exports directly instead of setting a
default property, matching the full Turbopack runtime's exportUrl
behavior.
…nsform The SourceTransform trait was updated to require an asset_context parameter but these two implementations were not updated, causing compilation failures.
Convert 4 importModule calls from absolute paths to relative paths to test that this.importModule() correctly resolves relative to the transformed module (app/file.test-file.ts), not the loader file.
Instead of sending individual module code strings via IPC and using a custom 431-line mini runtime to execute them, use Turbopack's existing Node.js chunking pipeline (root_entry_chunk_group_asset) to generate a proper bundle with the real runtime. The JS side evaluates the bundle with a simple ~100-line in-memory CJS module loader using vm.compileFunction. This eliminates the duplicated runtime helpers (esmBindings, interopEsm, asyncModule, etc.) and ensures importModule uses the exact same runtime behavior as the production build. Binary assets (WASM, images) are sent base64-encoded and served through a patched fs module for in-memory createReadStream access.
Two call sites in turbopack/src/lib.rs needed the new asset_context parameter added to SourceTransform::transform().
Provide the processed source file as the issue source for the esm_resolve call in the ImportModule handler, consistent with all other issue sources in the file.
The SourceTransform trait on this branch requires an asset_context parameter, but json_source_transform.rs (added on canary in #89631) uses the old 2-parameter signature. Add the parameter to fix the compilation error when CI merges canary into this branch.
Update root_entry_chunk_group_asset call to use ChunkGroup::Entry enum variant instead of Vc::cell, matching the updated chunking_context API. Add comment explaining webpack backward-compat for absolute import paths. Co-Authored-By: Claude <noreply@anthropic.com>
40989f7 to
6a1a742
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What?
Adds support for
this.importModule()in Turbopack's webpack loader compatibility layer. This API allows webpack loaders to dynamically import and execute modules (CJS, ESM, TypeScript, JSON, WebAssembly, etc.) during the build process, matching webpack's nativethis.importModule()behavior.Why?
Webpack loaders like
@vanilla-extract/webpack-plugin,val-loader, and custom loaders usethis.importModule()to:Without this, loaders relying on
importModule()cannot work with Turbopack.How?
Architecture
The implementation follows a request/response pattern between the Node.js loader runner and the Rust-side Turbopack compiler:
this.importModule(request)in the Node.js loader runnervm.compileFunctionwith a minimal CJS module systemThis approach leverages Turbopack's full module graph, so imported modules benefit from the same resolution (aliases, custom loaders, TypeScript support, etc.) as regular imports.
Key Changes
Rust side (
turbopack-node/src/transforms/webpack.rs):ImportModuleIPC response message typeesm_resolvewith fullAssetContextSourceTransformtrait (turbopack-core/src/source_transform.rs):asset_contextparameter toSourceTransform::transform()so transforms can access the full asset context for module resolution and bundlingJsonSourceTransform,TextSourceTransform,BytesSourceTransform,MdxTransform,PostCssTransform,WebpackLoaderItemsNode.js runtime (
turbopack-node/js/src/transforms/):webpack-loaders.ts: Implementsthis.importModule()on the loader context, sends IPC request and evaluates the returned bundlewebpack-loaders-runtime.ts: In-memory CJS module evaluator that loads bundle chunks usingvm.compileFunction, with support for:require()fs.createReadStreamfor in-memory WebAssembly binary assetsNew
ImportModulereference subtype (turbopack-core/src/reference_type.rs):EcmaScriptModulesReferenceSubType::ImportModuleto mark references created by importModule for proper handling in the module graphTest Coverage
Comprehensive e2e test (
test/e2e/app-dir/webpack-loader-import-module/) covering:.mjsmodules with shared dependenciesalias-data→alias-data.mjsviaresolveAliasconfig).custom-datafiles processed bytext-to-export-loader)new URL()asset references, WebAssembly imports (add.wasm), and dynamicimport()within importModule targetsAll tests pass for both webpack and Turbopack modes.