mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
180 lines
4.8 KiB
TypeScript
180 lines
4.8 KiB
TypeScript
import { IO, Parsing, scanFiles } from '@tailwindcss/oxide'
|
|
import { Features, transform } from 'lightningcss'
|
|
import path from 'path'
|
|
import { compile } from 'tailwindcss'
|
|
import type { Plugin, Update, ViteDevServer } from 'vite'
|
|
|
|
export default function tailwindcss(): Plugin[] {
|
|
let server: ViteDevServer | null = null
|
|
let candidates = new Set<string>()
|
|
let cssModules = new Set<string>()
|
|
let minify = false
|
|
|
|
function isCssFile(id: string) {
|
|
let [filename] = id.split('?', 2)
|
|
let extension = path.extname(filename).slice(1)
|
|
return extension === 'css'
|
|
}
|
|
|
|
// Trigger update to all css modules
|
|
function updateCssModules() {
|
|
// If we're building then we don't need to update anything
|
|
if (!server) return
|
|
|
|
let updates: Update[] = []
|
|
for (let id of cssModules.values()) {
|
|
let cssModule = server.moduleGraph.getModuleById(id)
|
|
if (!cssModule) {
|
|
// It is safe to remove the item here since we're iterating on a copy of
|
|
// the values.
|
|
cssModules.delete(id)
|
|
continue
|
|
}
|
|
|
|
server.moduleGraph.invalidateModule(cssModule)
|
|
updates.push({
|
|
type: `${cssModule.type}-update`,
|
|
path: cssModule.url,
|
|
acceptedPath: cssModule.url,
|
|
timestamp: Date.now(),
|
|
})
|
|
}
|
|
|
|
if (updates.length > 0) {
|
|
server.hot.send({ type: 'update', updates })
|
|
}
|
|
}
|
|
|
|
function scan(src: string, extension: string) {
|
|
let updated = false
|
|
// Parse all candidates given the resolved files
|
|
for (let candidate of scanFiles(
|
|
[{ content: src, extension }],
|
|
IO.Sequential | Parsing.Sequential,
|
|
)) {
|
|
// On an initial or full build, updated becomes true immediately so we
|
|
// won't be making extra checks.
|
|
if (!updated) {
|
|
if (candidates.has(candidate)) continue
|
|
updated = true
|
|
}
|
|
candidates.add(candidate)
|
|
}
|
|
return updated
|
|
}
|
|
|
|
function generateCss(css: string) {
|
|
return compile(css).build(Array.from(candidates))
|
|
}
|
|
|
|
function generateOptimizedCss(css: string) {
|
|
return optimizeCss(generateCss(css), { minify })
|
|
}
|
|
|
|
return [
|
|
{
|
|
// Step 1: Scan source files for candidates
|
|
name: '@tailwindcss/vite:scan',
|
|
enforce: 'pre',
|
|
|
|
configureServer(_server) {
|
|
server = _server
|
|
},
|
|
|
|
async configResolved(config) {
|
|
minify = config.build.cssMinify !== false
|
|
},
|
|
|
|
// Scan index.html for candidates
|
|
transformIndexHtml(html) {
|
|
let updated = scan(html, 'html')
|
|
|
|
// In dev mode, if the generated CSS contains a URL that causes the
|
|
// browser to load a page (e.g. an URL to a missing image), triggering a
|
|
// CSS update will cause an infinite loop. We only trigger if the
|
|
// candidates have been updated.
|
|
if (server && updated) {
|
|
updateCssModules()
|
|
}
|
|
},
|
|
|
|
// Scan all other files for candidates
|
|
transform(src, id) {
|
|
if (id.includes('/.vite/')) return
|
|
let [filename] = id.split('?', 2)
|
|
let extension = path.extname(filename).slice(1)
|
|
if (extension === '' || extension === 'css') return
|
|
|
|
scan(src, extension)
|
|
|
|
if (server) {
|
|
updateCssModules()
|
|
}
|
|
},
|
|
},
|
|
|
|
{
|
|
// Step 2 (dev mode): Generate CSS
|
|
name: '@tailwindcss/vite:generate:serve',
|
|
apply: 'serve',
|
|
|
|
async transform(src, id) {
|
|
if (!isCssFile(id) || !src.includes('@tailwind')) return
|
|
|
|
cssModules.add(id)
|
|
|
|
// Wait until all other files have been processed, so we can extract all
|
|
// candidates before generating CSS.
|
|
await server?.waitForRequestsIdle(id)
|
|
|
|
return { code: generateCss(src) }
|
|
},
|
|
},
|
|
|
|
{
|
|
// Step 2 (full build): Generate CSS
|
|
name: '@tailwindcss/vite:generate:build',
|
|
enforce: 'post',
|
|
apply: 'build',
|
|
generateBundle(_options, bundle) {
|
|
for (let id in bundle) {
|
|
let item = bundle[id]
|
|
if (item.type !== 'asset') continue
|
|
if (!isCssFile(id)) continue
|
|
let rawSource = item.source
|
|
let source =
|
|
rawSource instanceof Uint8Array ? new TextDecoder().decode(rawSource) : rawSource
|
|
|
|
if (source.includes('@tailwind')) {
|
|
item.source = generateOptimizedCss(source)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
] satisfies Plugin[]
|
|
}
|
|
|
|
function optimizeCss(
|
|
input: string,
|
|
{ file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {},
|
|
) {
|
|
return transform({
|
|
filename: file,
|
|
code: Buffer.from(input),
|
|
minify,
|
|
sourceMap: false,
|
|
drafts: {
|
|
customMedia: true,
|
|
},
|
|
nonStandard: {
|
|
deepSelectorCombinator: true,
|
|
},
|
|
include: Features.Nesting,
|
|
exclude: Features.LogicalProperties,
|
|
targets: {
|
|
safari: (16 << 16) | (4 << 8),
|
|
},
|
|
errorRecovery: true,
|
|
}).code.toString()
|
|
}
|