Skip to content

Commit 4a25bc1

Browse files
Ensure deterministic SSR builds in @tailwindcss/vite (#13457)
* ensure we wait in the `build` step as well It looks like when running `astro build` we only run this `build` step and not the `dev` step where we already use the `waitForRequestsIdle` code. Adding this to the `build` part as well does generate the correct result. * update changelog * fix typo * add comment * Don’t run transforms more than necessary * Don’t remove modules from the graph during SSR * Update changelog * Add `preview` script --------- Co-authored-by: Jordan Pittman <[email protected]>
1 parent 9b4e93d commit 4a25bc1

File tree

3 files changed

+57
-26
lines changed

3 files changed

+57
-26
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Ensure deterministic SSR builds in `@tailwindcss/vite` ([#13457](https://github.com/tailwindlabs/tailwindcss/pull/13457))
1113

1214
## [4.0.0-alpha.13] - 2024-04-04
1315

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

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,36 @@ import type { Plugin, Rollup, Update, ViteDevServer } from 'vite'
77
export default function tailwindcss(): Plugin[] {
88
let server: ViteDevServer | null = null
99
let candidates = new Set<string>()
10-
// In serve mode, we treat this as a set, storing storing empty strings.
10+
// In serve mode this is treated as a set — the content doesn't matter.
1111
// In build mode, we store file contents to use them in renderChunk.
12-
let cssModules: Record<string, string> = {}
12+
let cssModules: Record<
13+
string,
14+
{
15+
content: string
16+
handled: boolean
17+
}
18+
> = {}
19+
let isSSR = false
1320
let minify = false
1421
let cssPlugins: readonly Plugin[] = []
1522

1623
// Trigger update to all CSS modules
17-
function updateCssModules() {
24+
function updateCssModules(isSSR: boolean) {
1825
// If we're building then we don't need to update anything
1926
if (!server) return
2027

2128
let updates: Update[] = []
2229
for (let id of Object.keys(cssModules)) {
2330
let cssModule = server.moduleGraph.getModuleById(id)
2431
if (!cssModule) {
25-
// It is safe to remove the item here since we're iterating on a copy of
26-
// the keys.
27-
delete cssModules[id]
32+
// Note: Removing this during SSR is not safe and will produce
33+
// inconsistent results based on the timing of the removal and
34+
// the order / timing of transforms.
35+
if (!isSSR) {
36+
// It is safe to remove the item here since we're iterating on a copy
37+
// of the keys.
38+
delete cssModules[id]
39+
}
2840
continue
2941
}
3042

@@ -85,7 +97,7 @@ export default function tailwindcss(): Plugin[] {
8597
try {
8698
// Directly call the plugin's transform function to process the
8799
// generated CSS. In build mode, this updates the chunks later used to
88-
// generate the bundle. In serve mode, the transformed souce should be
100+
// generate the bundle. In serve mode, the transformed source should be
89101
// applied in transform.
90102
let result = await transformHandler.call(transformPluginContext, css, id)
91103
if (!result) continue
@@ -113,16 +125,21 @@ export default function tailwindcss(): Plugin[] {
113125

114126
async configResolved(config) {
115127
minify = config.build.cssMinify !== false
116-
// Apply the vite:css plugin to generated CSS for transformations like
117-
// URL path rewriting and image inlining.
118-
//
119-
// In build mode, since renderChunk runs after all transformations, we
120-
// need to also apply vite:css-post.
121-
cssPlugins = config.plugins.filter((plugin) =>
122-
['vite:css', ...(config.command === 'build' ? ['vite:css-post'] : [])].includes(
123-
plugin.name,
124-
),
125-
)
128+
isSSR = config.build.ssr !== false && config.build.ssr !== undefined
129+
130+
let allowedPlugins = [
131+
// Apply the vite:css plugin to generated CSS for transformations like
132+
// URL path rewriting and image inlining.
133+
'vite:css',
134+
135+
// In build mode, since renderChunk runs after all transformations, we
136+
// need to also apply vite:css-post.
137+
...(config.command === 'build' ? ['vite:css-post'] : []),
138+
]
139+
140+
cssPlugins = config.plugins.filter((plugin) => {
141+
return allowedPlugins.includes(plugin.name)
142+
})
126143
},
127144

128145
// Scan index.html for candidates
@@ -134,18 +151,18 @@ export default function tailwindcss(): Plugin[] {
134151
// CSS update will cause an infinite loop. We only trigger if the
135152
// candidates have been updated.
136153
if (updated) {
137-
updateCssModules()
154+
updateCssModules(isSSR)
138155
}
139156
},
140157

141158
// Scan all non-CSS files for candidates
142-
transform(src, id) {
159+
transform(src, id, options) {
143160
if (id.includes('/.vite/')) return
144161
let extension = getExtension(id)
145162
if (extension === '' || extension === 'css') return
146163

147164
scan(src, extension)
148-
updateCssModules()
165+
updateCssModules(options?.ssr ?? false)
149166
},
150167
},
151168

@@ -163,7 +180,7 @@ export default function tailwindcss(): Plugin[] {
163180
if (!isTailwindCssFile(id, src)) return
164181

165182
// In serve mode, we treat cssModules as a set, ignoring the value.
166-
cssModules[id] = ''
183+
cssModules[id] = { content: '', handled: true }
167184

168185
if (!options?.ssr) {
169186
// Wait until all other files have been processed, so we can extract
@@ -184,15 +201,26 @@ export default function tailwindcss(): Plugin[] {
184201

185202
transform(src, id) {
186203
if (!isTailwindCssFile(id, src)) return
187-
cssModules[id] = src
204+
cssModules[id] = { content: src, handled: false }
188205
},
189206

190207
// renderChunk runs in the bundle generation stage after all transforms.
191208
// We must run before `enforce: post` so the updated chunks are picked up
192209
// by vite:css-post.
193210
async renderChunk(_code, _chunk) {
194-
for (let [cssFile, css] of Object.entries(cssModules)) {
195-
await transformWithPlugins(this, cssFile, generateOptimizedCss(css))
211+
for (let [id, file] of Object.entries(cssModules)) {
212+
if (file.handled) {
213+
continue
214+
}
215+
216+
let css = generateOptimizedCss(file.content)
217+
218+
// These plugins have side effects which, during build, results in CSS
219+
// being written to the output dir. We need to run them here to ensure
220+
// the CSS is written before the bundle is generated.
221+
await transformWithPlugins(this, id, css)
222+
223+
file.handled = true
196224
}
197225
},
198226
},

playgrounds/vite/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"scripts": {
66
"lint": "tsc --noEmit",
77
"dev": "bun --bun vite ./src --config ./vite.config.ts",
8-
"build": "bun --bun vite build ./src --outDir ../dist --config ./vite.config.ts --emptyOutDir"
8+
"build": "bun --bun vite build ./src --outDir ../dist --config ./vite.config.ts --emptyOutDir",
9+
"preview": "bun --bun vite preview"
910
},
1011
"dependencies": {
1112
"@tailwindcss/vite": "workspace:^",

0 commit comments

Comments
 (0)