bertui 0.4.1 → 0.4.4

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.1",
3
+ "version": "0.4.4",
4
4
  "description": "Lightning-fast React dev server powered by Bun and Elysia",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -44,6 +44,9 @@
44
44
  "url": "https://github.com/BunElysiaReact/BERTUI.git"
45
45
  },
46
46
  "dependencies": {
47
+ "@jsquash/jpeg": "^1.6.0",
48
+ "@jsquash/png": "^3.1.1",
49
+ "@jsquash/webp": "^1.5.0",
47
50
  "elysia": "^1.0.0",
48
51
  "ernest-logger": "latest",
49
52
  "lightningcss": "^1.30.2"
@@ -1,18 +1,52 @@
1
- // bertui/src/build/image-optimizer.js
2
- import { join, extname, basename, dirname } from 'path';
3
- import { existsSync, mkdirSync, readdirSync, statSync, cpSync } from 'fs';
1
+ // bertui/src/build/image-optimizer.js - WASM-POWERED VERSION 🚀
2
+ import { join, extname } from 'path';
3
+ import { existsSync, mkdirSync, readdirSync, cpSync } from 'fs';
4
4
  import logger from '../logger/logger.js';
5
5
 
6
6
  /**
7
- * Optimize images using Bun's native image processing
8
- * This is FAST because it uses native code under the hood
7
+ * 🎯 WASM-powered image optimization using @jsquash
8
+ * Zero OS dependencies, pure JavaScript, blazing fast!
9
+ */
10
+
11
+ // Lazy-load WASM modules (only when needed)
12
+ let pngEncode, pngDecode;
13
+ let jpegEncode, jpegDecode;
14
+ let webpEncode, webpDecode;
15
+
16
+ async function initializePNG() {
17
+ if (!pngEncode) {
18
+ const { encode, decode } = await import('@jsquash/png');
19
+ pngEncode = encode;
20
+ pngDecode = decode;
21
+ }
22
+ }
23
+
24
+ async function initializeJPEG() {
25
+ if (!jpegEncode) {
26
+ const { encode, decode } = await import('@jsquash/jpeg');
27
+ jpegEncode = encode;
28
+ jpegDecode = decode;
29
+ }
30
+ }
31
+
32
+ async function initializeWebP() {
33
+ if (!webpEncode) {
34
+ const { encode, decode } = await import('@jsquash/webp');
35
+ webpEncode = encode;
36
+ webpDecode = decode;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Optimize images using WASM-powered codecs
42
+ * This is FAST and has ZERO OS dependencies! 🚀
9
43
  */
10
44
  export async function optimizeImages(srcDir, outDir) {
11
- const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif'];
45
+ const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg'];
12
46
  let optimized = 0;
13
47
  let totalSaved = 0;
14
48
 
15
- logger.info('🖼️ Optimizing images...');
49
+ logger.info('🖼️ Optimizing images with WASM codecs...');
16
50
 
17
51
  async function processDirectory(dir, targetDir) {
18
52
  const entries = readdirSync(dir, { withFileTypes: true });
@@ -35,10 +69,13 @@ export async function optimizeImages(srcDir, outDir) {
35
69
  if (result) {
36
70
  optimized++;
37
71
  totalSaved += result.saved;
38
- logger.debug(`Optimized: ${entry.name} (saved ${(result.saved / 1024).toFixed(2)}KB)`);
72
+ const savedPercent = ((result.saved / result.originalSize) * 100).toFixed(1);
73
+ logger.debug(
74
+ `✨ ${entry.name}: ${(result.originalSize / 1024).toFixed(1)}KB → ${(result.newSize / 1024).toFixed(1)}KB (-${savedPercent}%)`
75
+ );
39
76
  }
40
77
  } catch (error) {
41
- logger.warn(`Failed to optimize ${entry.name}: ${error.message}`);
78
+ logger.warn(`⚠️ Failed to optimize ${entry.name}: ${error.message}`);
42
79
  // Fallback: just copy the file
43
80
  cpSync(srcPath, destPath);
44
81
  }
@@ -50,15 +87,16 @@ export async function optimizeImages(srcDir, outDir) {
50
87
  await processDirectory(srcDir, outDir);
51
88
 
52
89
  if (optimized > 0) {
53
- logger.success(`Optimized ${optimized} images (saved ${(totalSaved / 1024).toFixed(2)}KB)`);
90
+ logger.success(
91
+ `✅ Optimized ${optimized} images (saved ${(totalSaved / 1024).toFixed(2)}KB total)`
92
+ );
54
93
  }
55
94
 
56
95
  return { optimized, saved: totalSaved };
57
96
  }
58
97
 
59
98
  /**
60
- * Optimize a single image using Bun's native capabilities
61
- * Falls back to direct copy if optimization fails
99
+ * Optimize a single image using WASM codecs
62
100
  */
63
101
  async function optimizeImage(srcPath, destPath) {
64
102
  const ext = extname(srcPath).toLowerCase();
@@ -66,118 +104,80 @@ async function optimizeImage(srcPath, destPath) {
66
104
  const originalSize = originalFile.size;
67
105
 
68
106
  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
- }
107
+ // For SVG and GIF, just copy (no optimization needed/supported)
108
+ if (ext === '.svg' || ext === '.gif') {
109
+ cpSync(srcPath, destPath);
110
+ return null;
83
111
  }
84
112
 
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
- }
113
+ // Read the original image
114
+ const originalBuffer = await originalFile.arrayBuffer();
94
115
 
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);
116
+ let optimizedBuffer;
108
117
 
109
118
  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'
119
+ await initializePNG();
120
+
121
+ // Decode → Re-encode with compression
122
+ const imageData = await pngDecode(originalBuffer);
123
+
124
+ // Encode with oxipng-level compression (quality 85, similar to oxipng -o 2)
125
+ optimizedBuffer = await pngEncode(imageData, {
126
+ quality: 85,
127
+ effort: 2 // 0-10, higher = better compression but slower
114
128
  });
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
- }
129
+
123
130
  } 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
- }
131
+ await initializeJPEG();
132
+
133
+ // Decode → Re-encode with quality 85 (mozjpeg-like quality)
134
+ const imageData = await jpegDecode(originalBuffer);
135
+ optimizedBuffer = await jpegEncode(imageData, { quality: 85 });
136
+
137
+ } else if (ext === '.webp') {
138
+ await initializeWebP();
139
+
140
+ // WebP optimization
141
+ const imageData = await webpDecode(originalBuffer);
142
+ optimizedBuffer = await webpEncode(imageData, { quality: 85 });
137
143
  }
138
144
 
139
- // Cleanup on failure
140
- if (existsSync(tempInput)) Bun.spawn(['rm', tempInput]);
141
- if (existsSync(tempOutput)) Bun.spawn(['rm', tempOutput]);
145
+ // Only save if we actually reduced the size
146
+ if (optimizedBuffer && optimizedBuffer.byteLength < originalSize) {
147
+ await Bun.write(destPath, optimizedBuffer);
148
+ const saved = originalSize - optimizedBuffer.byteLength;
149
+ return { saved, originalSize, newSize: optimizedBuffer.byteLength };
150
+ }
142
151
 
152
+ // If optimization didn't help, just copy the original
153
+ cpSync(srcPath, destPath);
143
154
  return null;
155
+
144
156
  } catch (error) {
157
+ // If anything fails, just copy the original
158
+ logger.warn(`Optimization failed for ${srcPath.split('/').pop()}, copying original`);
159
+ cpSync(srcPath, destPath);
145
160
  return null;
146
161
  }
147
162
  }
148
163
 
149
164
  /**
150
- * Check if optimization tools are installed
165
+ * Check if optimization is available (always true with WASM! 🎉)
151
166
  */
152
167
  export async function checkOptimizationTools() {
153
- const tools = [];
154
-
155
168
  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(', ')}`);
169
+ // Try to import the WASM modules
170
+ await import('@jsquash/png');
171
+ await import('@jsquash/jpeg');
172
+ await import('@jsquash/webp');
173
+
174
+ logger.success('✅ WASM image optimization available');
175
+ logger.info('📦 Using @jsquash (zero OS dependencies!)');
176
+ return ['png', 'jpeg', 'webp'];
177
+ } catch (error) {
178
+ logger.error('❌ WASM codecs not installed. Run: bun add @jsquash/png @jsquash/jpeg @jsquash/webp');
179
+ return [];
178
180
  }
179
-
180
- return tools;
181
181
  }
182
182
 
183
183
  /**
@@ -198,7 +198,7 @@ export function copyImages(srcDir, outDir) {
198
198
  if (!existsSync(destPath)) {
199
199
  mkdirSync(destPath, { recursive: true });
200
200
  }
201
- processDirectory(srcPath, destPath);
201
+ processDirectory(srcPath, targetDir);
202
202
  } else if (entry.isFile()) {
203
203
  const ext = extname(entry.name).toLowerCase();
204
204
 
@@ -211,6 +211,6 @@ export function copyImages(srcDir, outDir) {
211
211
  }
212
212
 
213
213
  processDirectory(srcDir, outDir);
214
- logger.info(`Copied ${copied} images without optimization`);
214
+ logger.info(`📋 Copied ${copied} images without optimization`);
215
215
  return copied;
216
216
  }
package/src/build.js CHANGED
@@ -1,8 +1,10 @@
1
+ // src/build.js - FIXED VERSION
1
2
  import { join, relative, basename, extname, dirname } from 'path';
2
3
  import { existsSync, mkdirSync, rmSync, cpSync, readdirSync, statSync } from 'fs';
3
4
  import logger from './logger/logger.js';
4
5
  import { buildCSS } from './build/css-builder.js';
5
6
  import { loadEnvVariables, replaceEnvInCode } from './utils/env.js';
7
+ import { optimizeImages, checkOptimizationTools, copyImages } from './build/image-optimizer.js';
6
8
 
7
9
  export async function buildProduction(options = {}) {
8
10
  const root = options.root || process.cwd();
@@ -38,11 +40,14 @@ export async function buildProduction(options = {}) {
38
40
  logger.info('Step 2: Building CSS with Lightning CSS...');
39
41
  await buildAllCSS(root, outDir);
40
42
 
41
- // ✅ FIX: Copy all static assets from src/ and public/
42
- logger.info('Step 3: Copying static assets...');
43
- await copyAllStaticAssets(root, outDir);
43
+ logger.info('Step 3: Checking image optimization tools...');
44
+ const optimizationTools = await checkOptimizationTools();
44
45
 
45
- logger.info('Step 4: Bundling JavaScript with Bun...');
46
+ logger.info('Step 4: Copying and optimizing static assets...');
47
+ // ✅ FIX 1: Copy images from BOTH src/images/ and public/
48
+ await copyAllStaticAssets(root, outDir, optimizationTools.length > 0);
49
+
50
+ logger.info('Step 5: Bundling JavaScript with Bun...');
46
51
  const buildEntry = join(buildDir, 'main.js');
47
52
 
48
53
  if (!existsSync(buildEntry)) {
@@ -65,9 +70,6 @@ export async function buildProduction(options = {}) {
65
70
  external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
66
71
  define: {
67
72
  'process.env.NODE_ENV': '"production"',
68
- 'process.env.PUBLIC_APP_NAME': JSON.stringify(envVars.PUBLIC_APP_NAME || 'BertUI App'),
69
- 'process.env.PUBLIC_API_URL': JSON.stringify(envVars.PUBLIC_API_URL || ''),
70
- 'process.env.PUBLIC_USERNAME': JSON.stringify(envVars.PUBLIC_USERNAME || ''),
71
73
  ...Object.fromEntries(
72
74
  Object.entries(envVars).map(([key, value]) => [
73
75
  `process.env.${key}`,
@@ -85,7 +87,8 @@ export async function buildProduction(options = {}) {
85
87
 
86
88
  logger.success('JavaScript bundled with tree-shaking');
87
89
 
88
- logger.info('Step 5: Generating SEO-optimized HTML files...');
90
+ logger.info('Step 6: Generating SEO-optimized HTML files...');
91
+ // ✅ FIX 2: Generate HTML for ALL routes including index.html
89
92
  await generateProductionHTML(root, outDir, result, routes);
90
93
 
91
94
  rmSync(buildDir, { recursive: true });
@@ -121,76 +124,45 @@ export async function buildProduction(options = {}) {
121
124
  }
122
125
  }
123
126
 
124
- // ✅ NEW FUNCTION: Copy all static assets
125
- async function copyAllStaticAssets(root, outDir) {
127
+ // ✅ FIX 3: Enhanced asset copying with proper directory structure
128
+ async function copyAllStaticAssets(root, outDir, optimize = true) {
126
129
  const publicDir = join(root, 'public');
127
- const srcDir = join(root, 'src');
130
+ const srcImagesDir = join(root, 'src', 'images');
128
131
 
129
132
  let assetsCopied = 0;
133
+ let assetsOptimized = 0;
130
134
 
131
- // Copy from public/
132
- if (existsSync(publicDir)) {
133
- assetsCopied += await copyStaticAssetsFromDir(publicDir, outDir, 'public');
134
- }
135
+ // Create images directory in dist/
136
+ const distImagesDir = join(outDir, 'images');
137
+ mkdirSync(distImagesDir, { recursive: true });
135
138
 
136
- // Copy static assets from src/ (images, fonts, etc.)
137
- if (existsSync(srcDir)) {
138
- const assetsOutDir = join(outDir, 'assets');
139
- mkdirSync(assetsOutDir, { recursive: true });
140
- assetsCopied += await copyStaticAssetsFromDir(srcDir, assetsOutDir, 'src', true);
139
+ // Copy from public/ to root of dist/
140
+ if (existsSync(publicDir)) {
141
+ logger.info('Copying public/ assets...');
142
+ if (optimize) {
143
+ const result = await optimizeImages(publicDir, outDir);
144
+ assetsOptimized += result.optimized;
145
+ } else {
146
+ assetsCopied += copyImages(publicDir, outDir);
147
+ }
141
148
  }
142
149
 
143
- logger.success(`Copied ${assetsCopied} static assets`);
144
- }
145
-
146
- // NEW FUNCTION: Recursively copy static assets
147
- async function copyStaticAssetsFromDir(sourceDir, targetDir, label, skipStyles = false) {
148
- const staticExtensions = [
149
- '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif', // Images
150
- '.woff', '.woff2', '.ttf', '.otf', '.eot', // Fonts
151
- '.mp4', '.webm', '.ogg', '.mp3', '.wav', // Media
152
- '.pdf', '.zip', '.json', '.xml', '.txt' // Documents
153
- ];
154
-
155
- let copiedCount = 0;
156
-
157
- function copyRecursive(dir, targetBase) {
158
- const entries = readdirSync(dir, { withFileTypes: true });
159
-
160
- for (const entry of entries) {
161
- const srcPath = join(dir, entry.name);
162
- const relativePath = relative(sourceDir, srcPath);
163
- const destPath = join(targetBase, relativePath);
164
-
165
- if (entry.isDirectory()) {
166
- // Skip node_modules, .bertui, etc.
167
- if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
168
- continue;
169
- }
170
-
171
- // Skip styles directory if requested
172
- if (skipStyles && entry.name === 'styles') {
173
- continue;
174
- }
175
-
176
- mkdirSync(destPath, { recursive: true });
177
- copyRecursive(srcPath, targetBase);
178
- } else if (entry.isFile()) {
179
- const ext = extname(entry.name);
180
-
181
- // Copy static assets only
182
- if (staticExtensions.includes(ext.toLowerCase())) {
183
- mkdirSync(dirname(destPath), { recursive: true });
184
- cpSync(srcPath, destPath);
185
- logger.debug(`Copied ${label}/${relativePath}`);
186
- copiedCount++;
187
- }
188
- }
150
+ // FIX: Copy from src/images/ to dist/images/
151
+ if (existsSync(srcImagesDir)) {
152
+ logger.info('Copying src/images/ to dist/images/...');
153
+ if (optimize) {
154
+ const result = await optimizeImages(srcImagesDir, distImagesDir);
155
+ assetsOptimized += result.optimized;
156
+ } else {
157
+ assetsCopied += copyImages(srcImagesDir, distImagesDir);
189
158
  }
190
159
  }
191
160
 
192
- copyRecursive(sourceDir, targetDir);
193
- return copiedCount;
161
+ if (optimize && assetsOptimized > 0) {
162
+ logger.success(`🎨 Optimized ${assetsOptimized} images with WASM codecs`);
163
+ } else {
164
+ logger.success(`📋 Copied ${assetsCopied} static assets`);
165
+ }
194
166
  }
195
167
 
196
168
  async function buildAllCSS(root, outDir) {
@@ -447,7 +419,6 @@ async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
447
419
  code = replaceEnvInCode(code, envVars);
448
420
  code = fixBuildImports(code, srcPath, outPath, root);
449
421
 
450
- // ✅ FIX: Add React import if needed
451
422
  if (usesJSX(code) && !code.includes('import React')) {
452
423
  code = `import React from 'react';\n${code}`;
453
424
  }
@@ -486,7 +457,6 @@ async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
486
457
 
487
458
  let compiled = await transpiler.transform(code);
488
459
 
489
- // ✅ FIX: Add React import if needed
490
460
  if (usesJSX(compiled) && !compiled.includes('import React')) {
491
461
  compiled = `import React from 'react';\n${compiled}`;
492
462
  }
@@ -582,6 +552,7 @@ function extractMetaFromSource(code) {
582
552
  }
583
553
  }
584
554
 
555
+ // ✅ FIX 4: Generate proper HTML files with correct meta tags
585
556
  async function generateProductionHTML(root, outDir, buildResult, routes) {
586
557
  const mainBundle = buildResult.outputs.find(o =>
587
558
  o.path.includes('main') && o.kind === 'entry-point'
@@ -604,18 +575,15 @@ async function generateProductionHTML(root, outDir, buildResult, routes) {
604
575
  ).join('\n');
605
576
  }
606
577
 
578
+ // ✅ Load config for default meta
607
579
  const { loadConfig } = await import('./config/loadConfig.js');
608
580
  const config = await loadConfig(root);
609
581
  const defaultMeta = config.meta || {};
610
582
 
611
583
  logger.info('Generating SEO-optimized HTML files...');
612
584
 
585
+ // ✅ FIX: Generate HTML for ALL routes (including dynamic as fallback)
613
586
  for (const route of routes) {
614
- if (route.type === 'dynamic') {
615
- logger.info(`Skipping dynamic route: ${route.route}`);
616
- continue;
617
- }
618
-
619
587
  const sourceCode = await Bun.file(route.path).text();
620
588
  const pageMeta = extractMetaFromSource(sourceCode);
621
589
  const meta = { ...defaultMeta, ...pageMeta };
@@ -624,7 +592,24 @@ async function generateProductionHTML(root, outDir, buildResult, routes) {
624
592
  logger.info(`Extracted meta for ${route.route}: ${JSON.stringify(pageMeta)}`);
625
593
  }
626
594
 
627
- const html = `<!DOCTYPE html>
595
+ const html = generateHTML(meta, route, bundlePath, userStylesheets);
596
+
597
+ let htmlPath;
598
+ if (route.route === '/') {
599
+ htmlPath = join(outDir, 'index.html');
600
+ } else {
601
+ const routeDir = join(outDir, route.route);
602
+ mkdirSync(routeDir, { recursive: true });
603
+ htmlPath = join(routeDir, 'index.html');
604
+ }
605
+
606
+ await Bun.write(htmlPath, html);
607
+ logger.success(`Generated ${route.route === '/' ? 'index.html' : route.route + '/index.html'}`);
608
+ }
609
+ }
610
+
611
+ function generateHTML(meta, route, bundlePath, userStylesheets) {
612
+ return `<!DOCTYPE html>
628
613
  <html lang="${meta.lang || 'en'}">
629
614
  <head>
630
615
  <meta charset="UTF-8">
@@ -668,17 +653,4 @@ ${userStylesheets}
668
653
  <script type="module" src="/${bundlePath}"></script>
669
654
  </body>
670
655
  </html>`;
671
-
672
- let htmlPath;
673
- if (route.route === '/') {
674
- htmlPath = join(outDir, 'index.html');
675
- } else {
676
- const routeDir = join(outDir, route.route);
677
- mkdirSync(routeDir, { recursive: true });
678
- htmlPath = join(routeDir, 'index.html');
679
- }
680
-
681
- await Bun.write(htmlPath, html);
682
- logger.success(`Generated ${route.route === '/' ? 'index.html' : route.route + '/index.html'} with meta: ${meta.title || 'default'}`);
683
- }
684
656
  }
@@ -1,4 +1,4 @@
1
- // src/server/dev-server.js - FIXED VERSION
1
+ // src/server/dev-server.js - FIXED IMAGE SERVING
2
2
  import { Elysia } from 'elysia';
3
3
  import { watch } from 'fs';
4
4
  import { join, extname } from 'path';
@@ -13,6 +13,7 @@ export async function startDevServer(options = {}) {
13
13
  const compiledDir = join(root, '.bertui', 'compiled');
14
14
  const stylesDir = join(root, '.bertui', 'styles');
15
15
  const srcDir = join(root, 'src');
16
+ const publicDir = join(root, 'public');
16
17
 
17
18
  const config = await loadConfig(root);
18
19
 
@@ -30,10 +31,10 @@ export async function startDevServer(options = {}) {
30
31
  return serveHTML(root, hasRouter, config);
31
32
  })
32
33
 
33
- // ✅ NEW: Serve images from src/images/
34
+ // ✅ FIX: Serve images from src/images/ (CRITICAL)
34
35
  .get('/images/*', async ({ params, set }) => {
35
- const imagesDir = join(srcDir, 'images');
36
- const filepath = join(imagesDir, params['*']);
36
+ const srcImagesDir = join(srcDir, 'images');
37
+ const filepath = join(srcImagesDir, params['*']);
37
38
  const file = Bun.file(filepath);
38
39
 
39
40
  if (!await file.exists()) {
@@ -47,12 +48,27 @@ export async function startDevServer(options = {}) {
47
48
  return new Response(file, {
48
49
  headers: {
49
50
  'Content-Type': contentType,
50
- 'Cache-Control': 'public, max-age=31536000'
51
+ 'Cache-Control': 'no-cache' // Dev server = no cache
51
52
  }
52
53
  });
53
54
  })
54
55
 
55
- // ✅ NEW: Serve any static asset from src/ (fonts, videos, etc.)
56
+ // ✅ Serve from public/ directory
57
+ .get('/public/*', async ({ params, set }) => {
58
+ const filepath = join(publicDir, params['*']);
59
+ const file = Bun.file(filepath);
60
+
61
+ if (!await file.exists()) {
62
+ set.status = 404;
63
+ return 'File not found';
64
+ }
65
+
66
+ return new Response(file, {
67
+ headers: { 'Cache-Control': 'no-cache' }
68
+ });
69
+ })
70
+
71
+ // ✅ Generic asset serving
56
72
  .get('/assets/*', async ({ params, set }) => {
57
73
  const filepath = join(srcDir, params['*']);
58
74
  const file = Bun.file(filepath);
@@ -68,7 +84,7 @@ export async function startDevServer(options = {}) {
68
84
  return new Response(file, {
69
85
  headers: {
70
86
  'Content-Type': contentType,
71
- 'Cache-Control': 'public, max-age=31536000'
87
+ 'Cache-Control': 'no-cache'
72
88
  }
73
89
  });
74
90
  })
@@ -108,16 +124,6 @@ export async function startDevServer(options = {}) {
108
124
  }
109
125
  }
110
126
 
111
- if (path.startsWith('public/')) {
112
- const publicDir = join(root, 'public');
113
- const filepath = join(publicDir, path.replace('public/', ''));
114
- const file = Bun.file(filepath);
115
-
116
- if (await file.exists()) {
117
- return new Response(file);
118
- }
119
- }
120
-
121
127
  set.status = 404;
122
128
  return 'File not found';
123
129
  }
@@ -436,19 +442,6 @@ ws.onclose = () => {
436
442
  });
437
443
  })
438
444
 
439
- .get('/public/*', async ({ params, set }) => {
440
- const publicDir = join(root, 'public');
441
- const filepath = join(publicDir, params['*']);
442
- const file = Bun.file(filepath);
443
-
444
- if (!await file.exists()) {
445
- set.status = 404;
446
- return 'File not found';
447
- }
448
-
449
- return new Response(file);
450
- })
451
-
452
445
  .listen(port);
453
446
 
454
447
  if (!app.server) {
@@ -458,7 +451,8 @@ ws.onclose = () => {
458
451
 
459
452
  logger.success(`🚀 Server running at http://localhost:${port}`);
460
453
  logger.info(`📁 Serving: ${root}`);
461
- logger.info(`🖼️ Images available at: /images/*`);
454
+ logger.info(`🖼️ Images: /images/* src/images/`);
455
+ logger.info(`📦 Public: /public/* → public/`);
462
456
 
463
457
  setupWatcher(root, compiledDir, clients, async () => {
464
458
  hasRouter = existsSync(join(compiledDir, 'router.js'));
@@ -536,7 +530,6 @@ ${userStylesheets}
536
530
  });
537
531
  }
538
532
 
539
- // ✅ NEW: Helper for image content types
540
533
  function getImageContentType(ext) {
541
534
  const types = {
542
535
  '.jpg': 'image/jpeg',
@@ -594,14 +587,12 @@ function setupWatcher(root, compiledDir, clients, onRecompile) {
594
587
  if (!filename) return;
595
588
 
596
589
  const ext = extname(filename);
597
-
598
- // ✅ Watch image changes too
599
590
  const watchedExtensions = ['.js', '.jsx', '.ts', '.tsx', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif'];
600
591
 
601
592
  if (watchedExtensions.includes(ext)) {
602
593
  logger.info(`📝 File changed: ${filename}`);
603
594
 
604
- // For images, just reload without recompiling
595
+ // For images, just reload
605
596
  if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif'].includes(ext)) {
606
597
  for (const client of clients) {
607
598
  try {