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:
parent
9b4e93d808
commit
4a25bc1fd5
3 changed files with 57 additions and 26 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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:^",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue