mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
## 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]>
53 lines
2.3 KiB
TypeScript
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')
|
|
}
|
|
})
|
|
})
|