Skip to content

Commit 036fc39

Browse files
committed
add incremental rebuilds to @tailwindcss/postcss
1 parent d590d06 commit 036fc39

File tree

1 file changed

+100
-17
lines changed
  • packages/@tailwindcss-postcss/src

1 file changed

+100
-17
lines changed

packages/@tailwindcss-postcss/src/index.ts

+100-17
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,30 @@
11
import { scanDir } from '@tailwindcss/oxide'
2+
import fs from 'fs'
23
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
34
import postcssImport from 'postcss-import'
45
import { 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+
628
type PluginOptions = {
729
// The base directory to scan for class candidates.
830
base?: string
@@ -15,13 +37,54 @@ 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+
previousCss: '',
45+
previousOptimizedCss: '',
46+
}
47+
})
48+
1849
return {
1950
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 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+
2588
let hasApply = false
2689
let hasTailwind = false
2790

@@ -40,22 +103,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
40103
// Do nothing if neither `@tailwind` nor `@apply` is used
41104
if (!hasTailwind && !hasApply) return
42105

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 = ''
59107

60108
// Look for candidates used to generate the CSS
61109
let { candidates, files, globs } = scanDir({ base, globs: true })
@@ -83,7 +131,42 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
83131
})
84132
}
85133

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+
}
87170
},
88171
],
89172
}

0 commit comments

Comments
 (0)