bertui 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/build.js +133 -32
- package/src/server/dev-server.js +2 -1
package/package.json
CHANGED
package/src/build.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function buildProduction(options = {}) {
|
|
|
26
26
|
|
|
27
27
|
try {
|
|
28
28
|
logger.info('Step 1: Compiling for production...');
|
|
29
|
-
await compileForBuild(root, buildDir);
|
|
29
|
+
const { routes } = await compileForBuild(root, buildDir);
|
|
30
30
|
logger.success('Production compilation complete');
|
|
31
31
|
|
|
32
32
|
logger.info('Step 2: Building CSS with Lightning CSS...');
|
|
@@ -35,7 +35,14 @@ export async function buildProduction(options = {}) {
|
|
|
35
35
|
const publicDir = join(root, 'public');
|
|
36
36
|
if (existsSync(publicDir)) {
|
|
37
37
|
logger.info('Step 3: Copying public assets...');
|
|
38
|
-
|
|
38
|
+
const publicFiles = readdirSync(publicDir);
|
|
39
|
+
for (const file of publicFiles) {
|
|
40
|
+
const srcFile = join(publicDir, file);
|
|
41
|
+
const destFile = join(outDir, file);
|
|
42
|
+
if (statSync(srcFile).isFile()) {
|
|
43
|
+
cpSync(srcFile, destFile);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
39
46
|
logger.success('Public assets copied');
|
|
40
47
|
} else {
|
|
41
48
|
logger.info('Step 3: No public directory found, skipping...');
|
|
@@ -72,8 +79,8 @@ export async function buildProduction(options = {}) {
|
|
|
72
79
|
|
|
73
80
|
logger.success('JavaScript bundled with tree-shaking');
|
|
74
81
|
|
|
75
|
-
logger.info('Step 5: Generating
|
|
76
|
-
await generateProductionHTML(root, outDir, result);
|
|
82
|
+
logger.info('Step 5: Generating SEO-optimized HTML files...');
|
|
83
|
+
await generateProductionHTML(root, outDir, result, routes);
|
|
77
84
|
|
|
78
85
|
rmSync(buildDir, { recursive: true });
|
|
79
86
|
logger.info('Cleaned up .bertuibuild/');
|
|
@@ -144,6 +151,8 @@ async function compileForBuild(root, buildDir) {
|
|
|
144
151
|
await generateBuildRouter(routes, buildDir);
|
|
145
152
|
logger.info('Generated router for build');
|
|
146
153
|
}
|
|
154
|
+
|
|
155
|
+
return { routes };
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
async function discoverRoutes(pagesDir) {
|
|
@@ -356,7 +365,6 @@ async function compileBuildDirectory(srcDir, buildDir, root) {
|
|
|
356
365
|
const outPath = join(buildDir, file);
|
|
357
366
|
let code = await Bun.file(srcPath).text();
|
|
358
367
|
|
|
359
|
-
// CRITICAL FIX: Remove CSS imports
|
|
360
368
|
code = removeCSSImports(code);
|
|
361
369
|
code = fixBuildImports(code, srcPath, outPath, root);
|
|
362
370
|
|
|
@@ -373,7 +381,6 @@ async function compileBuildFile(srcPath, buildDir, filename, root) {
|
|
|
373
381
|
try {
|
|
374
382
|
let code = await Bun.file(srcPath).text();
|
|
375
383
|
|
|
376
|
-
// CRITICAL FIX: Remove CSS imports before transpilation
|
|
377
384
|
code = removeCSSImports(code);
|
|
378
385
|
|
|
379
386
|
const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
|
|
@@ -407,7 +414,6 @@ async function compileBuildFile(srcPath, buildDir, filename, root) {
|
|
|
407
414
|
}
|
|
408
415
|
}
|
|
409
416
|
|
|
410
|
-
// NEW FUNCTION: Remove all CSS imports
|
|
411
417
|
function removeCSSImports(code) {
|
|
412
418
|
code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
|
|
413
419
|
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
@@ -442,36 +448,67 @@ function fixRelativeImports(code) {
|
|
|
442
448
|
return code;
|
|
443
449
|
}
|
|
444
450
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
451
|
+
// IMPROVED: Extract meta using regex (works on raw source code)
|
|
452
|
+
function extractMetaFromSource(code) {
|
|
453
|
+
try {
|
|
454
|
+
// Match: export const meta = { ... };
|
|
455
|
+
const metaRegex = /export\s+const\s+meta\s*=\s*\{([^}]+)\}/s;
|
|
456
|
+
const match = code.match(metaRegex);
|
|
457
|
+
|
|
458
|
+
if (!match) return null;
|
|
459
|
+
|
|
460
|
+
const metaContent = match[1];
|
|
461
|
+
const meta = {};
|
|
462
|
+
|
|
463
|
+
// Extract key-value pairs
|
|
464
|
+
const pairs = metaContent.match(/(\w+)\s*:\s*(['"`])((?:(?!\2).)*)\2/g);
|
|
465
|
+
|
|
466
|
+
if (!pairs) return null;
|
|
467
|
+
|
|
468
|
+
pairs.forEach(pair => {
|
|
469
|
+
const [key, value] = pair.split(':').map(s => s.trim());
|
|
470
|
+
meta[key] = value.replace(/['"]/g, '');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
return meta;
|
|
474
|
+
} catch (error) {
|
|
475
|
+
return null;
|
|
465
476
|
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// IMPROVED: Render component to static HTML using JSDOM-like approach
|
|
480
|
+
async function renderComponentToHTML(route, bundlePath, userStylesheets, meta) {
|
|
481
|
+
// For now, we'll create a basic shell with meta tags
|
|
482
|
+
// Full SSR would require running React server-side
|
|
466
483
|
|
|
467
484
|
const html = `<!DOCTYPE html>
|
|
468
485
|
<html lang="en">
|
|
469
486
|
<head>
|
|
470
487
|
<meta charset="UTF-8">
|
|
471
488
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
472
|
-
<meta
|
|
473
|
-
|
|
489
|
+
<title>${meta.title || 'BertUI App'}</title>
|
|
490
|
+
|
|
491
|
+
<meta name="description" content="${meta.description || 'Built with BertUI - Lightning fast React development'}">
|
|
492
|
+
${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
|
|
493
|
+
${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
|
|
494
|
+
${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
|
|
495
|
+
|
|
496
|
+
<meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
|
|
497
|
+
<meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
|
|
498
|
+
${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
|
|
499
|
+
<meta property="og:type" content="website">
|
|
500
|
+
<meta property="og:url" content="${route}">
|
|
501
|
+
|
|
502
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
503
|
+
<meta name="twitter:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
|
|
504
|
+
<meta name="twitter:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
|
|
505
|
+
${meta.ogImage ? `<meta name="twitter:image" content="${meta.ogImage}">` : ''}
|
|
506
|
+
|
|
507
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
508
|
+
<link rel="canonical" href="${route}">
|
|
509
|
+
|
|
474
510
|
${userStylesheets}
|
|
511
|
+
|
|
475
512
|
<script type="importmap">
|
|
476
513
|
{
|
|
477
514
|
"imports": {
|
|
@@ -482,13 +519,77 @@ ${userStylesheets}
|
|
|
482
519
|
}
|
|
483
520
|
}
|
|
484
521
|
</script>
|
|
522
|
+
|
|
523
|
+
<!-- SEO Preload Hints -->
|
|
524
|
+
<link rel="preconnect" href="https://esm.sh">
|
|
525
|
+
<link rel="dns-prefetch" href="https://esm.sh">
|
|
485
526
|
</head>
|
|
486
527
|
<body>
|
|
487
|
-
<div id="root"
|
|
528
|
+
<div id="root">
|
|
529
|
+
<!-- App shell - JavaScript will hydrate this -->
|
|
530
|
+
<div style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:system-ui">
|
|
531
|
+
<div style="text-align:center">
|
|
532
|
+
<h1 style="font-size:2rem;margin-bottom:1rem">${meta.title || 'Loading...'}</h1>
|
|
533
|
+
<p style="color:#666">${meta.description || 'Please wait while we load your content'}</p>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
488
537
|
<script type="module" src="/${bundlePath}"></script>
|
|
489
538
|
</body>
|
|
490
539
|
</html>`;
|
|
491
540
|
|
|
492
|
-
|
|
493
|
-
|
|
541
|
+
return html;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function generateProductionHTML(root, outDir, buildResult, routes) {
|
|
545
|
+
const mainBundle = buildResult.outputs.find(o =>
|
|
546
|
+
o.path.includes('main') && o.kind === 'entry-point'
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
if (!mainBundle) {
|
|
550
|
+
throw new Error('Could not find main bundle in build output');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const bundlePath = mainBundle.path.replace(outDir, '').replace(/^[\/\\]/, '');
|
|
554
|
+
|
|
555
|
+
const srcStylesDir = join(root, 'src', 'styles');
|
|
556
|
+
let userStylesheets = '';
|
|
557
|
+
|
|
558
|
+
if (existsSync(srcStylesDir)) {
|
|
559
|
+
const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
|
|
560
|
+
userStylesheets = cssFiles.map(f =>
|
|
561
|
+
` <link rel="stylesheet" href="/styles/${f.replace('.css', '.min.css')}">`
|
|
562
|
+
).join('\n');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
logger.info('Generating SEO-optimized HTML files...');
|
|
566
|
+
|
|
567
|
+
for (const route of routes) {
|
|
568
|
+
if (route.type === 'dynamic') {
|
|
569
|
+
logger.info(`Skipping dynamic route: ${route.route}`);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Read source file and extract meta
|
|
574
|
+
const sourceCode = await Bun.file(route.path).text();
|
|
575
|
+
const meta = extractMetaFromSource(sourceCode) || {};
|
|
576
|
+
|
|
577
|
+
logger.info(`Extracting meta for ${route.route}: ${JSON.stringify(meta)}`);
|
|
578
|
+
|
|
579
|
+
// Generate HTML with meta tags and app shell
|
|
580
|
+
const html = await renderComponentToHTML(route.route, bundlePath, userStylesheets, meta);
|
|
581
|
+
|
|
582
|
+
// Determine output path
|
|
583
|
+
let htmlPath;
|
|
584
|
+
if (route.route === '/') {
|
|
585
|
+
htmlPath = join(outDir, 'index.html');
|
|
586
|
+
} else {
|
|
587
|
+
const routeDir = join(outDir, route.route);
|
|
588
|
+
mkdirSync(routeDir, { recursive: true });
|
|
589
|
+
htmlPath = join(routeDir, 'index.html');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
await Bun.write(htmlPath, html);
|
|
593
|
+
logger.success(`Generated ${route.route === '/' ? 'index.html' : route.route + '/index.html'} with meta: ${meta.title || 'default'}`);
|
|
594
|
+
}
|
|
494
595
|
}
|
package/src/server/dev-server.js
CHANGED
|
@@ -166,6 +166,7 @@ ws.onclose = () => {
|
|
|
166
166
|
});
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
+
// Around line 60, update the route handler:
|
|
169
170
|
.get('/public/*', async ({ params, set }) => {
|
|
170
171
|
const publicDir = join(root, 'public');
|
|
171
172
|
const filepath = join(publicDir, params['*']);
|
|
@@ -225,7 +226,7 @@ function serveHTML(root, hasRouter, config) {
|
|
|
225
226
|
${meta.ogDescription ? `<meta property="og:description" content="${meta.ogDescription || meta.description}">` : ''}
|
|
226
227
|
${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
|
|
227
228
|
|
|
228
|
-
<link rel="icon" type="image/svg+xml" href="/
|
|
229
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
229
230
|
|
|
230
231
|
${userStylesheets}
|
|
231
232
|
|