santycss 1.0.0
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 +1 -0
- package/dist/santy-animations.css +519 -0
- package/dist/santy-components.css +680 -0
- package/dist/santy-core.css +11336 -0
- package/dist/santy.css +12538 -0
- package/dist/santy.min.css +1 -0
- package/index.js +50 -0
- package/lib/animations.js +539 -0
- package/lib/purge-core.js +223 -0
- package/package.json +65 -0
- package/postcss/index.js +79 -0
- package/purge.js +98 -0
- package/santy-jit.js +841 -0
- package/vite-plugin-santycss.js +128 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* SantyCSS — core purge engine (used by CLI, PostCSS plugin, and Vite plugin)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const EXTS = ['.html','.js','.jsx','.ts','.tsx','.vue','.svelte','.njk','.hbs','.pug','.php','.astro','.mdx','.md'];
|
|
10
|
+
const SKIP = new Set(['build.js','purge.js','postcss.config.js','postcss.config.cjs','vite.config.js','vite.config.ts','webpack.config.js','next.config.js','nuxt.config.js','nuxt.config.ts']);
|
|
11
|
+
|
|
12
|
+
// ─── File walker ──────────────────────────────────────────────────────────────
|
|
13
|
+
function walkDir(dir, files = []) {
|
|
14
|
+
if (!fs.existsSync(dir)) return files;
|
|
15
|
+
const stat = fs.statSync(dir);
|
|
16
|
+
if (stat.isFile()) {
|
|
17
|
+
if (!SKIP.has(path.basename(dir))) files.push(dir);
|
|
18
|
+
return files;
|
|
19
|
+
}
|
|
20
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
21
|
+
if (['node_modules','.git','dist','.next','.nuxt','.output','build','__pycache__','.svelte-kit'].includes(entry)) continue;
|
|
22
|
+
walkDir(path.join(dir, entry), files);
|
|
23
|
+
}
|
|
24
|
+
return files;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── Class extractor ──────────────────────────────────────────────────────────
|
|
28
|
+
const CLASS_ATTR = /class(?:Name)?=(?:"([^"]+)"|'([^']+)'|`([^`]+)`)/g;
|
|
29
|
+
const BARE_STRING = /["'`]([\w][\w\-:/]+)["'`]/g;
|
|
30
|
+
|
|
31
|
+
function extractClasses(content) {
|
|
32
|
+
const found = new Set();
|
|
33
|
+
|
|
34
|
+
// 1. class="..." / className="..." / class='...' / class=`...`
|
|
35
|
+
CLASS_ATTR.lastIndex = 0;
|
|
36
|
+
let m;
|
|
37
|
+
while ((m = CLASS_ATTR.exec(content)) !== null) {
|
|
38
|
+
const raw = (m[1] || m[2] || m[3] || '').trim();
|
|
39
|
+
raw.split(/\s+/).forEach(c => c && found.add(c.trim()));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. Dynamic: clsx(['foo','bar']), cn('a','b'), classList.add('x')
|
|
43
|
+
// Match any quoted string that looks like a CSS class (has a dash, no spaces)
|
|
44
|
+
BARE_STRING.lastIndex = 0;
|
|
45
|
+
while ((m = BARE_STRING.exec(content)) !== null) {
|
|
46
|
+
const c = m[1].trim();
|
|
47
|
+
if (c.includes('-') && !c.includes(' ')) found.add(c);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Template literals: `make-flex ${condition ? 'add-padding-16' : 'add-padding-8'}`
|
|
51
|
+
const tpl = content.match(/`[^`]+`/g) || [];
|
|
52
|
+
tpl.forEach(t => {
|
|
53
|
+
t.split(/\s+|\$\{[^}]+\}/).forEach(part => {
|
|
54
|
+
const c = part.replace(/[`'"]/g, '').trim();
|
|
55
|
+
if (c && c.includes('-')) found.add(c);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return found;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── CSS parser ───────────────────────────────────────────────────────────────
|
|
63
|
+
function parseCSS(css) {
|
|
64
|
+
const blocks = [];
|
|
65
|
+
let i = 0, len = css.length;
|
|
66
|
+
|
|
67
|
+
while (i < len) {
|
|
68
|
+
while (i < len && /\s/.test(css[i])) i++;
|
|
69
|
+
if (i >= len) break;
|
|
70
|
+
|
|
71
|
+
if (css[i] === '/' && css[i+1] === '*') {
|
|
72
|
+
const end = css.indexOf('*/', i + 2);
|
|
73
|
+
if (end === -1) break;
|
|
74
|
+
blocks.push({ type: 'comment', raw: css.slice(i, end + 2) });
|
|
75
|
+
i = end + 2;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (css[i] === '@' || css.slice(i, i+5) === ':root') {
|
|
80
|
+
let bs = i;
|
|
81
|
+
while (bs < len && css[bs] !== '{' && css[bs] !== ';') bs++;
|
|
82
|
+
if (bs >= len) break;
|
|
83
|
+
if (css[bs] === ';') { blocks.push({ type: 'at-simple', raw: css.slice(i, bs + 1) }); i = bs + 1; continue; }
|
|
84
|
+
let depth = 0, j = bs;
|
|
85
|
+
while (j < len) {
|
|
86
|
+
if (css[j] === '{') depth++;
|
|
87
|
+
else if (css[j] === '}' && --depth === 0) { j++; break; }
|
|
88
|
+
j++;
|
|
89
|
+
}
|
|
90
|
+
blocks.push({ type: 'at-block', raw: css.slice(i, j) });
|
|
91
|
+
i = j;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let bs = i;
|
|
96
|
+
while (bs < len && css[bs] !== '{') bs++;
|
|
97
|
+
if (bs >= len) break;
|
|
98
|
+
let be = bs;
|
|
99
|
+
while (be < len && css[be] !== '}') be++;
|
|
100
|
+
if (be >= len) break;
|
|
101
|
+
blocks.push({ type: 'rule', selector: css.slice(i, bs).trim(), raw: css.slice(i, be + 1) });
|
|
102
|
+
i = be + 1;
|
|
103
|
+
}
|
|
104
|
+
return blocks;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Selector usage check ─────────────────────────────────────────────────────
|
|
108
|
+
const BASE_SELECTORS = ['*', ':root', 'html', 'body', ':before', ':after', '::before', '::after'];
|
|
109
|
+
|
|
110
|
+
function selectorUsed(selector, usedSet) {
|
|
111
|
+
if (BASE_SELECTORS.some(s => selector.includes(s))) return true;
|
|
112
|
+
const classMatches = selector.match(/\.([\w\-\\:]+)/g);
|
|
113
|
+
if (!classMatches) return true; // non-class selector — keep
|
|
114
|
+
return classMatches.some(cm => usedSet.has(cm.slice(1).replace(/\\/g, '')));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── @media / @keyframes filter ──────────────────────────────────────────────
|
|
118
|
+
function filterAtBlock(raw, usedSet) {
|
|
119
|
+
const headerMatch = raw.match(/^([^{]+)\{/);
|
|
120
|
+
if (!headerMatch) return raw;
|
|
121
|
+
const atRule = headerMatch[1].trim();
|
|
122
|
+
|
|
123
|
+
// Always keep keyframes, font-face
|
|
124
|
+
if (atRule.startsWith('@keyframes') || atRule.startsWith('@font-face')) return raw;
|
|
125
|
+
|
|
126
|
+
// :root block — always keep
|
|
127
|
+
if (atRule.startsWith(':root')) return raw;
|
|
128
|
+
|
|
129
|
+
// For @media — filter inner rules
|
|
130
|
+
const innerStart = raw.indexOf('{') + 1;
|
|
131
|
+
const innerEnd = raw.lastIndexOf('}');
|
|
132
|
+
const inner = raw.slice(innerStart, innerEnd);
|
|
133
|
+
|
|
134
|
+
const kept = [];
|
|
135
|
+
const ruleRe = /([^{]+)\{([^}]+)\}/g;
|
|
136
|
+
let m;
|
|
137
|
+
while ((m = ruleRe.exec(inner)) !== null) {
|
|
138
|
+
if (selectorUsed(m[1].trim(), usedSet)) {
|
|
139
|
+
kept.push(` ${m[1].trim()} { ${m[2].trim()} }`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return kept.length ? `${atRule} {\n${kept.join('\n')}\n}` : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Minifier ─────────────────────────────────────────────────────────────────
|
|
146
|
+
function minify(css) {
|
|
147
|
+
return css
|
|
148
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
149
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
150
|
+
.replace(/\s{2,}/g, ' ')
|
|
151
|
+
.replace(/\s*([{}:;,>~+])\s*/g, '$1')
|
|
152
|
+
.replace(/;}/g, '}')
|
|
153
|
+
.trim();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Main purge function ──────────────────────────────────────────────────────
|
|
157
|
+
/**
|
|
158
|
+
* @param {object} opts
|
|
159
|
+
* @param {string} opts.css Full source CSS string (santy.css contents)
|
|
160
|
+
* @param {string[]} opts.content Array of file paths OR raw content strings to scan
|
|
161
|
+
* @param {string[]} [opts.safelist] Class names to always keep
|
|
162
|
+
* @param {boolean} [opts.minify] Minify output (default true)
|
|
163
|
+
* @returns {{ css: string, stats: object }}
|
|
164
|
+
*/
|
|
165
|
+
function purge({ css, content = [], safelist = [], minifyOutput = true }) {
|
|
166
|
+
const usedClasses = new Set(safelist);
|
|
167
|
+
|
|
168
|
+
for (const item of content) {
|
|
169
|
+
// item is either a file path or raw content
|
|
170
|
+
let text = item;
|
|
171
|
+
if (typeof item === 'string' && fs.existsSync(item) && !item.includes('\n')) {
|
|
172
|
+
try { text = fs.readFileSync(item, 'utf8'); } catch (_) { text = item; }
|
|
173
|
+
}
|
|
174
|
+
extractClasses(text).forEach(c => usedClasses.add(c));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const blocks = parseCSS(css);
|
|
178
|
+
const kept = [];
|
|
179
|
+
let keptRules = 0, droppedRules = 0;
|
|
180
|
+
|
|
181
|
+
for (const block of blocks) {
|
|
182
|
+
if (block.type === 'comment') { kept.push(block.raw); continue; }
|
|
183
|
+
if (block.type === 'at-simple'){ kept.push(block.raw); continue; }
|
|
184
|
+
|
|
185
|
+
if (block.type === 'at-block') {
|
|
186
|
+
const filtered = filterAtBlock(block.raw, usedClasses);
|
|
187
|
+
if (filtered) { kept.push(filtered); keptRules++; }
|
|
188
|
+
else droppedRules++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (block.type === 'rule') {
|
|
193
|
+
if (selectorUsed(block.selector, usedClasses)) {
|
|
194
|
+
kept.push(block.raw); keptRules++;
|
|
195
|
+
} else droppedRules++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const rawOutput = kept.join('\n');
|
|
200
|
+
const output = minifyOutput ? minify(rawOutput) : rawOutput;
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
css: output,
|
|
204
|
+
stats: {
|
|
205
|
+
classesFound: usedClasses.size,
|
|
206
|
+
rulesKept: keptRules,
|
|
207
|
+
rulesDropped: droppedRules,
|
|
208
|
+
originalSize: Buffer.byteLength(css, 'utf8'),
|
|
209
|
+
outputSize: Buffer.byteLength(output, 'utf8'),
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── File-based helper ────────────────────────────────────────────────────────
|
|
215
|
+
function purgeFiles({ cssFile = 'santy.css', inputDirs = ['.'], safelist = [], minifyOutput = true } = {}) {
|
|
216
|
+
const css = fs.readFileSync(cssFile, 'utf8');
|
|
217
|
+
const files = [];
|
|
218
|
+
inputDirs.forEach(d => walkDir(d, files));
|
|
219
|
+
const sourceFiles = files.filter(f => EXTS.includes(path.extname(f).toLowerCase()));
|
|
220
|
+
return purge({ css, content: sourceFiles, safelist, minifyOutput });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = { purge, purgeFiles, extractClasses, walkDir, minify, EXTS };
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "santycss",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Plain-English utility-first CSS framework — no build step, just classes",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"style": "dist/santy.css",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./css": "./dist/santy.css",
|
|
10
|
+
"./css/core": "./dist/santy-core.css",
|
|
11
|
+
"./css/components": "./dist/santy-components.css",
|
|
12
|
+
"./css/animations": "./dist/santy-animations.css",
|
|
13
|
+
"./min": "./dist/santy.min.css",
|
|
14
|
+
"./postcss": "./postcss/index.js",
|
|
15
|
+
"./jit": "./santy-jit.js",
|
|
16
|
+
"./vite": "./vite-plugin-santycss.js",
|
|
17
|
+
"./purge": "./lib/purge-core.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/",
|
|
21
|
+
"postcss/",
|
|
22
|
+
"lib/",
|
|
23
|
+
"index.js",
|
|
24
|
+
"santy-jit.js",
|
|
25
|
+
"vite-plugin-santycss.js",
|
|
26
|
+
"purge.js",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"bin": {
|
|
30
|
+
"santycss": "./purge.js"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"css",
|
|
34
|
+
"framework",
|
|
35
|
+
"utility-css",
|
|
36
|
+
"tailwind-alternative",
|
|
37
|
+
"plain-english",
|
|
38
|
+
"no-build",
|
|
39
|
+
"santycss",
|
|
40
|
+
"postcss",
|
|
41
|
+
"vite"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "node build.js",
|
|
45
|
+
"purge": "node purge.js",
|
|
46
|
+
"dev": "node build.js && echo 'Watching...'",
|
|
47
|
+
"prepublish": "node build.js"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"postcss": ">=8.0.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"postcss": { "optional": true }
|
|
54
|
+
},
|
|
55
|
+
"author": "Santy",
|
|
56
|
+
"license": "MIT",
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "https://github.com/ChintuSanty/santyCSS.git"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://santycss.santy.in",
|
|
62
|
+
"bugs": {
|
|
63
|
+
"url": "https://github.com/ChintuSanty/santyCSS/issues"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/postcss/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* postcss-santycss
|
|
4
|
+
*
|
|
5
|
+
* PostCSS plugin that purges unused SantyCSS utilities.
|
|
6
|
+
*
|
|
7
|
+
* Usage in postcss.config.js:
|
|
8
|
+
*
|
|
9
|
+
* const santycss = require('santycss/postcss');
|
|
10
|
+
*
|
|
11
|
+
* module.exports = {
|
|
12
|
+
* plugins: [
|
|
13
|
+
* santycss({
|
|
14
|
+
* content: ['./src/**\/*.{html,js,jsx,ts,tsx,vue,svelte}'],
|
|
15
|
+
* safelist: ['animate-spin', 'make-hidden'],
|
|
16
|
+
* }),
|
|
17
|
+
* ],
|
|
18
|
+
* };
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const { purge, extractClasses, EXTS } = require('../lib/purge-core');
|
|
24
|
+
|
|
25
|
+
function expandGlobs(patterns) {
|
|
26
|
+
const files = [];
|
|
27
|
+
for (const pat of patterns) {
|
|
28
|
+
if (!pat.includes('*')) {
|
|
29
|
+
if (fs.existsSync(pat)) files.push(pat);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const parts = pat.split(/[/\\]/);
|
|
33
|
+
const base = parts.slice(0, parts.findIndex(p => p.includes('*'))).join('/') || '.';
|
|
34
|
+
const extMatch = pat.match(/\.([\w,{}]+)$/);
|
|
35
|
+
const exts = extMatch ? extMatch[1].replace(/[{}]/g,'').split(',') : EXTS;
|
|
36
|
+
function walk(dir) {
|
|
37
|
+
if (!fs.existsSync(dir)) return;
|
|
38
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
39
|
+
const full = dir + '/' + entry.name;
|
|
40
|
+
if (entry.isDirectory()) walk(full);
|
|
41
|
+
else if (exts.some(e => entry.name.endsWith('.' + e))) files.push(full);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
walk(base);
|
|
45
|
+
}
|
|
46
|
+
return [...new Set(files)];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = (opts = {}) => {
|
|
50
|
+
const { content = [], safelist = [], sourceFile = null } = opts;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
postcssPlugin: 'postcss-santycss',
|
|
54
|
+
async Once(root, { result }) {
|
|
55
|
+
const files = expandGlobs(content);
|
|
56
|
+
if (!files.length && !sourceFile) return;
|
|
57
|
+
|
|
58
|
+
const sourceCSS = sourceFile
|
|
59
|
+
? fs.readFileSync(sourceFile, 'utf8')
|
|
60
|
+
: fs.readFileSync(path.join(__dirname, '../dist/santy.css'), 'utf8');
|
|
61
|
+
|
|
62
|
+
const html = files.map(f => fs.readFileSync(f, 'utf8')).join('\n');
|
|
63
|
+
const purged = purge(sourceCSS, html, { safelist });
|
|
64
|
+
|
|
65
|
+
root.removeAll();
|
|
66
|
+
const postcss = require('postcss');
|
|
67
|
+
const parsed = postcss.parse(purged);
|
|
68
|
+
parsed.each(node => root.append(node.clone()));
|
|
69
|
+
|
|
70
|
+
result.messages.push({
|
|
71
|
+
type: 'santycss',
|
|
72
|
+
plugin: 'postcss-santycss',
|
|
73
|
+
text: `Purged to ${(purged.length / 1024).toFixed(1)}KB from ${files.length} files`,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
module.exports.postcss = true;
|
package/purge.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SantyCSS CLI Purger — node purge.js [options]
|
|
4
|
+
*
|
|
5
|
+
* --input=<dir> Directories to scan (default: .)
|
|
6
|
+
* --out=<file> Output file (default: santy.min.css)
|
|
7
|
+
* --css=<file> Source CSS (default: santy.css)
|
|
8
|
+
* --keep=<a,b,c> Always-include class names
|
|
9
|
+
* --safelist=<file> JSON file ["class1","class2",...]
|
|
10
|
+
* --verbose List every class found
|
|
11
|
+
* --stats Print stats only, no file written
|
|
12
|
+
* --no-minify Write readable (un-minified) output
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { purge, walkDir, EXTS } = require('./lib/purge-core');
|
|
20
|
+
|
|
21
|
+
// ─── Parse CLI args ───────────────────────────────────────────────────────────
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
const getArg = (k, def) => { const m = args.find(a => a.startsWith(`--${k}=`)); return m ? m.slice(k.length + 3) : def; };
|
|
24
|
+
const hasFlag = (k) => args.includes(`--${k}`);
|
|
25
|
+
|
|
26
|
+
const inputDirs = getArg('input', '.').split(',').map(d => d.trim());
|
|
27
|
+
const outFile = getArg('out', 'santy.min.css');
|
|
28
|
+
const cssFile = getArg('css', 'santy.css');
|
|
29
|
+
const keepArg = getArg('keep', '');
|
|
30
|
+
const safeFile = getArg('safelist', '');
|
|
31
|
+
const verbose = hasFlag('verbose');
|
|
32
|
+
const statsOnly = hasFlag('stats');
|
|
33
|
+
const noMinify = hasFlag('no-minify');
|
|
34
|
+
|
|
35
|
+
// ─── Load source CSS ──────────────────────────────────────────────────────────
|
|
36
|
+
if (!fs.existsSync(cssFile)) {
|
|
37
|
+
console.error(`❌ Cannot find ${cssFile}. Run: node build.js first.`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const css = fs.readFileSync(cssFile, 'utf8');
|
|
41
|
+
|
|
42
|
+
// ─── Safelist ─────────────────────────────────────────────────────────────────
|
|
43
|
+
const safelist = [];
|
|
44
|
+
if (safeFile && fs.existsSync(safeFile)) {
|
|
45
|
+
JSON.parse(fs.readFileSync(safeFile, 'utf8')).forEach(c => safelist.push(c));
|
|
46
|
+
}
|
|
47
|
+
if (keepArg) keepArg.split(',').map(c => c.trim()).filter(Boolean).forEach(c => safelist.push(c));
|
|
48
|
+
|
|
49
|
+
// ─── Collect files ────────────────────────────────────────────────────────────
|
|
50
|
+
const allFiles = [];
|
|
51
|
+
inputDirs.forEach(d => walkDir(d, allFiles));
|
|
52
|
+
const sourceFiles = allFiles.filter(f => EXTS.includes(path.extname(f).toLowerCase()));
|
|
53
|
+
|
|
54
|
+
if (sourceFiles.length === 0) {
|
|
55
|
+
console.warn('⚠️ No source files found. Writing full CSS instead.');
|
|
56
|
+
fs.writeFileSync(outFile, css, 'utf8');
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Run purge ────────────────────────────────────────────────────────────────
|
|
61
|
+
const { css: output, stats } = purge({ css, content: sourceFiles, safelist, minifyOutput: !noMinify });
|
|
62
|
+
|
|
63
|
+
// ─── Stats ────────────────────────────────────────────────────────────────────
|
|
64
|
+
const estGzip = Math.round(stats.outputSize * 0.15);
|
|
65
|
+
const reduction = (((stats.originalSize - stats.outputSize) / stats.originalSize) * 100).toFixed(1);
|
|
66
|
+
|
|
67
|
+
console.log('\n📦 SantyCSS Purge Report');
|
|
68
|
+
console.log('─'.repeat(46));
|
|
69
|
+
console.log(` Source files scanned : ${sourceFiles.length}`);
|
|
70
|
+
console.log(` Unique classes found : ${stats.classesFound}`);
|
|
71
|
+
console.log(` Rules kept : ${stats.rulesKept}`);
|
|
72
|
+
console.log(` Rules dropped : ${stats.rulesDropped}`);
|
|
73
|
+
console.log('─'.repeat(46));
|
|
74
|
+
console.log(` Original size : ${(stats.originalSize/1024).toFixed(1)} KB`);
|
|
75
|
+
console.log(` Purged size : ${(stats.outputSize/1024).toFixed(1)} KB`);
|
|
76
|
+
console.log(` Est. gzip : ~${(estGzip/1024).toFixed(1)} KB`);
|
|
77
|
+
console.log(` Reduction : ${reduction}% smaller`);
|
|
78
|
+
console.log('─'.repeat(46));
|
|
79
|
+
|
|
80
|
+
if (verbose) {
|
|
81
|
+
// Re-extract for display (stats don't store them)
|
|
82
|
+
const { extractClasses } = require('./lib/purge-core');
|
|
83
|
+
const all = new Set(safelist);
|
|
84
|
+
sourceFiles.forEach(f => {
|
|
85
|
+
const text = fs.readFileSync(f, 'utf8');
|
|
86
|
+
extractClasses(text).forEach(c => all.add(c));
|
|
87
|
+
});
|
|
88
|
+
console.log('\nClasses found in source:');
|
|
89
|
+
[...all].sort().forEach(c => console.log(' ', c));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (statsOnly) {
|
|
93
|
+
console.log('\n(--stats: no file written)\n');
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fs.writeFileSync(outFile, output, 'utf8');
|
|
98
|
+
console.log(`\n✅ Written → ${outFile}\n`);
|