Skip to content

Commit

Permalink
Vite: Support Tailwind in Svelte <style> blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
philipp-spiess committed Nov 8, 2024
1 parent 99c4c04 commit 8a0cc56
Show file tree
Hide file tree
Showing 5 changed files with 398 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support derived spacing scales based on a single `--spacing` theme value ([#14857](https://github.com/tailwindlabs/tailwindcss/pull/14857))
- Add `svh`, `dvh`, `svw`, `dvw`, and `auto` values to all width/height/size utilities ([#14857](https://github.com/tailwindlabs/tailwindcss/pull/14857))
- Add new `**` variant ([#14903](https://github.com/tailwindlabs/tailwindcss/pull/14903))
- Process `<style>` blocks inside Svelte files when using the Vite extension ([#14151](https://github.com/tailwindlabs/tailwindcss/pull/14151))
- _Upgrade (experimental)_: Migrate `grid-cols-[subgrid]` and `grid-rows-[subgrid]` to `grid-cols-subgrid` and `grid-rows-subgrid` ([#14840](https://github.com/tailwindlabs/tailwindcss/pull/14840))
- _Upgrade (experimental)_: Support migrating projects with multiple config files ([#14863](https://github.com/tailwindlabs/tailwindcss/pull/14863))
- _Upgrade (experimental)_: Rename `shadow` to `shadow-sm`, `shadow-sm` to `shadow-xs`, and `shadow-xs` to `shadow-2xs` ([#14875](https://github.com/tailwindlabs/tailwindcss/pull/14875))
Expand Down
177 changes: 177 additions & 0 deletions integrations/vite/svelte.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { expect } from 'vitest'
import { candidate, css, html, json, retryAssertion, test, ts } from '../utils'

test(
'production build',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"svelte": "^4.2.18",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tailwindcss/vite": "workspace:^",
"vite": "^5.3.5"
}
}
`,
'vite.config.ts': ts`
import { defineConfig } from 'vite'
import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
svelte({
preprocess: [vitePreprocess()],
}),
tailwindcss(),
],
})
`,
'index.html': html`
<!doctype html>
<html>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>
`,
'src/main.ts': ts`
import App from './App.svelte'
const app = new App({
target: document.body,
})
`,
'src/App.svelte': html`
<script>
let name = 'world'
</script>
<h1 class="foo underline">Hello {name}!</h1>
<style global>
@import 'tailwindcss/utilities';
@import 'tailwindcss/theme' theme(reference);
@import './components.css';
</style>
`,
'src/components.css': css`
.foo {
@apply text-red-500;
}
`,
},
},
async ({ fs, exec }) => {
await exec('pnpm vite build')

let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)

await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`foo`])
},
)

test(
'watch mode',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"svelte": "^4.2.18",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tailwindcss/vite": "workspace:^",
"vite": "^5.3.5"
}
}
`,
'vite.config.ts': ts`
import { defineConfig } from 'vite'
import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
svelte({
preprocess: [vitePreprocess()],
}),
tailwindcss(),
],
})
`,
'index.html': html`
<!doctype html>
<html>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>
`,
'src/main.ts': ts`
import App from './App.svelte'
const app = new App({
target: document.body,
})
`,
'src/App.svelte': html`
<script>
let name = 'world'
</script>
<h1 class="foo underline">Hello {name}!</h1>
<style global>
@import 'tailwindcss/utilities';
@import 'tailwindcss/theme' theme(reference);
@import './components.css';
</style>
`,
'src/components.css': css`
.foo {
@apply text-red-500;
}
`,
},
},
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]
})

await fs.expectFileToContain(filename, [candidate`foo`, candidate`underline`])

await fs.write(
'src/components.css',
css`
.bar {
@apply text-green-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`)
})
},
)
1 change: 1 addition & 0 deletions packages/@tailwindcss-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@tailwindcss/node": "workspace:^",
"@tailwindcss/oxide": "workspace:^",
"lightningcss": "catalog:",
"svelte-preprocess": "^6.0.2",
"tailwindcss": "workspace:^"
},
"devDependencies": {
Expand Down
98 changes: 96 additions & 2 deletions packages/@tailwindcss-vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Scanner } from '@tailwindcss/oxide'
import { Features, transform } from 'lightningcss'
import fs from 'node:fs/promises'
import path from 'node:path'
import { sveltePreprocess } from 'svelte-preprocess'
import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite'

const SPECIAL_QUERY_RE = /[?&](raw|url)\b/
Expand Down Expand Up @@ -53,9 +54,14 @@ export default function tailwindcss(): Plugin[] {
function invalidateAllRoots(isSSR: boolean) {
for (let server of servers) {
let updates: Update[] = []
for (let id of roots.keys()) {
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) {
return
}

// 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 @@ -152,6 +158,7 @@ export default function tailwindcss(): Plugin[] {
}

return [
svelteProcessor(roots),
{
// Step 1: Scan source files for candidates
name: '@tailwindcss/vite:scan',
Expand Down Expand Up @@ -189,6 +196,19 @@ export default function tailwindcss(): Plugin[] {

let root = roots.get(id)

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
}

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 @@ -220,6 +240,18 @@ export default function tailwindcss(): Plugin[] {

let root = roots.get(id)

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 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 Down Expand Up @@ -266,11 +298,13 @@ function getExtension(id: string) {
}

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'))) &&
(extension === 'astro' && id.includes('&lang.css')) ||
(extension === 'svelte' && id.includes('&lang.css'))) &&
// Don't intercept special static asset resources
!SPECIAL_QUERY_RE.test(id)

Expand Down Expand Up @@ -338,6 +372,14 @@ 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 @@ -505,3 +547,55 @@ class Root {
return shared
}
}

// Register a plugin that can hook into the Svelte preprocessor if svelte is
// 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.
//
// In practice, it is not recommended to use `@tailwind utilities;` inside
// Svelte components. Use an external `.css` file instead.
function svelteProcessor(roots: DefaultMap<string, Root>) {
return {
name: '@tailwindcss/svelte',
api: {
sveltePreprocess: sveltePreprocess({
aliases: [
['postcss', 'tailwindcss'],
['css', 'tailwindcss'],
],
async tailwindcss({
content,
attributes,
filename,
}: {
content: string
attributes: Record<string, string>
filename?: string
}) {
if (!filename) return
let id = filename + '?svelte&type=style&lang.css'

let root = roots.get(id)
// 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 { code: generated, attributes }
},
}),
},
}
}
Loading

0 comments on commit 8a0cc56

Please sign in to comment.