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/src/purge.js CHANGED
@@ -1,9 +1,31 @@
1
1
  // new version
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- function getAllFiles(dir, extensions = ['.html', '.htm', '.twig', '.njk', '.liquid', '.hbs', '.jsx', '.tsx', '.vue', '.php', '.astro', '.svelte', '.blade.php', '.jinja', '.jinja2', '.j2', '.md']) {
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 (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist') {
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, '').trim();
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
- if (css[i] === '{') {
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(':root') ||
84
- block.startsWith('*,') ||
85
- block.startsWith('html') ||
86
- block.startsWith('body') ||
87
- block.startsWith('@layer theme,') // Keep layer definition
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('@') && block.includes('{')) {
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('\n ');
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('{')[0];
113
- if (!selectorPart) return '';
140
+ const selectorPart = block.split("{")[0];
141
+ if (!selectorPart) return "";
114
142
 
115
- const cleanSelectorPart = selectorPart.replace(/\/\*[\s\S]*?\*\//g, '').trim();
116
- const selectors = cleanSelectorPart.split(',').map(s => s.trim());
143
+ const cleanSelectorPart = selectorPart
144
+ .replace(/\/\*[\s\S]*?\*\//g, "")
145
+ .trim();
117
146
 
118
- const isUsed = selectors.some(selector => {
119
- if (!selector.includes('.')) return true;
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.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/:/g, '\\\\:');
123
- const boundaryRegex = new RegExp(`\\.${escapedUsed}(?::[\\w\\-]+|[\\s,>+~]|$)`);
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 purgeCSS(css, scanDir, config) {
133
- const extensions = config && config.purge && config.purge.extensions
134
- ? config.purge.extensions
135
- : ['.html', '.htm', '.njk', '.liquid', '.hbs', '.jsx', '.tsx', '.vue', '.php', '.astro', '.svelte', '.blade.php'];
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
- console.log(`\n🔍 Scanning for files in: ${scanDir}`);
138
- console.log(` Extensions: ${extensions.join(', ')}`);
178
+ return fg.sync(config.purge.sourceGlobs, {
179
+ ignore: config.purge.ignore || [],
180
+ onlyFiles: true,
181
+ unique: true,
182
+ });
183
+ }
139
184
 
140
- const files = getAllFiles(scanDir, extensions);
185
+ console.log(`\n🔍 Scanning fallback directory: ${scanDir}`);
186
+ console.log(` Extensions: ${extensions.join(", ")}`);
141
187
 
142
- // Show per-extension breakdown so missing extensions are immediately obvious
188
+ return getAllFiles(scanDir, extensions);
189
+ }
190
+
191
+ function printFileSummary(files, extensions) {
143
192
  const countsByExt = {};
144
- for (const ext of extensions) countsByExt[ext] = 0;
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)) || 'other';
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
- console.log(` Found: ${files.length === 0 ? 'no files' : extSummary}`);
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(' ⚠️ No template files found. Check that --purge points to the right directory and extensions are configured.');
157
- console.warn(` Expected extensions: ${extensions.join(', ')}`);
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, 'utf8');
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 from HTML`);
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 = { purgeCSS, getAllFiles, extractClassNames };
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
- 'use strict';
3
+ "use strict";
4
4
 
5
- const http = require('http');
6
- const fs = require('fs');
7
- const path = require('path');
8
- const { exec } = require('child_process');
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
- '.html': 'text/html; charset=utf-8',
15
- '.css': 'text/css',
16
- '.js': 'application/javascript',
17
- '.png': 'image/png',
18
- '.jpg': 'image/jpeg',
19
- '.svg': 'image/svg+xml',
20
- '.ico': 'image/x-icon',
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(' Building CSS first...\n');
27
- require('./index.js').build({ keepFull: true });
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('?')[0]; // strip query strings
32
- if (urlPath === '/') urlPath = '/showcase.html';
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('Forbidden');
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, { 'Content-Type': 'text/plain' });
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] || 'text/plain';
91
+ const mime = MIME[ext] || "text/plain";
52
92
 
53
- res.writeHead(200, { 'Content-Type': mime });
93
+ res.writeHead(200, { "Content-Type": mime });
54
94
  res.end(data);
55
95
  });
56
96
  });
57
97
 
58
- server.listen(PORT, '127.0.0.1', () => {
98
+ server.listen(PORT, "127.0.0.1", () => {
59
99
  const url = `http://localhost:${PORT}`;
60
100
 
61
- console.log('');
62
- console.log(' emilyCSS showcase');
63
- console.log(' ' + ''.repeat(30));
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(' Press Ctrl+C to stop');
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 === 'win32' ? `start "" "${url}"` :
72
- process.platform === 'darwin' ? `open "${url}"` :
73
- `xdg-open "${url}"`;
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(` Could not open browser automatically.`);
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('error', (err) => {
84
- if (err.code === 'EADDRINUSE') {
85
- console.error(`\n Port ${PORT} is already in use. Stop the other process and try again.\n`);
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('\n Server error:', err.message, '\n');
131
+ console.error("\n Server error:", err.message, "\n");
88
132
  }
133
+
89
134
  process.exit(1);
90
- });
135
+ });