diff --git a/next.config.mjs b/next.config.mjs index a9e0087f92..2049739ab5 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -329,11 +329,12 @@ const nextConfig = { // TODO: Re-enable after fixing all sub-component useTranslations scope issues ignoreBuildErrors: true, }, - webpack(config, { webpack }) { + webpack(config, { dev, webpack }) { config.ignoreWarnings = [ ...(config.ignoreWarnings || []), isNextIntlExtractorDynamicImportWarning, ]; + const nextDefaultSplitChunks = config.optimization?.splitChunks; config.optimization = config.optimization || {}; config.optimization.splitChunks = { ...config.optimization.splitChunks, @@ -392,6 +393,9 @@ const nextConfig = { }, }, }; + // Next's development defaults are tuned for incremental route compilation. + // Retain the custom vendor topology for production without imposing it on dev. + if (dev) config.optimization.splitChunks = nextDefaultSplitChunks; if (isMinimalBuild) { // Mirror the turbopack.resolveAlias entries for webpack-built artifacts. diff --git a/src/app/globals.css b/src/app/globals.css index 939fc9ada7..5a2b558236 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,4 +1,4 @@ -@import "tailwindcss"; +@import "tailwindcss" source(none); @import "fumadocs-ui/css/neutral.css"; @import "fumadocs-ui/css/preset.css"; /* Self-hosted Material Symbols icon font (#3695): the Google Fonts CDN @@ -10,14 +10,12 @@ @import "material-symbols/outlined.css"; @source "../../node_modules/fumadocs-ui/css/generated/*.css"; -/* Tailwind v4 auto-detection cannot scan directories with parentheses - (e.g. Next.js route groups like "(dashboard)"). Explicit @source - directives ensure all utility classes in route groups are included. */ -@source "../app/(dashboard)"; +/* Keep Tailwind v4 from scanning the entire repository. The UI lives in the + App Router and shared component trees; third-party Fumadocs sources remain + explicit because node_modules is ignored by automatic detection. */ +@source "../app"; +@source "../shared"; @source "../../node_modules/fumadocs-ui/dist/**/*.js"; -@source not "../../*.sqlite*"; -@source not "../../.claude*"; -@source not "../../.claude-memory"; @custom-variant dark (&:where(.dark, .dark *)); diff --git a/tests/unit/dev-bundler-boundaries-12074.test.ts b/tests/unit/dev-bundler-boundaries-12074.test.ts new file mode 100644 index 0000000000..9e8c20065f --- /dev/null +++ b/tests/unit/dev-bundler-boundaries-12074.test.ts @@ -0,0 +1,77 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const repoRoot = process.cwd(); +const nextConfigPath = path.join(repoRoot, "next.config.mjs"); + +async function loadNextConfig(label: string) { + return import(`${pathToFileURL(nextConfigPath).href}?case=${label}-${Date.now()}`); +} + +test("Tailwind scans only the UI source roots declared by OmniRoute (#12074 phase 1)", () => { + const css = readFileSync(path.join(repoRoot, "src/app/globals.css"), "utf8"); + + assert.match( + css, + /@import\s+"tailwindcss"\s+source\(none\);/, + "automatic repository-wide Tailwind source detection must stay disabled" + ); + assert.match(css, /@source\s+"\.\.\/app";/, "App Router components must stay scanned"); + assert.match(css, /@source\s+"\.\.\/shared";/, "shared UI components must stay scanned"); + assert.match( + css, + /@source\s+"\.\.\/\.\.\/node_modules\/fumadocs-ui\/dist\/\*\*\/\*\.js";/, + "Fumadocs runtime classes must stay scanned" + ); +}); + +test("webpack dev keeps Next defaults instead of production vendor cache groups (#12074 phase 1)", async () => { + const { default: nextConfig } = await loadNextConfig("dev-split-chunks"); + const originalSplitChunks = { + chunks: "async", + cacheGroups: { + framework: { name: "framework" }, + }, + }; + const config = { + context: repoRoot, + ignoreWarnings: [], + optimization: { splitChunks: originalSplitChunks }, + plugins: [], + }; + + nextConfig.webpack(config, { + dev: true, + isServer: false, + defaultLoaders: { babel: {} }, + webpack: {}, + }); + + assert.equal(config.optimization.splitChunks, originalSplitChunks); + const cacheGroups = config.optimization.splitChunks.cacheGroups as Record; + assert.equal(cacheGroups.recharts, undefined); + assert.equal(cacheGroups.fumadocs, undefined); +}); + +test("webpack production retains OmniRoute vendor cache groups (#12074 phase 1)", async () => { + const { default: nextConfig } = await loadNextConfig("production-split-chunks"); + const config = { + context: repoRoot, + ignoreWarnings: [], + optimization: { splitChunks: { cacheGroups: {} as Record } }, + plugins: [], + }; + + nextConfig.webpack(config, { + dev: false, + isServer: false, + defaultLoaders: { babel: {} }, + webpack: {}, + }); + + assert.ok(config.optimization.splitChunks.cacheGroups.recharts); + assert.ok(config.optimization.splitChunks.cacheGroups.fumadocs); +});