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 <jordan@cryptica.me>
This commit is contained in:
Robin Malfait 2024-04-09 00:10:08 +02:00 • committed by GitHub
parent 9b4e93d808
commit 4a25bc1fd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 57 additions and 26 deletions

View file

@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- Nothing yet!
### Fixed
- Ensure deterministic SSR builds in `@tailwindcss/vite` ([#13457](https://github.com/tailwindlabs/tailwindcss/pull/13457))
## [4.0.0-alpha.13] - 2024-04-04

View file

@ -7,14 +7,21 @@ import type { Plugin, Rollup, Update, ViteDevServer } from 'vite'
export default function tailwindcss(): Plugin[] {
let server: ViteDevServer | null = null
let candidates = new Set<string>()
// In serve mode, we treat this as a set, storing storing empty strings.
// In serve mode this is treated as a set — the content doesn't matter.
// In build mode, we store file contents to use them in renderChunk.
let cssModules: Record<string, string> = {}
let cssModules: Record<
string,
{
content: string
handled: boolean
}
> = {}
let isSSR = false
let minify = false
let cssPlugins: readonly Plugin[] = []
// Trigger update to all CSS modules
function updateCssModules() {
function updateCssModules(isSSR: boolean) {
// If we're building then we don't need to update anything
if (!server) return
@ -22,9 +29,14 @@ export default function tailwindcss(): Plugin[] {
for (let id of Object.keys(cssModules)) {
let cssModule = server.moduleGraph.getModuleById(id)
if (!cssModule) {
// It is safe to remove the item here since we're iterating on a copy of
// the keys.
delete cssModules[id]
// Note: Removing this during SSR is not safe and will produce
// inconsistent results based on the timing of the removal and
// the order / timing of transforms.
if (!isSSR) {
// It is safe to remove the item here since we're iterating on a copy
// of the keys.
delete cssModules[id]
}
continue
}
@ -85,7 +97,7 @@ export default function tailwindcss(): Plugin[] {
try {
// Directly call the plugin's transform function to process the
// generated CSS. In build mode, this updates the chunks later used to
// generate the bundle. In serve mode, the transformed souce should be
// generate the bundle. In serve mode, the transformed source should be
// applied in transform.
let result = await transformHandler.call(transformPluginContext, css, id)
if (!result) continue
@ -113,16 +125,21 @@ export default function tailwindcss(): Plugin[] {
async configResolved(config) {
minify = config.build.cssMinify !== false
// Apply the vite:css plugin to generated CSS for transformations like
// URL path rewriting and image inlining.
//
// In build mode, since renderChunk runs after all transformations, we
// need to also apply vite:css-post.
cssPlugins = config.plugins.filter((plugin) =>
['vite:css', ...(config.command === 'build' ? ['vite:css-post'] : [])].includes(
plugin.name,
),
)
isSSR = config.build.ssr !== false && config.build.ssr !== undefined
let allowedPlugins = [
// Apply the vite:css plugin to generated CSS for transformations like
// URL path rewriting and image inlining.
'vite:css',
// In build mode, since renderChunk runs after all transformations, we
// need to also apply vite:css-post.
...(config.command === 'build' ? ['vite:css-post'] : []),
]
cssPlugins = config.plugins.filter((plugin) => {
return allowedPlugins.includes(plugin.name)
})
},
// Scan index.html for candidates
@ -134,18 +151,18 @@ export default function tailwindcss(): Plugin[] {
// CSS update will cause an infinite loop. We only trigger if the
// candidates have been updated.
if (updated) {
updateCssModules()
updateCssModules(isSSR)
}
},
// Scan all non-CSS files for candidates
transform(src, id) {
transform(src, id, options) {
if (id.includes('/.vite/')) return
let extension = getExtension(id)
if (extension === '' || extension === 'css') return
scan(src, extension)
updateCssModules()
updateCssModules(options?.ssr ?? false)
},
},
@ -163,7 +180,7 @@ export default function tailwindcss(): Plugin[] {
if (!isTailwindCssFile(id, src)) return
// In serve mode, we treat cssModules as a set, ignoring the value.
cssModules[id] = ''
cssModules[id] = { content: '', handled: true }
if (!options?.ssr) {
// Wait until all other files have been processed, so we can extract
@ -184,15 +201,26 @@ export default function tailwindcss(): Plugin[] {
transform(src, id) {
if (!isTailwindCssFile(id, src)) return
cssModules[id] = src
cssModules[id] = { content: src, handled: false }
},
// renderChunk runs in the bundle generation stage after all transforms.
// We must run before `enforce: post` so the updated chunks are picked up
// by vite:css-post.
async renderChunk(_code, _chunk) {
for (let [cssFile, css] of Object.entries(cssModules)) {
await transformWithPlugins(this, cssFile, generateOptimizedCss(css))
for (let [id, file] of Object.entries(cssModules)) {
if (file.handled) {
continue
}
let css = generateOptimizedCss(file.content)
// These plugins have side effects which, during build, results in CSS
// being written to the output dir. We need to run them here to ensure
// the CSS is written before the bundle is generated.
await transformWithPlugins(this, id, css)
file.handled = true
}
},
},

View file

@ -5,7 +5,8 @@
"scripts": {
"lint": "tsc --noEmit",
"dev": "bun --bun vite ./src --config ./vite.config.ts",
"build": "bun --bun vite build ./src --outDir ../dist --config ./vite.config.ts --emptyOutDir"
"build": "bun --bun vite build ./src --outDir ../dist --config ./vite.config.ts --emptyOutDir",
"preview": "bun --bun vite preview"
},
"dependencies": {
"@tailwindcss/vite": "workspace:^",