Files
bdb2f2ce4d Turbopack: Add importModule() support to webpack loaders (#89630)
## 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 native
`this.importModule()` behavior.

## Why?

Webpack loaders like `@vanilla-extract/webpack-plugin`, `val-loader`,
and custom loaders use `this.importModule()` to:
- Load configuration files that are themselves modules with dependencies
- Execute code at build time to generate derived source
- Process dependency chains (e.g., a TS config that imports a CJS module
that requires JSON)

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:

1. **Loader calls `this.importModule(request)`** in the Node.js loader
runner
2. **IPC message sent to Rust** with the module request and lookup path
3. **Rust resolves the module** using Turbopack's full resolver (with
aliases, loader rules, etc.)
4. **Rust builds a Node.js bundle** containing the module and all its
dependencies using Turbopack's code generation pipeline
5. **Bundle chunks sent back to Node.js** via IPC
6. **Node.js evaluates the bundle in-memory** using `vm.compileFunction`
with a minimal CJS module system

This 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`):**
- New `ImportModule` IPC response message type
- Resolves modules through Turbopack's `esm_resolve` with full
`AssetContext`
- Generates a complete Node.js bundle (runtime + chunks + entry) for the
imported module
- Tracks file dependencies for proper cache invalidation

**`SourceTransform` trait (`turbopack-core/src/source_transform.rs`):**
- Added `asset_context` parameter to `SourceTransform::transform()` so
transforms can access the full asset context for module resolution and
bundling
- Updated all implementors: `JsonSourceTransform`,
`TextSourceTransform`, `BytesSourceTransform`, `MdxTransform`,
`PostCssTransform`, `WebpackLoaderItems`

**Node.js runtime (`turbopack-node/js/src/transforms/`):**
- `webpack-loaders.ts`: Implements `this.importModule()` on the loader
context, sends IPC request and evaluates the returned bundle
- `webpack-loaders-runtime.ts`: In-memory CJS module evaluator that
loads bundle chunks using `vm.compileFunction`, with support for:
  - Relative and absolute path resolution within the bundle
  - External package delegation to real Node.js `require()`
- Patched `fs.createReadStream` for in-memory WebAssembly binary assets

**New `ImportModule` reference subtype
(`turbopack-core/src/reference_type.rs`):**
- Added `EcmaScriptModulesReferenceSubType::ImportModule` to mark
references created by importModule for proper handling in the module
graph

### Test Coverage

Comprehensive e2e test
(`test/e2e/app-dir/webpack-loader-import-module/`) covering:
- **TypeScript modules** with CJS and ESM dependencies
- **CJS dependency chains** (TS → CJS → JSON)
- **ESM `.mjs` modules** with shared dependencies
- **Resolve aliases** (`alias-data` → `alias-data.mjs` via
`resolveAlias` config)
- **Custom loader rules** (`.custom-data` files processed by
`text-to-export-loader`)
- **Transitive dependencies** through aliases and custom loaders
- **Turbopack-only features**: `new URL()` asset references, WebAssembly
imports (`add.wasm`), and dynamic `import()` within importModule targets

All tests pass for both webpack and Turbopack modes.

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: Tobias Koppers <[email protected]>
2026-03-19 10:17:36 +01:00

62 lines
1.7 KiB
JavaScript

const path = require('path')
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
turbopack: {
resolveAlias: {
'alias-data': './alias-data.mjs',
'alias-dep': './esm-dep.mjs',
},
rules: {
'*.test-file.ts': {
loaders: [require.resolve('./test-file-loader.js')],
as: '*.js',
},
'*.custom-data': {
loaders: [require.resolve('./text-to-export-loader.js')],
as: '*.js',
},
},
},
webpack(config) {
config.resolve.alias = {
...config.resolve.alias,
'alias-data': path.resolve(__dirname, 'alias-data.mjs'),
'alias-dep': path.resolve(__dirname, 'esm-dep.mjs'),
}
// Wrap externals handlers to prevent aliased bare specifiers from
// being treated as external ESM packages by Next.js.
const aliasedModules = new Set(['alias-data', 'alias-dep'])
config.externals = config.externals.map((fn) => {
if (typeof fn !== 'function') return fn
return async function (ctx) {
if (aliasedModules.has(ctx.request)) return
return fn.apply(this, arguments)
}
})
config.module.rules.push(
{
test: /\.test-file\.ts/,
use: require.resolve('./test-file-loader.js'),
},
{
test: /\.custom-data$/,
use: require.resolve('./text-to-export-loader.js'),
}
)
// Required so webpack can compile .wasm files referenced by
// url-wasm-data.ts (used via importModule in the loader).
// The actual wasm execution in importModule doesn't work in
// webpack, but compilation needs to succeed.
config.experiments = {
...config.experiments,
asyncWebAssembly: true,
}
return config
},
}
module.exports = nextConfig