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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Lightning-fast React dev server powered by Bun and Elysia",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -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
+ }
@@ -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
- '.ico': 'image/x-icon'
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
- if (['.js', '.jsx', '.ts', '.tsx', '.css'].includes(ext)) {
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({