Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Support opacity values in increments of `0.25` by default ([#14980](https://github.com/tailwindlabs/tailwindcss/pull/14980))

### Fixed

- Ensure that CSS inside Svelte `<style>` blocks always run the expected Svelte processors when using the Vite extension ([#14981](https://github.com/tailwindlabs/tailwindcss/pull/14981))

## [4.0.0-alpha.33] - 2024-11-11

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ interface TestContext {
dumpFiles(pattern: string): Promise<string>
expectFileToContain(
filePath: string,
contents: string | string[] | RegExp | RegExp[],
contents: string | RegExp | (string | RegExp)[],
): Promise<void>
expectFileNotToContain(filePath: string, contents: string | string[]): Promise<void>
}
Expand Down
127 changes: 102 additions & 25 deletions integrations/vite/svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,50 @@ test(
target: document.body,
})
`,
'src/index.css': css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';
`,
'src/App.svelte': html`
<script>
import './index.css'
let name = 'world'
</script>

<h1 class="foo underline">Hello {name}!</h1>
<h1 class="global local underline">Hello {name}!</h1>

<style global>
@import 'tailwindcss/utilities';
<style>
@import 'tailwindcss/theme' theme(reference);
@import './components.css';
@import './other.css';
</style>
`,
'src/components.css': css`
.foo {
'src/other.css': css`
.local {
@apply text-red-500;
animation: 2s ease-in-out 0s infinite localKeyframes;
}

:global(.global) {
@apply text-green-500;
animation: 2s ease-in-out 0s infinite globalKeyframes;
}

@keyframes -global-globalKeyframes {
0% {
opacity: 0;
}
100% {
opacity: 100%;
}
}

@keyframes localKeyframes {
0% {
opacity: 0;
}
100% {
opacity: 100%;
}
}
`,
},
Expand All @@ -74,7 +102,13 @@ test(
let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)

await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`foo`])
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\}/,
/@keyframes globalKeyframes\{/,
/@keyframes svelte-.*-localKeyframes\{/,
])
},
)

Expand Down Expand Up @@ -127,51 +161,94 @@ test(
`,
'src/App.svelte': html`
<script>
import './index.css'
let name = 'world'
</script>

<h1 class="foo underline">Hello {name}!</h1>
<h1 class="local global underline">Hello {name}!</h1>

<style global>
@import 'tailwindcss/utilities';
<style>
@import 'tailwindcss/theme' theme(reference);
@import './components.css';
@import './other.css';
</style>
`,
'src/components.css': css`
.foo {
'src/index.css': css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';
`,
'src/other.css': css`
.local {
@apply text-red-500;
animation: 2s ease-in-out 0s infinite localKeyframes;
}

:global(.global) {
@apply text-green-500;
animation: 2s ease-in-out 0s infinite globalKeyframes;
}

@keyframes -global-globalKeyframes {
0% {
opacity: 0;
}
100% {
opacity: 100%;
}
}

@keyframes localKeyframes {
0% {
opacity: 0;
}
100% {
opacity: 100%;
}
}
`,
},
},
async ({ fs, spawn }) => {
await spawn(`pnpm vite build --watch`)

let filename = ''
await retryAssertion(async () => {
let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
filename = files[0][0]
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}',
)
expect(css).toMatch(
/\.local.svelte-.*\{color:var\(--color-red-500\);animation:2s ease-in-out 0s infinite svelte-.*-localKeyframes\}/,
)
expect(css).toMatch(/@keyframes globalKeyframes\{/)
expect(css).toMatch(/@keyframes svelte-.*-localKeyframes\{/)
})

await fs.expectFileToContain(filename, [candidate`foo`, candidate`underline`])
await fs.write(
'src/App.svelte',
(await fs.read('src/App.svelte')).replace('underline', 'font-bold bar'),
)

await fs.write(
'src/components.css',
css`
.bar {
@apply text-green-500;
}
`,
'src/other.css',
`${await fs.read('src/other.css')}\n.bar { @apply text-pink-500; }`,
)

await retryAssertion(async () => {
let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
let [, css] = files[0]
expect(css).toContain(candidate`underline`)
expect(css).toContain(candidate`bar`)
expect(css).not.toContain(candidate`foo`)
expect(css).toContain(candidate`font-bold`)
expect(css).toContain(
'.global{color:var(--color-green-500);animation:2s ease-in-out 0s infinite globalKeyframes}',
)
expect(css).toMatch(
/\.local.svelte-.*\{color:var\(--color-red-500\);animation:2s ease-in-out 0s infinite svelte-.*-localKeyframes\}/,
)
expect(css).toMatch(/@keyframes globalKeyframes\{/)
expect(css).toMatch(/@keyframes svelte-.*-localKeyframes\{/)
expect(css).toMatch(/\.bar.svelte-.*\{color:var\(--color-pink-500\)\}/)
})
},
)
90 changes: 56 additions & 34 deletions packages/@tailwindcss-vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default function tailwindcss(): Plugin[] {
if (!module) {
// The module for this root might not exist yet
if (root.builtBeforeTransform) {
return
continue
}

// Note: Removing this during SSR is not safe and will produce
Expand Down Expand Up @@ -196,17 +196,17 @@ 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
// When a root was built before this transform hook, the candidate
// list might be outdated already by the time the transform hook is
// called.
//
// This requires us to build the CSS file again. However, we do not
// expect dependencies to have changed, so we can avoid a full
// rebuild.
root.requiresRebuild = false
}

// We only process Svelte `<style>` tags in the `sveltePreprocessor`
if (isSvelteStyle(id)) {
return src
}

if (!options?.ssr) {
Expand Down Expand Up @@ -240,16 +240,17 @@ 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
// When a root was built before this transform hook, the candidate
// list might be outdated already by the time the transform hook is
// called.
//
// Since we already do a second render pass in build mode, we don't
// need to do any more work here.
return
}

// 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.
Expand All @@ -268,6 +269,9 @@ export default function tailwindcss(): Plugin[] {
// by vite:css-post.
async renderStart() {
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 @@ -304,13 +308,18 @@ function isPotentialCssRootFile(id: string) {
(extension === 'css' ||
(extension === 'vue' && id.includes('&lang.css')) ||
(extension === 'astro' && id.includes('&lang.css')) ||
(extension === 'svelte' && id.includes('&lang.css'))) &&
isSvelteStyle(id)) &&
// 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 @@ -552,50 +561,63 @@ class Root {
// enabled. 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 undergo a second pass through the vite transpiler
// later. This is necessary to compute `@tailwind utilities;` with the right
// candidate list.
// 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 not recommended to use `@tailwind utilities;` inside
// Svelte components. Use an external `.css` file instead.
// 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>) {
let preprocessor = sveltePreprocess()

return {
name: '@tailwindcss/svelte',
api: {
sveltePreprocess: sveltePreprocess({
aliases: [
['postcss', 'tailwindcss'],
['css', 'tailwindcss'],
],
async tailwindcss({
sveltePreprocess: {
markup: preprocessor.markup,
script: preprocessor.script,
async style({
content,
attributes,
filename,
...rest
}: {
content: string
attributes: Record<string, string>
filename?: string
attributes: Record<string, string | boolean>
markup: string
}) {
if (!filename) return
if (!filename) return preprocessor.style?.({ ...rest, content, filename })

// 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 = []

let generated = await root.generate(content, (file) =>
root?.builtBeforeTransform?.push(file),
)

if (!generated) {
roots.delete(id)
return { code: content, attributes }
return preprocessor.style?.({ ...rest, content, filename })
}
return { code: generated, attributes }

return preprocessor.style?.({ ...rest, content: generated, filename })
},
}),
},
},
}
}