emily-css 1.0.17 → 1.0.19
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/CHANGELOG.md +13 -0
- package/README.md +21 -29
- package/bin/emilyui.js +4 -8
- package/package.json +7 -2
- package/src/index.js +86 -77
- package/src/init.js +224 -116
- package/src/purge.js +123 -56
- package/src/showcase.js +84 -39
- package/src/watch.js +145 -57
- package/src/purge-cmd.js +0 -55
package/src/purge.js
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
// new version
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const DEFAULT_EXTENSIONS = [
|
|
7
|
+
".html",
|
|
8
|
+
".htm",
|
|
9
|
+
".twig",
|
|
10
|
+
".njk",
|
|
11
|
+
".liquid",
|
|
12
|
+
".hbs",
|
|
13
|
+
".js",
|
|
14
|
+
".jsx",
|
|
15
|
+
".ts",
|
|
16
|
+
".tsx",
|
|
17
|
+
".vue",
|
|
18
|
+
".php",
|
|
19
|
+
".astro",
|
|
20
|
+
".svelte",
|
|
21
|
+
".blade.php",
|
|
22
|
+
".jinja",
|
|
23
|
+
".jinja2",
|
|
24
|
+
".j2",
|
|
25
|
+
".md",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function getAllFiles(dir, extensions = DEFAULT_EXTENSIONS) {
|
|
7
29
|
let files = [];
|
|
8
30
|
|
|
9
31
|
try {
|
|
@@ -12,13 +34,17 @@ function getAllFiles(dir, extensions = ['.html', '.htm', '.twig', '.njk', '.liqu
|
|
|
12
34
|
for (const entry of entries) {
|
|
13
35
|
const fullPath = path.join(dir, entry.name);
|
|
14
36
|
|
|
15
|
-
if (
|
|
37
|
+
if (
|
|
38
|
+
entry.name.startsWith(".") ||
|
|
39
|
+
entry.name === "node_modules" ||
|
|
40
|
+
entry.name === "dist"
|
|
41
|
+
) {
|
|
16
42
|
continue;
|
|
17
43
|
}
|
|
18
44
|
|
|
19
45
|
if (entry.isDirectory()) {
|
|
20
46
|
files = files.concat(getAllFiles(fullPath, extensions));
|
|
21
|
-
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
47
|
+
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
22
48
|
files.push(fullPath);
|
|
23
49
|
}
|
|
24
50
|
}
|
|
@@ -36,7 +62,7 @@ function extractClassNames(content) {
|
|
|
36
62
|
|
|
37
63
|
while ((match = classRegex.exec(content)) !== null) {
|
|
38
64
|
const classes = match[1].split(/\s+/);
|
|
39
|
-
classes.forEach(cls => {
|
|
65
|
+
classes.forEach((cls) => {
|
|
40
66
|
if (cls.trim()) classNames.add(cls.trim());
|
|
41
67
|
});
|
|
42
68
|
}
|
|
@@ -44,8 +70,8 @@ function extractClassNames(content) {
|
|
|
44
70
|
const vueRegex = /(?::class|class\.|v-bind:class)\s*=\s*["'{]([^"'}]+)["'}]/g;
|
|
45
71
|
while ((match = vueRegex.exec(content)) !== null) {
|
|
46
72
|
const classes = match[1].split(/[\s,]+/);
|
|
47
|
-
classes.forEach(cls => {
|
|
48
|
-
const cleaned = cls.replace(/['"`{}"]/g,
|
|
73
|
+
classes.forEach((cls) => {
|
|
74
|
+
const cleaned = cls.replace(/['"`{}"]/g, "").trim();
|
|
49
75
|
if (cleaned) classNames.add(cleaned);
|
|
50
76
|
});
|
|
51
77
|
}
|
|
@@ -55,18 +81,20 @@ function extractClassNames(content) {
|
|
|
55
81
|
|
|
56
82
|
function extractBlocks(css) {
|
|
57
83
|
const blocks = [];
|
|
58
|
-
let current =
|
|
84
|
+
let current = "";
|
|
59
85
|
let depth = 0;
|
|
60
86
|
|
|
61
87
|
for (let i = 0; i < css.length; i++) {
|
|
62
88
|
current += css[i];
|
|
63
|
-
|
|
89
|
+
|
|
90
|
+
if (css[i] === "{") {
|
|
64
91
|
depth++;
|
|
65
|
-
} else if (css[i] ===
|
|
92
|
+
} else if (css[i] === "}") {
|
|
66
93
|
depth--;
|
|
94
|
+
|
|
67
95
|
if (depth === 0) {
|
|
68
96
|
blocks.push(current.trim());
|
|
69
|
-
current =
|
|
97
|
+
current = "";
|
|
70
98
|
}
|
|
71
99
|
}
|
|
72
100
|
}
|
|
@@ -80,18 +108,18 @@ function extractBlocks(css) {
|
|
|
80
108
|
|
|
81
109
|
function purgeBlock(block, usedClasses) {
|
|
82
110
|
if (
|
|
83
|
-
block.startsWith(
|
|
84
|
-
block.startsWith(
|
|
85
|
-
block.startsWith(
|
|
86
|
-
block.startsWith(
|
|
87
|
-
block.startsWith(
|
|
111
|
+
block.startsWith(":root") ||
|
|
112
|
+
block.startsWith("*,") ||
|
|
113
|
+
block.startsWith("html") ||
|
|
114
|
+
block.startsWith("body") ||
|
|
115
|
+
block.startsWith("@layer theme,")
|
|
88
116
|
) {
|
|
89
117
|
return block;
|
|
90
118
|
}
|
|
91
119
|
|
|
92
|
-
if (block.startsWith(
|
|
93
|
-
const firstBrace = block.indexOf(
|
|
94
|
-
const lastBrace = block.lastIndexOf(
|
|
120
|
+
if (block.startsWith("@") && block.includes("{")) {
|
|
121
|
+
const firstBrace = block.indexOf("{");
|
|
122
|
+
const lastBrace = block.lastIndexOf("}");
|
|
95
123
|
|
|
96
124
|
if (firstBrace === -1 || lastBrace === -1) return block;
|
|
97
125
|
|
|
@@ -100,61 +128,96 @@ function purgeBlock(block, usedClasses) {
|
|
|
100
128
|
|
|
101
129
|
const innerBlocks = extractBlocks(innerContent);
|
|
102
130
|
const purgedInner = innerBlocks
|
|
103
|
-
.map(b => purgeBlock(b, usedClasses))
|
|
104
|
-
.filter(b => b.trim() !==
|
|
105
|
-
.join(
|
|
131
|
+
.map((b) => purgeBlock(b, usedClasses))
|
|
132
|
+
.filter((b) => b.trim() !== "")
|
|
133
|
+
.join("\n ");
|
|
106
134
|
|
|
107
|
-
if (!purgedInner.trim()) return
|
|
135
|
+
if (!purgedInner.trim()) return "";
|
|
108
136
|
|
|
109
137
|
return `${wrapperSignature}\n ${purgedInner}\n}`;
|
|
110
138
|
}
|
|
111
139
|
|
|
112
|
-
const selectorPart = block.split(
|
|
113
|
-
if (!selectorPart) return
|
|
140
|
+
const selectorPart = block.split("{")[0];
|
|
141
|
+
if (!selectorPart) return "";
|
|
114
142
|
|
|
115
|
-
const cleanSelectorPart = selectorPart
|
|
116
|
-
|
|
143
|
+
const cleanSelectorPart = selectorPart
|
|
144
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
145
|
+
.trim();
|
|
117
146
|
|
|
118
|
-
const
|
|
119
|
-
|
|
147
|
+
const selectors = cleanSelectorPart.split(",").map((s) => s.trim());
|
|
148
|
+
|
|
149
|
+
const isUsed = selectors.some((selector) => {
|
|
150
|
+
if (!selector.includes(".")) return true;
|
|
120
151
|
|
|
121
152
|
for (const used of usedClasses) {
|
|
122
|
-
const escapedUsed = used
|
|
123
|
-
|
|
153
|
+
const escapedUsed = used
|
|
154
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
155
|
+
.replace(/:/g, "\\\\:");
|
|
156
|
+
|
|
157
|
+
const boundaryRegex = new RegExp(
|
|
158
|
+
`\\.${escapedUsed}(?::[\\w\\-]+|[\\s,>+~]|$)`,
|
|
159
|
+
);
|
|
160
|
+
|
|
124
161
|
if (boundaryRegex.test(selector)) return true;
|
|
125
162
|
}
|
|
163
|
+
|
|
126
164
|
return false;
|
|
127
165
|
});
|
|
128
166
|
|
|
129
|
-
return isUsed ? block :
|
|
167
|
+
return isUsed ? block : "";
|
|
130
168
|
}
|
|
131
169
|
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
170
|
+
function getFilesForPurge(scanDir, config, extensions) {
|
|
171
|
+
if (config?.purge?.sourceGlobs && config.purge.sourceGlobs.length) {
|
|
172
|
+
const fg = require("fast-glob");
|
|
173
|
+
|
|
174
|
+
console.log(`\n🔍 Scanning using sourceGlobs`);
|
|
175
|
+
config.purge.sourceGlobs.forEach((glob) => console.log(` - ${glob}`));
|
|
176
|
+
console.log(` Extensions: ${extensions.join(", ")}`);
|
|
136
177
|
|
|
137
|
-
|
|
138
|
-
|
|
178
|
+
return fg.sync(config.purge.sourceGlobs, {
|
|
179
|
+
ignore: config.purge.ignore || [],
|
|
180
|
+
onlyFiles: true,
|
|
181
|
+
unique: true,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
139
184
|
|
|
140
|
-
|
|
185
|
+
console.log(`\n🔍 Scanning fallback directory: ${scanDir}`);
|
|
186
|
+
console.log(` Extensions: ${extensions.join(", ")}`);
|
|
141
187
|
|
|
142
|
-
|
|
188
|
+
return getAllFiles(scanDir, extensions);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function printFileSummary(files, extensions) {
|
|
143
192
|
const countsByExt = {};
|
|
144
|
-
|
|
193
|
+
|
|
194
|
+
for (const ext of extensions) {
|
|
195
|
+
countsByExt[ext] = 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
145
198
|
for (const file of files) {
|
|
146
|
-
const ext = extensions.find(e => file.endsWith(e)) ||
|
|
199
|
+
const ext = extensions.find((e) => file.endsWith(e)) || "other";
|
|
147
200
|
countsByExt[ext] = (countsByExt[ext] || 0) + 1;
|
|
148
201
|
}
|
|
202
|
+
|
|
149
203
|
const extSummary = Object.entries(countsByExt)
|
|
150
204
|
.filter(([, count]) => count > 0)
|
|
151
205
|
.map(([ext, count]) => `${count} ${ext}`)
|
|
152
|
-
.join(
|
|
153
|
-
|
|
206
|
+
.join(", ");
|
|
207
|
+
|
|
208
|
+
console.log(` Found: ${files.length === 0 ? "no source files" : extSummary}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function purgeCSS(css, scanDir, config) {
|
|
212
|
+
const extensions = config?.purge?.extensions || DEFAULT_EXTENSIONS;
|
|
213
|
+
const files = getFilesForPurge(scanDir, config, extensions);
|
|
214
|
+
|
|
215
|
+
printFileSummary(files, extensions);
|
|
154
216
|
|
|
155
217
|
if (files.length === 0) {
|
|
156
|
-
console.warn(
|
|
157
|
-
|
|
218
|
+
console.warn(
|
|
219
|
+
" ⚠️ No template/source files found. Check your purge.sourceGlobs or purge.sourceDir.",
|
|
220
|
+
);
|
|
158
221
|
return css;
|
|
159
222
|
}
|
|
160
223
|
|
|
@@ -162,23 +225,23 @@ function purgeCSS(css, scanDir, config) {
|
|
|
162
225
|
|
|
163
226
|
for (const file of files) {
|
|
164
227
|
try {
|
|
165
|
-
const content = fs.readFileSync(file,
|
|
228
|
+
const content = fs.readFileSync(file, "utf8");
|
|
166
229
|
const classes = extractClassNames(content);
|
|
167
|
-
classes.forEach(cls => usedClasses.add(cls));
|
|
230
|
+
classes.forEach((cls) => usedClasses.add(cls));
|
|
168
231
|
} catch (err) {
|
|
169
232
|
console.warn(` ⚠️ Could not read ${file}: ${err.message}`);
|
|
170
233
|
}
|
|
171
234
|
}
|
|
172
235
|
|
|
173
|
-
console.log(` Extracted ${usedClasses.size} unique class names
|
|
236
|
+
console.log(` Extracted ${usedClasses.size} unique class names`);
|
|
174
237
|
|
|
175
238
|
const blocks = extractBlocks(css);
|
|
176
239
|
const purgedBlocks = blocks
|
|
177
|
-
.map(block => purgeBlock(block, usedClasses))
|
|
178
|
-
.filter(block => block.trim() !==
|
|
240
|
+
.map((block) => purgeBlock(block, usedClasses))
|
|
241
|
+
.filter((block) => block.trim() !== "");
|
|
242
|
+
|
|
243
|
+
const purgedCss = purgedBlocks.join("\n\n");
|
|
179
244
|
|
|
180
|
-
const purgedCss = purgedBlocks.join('\n\n');
|
|
181
|
-
|
|
182
245
|
const beforeSize = (css.length / 1024).toFixed(2);
|
|
183
246
|
const afterSize = (purgedCss.length / 1024).toFixed(2);
|
|
184
247
|
const reduction = (((css.length - purgedCss.length) / css.length) * 100).toFixed(1);
|
|
@@ -191,4 +254,8 @@ function purgeCSS(css, scanDir, config) {
|
|
|
191
254
|
return purgedCss;
|
|
192
255
|
}
|
|
193
256
|
|
|
194
|
-
module.exports = {
|
|
257
|
+
module.exports = {
|
|
258
|
+
purgeCSS,
|
|
259
|
+
getAllFiles,
|
|
260
|
+
extractClassNames,
|
|
261
|
+
};
|
package/src/showcase.js
CHANGED
|
@@ -1,90 +1,135 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
"use strict";
|
|
4
4
|
|
|
5
|
-
const http = require(
|
|
6
|
-
const fs = require(
|
|
7
|
-
const path = require(
|
|
8
|
-
const { exec } = require(
|
|
5
|
+
const http = require("http");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { exec } = require("child_process");
|
|
9
9
|
|
|
10
10
|
const PORT = 3456;
|
|
11
11
|
const ROOT = process.cwd();
|
|
12
|
+
const PACKAGE_ROOT = path.join(__dirname, "..");
|
|
13
|
+
|
|
14
|
+
const TEMPLATE_SHOWCASE = path.join(PACKAGE_ROOT, "template", "showcase.html");
|
|
15
|
+
const PROJECT_SHOWCASE = path.join(ROOT, "showcase.html");
|
|
12
16
|
|
|
13
17
|
const MIME = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
".html": "text/html; charset=utf-8",
|
|
19
|
+
".css": "text/css",
|
|
20
|
+
".js": "application/javascript",
|
|
21
|
+
".png": "image/png",
|
|
22
|
+
".jpg": "image/jpeg",
|
|
23
|
+
".svg": "image/svg+xml",
|
|
24
|
+
".ico": "image/x-icon",
|
|
21
25
|
};
|
|
22
26
|
|
|
27
|
+
const configPath = path.join(ROOT, "emily.config.json");
|
|
28
|
+
|
|
29
|
+
let cssPath = path.join(ROOT, "dist/emily.min.css");
|
|
30
|
+
let cssDisplayPath = "dist/emily.min.css";
|
|
31
|
+
|
|
32
|
+
if (fs.existsSync(configPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
35
|
+
|
|
36
|
+
if (config.output && config.output.css) {
|
|
37
|
+
cssPath = path.join(ROOT, config.output.css);
|
|
38
|
+
cssDisplayPath = config.output.css;
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
// Ensure CSS is built before serving
|
|
24
|
-
const cssPath = path.join(ROOT, 'dist/emily.min.css');
|
|
25
44
|
if (!fs.existsSync(cssPath)) {
|
|
26
|
-
console.log(
|
|
27
|
-
require(
|
|
45
|
+
console.log(" Building CSS first...\n");
|
|
46
|
+
require("./index.js").build({ keepFull: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Ensure showcase.html exists in the consuming project
|
|
50
|
+
if (!fs.existsSync(PROJECT_SHOWCASE)) {
|
|
51
|
+
if (!fs.existsSync(TEMPLATE_SHOWCASE)) {
|
|
52
|
+
console.error("\n Could not find bundled showcase template.");
|
|
53
|
+
console.error(" Expected: " + TEMPLATE_SHOWCASE + "\n");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let showcaseHtml = fs.readFileSync(TEMPLATE_SHOWCASE, "utf8");
|
|
58
|
+
|
|
59
|
+
showcaseHtml = showcaseHtml.replace(
|
|
60
|
+
/<link rel="stylesheet" href="[^"]*">/,
|
|
61
|
+
`<link rel="stylesheet" href="./${cssDisplayPath}">`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
fs.writeFileSync(PROJECT_SHOWCASE, showcaseHtml);
|
|
65
|
+
console.log(" Created showcase.html from EmilyCSS template.\n");
|
|
28
66
|
}
|
|
29
67
|
|
|
30
68
|
const server = http.createServer((req, res) => {
|
|
31
|
-
let urlPath = req.url.split(
|
|
32
|
-
|
|
69
|
+
let urlPath = req.url.split("?")[0];
|
|
70
|
+
|
|
71
|
+
if (urlPath === "/") {
|
|
72
|
+
urlPath = "/showcase.html";
|
|
73
|
+
}
|
|
33
74
|
|
|
34
75
|
const filePath = path.join(ROOT, urlPath);
|
|
35
76
|
|
|
36
|
-
// Safety: don't serve files outside ROOT
|
|
37
77
|
if (!filePath.startsWith(ROOT)) {
|
|
38
78
|
res.writeHead(403);
|
|
39
|
-
res.end(
|
|
79
|
+
res.end("Forbidden");
|
|
40
80
|
return;
|
|
41
81
|
}
|
|
42
82
|
|
|
43
83
|
fs.readFile(filePath, (err, data) => {
|
|
44
84
|
if (err) {
|
|
45
|
-
res.writeHead(404, {
|
|
85
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
46
86
|
res.end(`Not found: ${urlPath}`);
|
|
47
87
|
return;
|
|
48
88
|
}
|
|
49
89
|
|
|
50
90
|
const ext = path.extname(filePath).toLowerCase();
|
|
51
|
-
const mime = MIME[ext] ||
|
|
91
|
+
const mime = MIME[ext] || "text/plain";
|
|
52
92
|
|
|
53
|
-
res.writeHead(200, {
|
|
93
|
+
res.writeHead(200, { "Content-Type": mime });
|
|
54
94
|
res.end(data);
|
|
55
95
|
});
|
|
56
96
|
});
|
|
57
97
|
|
|
58
|
-
server.listen(PORT,
|
|
98
|
+
server.listen(PORT, "127.0.0.1", () => {
|
|
59
99
|
const url = `http://localhost:${PORT}`;
|
|
60
100
|
|
|
61
|
-
console.log(
|
|
62
|
-
console.log(
|
|
63
|
-
console.log(
|
|
101
|
+
console.log("");
|
|
102
|
+
console.log(" emilyCSS showcase");
|
|
103
|
+
console.log(" " + "─".repeat(30));
|
|
64
104
|
console.log(` Local: ${url}`);
|
|
65
|
-
console.log(
|
|
66
|
-
console.log(
|
|
67
|
-
console.log(
|
|
105
|
+
console.log(` Serving CSS from: ${cssDisplayPath}`);
|
|
106
|
+
console.log("");
|
|
107
|
+
console.log(" Press Ctrl+C to stop");
|
|
108
|
+
console.log("");
|
|
68
109
|
|
|
69
|
-
// Open in default browser
|
|
70
110
|
const openCmd =
|
|
71
|
-
process.platform ===
|
|
72
|
-
|
|
73
|
-
|
|
111
|
+
process.platform === "win32"
|
|
112
|
+
? `start "" "${url}"`
|
|
113
|
+
: process.platform === "darwin"
|
|
114
|
+
? `open "${url}"`
|
|
115
|
+
: `xdg-open "${url}"`;
|
|
74
116
|
|
|
75
117
|
exec(openCmd, (err) => {
|
|
76
118
|
if (err) {
|
|
77
|
-
console.log(
|
|
119
|
+
console.log(" Could not open browser automatically.");
|
|
78
120
|
console.log(` Open manually: ${url}\n`);
|
|
79
121
|
}
|
|
80
122
|
});
|
|
81
123
|
});
|
|
82
124
|
|
|
83
|
-
server.on(
|
|
84
|
-
if (err.code ===
|
|
85
|
-
console.error(
|
|
125
|
+
server.on("error", (err) => {
|
|
126
|
+
if (err.code === "EADDRINUSE") {
|
|
127
|
+
console.error(
|
|
128
|
+
`\n Port ${PORT} is already in use. Stop the other process and try again.\n`,
|
|
129
|
+
);
|
|
86
130
|
} else {
|
|
87
|
-
console.error(
|
|
131
|
+
console.error("\n Server error:", err.message, "\n");
|
|
88
132
|
}
|
|
133
|
+
|
|
89
134
|
process.exit(1);
|
|
90
|
-
});
|
|
135
|
+
});
|