use incremental builds in PostCSS

This commit is contained in:
Robin Malfait 2024-03-08 18:29:51 +01:00
parent 316190fe5a
commit a99bcfba60
No known key found for this signature in database

View file

@ -1,8 +1,30 @@
import { scanDir } from '@tailwindcss/oxide'
import fs from 'fs'
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
import postcssImport from 'postcss-import'
import { compile, optimizeCss } from 'tailwindcss'
/**
* A Map that can generate default values for keys that don't exist.
* Generated default values are added to the map to avoid recomputation.
*/
class DefaultMap<T = string, V = any> extends Map<T, V> {
constructor(private factory: (key: T, self: DefaultMap<T, V>) => V) {
super()
}
get(key: T): V {
let value = super.get(key)
if (value === undefined) {
value = this.factory(key, this)
this.set(key, value)
}
return value
}
}
type PluginOptions = {
// The base directory to scan for class candidates.
base?: string
@ -15,6 +37,15 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
let base = opts.base ?? process.cwd()
let optimize = opts.optimize ?? process.env.NODE_ENV === 'production'
let cache = new DefaultMap(() => {
return {
mtimes: new Map<string, number>(),
rebuild: null as null | ReturnType<typeof compile>['rebuild'],
previousCss: '',
previousOptimizedCss: '',
}
})
return {
postcssPlugin: '@tailwindcss/postcss',
plugins: [
@ -22,6 +53,38 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
postcssImport(),
(root, result) => {
let from = result.opts.from ?? ''
let context = cache.get(from)
let rebuildStrategy: 'full' | 'incremental' = 'incremental'
// Bookkeeping — track file modification times to CSS files
{
let changedTime = fs.statSync(from, { throwIfNoEntry: false })?.mtimeMs ?? null
if (changedTime !== null) {
let prevTime = context.mtimes.get(from)
if (prevTime !== changedTime) {
rebuildStrategy = 'full'
context.mtimes.set(from, changedTime)
}
} else {
rebuildStrategy = 'full'
}
for (let message of result.messages) {
if (message.type === 'dependency') {
let file = message.file as string
let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null
if (changedTime !== null) {
let prevTime = context.mtimes.get(file)
if (prevTime !== changedTime) {
rebuildStrategy = 'full'
context.mtimes.set(file, changedTime)
}
}
}
}
}
let hasApply = false
let hasTailwind = false
@ -40,24 +103,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
// Do nothing if neither `@tailwind` nor `@apply` is used
if (!hasTailwind && !hasApply) return
function replaceCss(css: string) {
root.removeAll()
let output = css
if (optimize) {
output = optimizeCss(output, {
minify: typeof optimize === 'object' ? optimize.minify : false,
})
}
root.append(postcss.parse(output, result.opts))
}
let compileResult = compile(root.toString(), [])
// No `@tailwind` means we don't have to look for candidates
if (!hasTailwind) {
replaceCss(compileResult.css)
return
}
let css = ''
// Look for candidates used to generate the CSS
let { candidates, files, globs } = scanDir({ base, globs: true })
@ -85,9 +131,42 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
})
}
console.time('Compile')
replaceCss(compileResult.rebuild(candidates))
console.timeEnd('Compile')
if (rebuildStrategy === 'full') {
if (hasTailwind) {
let compileResult = compile(root.toString(), candidates)
css = compileResult.css
context.rebuild = compileResult.rebuild.bind(compileResult)
} else {
css = compile(root.toString(), []).css
}
} else if (rebuildStrategy === 'incremental') {
css = context.rebuild!(candidates)
}
function replaceCss(css: string) {
root.removeAll()
root.append(postcss.parse(css, result.opts))
}
// Replace CSS
if (css === context.previousCss) {
if (optimize) {
replaceCss(context.previousOptimizedCss)
} else {
replaceCss(css)
}
} else {
if (optimize) {
let optimizedCss = optimizeCss(css, {
minify: typeof optimize === 'object' ? optimize.minify : false,
})
replaceCss(optimizedCss)
context.previousOptimizedCss = optimizedCss
} else {
replaceCss(css)
}
context.previousCss = css
}
},
],
}