emily-css 1.0.8 → 1.0.14

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-cmd.js CHANGED
@@ -30,7 +30,7 @@ function runPurge() {
30
30
  console.log('\nPurging unused utilities from ' + sourceDir + '...');
31
31
 
32
32
  const css = fs.readFileSync(cssPath, 'utf8');
33
- const purged = purgeCSS(css, sourceDir);
33
+ const purged = purgeCSS(css, sourceDir, config);
34
34
  const minified = purged
35
35
  .replace(/\/\*[\s\S]*?\*\//g, '')
36
36
  .replace(/\s+/g, ' ')
@@ -52,4 +52,4 @@ function runPurge() {
52
52
  console.log('\n ' + Math.round(original / 1024) + 'KB -> ' + Math.round(purgedSize / 1024) + 'KB (' + reduction + '% reduction)\n');
53
53
  }
54
54
 
55
- runPurge();
55
+ runPurge();
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const http = require('http');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { exec } = require('child_process');
9
+
10
+ const PORT = 3456;
11
+ const ROOT = process.cwd();
12
+
13
+ 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',
21
+ };
22
+
23
+ // Ensure CSS is built before serving
24
+ const cssPath = path.join(ROOT, 'dist/emily.min.css');
25
+ if (!fs.existsSync(cssPath)) {
26
+ console.log(' Building CSS first...\n');
27
+ require('./index.js').build({ keepFull: true });
28
+ }
29
+
30
+ const server = http.createServer((req, res) => {
31
+ let urlPath = req.url.split('?')[0]; // strip query strings
32
+ if (urlPath === '/') urlPath = '/showcase.html';
33
+
34
+ const filePath = path.join(ROOT, urlPath);
35
+
36
+ // Safety: don't serve files outside ROOT
37
+ if (!filePath.startsWith(ROOT)) {
38
+ res.writeHead(403);
39
+ res.end('Forbidden');
40
+ return;
41
+ }
42
+
43
+ fs.readFile(filePath, (err, data) => {
44
+ if (err) {
45
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
46
+ res.end(`Not found: ${urlPath}`);
47
+ return;
48
+ }
49
+
50
+ const ext = path.extname(filePath).toLowerCase();
51
+ const mime = MIME[ext] || 'text/plain';
52
+
53
+ res.writeHead(200, { 'Content-Type': mime });
54
+ res.end(data);
55
+ });
56
+ });
57
+
58
+ server.listen(PORT, '127.0.0.1', () => {
59
+ const url = `http://localhost:${PORT}`;
60
+
61
+ console.log('');
62
+ console.log(' emilyCSS showcase');
63
+ console.log(' ' + '─'.repeat(30));
64
+ console.log(` Local: ${url}`);
65
+ console.log('');
66
+ console.log(' Press Ctrl+C to stop');
67
+ console.log('');
68
+
69
+ // Open in default browser
70
+ const openCmd =
71
+ process.platform === 'win32' ? `start "" "${url}"` :
72
+ process.platform === 'darwin' ? `open "${url}"` :
73
+ `xdg-open "${url}"`;
74
+
75
+ exec(openCmd, (err) => {
76
+ if (err) {
77
+ console.log(` Could not open browser automatically.`);
78
+ console.log(` Open manually: ${url}\n`);
79
+ }
80
+ });
81
+ });
82
+
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`);
86
+ } else {
87
+ console.error('\n Server error:', err.message, '\n');
88
+ }
89
+ process.exit(1);
90
+ });
package/src/watch.js ADDED
@@ -0,0 +1,193 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chokidar = require('chokidar');
4
+ const chalk = require('chalk');
5
+ const { buildFullFramework, buildProductionCss, ensureFullFramework } = require('./index.js');
6
+ const { getAllFiles, extractClassNames } = require('./purge.js');
7
+
8
+ let isRunning = false;
9
+ let pendingRun = false;
10
+ let previousClasses = new Set();
11
+ let hasRunOnce = false;
12
+
13
+ function readConfig() {
14
+ const configPath = path.join(process.cwd(), 'emily.config.json');
15
+
16
+ if (!fs.existsSync(configPath)) {
17
+ console.error('\n emily-css: No config found. Run "emily-css init" first.\n');
18
+ process.exit(1);
19
+ }
20
+
21
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
22
+ }
23
+
24
+ function shouldIgnore(filePath) {
25
+ const normalised = filePath.replace(/\\/g, '/');
26
+
27
+ return [
28
+ 'node_modules/',
29
+ '.git/',
30
+ '.nuxt/',
31
+ '.next/',
32
+ '.output/',
33
+ 'dist/',
34
+ 'build/',
35
+ 'coverage/',
36
+ '.cache/',
37
+ '.vite/'
38
+ ].some(part => normalised.includes('/' + part) || normalised.startsWith(part));
39
+ }
40
+
41
+ function runQuietly(fn) {
42
+ const originalLog = console.log;
43
+ const originalWarn = console.warn;
44
+
45
+ console.log = () => {};
46
+ console.warn = () => {};
47
+
48
+ try {
49
+ return fn();
50
+ } finally {
51
+ console.log = originalLog;
52
+ console.warn = originalWarn;
53
+ }
54
+ }
55
+
56
+ function collectUsedClasses(sourceDir, config) {
57
+ const files = getAllFiles(sourceDir, config.purge?.extensions);
58
+ const usedClasses = new Set();
59
+
60
+ for (const file of files) {
61
+ if (shouldIgnore(file)) continue;
62
+
63
+ try {
64
+ const content = fs.readFileSync(file, 'utf8');
65
+ extractClassNames(content).forEach(cls => usedClasses.add(cls));
66
+ } catch {}
67
+ }
68
+
69
+ return usedClasses;
70
+ }
71
+
72
+ function getClassDiff(currentClasses) {
73
+ const added = [...currentClasses].filter(cls => !previousClasses.has(cls));
74
+ const removed = [...previousClasses].filter(cls => !currentClasses.has(cls));
75
+
76
+ previousClasses = new Set(currentClasses);
77
+
78
+ return { added, removed };
79
+ }
80
+
81
+ function formatClassList(classes) {
82
+ if (classes.length === 0) return '';
83
+
84
+ const shown = classes.slice(0, 8).join(', ');
85
+ const extra = classes.length > 8 ? ' +' + (classes.length - 8) + ' more' : '';
86
+
87
+ return shown + extra;
88
+ }
89
+
90
+ function printSummary({ currentClasses, result, added, removed }) {
91
+ const reduction = (((result.originalSize - result.outputSize) / result.originalSize) * 100).toFixed(1);
92
+ const sizeKb = (result.outputSize / 1024).toFixed(1);
93
+ const time = new Date().toLocaleTimeString();
94
+
95
+ console.log(
96
+ chalk.green('✓ ' + time + ' updated') +
97
+ chalk.gray(' | ' + currentClasses.size + ' classes | ' + sizeKb + ' KB | ' + reduction + '% reduced')
98
+ );
99
+
100
+ if (!hasRunOnce) return;
101
+
102
+ if (removed.length > 0) {
103
+ console.log(chalk.red('− removed ' + removed.length + ' class' + (removed.length === 1 ? '' : 'es')) + chalk.gray(' (' + formatClassList(removed) + ')'));
104
+ }
105
+
106
+ if (added.length > 0) {
107
+ console.log(chalk.green('+ added ' + added.length + ' class' + (added.length === 1 ? '' : 'es')) + chalk.gray(' (' + formatClassList(added) + ')'));
108
+ }
109
+ }
110
+
111
+ function runProductionUpdate(filePath) {
112
+ if (isRunning) {
113
+ pendingRun = true;
114
+ return;
115
+ }
116
+
117
+ isRunning = true;
118
+
119
+ try {
120
+ const config = readConfig();
121
+ const sourceDir = config.purge?.sourceDir || '.';
122
+ const isConfigChange = filePath && filePath.replace(/\\/g, '/').endsWith('emily.config.json');
123
+ const cssPath = path.join(process.cwd(), 'dist/emily.css');
124
+
125
+ if (isConfigChange) {
126
+ runQuietly(() => buildFullFramework());
127
+ } else {
128
+ runQuietly(() => ensureFullFramework());
129
+ }
130
+
131
+ const result = runQuietly(() => buildProductionCss());
132
+ const currentClasses = collectUsedClasses(sourceDir, config);
133
+ const { added, removed } = getClassDiff(currentClasses);
134
+
135
+ printSummary({ currentClasses, result, added, removed });
136
+
137
+ hasRunOnce = true;
138
+ } catch (error) {
139
+ console.error('\n❌ EmilyUI watch failed');
140
+ console.error(error.message);
141
+ } finally {
142
+ isRunning = false;
143
+
144
+ if (pendingRun) {
145
+ pendingRun = false;
146
+ runProductionUpdate();
147
+ }
148
+ }
149
+ }
150
+
151
+ function getWatchPaths(config) {
152
+ return [
153
+ config.purge?.sourceDir || '.',
154
+ 'emily.config.json'
155
+ ];
156
+ }
157
+
158
+ function queueUpdate(filePath) {
159
+ if (filePath && shouldIgnore(filePath)) return;
160
+ runProductionUpdate(filePath);
161
+ }
162
+
163
+ function runWatch() {
164
+ const config = readConfig();
165
+ const watchPaths = getWatchPaths(config);
166
+
167
+ console.log('\n👀 EmilyUI is watching...');
168
+ console.log(chalk.gray(' Watching:'));
169
+ watchPaths.forEach(item => console.log(chalk.gray(' - ' + item)));
170
+
171
+ runQuietly(() => ensureFullFramework());
172
+ runProductionUpdate();
173
+
174
+ const watcher = chokidar.watch(watchPaths, {
175
+ ignored: shouldIgnore,
176
+ ignoreInitial: true,
177
+ awaitWriteFinish: {
178
+ stabilityThreshold: 500,
179
+ pollInterval: 100
180
+ }
181
+ });
182
+
183
+ watcher.on('change', queueUpdate);
184
+ watcher.on('add', queueUpdate);
185
+ watcher.on('unlink', queueUpdate);
186
+
187
+ watcher.on('error', error => {
188
+ console.error('\n❌ EmilyUI watcher error');
189
+ console.error(error.message);
190
+ });
191
+ }
192
+
193
+ runWatch();