import { compile } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' import fs from 'fs' import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths' import { Features, transform } from 'lightningcss' import path from 'path' import postcss, { AtRule, type AcceptedPlugin, type PluginCreator } from 'postcss' import postcssImport from 'postcss-import' /** * A Map that can generate default values for keys that don't exist. * Generated default values are added to the map to avoid recomputation. */ class DefaultMap extends Map { constructor(private factory: (key: T, self: DefaultMap) => V) { super() } get(key: T): V { let value = super.get(key) if (value === undefined) { value = this.factory(key, this) this.set(key, value) } return value } } export type PluginOptions = { // The base directory to scan for class candidates. base?: string // Optimize and minify the output CSS. optimize?: boolean | { minify?: boolean } } function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { let base = opts.base ?? process.cwd() let optimize = opts.optimize ?? process.env.NODE_ENV === 'production' let cache = new DefaultMap(() => { return { mtimes: new Map(), compiler: null as null | Awaited>, css: '', optimizedCss: '', fullRebuildPaths: [] as string[], } }) let hasApply: boolean, hasTailwind: boolean return { postcssPlugin: '@tailwindcss/postcss', plugins: [ // We need to run `postcss-import` first to handle `@import` rules. postcssImport(), fixRelativePathsPlugin(), { postcssPlugin: 'tailwindcss', Once() { // Reset some state between builds hasApply = false hasTailwind = false }, AtRule(rule: AtRule) { if (rule.name === 'apply') { hasApply = true } else if (rule.name === 'tailwind') { hasApply = true hasTailwind = true } }, async OnceExit(root, { result }) { let inputFile = result.opts.from ?? '' let context = cache.get(inputFile) let inputBasePath = path.dirname(path.resolve(inputFile)) async function createCompiler() { clearRequireCache(context.fullRebuildPaths) context.fullRebuildPaths = [] return compile(root.toString(), { base: inputBasePath, onDependency: (path) => { context.fullRebuildPaths.push(path) }, }) } // Setup the compiler if it doesn't exist yet. This way we can // guarantee a `build()` function is available. context.compiler ??= await createCompiler() let rebuildStrategy: 'full' | 'incremental' = 'incremental' // Track file modification times to CSS files { for (let file of context.fullRebuildPaths) { result.messages.push({ type: 'dependency', plugin: '@tailwindcss/postcss', file, parent: result.opts.from, }) } let files = result.messages.flatMap((message) => { if (message.type !== 'dependency') return [] return message.file }) files.push(inputFile) for (let file of files) { let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null if (changedTime === null) { if (file === inputFile) { rebuildStrategy = 'full' } continue } let prevTime = context.mtimes.get(file) if (prevTime === changedTime) continue rebuildStrategy = 'full' context.mtimes.set(file, changedTime) } } // Do nothing if neither `@tailwind` nor `@apply` is used if (!hasTailwind && !hasApply) return let css = '' // Look for candidates used to generate the CSS let scanner = new Scanner({ detectSources: { base }, sources: context.compiler.globs.map(({ origin, pattern }) => ({ // Ensure the glob is relative to the input CSS file or the config // file where it is specified. base: origin ? path.dirname(path.resolve(inputBasePath, origin)) : inputBasePath, pattern, })), }) // let candidates = scanner.scan() // Add all found files as direct dependencies for (let file of scanner.files) { result.messages.push({ type: 'dependency', plugin: '@tailwindcss/postcss', file, parent: result.opts.from, }) } // Register dependencies so changes in `base` cause a rebuild while // giving tools like Vite or Parcel a glob that can be used to limit // the files that cause a rebuild to only those that match it. for (let { base, pattern } of scanner.globs) { result.messages.push({ type: 'dir-dependency', plugin: '@tailwindcss/postcss', dir: base, glob: pattern, parent: result.opts.from, }) } if (rebuildStrategy === 'full') { context.compiler = await createCompiler() css = context.compiler.build(hasTailwind ? candidates : []) } else if (rebuildStrategy === 'incremental') { css = context.compiler.build!(candidates) } // Replace CSS if (css !== context.css && optimize) { context.optimizedCss = optimizeCss(css, { minify: typeof optimize === 'object' ? optimize.minify : true, }) } context.css = css root.removeAll() root.append(postcss.parse(optimize ? context.optimizedCss : context.css, result.opts)) }, }, ], } } 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() } export default Object.assign(tailwindcss, { postcss: true }) as PluginCreator