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

Apply non-Tailwind CSS transforms in Vite plugin #14871

Merged
merged 7 commits into from
Nov 7, 2024
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure `url(…)` containing special characters such as `;` or `{}` end up in one declaration ([#14879](https://github.com/tailwindlabs/tailwindcss/pull/14879))
- Ensure adjacent rules are merged together after handling nesting when generating optimized CSS ([#14873](https://github.com/tailwindlabs/tailwindcss/pull/14873))
- Rebase `url()` inside imported CSS files when using Vite ([#14877](https://github.com/tailwindlabs/tailwindcss/pull/14877))
- Ensure that CSS transforms from other Vite plugins correctly work in full builds (e.g. `:deep()` in Vue) ([#14871](https://github.com/tailwindlabs/tailwindcss/pull/14871))
- _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830))
- _Upgrade (experimental)_: Remove whitespace around `,` separator when print arbitrary values ([#14838](https://github.com/tailwindlabs/tailwindcss/pull/14838))
- _Upgrade (experimental)_: Fix crash during upgrade when content globs escape root of project ([#14896](https://github.com/tailwindlabs/tailwindcss/pull/14896))
111 changes: 60 additions & 51 deletions integrations/vite/nuxt.test.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,74 @@
import { expect } from 'vitest'
import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'

test(
'dev mode',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"nuxt": "^3.13.1",
"tailwindcss": "workspace:^",
"vue": "latest"
}
const SETUP = {
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"nuxt": "^3.13.1",
"tailwindcss": "workspace:^",
"vue": "latest"
}
`,
'nuxt.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
}
`,
'nuxt.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
vite: {
plugins: [tailwindcss()],
},
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
vite: {
plugins: [tailwindcss()],
},

css: ['~/assets/css/main.css'],
devtools: { enabled: true },
compatibilityDate: '2024-08-30',
})
`,
'app.vue': html`
css: ['~/assets/css/main.css'],
devtools: { enabled: true },
compatibilityDate: '2024-08-30',
})
`,
'app.vue': html`
<template>
<div class="underline">Hello world!</div>
</template>
<div class="underline">Hello world!</div>
</template>
`,
'assets/css/main.css': css`@import 'tailwindcss';`,
},
'assets/css/main.css': css`@import 'tailwindcss';`,
},
async ({ fs, spawn, getFreePort }) => {
let port = await getFreePort()
await spawn(`pnpm nuxt dev --port ${port}`)
}

test('dev mode', SETUP, async ({ fs, spawn, getFreePort }) => {
let port = await getFreePort()
await spawn(`pnpm nuxt dev --port ${port}`)

await retryAssertion(async () => {
let css = await fetchStyles(port)
expect(css).toContain(candidate`underline`)
})
await retryAssertion(async () => {
let css = await fetchStyles(port)
expect(css).toContain(candidate`underline`)
})

await retryAssertion(async () => {
await fs.write(
'app.vue',
html`
await retryAssertion(async () => {
await fs.write(
'app.vue',
html`
<template>
<div class="underline font-bold">Hello world!</div>
</template>
<div class="underline font-bold">Hello world!</div>
</template>
`,
)
)

let css = await fetchStyles(port)
expect(css).toContain(candidate`underline`)
expect(css).toContain(candidate`font-bold`)
})
},
)
let css = await fetchStyles(port)
expect(css).toContain(candidate`underline`)
expect(css).toContain(candidate`font-bold`)
})
})

test('build', SETUP, async ({ spawn, getFreePort, exec }) => {
let port = await getFreePort()
await exec(`pnpm nuxt build`)
await spawn(`PORT=${port} pnpm nuxt preview`)

await retryAssertion(async () => {
let css = await fetchStyles(port)
expect(css).toContain(candidate`underline`)
})
})
172 changes: 172 additions & 0 deletions integrations/vite/other-transforms.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import dedent from 'dedent'
import { describe, expect } from 'vitest'
import { css, fetchStyles, html, retryAssertion, test, ts, txt } from '../utils'

function createSetup(transformer: 'postcss' | 'lightningcss') {
return {
fs: {
'package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
"vite": "^5.3.5"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'

export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [
tailwindcss(),
{
name: 'recolor',
transform(code, id) {
if (id.includes('.css')) {
return code.replace(/red;/g, 'blue;')
}
},
},
],
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="foo [background-color:red]">Hello, world!</div>
</body>
`,
'src/index.css': css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';

.foo {
color: red;
}
`,
},
}
}

for (let transformer of ['postcss', 'lightningcss'] as const) {
describe(transformer, () => {
test(`production build`, createSetup(transformer), async ({ fs, exec }) => {
await exec('pnpm vite build')

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

await fs.expectFileToContain(filename, [
css`
.foo {
color: blue;
}
`,
// Running the transforms on utilities generated by Tailwind might change in the future
dedent`
.\[background-color\:red\] {
background-color: blue;
}
`,
])
})

test(`dev mode`, createSetup(transformer), async ({ spawn, getFreePort, fs }) => {
let port = await getFreePort()
await spawn(`pnpm vite dev --port ${port}`)

await retryAssertion(async () => {
let styles = await fetchStyles(port, '/index.html')
expect(styles).toContain(css`
.foo {
color: blue;
}
`)
// Running the transforms on utilities generated by Tailwind might change in the future
expect(styles).toContain(dedent`
.\[background-color\:red\] {
background-color: blue;
}
`)
})

await retryAssertion(async () => {
await fs.write(
'src/index.css',
css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';

.foo {
background-color: red;
}
`,
)

let styles = await fetchStyles(port)
expect(styles).toContain(css`
.foo {
background-color: blue;
}
`)
})
})

test('watch mode', createSetup(transformer), async ({ spawn, fs }) => {
await spawn(`pnpm vite build --watch`)

await retryAssertion(async () => {
let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
let [, styles] = files[0]

expect(styles).toContain(css`
.foo {
color: blue;
}
`)
// Running the transforms on utilities generated by Tailwind might change in the future
expect(styles).toContain(dedent`
.\[background-color\:red\] {
background-color: blue;
}
`)
})

await retryAssertion(async () => {
await fs.write(
'src/index.css',
css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';

.foo {
background-color: red;
}
`,
)

let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
let [, styles] = files[0]

expect(styles).toContain(css`
.foo {
background-color: blue;
}
`)
})
})
})
}
philipp-spiess marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 9 additions & 2 deletions integrations/vite/vue.test.ts
Original file line number Diff line number Diff line change
@@ -51,9 +51,15 @@ test(
@apply text-red-500;
}
</style>

<style scoped>
@import 'tailwindcss/utilities';
@import 'tailwindcss/theme' theme(reference);
:deep(.bar) {
color: red;
}
</style>
<template>
<div class="underline foo">Hello Vue!</div>
<div class="underline foo bar">Hello Vue!</div>
</template>
`,
},
@@ -65,5 +71,6 @@ test(
expect(files).toHaveLength(1)

await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`foo`])
await fs.expectFileToContain(files[0][0], ['.bar{'])
},
)
44 changes: 24 additions & 20 deletions packages/@tailwindcss-vite/src/index.ts
Original file line number Diff line number Diff line change
@@ -15,11 +15,6 @@ export default function tailwindcss(): Plugin[] {
let isSSR = false
let minify = false

// A list of css plugins defined in the Vite config. We need to retain these
// so that we can rerun the right transformations in build mode where we have
// to manually rebuild the css file after the compilation is done.
let cssPlugins: readonly Plugin[] = []

// The Vite extension has two types of sources for candidates:
//
// 1. The module graph: These are all modules that vite transforms and we want
@@ -109,8 +104,31 @@ export default function tailwindcss(): Plugin[] {
},
}

for (let plugin of cssPlugins) {
for (let plugin of config!.plugins) {
if (!plugin.transform) continue

if (plugin.name.startsWith('@tailwindcss/')) {
// We do not run any Tailwind transforms anymore
continue
} else if (
plugin.name.startsWith('vite:') &&
// Apply the vite:css plugin to generated CSS for transformations like
// URL path rewriting and image inlining.
plugin.name !== 'vite:css' &&
// In build mode, since `renderStart` runs after all transformations, we
// need to also apply vite:css-post.
plugin.name !== 'vite:css-post' &&
// The vite:vue plugin handles CSS specific post-processing for Vue
plugin.name !== 'vite:vue'
) {
continue
} else if (plugin.name === 'ssr-styles') {
// The Nuxt ssr-styles plugin emits styles from server-side rendered
// components, we can't run it in the `renderStart` phase so we're
// skipping it.
continue
}

let transformHandler =
'handler' in plugin.transform! ? plugin.transform.handler : plugin.transform!

@@ -147,20 +165,6 @@ export default function tailwindcss(): Plugin[] {
config = _config
minify = config.build.cssMinify !== false
isSSR = config.build.ssr !== false && config.build.ssr !== undefined

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 all non-CSS files for candidates
27 changes: 6 additions & 21 deletions pnpm-lock.yaml