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 +4 -1
- package/src/build/image-optimizer.js +105 -105
- package/src/build.js +61 -89
- package/src/server/dev-server.js +26 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bertui",
|
|
3
|
-
"version": "0.4.
|
|
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
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync,
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
//
|
|
86
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
//
|
|
140
|
-
if (
|
|
141
|
-
|
|
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
|
|
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
|
-
|
|
157
|
-
await
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
42
|
-
|
|
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:
|
|
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
|
|
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
|
-
// ✅
|
|
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
|
|
130
|
+
const srcImagesDir = join(root, 'src', 'images');
|
|
128
131
|
|
|
129
132
|
let assetsCopied = 0;
|
|
133
|
+
let assetsOptimized = 0;
|
|
130
134
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
+
// Create images directory in dist/
|
|
136
|
+
const distImagesDir = join(outDir, 'images');
|
|
137
|
+
mkdirSync(distImagesDir, { recursive: true });
|
|
135
138
|
|
|
136
|
-
// Copy
|
|
137
|
-
if (existsSync(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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 =
|
|
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
|
}
|
package/src/server/dev-server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/server/dev-server.js - FIXED
|
|
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
|
-
// ✅
|
|
34
|
+
// ✅ FIX: Serve images from src/images/ (CRITICAL)
|
|
34
35
|
.get('/images/*', async ({ params, set }) => {
|
|
35
|
-
const
|
|
36
|
-
const filepath = join(
|
|
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': '
|
|
51
|
+
'Cache-Control': 'no-cache' // Dev server = no cache
|
|
51
52
|
}
|
|
52
53
|
});
|
|
53
54
|
})
|
|
54
55
|
|
|
55
|
-
// ✅
|
|
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': '
|
|
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
|
|
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
|
|
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 {
|