11import { scanDir } from '@tailwindcss/oxide'
2+ import fs from 'fs'
23import postcss , { type AcceptedPlugin , type PluginCreator } from 'postcss'
34import postcssImport from 'postcss-import'
45import { compile , optimizeCss } from 'tailwindcss'
56
7+ /**
8+ * A Map that can generate default values for keys that don't exist.
9+ * Generated default values are added to the map to avoid recomputation.
10+ */
11+ class DefaultMap < T = string , V = any > extends Map < T , V > {
12+ constructor ( private factory : ( key : T , self : DefaultMap < T , V > ) => V ) {
13+ super ( )
14+ }
15+
16+ get ( key : T ) : V {
17+ let value = super . get ( key )
18+
19+ if ( value === undefined ) {
20+ value = this . factory ( key , this )
21+ this . set ( key , value )
22+ }
23+
24+ return value
25+ }
26+ }
27+
628type PluginOptions = {
729 // The base directory to scan for class candidates.
830 base ?: string
@@ -15,13 +37,51 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
1537 let base = opts . base ?? process . cwd ( )
1638 let optimize = opts . optimize ?? process . env . NODE_ENV === 'production'
1739
40+ let cache = new DefaultMap ( ( ) => {
41+ return {
42+ mtimes : new Map < string , number > ( ) ,
43+ build : null as null | ReturnType < typeof compile > [ 'build' ] ,
44+ css : '' ,
45+ optimizedCss : '' ,
46+ }
47+ } )
48+
1849 return {
19- postcssPlugin : 'tailwindcss-v4 ' ,
50+ postcssPlugin : '@ tailwindcss/postcss ' ,
2051 plugins : [
2152 // We need to run `postcss-import` first to handle `@import` rules.
2253 postcssImport ( ) ,
2354
2455 ( root , result ) => {
56+ let inputFile = result . opts . from ?? ''
57+ let context = cache . get ( inputFile )
58+
59+ let rebuildStrategy : 'full' | 'incremental' = 'incremental'
60+
61+ // Track file modification times to CSS files
62+ {
63+ let files = result . messages . flatMap ( ( message ) => {
64+ if ( message . type !== 'dependency' ) return [ ]
65+ return message . file
66+ } )
67+ files . push ( inputFile )
68+ for ( let file of files ) {
69+ let changedTime = fs . statSync ( file , { throwIfNoEntry : false } ) ?. mtimeMs ?? null
70+ if ( changedTime === null ) {
71+ if ( file === inputFile ) {
72+ rebuildStrategy = 'full'
73+ }
74+ continue
75+ }
76+
77+ let prevTime = context . mtimes . get ( file )
78+ if ( prevTime === changedTime ) continue
79+
80+ rebuildStrategy = 'full'
81+ context . mtimes . set ( file , changedTime )
82+ }
83+ }
84+
2585 let hasApply = false
2686 let hasTailwind = false
2787
@@ -40,22 +100,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
40100 // Do nothing if neither `@tailwind` nor `@apply` is used
41101 if ( ! hasTailwind && ! hasApply ) return
42102
43- function replaceCss ( css : string ) {
44- root . removeAll ( )
45- let output = css
46- if ( optimize ) {
47- output = optimizeCss ( output , {
48- minify : typeof optimize === 'object' ? optimize . minify : true ,
49- } )
50- }
51- root . append ( postcss . parse ( output , result . opts ) )
52- }
53-
54- // No `@tailwind` means we don't have to look for candidates
55- if ( ! hasTailwind ) {
56- replaceCss ( compile ( root . toString ( ) ) . build ( [ ] ) )
57- return
58- }
103+ let css = ''
59104
60105 // Look for candidates used to generate the CSS
61106 let { candidates, files, globs } = scanDir ( { base, globs : true } )
@@ -64,7 +109,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
64109 for ( let file of files ) {
65110 result . messages . push ( {
66111 type : 'dependency' ,
67- plugin : 'tailwindcss-v4 ' ,
112+ plugin : '@ tailwindcss/postcss ' ,
68113 file,
69114 parent : result . opts . from ,
70115 } )
@@ -76,14 +121,30 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
76121 for ( let { base, glob } of globs ) {
77122 result . messages . push ( {
78123 type : 'dir-dependency' ,
79- plugin : 'tailwindcss-v4 ' ,
124+ plugin : '@ tailwindcss/postcss ' ,
80125 dir : base ,
81126 glob,
82127 parent : result . opts . from ,
83128 } )
84129 }
85130
86- replaceCss ( compile ( root . toString ( ) ) . build ( candidates ) )
131+ if ( rebuildStrategy === 'full' ) {
132+ let { build } = compile ( root . toString ( ) )
133+ context . build = build
134+ css = build ( hasTailwind ? candidates : [ ] )
135+ } else if ( rebuildStrategy === 'incremental' ) {
136+ css = context . build ! ( candidates )
137+ }
138+
139+ // Replace CSS
140+ if ( css !== context . css && optimize ) {
141+ context . optimizedCss = optimizeCss ( css , {
142+ minify : typeof optimize === 'object' ? optimize . minify : true ,
143+ } )
144+ }
145+ context . css = css
146+ root . removeAll ( )
147+ root . append ( postcss . parse ( optimize ? context . optimizedCss : context . css , result . opts ) )
87148 } ,
88149 ] ,
89150 }
0 commit comments