mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
This PR fixes slow rebuilds on Windows where rebuilds go from ~2s to ~20ms. Fixes: #16911 Fixes: #17522 ## Test plan 1. Tested it on a reproduction with the following results: Before: https://github.com/user-attachments/assets/10c5e9e0-3c41-4e1d-95f6-ee8d856577ef After: https://github.com/user-attachments/assets/2c7597e9-3fff-4922-a2da-a8d06eab9047 Zooming in on the times, it looks like this: <img width="674" alt="image" src="https://github.com/user-attachments/assets/85eee69c-bbf6-4c28-8ce3-6dcdad74be9c" /> But with these changes: <img width="719" alt="image" src="https://github.com/user-attachments/assets/d89cefda-0711-4f84-bfaf-2bea11977bf7" /> We also tested this on Windows with the following results: Before: <img width="961" alt="image" src="https://github.com/user-attachments/assets/3a42f822-f103-4598-9a91-e659ae09800c" /> After: <img width="956" alt="image" src="https://github.com/user-attachments/assets/05b6b6bc-d107-40d1-a207-3638aba3fc3a" /> [ci-all] --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
487 lines
13 KiB
TypeScript
487 lines
13 KiB
TypeScript
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 (
|
|
<html>
|
|
<body>{children}</body>
|
|
</html>
|
|
)
|
|
}
|
|
`,
|
|
'app/page.js': js`
|
|
import styles from './page.module.css'
|
|
export default function Page() {
|
|
return (
|
|
<h1 className={styles.heading + ' text-3xl font-bold underline'}>Hello, Next.js!</h1>
|
|
)
|
|
}
|
|
`,
|
|
'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 (
|
|
<html>
|
|
<body>{children}</body>
|
|
</html>
|
|
)
|
|
}
|
|
`,
|
|
'app/page.js': js`
|
|
import styles from './page.module.css'
|
|
export default function Page() {
|
|
return <h1 className={styles.heading + ' underline'}>Hello, Next.js!</h1>
|
|
}
|
|
`,
|
|
'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 <h1 className={styles.heading + ' underline bg-red-500'}>Hello, Next.js!</h1>
|
|
}
|
|
`,
|
|
)
|
|
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 <h1 className="content-['[slug]']">Hello, Next.js!</h1>
|
|
}
|
|
`,
|
|
'app/b/[...slug]/page.js': js`
|
|
export default function Page() {
|
|
return <h1 className="content-['[...slug]']">Hello, Next.js!</h1>
|
|
}
|
|
`,
|
|
'app/c/[[...slug]]/page.js': js`
|
|
export default function Page() {
|
|
return <h1 className="content-['[[...slug]]']">Hello, Next.js!</h1>
|
|
}
|
|
`,
|
|
'app/d/(theme)/page.js': js`
|
|
export default function Page() {
|
|
return <h1 className="content-['(theme)']">Hello, Next.js!</h1>
|
|
}
|
|
`,
|
|
'app/layout.js': js`
|
|
import './globals.css'
|
|
|
|
export default function RootLayout({ children }) {
|
|
return (
|
|
<html>
|
|
<body>{children}</body>
|
|
</html>
|
|
)
|
|
}
|
|
`,
|
|
'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 (
|
|
<html>
|
|
<body>{children}</body>
|
|
</html>
|
|
)
|
|
}
|
|
`,
|
|
'app/page.js': js`
|
|
export default function Page() {
|
|
return <div className="flex"></div>
|
|
}
|
|
`,
|
|
'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 (
|
|
<html>
|
|
<body>{children}</body>
|
|
</html>
|
|
)
|
|
}
|
|
`,
|
|
'app/page.js': js`
|
|
export default function Page() {
|
|
return <div className="flex"></div>
|
|
}
|
|
`,
|
|
'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 <div className="flex underline"></div>
|
|
}
|
|
`,
|
|
)
|
|
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')
|
|
},
|
|
)
|