Skip to content

Commit 172bc5d

Browse files
Add incremental rebuilds to @tailwindcss/postcss (tailwindlabs#13170)
* rename PostCSS plugin name from `tailwindcss-v4` to `@tailwindcss/postcss` * add incremental rebuilds to `@tailwindcss/postcss` * improve comment * update changelog * Simplify nested conditionals * Simplify more conditionals * Refactor file modification tracking --------- Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
1 parent a458e5d commit 172bc5d

File tree

2 files changed

+82
-20
lines changed

2 files changed

+82
-20
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
### Added
2121

2222
- Improve performance of incremental rebuilds for `@tailwindcss/cli` ([#13169](https://github.com/tailwindlabs/tailwindcss/pull/13169))
23+
- Improve performance of incremental rebuilds for `@tailwindcss/postcss` ([#13170](https://github.com/tailwindlabs/tailwindcss/pull/13170))
2324

2425
## [4.0.0-alpha.7] - 2024-03-08
2526

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

+81-20
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,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

Comments
 (0)