bertui 0.3.9 → 0.4.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/package.json +1 -1
- package/src/build.js +97 -31
- package/src/client/compiler.js +16 -24
- package/src/server/dev-server.js +244 -4
package/package.json
CHANGED
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,7 @@
|
|
|
1
1
|
import { Elysia } from 'elysia';
|
|
2
2
|
import { watch } from 'fs';
|
|
3
3
|
import { join, extname } from 'path';
|
|
4
|
-
import { existsSync, readdirSync } from 'fs';
|
|
4
|
+
import { existsSync, readdirSync } from 'fs';
|
|
5
5
|
import logger from '../logger/logger.js';
|
|
6
6
|
import { compileProject } from '../client/compiler.js';
|
|
7
7
|
import { loadConfig } from '../config/loadConfig.js';
|
|
@@ -32,7 +32,6 @@ export async function startDevServer(options = {}) {
|
|
|
32
32
|
const path = params['*'];
|
|
33
33
|
|
|
34
34
|
if (path.includes('.')) {
|
|
35
|
-
// Handle compiled directory files
|
|
36
35
|
if (path.startsWith('compiled/')) {
|
|
37
36
|
const filePath = join(compiledDir, path.replace('compiled/', ''));
|
|
38
37
|
const file = Bun.file(filePath);
|
|
@@ -50,7 +49,6 @@ export async function startDevServer(options = {}) {
|
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
// Handle CSS files from .bertui/styles
|
|
54
52
|
if (path.startsWith('styles/') && path.endsWith('.css')) {
|
|
55
53
|
const cssPath = join(stylesDir, path.replace('styles/', ''));
|
|
56
54
|
const file = Bun.file(cssPath);
|
|
@@ -101,6 +99,14 @@ ws.onmessage = (event) => {
|
|
|
101
99
|
if (data.type === 'recompiling') {
|
|
102
100
|
console.log('%c⚙️ Recompiling...', 'color: #3b82f6');
|
|
103
101
|
}
|
|
102
|
+
|
|
103
|
+
if (data.type === 'compilation-error') {
|
|
104
|
+
console.error('%c❌ Compilation Error', 'color: #ef4444; font-weight: bold');
|
|
105
|
+
console.error(data.message);
|
|
106
|
+
if (window.__BERTUI_SHOW_ERROR__) {
|
|
107
|
+
window.__BERTUI_SHOW_ERROR__(data);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
104
110
|
};
|
|
105
111
|
|
|
106
112
|
ws.onerror = (error) => {
|
|
@@ -116,6 +122,226 @@ ws.onclose = () => {
|
|
|
116
122
|
headers: { 'Content-Type': 'application/javascript' }
|
|
117
123
|
});
|
|
118
124
|
})
|
|
125
|
+
|
|
126
|
+
.get('/error-overlay.js', () => {
|
|
127
|
+
const errorOverlayScript = `
|
|
128
|
+
(function() {
|
|
129
|
+
'use strict';
|
|
130
|
+
|
|
131
|
+
let overlayElement = null;
|
|
132
|
+
|
|
133
|
+
function createOverlay() {
|
|
134
|
+
if (overlayElement) return overlayElement;
|
|
135
|
+
|
|
136
|
+
const overlay = document.createElement('div');
|
|
137
|
+
overlay.id = 'bertui-error-overlay';
|
|
138
|
+
overlay.style.cssText = \`
|
|
139
|
+
position: fixed;
|
|
140
|
+
top: 0;
|
|
141
|
+
left: 0;
|
|
142
|
+
width: 100%;
|
|
143
|
+
height: 100%;
|
|
144
|
+
background: rgba(0, 0, 0, 0.95);
|
|
145
|
+
color: #fff;
|
|
146
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
|
147
|
+
font-size: 14px;
|
|
148
|
+
line-height: 1.5;
|
|
149
|
+
z-index: 9999999;
|
|
150
|
+
overflow: auto;
|
|
151
|
+
padding: 20px;
|
|
152
|
+
box-sizing: border-box;
|
|
153
|
+
display: none;
|
|
154
|
+
\`;
|
|
155
|
+
document.body.appendChild(overlay);
|
|
156
|
+
overlayElement = overlay;
|
|
157
|
+
return overlay;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function showError(error) {
|
|
161
|
+
const overlay = createOverlay();
|
|
162
|
+
|
|
163
|
+
const errorType = error.type || 'Runtime Error';
|
|
164
|
+
const errorMessage = error.message || 'Unknown error';
|
|
165
|
+
const errorStack = error.stack || '';
|
|
166
|
+
const errorFile = error.file || 'Unknown file';
|
|
167
|
+
const errorLine = error.line || '';
|
|
168
|
+
const errorColumn = error.column || '';
|
|
169
|
+
|
|
170
|
+
overlay.innerHTML = \`
|
|
171
|
+
<div style="max-width: 1200px; margin: 0 auto;">
|
|
172
|
+
<div style="display: flex; align-items: center; margin-bottom: 30px;">
|
|
173
|
+
<div style="
|
|
174
|
+
background: #ef4444;
|
|
175
|
+
width: 50px;
|
|
176
|
+
height: 50px;
|
|
177
|
+
border-radius: 50%;
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
justify-content: center;
|
|
181
|
+
font-size: 24px;
|
|
182
|
+
margin-right: 15px;
|
|
183
|
+
">❌</div>
|
|
184
|
+
<div>
|
|
185
|
+
<h1 style="margin: 0; font-size: 28px; font-weight: bold; color: #ef4444;">
|
|
186
|
+
\${errorType}
|
|
187
|
+
</h1>
|
|
188
|
+
<p style="margin: 5px 0 0 0; color: #a0a0a0; font-size: 14px;">
|
|
189
|
+
BertUI detected an error in your application
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div style="
|
|
195
|
+
background: #1a1a1a;
|
|
196
|
+
border: 1px solid #333;
|
|
197
|
+
border-radius: 8px;
|
|
198
|
+
padding: 20px;
|
|
199
|
+
margin-bottom: 20px;
|
|
200
|
+
">
|
|
201
|
+
<div style="color: #fbbf24; font-weight: bold; margin-bottom: 10px;">
|
|
202
|
+
\${errorFile}\${errorLine ? ':' + errorLine : ''}\${errorColumn ? ':' + errorColumn : ''}
|
|
203
|
+
</div>
|
|
204
|
+
<div style="color: #fff; white-space: pre-wrap; word-break: break-word;">
|
|
205
|
+
\${escapeHtml(errorMessage)}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
\${errorStack ? \`
|
|
210
|
+
<div style="
|
|
211
|
+
background: #0a0a0a;
|
|
212
|
+
border: 1px solid #222;
|
|
213
|
+
border-radius: 8px;
|
|
214
|
+
padding: 20px;
|
|
215
|
+
margin-bottom: 20px;
|
|
216
|
+
">
|
|
217
|
+
<div style="color: #a0a0a0; font-weight: bold; margin-bottom: 10px;">
|
|
218
|
+
Stack Trace:
|
|
219
|
+
</div>
|
|
220
|
+
<pre style="
|
|
221
|
+
margin: 0;
|
|
222
|
+
color: #d0d0d0;
|
|
223
|
+
white-space: pre-wrap;
|
|
224
|
+
word-break: break-word;
|
|
225
|
+
font-size: 12px;
|
|
226
|
+
">\${escapeHtml(errorStack)}</pre>
|
|
227
|
+
</div>
|
|
228
|
+
\` : ''}
|
|
229
|
+
|
|
230
|
+
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
|
|
231
|
+
<button onclick="window.__BERTUI_HIDE_ERROR__()" style="
|
|
232
|
+
background: #3b82f6;
|
|
233
|
+
color: white;
|
|
234
|
+
border: none;
|
|
235
|
+
padding: 12px 24px;
|
|
236
|
+
border-radius: 6px;
|
|
237
|
+
font-size: 14px;
|
|
238
|
+
font-weight: 600;
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
">Dismiss (Esc)</button>
|
|
241
|
+
<button onclick="window.location.reload()" style="
|
|
242
|
+
background: #10b981;
|
|
243
|
+
color: white;
|
|
244
|
+
border: none;
|
|
245
|
+
padding: 12px 24px;
|
|
246
|
+
border-radius: 6px;
|
|
247
|
+
font-size: 14px;
|
|
248
|
+
font-weight: 600;
|
|
249
|
+
cursor: pointer;
|
|
250
|
+
">Reload Page</button>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div style="
|
|
254
|
+
margin-top: 30px;
|
|
255
|
+
padding-top: 20px;
|
|
256
|
+
border-top: 1px solid #333;
|
|
257
|
+
color: #666;
|
|
258
|
+
font-size: 12px;
|
|
259
|
+
">
|
|
260
|
+
💡 <strong>Tip:</strong> Fix the error in your code, and the page will automatically reload with HMR.
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
\`;
|
|
264
|
+
|
|
265
|
+
overlay.style.display = 'block';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function hideError() {
|
|
269
|
+
if (overlayElement) {
|
|
270
|
+
overlayElement.style.display = 'none';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function escapeHtml(text) {
|
|
275
|
+
const div = document.createElement('div');
|
|
276
|
+
div.textContent = text;
|
|
277
|
+
return div.innerHTML;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parseErrorStack(error) {
|
|
281
|
+
const stack = error.stack || '';
|
|
282
|
+
const lines = stack.split('\\n');
|
|
283
|
+
|
|
284
|
+
for (const line of lines) {
|
|
285
|
+
const match = line.match(/\\((.+):(\\d+):(\\d+)\\)/) ||
|
|
286
|
+
line.match(/at (.+):(\\d+):(\\d+)/) ||
|
|
287
|
+
line.match(/(.+):(\\d+):(\\d+)/);
|
|
288
|
+
|
|
289
|
+
if (match) {
|
|
290
|
+
return {
|
|
291
|
+
file: match[1].trim(),
|
|
292
|
+
line: match[2],
|
|
293
|
+
column: match[3]
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { file: null, line: null, column: null };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
window.addEventListener('error', function(event) {
|
|
302
|
+
const { file, line, column } = parseErrorStack(event.error || {});
|
|
303
|
+
|
|
304
|
+
showError({
|
|
305
|
+
type: 'Runtime Error',
|
|
306
|
+
message: event.message,
|
|
307
|
+
stack: event.error ? event.error.stack : null,
|
|
308
|
+
file: event.filename || file,
|
|
309
|
+
line: event.lineno || line,
|
|
310
|
+
column: event.colno || column
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
window.addEventListener('unhandledrejection', function(event) {
|
|
315
|
+
const error = event.reason;
|
|
316
|
+
const { file, line, column } = parseErrorStack(error);
|
|
317
|
+
|
|
318
|
+
showError({
|
|
319
|
+
type: 'Unhandled Promise Rejection',
|
|
320
|
+
message: error?.message || String(event.reason),
|
|
321
|
+
stack: error?.stack,
|
|
322
|
+
file,
|
|
323
|
+
line,
|
|
324
|
+
column
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
window.__BERTUI_SHOW_ERROR__ = showError;
|
|
329
|
+
window.__BERTUI_HIDE_ERROR__ = hideError;
|
|
330
|
+
|
|
331
|
+
document.addEventListener('keydown', function(e) {
|
|
332
|
+
if (e.key === 'Escape') {
|
|
333
|
+
hideError();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
console.log('%c🔥 BertUI Error Overlay Active', 'color: #10b981; font-weight: bold; font-size: 14px');
|
|
338
|
+
})();
|
|
339
|
+
`;
|
|
340
|
+
|
|
341
|
+
return new Response(errorOverlayScript, {
|
|
342
|
+
headers: { 'Content-Type': 'application/javascript' }
|
|
343
|
+
});
|
|
344
|
+
})
|
|
119
345
|
|
|
120
346
|
.ws('/hmr', {
|
|
121
347
|
open(ws) {
|
|
@@ -198,7 +424,6 @@ ws.onclose = () => {
|
|
|
198
424
|
function serveHTML(root, hasRouter, config) {
|
|
199
425
|
const meta = config.meta || {};
|
|
200
426
|
|
|
201
|
-
// ✅ FIXED: Proper ESM import for fs
|
|
202
427
|
const srcStylesDir = join(root, 'src', 'styles');
|
|
203
428
|
let userStylesheets = '';
|
|
204
429
|
|
|
@@ -254,6 +479,7 @@ ${userStylesheets}
|
|
|
254
479
|
</head>
|
|
255
480
|
<body>
|
|
256
481
|
<div id="root"></div>
|
|
482
|
+
<script type="module" src="/error-overlay.js"></script>
|
|
257
483
|
<script type="module" src="/hmr-client.js"></script>
|
|
258
484
|
<script type="module" src="/compiled/main.js"></script>
|
|
259
485
|
</body>
|
|
@@ -324,6 +550,20 @@ function setupWatcher(root, compiledDir, clients, onRecompile) {
|
|
|
324
550
|
}
|
|
325
551
|
} catch (error) {
|
|
326
552
|
logger.error(`Recompilation failed: ${error.message}`);
|
|
553
|
+
|
|
554
|
+
// Send compilation error to clients
|
|
555
|
+
for (const client of clients) {
|
|
556
|
+
try {
|
|
557
|
+
client.send(JSON.stringify({
|
|
558
|
+
type: 'compilation-error',
|
|
559
|
+
message: error.message,
|
|
560
|
+
stack: error.stack,
|
|
561
|
+
file: filename
|
|
562
|
+
}));
|
|
563
|
+
} catch (e) {
|
|
564
|
+
clients.delete(client);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
327
567
|
}
|
|
328
568
|
}
|
|
329
569
|
});
|