diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 0471429b0ac88..6564c30a60e2e 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -966,6 +966,73 @@ export default async function getBaseWebpackConfig( } } + const frameworkCacheGroup = { + chunks: 'all' as const, + name: 'framework', + // Ensures the framework chunk is not created for App Router. + layer: isWebpackDefaultLayer, + test(module: any) { + const resource = module.nameForCondition?.() + return resource + ? topLevelFrameworkPaths.some((pkgPath) => + resource.startsWith(pkgPath) + ) + : false + }, + priority: 40, + // Don't let webpack eliminate this chunk (prevents this chunk from + // becoming a part of the commons chunk) + enforce: true, + } + const libCacheGroup = { + test(module: { + size: Function + nameForCondition: Function + }): boolean { + return ( + module.size() > 160000 && + /node_modules[/\\]/.test(module.nameForCondition() || '') + ) + }, + name(module: { + layer: string | null | undefined + type: string + libIdent?: Function + updateHash: (hash: crypto.Hash) => void + }): string { + const hash = crypto.createHash('sha1') + if (isModuleCSS(module)) { + module.updateHash(hash) + } else { + if (!module.libIdent) { + throw new Error( + `Encountered unknown module type: ${module.type}. Please open an issue.` + ) + } + hash.update(module.libIdent({ context: dir })) + } + + // Ensures the name of the chunk is not the same between two modules in different layers + // E.g. if you import 'button-library' in App Router and Pages Router we don't want these to be bundled in the same chunk + // as they're never used on the same page. + if (module.layer) { + hash.update(module.layer) + } + + return hash.digest('hex').substring(0, 8) + }, + priority: 30, + minChunks: 1, + reuseExistingChunk: true, + } + const cssCacheGroup = { + test: /\.(css|sass|scss)$/i, + chunks: 'all' as const, + enforce: true, + type: /css/, + minChunks: 2, + priority: 100, + } return { // Keep main and _app chunks unsplitted in webpack 5 // as we don't need a separate vendor chunk from that @@ -973,67 +1040,16 @@ export default async function getBaseWebpackConfig( // duplication that need to be pulled out. chunks: (chunk: any) => !/^(polyfills|main|pages\/_app)$/.test(chunk.name), - cacheGroups: { - framework: { - chunks: 'all', - name: 'framework', - // Ensures the framework chunk is not created for App Router. - layer: isWebpackDefaultLayer, - test(module: any) { - const resource = module.nameForCondition?.() - return resource - ? topLevelFrameworkPaths.some((pkgPath) => - resource.startsWith(pkgPath) - ) - : false - }, - priority: 40, - // Don't let webpack eliminate this chunk (prevents this chunk from - // becoming a part of the commons chunk) - enforce: true, - }, - lib: { - test(module: { - size: Function - nameForCondition: Function - }): boolean { - return ( - module.size() > 160000 && - /node_modules[/\\]/.test(module.nameForCondition() || '') - ) - }, - name(module: { - layer: string | null | undefined - type: string - libIdent?: Function - updateHash: (hash: crypto.Hash) => void - }): string { - const hash = crypto.createHash('sha1') - if (isModuleCSS(module)) { - module.updateHash(hash) - } else { - if (!module.libIdent) { - throw new Error( - `Encountered unknown module type: ${module.type}. Please open an issue.` - ) - } - hash.update(module.libIdent({ context: dir })) - } - - // Ensures the name of the chunk is not the same between two modules in different layers - // E.g. if you import 'button-library' in App Router and Pages Router we don't want these to be bundled in the same chunk - // as they're never used on the same page. - if (module.layer) { - hash.update(module.layer) - } - - return hash.digest('hex').substring(0, 8) + cacheGroups: appDir + ? { + css: cssCacheGroup, + framework: frameworkCacheGroup, + lib: libCacheGroup, + } + : { + framework: frameworkCacheGroup, + lib: libCacheGroup, }, - priority: 30, - minChunks: 1, - reuseExistingChunk: true, - }, - }, maxInitialRequests: 25, minSize: 20000, } diff --git a/test/e2e/app-dir/css-order/app/base-scss.module.scss b/test/e2e/app-dir/css-order/app/base-scss.module.scss new file mode 100644 index 0000000000000..6067b55d88d63 --- /dev/null +++ b/test/e2e/app-dir/css-order/app/base-scss.module.scss @@ -0,0 +1,5 @@ +$base1: rgb(255, 1, 0); + +.base { + color: $base1; +} diff --git a/test/e2e/app-dir/css-order/app/base.module.css b/test/e2e/app-dir/css-order/app/base.module.css new file mode 100644 index 0000000000000..546b7a181f5b7 --- /dev/null +++ b/test/e2e/app-dir/css-order/app/base.module.css @@ -0,0 +1,3 @@ +.base { + color: rgb(255, 0, 0); +} diff --git a/test/e2e/app-dir/css-order/app/base2-scss.module.scss b/test/e2e/app-dir/css-order/app/base2-scss.module.scss new file mode 100644 index 0000000000000..9484078ecb60c --- /dev/null +++ b/test/e2e/app-dir/css-order/app/base2-scss.module.scss @@ -0,0 +1,3 @@ +.base { + color: rgb(255, 3, 0); +} diff --git a/test/e2e/app-dir/css-order/app/base2.module.css b/test/e2e/app-dir/css-order/app/base2.module.css new file mode 100644 index 0000000000000..755c87eccb2aa --- /dev/null +++ b/test/e2e/app-dir/css-order/app/base2.module.css @@ -0,0 +1,3 @@ +.base { + color: rgb(255, 2, 0); +} diff --git a/test/e2e/app-dir/css-order/app/first-client/page.tsx b/test/e2e/app-dir/css-order/app/first-client/page.tsx new file mode 100644 index 0000000000000..591cf63428b0b --- /dev/null +++ b/test/e2e/app-dir/css-order/app/first-client/page.tsx @@ -0,0 +1,34 @@ +'use client' + +import Link from 'next/link' +import baseStyle from '../base2.module.css' +import baseStyle2 from '../base2-scss.module.scss' +import style from './style.module.css' + +export default function Page() { + return ( +
+

+ hello world +

+ + First + + + First client + + + Second + + + Second client + + + Third + +
+ ) +} diff --git a/test/e2e/app-dir/css-order/app/first-client/style.module.css b/test/e2e/app-dir/css-order/app/first-client/style.module.css new file mode 100644 index 0000000000000..ff118a9778c2e --- /dev/null +++ b/test/e2e/app-dir/css-order/app/first-client/style.module.css @@ -0,0 +1,4 @@ +.name { + composes: base from '../base.module.css'; + color: rgb(255, 0, 255); +} diff --git a/test/e2e/app-dir/css-order/app/first/page.tsx b/test/e2e/app-dir/css-order/app/first/page.tsx new file mode 100644 index 0000000000000..d8ad145fa2622 --- /dev/null +++ b/test/e2e/app-dir/css-order/app/first/page.tsx @@ -0,0 +1,32 @@ +import Link from 'next/link' +import baseStyle from '../base2.module.css' +import baseStyle2 from '../base2-scss.module.scss' +import style from './style.module.css' + +export default function Page() { + return ( +
+

+ hello world +

+ + First + + + First client + + + Second + + + Second client + + + Third + +
+ ) +} diff --git a/test/e2e/app-dir/css-order/app/first/style.module.css b/test/e2e/app-dir/css-order/app/first/style.module.css new file mode 100644 index 0000000000000..90026c621f188 --- /dev/null +++ b/test/e2e/app-dir/css-order/app/first/style.module.css @@ -0,0 +1,4 @@ +.name { + composes: base from '../base.module.css'; + color: blue; +} diff --git a/test/e2e/app-dir/css-order/app/layout.tsx b/test/e2e/app-dir/css-order/app/layout.tsx new file mode 100644 index 0000000000000..e7077399c03ce --- /dev/null +++ b/test/e2e/app-dir/css-order/app/layout.tsx @@ -0,0 +1,7 @@ +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/css-order/app/page.tsx b/test/e2e/app-dir/css-order/app/page.tsx new file mode 100644 index 0000000000000..136769782c178 --- /dev/null +++ b/test/e2e/app-dir/css-order/app/page.tsx @@ -0,0 +1,11 @@ +import Link from 'next/link' + +export default function Page() { + return ( +
+

hello world

+ First + Second +
+ ) +} diff --git a/test/e2e/app-dir/css-order/app/second-client/page.tsx b/test/e2e/app-dir/css-order/app/second-client/page.tsx new file mode 100644 index 0000000000000..c2442d3d7829c --- /dev/null +++ b/test/e2e/app-dir/css-order/app/second-client/page.tsx @@ -0,0 +1,34 @@ +'use client' + +import Link from 'next/link' +import baseStyle from '../base2.module.css' +import baseStyle2 from '../base2-scss.module.scss' +import style from './style.module.css' + +export default function Page() { + return ( +
+

+ hello world +

+ + First + + + First client + + + Second + + + Second client + + + Third + +
+ ) +} diff --git a/test/e2e/app-dir/css-order/app/second-client/style.module.css b/test/e2e/app-dir/css-order/app/second-client/style.module.css new file mode 100644 index 0000000000000..08841ce8710ce --- /dev/null +++ b/test/e2e/app-dir/css-order/app/second-client/style.module.css @@ -0,0 +1,4 @@ +.name { + composes: base from '../base.module.css'; + color: rgb(255, 128, 0); +} diff --git a/test/e2e/app-dir/css-order/app/second/page.tsx b/test/e2e/app-dir/css-order/app/second/page.tsx new file mode 100644 index 0000000000000..ac8870b490e38 --- /dev/null +++ b/test/e2e/app-dir/css-order/app/second/page.tsx @@ -0,0 +1,32 @@ +import Link from 'next/link' +import baseStyle from '../base2.module.css' +import baseStyle2 from '../base2-scss.module.scss' +import style from './style.module.scss' + +export default function Page() { + return ( +
+

+ hello world +

+ + First + + + First client + + + Second + + + Second client + + + Third + +
+ ) +} diff --git a/test/e2e/app-dir/css-order/app/second/style.module.scss b/test/e2e/app-dir/css-order/app/second/style.module.scss new file mode 100644 index 0000000000000..92a409e7791cf --- /dev/null +++ b/test/e2e/app-dir/css-order/app/second/style.module.scss @@ -0,0 +1,4 @@ +.name { + composes: base from '../base-scss.module.scss'; + color: green; +} diff --git a/test/e2e/app-dir/css-order/app/third/page.tsx b/test/e2e/app-dir/css-order/app/third/page.tsx new file mode 100644 index 0000000000000..cd5c620743825 --- /dev/null +++ b/test/e2e/app-dir/css-order/app/third/page.tsx @@ -0,0 +1,32 @@ +import Link from 'next/link' +import baseStyle from '../base2.module.css' +import baseStyle2 from '../base2-scss.module.scss' +import style from './style.module.scss' + +export default function Page() { + return ( +
+

+ hello world +

+ + First + + + First client + + + Second + + + Second client + + + Third + +
+ ) +} diff --git a/test/e2e/app-dir/css-order/app/third/style.module.scss b/test/e2e/app-dir/css-order/app/third/style.module.scss new file mode 100644 index 0000000000000..35e2d2d900d67 --- /dev/null +++ b/test/e2e/app-dir/css-order/app/third/style.module.scss @@ -0,0 +1,4 @@ +.name { + composes: base from '../base-scss.module.scss'; + color: rgb(0, 128, 128); +} diff --git a/test/e2e/app-dir/css-order/css-order.test.ts b/test/e2e/app-dir/css-order/css-order.test.ts new file mode 100644 index 0000000000000..8b5afb3d2a54d --- /dev/null +++ b/test/e2e/app-dir/css-order/css-order.test.ts @@ -0,0 +1,144 @@ +import { createNextDescribe } from 'e2e-utils' + +function getPairs(all) { + const result = [] + + for (const first of all) { + for (const second of all) { + if (first === second) { + continue + } + result.push([first, second]) + } + } + + return result +} + +const PAGES = { + first: { + url: '/first', + selector: '#hello1', + color: 'rgb(0, 0, 255)', + }, + second: { + url: '/second', + selector: '#hello2', + color: 'rgb(0, 128, 0)', + }, + third: { + url: '/third', + selector: '#hello3', + color: 'rgb(0, 128, 128)', + }, + 'first-client': { + url: '/first-client', + selector: '#hello1c', + color: 'rgb(255, 0, 255)', + }, + 'second-client': { + url: '/second-client', + selector: '#hello2c', + color: 'rgb(255, 128, 0)', + }, +} + +const allPairs = getPairs(Object.keys(PAGES)) + +createNextDescribe( + 'css-order', + { + files: __dirname, + dependencies: { + sass: 'latest', + }, + }, + ({ next, isNextDev, isTurbopack }) => { + for (const ordering of allPairs) { + const name = `should load correct styles navigating back again ${ordering.join( + ' -> ' + )} -> ${ordering.join(' -> ')}` + // TODO fix this case + const broken = isNextDev + if (broken) { + it.todo(name) + continue + } + it(name, async () => { + const start = PAGES[ordering[0]] + const browser = await next.browser(start.url) + const check = async (pageInfo) => { + expect( + await browser + .waitForElementByCss(pageInfo.selector) + .getComputedCss('color') + ).toBe(pageInfo.color) + } + const navigate = async (page) => { + await browser.waitForElementByCss('#' + page).click() + } + await check(start) + for (const page of ordering.slice(1)) { + await navigate(page) + await check(PAGES[page]) + } + for (const page of ordering) { + await navigate(page) + await check(PAGES[page]) + } + }) + } + for (const ordering of allPairs) { + const name = `should load correct styles navigating ${ordering.join( + ' -> ' + )}` + // TODO fix this case + const broken = + isNextDev && + !isTurbopack && + ordering.some( + (page) => page.includes('client') || page.includes('first') + ) + if (broken) { + it.todo(name) + continue + } + it(name, async () => { + const start = PAGES[ordering[0]] + const browser = await next.browser(start.url) + const check = async (pageInfo) => { + expect( + await browser + .waitForElementByCss(pageInfo.selector) + .getComputedCss('color') + ).toBe(pageInfo.color) + } + const navigate = async (page) => { + await browser.waitForElementByCss('#' + page).click() + } + await check(start) + for (const page of ordering.slice(1)) { + await navigate(page) + await check(PAGES[page]) + } + }) + } + for (const [page, pageInfo] of Object.entries(PAGES)) { + const name = `should load correct styles on ${page}` + // TODO fix this case + const broken = isNextDev && !isTurbopack && page.includes('client') + if (broken) { + it.todo(name) + continue + } + it(name, async () => { + const browser = await next.browser(pageInfo.url) + expect( + await browser + .waitForElementByCss(pageInfo.selector) + .getComputedCss('color') + ).toBe(pageInfo.color) + }) + } + } +) diff --git a/test/e2e/app-dir/css-order/next.config.js b/test/e2e/app-dir/css-order/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/e2e/app-dir/css-order/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/turbopack-tests-manifest.json b/test/turbopack-tests-manifest.json index 6620b1141bd96..3a948a3b1c049 100644 --- a/test/turbopack-tests-manifest.json +++ b/test/turbopack-tests-manifest.json @@ -3233,6 +3233,39 @@ "flakey": [], "runtimeError": false }, + "test/e2e/app-dir/css-order/css-order.test.ts": { + "passed": [ + "css-order should load correct styles navigating first -> first-client", + "css-order should load correct styles navigating first -> second-client", + "css-order should load correct styles navigating first -> second", + "css-order should load correct styles navigating first -> third", + "css-order should load correct styles navigating first-client -> first", + "css-order should load correct styles navigating first-client -> second-client", + "css-order should load correct styles navigating first-client -> second", + "css-order should load correct styles navigating first-client -> third", + "css-order should load correct styles navigating second -> first-client", + "css-order should load correct styles navigating second -> first", + "css-order should load correct styles navigating second -> second-client", + "css-order should load correct styles navigating second -> third", + "css-order should load correct styles navigating second-client -> first-client", + "css-order should load correct styles navigating second-client -> first", + "css-order should load correct styles navigating second-client -> second", + "css-order should load correct styles navigating second-client -> third", + "css-order should load correct styles navigating third -> first-client", + "css-order should load correct styles navigating third -> first", + "css-order should load correct styles navigating third -> second-client", + "css-order should load correct styles navigating third -> second", + "css-order should load correct styles on first-client", + "css-order should load correct styles on first", + "css-order should load correct styles on second-client", + "css-order should load correct styles on second", + "css-order should load correct styles on third" + ], + "failed": [], + "pending": [], + "flakey": [], + "runtimeError": false + }, "test/e2e/app-dir/dynamic-data/dynamic-data.test.ts": { "passed": [ "dynamic-data inside cache scope displays redbox when accessing dynamic data inside a cache scope",