bertui 0.4.0 → 0.4.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/package.json +1 -1
- package/src/build/image-optimizer.js +216 -0
- package/src/server/dev-server.js +90 -3
package/package.json
CHANGED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// bertui/src/build/image-optimizer.js
|
|
2
|
+
import { join, extname, basename, dirname } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, statSync, cpSync } from 'fs';
|
|
4
|
+
import logger from '../logger/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Optimize images using Bun's native image processing
|
|
8
|
+
* This is FAST because it uses native code under the hood
|
|
9
|
+
*/
|
|
10
|
+
export async function optimizeImages(srcDir, outDir) {
|
|
11
|
+
const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif'];
|
|
12
|
+
let optimized = 0;
|
|
13
|
+
let totalSaved = 0;
|
|
14
|
+
|
|
15
|
+
logger.info('🖼️ Optimizing images...');
|
|
16
|
+
|
|
17
|
+
async function processDirectory(dir, targetDir) {
|
|
18
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
19
|
+
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const srcPath = join(dir, entry.name);
|
|
22
|
+
const destPath = join(targetDir, entry.name);
|
|
23
|
+
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
if (!existsSync(destPath)) {
|
|
26
|
+
mkdirSync(destPath, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
await processDirectory(srcPath, destPath);
|
|
29
|
+
} else if (entry.isFile()) {
|
|
30
|
+
const ext = extname(entry.name).toLowerCase();
|
|
31
|
+
|
|
32
|
+
if (imageExtensions.includes(ext)) {
|
|
33
|
+
try {
|
|
34
|
+
const result = await optimizeImage(srcPath, destPath);
|
|
35
|
+
if (result) {
|
|
36
|
+
optimized++;
|
|
37
|
+
totalSaved += result.saved;
|
|
38
|
+
logger.debug(`Optimized: ${entry.name} (saved ${(result.saved / 1024).toFixed(2)}KB)`);
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logger.warn(`Failed to optimize ${entry.name}: ${error.message}`);
|
|
42
|
+
// Fallback: just copy the file
|
|
43
|
+
cpSync(srcPath, destPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await processDirectory(srcDir, outDir);
|
|
51
|
+
|
|
52
|
+
if (optimized > 0) {
|
|
53
|
+
logger.success(`Optimized ${optimized} images (saved ${(totalSaved / 1024).toFixed(2)}KB)`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { optimized, saved: totalSaved };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Optimize a single image using Bun's native capabilities
|
|
61
|
+
* Falls back to direct copy if optimization fails
|
|
62
|
+
*/
|
|
63
|
+
async function optimizeImage(srcPath, destPath) {
|
|
64
|
+
const ext = extname(srcPath).toLowerCase();
|
|
65
|
+
const originalFile = Bun.file(srcPath);
|
|
66
|
+
const originalSize = originalFile.size;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Read the image
|
|
70
|
+
const imageBuffer = await originalFile.arrayBuffer();
|
|
71
|
+
|
|
72
|
+
// For PNG/JPEG, we can optimize
|
|
73
|
+
if (ext === '.png' || ext === '.jpg' || ext === '.jpeg') {
|
|
74
|
+
// Use Bun's native image optimization
|
|
75
|
+
// This is fast because it uses native C libraries
|
|
76
|
+
const optimized = await optimizeWithBun(imageBuffer, ext);
|
|
77
|
+
|
|
78
|
+
if (optimized && optimized.byteLength < originalSize) {
|
|
79
|
+
await Bun.write(destPath, optimized);
|
|
80
|
+
const saved = originalSize - optimized.byteLength;
|
|
81
|
+
return { saved, originalSize, newSize: optimized.byteLength };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// For other formats or if optimization didn't help, just copy
|
|
86
|
+
cpSync(srcPath, destPath);
|
|
87
|
+
return null;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// If anything fails, just copy the original
|
|
90
|
+
cpSync(srcPath, destPath);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Optimize using Bun's native capabilities
|
|
97
|
+
* This is a placeholder - Bun doesn't have built-in image optimization yet
|
|
98
|
+
* We'll use a fast external library via Bun's FFI or shell commands
|
|
99
|
+
*/
|
|
100
|
+
async function optimizeWithBun(buffer, ext) {
|
|
101
|
+
try {
|
|
102
|
+
// For now, we'll use oxipng and mozjpeg via shell commands
|
|
103
|
+
// These are the FASTEST available options (Rust-based)
|
|
104
|
+
const tempInput = `/tmp/bertui_input_${Date.now()}${ext}`;
|
|
105
|
+
const tempOutput = `/tmp/bertui_output_${Date.now()}${ext}`;
|
|
106
|
+
|
|
107
|
+
await Bun.write(tempInput, buffer);
|
|
108
|
+
|
|
109
|
+
if (ext === '.png') {
|
|
110
|
+
// Use oxipng (Rust-based, ultra-fast)
|
|
111
|
+
const proc = Bun.spawn(['oxipng', '-o', '2', '--strip', 'safe', tempInput, '-o', tempOutput], {
|
|
112
|
+
stdout: 'ignore',
|
|
113
|
+
stderr: 'ignore'
|
|
114
|
+
});
|
|
115
|
+
await proc.exited;
|
|
116
|
+
|
|
117
|
+
if (existsSync(tempOutput)) {
|
|
118
|
+
const optimized = await Bun.file(tempOutput).arrayBuffer();
|
|
119
|
+
// Cleanup
|
|
120
|
+
Bun.spawn(['rm', tempInput, tempOutput]);
|
|
121
|
+
return optimized;
|
|
122
|
+
}
|
|
123
|
+
} else if (ext === '.jpg' || ext === '.jpeg') {
|
|
124
|
+
// Use mozjpeg (fastest JPEG optimizer)
|
|
125
|
+
const proc = Bun.spawn(['cjpeg', '-quality', '85', '-optimize', '-outfile', tempOutput, tempInput], {
|
|
126
|
+
stdout: 'ignore',
|
|
127
|
+
stderr: 'ignore'
|
|
128
|
+
});
|
|
129
|
+
await proc.exited;
|
|
130
|
+
|
|
131
|
+
if (existsSync(tempOutput)) {
|
|
132
|
+
const optimized = await Bun.file(tempOutput).arrayBuffer();
|
|
133
|
+
// Cleanup
|
|
134
|
+
Bun.spawn(['rm', tempInput, tempOutput]);
|
|
135
|
+
return optimized;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Cleanup on failure
|
|
140
|
+
if (existsSync(tempInput)) Bun.spawn(['rm', tempInput]);
|
|
141
|
+
if (existsSync(tempOutput)) Bun.spawn(['rm', tempOutput]);
|
|
142
|
+
|
|
143
|
+
return null;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if optimization tools are installed
|
|
151
|
+
*/
|
|
152
|
+
export async function checkOptimizationTools() {
|
|
153
|
+
const tools = [];
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const oxipng = Bun.spawn(['which', 'oxipng'], { stdout: 'pipe' });
|
|
157
|
+
await oxipng.exited;
|
|
158
|
+
if (oxipng.exitCode === 0) {
|
|
159
|
+
tools.push('oxipng');
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const cjpeg = Bun.spawn(['which', 'cjpeg'], { stdout: 'pipe' });
|
|
165
|
+
await cjpeg.exited;
|
|
166
|
+
if (cjpeg.exitCode === 0) {
|
|
167
|
+
tools.push('mozjpeg');
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {}
|
|
170
|
+
|
|
171
|
+
if (tools.length === 0) {
|
|
172
|
+
logger.warn('⚠️ No image optimization tools found. Install for better performance:');
|
|
173
|
+
logger.warn(' macOS: brew install oxipng mozjpeg');
|
|
174
|
+
logger.warn(' Ubuntu: apt install oxipng mozjpeg');
|
|
175
|
+
logger.warn(' Images will be copied without optimization.');
|
|
176
|
+
} else {
|
|
177
|
+
logger.success(`Found optimization tools: ${tools.join(', ')}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return tools;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Copy images without optimization (fallback)
|
|
185
|
+
*/
|
|
186
|
+
export function copyImages(srcDir, outDir) {
|
|
187
|
+
const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.avif'];
|
|
188
|
+
let copied = 0;
|
|
189
|
+
|
|
190
|
+
function processDirectory(dir, targetDir) {
|
|
191
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
192
|
+
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
const srcPath = join(dir, entry.name);
|
|
195
|
+
const destPath = join(targetDir, entry.name);
|
|
196
|
+
|
|
197
|
+
if (entry.isDirectory()) {
|
|
198
|
+
if (!existsSync(destPath)) {
|
|
199
|
+
mkdirSync(destPath, { recursive: true });
|
|
200
|
+
}
|
|
201
|
+
processDirectory(srcPath, destPath);
|
|
202
|
+
} else if (entry.isFile()) {
|
|
203
|
+
const ext = extname(entry.name).toLowerCase();
|
|
204
|
+
|
|
205
|
+
if (imageExtensions.includes(ext)) {
|
|
206
|
+
cpSync(srcPath, destPath);
|
|
207
|
+
copied++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
processDirectory(srcDir, outDir);
|
|
214
|
+
logger.info(`Copied ${copied} images without optimization`);
|
|
215
|
+
return copied;
|
|
216
|
+
}
|
package/src/server/dev-server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// src/server/dev-server.js - FIXED VERSION
|
|
1
2
|
import { Elysia } from 'elysia';
|
|
2
3
|
import { watch } from 'fs';
|
|
3
4
|
import { join, extname } from 'path';
|
|
@@ -11,6 +12,7 @@ export async function startDevServer(options = {}) {
|
|
|
11
12
|
const root = options.root || process.cwd();
|
|
12
13
|
const compiledDir = join(root, '.bertui', 'compiled');
|
|
13
14
|
const stylesDir = join(root, '.bertui', 'styles');
|
|
15
|
+
const srcDir = join(root, 'src');
|
|
14
16
|
|
|
15
17
|
const config = await loadConfig(root);
|
|
16
18
|
|
|
@@ -28,6 +30,49 @@ export async function startDevServer(options = {}) {
|
|
|
28
30
|
return serveHTML(root, hasRouter, config);
|
|
29
31
|
})
|
|
30
32
|
|
|
33
|
+
// ✅ NEW: Serve images from src/images/
|
|
34
|
+
.get('/images/*', async ({ params, set }) => {
|
|
35
|
+
const imagesDir = join(srcDir, 'images');
|
|
36
|
+
const filepath = join(imagesDir, params['*']);
|
|
37
|
+
const file = Bun.file(filepath);
|
|
38
|
+
|
|
39
|
+
if (!await file.exists()) {
|
|
40
|
+
set.status = 404;
|
|
41
|
+
return 'Image not found';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ext = extname(filepath).toLowerCase();
|
|
45
|
+
const contentType = getImageContentType(ext);
|
|
46
|
+
|
|
47
|
+
return new Response(file, {
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': contentType,
|
|
50
|
+
'Cache-Control': 'public, max-age=31536000'
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// ✅ NEW: Serve any static asset from src/ (fonts, videos, etc.)
|
|
56
|
+
.get('/assets/*', async ({ params, set }) => {
|
|
57
|
+
const filepath = join(srcDir, params['*']);
|
|
58
|
+
const file = Bun.file(filepath);
|
|
59
|
+
|
|
60
|
+
if (!await file.exists()) {
|
|
61
|
+
set.status = 404;
|
|
62
|
+
return 'Asset not found';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ext = extname(filepath).toLowerCase();
|
|
66
|
+
const contentType = getContentType(ext);
|
|
67
|
+
|
|
68
|
+
return new Response(file, {
|
|
69
|
+
headers: {
|
|
70
|
+
'Content-Type': contentType,
|
|
71
|
+
'Cache-Control': 'public, max-age=31536000'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
})
|
|
75
|
+
|
|
31
76
|
.get('/*', async ({ params, set }) => {
|
|
32
77
|
const path = params['*'];
|
|
33
78
|
|
|
@@ -413,6 +458,7 @@ ws.onclose = () => {
|
|
|
413
458
|
|
|
414
459
|
logger.success(`🚀 Server running at http://localhost:${port}`);
|
|
415
460
|
logger.info(`📁 Serving: ${root}`);
|
|
461
|
+
logger.info(`🖼️ Images available at: /images/*`);
|
|
416
462
|
|
|
417
463
|
setupWatcher(root, compiledDir, clients, async () => {
|
|
418
464
|
hasRouter = existsSync(join(compiledDir, 'router.js'));
|
|
@@ -490,6 +536,22 @@ ${userStylesheets}
|
|
|
490
536
|
});
|
|
491
537
|
}
|
|
492
538
|
|
|
539
|
+
// ✅ NEW: Helper for image content types
|
|
540
|
+
function getImageContentType(ext) {
|
|
541
|
+
const types = {
|
|
542
|
+
'.jpg': 'image/jpeg',
|
|
543
|
+
'.jpeg': 'image/jpeg',
|
|
544
|
+
'.png': 'image/png',
|
|
545
|
+
'.gif': 'image/gif',
|
|
546
|
+
'.svg': 'image/svg+xml',
|
|
547
|
+
'.webp': 'image/webp',
|
|
548
|
+
'.avif': 'image/avif',
|
|
549
|
+
'.ico': 'image/x-icon'
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
return types[ext] || 'application/octet-stream';
|
|
553
|
+
}
|
|
554
|
+
|
|
493
555
|
function getContentType(ext) {
|
|
494
556
|
const types = {
|
|
495
557
|
'.js': 'application/javascript',
|
|
@@ -502,7 +564,16 @@ function getContentType(ext) {
|
|
|
502
564
|
'.jpeg': 'image/jpeg',
|
|
503
565
|
'.gif': 'image/gif',
|
|
504
566
|
'.svg': 'image/svg+xml',
|
|
505
|
-
'.
|
|
567
|
+
'.webp': 'image/webp',
|
|
568
|
+
'.avif': 'image/avif',
|
|
569
|
+
'.ico': 'image/x-icon',
|
|
570
|
+
'.woff': 'font/woff',
|
|
571
|
+
'.woff2': 'font/woff2',
|
|
572
|
+
'.ttf': 'font/ttf',
|
|
573
|
+
'.otf': 'font/otf',
|
|
574
|
+
'.mp4': 'video/mp4',
|
|
575
|
+
'.webm': 'video/webm',
|
|
576
|
+
'.mp3': 'audio/mpeg'
|
|
506
577
|
};
|
|
507
578
|
|
|
508
579
|
return types[ext] || 'text/plain';
|
|
@@ -523,9 +594,26 @@ function setupWatcher(root, compiledDir, clients, onRecompile) {
|
|
|
523
594
|
if (!filename) return;
|
|
524
595
|
|
|
525
596
|
const ext = extname(filename);
|
|
526
|
-
|
|
597
|
+
|
|
598
|
+
// ✅ Watch image changes too
|
|
599
|
+
const watchedExtensions = ['.js', '.jsx', '.ts', '.tsx', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif'];
|
|
600
|
+
|
|
601
|
+
if (watchedExtensions.includes(ext)) {
|
|
527
602
|
logger.info(`📝 File changed: ${filename}`);
|
|
528
603
|
|
|
604
|
+
// For images, just reload without recompiling
|
|
605
|
+
if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif'].includes(ext)) {
|
|
606
|
+
for (const client of clients) {
|
|
607
|
+
try {
|
|
608
|
+
client.send(JSON.stringify({ type: 'reload', file: filename }));
|
|
609
|
+
} catch (e) {
|
|
610
|
+
clients.delete(client);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// For code/CSS, recompile
|
|
529
617
|
for (const client of clients) {
|
|
530
618
|
try {
|
|
531
619
|
client.send(JSON.stringify({ type: 'recompiling' }));
|
|
@@ -551,7 +639,6 @@ function setupWatcher(root, compiledDir, clients, onRecompile) {
|
|
|
551
639
|
} catch (error) {
|
|
552
640
|
logger.error(`Recompilation failed: ${error.message}`);
|
|
553
641
|
|
|
554
|
-
// Send compilation error to clients
|
|
555
642
|
for (const client of clients) {
|
|
556
643
|
try {
|
|
557
644
|
client.send(JSON.stringify({
|