Skip to content

Commit 15fc7f4

Browse files
philipp-spiessthecrypticaceadamwathan
authored
Apply non-Tailwind CSS transforms in Vite plugin (#14871)
Fixes: #14839 Fixes: #14796 This PR fixes an issue in the Vite extension where we previously only ran a small list of allow-listed plugins for the second stage transform in the build step. This caused some CSS features to unexpectedly not work in production builds (one such example is Vue's `:deep(...)` selector). To fix this, I changed the allow listed plugins that we do want to run to a block list to filter out some plugins we know we don't want to run (e.g. the Tailwind Vite plugin for example or some built-in Vite plugins that are not necessary). ## Test plan This PR adds a new integration test suite to test interop with a custom Vite transformer that looks like this: ```js { name: 'recolor', transform(code, id) { if (id.includes('.css')) { return code.replace(/red/g, 'blue') } }, } ``` I also validated that this does indeed fix the Vue `:deep(...)` selector related issue that we were seeing by copying the repro of #14839 into our playground: ![Screenshot 2024-11-05 at 13.35.26.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/0Y77ilPI2WoJfMLFiAEw/4e46ab61-4acf-461a-9e40-f7c9ec3c69b2.png) You can see in the screenshot above that the `:deep()` selector overwrites the scoped styles as expected in both the dev mode and the prod build (screenshotted). Furthermore I reproduced the issue reported in #14796 and was able to confirm that in a production build, the styling works as expected: <img width="517" alt="Screenshot 2024-11-06 at 14 26 50" src="https://github.com/user-attachments/assets/ade6fe38-be0d-4bd0-9a9a-67b6fec05ae0"> Lastly, I created a repository out of the biggest known-to-me Vite projects: [Astro, Nuxt, Remix, SolidStart, and SvelteKit](https://github.com/philipp-spiess/tailwind-playgrounds) and verified that both dev and prod builds show no issue and the candidate list is properly appended in each case. --------- Co-authored-by: Jordan Pittman <[email protected]> Co-authored-by: Adam Wathan <[email protected]>
1 parent e82b316 commit 15fc7f4

File tree

6 files changed

+272
-94
lines changed

6 files changed

+272
-94
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
- Ensure `url(…)` containing special characters such as `;` or `{}` end up in one declaration ([#14879](https://github.com/tailwindlabs/tailwindcss/pull/14879))
2929
- Ensure adjacent rules are merged together after handling nesting when generating optimized CSS ([#14873](https://github.com/tailwindlabs/tailwindcss/pull/14873))
3030
- Rebase `url()` inside imported CSS files when using Vite ([#14877](https://github.com/tailwindlabs/tailwindcss/pull/14877))
31+
- Ensure that CSS transforms from other Vite plugins correctly work in full builds (e.g. `:deep()` in Vue) ([#14871](https://github.com/tailwindlabs/tailwindcss/pull/14871))
3132
- _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830))
3233
- _Upgrade (experimental)_: Remove whitespace around `,` separator when print arbitrary values ([#14838](https://github.com/tailwindlabs/tailwindcss/pull/14838))
3334
- _Upgrade (experimental)_: Fix crash during upgrade when content globs escape root of project ([#14896](https://github.com/tailwindlabs/tailwindcss/pull/14896))

integrations/vite/nuxt.test.ts

Lines changed: 60 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,74 @@
11
import { expect } from 'vitest'
22
import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
33

4-
test(
5-
'dev mode',
6-
{
7-
fs: {
8-
'package.json': json`
9-
{
10-
"type": "module",
11-
"dependencies": {
12-
"@tailwindcss/vite": "workspace:^",
13-
"nuxt": "^3.13.1",
14-
"tailwindcss": "workspace:^",
15-
"vue": "latest"
16-
}
4+
const SETUP = {
5+
fs: {
6+
'package.json': json`
7+
{
8+
"type": "module",
9+
"dependencies": {
10+
"@tailwindcss/vite": "workspace:^",
11+
"nuxt": "^3.13.1",
12+
"tailwindcss": "workspace:^",
13+
"vue": "latest"
1714
}
18-
`,
19-
'nuxt.config.ts': ts`
20-
import tailwindcss from '@tailwindcss/vite'
15+
}
16+
`,
17+
'nuxt.config.ts': ts`
18+
import tailwindcss from '@tailwindcss/vite'
2119
22-
// https://nuxt.com/docs/api/configuration/nuxt-config
23-
export default defineNuxtConfig({
24-
vite: {
25-
plugins: [tailwindcss()],
26-
},
20+
// https://nuxt.com/docs/api/configuration/nuxt-config
21+
export default defineNuxtConfig({
22+
vite: {
23+
plugins: [tailwindcss()],
24+
},
2725
28-
css: ['~/assets/css/main.css'],
29-
devtools: { enabled: true },
30-
compatibilityDate: '2024-08-30',
31-
})
32-
`,
33-
'app.vue': html`
26+
css: ['~/assets/css/main.css'],
27+
devtools: { enabled: true },
28+
compatibilityDate: '2024-08-30',
29+
})
30+
`,
31+
'app.vue': html`
3432
<template>
35-
<div class="underline">Hello world!</div>
36-
</template>
33+
<div class="underline">Hello world!</div>
34+
</template>
3735
`,
38-
'assets/css/main.css': css`@import 'tailwindcss';`,
39-
},
36+
'assets/css/main.css': css`@import 'tailwindcss';`,
4037
},
41-
async ({ fs, spawn, getFreePort }) => {
42-
let port = await getFreePort()
43-
await spawn(`pnpm nuxt dev --port ${port}`)
38+
}
39+
40+
test('dev mode', SETUP, async ({ fs, spawn, getFreePort }) => {
41+
let port = await getFreePort()
42+
await spawn(`pnpm nuxt dev --port ${port}`)
4443

45-
await retryAssertion(async () => {
46-
let css = await fetchStyles(port)
47-
expect(css).toContain(candidate`underline`)
48-
})
44+
await retryAssertion(async () => {
45+
let css = await fetchStyles(port)
46+
expect(css).toContain(candidate`underline`)
47+
})
4948

50-
await retryAssertion(async () => {
51-
await fs.write(
52-
'app.vue',
53-
html`
49+
await retryAssertion(async () => {
50+
await fs.write(
51+
'app.vue',
52+
html`
5453
<template>
55-
<div class="underline font-bold">Hello world!</div>
56-
</template>
54+
<div class="underline font-bold">Hello world!</div>
55+
</template>
5756
`,
58-
)
57+
)
5958

60-
let css = await fetchStyles(port)
61-
expect(css).toContain(candidate`underline`)
62-
expect(css).toContain(candidate`font-bold`)
63-
})
64-
},
65-
)
59+
let css = await fetchStyles(port)
60+
expect(css).toContain(candidate`underline`)
61+
expect(css).toContain(candidate`font-bold`)
62+
})
63+
})
64+
65+
test('build', SETUP, async ({ spawn, getFreePort, exec }) => {
66+
let port = await getFreePort()
67+
await exec(`pnpm nuxt build`)
68+
await spawn(`PORT=${port} pnpm nuxt preview`)
69+
70+
await retryAssertion(async () => {
71+
let css = await fetchStyles(port)
72+
expect(css).toContain(candidate`underline`)
73+
})
74+
})
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import dedent from 'dedent'
2+
import { describe, expect } from 'vitest'
3+
import { css, fetchStyles, html, retryAssertion, test, ts, txt } from '../utils'
4+
5+
function createSetup(transformer: 'postcss' | 'lightningcss') {
6+
return {
7+
fs: {
8+
'package.json': txt`
9+
{
10+
"type": "module",
11+
"dependencies": {
12+
"@tailwindcss/vite": "workspace:^",
13+
"tailwindcss": "workspace:^"
14+
},
15+
"devDependencies": {
16+
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
17+
"vite": "^5.3.5"
18+
}
19+
}
20+
`,
21+
'vite.config.ts': ts`
22+
import tailwindcss from '@tailwindcss/vite'
23+
import { defineConfig } from 'vite'
24+
25+
export default defineConfig({
26+
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
27+
build: { cssMinify: false },
28+
plugins: [
29+
tailwindcss(),
30+
{
31+
name: 'recolor',
32+
transform(code, id) {
33+
if (id.includes('.css')) {
34+
return code.replace(/red;/g, 'blue;')
35+
}
36+
},
37+
},
38+
],
39+
})
40+
`,
41+
'index.html': html`
42+
<head>
43+
<link rel="stylesheet" href="./src/index.css" />
44+
</head>
45+
<body>
46+
<div class="foo [background-color:red]">Hello, world!</div>
47+
</body>
48+
`,
49+
'src/index.css': css`
50+
@import 'tailwindcss/theme' theme(reference);
51+
@import 'tailwindcss/utilities';
52+
53+
.foo {
54+
color: red;
55+
}
56+
`,
57+
},
58+
}
59+
}
60+
61+
for (let transformer of ['postcss', 'lightningcss'] as const) {
62+
describe(transformer, () => {
63+
test(`production build`, createSetup(transformer), async ({ fs, exec }) => {
64+
await exec('pnpm vite build')
65+
66+
let files = await fs.glob('dist/**/*.css')
67+
expect(files).toHaveLength(1)
68+
let [filename] = files[0]
69+
70+
await fs.expectFileToContain(filename, [
71+
css`
72+
.foo {
73+
color: blue;
74+
}
75+
`,
76+
// Running the transforms on utilities generated by Tailwind might change in the future
77+
dedent`
78+
.\[background-color\:red\] {
79+
background-color: blue;
80+
}
81+
`,
82+
])
83+
})
84+
85+
test(`dev mode`, createSetup(transformer), async ({ spawn, getFreePort, fs }) => {
86+
let port = await getFreePort()
87+
await spawn(`pnpm vite dev --port ${port}`)
88+
89+
await retryAssertion(async () => {
90+
let styles = await fetchStyles(port, '/index.html')
91+
expect(styles).toContain(css`
92+
.foo {
93+
color: blue;
94+
}
95+
`)
96+
// Running the transforms on utilities generated by Tailwind might change in the future
97+
expect(styles).toContain(dedent`
98+
.\[background-color\:red\] {
99+
background-color: blue;
100+
}
101+
`)
102+
})
103+
104+
await retryAssertion(async () => {
105+
await fs.write(
106+
'src/index.css',
107+
css`
108+
@import 'tailwindcss/theme' theme(reference);
109+
@import 'tailwindcss/utilities';
110+
111+
.foo {
112+
background-color: red;
113+
}
114+
`,
115+
)
116+
117+
let styles = await fetchStyles(port)
118+
expect(styles).toContain(css`
119+
.foo {
120+
background-color: blue;
121+
}
122+
`)
123+
})
124+
})
125+
126+
test('watch mode', createSetup(transformer), async ({ spawn, fs }) => {
127+
await spawn(`pnpm vite build --watch`)
128+
129+
await retryAssertion(async () => {
130+
let files = await fs.glob('dist/**/*.css')
131+
expect(files).toHaveLength(1)
132+
let [, styles] = files[0]
133+
134+
expect(styles).toContain(css`
135+
.foo {
136+
color: blue;
137+
}
138+
`)
139+
// Running the transforms on utilities generated by Tailwind might change in the future
140+
expect(styles).toContain(dedent`
141+
.\[background-color\:red\] {
142+
background-color: blue;
143+
}
144+
`)
145+
})
146+
147+
await retryAssertion(async () => {
148+
await fs.write(
149+
'src/index.css',
150+
css`
151+
@import 'tailwindcss/theme' theme(reference);
152+
@import 'tailwindcss/utilities';
153+
154+
.foo {
155+
background-color: red;
156+
}
157+
`,
158+
)
159+
160+
let files = await fs.glob('dist/**/*.css')
161+
expect(files).toHaveLength(1)
162+
let [, styles] = files[0]
163+
164+
expect(styles).toContain(css`
165+
.foo {
166+
background-color: blue;
167+
}
168+
`)
169+
})
170+
})
171+
})
172+
}

integrations/vite/vue.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,15 @@ test(
5151
@apply text-red-500;
5252
}
5353
</style>
54-
54+
<style scoped>
55+
@import 'tailwindcss/utilities';
56+
@import 'tailwindcss/theme' theme(reference);
57+
:deep(.bar) {
58+
color: red;
59+
}
60+
</style>
5561
<template>
56-
<div class="underline foo">Hello Vue!</div>
62+
<div class="underline foo bar">Hello Vue!</div>
5763
</template>
5864
`,
5965
},
@@ -65,5 +71,6 @@ test(
6571
expect(files).toHaveLength(1)
6672

6773
await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`foo`])
74+
await fs.expectFileToContain(files[0][0], ['.bar{'])
6875
},
6976
)

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

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@ export default function tailwindcss(): Plugin[] {
1515
let isSSR = false
1616
let minify = false
1717

18-
// A list of css plugins defined in the Vite config. We need to retain these
19-
// so that we can rerun the right transformations in build mode where we have
20-
// to manually rebuild the css file after the compilation is done.
21-
let cssPlugins: readonly Plugin[] = []
22-
2318
// The Vite extension has two types of sources for candidates:
2419
//
2520
// 1. The module graph: These are all modules that vite transforms and we want
@@ -109,8 +104,31 @@ export default function tailwindcss(): Plugin[] {
109104
},
110105
}
111106

112-
for (let plugin of cssPlugins) {
107+
for (let plugin of config!.plugins) {
113108
if (!plugin.transform) continue
109+
110+
if (plugin.name.startsWith('@tailwindcss/')) {
111+
// We do not run any Tailwind transforms anymore
112+
continue
113+
} else if (
114+
plugin.name.startsWith('vite:') &&
115+
// Apply the vite:css plugin to generated CSS for transformations like
116+
// URL path rewriting and image inlining.
117+
plugin.name !== 'vite:css' &&
118+
// In build mode, since `renderStart` runs after all transformations, we
119+
// need to also apply vite:css-post.
120+
plugin.name !== 'vite:css-post' &&
121+
// The vite:vue plugin handles CSS specific post-processing for Vue
122+
plugin.name !== 'vite:vue'
123+
) {
124+
continue
125+
} else if (plugin.name === 'ssr-styles') {
126+
// The Nuxt ssr-styles plugin emits styles from server-side rendered
127+
// components, we can't run it in the `renderStart` phase so we're
128+
// skipping it.
129+
continue
130+
}
131+
114132
let transformHandler =
115133
'handler' in plugin.transform! ? plugin.transform.handler : plugin.transform!
116134

@@ -147,20 +165,6 @@ export default function tailwindcss(): Plugin[] {
147165
config = _config
148166
minify = config.build.cssMinify !== false
149167
isSSR = config.build.ssr !== false && config.build.ssr !== undefined
150-
151-
let allowedPlugins = [
152-
// Apply the vite:css plugin to generated CSS for transformations like
153-
// URL path rewriting and image inlining.
154-
'vite:css',
155-
156-
// In build mode, since renderChunk runs after all transformations, we
157-
// need to also apply vite:css-post.
158-
...(config.command === 'build' ? ['vite:css-post'] : []),
159-
]
160-
161-
cssPlugins = config.plugins.filter((plugin) => {
162-
return allowedPlugins.includes(plugin.name)
163-
})
164168
},
165169

166170
// Scan all non-CSS files for candidates

0 commit comments

Comments
 (0)