1
- import type { Component , Editor } from "grapesjs" ;
2
1
// @ts -ignore
3
2
import * as tailwindcss from "tailwindcss" ;
4
3
import * as assets from "./assets" ;
5
- import type { WorkerMessageData , WorkerResponse } from "./worker" ;
4
+
5
+ import type { Component , Editor } from "grapesjs" ;
6
6
7
7
export type TailwindPluginOptions = {
8
8
/**
@@ -64,34 +64,118 @@ export default (editor: Editor, opts: TailwindPluginOptions = {}) => {
64
64
65
65
const STYLE_ID = "tailwindcss-plugin" ;
66
66
67
- // Create worker to elaborate big html structure in chunk
68
- const worker = new Worker ( new URL ( "./worker.ts" , import . meta. url ) , {
69
- type : "module" ,
70
- } ) ;
67
+ /** Tailwind CSS compiler instance */
68
+ let compiler : Awaited < ReturnType < typeof tailwindcss . compile > > ;
71
69
72
70
/** Reference to the <style> element where generated Tailwind CSS is injected */
73
71
let tailwindStyle : HTMLStyleElement | undefined ;
74
72
73
+ // Cache to store processed Tailwind classes to avoid unnecessary recompilation
74
+ const classesCache = new Set < string > ( ) ;
75
+
76
+ // Build the Tailwind CSS compiler using tailwindcss.compile with a custom stylesheet loader
77
+ const buildCompiler = async ( ) => {
78
+ compiler = await tailwindcss . compile (
79
+ `@import "tailwindcss" prefix(${ options . prefix } );${
80
+ options . customCss ?? ""
81
+ } `,
82
+ {
83
+ base : "/" ,
84
+ loadStylesheet,
85
+ } ,
86
+ ) ;
87
+ } ;
88
+
75
89
// Override the editor's getCss method to append the generated Tailwind CSS
76
90
const originalGetCss = editor . getCss . bind ( editor ) ;
77
91
editor . getCss = ( ) => {
78
92
const originalCss = originalGetCss ( ) ;
79
93
return `${ originalCss } \n${ tailwindStyle ?. textContent ?? "" } ` ;
80
94
} ;
81
95
82
- // Worker response handler. Here we receive the compiled tailwind css
83
- worker . onmessage = async ( event : MessageEvent < WorkerResponse > ) => {
84
- const { data } = event . data ;
85
- if ( data ) {
86
- const { tailwindcss, notify } = data ;
87
- // Append the compiled tailwind css to the tailwind style element
88
- if ( typeof tailwindcss === "string" && tailwindStyle !== undefined ) {
89
- tailwindStyle . textContent = tailwindcss ;
96
+ // Custom stylesheet loader function for Tailwind CSS assets
97
+ async function loadStylesheet ( id : string , base : string ) {
98
+ if ( id === "tailwindcss" ) {
99
+ return { base, content : assets . css . index } ;
100
+ }
101
+ if ( id . includes ( "preflight" ) ) {
102
+ return { base, content : assets . css . preflight } ;
103
+ }
104
+ if ( id . includes ( "theme" ) ) {
105
+ return { base, content : assets . css . theme } ;
106
+ }
107
+ if ( id . includes ( "utilities" ) ) {
108
+ return { base, content : assets . css . utilities } ;
109
+ }
110
+ return { base, content : "" } ;
111
+ }
112
+
113
+ // Initialize the Tailwind compiler, clear the classes cache, and set up the style element
114
+ const initTailwindCompiler = async ( ) => {
115
+ await buildCompiler ( ) ;
116
+ classesCache . clear ( ) ;
117
+ } ;
118
+
119
+ // Extract all Tailwind-related classes from the editor's HTML content
120
+ const getClassesFromHtml = ( html : string ) => {
121
+ const classRegex = / c l a s s = [ " ' ] ( [ ^ " ' ] + ) [ " ' ] / g;
122
+ const currentClasses = new Set < string > ( ) ;
123
+
124
+ // biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
125
+ let match ;
126
+ // biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
127
+ while ( ( match = classRegex . exec ( html ) ) !== null ) {
128
+ const classes = match [ 1 ] . split ( " " ) ;
129
+ for ( const cls of classes ) {
130
+ if ( cls . startsWith ( options . prefix ) ) {
131
+ currentClasses . add ( cls ) ;
132
+ }
90
133
}
91
- if ( notify ) {
92
- options . notificationCallback ( ) ;
134
+ }
135
+ return currentClasses ;
136
+ } ;
137
+
138
+ const processRemovedClasses = async ( currentClasses : Set < string > ) => {
139
+ // Identify classes that have been removed
140
+ let changed = false ;
141
+ const classesToRemove : string [ ] = [ ] ;
142
+ for ( const cls of classesCache ) {
143
+ if ( ! currentClasses . has ( cls ) ) {
144
+ classesToRemove . push ( cls ) ;
145
+ changed = true ;
93
146
}
94
147
}
148
+
149
+ // Remove classes non more used
150
+ if ( classesToRemove . length ) {
151
+ for ( const c of classesToRemove ) {
152
+ classesCache . delete ( c ) ;
153
+ }
154
+ // Rebuild the compiler to purge Tailwind's internal cache
155
+ await buildCompiler ( ) ;
156
+ }
157
+
158
+ return changed ;
159
+ } ;
160
+
161
+ const processAddedClasses = ( currentClasses : Set < string > ) : boolean => {
162
+ // Identify new classes to add by checking if they are in cache
163
+ let changed = false ;
164
+ for ( const c of currentClasses ) {
165
+ if ( ! classesCache . has ( c ) ) {
166
+ classesCache . add ( c ) ;
167
+ changed = true ;
168
+ }
169
+ }
170
+ return changed ;
171
+ } ;
172
+
173
+ const compileTailwindCss = async ( ) => {
174
+ // Build Tailwind CSS if there are classes in the cache
175
+ if ( classesCache . size > 0 ) {
176
+ return compiler . build ( Array . from ( classesCache ) ) as string ;
177
+ }
178
+ return "" ;
95
179
} ;
96
180
97
181
const setTailwindStyleElement = ( ) => {
@@ -110,40 +194,60 @@ export default (editor: Editor, opts: TailwindPluginOptions = {}) => {
110
194
} ;
111
195
112
196
// Build and update the Tailwind CSS based on the current classes in the editor
113
- const runWorker = ( html : string , notify = false ) => {
114
- const payload : WorkerMessageData = {
115
- html,
116
- prefix : options . prefix ,
117
- customCss : options . customCss ,
118
- notify,
119
- } ;
120
- worker . postMessage ( payload ) ;
197
+ const buildTailwindCss = async ( html : string , notify = false ) => {
198
+ if ( ! compiler ) await initTailwindCompiler ( ) ;
199
+
200
+ try {
201
+ // Get all current tailwind related classes
202
+ const currentClasses = getClassesFromHtml ( html ) ;
203
+
204
+ // Identify classes that have been removed
205
+ const classesRemoved = await processRemovedClasses ( currentClasses ) ;
206
+
207
+ // Identify new classes to add
208
+ const classesAdded = processAddedClasses ( currentClasses ) ;
209
+
210
+ const shouldRebuildCss = classesRemoved || classesAdded ;
211
+
212
+ if ( ! shouldRebuildCss ) return ;
213
+
214
+ const tailwindcss = await compileTailwindCss ( ) ;
215
+ // Append the compiled tailwind css to the tailwind style element
216
+ if ( tailwindStyle !== undefined ) {
217
+ tailwindStyle . textContent = tailwindcss ;
218
+ }
219
+ if ( notify ) {
220
+ options . notificationCallback ( ) ;
221
+ }
222
+
223
+ // biome-ignore lint/suspicious/noExplicitAny: unknown
224
+ } catch ( error : any ) { }
121
225
} ;
122
226
123
227
// Build the Tailwind CSS on initial HTML load
124
228
editor . on ( "load" , async ( ) => {
125
229
// On load we need to set up the tailwind style element where we append the compiled tailwind css
126
230
setTailwindStyleElement ( ) ;
127
- runWorker ( editor . getHtml ( ) ) ;
231
+ buildTailwindCss ( editor . getHtml ( ) ) ;
128
232
} ) ;
129
233
130
234
// Fired by grapesjs-preset-webpage on import close
131
235
editor . on ( "command:stop:gjs-open-import-webpage" , ( ) =>
132
- runWorker ( editor . getHtml ( ) ) ,
236
+ buildTailwindCss ( editor . getHtml ( ) ) ,
133
237
) ;
134
238
135
239
// If autobuild option is true, listen to the editor's update events to trigger Tailwind CSS rebuilds.
136
240
if ( options . autobuild ) {
137
241
// Listen to the editor's update events to trigger Tailwind CSS rebuilds
138
242
editor . on ( "component:update:classes" , ( cmp : Component ) =>
139
- runWorker ( cmp . toHTML ( ) ) ,
243
+ buildTailwindCss ( cmp . toHTML ( ) ) ,
140
244
) ;
141
245
}
142
246
143
247
// Register a new command "build-tailwind" that can be triggered programmatically.
144
248
editor . Commands . add ( "build-tailwind" , {
145
249
run ( _ , sender ) {
146
- runWorker ( editor . getHtml ( ) , sender . id === "build-tailwind-button" ) ;
250
+ buildTailwindCss ( editor . getHtml ( ) , sender . id === "build-tailwind-button" ) ;
147
251
} ,
148
252
} ) ;
149
253
0 commit comments