bertui 0.3.5 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/build.js +93 -55
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Lightning-fast React dev server powered by Bun and Elysia",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/src/build.js CHANGED
@@ -32,15 +32,16 @@ export async function buildProduction(options = {}) {
32
32
  logger.info('Step 2: Building CSS with Lightning CSS...');
33
33
  await buildAllCSS(root, outDir);
34
34
 
35
-
36
35
  const publicDir = join(root, 'public');
37
36
  if (existsSync(publicDir)) {
38
37
  logger.info('Step 3: Copying public assets...');
39
38
  const publicFiles = readdirSync(publicDir);
40
39
  for (const file of publicFiles) {
41
40
  const srcFile = join(publicDir, file);
42
- const destFile = join(outDir, file);
43
- cpSync(srcFile, destFile);
41
+ const destFile = join(outDir, file);
42
+ if (statSync(srcFile).isFile()) {
43
+ cpSync(srcFile, destFile);
44
+ }
44
45
  }
45
46
  logger.success('Public assets copied');
46
47
  } else {
@@ -78,7 +79,7 @@ export async function buildProduction(options = {}) {
78
79
 
79
80
  logger.success('JavaScript bundled with tree-shaking');
80
81
 
81
- logger.info('Step 5: Generating HTML files with page-specific meta...');
82
+ logger.info('Step 5: Generating SEO-optimized HTML files...');
82
83
  await generateProductionHTML(root, outDir, result, routes);
83
84
 
84
85
  rmSync(buildDir, { recursive: true });
@@ -447,80 +448,64 @@ function fixRelativeImports(code) {
447
448
  return code;
448
449
  }
449
450
 
450
- // NEW: Extract meta from page component
451
- async function extractMetaFromPage(pagePath) {
451
+ // IMPROVED: Extract meta using regex (works on raw source code)
452
+ function extractMetaFromSource(code) {
452
453
  try {
453
- const code = await Bun.file(pagePath).text();
454
-
455
- // Match: export const meta = { ... }
456
- const metaRegex = /export\s+const\s+meta\s*=\s*(\{[\s\S]*?\});/;
454
+ // Match: export const meta = { ... };
455
+ const metaRegex = /export\s+const\s+meta\s*=\s*\{([^}]+)\}/s;
457
456
  const match = code.match(metaRegex);
458
457
 
459
458
  if (!match) return null;
460
459
 
461
- // Parse the object (simple eval - production should use AST parser)
462
- const metaStr = match[1];
463
- const meta = eval(`(${metaStr})`);
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
+ });
464
472
 
465
473
  return meta;
466
474
  } catch (error) {
467
- logger.warn(`Failed to extract meta from ${pagePath}: ${error.message}`);
468
475
  return null;
469
476
  }
470
477
  }
471
478
 
472
- async function generateProductionHTML(root, outDir, buildResult, routes) {
473
- const mainBundle = buildResult.outputs.find(o =>
474
- o.path.includes('main') && o.kind === 'entry-point'
475
- );
476
-
477
- if (!mainBundle) {
478
- throw new Error('Could not find main bundle in build output');
479
- }
480
-
481
- const bundlePath = mainBundle.path.replace(outDir, '').replace(/^\//, '');
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
482
483
 
483
- const srcStylesDir = join(root, 'src', 'styles');
484
- let userStylesheets = '';
485
-
486
- if (existsSync(srcStylesDir)) {
487
- const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
488
- userStylesheets = cssFiles.map(f =>
489
- ` <link rel="stylesheet" href="/styles/${f.replace('.css', '.min.css')}">`
490
- ).join('\n');
491
- }
492
-
493
- // Generate HTML for each route
494
- logger.info('Generating HTML files for each route...');
495
-
496
- for (const route of routes) {
497
- if (route.type === 'dynamic') continue; // Skip dynamic routes
498
-
499
- // Extract meta from page component
500
- const pageMeta = await extractMetaFromPage(route.path);
501
-
502
- const meta = pageMeta || {
503
- title: 'BertUI App',
504
- description: 'Built with BertUI - Lightning fast React development'
505
- };
506
-
507
- const html = `<!DOCTYPE html>
484
+ const html = `<!DOCTYPE html>
508
485
  <html lang="en">
509
486
  <head>
510
487
  <meta charset="UTF-8">
511
488
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
512
- <title>${meta.title}</title>
489
+ <title>${meta.title || 'BertUI App'}</title>
513
490
 
514
- <meta name="description" content="${meta.description || ''}">
491
+ <meta name="description" content="${meta.description || 'Built with BertUI - Lightning fast React development'}">
515
492
  ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
516
493
  ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
517
494
  ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
518
495
 
519
- ${meta.ogTitle ? `<meta property="og:title" content="${meta.ogTitle}">` : `<meta property="og:title" content="${meta.title}">`}
520
- ${meta.ogDescription ? `<meta property="og:description" content="${meta.ogDescription}">` : meta.description ? `<meta property="og:description" content="${meta.description}">` : ''}
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'}">
521
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}">` : ''}
522
506
 
523
507
  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
508
+ <link rel="canonical" href="${route}">
524
509
 
525
510
  ${userStylesheets}
526
511
 
@@ -534,12 +519,65 @@ ${userStylesheets}
534
519
  }
535
520
  }
536
521
  </script>
522
+
523
+ <!-- SEO Preload Hints -->
524
+ <link rel="preconnect" href="https://esm.sh">
525
+ <link rel="dns-prefetch" href="https://esm.sh">
537
526
  </head>
538
527
  <body>
539
- <div id="root"></div>
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>
540
537
  <script type="module" src="/${bundlePath}"></script>
541
538
  </body>
542
539
  </html>`;
540
+
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);
543
581
 
544
582
  // Determine output path
545
583
  let htmlPath;
@@ -552,6 +590,6 @@ ${userStylesheets}
552
590
  }
553
591
 
554
592
  await Bun.write(htmlPath, html);
555
- logger.success(`Generated ${route.route === '/' ? 'index.html' : route.route + '/index.html'}`);
593
+ logger.success(`Generated ${route.route === '/' ? 'index.html' : route.route + '/index.html'} with meta: ${meta.title || 'default'}`);
556
594
  }
557
595
  }