mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
## What? Adds support for [Vite's `import.meta.glob`](https://vite.dev/guide/features.html#glob-import) in Turbopack. This is a compile-time transform that resolves glob patterns into a map of module paths to lazy/eager imports. ## Why? This is a commonly used Vite feature for dynamically importing groups of files (e.g., all markdown files in a directory, all route modules matching a pattern). ## How? ### Core implementation (`import_meta_glob.rs`) 1. **Analysis phase**: `import.meta.glob` is recognized as a well-known function via `WellKnownFunctionKind::ImportMetaGlob`. When called, arguments are statically analyzed to extract patterns and options. 2. **File discovery**: Uses Turbopack's `read_glob` (with `Glob::can_match_in_directory` for efficient directory pruning) to find matching files. Negative patterns (prefixed with `!`) are applied via a separate `Glob` matcher. 3. **Virtual module**: Each unique `import.meta.glob()` call generates a virtual `ImportMetaGlobAsset` module that exports an object mapping file paths to either: - **Lazy mode** (default): `() => import('./path')` thunks — resolved with `EcmaScriptModulesReferenceSubType::DynamicImport` - **Eager mode**: Direct `require('./path')` results — resolved with `EcmaScriptModulesReferenceSubType::Import` 4. **Code generation**: The `import.meta.glob(...)` call site is replaced with `__turbopack_require__(virtual_module_id)`. ### Architecture - **`ImportMetaGlobAsset`** is the virtual module. It stores only the origin, patterns, and options — no `source` reference. - **`ImportMetaGlobAsset::map()`** is a `#[turbo_tasks::function]` that builds glob matchers, scans the filesystem, and resolves all matched files as ESM imports. Being a turbo-tasks function, the result is memoised. Both `references()` and `chunk_item_content()` call this single cached function. - **`ident()`** is derived from `AssetIdent::from_path(origin_path)` with a modifier encoding all glob options, so two `import.meta.glob()` calls with different options produce different module idents. - **Side effects**: Lazy mode → `SideEffectFree` (only exports thunks, nothing evaluated). Eager mode → `ModuleEvaluationIsSideEffectFree` (the virtual module itself has no side effects, but its synchronous requires trigger real module evaluations). ### Supported options | Option | Description | |--------|-------------| | `eager` | `boolean` — load modules synchronously (default: `false`) | | `import` | `string` — select a named export (e.g., `'default'`) | | `query` | `string` — append query to imports (e.g., `'?raw'`) | | `base` | `string` — base directory for glob scanning | ### Error handling Unsupported or invalid usage produces clear compile-time diagnostics (error code `TP1008`): - `as` option → "not supported, use `query` instead" - Unknown option keys → lists supported options - Non-constant `eager` → "must be a constant boolean" - Non-constant patterns → "must be string literals" ### Not supported (intentionally) - `import.meta.globEager()` (removed in Vite 3) — users should use `{ eager: true }` - `as` option (deprecated in Vite 5) — users should use `query` ## Test Plan - [x] **Turbopack execution test** (`turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/`) - Lazy mode, eager mode, named import (`import: 'default'`), negative patterns - [x] **Turbopack snapshot tests** (`turbopack/crates/turbopack-tests/tests/snapshot/import-meta/glob/`) - Lazy glob, eager glob, named import - Negative patterns (`!**/bar.js`) - Multiple patterns (`['./dir/*.js', './other/*.js']`) - [x] **Turbopack snapshot error tests** (`turbopack/crates/turbopack-tests/tests/snapshot/import-meta/glob-error/`) - `as` option error, non-constant `eager` error, unknown option error - [x] **Next.js e2e tests** (`test/e2e/import-meta-glob/`) - Lazy/eager/named import modules, negative patterns, multi-pattern - Passes in both dev (`next dev`) and production (`next build && next start`) modes - Skipped under webpack (`IS_WEBPACK_TEST=1`) since this is a Turbopack-only feature <!-- NEXT_JS_LLM_PR --> --------- Co-authored-by: Tobias Koppers <[email protected]> Co-authored-by: Claude <[email protected]> Co-authored-by: Niklas Mischkulnig <[email protected]>
89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
|
|
// import.meta.glob is a Turbopack-only feature; skip under webpack
|
|
const testFn = process.env.IS_WEBPACK_TEST ? describe.skip : describe
|
|
|
|
testFn('import-meta-glob', () => {
|
|
const { next, skipped } = nextTestSetup({
|
|
files: __dirname,
|
|
skipDeployment: true,
|
|
})
|
|
|
|
if (skipped) return
|
|
|
|
it('should resolve lazy glob modules', async () => {
|
|
const $ = await next.render$('/')
|
|
const lazyKeys = JSON.parse($('#lazy-keys').text())
|
|
expect(lazyKeys).toEqual([
|
|
'./modules/bar.ts',
|
|
'./modules/foo.ts',
|
|
'./modules/skip.ts',
|
|
])
|
|
|
|
const lazyResults = JSON.parse($('#lazy-results').text())
|
|
expect(lazyResults).toEqual({
|
|
'./modules/bar.ts': 'bar',
|
|
'./modules/foo.ts': 'foo',
|
|
'./modules/skip.ts': 'skip',
|
|
})
|
|
})
|
|
|
|
it('should resolve eager glob modules', async () => {
|
|
const $ = await next.render$('/')
|
|
const eagerKeys = JSON.parse($('#eager-keys').text())
|
|
expect(eagerKeys).toEqual([
|
|
'./modules/bar.ts',
|
|
'./modules/foo.ts',
|
|
'./modules/skip.ts',
|
|
])
|
|
|
|
const eagerResults = JSON.parse($('#eager-results').text())
|
|
expect(eagerResults).toEqual({
|
|
'./modules/bar.ts': 'bar',
|
|
'./modules/foo.ts': 'foo',
|
|
'./modules/skip.ts': 'skip',
|
|
})
|
|
})
|
|
|
|
it('should resolve named import glob modules', async () => {
|
|
const $ = await next.render$('/')
|
|
const defaultResults = JSON.parse($('#default-results').text())
|
|
expect(defaultResults).toEqual({
|
|
'./modules/bar.ts': 'bar-value',
|
|
'./modules/foo.ts': 'foo-value',
|
|
'./modules/skip.ts': 'skip-value',
|
|
})
|
|
})
|
|
|
|
it('should support negative patterns', async () => {
|
|
const $ = await next.render$('/')
|
|
const filteredKeys = JSON.parse($('#filtered-keys').text())
|
|
expect(filteredKeys).toEqual(['./modules/bar.ts', './modules/foo.ts'])
|
|
|
|
const filteredResults = JSON.parse($('#filtered-results').text())
|
|
expect(filteredResults).toEqual({
|
|
'./modules/bar.ts': 'bar',
|
|
'./modules/foo.ts': 'foo',
|
|
})
|
|
})
|
|
|
|
it('should support multiple patterns', async () => {
|
|
const $ = await next.render$('/')
|
|
const multiKeys = JSON.parse($('#multi-keys').text())
|
|
expect(multiKeys).toEqual([
|
|
'./modules/bar.ts',
|
|
'./modules/foo.ts',
|
|
'./modules/skip.ts',
|
|
'./other/baz.ts',
|
|
])
|
|
|
|
const multiResults = JSON.parse($('#multi-results').text())
|
|
expect(multiResults).toEqual({
|
|
'./modules/bar.ts': 'bar',
|
|
'./modules/foo.ts': 'foo',
|
|
'./modules/skip.ts': 'skip',
|
|
'./other/baz.ts': 'baz',
|
|
})
|
|
})
|
|
})
|