import { describe } from 'vitest' import { candidate, css, fetchStyles, js, json, jsx, retryAssertion, test, txt } from '../utils' test( 'production build', { fs: { 'package.json': json` { "dependencies": { "react": "^18", "react-dom": "^18", "next": "^14" }, "devDependencies": { "@tailwindcss/postcss": "workspace:^", "tailwindcss": "workspace:^" } } `, 'postcss.config.mjs': js` export default { plugins: { '@tailwindcss/postcss': {}, }, } `, 'next.config.mjs': js`export default {}`, 'app/layout.js': js` import './globals.css' export default function RootLayout({ children }) { return ( {children} ) } `, 'app/page.js': js` import styles from './page.module.css' export default function Page() { return (

Hello, Next.js!

) } `, 'app/page.module.css': css` @reference './globals.css'; .heading { @apply text-red-500 animate-ping skew-7; } `, 'app/globals.css': css` @reference 'tailwindcss/theme'; @import 'tailwindcss/utilities'; `, }, }, async ({ fs, exec, expect }) => { await exec('pnpm next build') let files = await fs.glob('.next/static/css/**/*.css') expect(files).toHaveLength(2) let globalCss: string | null = null let moduleCss: string | null = null for (let [filename, content] of files) { if (content.includes('@keyframes page_ping')) moduleCss = filename else globalCss = filename } await fs.expectFileToContain(globalCss!, [ candidate`underline`, candidate`font-bold`, candidate`text-3xl`, ]) await fs.expectFileToContain(moduleCss!, [ 'color:var(--color-red-500,oklch(63.7% .237 25.331)', 'animation:var(--animate-ping,ping 1s cubic-bezier(0,0,.2,1) infinite)', /@keyframes page_ping.*{75%,to{transform:scale\(2\);opacity:0}/, '--tw-skew-x:skewX(7deg);', ]) }, ) describe.each(['turbo', 'webpack'])('%s', (bundler) => { test( 'dev mode', { fs: { 'package.json': json` { "dependencies": { "react": "^18", "react-dom": "^18", "next": "^14" }, "devDependencies": { "@tailwindcss/postcss": "workspace:^", "tailwindcss": "workspace:^" } } `, 'postcss.config.mjs': js` export default { plugins: { '@tailwindcss/postcss': {}, }, } `, 'next.config.mjs': js`export default {}`, 'app/layout.js': js` import './globals.css' export default function RootLayout({ children }) { return ( {children} ) } `, 'app/page.js': js` import styles from './page.module.css' export default function Page() { return

Hello, Next.js!

} `, 'app/page.module.css': css` @reference './globals.css'; .heading { @apply text-red-500 animate-ping skew-7 content-['module']; } `, 'app/globals.css': css` @reference 'tailwindcss/theme'; @import 'tailwindcss/utilities'; `, }, }, async ({ fs, spawn, expect }) => { let process = await spawn(`pnpm next dev ${bundler === 'turbo' ? '--turbo' : ''}`) let url = '' await process.onStdout((m) => { let match = /Local:\s*(http.*)/.exec(m) if (match) url = match[1] return Boolean(url) }) await process.onStdout((m) => m.includes('Ready in')) await retryAssertion(async () => { let css = await fetchStyles(url) expect(css).toContain(candidate`underline`) expect(css).toContain('content: var(--tw-content)') expect(css).toContain('@keyframes') }) await fs.write( 'app/page.js', js` import styles from './page.module.css' export default function Page() { return

Hello, Next.js!

} `, ) await process.onStdout((m) => m.includes('Compiled in')) await retryAssertion(async () => { let css = await fetchStyles(url) expect(css).toContain(candidate`underline`) expect(css).toContain(candidate`bg-red-500`) expect(css).toContain('--tw-skew-x: skewX(7deg);') expect(css).toContain('content: var(--tw-content)') expect(css).toContain('@keyframes') }) }, ) }) test( 'should scan dynamic route segments', { fs: { 'package.json': json` { "dependencies": { "react": "^18", "react-dom": "^18", "next": "^14" }, "devDependencies": { "@tailwindcss/postcss": "workspace:^", "tailwindcss": "workspace:^" } } `, 'postcss.config.mjs': js` export default { plugins: { '@tailwindcss/postcss': {}, }, } `, 'next.config.mjs': js`export default {}`, 'app/a/[slug]/page.js': js` export default function Page() { return

Hello, Next.js!

} `, 'app/b/[...slug]/page.js': js` export default function Page() { return

Hello, Next.js!

} `, 'app/c/[[...slug]]/page.js': js` export default function Page() { return

Hello, Next.js!

} `, 'app/d/(theme)/page.js': js` export default function Page() { return

Hello, Next.js!

} `, 'app/layout.js': js` import './globals.css' export default function RootLayout({ children }) { return ( {children} ) } `, 'app/globals.css': css` @import 'tailwindcss/utilities' source(none); @source './**/*.{js,ts,jsx,tsx,mdx}'; `, }, }, async ({ fs, exec, expect }) => { await exec('pnpm next build') let files = await fs.glob('.next/static/css/**/*.css') expect(files).toHaveLength(1) let [filename] = files[0] await fs.expectFileToContain(filename, [ candidate`content-['[slug]']`, candidate`content-['[...slug]']`, candidate`content-['[[...slug]]']`, candidate`content-['(theme)']`, ]) }, ) test( 'changes to CSS files should pick up new CSS variables (if any)', { fs: { 'package.json': json` { "dependencies": { "react": "^18", "react-dom": "^18", "next": "^14" }, "devDependencies": { "@tailwindcss/postcss": "workspace:^", "tailwindcss": "workspace:^" } } `, 'postcss.config.mjs': js` export default { plugins: { '@tailwindcss/postcss': {}, }, } `, 'next.config.mjs': js`export default {}`, 'app/layout.js': js` import './globals.css' export default function RootLayout({ children }) { return ( {children} ) } `, 'app/page.js': js` export default function Page() { return
} `, 'unrelated.module.css': css` .module { color: var(--color-blue-500); } `, 'app/globals.css': css` @import 'tailwindcss/theme'; @import 'tailwindcss/utilities'; `, }, }, async ({ spawn, exec, fs, expect }) => { // Generate the initial build so output CSS files exist on disk await exec('pnpm next build') // NOTE: We are writing to an output CSS file which is not being ignored by // `.gitignore` nor marked with `@source not`. This should not result in an // infinite loop. let process = await spawn(`pnpm next dev`) let url = '' await process.onStdout((m) => { let match = /Local:\s*(http.*)/.exec(m) if (match) url = match[1] return Boolean(url) }) await process.onStdout((m) => m.includes('Ready in')) await retryAssertion(async () => { let css = await fetchStyles(url) expect(css).toContain(candidate`flex`) expect(css).toContain('--color-blue-500:') expect(css).not.toContain('--color-red-500:') }) await fs.write( 'unrelated.module.css', css` .module { color: var(--color-blue-500); background-color: var(--color-red-500); } `, ) await process.onStdout((m) => m.includes('Compiled in')) await retryAssertion(async () => { let css = await fetchStyles(url) expect(css).toContain(candidate`flex`) expect(css).toContain('--color-blue-500:') expect(css).toContain('--color-red-500:') }) }, ) test( 'changes to `public/` should not trigger an infinite loop', { fs: { 'package.json': json` { "dependencies": { "react": "^18", "react-dom": "^18", "next": "^15", "@ducanh2912/next-pwa": "^10.2.9" }, "devDependencies": { "@tailwindcss/postcss": "workspace:^", "tailwindcss": "workspace:^" } } `, '.gitignore': txt` .next/ public/workbox-*.js public/sw.js `, 'postcss.config.mjs': js` export default { plugins: { '@tailwindcss/postcss': {}, }, } `, 'next.config.mjs': js` import withPWA from '@ducanh2912/next-pwa' const pwaConfig = { dest: 'public', register: true, skipWaiting: true, reloadOnOnline: false, cleanupOutdatedCaches: true, clientsClaim: true, maximumFileSizeToCacheInBytes: 20 * 1024 * 1024, } const nextConfig = {} const configWithPWA = withPWA(pwaConfig)(nextConfig) export default configWithPWA `, 'app/layout.js': js` import './globals.css' export default function RootLayout({ children }) { return ( {children} ) } `, 'app/page.js': js` export default function Page() { return
} `, 'app/globals.css': css` @import 'tailwindcss/theme'; @import 'tailwindcss/utilities'; `, }, }, async ({ spawn, fs, expect }) => { let process = await spawn('pnpm next dev') let url = '' await process.onStdout((m) => { let match = /Local:\s*(http.*)/.exec(m) if (match) url = match[1] return Boolean(url) }) await process.onStdout((m) => m.includes('Ready in')) await retryAssertion(async () => { let css = await fetchStyles(url) expect(css).toContain(candidate`flex`) expect(css).not.toContain(candidate`underline`) }) await fs.write( 'app/page.js', jsx` export default function Page() { return
} `, ) await process.onStdout((m) => m.includes('Compiled in')) await retryAssertion(async () => { let css = await fetchStyles(url) expect(css).toContain(candidate`flex`) expect(css).toContain(candidate`underline`) }) // Flush all existing messages in the queue process.flush() // Fetch the styles one more time, to ensure we see the latest version of // the CSS await fetchStyles(url) // At this point, no changes should triger a compile step. If we see any // changes, there is an infinite loop because we (the user) didn't write any // files to disk. // // Ensure there are no more changes in stdout (signaling no infinite loop) let result = await Promise.race([ // If this succeeds, it means that it saw another change which indicates // an infinite loop. process.onStdout((m) => m.includes('Compiled in')).then(() => 'infinite loop detected'), // There should be no changes in stdout new Promise((resolve) => setTimeout(() => resolve('no infinite loop detected'), 2_000)), ]) expect(result).toBe('no infinite loop detected') }, )