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]>
107 lines
4.5 KiB
JavaScript
107 lines
4.5 KiB
JavaScript
const path = require('path')
|
|
|
|
module.exports = async function () {
|
|
// Import a TypeScript module that has its own dependencies
|
|
// (cjs-dep.ts -> metadata.json, esm-dep.mjs)
|
|
const configPath = path.resolve(__dirname, 'config-data.ts')
|
|
const configModule = await this.importModule(configPath)
|
|
const config = configModule.default || configModule
|
|
|
|
// Import an ESM .mjs module that also imports esm-dep.mjs
|
|
// Uses a relative path (resolved relative to the transformed module, app/file.test-file.ts)
|
|
const mjsModule = await this.importModule('../config-data.mjs')
|
|
const mjsConfig = mjsModule.default || mjsModule
|
|
|
|
const title = config.title
|
|
const items = config.items.join(', ')
|
|
const cjsGreeting = config.cjsGreeting
|
|
const version = config.version
|
|
const esmLabel = config.esmLabel
|
|
|
|
const mjsTitle = mjsConfig.mjsTitle
|
|
const mjsEsmLabel = mjsConfig.esmLabel
|
|
|
|
// --- resolveAlias tests ---
|
|
let aliasValue = 'unsupported'
|
|
let aliasDepLabel = 'unsupported'
|
|
try {
|
|
// importModule with alias as the request
|
|
const aliasModule = await this.importModule('alias-data')
|
|
const aliasData = aliasModule.default || aliasModule
|
|
aliasValue = aliasData.aliasValue
|
|
|
|
// importModule on a module whose dependency uses an alias
|
|
// Uses a relative path (resolved relative to the transformed module)
|
|
const aliasConfigModule = await this.importModule('../alias-config.mjs')
|
|
const aliasConfig = aliasConfigModule.default || aliasConfigModule
|
|
aliasDepLabel = aliasConfig.depLabel
|
|
} catch (e) {
|
|
// resolveAlias may not be supported in all contexts
|
|
console.error('resolveAlias importModule failed:', e)
|
|
}
|
|
|
|
// --- loader rules tests ---
|
|
let customDataValue = 'unsupported'
|
|
let consumedValue = 'unsupported'
|
|
try {
|
|
// importModule on a file that requires a custom loader rule
|
|
const customDataPath = path.resolve(__dirname, 'values.custom-data')
|
|
const customDataModule = await this.importModule(customDataPath)
|
|
customDataValue = customDataModule.default || customDataModule
|
|
|
|
// importModule on a module whose dependency needs a custom loader rule
|
|
// Uses a relative path (resolved relative to the transformed module)
|
|
const dataConsumerModule = await this.importModule('../data-consumer.mjs')
|
|
const dataConsumer = dataConsumerModule.default || dataConsumerModule
|
|
consumedValue = dataConsumer.consumedValue
|
|
} catch (e) {
|
|
// loader rules may not be supported in importModule in all contexts
|
|
console.error('loader rules importModule failed:', e)
|
|
}
|
|
|
|
// Try importing the wasm+URL+dynamic-import module (supported in
|
|
// Turbopack, not in webpack's importModule)
|
|
let imageUrl = 'unsupported'
|
|
let wasmAddResult = 'unsupported'
|
|
let dynamicValue = 'unsupported'
|
|
let mjsImageUrl = 'unsupported'
|
|
let mjsWasmAddResult = 'unsupported'
|
|
let mjsDynamicValue = 'unsupported'
|
|
try {
|
|
const urlWasmPath = path.resolve(__dirname, 'url-wasm-data.ts')
|
|
const urlWasmModule = await this.importModule(urlWasmPath)
|
|
const urlWasm = urlWasmModule.default || urlWasmModule
|
|
imageUrl = urlWasm.imageUrl
|
|
wasmAddResult = String(urlWasm.wasmAddResult)
|
|
dynamicValue = urlWasm.dynamicValue
|
|
|
|
const mjsUrlWasmModule = await this.importModule('../url-wasm-data.mjs')
|
|
const mjsUrlWasm = mjsUrlWasmModule.default || mjsUrlWasmModule
|
|
mjsImageUrl = mjsUrlWasm.mjsImageUrl
|
|
mjsWasmAddResult = String(mjsUrlWasm.mjsWasmAddResult)
|
|
mjsDynamicValue = mjsUrlWasm.mjsDynamicValue
|
|
} catch {
|
|
// webpack's importModule doesn't support wasm/URL asset patterns
|
|
}
|
|
|
|
return `
|
|
export const title = ${JSON.stringify(title)};
|
|
export const items = ${JSON.stringify(items)};
|
|
export const cjsGreeting = ${JSON.stringify(cjsGreeting)};
|
|
export const version = ${JSON.stringify(version)};
|
|
export const esmLabel = ${JSON.stringify(esmLabel)};
|
|
export const imageUrl = ${JSON.stringify(imageUrl)};
|
|
export const wasmAddResult = ${JSON.stringify(wasmAddResult)};
|
|
export const dynamicValue = ${JSON.stringify(dynamicValue)};
|
|
export const mjsTitle = ${JSON.stringify(mjsTitle)};
|
|
export const mjsEsmLabel = ${JSON.stringify(mjsEsmLabel)};
|
|
export const mjsImageUrl = ${JSON.stringify(mjsImageUrl)};
|
|
export const mjsWasmAddResult = ${JSON.stringify(mjsWasmAddResult)};
|
|
export const mjsDynamicValue = ${JSON.stringify(mjsDynamicValue)};
|
|
export const aliasValue = ${JSON.stringify(aliasValue)};
|
|
export const aliasDepLabel = ${JSON.stringify(aliasDepLabel)};
|
|
export const customDataValue = ${JSON.stringify(customDataValue)};
|
|
export const consumedValue = ${JSON.stringify(consumedValue)};
|
|
`
|
|
}
|