Vite: Process <style> blocks inside Svelte files as a post-processor (#15436)

This PR changes the Svelte integration to be a post-processor similar to
what we're doing for `<style>` blocks in Astro and Vue files.

More details can be found in the GitHub discussion:
https://github.com/sveltejs/svelte/discussions/14668#discussioncomment-11620743
This commit is contained in:
Philipp Spiess 2025-01-09 17:16:33 +01:00 committed by GitHub
parent a11c80d6c6
commit c766d7e274
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 18 additions and 139 deletions

View File

@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Removed `--container-prose` in favor of a deprecated `--max-width-prose` theme variable so that `*-prose` is only available for max-width utilities and only for backward compatibility ([#15439](https://github.com/tailwindlabs/tailwindcss/pull/15439))
- Use Vite post-processor APIs for processing Svelte `<style>` blocks ([#15436](https://github.com/tailwindlabs/tailwindcss/pull/15436))
## [4.0.0-beta.8] - 2024-12-17

View File

@ -57,19 +57,19 @@ test(
<h1 class="global local underline">Hello {name}!</h1>
<style>
@import 'tailwindcss' reference;
@import './other.css';
@reference 'tailwindcss';
</style>
`,
'src/other.css': css`
.local {
@apply text-red-500;
animation: 2s ease-in-out 0s infinite localKeyframes;
animation: 2s ease-in-out infinite localKeyframes;
}
:global(.global) {
@apply text-green-500;
animation: 2s ease-in-out 0s infinite globalKeyframes;
animation: 2s ease-in-out infinite globalKeyframes;
}
@keyframes -global-globalKeyframes {
@ -93,18 +93,21 @@ test(
},
},
async ({ exec, fs, expect }) => {
await exec('pnpm vite build')
let output = await exec('pnpm vite build')
let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
await fs.expectFileToContain(files[0][0], [
candidate`underline`,
'.global{color:var(--color-green-500);animation:2s ease-in-out 0s infinite globalKeyframes}',
/\.local.svelte-.*\{color:var\(--color-red-500\);animation:2s ease-in-out 0s infinite svelte-.*-localKeyframes\}/,
'.global{color:var(--color-green-500);animation:2s ease-in-out infinite globalKeyframes}',
/\.local.svelte-.*\{color:var\(--color-red-500\);animation:2s ease-in-out infinite svelte-.*-localKeyframes\}/,
/@keyframes globalKeyframes\{/,
/@keyframes svelte-.*-localKeyframes\{/,
])
// Should not print any warnings
expect(output).not.toContain('vite-plugin-svelte')
},
)
@ -164,20 +167,20 @@ test(
<h1 class="local global underline">Hello {name}!</h1>
<style>
@import 'tailwindcss' reference;
@import './other.css';
@reference 'tailwindcss';
</style>
`,
'src/index.css': css` @import 'tailwindcss'; `,
'src/other.css': css`
.local {
@apply text-red-500;
animation: 2s ease-in-out 0s infinite localKeyframes;
animation: 2s ease-in-out infinite localKeyframes;
}
:global(.global) {
@apply text-green-500;
animation: 2s ease-in-out 0s infinite globalKeyframes;
animation: 2s ease-in-out infinite globalKeyframes;
}
@keyframes -global-globalKeyframes {
@ -210,10 +213,10 @@ test(
let [, css] = files[0]
expect(css).toContain(candidate`underline`)
expect(css).toContain(
'.global{color:var(--color-green-500);animation:2s ease-in-out 0s infinite globalKeyframes}',
'.global{color:var(--color-green-500);animation:2s ease-in-out infinite globalKeyframes}',
)
expect(css).toMatch(
/\.local.svelte-.*\{color:var\(--color-red-500\);animation:2s ease-in-out 0s infinite svelte-.*-localKeyframes\}/,
/\.local.svelte-.*\{color:var\(--color-red-500\);animation:2s ease-in-out infinite svelte-.*-localKeyframes\}/,
)
expect(css).toMatch(/@keyframes globalKeyframes\{/)
expect(css).toMatch(/@keyframes svelte-.*-localKeyframes\{/)
@ -235,10 +238,10 @@ test(
let [, css] = files[0]
expect(css).toContain(candidate`font-bold`)
expect(css).toContain(
'.global{color:var(--color-green-500);animation:2s ease-in-out 0s infinite globalKeyframes}',
'.global{color:var(--color-green-500);animation:2s ease-in-out infinite globalKeyframes}',
)
expect(css).toMatch(
/\.local.svelte-.*\{color:var\(--color-red-500\);animation:2s ease-in-out 0s infinite svelte-.*-localKeyframes\}/,
/\.local.svelte-.*\{color:var\(--color-red-500\);animation:2s ease-in-out infinite svelte-.*-localKeyframes\}/,
)
expect(css).toMatch(/@keyframes globalKeyframes\{/)
expect(css).toMatch(/@keyframes svelte-.*-localKeyframes\{/)

View File

@ -79,11 +79,6 @@ export default function tailwindcss(): Plugin[] {
for (let [id, root] of roots.entries()) {
let module = server.moduleGraph.getModuleById(id)
if (!module) {
// The module for this root might not exist yet
if (root.builtBeforeTransform) {
continue
}
// 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.
@ -184,7 +179,6 @@ export default function tailwindcss(): Plugin[] {
}
return [
svelteProcessor(roots),
{
// Step 1: Scan source files for candidates
name: '@tailwindcss/vite:scan',
@ -225,19 +219,6 @@ export default function tailwindcss(): Plugin[] {
let root = roots.get(id)
// If the root was built outside of the transform hook (e.g. in the
// Svelte preprocessor), we still want to mark all dependencies of the
// root as watched files.
if (root.builtBeforeTransform) {
root.builtBeforeTransform.forEach((file) => this.addWatchFile(file))
root.builtBeforeTransform = undefined
}
// We only process Svelte `<style>` tags in the `sveltePreprocessor`
if (isSvelteStyle(id)) {
return src
}
if (!options?.ssr) {
// Wait until all other files have been processed, so we can extract
// all candidates before generating CSS. This must not be called
@ -272,19 +253,6 @@ export default function tailwindcss(): Plugin[] {
let root = roots.get(id)
// If the root was built outside of the transform hook (e.g. in the
// Svelte preprocessor), we still want to mark all dependencies of the
// root as watched files.
if (root.builtBeforeTransform) {
root.builtBeforeTransform.forEach((file) => this.addWatchFile(file))
root.builtBeforeTransform = undefined
}
// We only process Svelte `<style>` tags in the `sveltePreprocessor`
if (isSvelteStyle(id)) {
return src
}
// We do a first pass to generate valid CSS for the downstream plugins.
// However, since not all candidates are guaranteed to be extracted by
// this time, we have to re-run a transform for the root later.
@ -304,9 +272,6 @@ export default function tailwindcss(): Plugin[] {
I.start('[@tailwindcss/vite] (render start)')
for (let [id, root] of roots.entries()) {
// Do not do a second render pass on Svelte `<style>` tags.
if (isSvelteStyle(id)) continue
let generated = await regenerateOptimizedCss(
root,
// During the renderStart phase, we can not add watch files since
@ -341,23 +306,13 @@ function isPotentialCssRootFile(id: string) {
if (id.includes('/.vite/')) return
let extension = getExtension(id)
let isCssFile =
(extension === 'css' ||
(extension === 'vue' && id.includes('&lang.css')) ||
(extension === 'astro' && id.includes('&lang.css')) ||
// We want to process Svelte `<style>` tags to properly add dependency
// tracking for imported files.
isSvelteStyle(id)) &&
(extension === 'css' || id.includes('&lang.css')) &&
// Don't intercept special static asset resources
!SPECIAL_QUERY_RE.test(id)
return isCssFile
}
function isSvelteStyle(id: string) {
let extension = getExtension(id)
return extension === 'svelte' && id.includes('&lang.css')
}
function optimizeCss(
input: string,
{ file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {},
@ -425,14 +380,6 @@ class Root {
// `renderStart` hook.
public lastContent: string = ''
// When set, indicates that the root was built before the Vite transform hook
// was being called. This can happen in scenarios like when preprocessing
// `<style>` tags for Svelte components.
//
// It can be set to a list of dependencies that will be added whenever the
// next `transform` hook is being called.
public builtBeforeTransform: string[] | undefined
// The lazily-initialized Tailwind compiler components. These are persisted
// throughout rebuilds but will be re-initialized if the rebuild strategy is
// set to `full`.
@ -626,75 +573,3 @@ class Root {
return shared
}
}
// Register a plugin that can hook into the Svelte preprocessor if Svelte is
// configured. This allows us to transform CSS in `<style>` tags and create a
// stricter version of CSS that passes the Svelte compiler.
//
// Note that these files will not undergo a second pass through the vite
// transpiler later. This means that `@tailwind utilities;` will not be up to
// date.
//
// In practice, it is discouraged to use `@tailwind utilities;` inside Svelte
// components, as the styles it create would be scoped anyways. Use an external
// `.css` file instead.
function svelteProcessor(roots: DefaultMap<string, Root>): Plugin {
return {
name: '@tailwindcss/svelte',
api: {
sveltePreprocess: {
async style({
content,
filename,
markup,
}: {
content: string
filename?: string
markup: string
}) {
if (!filename) return
using I = new Instrumentation()
DEBUG && I.start('[@tailwindcss/vite] Preprocess svelte')
// Create the ID used by Vite to identify the `<style>` contents. This
// way, the Vite `transform` hook can find the right root and thus
// track the right dependencies.
let id = filename + '?svelte&type=style&lang.css'
let root = roots.get(id)
// Since a Svelte pre-processor call means that the CSS has changed,
// we need to trigger a rebuild.
root.requiresRebuild = true
// Mark this root as being built before the Vite transform hook is
// called. We capture all eventually added dependencies so that we can
// connect them to the vite module graph later, when the transform
// hook is called.
root.builtBeforeTransform = []
// We only want to consider candidates from the current template file,
// this ensures that no one can depend on this having the full candidate
// list in some builds (as this is undefined behavior).
let scanner = new Scanner({})
root.overwriteCandidates = scanner.scanFiles([
{ content: markup, file: filename, extension: 'svelte' },
])
let generated = await root.generate(
content,
(file) => root.builtBeforeTransform?.push(file),
I,
)
if (!generated) {
roots.delete(id)
return
}
return { code: generated }
},
},
},
}
}