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 +1 -1
- package/src/build/image-optimizer.js +216 -0
- package/src/build.js +97 -31
- package/src/client/compiler.js +16 -24
- package/src/server/dev-server.js +333 -6
package/package.json
CHANGED
|
@@ -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';
|
|
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);
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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);
|
|
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);
|
|
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);
|
|
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);
|
|
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
|
-
|
|
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, '');
|
package/src/client/compiler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
package/src/server/dev-server.js
CHANGED
|
@@ -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';
|
|
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
|
-
'.
|
|
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
|
-
|
|
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
|
});
|