bertui 0.3.9 → 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.3.9",
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
+ }
package/src/build.js CHANGED
@@ -1,9 +1,8 @@
1
- import { join, relative, basename } from 'path';
1
+ import { join, relative, basename, extname, dirname } from 'path';
2
2
  import { existsSync, mkdirSync, rmSync, cpSync, readdirSync, statSync } from 'fs';
3
- import { extname, dirname } from 'path';
4
3
  import logger from './logger/logger.js';
5
4
  import { buildCSS } from './build/css-builder.js';
6
- import { loadEnvVariables, replaceEnvInCode } from './utils/env.js'; // ✅ IMPORT THIS!
5
+ import { loadEnvVariables, replaceEnvInCode } from './utils/env.js';
7
6
 
8
7
  export async function buildProduction(options = {}) {
9
8
  const root = options.root || process.cwd();
@@ -26,7 +25,6 @@ export async function buildProduction(options = {}) {
26
25
  const startTime = Date.now();
27
26
 
28
27
  try {
29
- // ✅ LOAD ENV VARS BEFORE COMPILATION!
30
28
  logger.info('Step 0: Loading environment variables...');
31
29
  const envVars = loadEnvVariables(root);
32
30
  if (Object.keys(envVars).length > 0) {
@@ -34,27 +32,15 @@ export async function buildProduction(options = {}) {
34
32
  }
35
33
 
36
34
  logger.info('Step 1: Compiling for production...');
37
- const { routes } = await compileForBuild(root, buildDir, envVars); // ✅ PASS ENV VARS!
35
+ const { routes } = await compileForBuild(root, buildDir, envVars);
38
36
  logger.success('Production compilation complete');
39
37
 
40
38
  logger.info('Step 2: Building CSS with Lightning CSS...');
41
39
  await buildAllCSS(root, outDir);
42
40
 
43
- const publicDir = join(root, 'public');
44
- if (existsSync(publicDir)) {
45
- logger.info('Step 3: Copying public assets...');
46
- const publicFiles = readdirSync(publicDir);
47
- for (const file of publicFiles) {
48
- const srcFile = join(publicDir, file);
49
- const destFile = join(outDir, file);
50
- if (statSync(srcFile).isFile()) {
51
- cpSync(srcFile, destFile);
52
- }
53
- }
54
- logger.success('Public assets copied');
55
- } else {
56
- logger.info('Step 3: No public directory found, skipping...');
57
- }
41
+ // FIX: Copy all static assets from src/ and public/
42
+ logger.info('Step 3: Copying static assets...');
43
+ await copyAllStaticAssets(root, outDir);
58
44
 
59
45
  logger.info('Step 4: Bundling JavaScript with Bun...');
60
46
  const buildEntry = join(buildDir, 'main.js');
@@ -77,13 +63,11 @@ export async function buildProduction(options = {}) {
77
63
  asset: '[name]-[hash].[ext]'
78
64
  },
79
65
  external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
80
- // ✅ CRITICAL: Add define to replace process.env at bundle time!
81
66
  define: {
82
67
  'process.env.NODE_ENV': '"production"',
83
68
  'process.env.PUBLIC_APP_NAME': JSON.stringify(envVars.PUBLIC_APP_NAME || 'BertUI App'),
84
69
  'process.env.PUBLIC_API_URL': JSON.stringify(envVars.PUBLIC_API_URL || ''),
85
70
  'process.env.PUBLIC_USERNAME': JSON.stringify(envVars.PUBLIC_USERNAME || ''),
86
- // Add all other env vars dynamically
87
71
  ...Object.fromEntries(
88
72
  Object.entries(envVars).map(([key, value]) => [
89
73
  `process.env.${key}`,
@@ -137,6 +121,78 @@ export async function buildProduction(options = {}) {
137
121
  }
138
122
  }
139
123
 
124
+ // ✅ NEW FUNCTION: Copy all static assets
125
+ async function copyAllStaticAssets(root, outDir) {
126
+ const publicDir = join(root, 'public');
127
+ const srcDir = join(root, 'src');
128
+
129
+ let assetsCopied = 0;
130
+
131
+ // Copy from public/
132
+ if (existsSync(publicDir)) {
133
+ assetsCopied += await copyStaticAssetsFromDir(publicDir, outDir, 'public');
134
+ }
135
+
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);
141
+ }
142
+
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
+ }
189
+ }
190
+ }
191
+
192
+ copyRecursive(sourceDir, targetDir);
193
+ return copiedCount;
194
+ }
195
+
140
196
  async function buildAllCSS(root, outDir) {
141
197
  const srcStylesDir = join(root, 'src', 'styles');
142
198
  const stylesOutDir = join(outDir, 'styles');
@@ -153,7 +209,6 @@ async function buildAllCSS(root, outDir) {
153
209
  }
154
210
  }
155
211
 
156
- // ✅ ACCEPT ENV VARS PARAMETER
157
212
  async function compileForBuild(root, buildDir, envVars) {
158
213
  const srcDir = join(root, 'src');
159
214
  const pagesDir = join(srcDir, 'pages');
@@ -168,7 +223,6 @@ async function compileForBuild(root, buildDir, envVars) {
168
223
  logger.info(`Found ${routes.length} routes`);
169
224
  }
170
225
 
171
- // ✅ PASS ENV VARS TO COMPILATION
172
226
  await compileBuildDirectory(srcDir, buildDir, root, envVars);
173
227
 
174
228
  if (routes.length > 0) {
@@ -367,7 +421,6 @@ ${routeConfigs}
367
421
  await Bun.write(join(buildDir, 'router.js'), routerCode);
368
422
  }
369
423
 
370
- // ✅ ACCEPT ENV VARS PARAMETER
371
424
  async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
372
425
  const files = readdirSync(srcDir);
373
426
 
@@ -378,29 +431,33 @@ async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
378
431
  if (stat.isDirectory()) {
379
432
  const subBuildDir = join(buildDir, file);
380
433
  mkdirSync(subBuildDir, { recursive: true });
381
- await compileBuildDirectory(srcPath, subBuildDir, root, envVars); // ✅ PASS IT DOWN
434
+ await compileBuildDirectory(srcPath, subBuildDir, root, envVars);
382
435
  } else {
383
436
  const ext = extname(file);
384
437
 
385
438
  if (ext === '.css') continue;
386
439
 
387
440
  if (['.jsx', '.tsx', '.ts'].includes(ext)) {
388
- await compileBuildFile(srcPath, buildDir, file, root, envVars); // ✅ PASS IT HERE
441
+ await compileBuildFile(srcPath, buildDir, file, root, envVars);
389
442
  } else if (ext === '.js') {
390
443
  const outPath = join(buildDir, file);
391
444
  let code = await Bun.file(srcPath).text();
392
445
 
393
446
  code = removeCSSImports(code);
394
- code = replaceEnvInCode(code, envVars); // ✅ REPLACE ENV VARS!
447
+ code = replaceEnvInCode(code, envVars);
395
448
  code = fixBuildImports(code, srcPath, outPath, root);
396
449
 
450
+ // ✅ FIX: Add React import if needed
451
+ if (usesJSX(code) && !code.includes('import React')) {
452
+ code = `import React from 'react';\n${code}`;
453
+ }
454
+
397
455
  await Bun.write(outPath, code);
398
456
  }
399
457
  }
400
458
  }
401
459
  }
402
460
 
403
- // ✅ ACCEPT ENV VARS PARAMETER
404
461
  async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
405
462
  const ext = extname(filename);
406
463
  const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
@@ -409,7 +466,7 @@ async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
409
466
  let code = await Bun.file(srcPath).text();
410
467
 
411
468
  code = removeCSSImports(code);
412
- code = replaceEnvInCode(code, envVars); // ✅ REPLACE ENV VARS BEFORE TRANSPILATION!
469
+ code = replaceEnvInCode(code, envVars);
413
470
 
414
471
  const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
415
472
  const outPath = join(buildDir, outFilename);
@@ -429,7 +486,8 @@ async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
429
486
 
430
487
  let compiled = await transpiler.transform(code);
431
488
 
432
- if (!compiled.includes('import React') && (compiled.includes('React.createElement') || compiled.includes('React.Fragment'))) {
489
+ // FIX: Add React import if needed
490
+ if (usesJSX(compiled) && !compiled.includes('import React')) {
433
491
  compiled = `import React from 'react';\n${compiled}`;
434
492
  }
435
493
 
@@ -442,6 +500,14 @@ async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
442
500
  }
443
501
  }
444
502
 
503
+ function usesJSX(code) {
504
+ return code.includes('React.createElement') ||
505
+ code.includes('React.Fragment') ||
506
+ /<[A-Z]/.test(code) ||
507
+ code.includes('jsx(') ||
508
+ code.includes('jsxs(');
509
+ }
510
+
445
511
  function removeCSSImports(code) {
446
512
  code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
447
513
  code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
@@ -20,13 +20,11 @@ export async function compileProject(root) {
20
20
  logger.info('Created .bertui/compiled/');
21
21
  }
22
22
 
23
- // Load environment variables
24
23
  const envVars = loadEnvVariables(root);
25
24
  if (Object.keys(envVars).length > 0) {
26
25
  logger.info(`Loaded ${Object.keys(envVars).length} environment variables`);
27
26
  }
28
27
 
29
- // Generate env.js file
30
28
  const envCode = generateEnvCode(envVars);
31
29
  await Bun.write(join(outDir, 'env.js'), envCode);
32
30
 
@@ -285,13 +283,15 @@ async function compileDirectory(srcDir, outDir, root, envVars) {
285
283
  const outPath = join(outDir, file);
286
284
  let code = await Bun.file(srcPath).text();
287
285
 
288
- // Remove ALL CSS imports
289
286
  code = removeCSSImports(code);
290
- // Inject environment variables
291
287
  code = replaceEnvInCode(code, envVars);
292
- // Fix router imports
293
288
  code = fixRouterImports(code, outPath, root);
294
289
 
290
+ // ✅ CRITICAL FIX: Ensure React import for .js files with JSX
291
+ if (usesJSX(code) && !code.includes('import React')) {
292
+ code = `import React from 'react';\n${code}`;
293
+ }
294
+
295
295
  await Bun.write(outPath, code);
296
296
  logger.debug(`Copied: ${relativePath}`);
297
297
  stats.files++;
@@ -312,13 +312,8 @@ async function compileFile(srcPath, outDir, filename, relativePath, root, envVar
312
312
  try {
313
313
  let code = await Bun.file(srcPath).text();
314
314
 
315
- // CRITICAL FIX: Remove ALL CSS imports before transpilation
316
315
  code = removeCSSImports(code);
317
-
318
- // Remove dotenv imports (not needed in browser)
319
316
  code = removeDotenvImports(code);
320
-
321
- // Inject environment variables
322
317
  code = replaceEnvInCode(code, envVars);
323
318
 
324
319
  const outPath = join(outDir, filename.replace(/\.(jsx|tsx|ts)$/, '.js'));
@@ -336,7 +331,8 @@ async function compileFile(srcPath, outDir, filename, relativePath, root, envVar
336
331
  });
337
332
  let compiled = await transpiler.transform(code);
338
333
 
339
- if (!compiled.includes('import React') && (compiled.includes('React.createElement') || compiled.includes('React.Fragment'))) {
334
+ // CRITICAL FIX: Always add React import if JSX is present
335
+ if (usesJSX(compiled) && !compiled.includes('import React')) {
340
336
  compiled = `import React from 'react';\n${compiled}`;
341
337
  }
342
338
 
@@ -350,29 +346,25 @@ async function compileFile(srcPath, outDir, filename, relativePath, root, envVar
350
346
  }
351
347
  }
352
348
 
353
- // NEW FUNCTION: Remove all CSS imports
349
+ // NEW: Detect if code uses JSX
350
+ function usesJSX(code) {
351
+ return code.includes('React.createElement') ||
352
+ code.includes('React.Fragment') ||
353
+ /<[A-Z]/.test(code) || // Detects JSX tags like <Component>
354
+ code.includes('jsx(') || // Runtime JSX
355
+ code.includes('jsxs('); // Runtime JSX
356
+ }
357
+
354
358
  function removeCSSImports(code) {
355
- // Remove CSS imports (with or without quotes, single or double)
356
- // Matches: import './styles.css', import "./styles.css", import "styles.css", import 'styles.css'
357
359
  code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
358
-
359
- // Also remove bertui/styles imports
360
360
  code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
361
-
362
361
  return code;
363
362
  }
364
363
 
365
- // NEW FUNCTION: Remove dotenv imports and dotenv.config() calls
366
364
  function removeDotenvImports(code) {
367
- // Remove: import dotenv from 'dotenv'
368
365
  code = code.replace(/import\s+\w+\s+from\s+['"]dotenv['"]\s*;?\s*/g, '');
369
-
370
- // Remove: import { config } from 'dotenv'
371
366
  code = code.replace(/import\s+\{[^}]+\}\s+from\s+['"]dotenv['"]\s*;?\s*/g, '');
372
-
373
- // Remove: dotenv.config()
374
367
  code = code.replace(/\w+\.config\(\s*\)\s*;?\s*/g, '');
375
-
376
368
  return code;
377
369
  }
378
370
 
@@ -1,7 +1,8 @@
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';
4
- import { existsSync, readdirSync } from 'fs'; // ✅ FIXED: Import properly
5
+ import { existsSync, readdirSync } from 'fs';
5
6
  import logger from '../logger/logger.js';
6
7
  import { compileProject } from '../client/compiler.js';
7
8
  import { loadConfig } from '../config/loadConfig.js';
@@ -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,11 +30,53 @@ 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
 
34
79
  if (path.includes('.')) {
35
- // Handle compiled directory files
36
80
  if (path.startsWith('compiled/')) {
37
81
  const filePath = join(compiledDir, path.replace('compiled/', ''));
38
82
  const file = Bun.file(filePath);
@@ -50,7 +94,6 @@ export async function startDevServer(options = {}) {
50
94
  }
51
95
  }
52
96
 
53
- // Handle CSS files from .bertui/styles
54
97
  if (path.startsWith('styles/') && path.endsWith('.css')) {
55
98
  const cssPath = join(stylesDir, path.replace('styles/', ''));
56
99
  const file = Bun.file(cssPath);
@@ -101,6 +144,14 @@ ws.onmessage = (event) => {
101
144
  if (data.type === 'recompiling') {
102
145
  console.log('%c⚙️ Recompiling...', 'color: #3b82f6');
103
146
  }
147
+
148
+ if (data.type === 'compilation-error') {
149
+ console.error('%c❌ Compilation Error', 'color: #ef4444; font-weight: bold');
150
+ console.error(data.message);
151
+ if (window.__BERTUI_SHOW_ERROR__) {
152
+ window.__BERTUI_SHOW_ERROR__(data);
153
+ }
154
+ }
104
155
  };
105
156
 
106
157
  ws.onerror = (error) => {
@@ -116,6 +167,226 @@ ws.onclose = () => {
116
167
  headers: { 'Content-Type': 'application/javascript' }
117
168
  });
118
169
  })
170
+
171
+ .get('/error-overlay.js', () => {
172
+ const errorOverlayScript = `
173
+ (function() {
174
+ 'use strict';
175
+
176
+ let overlayElement = null;
177
+
178
+ function createOverlay() {
179
+ if (overlayElement) return overlayElement;
180
+
181
+ const overlay = document.createElement('div');
182
+ overlay.id = 'bertui-error-overlay';
183
+ overlay.style.cssText = \`
184
+ position: fixed;
185
+ top: 0;
186
+ left: 0;
187
+ width: 100%;
188
+ height: 100%;
189
+ background: rgba(0, 0, 0, 0.95);
190
+ color: #fff;
191
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
192
+ font-size: 14px;
193
+ line-height: 1.5;
194
+ z-index: 9999999;
195
+ overflow: auto;
196
+ padding: 20px;
197
+ box-sizing: border-box;
198
+ display: none;
199
+ \`;
200
+ document.body.appendChild(overlay);
201
+ overlayElement = overlay;
202
+ return overlay;
203
+ }
204
+
205
+ function showError(error) {
206
+ const overlay = createOverlay();
207
+
208
+ const errorType = error.type || 'Runtime Error';
209
+ const errorMessage = error.message || 'Unknown error';
210
+ const errorStack = error.stack || '';
211
+ const errorFile = error.file || 'Unknown file';
212
+ const errorLine = error.line || '';
213
+ const errorColumn = error.column || '';
214
+
215
+ overlay.innerHTML = \`
216
+ <div style="max-width: 1200px; margin: 0 auto;">
217
+ <div style="display: flex; align-items: center; margin-bottom: 30px;">
218
+ <div style="
219
+ background: #ef4444;
220
+ width: 50px;
221
+ height: 50px;
222
+ border-radius: 50%;
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ font-size: 24px;
227
+ margin-right: 15px;
228
+ ">❌</div>
229
+ <div>
230
+ <h1 style="margin: 0; font-size: 28px; font-weight: bold; color: #ef4444;">
231
+ \${errorType}
232
+ </h1>
233
+ <p style="margin: 5px 0 0 0; color: #a0a0a0; font-size: 14px;">
234
+ BertUI detected an error in your application
235
+ </p>
236
+ </div>
237
+ </div>
238
+
239
+ <div style="
240
+ background: #1a1a1a;
241
+ border: 1px solid #333;
242
+ border-radius: 8px;
243
+ padding: 20px;
244
+ margin-bottom: 20px;
245
+ ">
246
+ <div style="color: #fbbf24; font-weight: bold; margin-bottom: 10px;">
247
+ \${errorFile}\${errorLine ? ':' + errorLine : ''}\${errorColumn ? ':' + errorColumn : ''}
248
+ </div>
249
+ <div style="color: #fff; white-space: pre-wrap; word-break: break-word;">
250
+ \${escapeHtml(errorMessage)}
251
+ </div>
252
+ </div>
253
+
254
+ \${errorStack ? \`
255
+ <div style="
256
+ background: #0a0a0a;
257
+ border: 1px solid #222;
258
+ border-radius: 8px;
259
+ padding: 20px;
260
+ margin-bottom: 20px;
261
+ ">
262
+ <div style="color: #a0a0a0; font-weight: bold; margin-bottom: 10px;">
263
+ Stack Trace:
264
+ </div>
265
+ <pre style="
266
+ margin: 0;
267
+ color: #d0d0d0;
268
+ white-space: pre-wrap;
269
+ word-break: break-word;
270
+ font-size: 12px;
271
+ ">\${escapeHtml(errorStack)}</pre>
272
+ </div>
273
+ \` : ''}
274
+
275
+ <div style="display: flex; gap: 15px; flex-wrap: wrap;">
276
+ <button onclick="window.__BERTUI_HIDE_ERROR__()" style="
277
+ background: #3b82f6;
278
+ color: white;
279
+ border: none;
280
+ padding: 12px 24px;
281
+ border-radius: 6px;
282
+ font-size: 14px;
283
+ font-weight: 600;
284
+ cursor: pointer;
285
+ ">Dismiss (Esc)</button>
286
+ <button onclick="window.location.reload()" style="
287
+ background: #10b981;
288
+ color: white;
289
+ border: none;
290
+ padding: 12px 24px;
291
+ border-radius: 6px;
292
+ font-size: 14px;
293
+ font-weight: 600;
294
+ cursor: pointer;
295
+ ">Reload Page</button>
296
+ </div>
297
+
298
+ <div style="
299
+ margin-top: 30px;
300
+ padding-top: 20px;
301
+ border-top: 1px solid #333;
302
+ color: #666;
303
+ font-size: 12px;
304
+ ">
305
+ 💡 <strong>Tip:</strong> Fix the error in your code, and the page will automatically reload with HMR.
306
+ </div>
307
+ </div>
308
+ \`;
309
+
310
+ overlay.style.display = 'block';
311
+ }
312
+
313
+ function hideError() {
314
+ if (overlayElement) {
315
+ overlayElement.style.display = 'none';
316
+ }
317
+ }
318
+
319
+ function escapeHtml(text) {
320
+ const div = document.createElement('div');
321
+ div.textContent = text;
322
+ return div.innerHTML;
323
+ }
324
+
325
+ function parseErrorStack(error) {
326
+ const stack = error.stack || '';
327
+ const lines = stack.split('\\n');
328
+
329
+ for (const line of lines) {
330
+ const match = line.match(/\\((.+):(\\d+):(\\d+)\\)/) ||
331
+ line.match(/at (.+):(\\d+):(\\d+)/) ||
332
+ line.match(/(.+):(\\d+):(\\d+)/);
333
+
334
+ if (match) {
335
+ return {
336
+ file: match[1].trim(),
337
+ line: match[2],
338
+ column: match[3]
339
+ };
340
+ }
341
+ }
342
+
343
+ return { file: null, line: null, column: null };
344
+ }
345
+
346
+ window.addEventListener('error', function(event) {
347
+ const { file, line, column } = parseErrorStack(event.error || {});
348
+
349
+ showError({
350
+ type: 'Runtime Error',
351
+ message: event.message,
352
+ stack: event.error ? event.error.stack : null,
353
+ file: event.filename || file,
354
+ line: event.lineno || line,
355
+ column: event.colno || column
356
+ });
357
+ });
358
+
359
+ window.addEventListener('unhandledrejection', function(event) {
360
+ const error = event.reason;
361
+ const { file, line, column } = parseErrorStack(error);
362
+
363
+ showError({
364
+ type: 'Unhandled Promise Rejection',
365
+ message: error?.message || String(event.reason),
366
+ stack: error?.stack,
367
+ file,
368
+ line,
369
+ column
370
+ });
371
+ });
372
+
373
+ window.__BERTUI_SHOW_ERROR__ = showError;
374
+ window.__BERTUI_HIDE_ERROR__ = hideError;
375
+
376
+ document.addEventListener('keydown', function(e) {
377
+ if (e.key === 'Escape') {
378
+ hideError();
379
+ }
380
+ });
381
+
382
+ console.log('%c🔥 BertUI Error Overlay Active', 'color: #10b981; font-weight: bold; font-size: 14px');
383
+ })();
384
+ `;
385
+
386
+ return new Response(errorOverlayScript, {
387
+ headers: { 'Content-Type': 'application/javascript' }
388
+ });
389
+ })
119
390
 
120
391
  .ws('/hmr', {
121
392
  open(ws) {
@@ -187,6 +458,7 @@ ws.onclose = () => {
187
458
 
188
459
  logger.success(`🚀 Server running at http://localhost:${port}`);
189
460
  logger.info(`📁 Serving: ${root}`);
461
+ logger.info(`🖼️ Images available at: /images/*`);
190
462
 
191
463
  setupWatcher(root, compiledDir, clients, async () => {
192
464
  hasRouter = existsSync(join(compiledDir, 'router.js'));
@@ -198,7 +470,6 @@ ws.onclose = () => {
198
470
  function serveHTML(root, hasRouter, config) {
199
471
  const meta = config.meta || {};
200
472
 
201
- // ✅ FIXED: Proper ESM import for fs
202
473
  const srcStylesDir = join(root, 'src', 'styles');
203
474
  let userStylesheets = '';
204
475
 
@@ -254,6 +525,7 @@ ${userStylesheets}
254
525
  </head>
255
526
  <body>
256
527
  <div id="root"></div>
528
+ <script type="module" src="/error-overlay.js"></script>
257
529
  <script type="module" src="/hmr-client.js"></script>
258
530
  <script type="module" src="/compiled/main.js"></script>
259
531
  </body>
@@ -264,6 +536,22 @@ ${userStylesheets}
264
536
  });
265
537
  }
266
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
+
267
555
  function getContentType(ext) {
268
556
  const types = {
269
557
  '.js': 'application/javascript',
@@ -276,7 +564,16 @@ function getContentType(ext) {
276
564
  '.jpeg': 'image/jpeg',
277
565
  '.gif': 'image/gif',
278
566
  '.svg': 'image/svg+xml',
279
- '.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'
280
577
  };
281
578
 
282
579
  return types[ext] || 'text/plain';
@@ -297,9 +594,26 @@ function setupWatcher(root, compiledDir, clients, onRecompile) {
297
594
  if (!filename) return;
298
595
 
299
596
  const ext = extname(filename);
300
- 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)) {
301
602
  logger.info(`📝 File changed: ${filename}`);
302
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
303
617
  for (const client of clients) {
304
618
  try {
305
619
  client.send(JSON.stringify({ type: 'recompiling' }));
@@ -324,6 +638,19 @@ function setupWatcher(root, compiledDir, clients, onRecompile) {
324
638
  }
325
639
  } catch (error) {
326
640
  logger.error(`Recompilation failed: ${error.message}`);
641
+
642
+ for (const client of clients) {
643
+ try {
644
+ client.send(JSON.stringify({
645
+ type: 'compilation-error',
646
+ message: error.message,
647
+ stack: error.stack,
648
+ file: filename
649
+ }));
650
+ } catch (e) {
651
+ clients.delete(client);
652
+ }
653
+ }
327
654
  }
328
655
  }
329
656
  });