1- import type { TailwindV4Engine , TailwindV4GenerateOptions , TailwindV4ResolvedSource } from './types'
1+ import type { IStyleHandlerOptions } from '@weapp-tailwindcss/postcss/types'
2+ import type {
3+ TailwindV4Engine ,
4+ TailwindV4GenerateOptions ,
5+ TailwindV4GenerateTarget ,
6+ TailwindV4ResolvedSource ,
7+ TailwindV4SourcePattern ,
8+ } from './types'
9+ import fs from 'node:fs'
210import path from 'node:path'
311import postcss from 'postcss'
412import { createTailwindV4Engine as createPatchTailwindV4Engine } from 'tailwindcss-patch'
513import { omitUndefined } from '@/utils/object'
614import { filterUnsupportedMiniProgramTailwindV4Candidates } from './candidates'
15+ import { loadTailwindV4DesignSystem } from './design-system'
716import { transformTailwindV4CssByTarget } from './miniprogram'
817import { applyTailwindV3CompatibilityCss } from './tailwind-v3-compatibility'
918import { createTailwindV4DefaultColorThemeCss } from './tailwind-v4-default-colors'
1019
1120type TailwindV4ScanSourcePatterns = Exclude < NonNullable < TailwindV4GenerateOptions [ 'scanSources' ] > , boolean >
1221type TailwindV4ResolvedScanSources = TailwindV4GenerateOptions [ 'scanSources' ]
1322
23+ const incrementalGenerateCache = new Map < string , TailwindV4IncrementalGenerateCacheEntry > ( )
24+
25+ interface TailwindV4IncrementalGenerateCacheEntry {
26+ seenCandidates : Set < string >
27+ classSet : Set < string >
28+ css : string
29+ rawCss : string
30+ dependencies : string [ ]
31+ sources : TailwindV4SourcePattern [ ]
32+ root : null | 'none' | {
33+ base : string
34+ pattern : string
35+ }
36+ target : TailwindV4GenerateTarget
37+ }
38+
1439function findLeadingImportInsertionIndex ( css : string ) {
1540 const importPattern = / (?: ^ | \n ) \s * @ i m p o r t \b [ ^ ; ] * ; / g
1641 let insertionIndex = 0
@@ -31,6 +56,84 @@ function applyMiniProgramTailwindV4DefaultColorCss(css: string) {
3156 return `${ css . slice ( 0 , insertionIndex ) } \n${ themeCss } \n${ css . slice ( insertionIndex ) } `
3257}
3358
59+ function collectCandidates ( candidates : Iterable < string > | undefined ) {
60+ return new Set ( candidates ?? [ ] )
61+ }
62+
63+ function createStableJson ( value : unknown ) : string {
64+ if ( value === undefined ) {
65+ return 'undefined'
66+ }
67+ if ( value === null || typeof value !== 'object' ) {
68+ return JSON . stringify ( value )
69+ }
70+ if ( Array . isArray ( value ) ) {
71+ return `[${ value . map ( item => createStableJson ( item ) ) . join ( ',' ) } ]`
72+ }
73+ return `{${ Object . keys ( value ) . sort ( ) . map ( ( key ) => {
74+ const record = value as Record < string , unknown >
75+ return `${ JSON . stringify ( key ) } :${ createStableJson ( record [ key ] ) } `
76+ } ) . join ( ',' ) } }`
77+ }
78+
79+ function createDependencyFingerprint ( files : string [ ] ) {
80+ return files . map ( ( file ) => {
81+ try {
82+ const stat = fs . statSync ( file )
83+ return `${ file } :${ stat . size } :${ stat . mtimeMs } `
84+ }
85+ catch {
86+ return `${ file } :missing`
87+ }
88+ } ) . join ( '|' )
89+ }
90+
91+ function createIncrementalGenerateCacheKey (
92+ source : TailwindV4ResolvedSource ,
93+ target : TailwindV4GenerateTarget ,
94+ styleOptions : Partial < IStyleHandlerOptions > | undefined ,
95+ tailwindcssV3Compatibility : boolean | undefined ,
96+ ) {
97+ return [
98+ source . projectRoot ,
99+ source . base ,
100+ createStableJson ( source . baseFallbacks ) ,
101+ source . css ,
102+ createDependencyFingerprint ( source . dependencies ) ,
103+ target ,
104+ createStableJson ( styleOptions ) ,
105+ createStableJson ( tailwindcssV3Compatibility ) ,
106+ ] . join ( '\0' )
107+ }
108+
109+ function createCompatibleSource (
110+ source : TailwindV4ResolvedSource ,
111+ target : TailwindV4GenerateTarget ,
112+ tailwindcssV3Compatibility : boolean | undefined ,
113+ ) {
114+ const shouldApplyTailwindV3Compatibility = tailwindcssV3Compatibility ?? target === 'weapp'
115+ const filteredSourceCss = target === 'weapp'
116+ ? removeUnlayeredTailwindV4PreflightImports ( source . css )
117+ : source . css
118+ const sourceCss = shouldApplyTailwindV3Compatibility
119+ ? applyTailwindV3CompatibilityCss ( filteredSourceCss )
120+ : target === 'weapp'
121+ ? applyMiniProgramTailwindV4DefaultColorCss ( filteredSourceCss )
122+ : filteredSourceCss
123+ const compatibleSourceCss = removeUnsupportedThemeVendorKeyframes ( sourceCss )
124+ return compatibleSourceCss === source . css ? source : { ...source , css : compatibleSourceCss }
125+ }
126+
127+ function resolveTargetCandidates (
128+ candidates : Iterable < string > | undefined ,
129+ target : TailwindV4GenerateTarget ,
130+ ) {
131+ const collected = collectCandidates ( candidates )
132+ return target === 'weapp'
133+ ? filterUnsupportedMiniProgramTailwindV4Candidates ( collected )
134+ : collected
135+ }
136+
34137function parseImportSourceParam ( params : string ) {
35138 const match = / \b s o u r c e \( \s * ( n o n e | ( [ ' " ] ) ( .* ?) \2) \s * \) / . exec ( params )
36139 if ( ! match ) {
@@ -226,32 +329,22 @@ function removeUnsupportedThemeVendorKeyframes(css: string) {
226329}
227330
228331export function createTailwindV4Engine ( source : TailwindV4ResolvedSource ) : TailwindV4Engine {
229- async function generate ( options : TailwindV4GenerateOptions = { } ) {
332+ async function generateOnce (
333+ generateSource : TailwindV4ResolvedSource ,
334+ options : TailwindV4GenerateOptions = { } ,
335+ ) {
230336 const {
231337 scanSources = true ,
232338 styleOptions,
233339 tailwindcssV3Compatibility,
234340 target = 'weapp' ,
235341 ...patchOptions
236342 } = options
237- const shouldApplyTailwindV3Compatibility = tailwindcssV3Compatibility ?? target === 'weapp'
238- const filteredSourceCss = target === 'weapp'
239- ? removeUnlayeredTailwindV4PreflightImports ( source . css )
240- : source . css
241- const sourceCss = shouldApplyTailwindV3Compatibility
242- ? applyTailwindV3CompatibilityCss ( filteredSourceCss )
243- : target === 'weapp'
244- ? applyMiniProgramTailwindV4DefaultColorCss ( filteredSourceCss )
245- : filteredSourceCss
246- const compatibleSourceCss = removeUnsupportedThemeVendorKeyframes ( sourceCss )
247- const candidates = target === 'weapp'
248- ? filterUnsupportedMiniProgramTailwindV4Candidates ( patchOptions . candidates )
249- : patchOptions . candidates
250- const engine = createPatchTailwindV4Engine (
251- compatibleSourceCss === source . css ? source : { ...source , css : compatibleSourceCss } ,
252- )
343+ const compatibleSource = createCompatibleSource ( generateSource , target , tailwindcssV3Compatibility )
344+ const candidates = resolveTargetCandidates ( patchOptions . candidates , target )
345+ const engine = createPatchTailwindV4Engine ( compatibleSource )
253346 const result = await engine . generate ( omitUndefined ( {
254- scanSources : resolveScanSources ( source , scanSources ) ,
347+ scanSources : resolveScanSources ( compatibleSource , scanSources ) ,
255348 ...patchOptions ,
256349 candidates,
257350 } ) )
@@ -266,6 +359,93 @@ export function createTailwindV4Engine(source: TailwindV4ResolvedSource): Tailwi
266359 }
267360 }
268361
362+ async function generateWithIncrementalCache ( options : TailwindV4GenerateOptions = { } ) {
363+ if ( ( options . sources ?. length ?? 0 ) > 0 || options . bareArbitraryValues !== undefined || options . scanSources === true || Array . isArray ( options . scanSources ) ) {
364+ return generateOnce ( source , options )
365+ }
366+
367+ const target = options . target ?? 'weapp'
368+ const compatibleSource = createCompatibleSource ( source , target , options . tailwindcssV3Compatibility )
369+ const requestedCandidates = resolveTargetCandidates ( options . candidates , target )
370+ const cacheKey = createIncrementalGenerateCacheKey (
371+ compatibleSource ,
372+ target ,
373+ options . styleOptions ,
374+ options . tailwindcssV3Compatibility ,
375+ )
376+ const cached = incrementalGenerateCache . get ( cacheKey )
377+ if ( cached ) {
378+ const missingCandidates = [ ...requestedCandidates ] . filter ( candidate => ! cached . seenCandidates . has ( candidate ) )
379+ if ( missingCandidates . length === 0 ) {
380+ return {
381+ css : cached . css ,
382+ rawCss : cached . rawCss ,
383+ classSet : new Set ( cached . classSet ) ,
384+ rawCandidates : new Set ( cached . seenCandidates ) ,
385+ dependencies : cached . dependencies ,
386+ sources : cached . sources ,
387+ root : cached . root ,
388+ target : cached . target ,
389+ }
390+ }
391+
392+ const designSystem = await loadTailwindV4DesignSystem ( compatibleSource )
393+ const cssByCandidate = designSystem . candidatesToCss ( missingCandidates )
394+ const rawCssParts : string [ ] = [ ]
395+ const classSet = new Set < string > ( )
396+ for ( let index = 0 ; index < missingCandidates . length ; index += 1 ) {
397+ const candidate = missingCandidates [ index ]
398+ const css = cssByCandidate [ index ]
399+ if ( candidate && typeof css === 'string' && css . trim ( ) . length > 0 ) {
400+ rawCssParts . push ( css )
401+ classSet . add ( candidate )
402+ }
403+ }
404+ const rawCss = rawCssParts . join ( '\n' )
405+ const css = rawCss . length > 0
406+ ? await transformTailwindV4CssByTarget ( rawCss , target , options . styleOptions )
407+ : ''
408+
409+ for ( const candidate of missingCandidates ) {
410+ cached . seenCandidates . add ( candidate )
411+ }
412+ for ( const className of classSet ) {
413+ cached . classSet . add ( className )
414+ }
415+ cached . css = [ cached . css , css ] . filter ( Boolean ) . join ( '\n' )
416+ cached . rawCss = [ cached . rawCss , rawCss ] . filter ( Boolean ) . join ( '\n' )
417+ return {
418+ css : cached . css ,
419+ rawCss : cached . rawCss ,
420+ classSet : new Set ( cached . classSet ) ,
421+ rawCandidates : new Set ( cached . seenCandidates ) ,
422+ dependencies : cached . dependencies ,
423+ sources : cached . sources ,
424+ root : cached . root ,
425+ target : cached . target ,
426+ }
427+ }
428+
429+ const generated = await generateOnce ( source , options )
430+ incrementalGenerateCache . set ( cacheKey , {
431+ seenCandidates : new Set ( requestedCandidates ) ,
432+ classSet : new Set ( generated . classSet ) ,
433+ css : generated . css ,
434+ rawCss : generated . rawCss ,
435+ dependencies : generated . dependencies ,
436+ sources : generated . sources ,
437+ root : generated . root ,
438+ target : generated . target ,
439+ } )
440+ return generated
441+ }
442+
443+ async function generate ( options : TailwindV4GenerateOptions = { } ) {
444+ return options . incrementalCache
445+ ? generateWithIncrementalCache ( options )
446+ : generateOnce ( source , options )
447+ }
448+
269449 return {
270450 source,
271451 loadDesignSystem : createPatchTailwindV4Engine ( source ) . loadDesignSystem ,
0 commit comments