Skip to content

Commit

Permalink
Vite: Process <style> blocks inside Svelte files as a post-processor (
Browse files Browse the repository at this point in the history
#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:
sveltejs/svelte#14668 (reply in thread)
  • Loading branch information
philipp-spiess authored Jan 9, 2025
1 parent a11c80d commit c766d7e
Show file tree
Hide file tree
Showing 3 changed files with 18 additions and 139 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
29 changes: 16 additions & 13 deletions integrations/vite/svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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')
},
)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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\{/)
Expand All @@ -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\{/)
Expand Down
127 changes: 1 addition & 126 deletions packages/@tailwindcss-vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -184,7 +179,6 @@ export default function tailwindcss(): Plugin[] {
}

return [
svelteProcessor(roots),
{
// Step 1: Scan source files for candidates
name: '@tailwindcss/vite:scan',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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 } = {},
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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 }
},
},
},
}
}

0 comments on commit c766d7e

Please sign in to comment.