From a99bcfba60715f9b26540f956fe2c90b2725fed9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 18:29:51 +0100 Subject: [PATCH] use incremental builds in PostCSS --- packages/@tailwindcss-postcss/src/index.ts | 121 +++++++++++++++++---- 1 file changed, 100 insertions(+), 21 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 6555bbfd0..0e200a6f1 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -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 extends Map { + constructor(private factory: (key: T, self: DefaultMap) => 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(), + rebuild: null as null | ReturnType['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 + } }, ], }