Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure deterministic SSR builds in @tailwindcss/vite #13457

Merged
merged 8 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
76 changes: 52 additions & 24 deletions packages/@tailwindcss-vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,36 @@ 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

let updates: Update[] = []
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
}

Expand Down Expand Up @@ -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
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
// applied in transform.
let result = await transformHandler.call(transformPluginContext, css, id)
if (!result) continue
Expand Down Expand Up @@ -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
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved

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
Expand All @@ -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)
},
},

Expand All @@ -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
Expand All @@ -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
}
},
},
Expand Down
3 changes: 2 additions & 1 deletion playgrounds/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:^",
Expand Down
Loading