From de4ba91f68f606a9a7d06dba433536acf1f185bd Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 23:39:29 +0100 Subject: [PATCH 1/7] rename PostCSS plugin name from `tailwindcss-v4` to `@tailwindcss/postcss` --- packages/@tailwindcss-postcss/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 303ffb92414b..82b353f5a024 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -16,7 +16,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { let optimize = opts.optimize ?? process.env.NODE_ENV === 'production' return { - postcssPlugin: 'tailwindcss-v4', + postcssPlugin: '@tailwindcss/postcss', plugins: [ // We need to run `postcss-import` first to handle `@import` rules. postcssImport(), @@ -64,7 +64,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { for (let file of files) { result.messages.push({ type: 'dependency', - plugin: 'tailwindcss-v4', + plugin: '@tailwindcss/postcss', file, parent: result.opts.from, }) @@ -76,7 +76,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { for (let { base, glob } of globs) { result.messages.push({ type: 'dir-dependency', - plugin: 'tailwindcss-v4', + plugin: '@tailwindcss/postcss', dir: base, glob, parent: result.opts.from, From e7973db64793f72b07469f8f6f461f22363a8ab9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 23:40:03 +0100 Subject: [PATCH 2/7] add incremental rebuilds to `@tailwindcss/postcss` --- packages/@tailwindcss-postcss/src/index.ts | 117 ++++++++++++++++++--- 1 file changed, 100 insertions(+), 17 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 82b353f5a024..d119c28c1192 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(), + build: null as null | ReturnType['build'], + 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,22 +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 : true, - }) - } - root.append(postcss.parse(output, result.opts)) - } - - // No `@tailwind` means we don't have to look for candidates - if (!hasTailwind) { - replaceCss(compile(root.toString()).build([])) - return - } + let css = '' // Look for candidates used to generate the CSS let { candidates, files, globs } = scanDir({ base, globs: true }) @@ -83,7 +131,42 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { }) } - replaceCss(compile(root.toString()).build(candidates)) + if (rebuildStrategy === 'full') { + if (hasTailwind) { + let { build } = compile(root.toString()) + css = build(candidates) + context.build = build + } else { + css = compile(root.toString()).build([]) + } + } else if (rebuildStrategy === 'incremental') { + css = context.build!(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 : true, + }) + replaceCss(optimizedCss) + context.previousOptimizedCss = optimizedCss + } else { + replaceCss(css) + } + context.previousCss = css + } }, ], } From 479910468f61571578f95b3bdf6bcf69168d8543 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 11 Mar 2024 19:29:20 +0100 Subject: [PATCH 3/7] improve comment --- packages/@tailwindcss-postcss/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index d119c28c1192..d316607662e6 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -58,7 +58,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { let rebuildStrategy: 'full' | 'incremental' = 'incremental' - // Bookkeeping — track file modification times to CSS files + // Track file modification times to CSS files { let changedTime = fs.statSync(from, { throwIfNoEntry: false })?.mtimeMs ?? null if (changedTime !== null) { From 20c823cf63701382bdaf4629117b9214b629bdfb Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 11 Mar 2024 19:33:39 +0100 Subject: [PATCH 4/7] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f5e5b536f7..e34ff6c6481f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Improve performance of incremental rebuilds for `@tailwindcss/cli` ([#13169](https://github.com/tailwindlabs/tailwindcss/pull/13169)) +- Improve performance of incremental rebuilds for `@tailwindcss/postcss` ([#13170](https://github.com/tailwindlabs/tailwindcss/pull/13170)) ## [4.0.0-alpha.7] - 2024-03-08 From ab90cb8905dba61cbb2a967acfb619faf14f6c57 Mon Sep 17 00:00:00 2001 From: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:41:30 -0400 Subject: [PATCH 5/7] Simplify nested conditionals --- packages/@tailwindcss-postcss/src/index.ts | 27 +++++++--------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index d316607662e6..f4a0437578d5 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -41,8 +41,8 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { return { mtimes: new Map(), build: null as null | ReturnType['build'], - previousCss: '', - previousOptimizedCss: '', + css: '', + optimizedCss: '', } }) @@ -149,24 +149,13 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } // 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 : true, - }) - replaceCss(optimizedCss) - context.previousOptimizedCss = optimizedCss - } else { - replaceCss(css) - } - context.previousCss = css + if (css !== context.css && optimize) { + context.optimizedCss = optimizeCss(css, { + minify: typeof optimize === 'object' ? optimize.minify : true, + }) } + context.css = css + replaceCss(optimize ? context.optimizedCss : context.css) }, ], } From c609dfb8529968be13c8a70b99edc2e9baf7fa3c Mon Sep 17 00:00:00 2001 From: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:45:44 -0400 Subject: [PATCH 6/7] Simplify more conditionals --- packages/@tailwindcss-postcss/src/index.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index f4a0437578d5..bb6bc3f06cb3 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -132,22 +132,13 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } if (rebuildStrategy === 'full') { - if (hasTailwind) { - let { build } = compile(root.toString()) - css = build(candidates) - context.build = build - } else { - css = compile(root.toString()).build([]) - } + let { build } = compile(root.toString()) + context.build = build + css = build(hasTailwind ? candidates : []) } else if (rebuildStrategy === 'incremental') { css = context.build!(candidates) } - function replaceCss(css: string) { - root.removeAll() - root.append(postcss.parse(css, result.opts)) - } - // Replace CSS if (css !== context.css && optimize) { context.optimizedCss = optimizeCss(css, { @@ -155,7 +146,8 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { }) } context.css = css - replaceCss(optimize ? context.optimizedCss : context.css) + root.removeAll() + root.append(postcss.parse(optimize ? context.optimizedCss : context.css, result.opts)) }, ], } From 12f6743382cb4f54a112a701d8edc24345a1ebb1 Mon Sep 17 00:00:00 2001 From: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:57:47 -0400 Subject: [PATCH 7/7] Refactor file modification tracking --- packages/@tailwindcss-postcss/src/index.ts | 41 ++++++++++------------ 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index bb6bc3f06cb3..fbaeb0bd842f 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -53,35 +53,32 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { postcssImport(), (root, result) => { - let from = result.opts.from ?? '' - let context = cache.get(from) + let inputFile = result.opts.from ?? '' + let context = cache.get(inputFile) let rebuildStrategy: 'full' | 'incremental' = 'incremental' // 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 files = result.messages.flatMap((message) => { + if (message.type !== 'dependency') return [] + return message.file + }) + files.push(inputFile) + for (let file of files) { + let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null + if (changedTime === null) { + if (file === inputFile) { + rebuildStrategy = 'full' } + continue } + + let prevTime = context.mtimes.get(file) + if (prevTime === changedTime) continue + + rebuildStrategy = 'full' + context.mtimes.set(file, changedTime) } }