eva-css-purge 2.0.5 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/purge.js +200 -282
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eva-css-purge",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Intelligent CSS purging tool for EVA CSS projects",
5
5
  "type": "commonjs",
6
6
  "main": "src/purge.js",
package/src/purge.js CHANGED
@@ -1,11 +1,9 @@
1
1
 
2
2
  /**
3
- * CSS Purge Script for EvaCSS
4
- *
5
- * This script analyzes HTML and JavaScript files to extract used CSS classes and variables,
6
- * then creates a compressed CSS file with only the used styles.
7
- *
8
- * Usage: npm run purge
3
+ * CSS Purge Script for EvaCSS - FIXED VERSION
4
+ *
5
+ * FIX: Use config.content patterns instead of hardcoded patterns that ignore dist/
6
+ * This allows scanning of built files in docs/.vuepress/dist/
9
7
  */
10
8
 
11
9
  const fs = require('fs');
@@ -33,29 +31,26 @@ class CSSPurger {
33
31
  */
34
32
  async purge() {
35
33
  console.log('šŸš€ Starting CSS purge process...');
36
-
34
+
37
35
  try {
38
- // Step 1: Analyze HTML files
39
- await this.analyzeHTMLFiles();
40
-
41
- // Step 2: Analyze JavaScript files
42
- await this.analyzeJavaScriptFiles();
43
-
44
- // Step 3: Read compiled CSS
36
+ // Step 1: Analyze content files using config patterns - FIXED
37
+ await this.analyzeContentFiles();
38
+
39
+ // Step 2: Read compiled CSS
45
40
  await this.readCompiledCSS();
46
-
47
- // Step 4: Extract used styles
41
+
42
+ // Step 3: Extract used styles
48
43
  await this.extractUsedStyles();
49
-
50
- // Step 5: Compress and optimize
44
+
45
+ // Step 4: Compress and optimize
51
46
  await this.compressCSS();
52
-
53
- // Step 6: Write output
47
+
48
+ // Step 5: Write output
54
49
  await this.writeCompressedCSS();
55
-
50
+
56
51
  console.log('āœ… CSS purge completed successfully!');
57
52
  this.showStats();
58
-
53
+
59
54
  } catch (error) {
60
55
  console.error('āŒ Error during CSS purge:', error);
61
56
  process.exit(1);
@@ -63,177 +58,152 @@ class CSSPurger {
63
58
  }
64
59
 
65
60
  /**
66
- * Analyze HTML files to extract used classes and CSS variables
61
+ * Analyze content files using config patterns - FIXED
62
+ * This now uses config.content instead of hardcoded patterns
67
63
  */
68
- async analyzeHTMLFiles() {
69
- console.log('šŸ“„ Analyzing HTML files...');
70
-
71
- const htmlFiles = glob.sync('**/*.html', {
72
- ignore: ['node_modules/**', 'dist/**', 'build/**']
73
- });
74
-
75
- for (const file of htmlFiles) {
76
- const content = fs.readFileSync(file, 'utf8');
77
-
78
- // Extract classes from class attributes
79
- const classMatches = content.match(/class=["']([^"']+)["']/g);
80
- if (classMatches) {
81
- classMatches.forEach(match => {
82
- const classes = match.replace(/class=["']([^"']+)["']/, '$1').split(/\s+/);
83
- classes.forEach(cls => {
84
- if (cls.trim()) {
85
- this.usedClasses.add(cls.trim());
86
- }
87
- });
88
- });
89
- }
90
-
91
- // Extract IDs from id attributes
92
- const idMatches = content.match(/id=["']([^"']+)["']/g);
93
- if (idMatches) {
94
- idMatches.forEach(match => {
95
- const id = match.replace(/id=["']([^"']+)["']/, '$1').trim();
96
- if (id) {
97
- this.usedIds.add(id);
98
- }
99
- });
100
- }
101
-
102
- // Extract CSS variables from style attributes and content
103
- const varMatches = content.match(/var\(--[^)]+\)/g);
104
- if (varMatches) {
105
- varMatches.forEach(match => {
106
- const varName = match.replace(/var\((--[^)]+)\)/, '$1');
107
- this.usedVariables.add(varName);
108
- });
64
+ async analyzeContentFiles() {
65
+ console.log('šŸ“„ Analyzing content files...');
66
+
67
+ const allFiles = new Set();
68
+
69
+ // Use config.content patterns instead of hardcoded patterns - THIS IS THE FIX
70
+ for (const pattern of this.config.content) {
71
+ const files = glob.sync(pattern, {
72
+ ignore: ['node_modules/**'], // Only ignore node_modules, NOT dist/
73
+ nodir: true
74
+ });
75
+ files.forEach(file => allFiles.add(file));
76
+ }
77
+
78
+ console.log(`šŸ“ Found ${allFiles.size} files to analyze`);
79
+
80
+ for (const file of allFiles) {
81
+ try {
82
+ const content = fs.readFileSync(file, 'utf8');
83
+ this.extractFromContent(content, file);
84
+ } catch (error) {
85
+ console.warn(`āš ļø Could not read file: ${file}`);
109
86
  }
110
87
  }
111
-
112
- console.log(`šŸ“Š Found ${this.usedClasses.size} unique classes in HTML`);
113
- console.log(`šŸ“Š Found ${this.usedIds.size} unique IDs in HTML`);
114
- console.log(`šŸ“Š Found ${this.usedVariables.size} CSS variables in HTML`);
88
+
89
+ console.log(`šŸ“Š Found ${this.usedClasses.size} unique classes`);
90
+ console.log(`šŸ“Š Found ${this.usedIds.size} unique IDs`);
91
+ console.log(`šŸ“Š Found ${this.usedVariables.size} CSS variables`);
115
92
  }
116
93
 
117
94
  /**
118
- * Analyze JavaScript files to extract dynamically used classes and IDs
95
+ * Extract classes, IDs and variables from content
119
96
  */
120
- async analyzeJavaScriptFiles() {
121
- console.log('šŸ“„ Analyzing JavaScript files...');
122
-
123
- const jsFiles = glob.sync('**/*.js', {
124
- ignore: ['node_modules/**', 'dist/**', 'build/**', 'scripts/purge-css.js']
125
- });
126
-
127
- let jsClassCount = 0;
128
- let jsIdCount = 0;
129
- let jsVarCount = 0;
130
-
131
- for (const file of jsFiles) {
132
- const content = fs.readFileSync(file, 'utf8');
133
-
134
- // Extract classes from classList methods
135
- const classListMatches = content.match(/classList\.(add|remove|toggle|contains)\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g);
136
- if (classListMatches) {
137
- classListMatches.forEach(match => {
138
- const className = match.replace(/.*['"`]([^'"`]+)['"`].*/, '$1');
139
- if (className) {
140
- this.usedClasses.add(className);
141
- jsClassCount++;
97
+ extractFromContent(content, filename) {
98
+ // Extract classes from class attributes
99
+ const classMatches = content.match(/class=["']([^"']+)["']/g);
100
+ if (classMatches) {
101
+ classMatches.forEach(match => {
102
+ const classes = match.replace(/class=["']([^"']+)["']/, '$1').split(/\s+/);
103
+ classes.forEach(cls => {
104
+ if (cls.trim()) {
105
+ this.usedClasses.add(cls.trim());
142
106
  }
143
107
  });
144
- }
145
-
146
- // Extract classes from className assignments
147
- const classNameMatches = content.match(/\.className\s*=\s*['"`]([^'"`]+)['"`]/g);
148
- if (classNameMatches) {
149
- classNameMatches.forEach(match => {
150
- const classes = match.replace(/.*['"`]([^'"`]+)['"`].*/, '$1').split(/\s+/);
151
- classes.forEach(cls => {
152
- if (cls.trim()) {
153
- this.usedClasses.add(cls.trim());
154
- jsClassCount++;
155
- }
156
- });
157
- });
158
- }
159
-
160
- // Extract classes from querySelector and querySelectorAll
161
- const querySelectorMatches = content.match(/querySelector(?:All)?\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g);
162
- if (querySelectorMatches) {
163
- querySelectorMatches.forEach(match => {
164
- const selector = match.replace(/.*['"`]([^'"`]+)['"`].*/, '$1');
165
-
166
- // Extract class names (starts with .)
167
- const classMatches = selector.match(/\.([a-zA-Z][a-zA-Z0-9_-]*)/g);
168
- if (classMatches) {
169
- classMatches.forEach(cls => {
170
- const className = cls.substring(1); // Remove the dot
171
- this.usedClasses.add(className);
172
- jsClassCount++;
173
- });
174
- }
175
-
176
- // Extract IDs (starts with #)
177
- const idMatches = selector.match(/#([a-zA-Z][a-zA-Z0-9_-]*)/g);
178
- if (idMatches) {
179
- idMatches.forEach(id => {
180
- const idName = id.substring(1); // Remove the hash
181
- this.usedIds.add(idName);
182
- jsIdCount++;
183
- });
184
- }
185
- });
186
- }
187
-
188
- // Extract IDs from getElementById
189
- const getElementByIdMatches = content.match(/getElementById\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g);
190
- if (getElementByIdMatches) {
191
- getElementByIdMatches.forEach(match => {
192
- const id = match.replace(/.*['"`]([^'"`]+)['"`].*/, '$1');
193
- if (id) {
194
- this.usedIds.add(id);
195
- jsIdCount++;
108
+ });
109
+ }
110
+
111
+ // Extract IDs from id attributes
112
+ const idMatches = content.match(/id=["']([^"']+)["']/g);
113
+ if (idMatches) {
114
+ idMatches.forEach(match => {
115
+ const id = match.replace(/id=["']([^"']+)["']/, '$1').trim();
116
+ if (id) {
117
+ this.usedIds.add(id);
118
+ }
119
+ });
120
+ }
121
+
122
+ // Extract CSS variables
123
+ const varMatches = content.match(/var\(--[^)]+\)/g);
124
+ if (varMatches) {
125
+ varMatches.forEach(match => {
126
+ const varName = match.replace(/var\((--[^)]+)\)/, '$1');
127
+ this.usedVariables.add(varName);
128
+ });
129
+ }
130
+
131
+ // Extract classes from classList methods (JS)
132
+ const classListMatches = content.match(/classList\.(add|remove|toggle|contains)\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g);
133
+ if (classListMatches) {
134
+ classListMatches.forEach(match => {
135
+ const className = match.replace(/.*['"`]([^'"`]+)['"`].*/, '$1');
136
+ if (className) {
137
+ this.usedClasses.add(className);
138
+ }
139
+ });
140
+ }
141
+
142
+ // Extract classes from className assignments (JS)
143
+ const classNameMatches = content.match(/\.className\s*=\s*['"`]([^'"`]+)['"`]/g);
144
+ if (classNameMatches) {
145
+ classNameMatches.forEach(match => {
146
+ const classes = match.replace(/.*['"`]([^'"`]+)['"`].*/, '$1').split(/\s+/);
147
+ classes.forEach(cls => {
148
+ if (cls.trim()) {
149
+ this.usedClasses.add(cls.trim());
196
150
  }
197
151
  });
198
- }
199
-
200
- // Extract CSS variables from JavaScript
201
- const jsVarMatches = content.match(/['"`]--[a-zA-Z][a-zA-Z0-9_-]*['"`]/g);
202
- if (jsVarMatches) {
203
- jsVarMatches.forEach(match => {
204
- const varName = match.replace(/['"`]/g, '');
205
- this.usedVariables.add(varName);
206
- jsVarCount++;
207
- });
208
- }
209
-
210
- // Extract CSS variables from setProperty calls
211
- const setPropMatches = content.match(/setProperty\s*\(\s*['"`](--[^'"`]+)['"`]/g);
212
- if (setPropMatches) {
213
- setPropMatches.forEach(match => {
214
- const varName = match.replace(/.*['"`](--[^'"`]+)['"`].*/, '$1');
215
- this.usedVariables.add(varName);
216
- jsVarCount++;
217
- });
218
- }
219
-
220
- // Extract CSS variables from getPropertyValue calls
221
- const getPropMatches = content.match(/getPropertyValue\s*\(\s*['"`](--[^'"`]+)['"`]/g);
222
- if (getPropMatches) {
223
- getPropMatches.forEach(match => {
224
- const varName = match.replace(/.*['"`](--[^'"`]+)['"`].*/, '$1');
225
- this.usedVariables.add(varName);
226
- jsVarCount++;
227
- });
228
- }
152
+ });
153
+ }
154
+
155
+ // Extract from querySelector/querySelectorAll
156
+ const querySelectorMatches = content.match(/querySelector(?:All)?\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g);
157
+ if (querySelectorMatches) {
158
+ querySelectorMatches.forEach(match => {
159
+ const selector = match.replace(/.*['"`]([^'"`]+)['"`].*/, '$1');
160
+
161
+ // Extract class names
162
+ const selectorClassMatches = selector.match(/\.([a-zA-Z_-][a-zA-Z0-9_-]*)/g);
163
+ if (selectorClassMatches) {
164
+ selectorClassMatches.forEach(cls => {
165
+ this.usedClasses.add(cls.substring(1));
166
+ });
167
+ }
168
+
169
+ // Extract IDs
170
+ const selectorIdMatches = selector.match(/#([a-zA-Z_-][a-zA-Z0-9_-]*)/g);
171
+ if (selectorIdMatches) {
172
+ selectorIdMatches.forEach(id => {
173
+ this.usedIds.add(id.substring(1));
174
+ });
175
+ }
176
+ });
177
+ }
178
+
179
+ // Extract from getElementById
180
+ const getElementByIdMatches = content.match(/getElementById\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g);
181
+ if (getElementByIdMatches) {
182
+ getElementByIdMatches.forEach(match => {
183
+ const id = match.replace(/.*['"`]([^'"`]+)['"`].*/, '$1');
184
+ if (id) {
185
+ this.usedIds.add(id);
186
+ }
187
+ });
188
+ }
189
+
190
+ // Extract CSS variables from setProperty calls
191
+ const setPropMatches = content.match(/setProperty\s*\(\s*['"`](--[^'"`]+)['"`]/g);
192
+ if (setPropMatches) {
193
+ setPropMatches.forEach(match => {
194
+ const varName = match.replace(/.*['"`](--[^'"`]+)['"`].*/, '$1');
195
+ this.usedVariables.add(varName);
196
+ });
197
+ }
198
+
199
+ // Extract CSS variables from getPropertyValue calls
200
+ const getPropMatches = content.match(/getPropertyValue\s*\(\s*['"`](--[^'"`]+)['"`]/g);
201
+ if (getPropMatches) {
202
+ getPropMatches.forEach(match => {
203
+ const varName = match.replace(/.*['"`](--[^'"`]+)['"`].*/, '$1');
204
+ this.usedVariables.add(varName);
205
+ });
229
206
  }
230
-
231
- console.log(`šŸ“Š Found ${jsClassCount} class references in JavaScript`);
232
- console.log(`šŸ“Š Found ${jsIdCount} ID references in JavaScript`);
233
- console.log(`šŸ“Š Found ${jsVarCount} CSS variable references in JavaScript`);
234
- console.log(`šŸ“Š Total unique classes: ${this.usedClasses.size}`);
235
- console.log(`šŸ“Š Total unique IDs: ${this.usedIds.size}`);
236
- console.log(`šŸ“Š Total unique CSS variables: ${this.usedVariables.size}`);
237
207
  }
238
208
 
239
209
  /**
@@ -257,105 +227,88 @@ class CSSPurger {
257
227
  */
258
228
  async extractUsedStyles() {
259
229
  console.log('šŸ” Extracting used styles...');
260
-
230
+
261
231
  const lines = this.cssContent.split('\n');
262
232
  const usedLines = [];
263
233
  let currentRule = '';
264
234
  let currentRuleContent = '';
265
235
  let inRule = false;
266
- let inRootRule = false; // Special flag for :root blocks
267
- let inMediaQuery = false; // Special flag for @media blocks
236
+ let inRootRule = false;
237
+ let inMediaQuery = false;
268
238
  let braceCount = 0;
269
239
  let ruleLines = [];
270
240
  let processedRules = 0;
271
241
  let mediaQueryCount = 0;
272
-
242
+
273
243
  for (let i = 0; i < lines.length; i++) {
274
244
  const line = lines[i];
275
245
  const trimmedLine = line.trim();
276
-
277
- // Progress indicator
246
+
278
247
  if (i % 1000 === 0) {
279
248
  console.log(`šŸ“Š Processing line ${i}/${lines.length} (${Math.round(i/lines.length*100)}%)`);
280
249
  }
281
-
282
- // Skip empty lines and comments at the root level
250
+
283
251
  if (!trimmedLine || (trimmedLine.startsWith('/*') && !inRule)) {
284
252
  continue;
285
253
  }
286
-
287
- // If we're not in a rule but this line looks like a selector (no { yet), accumulate it for multi-line selectors
288
- if (!inRule && !trimmedLine.includes('{') && !trimmedLine.includes('}') &&
254
+
255
+ if (!inRule && !trimmedLine.includes('{') && !trimmedLine.includes('}') &&
289
256
  (trimmedLine.includes(',') || /^[a-zA-Z*#.\[]/.test(trimmedLine) || trimmedLine.includes('::') || trimmedLine.includes(':'))) {
290
- // This might be part of a multi-line selector
291
257
  if (!currentRule) {
292
258
  currentRule = trimmedLine;
293
259
  ruleLines = [line];
294
260
  } else {
295
- // Append to existing selector
296
261
  currentRule += ',' + trimmedLine;
297
262
  ruleLines.push(line);
298
263
  }
299
264
  continue;
300
265
  }
301
-
302
- // Handle opening braces
266
+
303
267
  if (trimmedLine.includes('{')) {
304
268
  braceCount++;
305
269
  if (!inRule && braceCount === 1) {
306
- // Complete the selector if it wasn't already accumulated
307
270
  if (!currentRule) {
308
271
  currentRule = trimmedLine.replace(/\s*\{.*/, '');
309
272
  ruleLines = [line];
310
273
  } else {
311
- // Use accumulated selector and add the line with the opening brace
312
274
  currentRule += trimmedLine.replace(/\s*\{.*/, '');
313
275
  ruleLines.push(line);
314
276
  }
315
277
  currentRuleContent = '';
316
278
  inRule = true;
317
-
318
- // Check if this is a media query (always keep these completely)
279
+
319
280
  if (currentRule.includes('@media')) {
320
281
  inMediaQuery = true;
321
282
  mediaQueryCount++;
322
283
  console.log(`šŸ“± Found media query: ${currentRule.trim()} - keeping completely`);
323
- }
324
- // Check if this is a :root or .all-grads rule (always keep these completely)
325
- else if (currentRule.includes(':root') || currentRule.includes('.all-grads')) {
284
+ } else if (currentRule.includes(':root') || currentRule.includes('.all-grads')) {
326
285
  inRootRule = true;
327
286
  console.log(`šŸ“‹ Found ${currentRule.trim()} block - keeping all variables`);
328
287
  }
329
288
  } else if (inRule) {
330
289
  ruleLines.push(line);
331
290
  }
332
- }
333
- // Handle closing braces
334
- else if (trimmedLine.includes('}')) {
291
+ } else if (trimmedLine.includes('}')) {
335
292
  braceCount--;
336
293
  if (inRule) {
337
294
  ruleLines.push(line);
338
-
339
- // Check if rule should be kept when we close the main rule
295
+
340
296
  if (braceCount === 0) {
341
297
  processedRules++;
342
-
343
- // Always keep media queries, :root and .all-grads blocks completely
344
- const shouldKeep = inMediaQuery ||
345
- inRootRule ||
346
- this.isUsedSelector(currentRule) ||
298
+
299
+ const shouldKeep = inMediaQuery ||
300
+ inRootRule ||
301
+ this.isUsedSelector(currentRule) ||
347
302
  this.hasCurrentVariables(currentRuleContent);
348
-
303
+
349
304
  if (shouldKeep) {
350
305
  usedLines.push(...ruleLines);
351
306
  if (inRootRule) {
352
- // Count variables in this :root block
353
307
  const varCount = ruleLines.join('').match(/--[a-zA-Z][a-zA-Z0-9_-]*:/g)?.length || 0;
354
308
  console.log(`šŸ“‹ Kept ${varCount} CSS variables from ${currentRule.trim()}`);
355
309
  }
356
310
  }
357
-
358
- // Reset for next rule
311
+
359
312
  inRule = false;
360
313
  inRootRule = false;
361
314
  inMediaQuery = false;
@@ -364,14 +317,12 @@ class CSSPurger {
364
317
  ruleLines = [];
365
318
  }
366
319
  }
367
- }
368
- // Handle properties within rules
369
- else if (inRule) {
320
+ } else if (inRule) {
370
321
  ruleLines.push(line);
371
322
  currentRuleContent += ' ' + trimmedLine;
372
323
  }
373
324
  }
374
-
325
+
375
326
  console.log(`šŸ“Š Processed ${processedRules} CSS rules`);
376
327
  console.log(`šŸ“± Kept ${mediaQueryCount} media queries`);
377
328
  this.compressedCSS = usedLines.join('\n');
@@ -390,19 +341,17 @@ class CSSPurger {
390
341
  */
391
342
  isElementOrResetSelector(selector) {
392
343
  const cleanSelector = selector.replace(/\s*\{.*/, '').trim();
393
-
394
- // Always keep universal selectors and reset selectors (complete groups)
395
- if (cleanSelector.includes('*') ||
396
- cleanSelector.includes('::before') ||
344
+
345
+ if (cleanSelector.includes('*') ||
346
+ cleanSelector.includes('::before') ||
397
347
  cleanSelector.includes('::after') ||
398
348
  cleanSelector.includes(':target') ||
399
349
  /^body[,\s]|^html[,\s]/.test(cleanSelector)) {
400
350
  return true;
401
351
  }
402
-
403
- // Handle multiple selectors separated by commas - if ANY selector in the group is an element/reset, keep the whole group
352
+
404
353
  const selectors = cleanSelector.split(',');
405
-
354
+
406
355
  const htmlElements = [
407
356
  'html', 'body', 'head', 'title', 'meta', 'link', 'script', 'style',
408
357
  'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'hr',
@@ -415,34 +364,29 @@ class CSSPurger {
415
364
  'em', 'strong', 'b', 'i', 'u', 's', 'small', 'mark', 'del', 'ins', 'sub', 'sup',
416
365
  'video', 'audio', 'canvas', 'svg', 'iframe', 'embed', 'object', 'param'
417
366
  ];
418
-
367
+
419
368
  for (const sel of selectors) {
420
369
  const trimmedSel = sel.trim();
421
-
422
- // Check if it's a pure element selector
370
+
423
371
  const baseElement = trimmedSel.replace(/::[^,\s]+|:[^,\s(]+(\([^)]*\))?/g, '').trim();
424
372
  if (htmlElements.includes(baseElement)) {
425
373
  return true;
426
374
  }
427
-
428
- // Check for element selectors with pseudo-classes/elements (like a:hover, input:focus)
375
+
429
376
  if (htmlElements.some(element => trimmedSel.startsWith(element + ':') || trimmedSel.startsWith(element + '::') || trimmedSel === element)) {
430
377
  return true;
431
378
  }
432
-
433
- // Check for attribute selectors on elements (like input[type="text"])
379
+
434
380
  if (htmlElements.some(element => trimmedSel.startsWith(element + '[') && trimmedSel.includes(']'))) {
435
381
  return true;
436
382
  }
437
-
438
- // Check for selectors starting with element but containing other selectors
439
- // e.g., "ul[role=list]", "a:not([class])", etc.
383
+
440
384
  const match = trimmedSel.match(/^([a-zA-Z][a-zA-Z0-9]*)/);
441
385
  if (match && htmlElements.includes(match[1])) {
442
386
  return true;
443
387
  }
444
388
  }
445
-
389
+
446
390
  return false;
447
391
  }
448
392
 
@@ -450,75 +394,65 @@ class CSSPurger {
450
394
  * Check if a CSS selector is used in the HTML or JavaScript
451
395
  */
452
396
  isUsedSelector(selector) {
453
- // Clean the selector
454
397
  const cleanSelector = selector.replace(/\s*\{.*/, '').trim();
455
-
456
- // Always keep element and reset selectors
398
+
457
399
  if (this.isElementOrResetSelector(selector)) {
458
400
  return true;
459
401
  }
460
-
461
- // Handle multiple selectors separated by commas
402
+
462
403
  const selectors = cleanSelector.split(',');
463
-
404
+
464
405
  for (const sel of selectors) {
465
406
  const trimmedSel = sel.trim();
466
-
467
- // Skip pseudo-elements and pseudo-classes for now
407
+
468
408
  if (trimmedSel.includes('::') || trimmedSel.includes(':hover') || trimmedSel.includes(':before') || trimmedSel.includes(':after')) {
469
- // Check the base selector
470
409
  const baseSelector = trimmedSel.replace(/::[^,\s]+|:[^,\s]+/g, '').trim();
471
410
  if (this.isUsedSelector(baseSelector)) {
472
411
  return true;
473
412
  }
474
413
  continue;
475
414
  }
476
-
477
- // Handle ID selectors
415
+
478
416
  if (trimmedSel.startsWith('#')) {
479
417
  const idName = trimmedSel.substring(1).replace(/[^\w-_]/g, '');
480
418
  if (this.usedIds.has(idName)) {
481
419
  return true;
482
420
  }
483
421
  }
484
-
485
- // Handle class selectors
422
+
486
423
  if (trimmedSel.startsWith('.')) {
487
424
  const className = trimmedSel.substring(1).replace(/[^\w-_]/g, '');
488
425
  if (this.usedClasses.has(className)) {
489
426
  return true;
490
427
  }
491
428
  }
492
-
493
- // Handle compound selectors (e.g., .class1.class2, #id.class)
429
+
494
430
  const classes = trimmedSel.match(/\.[a-zA-Z][a-zA-Z0-9_-]*/g);
495
431
  const ids = trimmedSel.match(/#[a-zA-Z][a-zA-Z0-9_-]*/g);
496
-
432
+
497
433
  if (classes || ids) {
498
434
  let allSelectorsUsed = true;
499
-
500
- // Check all classes in compound selector
435
+
501
436
  if (classes) {
502
437
  allSelectorsUsed = allSelectorsUsed && classes.every(cls => {
503
438
  const className = cls.substring(1);
504
439
  return this.usedClasses.has(className);
505
440
  });
506
441
  }
507
-
508
- // Check all IDs in compound selector
442
+
509
443
  if (ids) {
510
444
  allSelectorsUsed = allSelectorsUsed && ids.every(id => {
511
445
  const idName = id.substring(1);
512
446
  return this.usedIds.has(idName);
513
447
  });
514
448
  }
515
-
449
+
516
450
  if (allSelectorsUsed && (classes?.length > 0 || ids?.length > 0)) {
517
451
  return true;
518
452
  }
519
453
  }
520
454
  }
521
-
455
+
522
456
  return false;
523
457
  }
524
458
 
@@ -527,34 +461,20 @@ class CSSPurger {
527
461
  */
528
462
  async compressCSS() {
529
463
  console.log('šŸ—œļø Compressing CSS...');
530
-
464
+
531
465
  let compressed = this.compressedCSS;
532
-
533
- // Remove comments
466
+
534
467
  compressed = compressed.replace(/\/\*[\s\S]*?\*\//g, '');
535
-
536
- // Remove unnecessary whitespace but preserve selector grouping structure
537
468
  compressed = compressed.replace(/\s+/g, ' ');
538
-
539
- // Remove whitespace around braces and semicolons - but preserve commas in selectors
540
469
  compressed = compressed.replace(/\s*{\s*/g, '{');
541
470
  compressed = compressed.replace(/\s*}\s*/g, '}');
542
471
  compressed = compressed.replace(/\s*;\s*/g, ';');
543
472
  compressed = compressed.replace(/\s*:\s*/g, ':');
544
-
545
- // CAREFULLY handle comma spacing - preserve commas in selectors but remove extra spaces
546
- // Only remove spaces around commas that are NOT inside function calls or @media queries
547
473
  compressed = compressed.replace(/(\w|[)\]])\s*,\s*(\w|[.#*:])/g, '$1,$2');
548
-
549
- // Remove trailing semicolons before }
550
474
  compressed = compressed.replace(/;}/g, '}');
551
-
552
- // Remove empty rules
553
475
  compressed = compressed.replace(/[^}]+{\s*}/g, '');
554
-
555
- // Final cleanup
556
476
  compressed = compressed.trim();
557
-
477
+
558
478
  this.compressedCSS = compressed;
559
479
  console.log(`šŸ“Š Compressed CSS size: ${(compressed.length / 1024).toFixed(2)} KB`);
560
480
  }
@@ -567,13 +487,11 @@ class CSSPurger {
567
487
 
568
488
  const outputPath = path.resolve(process.cwd(), this.config.output);
569
489
 
570
- // Ensure output directory exists
571
490
  const outputDir = path.dirname(outputPath);
572
491
  if (!fs.existsSync(outputDir)) {
573
492
  fs.mkdirSync(outputDir, { recursive: true });
574
493
  }
575
494
 
576
- // Add a header comment
577
495
  const header = `/* EVA CSS Purged - Generated on ${new Date().toISOString()} */\n`;
578
496
  const finalCSS = header + this.compressedCSS;
579
497
 
@@ -588,7 +506,7 @@ class CSSPurger {
588
506
  const originalSize = this.cssContent.length;
589
507
  const compressedSize = this.compressedCSS.length;
590
508
  const savings = ((originalSize - compressedSize) / originalSize * 100).toFixed(2);
591
-
509
+
592
510
  console.log('\nšŸ“Š Compression Statistics:');
593
511
  console.log(` Original size: ${(originalSize / 1024).toFixed(2)} KB`);
594
512
  console.log(` Compressed size: ${(compressedSize / 1024).toFixed(2)} KB`);
@@ -605,4 +523,4 @@ if (require.main === module) {
605
523
  purger.purge();
606
524
  }
607
525
 
608
- module.exports = CSSPurger;
526
+ module.exports = CSSPurger;