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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "0.3.4",
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
@@ -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
- cpSync(publicDir, outDir, { recursive: true });
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 index.html...');
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
- async function generateProductionHTML(root, outDir, buildResult) {
446
- const mainBundle = buildResult.outputs.find(o =>
447
- o.path.includes('main') && o.kind === 'entry-point'
448
- );
449
-
450
- if (!mainBundle) {
451
- throw new Error('Could not find main bundle in build output');
452
- }
453
-
454
- const bundlePath = mainBundle.path.replace(outDir, '').replace(/^\//, '');
455
-
456
- // Find user CSS files
457
- const srcStylesDir = join(root, 'src', 'styles');
458
- let userStylesheets = '';
459
-
460
- if (existsSync(srcStylesDir)) {
461
- const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
462
- userStylesheets = cssFiles.map(f =>
463
- ` <link rel="stylesheet" href="/styles/${f.replace('.css', '.min.css')}">`
464
- ).join('\n');
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 name="description" content="Built with BertUI - Lightning fast React development">
473
- <title>BertUI App</title>
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"></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>
488
537
  <script type="module" src="/${bundlePath}"></script>
489
538
  </body>
490
539
  </html>`;
491
540
 
492
- await Bun.write(join(outDir, 'index.html'), html);
493
- logger.success('Generated index.html');
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
  }
@@ -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="/public/favicon.svg">
229
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
229
230
 
230
231
  ${userStylesheets}
231
232