Skip to content

Commit 8395e3f

Browse files
Add a test case for moving a file from being a Tailwind root back to a normal CSS file and back
1 parent e452c72 commit 8395e3f

File tree

2 files changed

+142
-27
lines changed

2 files changed

+142
-27
lines changed

integrations/vite/index.test.ts

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ import {
4747
})
4848
`,
4949
'project-a/index.html': html`
50-
<head>
50+
<head>
5151
<link rel="stylesheet" href="./src/index.css" />
5252
</head>
5353
<body>
5454
<div class="underline m-2">Hello, world!</div>
5555
</body>
56-
`,
56+
`,
5757
'project-a/src/index.css': css`
5858
@import 'tailwindcss/theme' theme(reference);
5959
@import 'tailwindcss/utilities';
@@ -113,21 +113,21 @@ import {
113113
})
114114
`,
115115
'project-a/index.html': html`
116-
<head>
116+
<head>
117117
<link rel="stylesheet" href="./src/index.css" />
118118
</head>
119119
<body>
120120
<div class="underline">Hello, world!</div>
121121
</body>
122-
`,
122+
`,
123123
'project-a/about.html': html`
124-
<head>
124+
<head>
125125
<link rel="stylesheet" href="./src/index.css" />
126126
</head>
127127
<body>
128128
<div class="font-bold">Tailwind Labs</div>
129129
</body>
130-
`,
130+
`,
131131
'project-a/src/index.css': css`
132132
@import 'tailwindcss/theme' theme(reference);
133133
@import 'tailwindcss/utilities';
@@ -165,13 +165,13 @@ import {
165165
await fs.write(
166166
'project-a/index.html',
167167
html`
168-
<head>
168+
<head>
169169
<link rel="stylesheet" href="./src/index.css" />
170170
</head>
171171
<body>
172172
<div class="underline m-2">Hello, world!</div>
173173
</body>
174-
`,
174+
`,
175175
)
176176
await retryAssertion(async () => {
177177
let css = await fetchStyles(port)
@@ -347,3 +347,77 @@ import {
347347
)
348348
})
349349
})
350+
351+
test(
352+
`demote Tailwind roots to regular CSS files and back to Tailwind roots while restoring all candidates`,
353+
{
354+
fs: {
355+
'package.json': json`
356+
{
357+
"type": "module",
358+
"dependencies": {
359+
"@tailwindcss/vite": "workspace:^",
360+
"tailwindcss": "workspace:^"
361+
},
362+
"devDependencies": {
363+
"vite": "^5.3.5"
364+
}
365+
}
366+
`,
367+
'vite.config.ts': ts`
368+
import tailwindcss from '@tailwindcss/vite'
369+
import { defineConfig } from 'vite'
370+
371+
export default defineConfig({
372+
build: { cssMinify: false },
373+
plugins: [tailwindcss()],
374+
})
375+
`,
376+
'index.html': html`
377+
<head>
378+
<link rel="stylesheet" href="./src/index.css" />
379+
</head>
380+
<body>
381+
<div class="underline">Hello, world!</div>
382+
</body>
383+
`,
384+
'about.html': html`
385+
<head>
386+
<link rel="stylesheet" href="./src/index.css" />
387+
</head>
388+
<body>
389+
<div class="font-bold">Tailwind Labs</div>
390+
</body>
391+
`,
392+
'src/index.css': css`@import 'tailwindcss';`,
393+
},
394+
},
395+
async ({ spawn, getFreePort, fs }) => {
396+
let port = await getFreePort()
397+
await spawn(`pnpm vite dev --port ${port}`)
398+
399+
// Candidates are resolved lazily, so the first visit of index.html
400+
// will only have candidates from this file.
401+
await retryAssertion(async () => {
402+
let css = await fetchStyles(port, '/index.html')
403+
expect(css).toContain(candidate`underline`)
404+
expect(css).not.toContain(candidate`font-bold`)
405+
})
406+
407+
// Going to about.html will extend the candidate list to include
408+
// candidates from about.html.
409+
await retryAssertion(async () => {
410+
let css = await fetchStyles(port, '/about.html')
411+
expect(css).toContain(candidate`underline`)
412+
expect(css).toContain(candidate`font-bold`)
413+
})
414+
415+
// We change the CSS file so it is no longer a valid Tailwind root.
416+
await fs.write('src/index.css', css`@import 'tailwindcss';`)
417+
await retryAssertion(async () => {
418+
let css = await fetchStyles(port)
419+
expect(css).toContain(candidate`underline`)
420+
expect(css).toContain(candidate`font-bold`)
421+
})
422+
},
423+
)

packages/@tailwindcss-vite/src/index.ts

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,27 @@ export default function tailwindcss(): Plugin[] {
8080

8181
constructor(private id: string) {}
8282

83-
public async generate(content: string, addWatchFile: (file: string) => void) {
83+
// Generate the CSS for the root file. This can return false if the file is
84+
// not considered a Tailwind root. When this happened, the root can be GCed.
85+
public async generate(
86+
content: string,
87+
addWatchFile: (file: string) => void,
88+
): Promise<string | false> {
8489
await import('@tailwindcss/node/esm-cache-hook')
8590

8691
this.lastContent = content
8792

8893
let inputPath = idToPath(this.id)
8994
let inputBase = path.dirname(path.resolve(inputPath))
9095

91-
if (this.compiler === null || this.scanner === null || this.rebuildStrategy === 'full') {
96+
let compiler = this.compiler
97+
let scanner = this.scanner
98+
99+
if (compiler === null || scanner === null || this.rebuildStrategy === 'full') {
92100
this.rebuildStrategy = 'incremental'
93101
clearRequireCache(Array.from(this.dependencies))
94102
this.dependencies = new Set([idToPath(inputPath)])
95103

96-
// TODO: Move this out of here, we need to check wether something is a
97-
// valid root on the flattened file
98104
let postcssCompiled = await postcss([
99105
postcssImport({
100106
load: (path) => {
@@ -110,7 +116,15 @@ export default function tailwindcss(): Plugin[] {
110116
})
111117
let css = postcssCompiled.css
112118

113-
this.compiler = await compile(css, {
119+
// This is done inside the Root#generate() method so that we can later
120+
// use information from the Tailwind compiler to determine if the file
121+
// is a CSS root (necessary because we will probably inline the
122+
// `@import` resolution at some point).
123+
if (!isCssRootFile(css)) {
124+
return false
125+
}
126+
127+
compiler = await compile(css, {
114128
loadPlugin: async (pluginPath) => {
115129
if (pluginPath[0] !== '.') {
116130
return import(pluginPath).then((m) => m.default ?? m)
@@ -150,17 +164,20 @@ export default function tailwindcss(): Plugin[] {
150164
return module.default ?? module
151165
},
152166
})
167+
this.compiler = compiler
153168

154-
this.scanner = new Scanner({
169+
scanner = new Scanner({
155170
sources: this.compiler.globs.map((pattern) => ({
156171
base: inputBase, // Globs are relative to the input.css file
157172
pattern,
158173
})),
159174
})
175+
this.scanner
160176
}
161177

162178
if (!this.scanner || !this.compiler) {
163-
// TODO: This is only here for TypeScript
179+
// TypeScript does not properly refine the scanner and compiler
180+
// properties (even when extracted into a variable)
164181
throw new Error('Tailwind CSS compiler is not initialized.')
165182
}
166183

@@ -253,7 +270,11 @@ export default function tailwindcss(): Plugin[] {
253270

254271
async function regenerateOptimizedCss(root: Root, addWatchFile: (file: string) => void) {
255272
let content = root.lastContent
256-
return optimizeCss(await root.generate(content, addWatchFile), { minify })
273+
let generated = await root.generate(content, addWatchFile)
274+
if (generated === false) {
275+
return
276+
}
277+
return optimizeCss(generated, { minify })
257278
}
258279

259280
// Manually run the transform functions of non-Tailwind plugins on the given CSS
@@ -337,7 +358,7 @@ export default function tailwindcss(): Plugin[] {
337358
enforce: 'pre',
338359

339360
async transform(src, id, options) {
340-
if (!isTailwindCssFile(id, src)) return
361+
if (!isPotentialCssRootFile(id)) return
341362

342363
let root = roots.get(id)
343364
if (!root) {
@@ -358,9 +379,13 @@ export default function tailwindcss(): Plugin[] {
358379
}
359380

360381
// TODO: This had a call to `transformWithPlugins`. Since we're now in
361-
// pre, we might not need it
362-
let code = await root.generate(src, (file) => this.addWatchFile(file))
363-
return { code }
382+
// pre, we might not need it. Validate this.
383+
let generated = await root.generate(src, (file) => this.addWatchFile(file))
384+
if (!generated) {
385+
roots.delete(id)
386+
return src
387+
}
388+
return { code: generated }
364389
},
365390
},
366391

@@ -371,7 +396,7 @@ export default function tailwindcss(): Plugin[] {
371396
enforce: 'pre',
372397

373398
async transform(src, id) {
374-
if (!isTailwindCssFile(id, src)) return
399+
if (!isPotentialCssRootFile(id)) return
375400

376401
let root = roots.get(id)
377402
if (!root) {
@@ -387,21 +412,29 @@ export default function tailwindcss(): Plugin[] {
387412
// TODO: This had a call to `transformWithPlugins`. Since we always do
388413
// a transform step in the renderStart for all roots anyways, this might
389414
// not be necessary
390-
let code = await root.generate(src, (file) => this.addWatchFile(file))
391-
return { code }
415+
let generated = await root.generate(src, (file) => this.addWatchFile(file))
416+
if (!generated) {
417+
roots.delete(id)
418+
return src
419+
}
420+
return { code: generated }
392421
},
393422

394423
// `renderStart` runs in the bundle generation stage after all transforms.
395424
// We must run before `enforce: post` so the updated chunks are picked up
396425
// by vite:css-post.
397426
async renderStart() {
398427
for (let [id, root] of roots.entries()) {
399-
let css = await regenerateOptimizedCss(root, (file) => this.addWatchFile(file))
428+
let generated = await regenerateOptimizedCss(root, (file) => this.addWatchFile(file))
429+
if (!generated) {
430+
roots.delete(id)
431+
continue
432+
}
400433

401434
// These plugins have side effects which, during build, results in CSS
402435
// being written to the output dir. We need to run them here to ensure
403436
// the CSS is written before the bundle is generated.
404-
await transformWithPlugins(this, id, css)
437+
await transformWithPlugins(this, id, generated)
405438
}
406439
},
407440
},
@@ -413,13 +446,21 @@ function getExtension(id: string) {
413446
return path.extname(filename).slice(1)
414447
}
415448

416-
// TODO: This needs to run on the `@import`-flattened file
417-
function isTailwindCssFile(id: string, src: string) {
449+
function isPotentialCssRootFile(id: string) {
418450
let extension = getExtension(id)
419451
let isCssFile = extension === 'css' || (extension === 'vue' && id.includes('&lang.css'))
420452
return isCssFile
421453
}
422454

455+
function isCssRootFile(content: string) {
456+
return (
457+
content.includes('@tailwind') ||
458+
content.includes('@config') ||
459+
content.includes('@plugin') ||
460+
content.includes('@apply')
461+
)
462+
}
463+
423464
function optimizeCss(
424465
input: string,
425466
{ file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {},

0 commit comments

Comments
 (0)