bertui 0.4.4 → 0.4.6

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.4",
3
+ "version": "0.4.6",
4
4
  "description": "Lightning-fast React dev server powered by Bun and Elysia",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -1,216 +1,87 @@
1
- // bertui/src/build/image-optimizer.js - WASM-POWERED VERSION 🚀
1
+ // bertui/src/build/image-optimizer.js - SIMPLE WORKING VERSION
2
2
  import { join, extname } from 'path';
3
3
  import { existsSync, mkdirSync, readdirSync, cpSync } from 'fs';
4
4
  import logger from '../logger/logger.js';
5
5
 
6
6
  /**
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! 🚀
7
+ * Simple image copying - skip WASM optimization for now
43
8
  */
44
9
  export async function optimizeImages(srcDir, outDir) {
45
- const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg'];
46
- let optimized = 0;
47
- let totalSaved = 0;
48
-
49
- logger.info('🖼️ Optimizing images with WASM codecs...');
50
-
51
- async function processDirectory(dir, targetDir) {
52
- const entries = readdirSync(dir, { withFileTypes: true });
53
-
54
- for (const entry of entries) {
55
- const srcPath = join(dir, entry.name);
56
- const destPath = join(targetDir, entry.name);
57
-
58
- if (entry.isDirectory()) {
59
- if (!existsSync(destPath)) {
60
- mkdirSync(destPath, { recursive: true });
61
- }
62
- await processDirectory(srcPath, destPath);
63
- } else if (entry.isFile()) {
64
- const ext = extname(entry.name).toLowerCase();
65
-
66
- if (imageExtensions.includes(ext)) {
67
- try {
68
- const result = await optimizeImage(srcPath, destPath);
69
- if (result) {
70
- optimized++;
71
- totalSaved += result.saved;
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
- );
76
- }
77
- } catch (error) {
78
- logger.warn(`⚠️ Failed to optimize ${entry.name}: ${error.message}`);
79
- // Fallback: just copy the file
80
- cpSync(srcPath, destPath);
81
- }
82
- }
83
- }
84
- }
85
- }
86
-
87
- await processDirectory(srcDir, outDir);
88
-
89
- if (optimized > 0) {
90
- logger.success(
91
- `✅ Optimized ${optimized} images (saved ${(totalSaved / 1024).toFixed(2)}KB total)`
92
- );
93
- }
94
-
95
- return { optimized, saved: totalSaved };
96
- }
97
-
98
- /**
99
- * Optimize a single image using WASM codecs
100
- */
101
- async function optimizeImage(srcPath, destPath) {
102
- const ext = extname(srcPath).toLowerCase();
103
- const originalFile = Bun.file(srcPath);
104
- const originalSize = originalFile.size;
105
-
106
- try {
107
- // For SVG and GIF, just copy (no optimization needed/supported)
108
- if (ext === '.svg' || ext === '.gif') {
109
- cpSync(srcPath, destPath);
110
- return null;
111
- }
112
-
113
- // Read the original image
114
- const originalBuffer = await originalFile.arrayBuffer();
115
-
116
- let optimizedBuffer;
117
-
118
- if (ext === '.png') {
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
128
- });
129
-
130
- } else if (ext === '.jpg' || ext === '.jpeg') {
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 });
143
- }
144
-
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
- }
151
-
152
- // If optimization didn't help, just copy the original
153
- cpSync(srcPath, destPath);
154
- return null;
155
-
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);
160
- return null;
161
- }
10
+ logger.info(`📋 Copying images from ${srcDir} to ${outDir}...`);
11
+ const copied = copyImages(srcDir, outDir);
12
+ return { optimized: 0, saved: 0, copied };
162
13
  }
163
14
 
164
- /**
165
- * Check if optimization is available (always true with WASM! 🎉)
166
- */
167
15
  export async function checkOptimizationTools() {
168
- try {
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 [];
180
- }
16
+ logger.info('📋 Image optimization disabled (simple mode)');
17
+ return [];
181
18
  }
182
19
 
183
- /**
184
- * Copy images without optimization (fallback)
185
- */
186
20
  export function copyImages(srcDir, outDir) {
187
- const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.avif'];
21
+ const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.avif', '.ico'];
188
22
  let copied = 0;
189
23
 
190
- function processDirectory(dir, targetDir) {
191
- const entries = readdirSync(dir, { withFileTypes: true });
24
+ // Check if source directory exists
25
+ if (!existsSync(srcDir)) {
26
+ logger.warn(`⚠️ Source directory not found: ${srcDir}`);
27
+ return 0;
28
+ }
192
29
 
193
- for (const entry of entries) {
194
- const srcPath = join(dir, entry.name);
195
- const destPath = join(targetDir, entry.name);
30
+ // Create output directory if it doesn't exist
31
+ if (!existsSync(outDir)) {
32
+ mkdirSync(outDir, { recursive: true });
33
+ logger.info(`Created directory: ${outDir}`);
34
+ }
196
35
 
197
- if (entry.isDirectory()) {
198
- if (!existsSync(destPath)) {
199
- mkdirSync(destPath, { recursive: true });
200
- }
201
- processDirectory(srcPath, targetDir);
202
- } else if (entry.isFile()) {
203
- const ext = extname(entry.name).toLowerCase();
36
+ function processDirectory(dir, targetDir) {
37
+ try {
38
+ const entries = readdirSync(dir, { withFileTypes: true });
39
+
40
+ if (entries.length === 0) {
41
+ logger.warn(`Directory empty: ${dir}`);
42
+ return;
43
+ }
44
+
45
+ for (const entry of entries) {
46
+ const srcPath = join(dir, entry.name);
47
+ const destPath = join(targetDir, entry.name);
204
48
 
205
- if (imageExtensions.includes(ext)) {
206
- cpSync(srcPath, destPath);
207
- copied++;
49
+ if (entry.isDirectory()) {
50
+ // Create subdirectory in target
51
+ const subDestPath = join(targetDir, entry.name);
52
+ if (!existsSync(subDestPath)) {
53
+ mkdirSync(subDestPath, { recursive: true });
54
+ }
55
+ processDirectory(srcPath, subDestPath);
56
+ } else if (entry.isFile()) {
57
+ const ext = extname(entry.name).toLowerCase();
58
+
59
+ if (imageExtensions.includes(ext)) {
60
+ try {
61
+ cpSync(srcPath, destPath);
62
+ copied++;
63
+ logger.debug(` ✓ ${entry.name}`);
64
+ } catch (error) {
65
+ logger.warn(` ✗ ${entry.name} - ${error.message}`);
66
+ }
67
+ } else {
68
+ logger.debug(` - ${entry.name} (skipped, not an image)`);
69
+ }
208
70
  }
209
71
  }
72
+ } catch (error) {
73
+ logger.error(`Error processing directory ${dir}: ${error.message}`);
210
74
  }
211
75
  }
212
76
 
77
+ logger.info(`Processing ${srcDir}...`);
213
78
  processDirectory(srcDir, outDir);
214
- logger.info(`📋 Copied ${copied} images without optimization`);
79
+
80
+ if (copied > 0) {
81
+ logger.success(`✅ Copied ${copied} images to ${outDir}`);
82
+ } else {
83
+ logger.warn(`⚠️ No images found in ${srcDir}`);
84
+ }
85
+
215
86
  return copied;
216
87
  }
package/src/build.js CHANGED
@@ -1,4 +1,4 @@
1
- // src/build.js - FIXED VERSION
1
+ // src/build.js - COMPLETELY FIXED VERSION
2
2
  import { join, relative, basename, extname, dirname } from 'path';
3
3
  import { existsSync, mkdirSync, rmSync, cpSync, readdirSync, statSync } from 'fs';
4
4
  import logger from './logger/logger.js';
@@ -13,6 +13,7 @@ export async function buildProduction(options = {}) {
13
13
 
14
14
  logger.bigLog('BUILDING FOR PRODUCTION', { color: 'green' });
15
15
 
16
+ // Clean up old builds
16
17
  if (existsSync(buildDir)) {
17
18
  rmSync(buildDir, { recursive: true });
18
19
  }
@@ -35,7 +36,7 @@ export async function buildProduction(options = {}) {
35
36
 
36
37
  logger.info('Step 1: Compiling for production...');
37
38
  const { routes } = await compileForBuild(root, buildDir, envVars);
38
- logger.success('Production compilation complete');
39
+ logger.success(`Production compilation complete - ${routes.length} routes`);
39
40
 
40
41
  logger.info('Step 2: Building CSS with Lightning CSS...');
41
42
  await buildAllCSS(root, outDir);
@@ -44,8 +45,8 @@ export async function buildProduction(options = {}) {
44
45
  const optimizationTools = await checkOptimizationTools();
45
46
 
46
47
  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);
48
+ // SKIP OPTIMIZATION FOR NOW - JUST COPY
49
+ await copyAllStaticAssets(root, outDir, false);
49
50
 
50
51
  logger.info('Step 5: Bundling JavaScript with Bun...');
51
52
  const buildEntry = join(buildDir, 'main.js');
@@ -85,22 +86,33 @@ export async function buildProduction(options = {}) {
85
86
  process.exit(1);
86
87
  }
87
88
 
88
- logger.success('JavaScript bundled with tree-shaking');
89
+ logger.success('JavaScript bundled successfully');
89
90
 
90
- logger.info('Step 6: Generating SEO-optimized HTML files...');
91
- // ✅ FIX 2: Generate HTML for ALL routes including index.html
91
+ // DEBUG: Show what was built
92
+ logger.info('Built outputs:');
93
+ result.outputs.forEach((output, i) => {
94
+ logger.info(` ${i + 1}. ${relative(outDir, output.path)} (${output.kind})`);
95
+ });
96
+
97
+ logger.info('Step 6: Generating HTML files...');
98
+ // ✅ CRITICAL FIX: Generate HTML files
92
99
  await generateProductionHTML(root, outDir, result, routes);
93
100
 
94
- rmSync(buildDir, { recursive: true });
95
- logger.info('Cleaned up .bertuibuild/');
101
+ // Clean up build directory
102
+ if (existsSync(buildDir)) {
103
+ rmSync(buildDir, { recursive: true });
104
+ logger.info('Cleaned up .bertuibuild/');
105
+ }
96
106
 
97
107
  const duration = Date.now() - startTime;
98
108
  logger.success(`✨ Build complete in ${duration}ms`);
99
109
  logger.info(`📦 Output: ${outDir}`);
100
110
 
111
+ // Show build summary
101
112
  logger.table(result.outputs.map(o => ({
102
113
  file: o.path.replace(outDir, ''),
103
- size: `${(o.size / 1024).toFixed(2)} KB`
114
+ size: `${(o.size / 1024).toFixed(2)} KB`,
115
+ type: o.kind
104
116
  })));
105
117
 
106
118
  logger.bigLog('READY TO DEPLOY', { color: 'green' });
@@ -108,7 +120,7 @@ export async function buildProduction(options = {}) {
108
120
  console.log(' Vercel: bunx vercel');
109
121
  console.log(' Netlify: bunx netlify deploy');
110
122
  console.log('\n🔍 Preview locally:\n');
111
- console.log(' bun run preview\n');
123
+ console.log(' cd dist && bun run preview\n');
112
124
 
113
125
  } catch (error) {
114
126
  logger.error(`Build failed: ${error.message}`);
@@ -124,44 +136,30 @@ export async function buildProduction(options = {}) {
124
136
  }
125
137
  }
126
138
 
127
- // ✅ FIX 3: Enhanced asset copying with proper directory structure
139
+ // ✅ SIMPLE asset copying
128
140
  async function copyAllStaticAssets(root, outDir, optimize = true) {
129
141
  const publicDir = join(root, 'public');
130
142
  const srcImagesDir = join(root, 'src', 'images');
131
143
 
132
- let assetsCopied = 0;
133
- let assetsOptimized = 0;
134
-
135
- // Create images directory in dist/
136
- const distImagesDir = join(outDir, 'images');
137
- mkdirSync(distImagesDir, { recursive: true });
144
+ // ALWAYS use simple copy for now
145
+ logger.info('Using simple asset copy (optimization disabled)...');
138
146
 
139
147
  // Copy from public/ to root of dist/
140
148
  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
- }
149
+ logger.info('📁 Copying public/ directory...');
150
+ copyImages(publicDir, outDir);
151
+ } else {
152
+ logger.info('No public/ directory found');
148
153
  }
149
154
 
150
- // ✅ FIX: Copy from src/images/ to dist/images/
155
+ // Copy from src/images/ to dist/images/
151
156
  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);
158
- }
159
- }
160
-
161
- if (optimize && assetsOptimized > 0) {
162
- logger.success(`🎨 Optimized ${assetsOptimized} images with WASM codecs`);
157
+ logger.info('🖼️ Copying src/images/ to dist/images/...');
158
+ const distImagesDir = join(outDir, 'images');
159
+ mkdirSync(distImagesDir, { recursive: true });
160
+ copyImages(srcImagesDir, distImagesDir);
163
161
  } else {
164
- logger.success(`📋 Copied ${assetsCopied} static assets`);
162
+ logger.info('No src/images/ directory found');
165
163
  }
166
164
  }
167
165
 
@@ -552,63 +550,71 @@ function extractMetaFromSource(code) {
552
550
  }
553
551
  }
554
552
 
555
- // ✅ FIX 4: Generate proper HTML files with correct meta tags
553
+ // ✅ CRITICAL FIX: Generate HTML files
556
554
  async function generateProductionHTML(root, outDir, buildResult, routes) {
555
+ if (routes.length === 0) {
556
+ logger.warn('No routes found, skipping HTML generation');
557
+ return;
558
+ }
559
+
560
+ logger.info(`Generating HTML files for ${routes.length} routes...`);
561
+
562
+ // Find main JS bundle
557
563
  const mainBundle = buildResult.outputs.find(o =>
558
564
  o.path.includes('main') && o.kind === 'entry-point'
559
565
  );
560
566
 
561
567
  if (!mainBundle) {
562
- throw new Error('Could not find main bundle in build output');
568
+ logger.error('Could not find main bundle in build output');
569
+ // List all outputs for debugging
570
+ logger.info('Available outputs:');
571
+ buildResult.outputs.forEach((o, i) => {
572
+ logger.info(` ${i + 1}. ${o.path} (${o.kind})`);
573
+ });
574
+ return;
563
575
  }
564
576
 
565
577
  const bundlePath = relative(outDir, mainBundle.path).replace(/\\/g, '/');
566
- logger.info(`Main bundle path: ${bundlePath}`);
567
-
568
- const srcStylesDir = join(root, 'src', 'styles');
569
- let userStylesheets = '';
570
-
571
- if (existsSync(srcStylesDir)) {
572
- const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
573
- userStylesheets = cssFiles.map(f =>
574
- ` <link rel="stylesheet" href="/styles/${f.replace('.css', '.min.css')}">`
575
- ).join('\n');
576
- }
578
+ logger.info(`Main bundle: ${bundlePath}`);
577
579
 
578
- // Load config for default meta
580
+ // Load config for default meta
579
581
  const { loadConfig } = await import('./config/loadConfig.js');
580
582
  const config = await loadConfig(root);
581
583
  const defaultMeta = config.meta || {};
582
584
 
583
- logger.info('Generating SEO-optimized HTML files...');
584
-
585
- // ✅ FIX: Generate HTML for ALL routes (including dynamic as fallback)
585
+ // Generate HTML for each route
586
586
  for (const route of routes) {
587
- const sourceCode = await Bun.file(route.path).text();
588
- const pageMeta = extractMetaFromSource(sourceCode);
589
- const meta = { ...defaultMeta, ...pageMeta };
590
-
591
- if (pageMeta) {
592
- logger.info(`Extracted meta for ${route.route}: ${JSON.stringify(pageMeta)}`);
593
- }
594
-
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');
587
+ try {
588
+ const sourceCode = await Bun.file(route.path).text();
589
+ const pageMeta = extractMetaFromSource(sourceCode);
590
+ const meta = { ...defaultMeta, ...pageMeta };
591
+
592
+ const html = generateHTML(meta, route, bundlePath);
593
+
594
+ let htmlPath;
595
+ if (route.route === '/') {
596
+ htmlPath = join(outDir, 'index.html');
597
+ } else {
598
+ // Create directory for the route
599
+ const routeDir = join(outDir, route.route.slice(1)); // Remove leading slash
600
+ mkdirSync(routeDir, { recursive: true });
601
+ htmlPath = join(routeDir, 'index.html');
602
+ }
603
+
604
+ await Bun.write(htmlPath, html);
605
+ logger.success(`Generated: ${route.route === '/' ? 'index.html' : route.route + '/index.html'}`);
606
+ } catch (error) {
607
+ logger.error(`Failed to generate HTML for ${route.route}: ${error.message}`);
604
608
  }
605
-
606
- await Bun.write(htmlPath, html);
607
- logger.success(`Generated ${route.route === '/' ? 'index.html' : route.route + '/index.html'}`);
608
609
  }
609
610
  }
610
611
 
611
- function generateHTML(meta, route, bundlePath, userStylesheets) {
612
+ function generateHTML(meta, route, bundlePath) {
613
+ const cssFiles = ['global.min.css', 'home.min.css'];
614
+ const stylesheets = cssFiles.map(css =>
615
+ ` <link rel="stylesheet" href="/styles/${css}">`
616
+ ).join('\n');
617
+
612
618
  return `<!DOCTYPE html>
613
619
  <html lang="${meta.lang || 'en'}">
614
620
  <head>
@@ -619,23 +625,10 @@ function generateHTML(meta, route, bundlePath, userStylesheets) {
619
625
  <meta name="description" content="${meta.description || 'Built with BertUI - Lightning fast React development'}">
620
626
  ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
621
627
  ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
622
- ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
623
-
624
- <meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
625
- <meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
626
- ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
627
- <meta property="og:type" content="website">
628
- <meta property="og:url" content="${route.route}">
629
-
630
- <meta name="twitter:card" content="summary_large_image">
631
- <meta name="twitter:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
632
- <meta name="twitter:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
633
- ${meta.ogImage ? `<meta name="twitter:image" content="${meta.ogImage}">` : ''}
634
628
 
635
629
  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
636
- <link rel="canonical" href="${route.route}">
637
630
 
638
- ${userStylesheets}
631
+ ${stylesheets}
639
632
 
640
633
  <script type="importmap">
641
634
  {