bertui 0.4.5 → 1.0.0
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/README.md +4 -0
- package/package.json +1 -4
- package/src/build/image-optimizer.js +48 -253
- package/src/build.js +81 -110
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# BertUI ⚡
|
|
2
2
|
|
|
3
|
+
[](https://github.com/your-repo)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
**The fastest, zero-config React static site generator. Now stable and production-ready.**
|
|
3
7
|
Lightning-fast React development powered by Bun.
|
|
4
8
|
|
|
5
9
|
## ⚠️ Important Notice - CSS Animations Temporarily Unavailable
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bertui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Lightning-fast React dev server powered by Bun and Elysia",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -44,9 +44,6 @@
|
|
|
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",
|
|
50
47
|
"elysia": "^1.0.0",
|
|
51
48
|
"ernest-logger": "latest",
|
|
52
49
|
"lightningcss": "^1.30.2"
|
|
@@ -1,291 +1,86 @@
|
|
|
1
|
-
// bertui/src/build/image-optimizer.js -
|
|
1
|
+
// bertui/src/build/image-optimizer.js - SIMPLE & STABLE
|
|
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
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Simple, reliable image copying
|
|
8
|
+
* No WASM, no optimization, just copy files
|
|
9
9
|
*/
|
|
10
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! 🚀
|
|
43
|
-
*/
|
|
44
11
|
export async function optimizeImages(srcDir, outDir) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
logger.info(`🖼️ Optimizing images from ${srcDir} to ${outDir}...`);
|
|
50
|
-
|
|
51
|
-
// Check if source directory exists
|
|
52
|
-
if (!existsSync(srcDir)) {
|
|
53
|
-
logger.warn(`⚠️ Source directory not found: ${srcDir}`);
|
|
54
|
-
return { optimized: 0, saved: 0 };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Create output directory if it doesn't exist
|
|
58
|
-
if (!existsSync(outDir)) {
|
|
59
|
-
mkdirSync(outDir, { recursive: true });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function processDirectory(dir, targetDir) {
|
|
63
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
64
|
-
|
|
65
|
-
for (const entry of entries) {
|
|
66
|
-
const srcPath = join(dir, entry.name);
|
|
67
|
-
const destPath = join(targetDir, entry.name);
|
|
68
|
-
|
|
69
|
-
if (entry.isDirectory()) {
|
|
70
|
-
// Create subdirectory in target
|
|
71
|
-
const subDestPath = join(targetDir, entry.name);
|
|
72
|
-
if (!existsSync(subDestPath)) {
|
|
73
|
-
mkdirSync(subDestPath, { recursive: true });
|
|
74
|
-
}
|
|
75
|
-
await processDirectory(srcPath, subDestPath);
|
|
76
|
-
} else if (entry.isFile()) {
|
|
77
|
-
const ext = extname(entry.name).toLowerCase();
|
|
78
|
-
|
|
79
|
-
if (imageExtensions.includes(ext)) {
|
|
80
|
-
try {
|
|
81
|
-
const result = await optimizeImage(srcPath, destPath);
|
|
82
|
-
if (result) {
|
|
83
|
-
optimized++;
|
|
84
|
-
totalSaved += result.saved;
|
|
85
|
-
const savedPercent = ((result.saved / result.originalSize) * 100).toFixed(1);
|
|
86
|
-
logger.debug(
|
|
87
|
-
`✨ ${entry.name}: ${(result.originalSize / 1024).toFixed(1)}KB → ${(result.newSize / 1024).toFixed(1)}KB (-${savedPercent}%)`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
} catch (error) {
|
|
91
|
-
logger.warn(`⚠️ Failed to optimize ${entry.name}: ${error.message}`);
|
|
92
|
-
// Fallback: just copy the file
|
|
93
|
-
try {
|
|
94
|
-
cpSync(srcPath, destPath);
|
|
95
|
-
logger.debug(`📋 Copied ${entry.name} (fallback)`);
|
|
96
|
-
} catch (copyError) {
|
|
97
|
-
logger.error(`❌ Failed to copy ${entry.name}: ${copyError.message}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
await processDirectory(srcDir, outDir);
|
|
106
|
-
|
|
107
|
-
if (optimized > 0) {
|
|
108
|
-
logger.success(
|
|
109
|
-
`✅ Optimized ${optimized} images (saved ${(totalSaved / 1024).toFixed(2)}KB total)`
|
|
110
|
-
);
|
|
111
|
-
} else {
|
|
112
|
-
logger.info(`📋 No images optimized (copied ${countFilesInDir(outDir)} files)`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return { optimized, saved: totalSaved };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Count files in directory (for logging)
|
|
120
|
-
*/
|
|
121
|
-
function countFilesInDir(dir) {
|
|
122
|
-
if (!existsSync(dir)) return 0;
|
|
123
|
-
|
|
124
|
-
let count = 0;
|
|
125
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
126
|
-
|
|
127
|
-
for (const entry of entries) {
|
|
128
|
-
if (entry.isFile()) {
|
|
129
|
-
count++;
|
|
130
|
-
} else if (entry.isDirectory()) {
|
|
131
|
-
count += countFilesInDir(join(dir, entry.name));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return count;
|
|
12
|
+
// Alias for copyImages to maintain API
|
|
13
|
+
logger.info(`📁 Copying from ${srcDir} to ${outDir}...`);
|
|
14
|
+
const copied = copyImages(srcDir, outDir);
|
|
15
|
+
return { optimized: 0, saved: 0, copied };
|
|
136
16
|
}
|
|
137
17
|
|
|
138
|
-
/**
|
|
139
|
-
* Optimize a single image using WASM codecs
|
|
140
|
-
*/
|
|
141
|
-
async function optimizeImage(srcPath, destPath) {
|
|
142
|
-
const ext = extname(srcPath).toLowerCase();
|
|
143
|
-
const originalFile = Bun.file(srcPath);
|
|
144
|
-
|
|
145
|
-
// Check if file exists
|
|
146
|
-
if (!await originalFile.exists()) {
|
|
147
|
-
throw new Error(`File not found: ${srcPath}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const originalSize = originalFile.size;
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
// For SVG and GIF, just copy (no optimization needed/supported)
|
|
154
|
-
if (ext === '.svg' || ext === '.gif') {
|
|
155
|
-
cpSync(srcPath, destPath);
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Read the original image
|
|
160
|
-
const originalBuffer = await originalFile.arrayBuffer();
|
|
161
|
-
|
|
162
|
-
let optimizedBuffer;
|
|
163
|
-
|
|
164
|
-
if (ext === '.png') {
|
|
165
|
-
await initializePNG();
|
|
166
|
-
|
|
167
|
-
// Decode → Re-encode with compression
|
|
168
|
-
const imageData = await pngDecode(originalBuffer);
|
|
169
|
-
|
|
170
|
-
// Encode with oxipng-level compression (quality 85, similar to oxipng -o 2)
|
|
171
|
-
optimizedBuffer = await pngEncode(imageData, {
|
|
172
|
-
quality: 85,
|
|
173
|
-
effort: 2 // 0-10, higher = better compression but slower
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
} else if (ext === '.jpg' || ext === '.jpeg') {
|
|
177
|
-
await initializeJPEG();
|
|
178
|
-
|
|
179
|
-
// Decode → Re-encode with quality 85 (mozjpeg-like quality)
|
|
180
|
-
const imageData = await jpegDecode(originalBuffer);
|
|
181
|
-
optimizedBuffer = await jpegEncode(imageData, { quality: 85 });
|
|
182
|
-
|
|
183
|
-
} else if (ext === '.webp') {
|
|
184
|
-
await initializeWebP();
|
|
185
|
-
|
|
186
|
-
// WebP optimization
|
|
187
|
-
const imageData = await webpDecode(originalBuffer);
|
|
188
|
-
optimizedBuffer = await webpEncode(imageData, { quality: 85 });
|
|
189
|
-
} else {
|
|
190
|
-
// Unsupported format, just copy
|
|
191
|
-
cpSync(srcPath, destPath);
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Only save if we actually reduced the size
|
|
196
|
-
if (optimizedBuffer && optimizedBuffer.byteLength < originalSize) {
|
|
197
|
-
await Bun.write(destPath, optimizedBuffer);
|
|
198
|
-
const saved = originalSize - optimizedBuffer.byteLength;
|
|
199
|
-
return { saved, originalSize, newSize: optimizedBuffer.byteLength };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// If optimization didn't help, just copy the original
|
|
203
|
-
cpSync(srcPath, destPath);
|
|
204
|
-
return null;
|
|
205
|
-
|
|
206
|
-
} catch (error) {
|
|
207
|
-
// If anything fails, just copy the original
|
|
208
|
-
logger.warn(`Optimization failed for ${srcPath.split('/').pop()}, copying original`);
|
|
209
|
-
cpSync(srcPath, destPath);
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Check if optimization is available (always true with WASM! 🎉)
|
|
216
|
-
*/
|
|
217
18
|
export async function checkOptimizationTools() {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
await import('@jsquash/png');
|
|
221
|
-
await import('@jsquash/jpeg');
|
|
222
|
-
await import('@jsquash/webp');
|
|
223
|
-
|
|
224
|
-
logger.success('✅ WASM image optimization available');
|
|
225
|
-
logger.info('📦 Using @jsquash (zero OS dependencies!)');
|
|
226
|
-
return ['png', 'jpeg', 'webp'];
|
|
227
|
-
} catch (error) {
|
|
228
|
-
logger.error('❌ WASM codecs not installed. Run: bun add @jsquash/png @jsquash/jpeg @jsquash/webp');
|
|
229
|
-
return [];
|
|
230
|
-
}
|
|
19
|
+
// Always return empty array to disable optimization
|
|
20
|
+
return [];
|
|
231
21
|
}
|
|
232
22
|
|
|
233
|
-
/**
|
|
234
|
-
* Copy images without optimization (fallback)
|
|
235
|
-
*/
|
|
236
23
|
export function copyImages(srcDir, outDir) {
|
|
237
|
-
|
|
24
|
+
// All common image formats
|
|
25
|
+
const imageExtensions = [
|
|
26
|
+
'.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg',
|
|
27
|
+
'.avif', '.ico', '.bmp', '.tiff', '.tif'
|
|
28
|
+
];
|
|
29
|
+
|
|
238
30
|
let copied = 0;
|
|
31
|
+
let skipped = 0;
|
|
239
32
|
|
|
240
|
-
// Check if source directory exists
|
|
241
33
|
if (!existsSync(srcDir)) {
|
|
242
|
-
logger.warn(`⚠️ Source
|
|
34
|
+
logger.warn(`⚠️ Source not found: ${srcDir}`);
|
|
243
35
|
return 0;
|
|
244
36
|
}
|
|
245
37
|
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
mkdirSync(outDir, { recursive: true });
|
|
249
|
-
}
|
|
38
|
+
// Ensure output directory exists
|
|
39
|
+
mkdirSync(outDir, { recursive: true });
|
|
250
40
|
|
|
251
41
|
function processDirectory(dir, targetDir) {
|
|
252
|
-
|
|
42
|
+
try {
|
|
43
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
253
44
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const srcPath = join(dir, entry.name);
|
|
47
|
+
const destPath = join(targetDir, entry.name);
|
|
257
48
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (!existsSync(subDestPath)) {
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
// Recursively process subdirectories
|
|
51
|
+
const subDestPath = join(targetDir, entry.name);
|
|
262
52
|
mkdirSync(subDestPath, { recursive: true });
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} else if (entry.isFile()) {
|
|
267
|
-
const ext = extname(entry.name).toLowerCase();
|
|
53
|
+
processDirectory(srcPath, subDestPath);
|
|
54
|
+
} else if (entry.isFile()) {
|
|
55
|
+
const ext = extname(entry.name).toLowerCase();
|
|
268
56
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
57
|
+
if (imageExtensions.includes(ext)) {
|
|
58
|
+
try {
|
|
59
|
+
cpSync(srcPath, destPath);
|
|
60
|
+
copied++;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.warn(` Failed to copy ${entry.name}: ${error.message}`);
|
|
63
|
+
skipped++;
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
skipped++;
|
|
276
67
|
}
|
|
277
68
|
}
|
|
278
69
|
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error(`Error processing ${dir}: ${error.message}`);
|
|
279
72
|
}
|
|
280
73
|
}
|
|
281
74
|
|
|
282
75
|
processDirectory(srcDir, outDir);
|
|
283
|
-
|
|
76
|
+
|
|
284
77
|
if (copied > 0) {
|
|
285
|
-
logger.
|
|
286
|
-
} else {
|
|
287
|
-
logger.warn(`⚠️ No images found in ${srcDir}`);
|
|
78
|
+
logger.success(`✅ Copied ${copied} image(s) to ${outDir}`);
|
|
288
79
|
}
|
|
289
80
|
|
|
81
|
+
if (skipped > 0) {
|
|
82
|
+
logger.info(`📝 Skipped ${skipped} non-image file(s)`);
|
|
83
|
+
}
|
|
84
|
+
|
|
290
85
|
return copied;
|
|
291
86
|
}
|
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(
|
|
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
|
-
//
|
|
48
|
-
await copyAllStaticAssets(root, outDir
|
|
48
|
+
// SKIP OPTIMIZATION FOR NOW - JUST COPY
|
|
49
|
+
await copyAllStaticAssets(root, outDir);
|
|
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
|
|
89
|
+
logger.success('JavaScript bundled successfully');
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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,64 +136,28 @@ export async function buildProduction(options = {}) {
|
|
|
124
136
|
}
|
|
125
137
|
}
|
|
126
138
|
|
|
127
|
-
|
|
128
|
-
// ✅ FIX 3: Enhanced asset copying with proper directory structure
|
|
129
|
-
async function copyAllStaticAssets(root, outDir, optimize = true) {
|
|
139
|
+
async function copyAllStaticAssets(root, outDir) {
|
|
130
140
|
const publicDir = join(root, 'public');
|
|
131
141
|
const srcImagesDir = join(root, 'src', 'images');
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
let assetsOptimized = 0;
|
|
143
|
+
logger.info('📦 Copying static assets...');
|
|
135
144
|
|
|
136
|
-
|
|
137
|
-
logger.info(` public/: ${existsSync(publicDir) ? '✅ exists' : '❌ not found'}`);
|
|
138
|
-
logger.info(` src/images/: ${existsSync(srcImagesDir) ? '✅ exists' : '❌ not found'}`);
|
|
139
|
-
|
|
140
|
-
// Create images directory in dist/
|
|
141
|
-
const distImagesDir = join(outDir, 'images');
|
|
142
|
-
if (!existsSync(distImagesDir)) {
|
|
143
|
-
mkdirSync(distImagesDir, { recursive: true });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Copy from public/ to root of dist/
|
|
145
|
+
// Copy from public/ to dist/
|
|
147
146
|
if (existsSync(publicDir)) {
|
|
148
|
-
logger.info('Copying public/
|
|
149
|
-
|
|
150
|
-
const result = await optimizeImages(publicDir, outDir);
|
|
151
|
-
assetsOptimized += result.optimized;
|
|
152
|
-
} else {
|
|
153
|
-
assetsCopied += copyImages(publicDir, outDir);
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
logger.info('No public/ directory found, skipping...');
|
|
147
|
+
logger.info(' Copying public/ directory...');
|
|
148
|
+
copyImages(publicDir, outDir);
|
|
157
149
|
}
|
|
158
150
|
|
|
159
|
-
//
|
|
151
|
+
// Copy from src/images/ to dist/images/
|
|
160
152
|
if (existsSync(srcImagesDir)) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const files = readdirSync(srcImagesDir);
|
|
165
|
-
logger.info(`Found ${files.length} items in src/images/: ${files.join(', ')}`);
|
|
166
|
-
|
|
167
|
-
if (optimize) {
|
|
168
|
-
const result = await optimizeImages(srcImagesDir, distImagesDir);
|
|
169
|
-
assetsOptimized += result.optimized;
|
|
170
|
-
} else {
|
|
171
|
-
assetsCopied += copyImages(srcImagesDir, distImagesDir);
|
|
172
|
-
}
|
|
173
|
-
} else {
|
|
174
|
-
logger.info('No src/images/ directory found, skipping...');
|
|
153
|
+
const distImagesDir = join(outDir, 'images');
|
|
154
|
+
logger.info(` Copying src/images/ to ${relative(root, distImagesDir)}/...`);
|
|
155
|
+
copyImages(srcImagesDir, distImagesDir);
|
|
175
156
|
}
|
|
176
157
|
|
|
177
|
-
|
|
178
|
-
logger.success(`🎨 Optimized ${assetsOptimized} images with WASM codecs`);
|
|
179
|
-
} else if (assetsCopied > 0) {
|
|
180
|
-
logger.success(`📋 Copied ${assetsCopied} static assets`);
|
|
181
|
-
} else {
|
|
182
|
-
logger.warn(`⚠️ No static assets found or copied`);
|
|
183
|
-
}
|
|
158
|
+
logger.success('✅ All assets copied');
|
|
184
159
|
}
|
|
160
|
+
|
|
185
161
|
async function buildAllCSS(root, outDir) {
|
|
186
162
|
const srcStylesDir = join(root, 'src', 'styles');
|
|
187
163
|
const stylesOutDir = join(outDir, 'styles');
|
|
@@ -569,63 +545,71 @@ function extractMetaFromSource(code) {
|
|
|
569
545
|
}
|
|
570
546
|
}
|
|
571
547
|
|
|
572
|
-
// ✅ FIX
|
|
548
|
+
// ✅ CRITICAL FIX: Generate HTML files
|
|
573
549
|
async function generateProductionHTML(root, outDir, buildResult, routes) {
|
|
550
|
+
if (routes.length === 0) {
|
|
551
|
+
logger.warn('No routes found, skipping HTML generation');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
logger.info(`Generating HTML files for ${routes.length} routes...`);
|
|
556
|
+
|
|
557
|
+
// Find main JS bundle
|
|
574
558
|
const mainBundle = buildResult.outputs.find(o =>
|
|
575
559
|
o.path.includes('main') && o.kind === 'entry-point'
|
|
576
560
|
);
|
|
577
561
|
|
|
578
562
|
if (!mainBundle) {
|
|
579
|
-
|
|
563
|
+
logger.error('Could not find main bundle in build output');
|
|
564
|
+
// List all outputs for debugging
|
|
565
|
+
logger.info('Available outputs:');
|
|
566
|
+
buildResult.outputs.forEach((o, i) => {
|
|
567
|
+
logger.info(` ${i + 1}. ${o.path} (${o.kind})`);
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
580
570
|
}
|
|
581
571
|
|
|
582
572
|
const bundlePath = relative(outDir, mainBundle.path).replace(/\\/g, '/');
|
|
583
|
-
logger.info(`Main bundle
|
|
573
|
+
logger.info(`Main bundle: ${bundlePath}`);
|
|
584
574
|
|
|
585
|
-
|
|
586
|
-
let userStylesheets = '';
|
|
587
|
-
|
|
588
|
-
if (existsSync(srcStylesDir)) {
|
|
589
|
-
const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
|
|
590
|
-
userStylesheets = cssFiles.map(f =>
|
|
591
|
-
` <link rel="stylesheet" href="/styles/${f.replace('.css', '.min.css')}">`
|
|
592
|
-
).join('\n');
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// ✅ Load config for default meta
|
|
575
|
+
// Load config for default meta
|
|
596
576
|
const { loadConfig } = await import('./config/loadConfig.js');
|
|
597
577
|
const config = await loadConfig(root);
|
|
598
578
|
const defaultMeta = config.meta || {};
|
|
599
579
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
// ✅ FIX: Generate HTML for ALL routes (including dynamic as fallback)
|
|
580
|
+
// Generate HTML for each route
|
|
603
581
|
for (const route of routes) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
582
|
+
try {
|
|
583
|
+
const sourceCode = await Bun.file(route.path).text();
|
|
584
|
+
const pageMeta = extractMetaFromSource(sourceCode);
|
|
585
|
+
const meta = { ...defaultMeta, ...pageMeta };
|
|
586
|
+
|
|
587
|
+
const html = generateHTML(meta, route, bundlePath);
|
|
588
|
+
|
|
589
|
+
let htmlPath;
|
|
590
|
+
if (route.route === '/') {
|
|
591
|
+
htmlPath = join(outDir, 'index.html');
|
|
592
|
+
} else {
|
|
593
|
+
// Create directory for the route
|
|
594
|
+
const routeDir = join(outDir, route.route.slice(1)); // Remove leading slash
|
|
595
|
+
mkdirSync(routeDir, { recursive: true });
|
|
596
|
+
htmlPath = join(routeDir, 'index.html');
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
await Bun.write(htmlPath, html);
|
|
600
|
+
logger.success(`Generated: ${route.route === '/' ? 'index.html' : route.route + '/index.html'}`);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
logger.error(`Failed to generate HTML for ${route.route}: ${error.message}`);
|
|
621
603
|
}
|
|
622
|
-
|
|
623
|
-
await Bun.write(htmlPath, html);
|
|
624
|
-
logger.success(`Generated ${route.route === '/' ? 'index.html' : route.route + '/index.html'}`);
|
|
625
604
|
}
|
|
626
605
|
}
|
|
627
606
|
|
|
628
|
-
function generateHTML(meta, route, bundlePath
|
|
607
|
+
function generateHTML(meta, route, bundlePath) {
|
|
608
|
+
const cssFiles = ['global.min.css', 'home.min.css'];
|
|
609
|
+
const stylesheets = cssFiles.map(css =>
|
|
610
|
+
` <link rel="stylesheet" href="/styles/${css}">`
|
|
611
|
+
).join('\n');
|
|
612
|
+
|
|
629
613
|
return `<!DOCTYPE html>
|
|
630
614
|
<html lang="${meta.lang || 'en'}">
|
|
631
615
|
<head>
|
|
@@ -636,23 +620,10 @@ function generateHTML(meta, route, bundlePath, userStylesheets) {
|
|
|
636
620
|
<meta name="description" content="${meta.description || 'Built with BertUI - Lightning fast React development'}">
|
|
637
621
|
${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
|
|
638
622
|
${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
|
|
639
|
-
${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
|
|
640
|
-
|
|
641
|
-
<meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
|
|
642
|
-
<meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
|
|
643
|
-
${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
|
|
644
|
-
<meta property="og:type" content="website">
|
|
645
|
-
<meta property="og:url" content="${route.route}">
|
|
646
|
-
|
|
647
|
-
<meta name="twitter:card" content="summary_large_image">
|
|
648
|
-
<meta name="twitter:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
|
|
649
|
-
<meta name="twitter:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
|
|
650
|
-
${meta.ogImage ? `<meta name="twitter:image" content="${meta.ogImage}">` : ''}
|
|
651
623
|
|
|
652
624
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
653
|
-
<link rel="canonical" href="${route.route}">
|
|
654
625
|
|
|
655
|
-
${
|
|
626
|
+
${stylesheets}
|
|
656
627
|
|
|
657
628
|
<script type="importmap">
|
|
658
629
|
{
|