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,54 @@ 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
+ previousCss : '' ,
45
+ previousOptimizedCss : '' ,
46
+ }
47
+ } )
48
+
18
49
return {
19
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 from = result . opts . from ?? ''
57
+ let context = cache . get ( from )
58
+
59
+ let rebuildStrategy : 'full' | 'incremental' = 'incremental'
60
+
61
+ // Bookkeeping — track file modification times to CSS files
62
+ {
63
+ let changedTime = fs . statSync ( from , { throwIfNoEntry : false } ) ?. mtimeMs ?? null
64
+ if ( changedTime !== null ) {
65
+ let prevTime = context . mtimes . get ( from )
66
+ if ( prevTime !== changedTime ) {
67
+ rebuildStrategy = 'full'
68
+ context . mtimes . set ( from , changedTime )
69
+ }
70
+ } else {
71
+ rebuildStrategy = 'full'
72
+ }
73
+ for ( let message of result . messages ) {
74
+ if ( message . type === 'dependency' ) {
75
+ let file = message . file as string
76
+ let changedTime = fs . statSync ( file , { throwIfNoEntry : false } ) ?. mtimeMs ?? null
77
+ if ( changedTime !== null ) {
78
+ let prevTime = context . mtimes . get ( file )
79
+ if ( prevTime !== changedTime ) {
80
+ rebuildStrategy = 'full'
81
+ context . mtimes . set ( file , changedTime )
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+
25
88
let hasApply = false
26
89
let hasTailwind = false
27
90
@@ -40,22 +103,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
40
103
// Do nothing if neither `@tailwind` nor `@apply` is used
41
104
if ( ! hasTailwind && ! hasApply ) return
42
105
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 : false ,
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
- }
106
+ let css = ''
59
107
60
108
// Look for candidates used to generate the CSS
61
109
let { candidates, files, globs } = scanDir ( { base, globs : true } )
@@ -83,7 +131,42 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
83
131
} )
84
132
}
85
133
86
- replaceCss ( compile ( root . toString ( ) ) . build ( candidates ) )
134
+ if ( rebuildStrategy === 'full' ) {
135
+ if ( hasTailwind ) {
136
+ let { build } = compile ( root . toString ( ) )
137
+ css = build ( candidates )
138
+ context . build = build
139
+ } else {
140
+ css = compile ( root . toString ( ) ) . build ( [ ] )
141
+ }
142
+ } else if ( rebuildStrategy === 'incremental' ) {
143
+ css = context . build ! ( candidates )
144
+ }
145
+
146
+ function replaceCss ( css : string ) {
147
+ root . removeAll ( )
148
+ root . append ( postcss . parse ( css , result . opts ) )
149
+ }
150
+
151
+ // Replace CSS
152
+ if ( css === context . previousCss ) {
153
+ if ( optimize ) {
154
+ replaceCss ( context . previousOptimizedCss )
155
+ } else {
156
+ replaceCss ( css )
157
+ }
158
+ } else {
159
+ if ( optimize ) {
160
+ let optimizedCss = optimizeCss ( css , {
161
+ minify : typeof optimize === 'object' ? optimize . minify : false ,
162
+ } )
163
+ replaceCss ( optimizedCss )
164
+ context . previousOptimizedCss = optimizedCss
165
+ } else {
166
+ replaceCss ( css )
167
+ }
168
+ context . previousCss = css
169
+ }
87
170
} ,
88
171
] ,
89
172
}
0 commit comments