Files
next.js/test/e2e/app-dir/webpack-loader-import-module/webpack-loader-import-module.test.ts
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

53 lines
2.3 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
describe('webpack-loader-import-module', () => {
const { next, skipped, isTurbopack } = nextTestSetup({
files: __dirname,
skipDeployment: true,
})
if (skipped) {
return
}
it('should support this.importModule() in a webpack loader', async () => {
const $ = await next.render$('/')
expect($('#title').text()).toBe('Import Module Works')
expect($('#items').text()).toBe('apple, banana, cherry')
// CJS dependency that itself requires a JSON file
expect($('#cjs-greeting').text()).toBe('hello from cjs')
expect($('#version').text()).toBe('1.0.0')
// ESM dependency imported from config-data.ts
expect($('#esm-label').text()).toBe('hello from esm')
// ESM .mjs module (config-data.mjs)
expect($('#mjs-title').text()).toBe('ESM Config Works')
expect($('#mjs-esm-label').text()).toBe('hello from esm')
// resolveAlias: importModule with alias as request
expect($('#alias-value').text()).toBe('resolved via alias')
// resolveAlias: dependency of importModule target uses alias
expect($('#alias-dep-label').text()).toBe('hello from esm')
// loader rules: importModule on file requiring custom loader
expect($('#custom-data-value').text()).toBe('hello from custom loader')
// loader rules: dependency of importModule target needs custom loader
expect($('#consumed-value').text()).toBe('hello from custom loader')
if (isTurbopack) {
// new URL('./image.png', import.meta.url) in url-wasm-data.ts
expect($('#image-url').text()).toContain('image')
expect($('#image-url').text()).toMatch(/\.png/)
// WebAssembly add(1, 2) from add.wasm in url-wasm-data.ts
expect($('#wasm-add-result').text()).toBe('3')
// Dynamic import('./module.js') in url-wasm-data.ts
expect($('#dynamic-value').text()).toBe('loaded dynamically')
// new URL('./image.png', import.meta.url) in url-wasm-data.mjs
expect($('#mjs-image-url').text()).toContain('image')
expect($('#mjs-image-url').text()).toMatch(/\.png/)
// WebAssembly add(10, 20) from add.wasm in url-wasm-data.mjs
expect($('#mjs-wasm-add-result').text()).toBe('30')
// Dynamic import('./module.js') in url-wasm-data.mjs
expect($('#mjs-dynamic-value').text()).toBe('loaded dynamically')
}
})
})