eva-css-purge 1.0.1
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.
- package/README.md +197 -0
- package/cli.js +146 -0
- package/package.json +41 -0
- package/src/purge.js +608 -0
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# @eva/purge
|
|
2
|
+
|
|
3
|
+
> Intelligent CSS purging tool for EVA CSS projects
|
|
4
|
+
|
|
5
|
+
Removes unused CSS classes, IDs, and optimizes your stylesheets while keeping what matters.
|
|
6
|
+
|
|
7
|
+
## šÆ Features
|
|
8
|
+
|
|
9
|
+
- **Smart Analysis**: Scans HTML, JS, Vue, JSX, TSX files
|
|
10
|
+
- **CSS Variables**: Preserves all CSS variables (critical for EVA CSS)
|
|
11
|
+
- **Element Selectors**: Keeps all HTML element styles
|
|
12
|
+
- **Dynamic Classes**: Detects classes used in JavaScript
|
|
13
|
+
- **Compression**: Minifies output CSS
|
|
14
|
+
- **Safelist**: Protect specific classes from removal
|
|
15
|
+
- **CLI & Programmatic**: Use via command line or in your build process
|
|
16
|
+
|
|
17
|
+
## š¦ Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @eva/purge
|
|
21
|
+
# or
|
|
22
|
+
pnpm add @eva/purge
|
|
23
|
+
# or
|
|
24
|
+
yarn add @eva/purge
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## š Usage
|
|
28
|
+
|
|
29
|
+
### CLI Usage
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Basic usage
|
|
33
|
+
eva-purge --css dist/style.css --content "src/**/*.html"
|
|
34
|
+
|
|
35
|
+
# Multiple content patterns
|
|
36
|
+
eva-purge --css dist/style.css --content "src/**/*.{html,js,vue}"
|
|
37
|
+
|
|
38
|
+
# Custom output
|
|
39
|
+
eva-purge --css dist/style.css --content "src/**/*" --output dist/style-purged.css
|
|
40
|
+
|
|
41
|
+
# With safelist (classes to always keep)
|
|
42
|
+
eva-purge --css dist/style.css --safelist "theme-,current-,all-grads"
|
|
43
|
+
|
|
44
|
+
# Using config file
|
|
45
|
+
eva-purge --config eva.config.js
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Configuration File
|
|
49
|
+
|
|
50
|
+
Create `eva.config.js`:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
module.exports = {
|
|
54
|
+
purge: {
|
|
55
|
+
// Content files to scan
|
|
56
|
+
content: [
|
|
57
|
+
'src/**/*.html',
|
|
58
|
+
'src/**/*.js',
|
|
59
|
+
'src/**/*.vue',
|
|
60
|
+
'src/**/*.jsx',
|
|
61
|
+
'src/**/*.tsx'
|
|
62
|
+
],
|
|
63
|
+
|
|
64
|
+
// CSS file to purge
|
|
65
|
+
css: 'dist/style.css',
|
|
66
|
+
|
|
67
|
+
// Output file
|
|
68
|
+
output: 'dist/style-purged.css',
|
|
69
|
+
|
|
70
|
+
// Classes to keep (optional)
|
|
71
|
+
safelist: {
|
|
72
|
+
standard: ['current-theme', 'all-grads', 'toggle-theme'],
|
|
73
|
+
deep: [/^theme-/], // Regex patterns
|
|
74
|
+
greedy: [/^brand-/, /^accent-/]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Then run:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
eva-purge --config eva.config.js
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Programmatic Usage
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const CSSPurger = require('@eva/purge');
|
|
90
|
+
|
|
91
|
+
const config = {
|
|
92
|
+
content: ['src/**/*.{html,js}'],
|
|
93
|
+
css: 'dist/style.css',
|
|
94
|
+
output: 'dist/style-purged.css'
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const purger = new CSSPurger(config);
|
|
98
|
+
await purger.purge();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Integration with Build Tools
|
|
102
|
+
|
|
103
|
+
**With npm scripts:**
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"scripts": {
|
|
108
|
+
"build:css": "sass src/styles.scss dist/style.css",
|
|
109
|
+
"purge:css": "eva-purge --css dist/style.css --content 'src/**/*.html'",
|
|
110
|
+
"build": "npm run build:css && npm run purge:css"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**With Vite:**
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
// vite.config.js
|
|
119
|
+
import { defineConfig } from 'vite';
|
|
120
|
+
import { exec } from 'child_process';
|
|
121
|
+
|
|
122
|
+
export default defineConfig({
|
|
123
|
+
plugins: [
|
|
124
|
+
{
|
|
125
|
+
name: 'eva-purge',
|
|
126
|
+
closeBundle() {
|
|
127
|
+
exec('eva-purge --css dist/assets/style.css --content "dist/**/*.html"');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## š What Gets Kept
|
|
135
|
+
|
|
136
|
+
@eva/purge intelligently keeps:
|
|
137
|
+
|
|
138
|
+
ā
**CSS Variables** - All `:root` variables (essential for EVA CSS)
|
|
139
|
+
ā
**HTML Elements** - `body`, `h1`, `p`, `button`, etc.
|
|
140
|
+
ā
**Used Classes** - Classes found in HTML/JS files
|
|
141
|
+
ā
**Used IDs** - IDs found in HTML/JS files
|
|
142
|
+
ā
**Media Queries** - All responsive breakpoints
|
|
143
|
+
ā
**Theme Classes** - `.current-theme`, `.all-grads`, etc.
|
|
144
|
+
ā
**Dynamic Classes** - Classes from `classList.add()`, `querySelector()`
|
|
145
|
+
|
|
146
|
+
## ā What Gets Removed
|
|
147
|
+
|
|
148
|
+
ā **Unused Classes** - Classes not found in any content files
|
|
149
|
+
ā **Unused IDs** - IDs not referenced anywhere
|
|
150
|
+
ā **Comments** - CSS comments (optional)
|
|
151
|
+
ā **Whitespace** - Extra spaces and newlines
|
|
152
|
+
|
|
153
|
+
## šØ Perfect for EVA CSS
|
|
154
|
+
|
|
155
|
+
@eva/purge is specifically designed for EVA CSS projects:
|
|
156
|
+
|
|
157
|
+
- Preserves all CSS variable definitions in `:root`
|
|
158
|
+
- Keeps utility classes like `w-64`, `p-16`, `fs-32`
|
|
159
|
+
- Maintains color classes like `_bg-brand`, `_c-accent`
|
|
160
|
+
- Protects theme classes like `.theme-*`
|
|
161
|
+
- Preserves gradient system classes `.all-grads`
|
|
162
|
+
|
|
163
|
+
## š Example Results
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
Original CSS: 120 KB
|
|
167
|
+
Purged CSS: 45 KB
|
|
168
|
+
Space saved: 62.5%
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Typical savings: **40-70%** depending on your project.
|
|
172
|
+
|
|
173
|
+
## š§Ŗ Testing
|
|
174
|
+
|
|
175
|
+
Run the included test suite:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
cd packages/eva-purge
|
|
179
|
+
pnpm test
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## š License
|
|
183
|
+
|
|
184
|
+
MIT © [Michaël Tati](https://ulysse-2029.com/)
|
|
185
|
+
|
|
186
|
+
## šØāš» Author
|
|
187
|
+
|
|
188
|
+
**Michaƫl Tati**
|
|
189
|
+
- Portfolio: [ulysse-2029.com](https://ulysse-2029.com/)
|
|
190
|
+
- LinkedIn: [linkedin.com/in/mtati](https://www.linkedin.com/in/mtati/)
|
|
191
|
+
- Website: [eva-css.xyz](https://eva-css.xyz/)
|
|
192
|
+
|
|
193
|
+
## š Related Packages
|
|
194
|
+
|
|
195
|
+
- [@eva/css](https://www.npmjs.com/package/@eva/css) - Fluid design framework
|
|
196
|
+
- [@eva/colors](https://www.npmjs.com/package/@eva/colors) - OKLCH color utilities
|
|
197
|
+
- [@eva/mcp-server](https://www.npmjs.com/package/@eva/mcp-server) - Figma to HTML MCP server
|
package/cli.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EVA Purge CLI
|
|
5
|
+
* Intelligent CSS purging for EVA CSS projects
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const CSSPurger = require('./src/purge.js');
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const command = args[0];
|
|
14
|
+
|
|
15
|
+
function printHelp() {
|
|
16
|
+
console.log(`
|
|
17
|
+
EVA Purge - Intelligent CSS Optimization
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
eva-purge [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--content <pattern> Content files to scan (default: **/*.{html,js,vue,jsx,tsx})
|
|
24
|
+
--css <file> CSS file to purge (required)
|
|
25
|
+
--output <file> Output file path (default: [css]-purged.css)
|
|
26
|
+
--config <file> Config file path (eva.config.js)
|
|
27
|
+
--safelist <classes> Comma-separated list of classes to keep
|
|
28
|
+
--help Show this help
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
# Basic usage
|
|
32
|
+
eva-purge --css dist/style.css --content "src/**/*.html"
|
|
33
|
+
|
|
34
|
+
# With safelist
|
|
35
|
+
eva-purge --css dist/style.css --safelist "theme-,current-,all-grads"
|
|
36
|
+
|
|
37
|
+
# Using config file
|
|
38
|
+
eva-purge --config eva.config.js
|
|
39
|
+
|
|
40
|
+
Config file example (eva.config.js):
|
|
41
|
+
module.exports = {
|
|
42
|
+
purge: {
|
|
43
|
+
content: ['src/**/*.html', 'src/**/*.js'],
|
|
44
|
+
css: 'dist/style.css',
|
|
45
|
+
output: 'dist/style-purged.css',
|
|
46
|
+
safelist: {
|
|
47
|
+
standard: ['current-theme', 'all-grads'],
|
|
48
|
+
deep: [/^theme-/],
|
|
49
|
+
greedy: [/^brand-/, /^accent-/]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse CLI arguments
|
|
57
|
+
function parseArgs() {
|
|
58
|
+
const config = {
|
|
59
|
+
content: [],
|
|
60
|
+
css: null,
|
|
61
|
+
output: null,
|
|
62
|
+
safelist: []
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < args.length; i++) {
|
|
66
|
+
switch (args[i]) {
|
|
67
|
+
case '--content':
|
|
68
|
+
config.content.push(args[++i]);
|
|
69
|
+
break;
|
|
70
|
+
case '--css':
|
|
71
|
+
config.css = args[++i];
|
|
72
|
+
break;
|
|
73
|
+
case '--output':
|
|
74
|
+
config.output = args[++i];
|
|
75
|
+
break;
|
|
76
|
+
case '--safelist':
|
|
77
|
+
config.safelist = args[++i].split(',').map(s => s.trim());
|
|
78
|
+
break;
|
|
79
|
+
case '--config':
|
|
80
|
+
const configPath = path.resolve(process.cwd(), args[++i]);
|
|
81
|
+
if (fs.existsSync(configPath)) {
|
|
82
|
+
const fileConfig = require(configPath);
|
|
83
|
+
return fileConfig.purge || fileConfig;
|
|
84
|
+
} else {
|
|
85
|
+
console.error(`ā Config file not found: ${configPath}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case '--help':
|
|
90
|
+
case '-h':
|
|
91
|
+
printHelp();
|
|
92
|
+
process.exit(0);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Set defaults
|
|
98
|
+
if (config.content.length === 0) {
|
|
99
|
+
config.content = ['**/*.{html,js,vue,jsx,tsx}'];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!config.output && config.css) {
|
|
103
|
+
const parsed = path.parse(config.css);
|
|
104
|
+
config.output = path.join(parsed.dir, `${parsed.name}-purged${parsed.ext}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return config;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function main() {
|
|
111
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
112
|
+
printHelp();
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const config = parseArgs();
|
|
117
|
+
|
|
118
|
+
if (!config.css) {
|
|
119
|
+
console.error('ā Error: --css parameter is required');
|
|
120
|
+
console.log('Run eva-purge --help for usage information');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!fs.existsSync(config.css)) {
|
|
125
|
+
console.error(`ā Error: CSS file not found: ${config.css}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log('š EVA Purge - Starting CSS optimization...\n');
|
|
130
|
+
console.log('š Configuration:');
|
|
131
|
+
console.log(` CSS Input: ${config.css}`);
|
|
132
|
+
console.log(` CSS Output: ${config.output}`);
|
|
133
|
+
console.log(` Content: ${config.content.join(', ')}`);
|
|
134
|
+
if (config.safelist && config.safelist.length > 0) {
|
|
135
|
+
console.log(` Safelist: ${config.safelist.join(', ')}`);
|
|
136
|
+
}
|
|
137
|
+
console.log('');
|
|
138
|
+
|
|
139
|
+
const purger = new CSSPurger(config);
|
|
140
|
+
await purger.purge();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
main().catch(error => {
|
|
144
|
+
console.error('ā Fatal error:', error);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eva-css-purge",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Intelligent CSS purging tool for EVA CSS projects",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "src/purge.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"eva-purge": "./cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"cli.js",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"css",
|
|
17
|
+
"purge",
|
|
18
|
+
"optimization",
|
|
19
|
+
"eva",
|
|
20
|
+
"css-optimizer",
|
|
21
|
+
"tree-shaking",
|
|
22
|
+
"unused-css"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node test/test.js"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"glob": "^10.3.10"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/nkdeus/eva.git",
|
|
33
|
+
"directory": "packages/eva-purge"
|
|
34
|
+
},
|
|
35
|
+
"author": {
|
|
36
|
+
"name": "Michaƫl Tati",
|
|
37
|
+
"url": "https://ulysse-2029.com/"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://eva-css.xyz/",
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|
package/src/purge.js
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
|
|
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
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const glob = require('glob');
|
|
14
|
+
|
|
15
|
+
class CSSPurger {
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.config = {
|
|
18
|
+
content: config.content || ['**/*.{html,js}'],
|
|
19
|
+
css: config.css || 'styles/main.css',
|
|
20
|
+
output: config.output || 'styles/main-compressed.css',
|
|
21
|
+
safelist: config.safelist || [],
|
|
22
|
+
...config
|
|
23
|
+
};
|
|
24
|
+
this.usedClasses = new Set();
|
|
25
|
+
this.usedVariables = new Set();
|
|
26
|
+
this.usedIds = new Set();
|
|
27
|
+
this.cssContent = '';
|
|
28
|
+
this.compressedCSS = '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Main purge process
|
|
33
|
+
*/
|
|
34
|
+
async purge() {
|
|
35
|
+
console.log('š Starting CSS purge process...');
|
|
36
|
+
|
|
37
|
+
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
|
|
45
|
+
await this.readCompiledCSS();
|
|
46
|
+
|
|
47
|
+
// Step 4: Extract used styles
|
|
48
|
+
await this.extractUsedStyles();
|
|
49
|
+
|
|
50
|
+
// Step 5: Compress and optimize
|
|
51
|
+
await this.compressCSS();
|
|
52
|
+
|
|
53
|
+
// Step 6: Write output
|
|
54
|
+
await this.writeCompressedCSS();
|
|
55
|
+
|
|
56
|
+
console.log('ā
CSS purge completed successfully!');
|
|
57
|
+
this.showStats();
|
|
58
|
+
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('ā Error during CSS purge:', error);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Analyze HTML files to extract used classes and CSS variables
|
|
67
|
+
*/
|
|
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
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
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`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Analyze JavaScript files to extract dynamically used classes and IDs
|
|
119
|
+
*/
|
|
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++;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
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++;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
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
|
+
}
|
|
229
|
+
}
|
|
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
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Read the compiled CSS file
|
|
241
|
+
*/
|
|
242
|
+
async readCompiledCSS() {
|
|
243
|
+
console.log('š Reading compiled CSS...');
|
|
244
|
+
|
|
245
|
+
const cssPath = path.resolve(process.cwd(), this.config.css);
|
|
246
|
+
|
|
247
|
+
if (!fs.existsSync(cssPath)) {
|
|
248
|
+
throw new Error(`CSS file not found: ${cssPath}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.cssContent = fs.readFileSync(cssPath, 'utf8');
|
|
252
|
+
console.log(`š Original CSS size: ${(this.cssContent.length / 1024).toFixed(2)} KB`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Extract only the used CSS rules and variables
|
|
257
|
+
*/
|
|
258
|
+
async extractUsedStyles() {
|
|
259
|
+
console.log('š Extracting used styles...');
|
|
260
|
+
|
|
261
|
+
const lines = this.cssContent.split('\n');
|
|
262
|
+
const usedLines = [];
|
|
263
|
+
let currentRule = '';
|
|
264
|
+
let currentRuleContent = '';
|
|
265
|
+
let inRule = false;
|
|
266
|
+
let inRootRule = false; // Special flag for :root blocks
|
|
267
|
+
let inMediaQuery = false; // Special flag for @media blocks
|
|
268
|
+
let braceCount = 0;
|
|
269
|
+
let ruleLines = [];
|
|
270
|
+
let processedRules = 0;
|
|
271
|
+
let mediaQueryCount = 0;
|
|
272
|
+
|
|
273
|
+
for (let i = 0; i < lines.length; i++) {
|
|
274
|
+
const line = lines[i];
|
|
275
|
+
const trimmedLine = line.trim();
|
|
276
|
+
|
|
277
|
+
// Progress indicator
|
|
278
|
+
if (i % 1000 === 0) {
|
|
279
|
+
console.log(`š Processing line ${i}/${lines.length} (${Math.round(i/lines.length*100)}%)`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Skip empty lines and comments at the root level
|
|
283
|
+
if (!trimmedLine || (trimmedLine.startsWith('/*') && !inRule)) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
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('}') &&
|
|
289
|
+
(trimmedLine.includes(',') || /^[a-zA-Z*#.\[]/.test(trimmedLine) || trimmedLine.includes('::') || trimmedLine.includes(':'))) {
|
|
290
|
+
// This might be part of a multi-line selector
|
|
291
|
+
if (!currentRule) {
|
|
292
|
+
currentRule = trimmedLine;
|
|
293
|
+
ruleLines = [line];
|
|
294
|
+
} else {
|
|
295
|
+
// Append to existing selector
|
|
296
|
+
currentRule += ',' + trimmedLine;
|
|
297
|
+
ruleLines.push(line);
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Handle opening braces
|
|
303
|
+
if (trimmedLine.includes('{')) {
|
|
304
|
+
braceCount++;
|
|
305
|
+
if (!inRule && braceCount === 1) {
|
|
306
|
+
// Complete the selector if it wasn't already accumulated
|
|
307
|
+
if (!currentRule) {
|
|
308
|
+
currentRule = trimmedLine.replace(/\s*\{.*/, '');
|
|
309
|
+
ruleLines = [line];
|
|
310
|
+
} else {
|
|
311
|
+
// Use accumulated selector and add the line with the opening brace
|
|
312
|
+
currentRule += trimmedLine.replace(/\s*\{.*/, '');
|
|
313
|
+
ruleLines.push(line);
|
|
314
|
+
}
|
|
315
|
+
currentRuleContent = '';
|
|
316
|
+
inRule = true;
|
|
317
|
+
|
|
318
|
+
// Check if this is a media query (always keep these completely)
|
|
319
|
+
if (currentRule.includes('@media')) {
|
|
320
|
+
inMediaQuery = true;
|
|
321
|
+
mediaQueryCount++;
|
|
322
|
+
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')) {
|
|
326
|
+
inRootRule = true;
|
|
327
|
+
console.log(`š Found ${currentRule.trim()} block - keeping all variables`);
|
|
328
|
+
}
|
|
329
|
+
} else if (inRule) {
|
|
330
|
+
ruleLines.push(line);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Handle closing braces
|
|
334
|
+
else if (trimmedLine.includes('}')) {
|
|
335
|
+
braceCount--;
|
|
336
|
+
if (inRule) {
|
|
337
|
+
ruleLines.push(line);
|
|
338
|
+
|
|
339
|
+
// Check if rule should be kept when we close the main rule
|
|
340
|
+
if (braceCount === 0) {
|
|
341
|
+
processedRules++;
|
|
342
|
+
|
|
343
|
+
// Always keep media queries, :root and .all-grads blocks completely
|
|
344
|
+
const shouldKeep = inMediaQuery ||
|
|
345
|
+
inRootRule ||
|
|
346
|
+
this.isUsedSelector(currentRule) ||
|
|
347
|
+
this.hasCurrentVariables(currentRuleContent);
|
|
348
|
+
|
|
349
|
+
if (shouldKeep) {
|
|
350
|
+
usedLines.push(...ruleLines);
|
|
351
|
+
if (inRootRule) {
|
|
352
|
+
// Count variables in this :root block
|
|
353
|
+
const varCount = ruleLines.join('').match(/--[a-zA-Z][a-zA-Z0-9_-]*:/g)?.length || 0;
|
|
354
|
+
console.log(`š Kept ${varCount} CSS variables from ${currentRule.trim()}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Reset for next rule
|
|
359
|
+
inRule = false;
|
|
360
|
+
inRootRule = false;
|
|
361
|
+
inMediaQuery = false;
|
|
362
|
+
currentRule = '';
|
|
363
|
+
currentRuleContent = '';
|
|
364
|
+
ruleLines = [];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Handle properties within rules
|
|
369
|
+
else if (inRule) {
|
|
370
|
+
ruleLines.push(line);
|
|
371
|
+
currentRuleContent += ' ' + trimmedLine;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log(`š Processed ${processedRules} CSS rules`);
|
|
376
|
+
console.log(`š± Kept ${mediaQueryCount} media queries`);
|
|
377
|
+
this.compressedCSS = usedLines.join('\n');
|
|
378
|
+
console.log(`š Extracted CSS size: ${(this.compressedCSS.length / 1024).toFixed(2)} KB`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Check if a CSS rule uses --current- variables and should be kept
|
|
383
|
+
*/
|
|
384
|
+
hasCurrentVariables(ruleContent) {
|
|
385
|
+
return /var\(--current-/.test(ruleContent);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Check if a CSS selector is an HTML element or reset selector that should be kept
|
|
390
|
+
*/
|
|
391
|
+
isElementOrResetSelector(selector) {
|
|
392
|
+
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') ||
|
|
397
|
+
cleanSelector.includes('::after') ||
|
|
398
|
+
cleanSelector.includes(':target') ||
|
|
399
|
+
/^body[,\s]|^html[,\s]/.test(cleanSelector)) {
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Handle multiple selectors separated by commas - if ANY selector in the group is an element/reset, keep the whole group
|
|
404
|
+
const selectors = cleanSelector.split(',');
|
|
405
|
+
|
|
406
|
+
const htmlElements = [
|
|
407
|
+
'html', 'body', 'head', 'title', 'meta', 'link', 'script', 'style',
|
|
408
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'hr',
|
|
409
|
+
'a', 'img', 'figure', 'figcaption', 'picture', 'source',
|
|
410
|
+
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
|
|
411
|
+
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption',
|
|
412
|
+
'form', 'input', 'button', 'textarea', 'select', 'option', 'label', 'fieldset', 'legend',
|
|
413
|
+
'div', 'span', 'section', 'article', 'aside', 'header', 'footer', 'nav', 'main',
|
|
414
|
+
'blockquote', 'cite', 'q', 'pre', 'code', 'kbd', 'samp', 'var',
|
|
415
|
+
'em', 'strong', 'b', 'i', 'u', 's', 'small', 'mark', 'del', 'ins', 'sub', 'sup',
|
|
416
|
+
'video', 'audio', 'canvas', 'svg', 'iframe', 'embed', 'object', 'param'
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
for (const sel of selectors) {
|
|
420
|
+
const trimmedSel = sel.trim();
|
|
421
|
+
|
|
422
|
+
// Check if it's a pure element selector
|
|
423
|
+
const baseElement = trimmedSel.replace(/::[^,\s]+|:[^,\s(]+(\([^)]*\))?/g, '').trim();
|
|
424
|
+
if (htmlElements.includes(baseElement)) {
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Check for element selectors with pseudo-classes/elements (like a:hover, input:focus)
|
|
429
|
+
if (htmlElements.some(element => trimmedSel.startsWith(element + ':') || trimmedSel.startsWith(element + '::') || trimmedSel === element)) {
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Check for attribute selectors on elements (like input[type="text"])
|
|
434
|
+
if (htmlElements.some(element => trimmedSel.startsWith(element + '[') && trimmedSel.includes(']'))) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Check for selectors starting with element but containing other selectors
|
|
439
|
+
// e.g., "ul[role=list]", "a:not([class])", etc.
|
|
440
|
+
const match = trimmedSel.match(/^([a-zA-Z][a-zA-Z0-9]*)/);
|
|
441
|
+
if (match && htmlElements.includes(match[1])) {
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Check if a CSS selector is used in the HTML or JavaScript
|
|
451
|
+
*/
|
|
452
|
+
isUsedSelector(selector) {
|
|
453
|
+
// Clean the selector
|
|
454
|
+
const cleanSelector = selector.replace(/\s*\{.*/, '').trim();
|
|
455
|
+
|
|
456
|
+
// Always keep element and reset selectors
|
|
457
|
+
if (this.isElementOrResetSelector(selector)) {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Handle multiple selectors separated by commas
|
|
462
|
+
const selectors = cleanSelector.split(',');
|
|
463
|
+
|
|
464
|
+
for (const sel of selectors) {
|
|
465
|
+
const trimmedSel = sel.trim();
|
|
466
|
+
|
|
467
|
+
// Skip pseudo-elements and pseudo-classes for now
|
|
468
|
+
if (trimmedSel.includes('::') || trimmedSel.includes(':hover') || trimmedSel.includes(':before') || trimmedSel.includes(':after')) {
|
|
469
|
+
// Check the base selector
|
|
470
|
+
const baseSelector = trimmedSel.replace(/::[^,\s]+|:[^,\s]+/g, '').trim();
|
|
471
|
+
if (this.isUsedSelector(baseSelector)) {
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Handle ID selectors
|
|
478
|
+
if (trimmedSel.startsWith('#')) {
|
|
479
|
+
const idName = trimmedSel.substring(1).replace(/[^\w-_]/g, '');
|
|
480
|
+
if (this.usedIds.has(idName)) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Handle class selectors
|
|
486
|
+
if (trimmedSel.startsWith('.')) {
|
|
487
|
+
const className = trimmedSel.substring(1).replace(/[^\w-_]/g, '');
|
|
488
|
+
if (this.usedClasses.has(className)) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Handle compound selectors (e.g., .class1.class2, #id.class)
|
|
494
|
+
const classes = trimmedSel.match(/\.[a-zA-Z][a-zA-Z0-9_-]*/g);
|
|
495
|
+
const ids = trimmedSel.match(/#[a-zA-Z][a-zA-Z0-9_-]*/g);
|
|
496
|
+
|
|
497
|
+
if (classes || ids) {
|
|
498
|
+
let allSelectorsUsed = true;
|
|
499
|
+
|
|
500
|
+
// Check all classes in compound selector
|
|
501
|
+
if (classes) {
|
|
502
|
+
allSelectorsUsed = allSelectorsUsed && classes.every(cls => {
|
|
503
|
+
const className = cls.substring(1);
|
|
504
|
+
return this.usedClasses.has(className);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Check all IDs in compound selector
|
|
509
|
+
if (ids) {
|
|
510
|
+
allSelectorsUsed = allSelectorsUsed && ids.every(id => {
|
|
511
|
+
const idName = id.substring(1);
|
|
512
|
+
return this.usedIds.has(idName);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (allSelectorsUsed && (classes?.length > 0 || ids?.length > 0)) {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Compress and optimize the CSS
|
|
527
|
+
*/
|
|
528
|
+
async compressCSS() {
|
|
529
|
+
console.log('šļø Compressing CSS...');
|
|
530
|
+
|
|
531
|
+
let compressed = this.compressedCSS;
|
|
532
|
+
|
|
533
|
+
// Remove comments
|
|
534
|
+
compressed = compressed.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
535
|
+
|
|
536
|
+
// Remove unnecessary whitespace but preserve selector grouping structure
|
|
537
|
+
compressed = compressed.replace(/\s+/g, ' ');
|
|
538
|
+
|
|
539
|
+
// Remove whitespace around braces and semicolons - but preserve commas in selectors
|
|
540
|
+
compressed = compressed.replace(/\s*{\s*/g, '{');
|
|
541
|
+
compressed = compressed.replace(/\s*}\s*/g, '}');
|
|
542
|
+
compressed = compressed.replace(/\s*;\s*/g, ';');
|
|
543
|
+
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
|
+
compressed = compressed.replace(/(\w|[)\]])\s*,\s*(\w|[.#*:])/g, '$1,$2');
|
|
548
|
+
|
|
549
|
+
// Remove trailing semicolons before }
|
|
550
|
+
compressed = compressed.replace(/;}/g, '}');
|
|
551
|
+
|
|
552
|
+
// Remove empty rules
|
|
553
|
+
compressed = compressed.replace(/[^}]+{\s*}/g, '');
|
|
554
|
+
|
|
555
|
+
// Final cleanup
|
|
556
|
+
compressed = compressed.trim();
|
|
557
|
+
|
|
558
|
+
this.compressedCSS = compressed;
|
|
559
|
+
console.log(`š Compressed CSS size: ${(compressed.length / 1024).toFixed(2)} KB`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Write the compressed CSS to file
|
|
564
|
+
*/
|
|
565
|
+
async writeCompressedCSS() {
|
|
566
|
+
console.log('š¾ Writing compressed CSS...');
|
|
567
|
+
|
|
568
|
+
const outputPath = path.resolve(process.cwd(), this.config.output);
|
|
569
|
+
|
|
570
|
+
// Ensure output directory exists
|
|
571
|
+
const outputDir = path.dirname(outputPath);
|
|
572
|
+
if (!fs.existsSync(outputDir)) {
|
|
573
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Add a header comment
|
|
577
|
+
const header = `/* EVA CSS Purged - Generated on ${new Date().toISOString()} */\n`;
|
|
578
|
+
const finalCSS = header + this.compressedCSS;
|
|
579
|
+
|
|
580
|
+
fs.writeFileSync(outputPath, finalCSS);
|
|
581
|
+
console.log(`š Compressed CSS saved to: ${outputPath}`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Show compression statistics
|
|
586
|
+
*/
|
|
587
|
+
showStats() {
|
|
588
|
+
const originalSize = this.cssContent.length;
|
|
589
|
+
const compressedSize = this.compressedCSS.length;
|
|
590
|
+
const savings = ((originalSize - compressedSize) / originalSize * 100).toFixed(2);
|
|
591
|
+
|
|
592
|
+
console.log('\nš Compression Statistics:');
|
|
593
|
+
console.log(` Original size: ${(originalSize / 1024).toFixed(2)} KB`);
|
|
594
|
+
console.log(` Compressed size: ${(compressedSize / 1024).toFixed(2)} KB`);
|
|
595
|
+
console.log(` Space saved: ${savings}%`);
|
|
596
|
+
console.log(` Used classes: ${this.usedClasses.size}`);
|
|
597
|
+
console.log(` Used variables: ${this.usedVariables.size}`);
|
|
598
|
+
console.log(` Used IDs: ${this.usedIds.size}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Run the purger
|
|
603
|
+
if (require.main === module) {
|
|
604
|
+
const purger = new CSSPurger();
|
|
605
|
+
purger.purge();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
module.exports = CSSPurger;
|