This PR is a follow-up PR for: https://github.com/tailwindlabs/tailwindcss/pull/17433 In the other PR we allow scanning CSS files for extracting usages of CSS variables. This is important for `.module.css` files that reference these variables but aren't in the same big AST of the main CSS file. This PR also makes sure to watch for changes in those registered CSS files and re-extract the variables when they change. This PR took a bit longer than expected because I was trying to make sure that writing to `./dist/out.css` works without infinite-looping (e.g.: we had issues with this in Tailwind CSS v3 with webpack). But I couldn't reproduce the issue at all. I did had some code that tried to detect if the CSS file contained license headers and skip in (because then it's very likely an output CSS file) but even without it the tests were fine. I setup integration tests with `@tailwindcss/cli` itself, and with tools that use webpack. Added a test for Next.js, and a dedicated webpack test as well. Even without tests, locally, I couldn't reproduce an infinite loop due to changes in an output CSS file... Eventually dropped the code that tries to detect output CSS files. One thing to keep in mind is that if you change any of your "main" CSS files, then we will trigger a full rebuild anyway, so this change is only required for unrelated CSS files (like CSS module files) that use CSS variables. ## Test plan 1. Added integration tests for the CLI and Next.js 2. Added new dedicated test for webpack
356 lines
9.4 KiB
TypeScript
356 lines
9.4 KiB
TypeScript
import { describe } from 'vitest'
|
|
import { candidate, css, fetchStyles, js, json, retryAssertion, test } 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;
|
|
}
|
|
`,
|
|
'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(.637 .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}/,
|
|
])
|
|
},
|
|
)
|
|
|
|
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 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('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:')
|
|
})
|
|
},
|
|
)
|