1+ import type { IStyleHandlerOptions } from '@weapp-tailwindcss/postcss/types'
12import type { Config } from 'tailwindcss'
23import type {
34 TailwindV3CandidateSource ,
45 TailwindV3Engine ,
56 TailwindV3GenerateOptions ,
7+ TailwindV3GenerateTarget ,
68 TailwindV3ResolvedSource ,
79} from './types'
10+ import fs from 'node:fs'
811import { createRequire } from 'node:module'
912import postcss from 'postcss'
1013import { createTailwindcssPatcher } from '@/tailwindcss/patcher'
@@ -21,6 +24,16 @@ interface TailwindcssPlugin {
2124}
2225
2326const runtimeReadyPromiseCache = new Map < string , Promise < void > > ( )
27+ const incrementalGenerateCache = new Map < string , TailwindV3IncrementalGenerateCacheEntry > ( )
28+
29+ interface TailwindV3IncrementalGenerateCacheEntry {
30+ seenCandidates : Set < string >
31+ classSet : Set < string >
32+ css : string
33+ rawCss : string
34+ dependencies : string [ ]
35+ target : TailwindV3GenerateTarget
36+ }
2437
2538interface LegacyContentObject {
2639 files ?: unknown
@@ -51,6 +64,10 @@ function createRawContentEntries(candidates: Iterable<string>, sources: Tailwind
5164 return entries
5265}
5366
67+ function collectCandidates ( candidates : Iterable < string > | undefined ) {
68+ return new Set ( candidates ?? [ ] )
69+ }
70+
5471function mergeContent ( content : unknown , rawEntries : Array < { raw : string , extension : string } > ) {
5572 if ( isLegacyContentObject ( content ) ) {
5673 return {
@@ -111,6 +128,51 @@ function loadTailwindcssPlugin(source: TailwindV3ResolvedSource): TailwindcssPlu
111128 return typeof plugin === 'function' ? plugin : plugin . default as TailwindcssPlugin
112129}
113130
131+ function createStableJson ( value : unknown ) : string {
132+ if ( value === undefined ) {
133+ return 'undefined'
134+ }
135+ if ( value === null || typeof value !== 'object' ) {
136+ return JSON . stringify ( value )
137+ }
138+ if ( Array . isArray ( value ) ) {
139+ return `[${ value . map ( item => createStableJson ( item ) ) . join ( ',' ) } ]`
140+ }
141+ return `{${ Object . keys ( value ) . sort ( ) . map ( ( key ) => {
142+ const record = value as Record < string , unknown >
143+ return `${ JSON . stringify ( key ) } :${ createStableJson ( record [ key ] ) } `
144+ } ) . join ( ',' ) } }`
145+ }
146+
147+ function createDependencyFingerprint ( files : string [ ] ) {
148+ return files . map ( ( file ) => {
149+ try {
150+ const stat = fs . statSync ( file )
151+ return `${ file } :${ stat . size } :${ stat . mtimeMs } `
152+ }
153+ catch {
154+ return `${ file } :missing`
155+ }
156+ } ) . join ( '|' )
157+ }
158+
159+ function createIncrementalGenerateCacheKey (
160+ source : TailwindV3ResolvedSource ,
161+ target : TailwindV3GenerateTarget ,
162+ styleOptions : Partial < IStyleHandlerOptions > | undefined ,
163+ ) {
164+ return [
165+ source . packageName ,
166+ source . postcssPlugin ,
167+ source . cwd ,
168+ source . config ?? 'config:missing' ,
169+ createDependencyFingerprint ( source . dependencies ) ,
170+ source . css ,
171+ target ,
172+ createStableJson ( styleOptions ) ,
173+ ] . join ( '\0' )
174+ }
175+
114176function createRuntimeReadyCacheKey ( source : TailwindV3ResolvedSource , rootPath : string | undefined ) {
115177 return [
116178 source . packageName ,
@@ -127,6 +189,42 @@ function resetTailwindcssPluginContext(plugin: TailwindcssPlugin) {
127189 }
128190}
129191
192+ function isTailwindImport ( params : string , layer : string ) {
193+ const trimmed = params . trim ( )
194+ return new RegExp ( `^(?:url\\()?['"]tailwindcss/${ layer } (?:\\.css)?['"]\\)?(?:\\s|$)` ) . test ( trimmed )
195+ }
196+
197+ function createUtilitiesOnlyCss ( css : string ) {
198+ try {
199+ const root = postcss . parse ( css )
200+ root . walkAtRules ( ( rule ) => {
201+ if ( rule . name === 'tailwind' ) {
202+ const layer = rule . params . trim ( )
203+ if ( layer === 'base' || layer === 'components' ) {
204+ rule . remove ( )
205+ }
206+ return
207+ }
208+ if ( rule . name === 'import' && ( isTailwindImport ( rule . params , 'base' ) || isTailwindImport ( rule . params , 'components' ) ) ) {
209+ rule . remove ( )
210+ return
211+ }
212+ if ( rule . name === 'layer' ) {
213+ const layer = rule . params . trim ( )
214+ if ( layer === 'base' || layer === 'components' ) {
215+ rule . remove ( )
216+ }
217+ }
218+ } )
219+ return root . toString ( )
220+ }
221+ catch {
222+ return css
223+ . replace ( / @ t a i l w i n d \s + (?: b a s e | c o m p o n e n t s ) \s * ; / g, '' )
224+ . replace ( / @ i m p o r t \s + (?: u r l \( ) ? [ ' " ] t a i l w i n d c s s \/ (?: b a s e | c o m p o n e n t s ) (?: \. c s s ) ? [ ' " ] [ ^ ; ] * ; / g, '' )
225+ }
226+ }
227+
130228function collectClassSet ( plugin : TailwindcssPlugin ) {
131229 const classSet = new Set < string > ( )
132230 for ( const context of plugin . contextRef ?. value ?? [ ] ) {
@@ -181,25 +279,28 @@ function createRuntimeReadyPromise(source: TailwindV3ResolvedSource) {
181279export function createTailwindV3Engine ( source : TailwindV3ResolvedSource ) : TailwindV3Engine {
182280 const runtimeReadyPromise = createRuntimeReadyPromise ( source )
183281
184- async function generate ( options : TailwindV3GenerateOptions = { } ) {
282+ async function generateOnce (
283+ generateSource : TailwindV3ResolvedSource ,
284+ options : TailwindV3GenerateOptions = { } ,
285+ ) {
185286 await runtimeReadyPromise
186287
187288 const {
188289 styleOptions,
189290 target = 'weapp' ,
190291 } = options
191- const tailwindcss = loadTailwindcssPlugin ( source )
292+ const tailwindcss = loadTailwindcssPlugin ( generateSource )
192293 resetTailwindcssPluginContext ( tailwindcss )
193- const tailwindConfig = createTailwindConfig ( source , options )
294+ const tailwindConfig = createTailwindConfig ( generateSource , options )
194295 const result = await postcss ( [
195296 tailwindcss ( tailwindConfig ) ,
196- ] ) . process ( source . css , {
297+ ] ) . process ( generateSource . css , {
197298 from : undefined ,
198299 } )
199300 const rawCss = result . css
200301 const css = await transformTailwindV3CssByTarget ( rawCss , target , styleOptions )
201302 const dependencies = collectDependencyMessages ( result )
202- for ( const dependency of source . dependencies ) {
303+ for ( const dependency of generateSource . dependencies ) {
203304 dependencies . add ( dependency )
204305 }
205306 const classSet = collectClassSet ( tailwindcss )
@@ -208,7 +309,7 @@ export function createTailwindV3Engine(source: TailwindV3ResolvedSource): Tailwi
208309 css,
209310 rawCss,
210311 classSet,
211- rawCandidates : new Set ( options . candidates ?? [ ] ) ,
312+ rawCandidates : collectCandidates ( options . candidates ) ,
212313 dependencies : [ ...dependencies ] ,
213314 sources : [ ] ,
214315 root : null ,
@@ -217,6 +318,79 @@ export function createTailwindV3Engine(source: TailwindV3ResolvedSource): Tailwi
217318 }
218319 }
219320
321+ async function generateWithIncrementalCache ( options : TailwindV3GenerateOptions = { } ) {
322+ if ( ( options . sources ?. length ?? 0 ) > 0 ) {
323+ return generateOnce ( source , options )
324+ }
325+
326+ const target = options . target ?? 'weapp'
327+ const requestedCandidates = collectCandidates ( options . candidates )
328+ const cacheKey = createIncrementalGenerateCacheKey ( source , target , options . styleOptions )
329+ const cached = incrementalGenerateCache . get ( cacheKey )
330+ if ( cached ) {
331+ const missingCandidates = [ ...requestedCandidates ] . filter ( candidate => ! cached . seenCandidates . has ( candidate ) )
332+ if ( missingCandidates . length === 0 ) {
333+ return {
334+ css : cached . css ,
335+ rawCss : cached . rawCss ,
336+ classSet : new Set ( cached . classSet ) ,
337+ rawCandidates : new Set ( cached . seenCandidates ) ,
338+ dependencies : cached . dependencies ,
339+ sources : [ ] ,
340+ root : null ,
341+ target : cached . target ,
342+ version : 3 as const ,
343+ }
344+ }
345+
346+ const utilitySource = {
347+ ...source ,
348+ css : createUtilitiesOnlyCss ( source . css ) ,
349+ }
350+ const generated = await generateOnce ( utilitySource , {
351+ ...options ,
352+ candidates : missingCandidates ,
353+ } )
354+ for ( const candidate of missingCandidates ) {
355+ cached . seenCandidates . add ( candidate )
356+ }
357+ for ( const className of generated . classSet ) {
358+ cached . classSet . add ( className )
359+ }
360+ cached . css = [ cached . css , generated . css ] . filter ( Boolean ) . join ( '\n' )
361+ cached . rawCss = [ cached . rawCss , generated . rawCss ] . filter ( Boolean ) . join ( '\n' )
362+ cached . dependencies = [ ...new Set ( [ ...cached . dependencies , ...generated . dependencies ] ) ]
363+ return {
364+ css : cached . css ,
365+ rawCss : cached . rawCss ,
366+ classSet : new Set ( cached . classSet ) ,
367+ rawCandidates : new Set ( cached . seenCandidates ) ,
368+ dependencies : cached . dependencies ,
369+ sources : [ ] ,
370+ root : null ,
371+ target : cached . target ,
372+ version : 3 as const ,
373+ }
374+ }
375+
376+ const generated = await generateOnce ( source , options )
377+ incrementalGenerateCache . set ( cacheKey , {
378+ seenCandidates : new Set ( requestedCandidates ) ,
379+ classSet : new Set ( generated . classSet ) ,
380+ css : generated . css ,
381+ rawCss : generated . rawCss ,
382+ dependencies : generated . dependencies ,
383+ target : generated . target ,
384+ } )
385+ return generated
386+ }
387+
388+ async function generate ( options : TailwindV3GenerateOptions = { } ) {
389+ return options . incrementalCache
390+ ? generateWithIncrementalCache ( options )
391+ : generateOnce ( source , options )
392+ }
393+
220394 return {
221395 source,
222396 async validateCandidates ( candidates ) {
0 commit comments