tailwindcss/integrations/vite/multi-root.test.ts
Rózsa Zoltán aa859314d9
feat: add Vite 7 support to the @tailwindcss/vite plugin (#18384)
Closes #18381 

* [Changelog for Vite 7.0.0
(2025-06-24)](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md#700-2025-06-24)

Starting from Vite 7, Node 18 support will be dropped, which doesn't
really affect Tailwind. It might be worth mentioning in the
documentation that the recommended minimum Node versions are 20.19 and
22.12.

Vite 7 is only available in ESM format, which is also not an issue.

Vite's browser support aligns with the v4 guidelines:
```
Chrome 87 → 107       (tw: 111)
Edge 88 → 107         (tw: 111)
Firefox 78 → 104      (tw: 128)
Safari 14.0 → 16.0    (tw: 16.4)
```
* [Vite 7 - Browser
Support](https://vite.dev/guide/migration.html#default-browser-target-change)
* [Tailwind CSS v4 - Browser
Support](https://tailwindcss.com/docs/compatibility#browser-support)

So, at first glance, there's nothing more to do except enabling support
for these versions.

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2025-06-24 12:31:17 -04:00

171 lines
4.7 KiB
TypeScript

import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
test(
`production build`,
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^7"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import path from 'node:path'
import { defineConfig } from 'vite'
export default defineConfig({
build: {
cssMinify: false,
rollupOptions: {
input: {
root1: path.resolve(__dirname, 'root1.html'),
root2: path.resolve(__dirname, 'root2.html'),
},
},
},
plugins: [tailwindcss()],
})
`,
'root1.html': html`
<head>
<link rel="stylesheet" href="./src/root1.css" />
</head>
<body>
<div class="one:underline two:underline">Hello, world!</div>
</body>
`,
'src/shared.css': css`
@reference 'tailwindcss/theme';
@import 'tailwindcss/utilities';
`,
'src/root1.css': css`
@import './shared.css';
@custom-variant one (&:is([data-root='1']));
`,
'root2.html': html`
<head>
<link rel="stylesheet" href="./src/root2.css" />
</head>
<body>
<div class="one:underline two:underline">Hello, world!</div>
</body>
`,
'src/root2.css': css`
@import './shared.css';
@custom-variant two (&:is([data-root='2']));
`,
},
},
async ({ fs, exec, expect }) => {
await exec('pnpm vite build')
let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(2)
let root1 = files.find(([filename]) => filename.includes('root1'))
let root2 = files.find(([filename]) => filename.includes('root2'))
expect(root1).toBeDefined()
expect(root2).toBeDefined()
expect(root1![1]).toContain(candidate`one:underline`)
expect(root1![1]).not.toContain(candidate`two:underline`)
expect(root2![1]).not.toContain(candidate`one:underline`)
expect(root2![1]).toContain(candidate`two:underline`)
},
)
test(
'dev mode',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^7"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import path from 'node:path'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'root1.html': html`
<head>
<link rel="stylesheet" href="./src/root1.css" />
</head>
<body>
<div class="one:underline two:underline">Hello, world!</div>
</body>
`,
'src/shared.css': css`
@reference 'tailwindcss/theme';
@import 'tailwindcss/utilities';
`,
'src/root1.css': css`
@import './shared.css';
@custom-variant one (&:is([data-root='1']));
`,
'root2.html': html`
<head>
<link rel="stylesheet" href="./src/root2.css" />
</head>
<body>
<div class="one:underline two:underline">Hello, world!</div>
</body>
`,
'src/root2.css': css`
@import './shared.css';
@custom-variant two (&:is([data-root='2']));
`,
},
},
async ({ spawn, expect }) => {
let process = await spawn('pnpm vite dev')
await process.onStdout((m) => m.includes('ready in'))
let url = ''
await process.onStdout((m) => {
let match = /Local:\s*(http.*)\//.exec(m)
if (match) url = match[1]
return Boolean(url)
})
// Candidates are resolved lazily, so the first visit of index.html
// will only have candidates from this file.
await retryAssertion(async () => {
let styles = await fetchStyles(url, '/root1.html')
expect(styles).toContain(candidate`one:underline`)
expect(styles).not.toContain(candidate`two:underline`)
})
// Going to about.html will extend the candidate list to include
// candidates from about.html.
await retryAssertion(async () => {
let styles = await fetchStyles(url, '/root2.html')
expect(styles).not.toContain(candidate`one:underline`)
expect(styles).toContain(candidate`two:underline`)
})
},
)