mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
### What? Adds a new `experimental.turbopackLocalPostcssConfig` option and corresponding `PostCssConfigLocation::LocalPathOrProjectPath` variant so Turbopack can resolve `postcss.config.js` starting from the CSS file's directory first, falling back to the project root. Also fixes an issue that surfaces when multiple per-directory `postcss.config.js` files are used: **Evaluate pool asset name collisions** — output file names for the Node.js evaluate pool are now derived from an xxh3 hash of the full module ident rather than the bare file name, so multiple `postcss.config.js` files in different directories no longer collide. ### Why? Currently Turbopack uses `ProjectPathOrLocalPath` which checks the project root first, then falls back to the local directory. This means per-directory `postcss.config.js` files cannot override the root config — the root config always wins. For projects that need different PostCSS transforms in different directories (e.g. a monorepo with multiple apps or style directories), the more specific config should take precedence. ### How? **Turbopack PostCSS resolution** (`turbopack/crates/turbopack-node/src/transforms/postcss.rs`): - Added `PostCssConfigLocation::LocalPathOrProjectPath` enum variant with doc comments on all variants - Refactored `find_config_in_location()` to express the search order as a `Vec<FileSystemPath>` built from a single `match`, making each variant's strategy immediately clear **Evaluate pool asset file naming** (`turbopack/crates/turbopack-node/src/evaluate.rs`): - Replaced file-name-based output path derivation with a deterministic xxh3 hash of the full module ident - Adds `turbo-tasks-hash` dependency to `turbopack-node` - Eliminates name collisions when multiple entries share the same file name (e.g. per-directory `postcss.config.js` files) **Next.js config flag** (`packages/next/src/server/config-shared.ts`, `config-schema.ts`): - Added `experimental.turbopackLocalPostcssConfig: boolean` option (opt-in, default `false`) - Wired through `crates/next-core/src/next_config.rs` → both client and server contexts (`next_client/context.rs`, `next_server/context.rs`) - When `true`, uses `LocalPathOrProjectPath`; when `false`/unset, uses the existing `ProjectPathOrLocalPath` (no behavior change by default) **E2E test** (`test/e2e/app-dir/turbopack-postcss-multiple-configs/`): - Runs in both **Turbopack dev and production modes** — skips webpack only (webpack does not support function-valued PostCSS plugins and the `turbopackLocalPostcssConfig` flag is Turbopack-specific) - 5 style directories, each with its own `postcss.config.js` passing a **unique color option** (`blue`, `purple`, `orange`, `cyan`, `magenta`) to a shared PostCSS plugin, proving that per-directory config resolution produces distinct results rather than all sharing the same transform - Root `postcss.config.js` is a no-op — if only the root config were used, CSS would remain unmodified - Test validates all 15 elements (5 dirs × 3 files) render with CSS module classes - Test collects all CSS (inline `<style>` tags for dev, linked `.css` files for production) and asserts each directory's expected color is present and `color: red` is absent **Documentation** (`docs/01-app/03-api-reference/05-config/01-next-config-js/turbopackLocalPostcssConfig.mdx`, `docs/01-app/03-api-reference/08-turbopack.mdx`): - New dedicated reference page for `experimental.turbopackLocalPostcssConfig` covering usage, behavior table, and a directory-tree example - Added to the experimental options table in the Turbopack API reference ## Test plan - [x] `cargo check -p turbo-tasks-backend` passes - [x] `cargo check -p turbopack-node` and `cargo check -p next-core` pass - [x] E2E test `turbopack-postcss-multiple-configs` validates per-directory PostCSS config resolution across 5 directories with distinct per-directory colors (Turbopack dev and production modes) - [x] Test correctly skips in webpack mode and deployment - [x] Documentation page and experimental options table updated <!-- NEXT_JS_LLM_PR --> --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Tobias Koppers <[email protected]>
98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
|
|
describe('turbopack-postcss-multiple-configs', () => {
|
|
const { next, isTurbopack, skipped } = nextTestSetup({
|
|
files: __dirname,
|
|
// Per-directory PostCSS config resolution is a Turbopack-only feature
|
|
// (turbopackLocalPostcssConfig). Webpack does not support this feature and
|
|
// does not accept function-valued PostCSS plugins, so skip non-Turbopack runs.
|
|
skipStart: true,
|
|
skipDeployment: true,
|
|
})
|
|
|
|
if (skipped) return
|
|
|
|
if (!isTurbopack) {
|
|
it('should only run with Turbopack', () => {})
|
|
return
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
await next.start()
|
|
})
|
|
|
|
// Each directory's postcss.config.js passes a unique color option to the
|
|
// shared plugin, which replaces `color: red` with the given color.
|
|
// In production mode the CSS minifier may shorten named colors to hex
|
|
// (e.g. blue → #00f), so we match on patterns that cover both forms.
|
|
const DIR_COLORS: Record<number, string | RegExp> = {
|
|
1: /blue|#00f/,
|
|
2: /purple|#800080/,
|
|
3: /orange|#ffa500/,
|
|
4: /cyan|#0ff/,
|
|
5: /magenta|#f0f/,
|
|
}
|
|
|
|
const DIRS = 5
|
|
const FILES_PER_DIR = 3
|
|
|
|
it('should render all elements with CSS module classes applied', async () => {
|
|
const $ = await next.render$('/')
|
|
|
|
for (let dir = 1; dir <= DIRS; dir++) {
|
|
for (let file = 1; file <= FILES_PER_DIR; file++) {
|
|
const padded = String(file).padStart(2, '0')
|
|
const id = `dir${dir}-file${padded}`
|
|
const el = $(`#${id}`)
|
|
expect(el.length).toBe(1)
|
|
expect(el.text().trim()).toBe(`dir${dir} file${padded}`)
|
|
expect(el.attr('class')).toBeTruthy()
|
|
}
|
|
}
|
|
})
|
|
|
|
it('should apply per-directory PostCSS transforms with distinct colors', async () => {
|
|
const cssContent = await collectCss(next)
|
|
|
|
// Each directory's PostCSS config passes a unique color option.
|
|
// Verify every expected color appears in the output.
|
|
for (const [, pattern] of Object.entries(DIR_COLORS)) {
|
|
expect(cssContent).toMatch(pattern)
|
|
}
|
|
|
|
// No original `color: red` should remain — all were transformed.
|
|
expect(cssContent).not.toMatch(/color\s*:\s*red/)
|
|
|
|
// The old hardcoded green should NOT appear, proving options are used.
|
|
expect(cssContent).not.toMatch(/green|#0f0|#008000/)
|
|
})
|
|
})
|
|
|
|
/** Collect all CSS from the page: inline <style> tags + linked .css files. */
|
|
async function collectCss(next: any): Promise<string> {
|
|
const html = await next.render('/')
|
|
const parts: string[] = []
|
|
|
|
// Inline <style> content
|
|
const styleMatches = html.match(/<style[^>]*>([\s\S]*?)<\/style>/gi)
|
|
if (styleMatches) {
|
|
parts.push(...styleMatches)
|
|
}
|
|
|
|
// Linked CSS files
|
|
const hrefMatches = html.match(/href="([^"]*\.css[^"]*)"/gi)
|
|
if (hrefMatches) {
|
|
for (const match of hrefMatches) {
|
|
const href = match.match(/href="([^"]+)"/)?.[1]
|
|
if (href) {
|
|
const res = await next.fetch(href)
|
|
if (res.ok) {
|
|
parts.push(await res.text())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return parts.join('\n')
|
|
}
|