1
1
import { scanDir } from '@tailwindcss/oxide'
2
+ import fs from 'fs'
2
3
import postcss , { type AcceptedPlugin , type PluginCreator } from 'postcss'
3
4
import postcssImport from 'postcss-import'
4
5
import { compile , optimizeCss } from 'tailwindcss'
5
6
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
+
6
28
type PluginOptions = {
7
29
// The base directory to scan for class candidates.
8
30
base ?: string
@@ -15,13 +37,51 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
15
37
let base = opts . base ?? process . cwd ( )
16
38
let optimize = opts . optimize ?? process . env . NODE_ENV === 'production'
17
39
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
+
18
49
return {
19
- postcssPlugin : 'tailwindcss-v4 ' ,
50
+ postcssPlugin : '@ tailwindcss/postcss ' ,
20
51
plugins : [
21
52
// We need to run `postcss-import` first to handle `@import` rules.
22
53
postcssImport ( ) ,
23
54
24
55
( 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
+
25
85
let hasApply = false
26
86
let hasTailwind = false
27
87
@@ -40,22 +100,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
40
100
// Do nothing if neither `@tailwind` nor `@apply` is used
41
101
if ( ! hasTailwind && ! hasApply ) return
42
102
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 = ''
59
104
60
105
// Look for candidates used to generate the CSS
61
106
let { candidates, files, globs } = scanDir ( { base, globs : true } )
@@ -64,7 +109,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
64
109
for ( let file of files ) {
65
110
result . messages . push ( {
66
111
type : 'dependency' ,
67
- plugin : 'tailwindcss-v4 ' ,
112
+ plugin : '@ tailwindcss/postcss ' ,
68
113
file,
69
114
parent : result . opts . from ,
70
115
} )
@@ -76,14 +121,30 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
76
121
for ( let { base, glob } of globs ) {
77
122
result . messages . push ( {
78
123
type : 'dir-dependency' ,
79
- plugin : 'tailwindcss-v4 ' ,
124
+ plugin : '@ tailwindcss/postcss ' ,
80
125
dir : base ,
81
126
glob,
82
127
parent : result . opts . from ,
83
128
} )
84
129
}
85
130
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 ) )
87
148
} ,
88
149
] ,
89
150
}
0 commit comments