1+ const { execSync } = require ( 'child_process' ) ;
2+ const { readdirSync, statSync, readFileSync, writeFileSync, existsSync, unlinkSync } = require ( 'fs' ) ;
3+ const { join, dirname } = require ( 'path' ) ;
4+ const { mkdirSync } = require ( 'fs' ) ;
5+
6+ /**
7+ * Configuration for asset processing.
8+ */
9+ const config = {
10+ // Directories to exclude from processing.
11+ excludeDirs : [ 'node_modules' , 'vendor' , 'build' , '.git' , 'includes/blocks' ] ,
12+
13+ // File patterns to exclude from discovery.
14+ // Note: We exclude -rtl.css but not .min files to allow re-minification.
15+ excludePatterns : [
16+ / - r t l \. c s s $ / ,
17+ / ^ b u i l d - a s s e t s \. j s $ / ,
18+ ] ,
19+
20+ // CSS files to combine - order matters!
21+ // Example: 'path/to/output.css': ['path/to/source1.css', 'path/to/source2.css']
22+ combineCss : { } ,
23+
24+ // JS files to combine - order matters!
25+ // Example: 'path/to/output.js': ['path/to/source1.js', 'path/to/source2.js']
26+ combineJs : { } ,
27+
28+ // If true, minify source files before combining (results in smaller bundles).
29+ minifyBeforeCombine : false ,
30+
31+ // If true, keep individual .min versions of combined output files.
32+ createMinifiedCombinedFiles : true ,
33+ } ;
34+
35+ // Track errors for final exit code.
36+ let errorCount = 0 ;
37+
38+ // Paths to local binaries.
39+ const binaries = {
40+ cleancss : './node_modules/.bin/cleancss' ,
41+ terser : './node_modules/.bin/terser' ,
42+ rtlcss : './node_modules/.bin/rtlcss' ,
43+ wpscripts : './node_modules/.bin/wp-scripts' ,
44+ } ;
45+
46+ /**
47+ * Ensure directory exists.
48+ *
49+ * @param {string } filePath File path to check.
50+ */
51+ function ensureDirectoryExists ( filePath ) {
52+ const dir = dirname ( filePath ) ;
53+ if ( ! existsSync ( dir ) ) {
54+ mkdirSync ( dir , { recursive : true } ) ;
55+ }
56+ }
57+
58+ /**
59+ * Check if a path should be excluded.
60+ *
61+ * @param {string } filePath File path to check.
62+ * @return {boolean } True if should be excluded.
63+ */
64+ function shouldExclude ( filePath ) {
65+ // Check directory exclusions.
66+ for ( const excludeDir of config . excludeDirs ) {
67+ if ( filePath . includes ( `/${ excludeDir } /` ) || filePath . startsWith ( `${ excludeDir } /` ) ) {
68+ return true ;
69+ }
70+ }
71+
72+ // Check pattern exclusions.
73+ for ( const pattern of config . excludePatterns ) {
74+ if ( pattern . test ( filePath ) ) {
75+ return true ;
76+ }
77+ }
78+
79+ return false ;
80+ }
81+
82+ /**
83+ * Recursively find all files with a specific extension.
84+ *
85+ * @param {string } dir Directory to search.
86+ * @param {string } extension File extension to match.
87+ * @param {Array } fileList Accumulated file list.
88+ * @return {Array } List of file paths.
89+ */
90+ function findFiles ( dir , extension , fileList = [ ] ) {
91+ if ( ! existsSync ( dir ) ) {
92+ return fileList ;
93+ }
94+
95+ const files = readdirSync ( dir ) ;
96+
97+ files . forEach ( ( file ) => {
98+ const filePath = join ( dir , file ) ;
99+
100+ if ( shouldExclude ( filePath ) ) {
101+ return ;
102+ }
103+
104+ const stat = statSync ( filePath ) ;
105+
106+ if ( stat . isDirectory ( ) ) {
107+ findFiles ( filePath , extension , fileList ) ;
108+ } else if ( file . endsWith ( extension ) && ! shouldExclude ( filePath ) ) {
109+ fileList . push ( filePath ) ;
110+ }
111+ } ) ;
112+
113+ return fileList ;
114+ }
115+
116+ /**
117+ * Minify a CSS file.
118+ *
119+ * @param {string } inputFile Input file path.
120+ * @param {string } outputFile Output file path.
121+ * @return {boolean } True if successful.
122+ */
123+ function minifyCss ( inputFile , outputFile ) {
124+ try {
125+ ensureDirectoryExists ( outputFile ) ;
126+ execSync ( `${ binaries . cleancss } -o ${ outputFile } ${ inputFile } ` , { stdio : 'pipe' } ) ;
127+ return true ;
128+ } catch ( error ) {
129+ console . error ( ` ✗ Error minifying ${ inputFile } :` , error . message ) ;
130+ errorCount ++ ;
131+ return false ;
132+ }
133+ }
134+
135+ /**
136+ * Minify a JS file.
137+ *
138+ * @param {string } inputFile Input file path.
139+ * @param {string } outputFile Output file path.
140+ * @return {boolean } True if successful.
141+ */
142+ function minifyJs ( inputFile , outputFile ) {
143+ try {
144+ ensureDirectoryExists ( outputFile ) ;
145+ execSync ( `${ binaries . terser } ${ inputFile } -o ${ outputFile } -c -m` , { stdio : 'pipe' } ) ;
146+ return true ;
147+ } catch ( error ) {
148+ console . error ( ` ✗ Error minifying ${ inputFile } :` , error . message ) ;
149+ errorCount ++ ;
150+ return false ;
151+ }
152+ }
153+
154+ /**
155+ * Generate RTL version of CSS file.
156+ *
157+ * @param {string } inputFile Input file path.
158+ * @param {string } outputFile Output file path.
159+ * @return {boolean } True if successful.
160+ */
161+ function generateRtl ( inputFile , outputFile ) {
162+ try {
163+ ensureDirectoryExists ( outputFile ) ;
164+ execSync ( `${ binaries . rtlcss } ${ inputFile } ${ outputFile } ` , { stdio : 'pipe' } ) ;
165+ return true ;
166+ } catch ( error ) {
167+ console . error ( ` ✗ Error creating RTL for ${ inputFile } :` , error . message ) ;
168+ errorCount ++ ;
169+ return false ;
170+ }
171+ }
172+
173+ /**
174+ * Format a CSS file with wp-scripts (uses WordPress Prettier config).
175+ *
176+ * @param {string } file File path to format.
177+ * @return {boolean } True if successful.
178+ */
179+ function formatCss ( file ) {
180+ try {
181+ execSync ( `${ binaries . wpscripts } format ${ file } ` , { stdio : 'pipe' } ) ;
182+ return true ;
183+ } catch ( error ) {
184+ console . error ( ` ✗ Error formatting ${ file } :` , error . message ) ;
185+ errorCount ++ ;
186+ return false ;
187+ }
188+ }
189+
190+ /**
191+ * Combine multiple files into one.
192+ *
193+ * @param {string } outputFile Output file path.
194+ * @param {Array } inputFiles Array of input file paths.
195+ * @param {boolean } isJs Whether files are JavaScript.
196+ */
197+ function combineFiles ( outputFile , inputFiles , isJs = false ) {
198+ console . log ( `\nCombining files into: ${ outputFile } ` ) ;
199+
200+ let combinedContent = '' ;
201+ const tempFiles = [ ] ;
202+
203+ inputFiles . forEach ( ( file ) => {
204+ try {
205+ let content ;
206+
207+ // Minify before combining if configured.
208+ if ( config . minifyBeforeCombine ) {
209+ const tempMinFile = file . replace ( / \. ( c s s | j s ) $ / , '.temp.min.$1' ) ;
210+ tempFiles . push ( tempMinFile ) ;
211+
212+ if ( isJs ) {
213+ if ( ! minifyJs ( file , tempMinFile ) ) {
214+ return ;
215+ }
216+ } else {
217+ if ( ! minifyCss ( file , tempMinFile ) ) {
218+ return ;
219+ }
220+ }
221+
222+ content = readFileSync ( tempMinFile , 'utf8' ) ;
223+ } else {
224+ content = readFileSync ( file , 'utf8' ) ;
225+ }
226+
227+ combinedContent += `\n/* Source: ${ file } */\n` ;
228+ combinedContent += content ;
229+ combinedContent += '\n' ;
230+ console . log ( ` ✓ Added: ${ file } ` ) ;
231+ } catch ( error ) {
232+ console . error ( ` ✗ Error reading ${ file } :` , error . message ) ;
233+ errorCount ++ ;
234+ }
235+ } ) ;
236+
237+ try {
238+ ensureDirectoryExists ( outputFile ) ;
239+ writeFileSync ( outputFile , combinedContent ) ;
240+ console . log ( ` ✓ Created: ${ outputFile } ` ) ;
241+
242+ // Clean up temp files (cross-platform).
243+ tempFiles . forEach ( ( tempFile ) => {
244+ try {
245+ if ( existsSync ( tempFile ) ) {
246+ unlinkSync ( tempFile ) ;
247+ }
248+ } catch ( error ) {
249+ // Silently ignore cleanup errors.
250+ }
251+ } ) ;
252+ } catch ( error ) {
253+ console . error ( ` ✗ Error writing ${ outputFile } :` , error . message ) ;
254+ errorCount ++ ;
255+ }
256+ }
257+
258+ // Step 1: Combine CSS files.
259+ console . log ( '=== Combining CSS Files ===' ) ;
260+ Object . entries ( config . combineCss ) . forEach ( ( [ output , inputs ] ) => {
261+ combineFiles ( output , inputs , false ) ;
262+ } ) ;
263+
264+ // Step 2: Combine JS files.
265+ console . log ( '\n=== Combining JS Files ===' ) ;
266+ Object . entries ( config . combineJs ) . forEach ( ( [ output , inputs ] ) => {
267+ combineFiles ( output , inputs , true ) ;
268+ } ) ;
269+
270+ // Step 3: Build exclusion lists.
271+ const combinedSourceFiles = [
272+ ...Object . values ( config . combineCss ) . flat ( ) ,
273+ ...Object . values ( config . combineJs ) . flat ( ) ,
274+ ] ;
275+
276+ const combinedOutputFiles = [
277+ ...Object . keys ( config . combineCss ) ,
278+ ...Object . keys ( config . combineJs ) ,
279+ ] ;
280+
281+ // Step 4: Find all CSS and JS files.
282+ // Note: This includes .min files to allow re-minification of existing minified files.
283+ const allCssFiles = findFiles ( '.' , '.css' ) ;
284+ const allJsFiles = findFiles ( '.' , '.js' ) ;
285+
286+ // Filter out source files used in combinations.
287+ // Also filter out .min and combined output files based on config.
288+ const cssFilesToMinify = allCssFiles . filter ( ( file ) => {
289+ // Skip source files that are part of combinations.
290+ if ( combinedSourceFiles . includes ( file ) ) {
291+ return false ;
292+ }
293+ // Skip already-minified files (we don't want to minify .min.css again).
294+ if ( file . endsWith ( '.min.css' ) ) {
295+ return false ;
296+ }
297+ // Skip combined output files if not creating minified versions.
298+ if ( ! config . createMinifiedCombinedFiles && combinedOutputFiles . includes ( file ) ) {
299+ return false ;
300+ }
301+ return true ;
302+ } ) ;
303+
304+ const jsFilesToMinify = allJsFiles . filter ( ( file ) => {
305+ // Skip source files that are part of combinations.
306+ if ( combinedSourceFiles . includes ( file ) ) {
307+ return false ;
308+ }
309+ // Skip already-minified files (we don't want to minify .min.js again).
310+ if ( file . endsWith ( '.min.js' ) ) {
311+ return false ;
312+ }
313+ // Skip combined output files if not creating minified versions.
314+ if ( ! config . createMinifiedCombinedFiles && combinedOutputFiles . includes ( file ) ) {
315+ return false ;
316+ }
317+ return true ;
318+ } ) ;
319+
320+ console . log ( `\n=== Processing Assets ===` ) ;
321+ console . log ( `Found ${ cssFilesToMinify . length } CSS files and ${ jsFilesToMinify . length } JS files to minify.\n` ) ;
322+
323+ // Step 5: Minify CSS files.
324+ console . log ( '=== Minifying CSS ===' ) ;
325+ cssFilesToMinify . forEach ( ( file ) => {
326+ const minFile = file . replace ( '.css' , '.min.css' ) ;
327+ console . log ( ` ${ file } → ${ minFile } ` ) ;
328+ minifyCss ( file , minFile ) ;
329+ } ) ;
330+
331+ // Step 6: Minify JS files.
332+ console . log ( '\n=== Minifying JS ===' ) ;
333+ jsFilesToMinify . forEach ( ( file ) => {
334+ const minFile = file . replace ( '.js' , '.min.js' ) ;
335+ console . log ( ` ${ file } → ${ minFile } ` ) ;
336+ minifyJs ( file , minFile ) ;
337+ } ) ;
338+
339+ // Step 7: Generate RTL versions for all CSS files (including minified and combined).
340+ console . log ( '\n=== Generating RTL CSS ===' ) ;
341+ const cssFilesForRtl = allCssFiles . filter ( ( file ) => ! file . includes ( '-rtl.' ) && ! file . endsWith ( '-rtl.css' ) ) ;
342+
343+ const rtlFilesGenerated = [ ] ;
344+ cssFilesForRtl . forEach ( ( file ) => {
345+ // Handle .min.css files correctly: name.min.css → name-rtl.min.css
346+ const rtlFile = file . endsWith ( '.min.css' )
347+ ? file . replace ( '.min.css' , '-rtl.min.css' )
348+ : file . replace ( '.css' , '-rtl.css' ) ;
349+ console . log ( ` ${ file } → ${ rtlFile } ` ) ;
350+ if ( generateRtl ( file , rtlFile ) ) {
351+ // Only format non-minified RTL files.
352+ if ( ! rtlFile . endsWith ( '.min.css' ) ) {
353+ rtlFilesGenerated . push ( rtlFile ) ;
354+ }
355+ }
356+ } ) ;
357+
358+ // Step 8: Format RTL CSS files for better readability.
359+ if ( rtlFilesGenerated . length > 0 ) {
360+ console . log ( '\n=== Formatting RTL CSS ===' ) ;
361+ rtlFilesGenerated . forEach ( ( file ) => {
362+ console . log ( ` Formatting: ${ file } ` ) ;
363+ formatCss ( file ) ;
364+ } ) ;
365+ }
366+
367+ // Final summary.
368+ console . log ( '\n==================================' ) ;
369+ if ( errorCount > 0 ) {
370+ console . error ( `✗ Completed with ${ errorCount } error(s).` ) ;
371+ process . exit ( 1 ) ;
372+ } else {
373+ console . log ( '✓ All assets processed successfully!' ) ;
374+ process . exit ( 0 ) ;
375+ }
0 commit comments