bertui 0.3.6 → 0.3.8

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.6",
3
+ "version": "0.3.8",
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
@@ -1,8 +1,9 @@
1
- import { join } from 'path';
1
+ import { join, relative, basename } from 'path';
2
2
  import { existsSync, mkdirSync, rmSync, cpSync, readdirSync, statSync } from 'fs';
3
- import { extname, relative, dirname } from 'path';
3
+ import { extname, dirname } from 'path';
4
4
  import logger from './logger/logger.js';
5
5
  import { buildCSS } from './build/css-builder.js';
6
+ import { loadEnvVariables, replaceEnvInCode } from './utils/env.js'; // ✅ IMPORT THIS!
6
7
 
7
8
  export async function buildProduction(options = {}) {
8
9
  const root = options.root || process.cwd();
@@ -25,8 +26,15 @@ export async function buildProduction(options = {}) {
25
26
  const startTime = Date.now();
26
27
 
27
28
  try {
29
+ // ✅ LOAD ENV VARS BEFORE COMPILATION!
30
+ logger.info('Step 0: Loading environment variables...');
31
+ const envVars = loadEnvVariables(root);
32
+ if (Object.keys(envVars).length > 0) {
33
+ logger.info(`Loaded ${Object.keys(envVars).length} environment variables`);
34
+ }
35
+
28
36
  logger.info('Step 1: Compiling for production...');
29
- const { routes } = await compileForBuild(root, buildDir);
37
+ const { routes } = await compileForBuild(root, buildDir, envVars); // ✅ PASS ENV VARS!
30
38
  logger.success('Production compilation complete');
31
39
 
32
40
  logger.info('Step 2: Building CSS with Lightning CSS...');
@@ -68,7 +76,21 @@ export async function buildProduction(options = {}) {
68
76
  chunk: 'chunks/[name]-[hash].js',
69
77
  asset: '[name]-[hash].[ext]'
70
78
  },
71
- external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime']
79
+ external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
80
+ // ✅ CRITICAL: Add define to replace process.env at bundle time!
81
+ define: {
82
+ 'process.env.NODE_ENV': '"production"',
83
+ 'process.env.PUBLIC_APP_NAME': JSON.stringify(envVars.PUBLIC_APP_NAME || 'BertUI App'),
84
+ 'process.env.PUBLIC_API_URL': JSON.stringify(envVars.PUBLIC_API_URL || ''),
85
+ 'process.env.PUBLIC_USERNAME': JSON.stringify(envVars.PUBLIC_USERNAME || ''),
86
+ // Add all other env vars dynamically
87
+ ...Object.fromEntries(
88
+ Object.entries(envVars).map(([key, value]) => [
89
+ `process.env.${key}`,
90
+ JSON.stringify(value)
91
+ ])
92
+ )
93
+ }
72
94
  });
73
95
 
74
96
  if (!result.success) {
@@ -131,7 +153,8 @@ async function buildAllCSS(root, outDir) {
131
153
  }
132
154
  }
133
155
 
134
- async function compileForBuild(root, buildDir) {
156
+ // ACCEPT ENV VARS PARAMETER
157
+ async function compileForBuild(root, buildDir, envVars) {
135
158
  const srcDir = join(root, 'src');
136
159
  const pagesDir = join(srcDir, 'pages');
137
160
 
@@ -145,7 +168,8 @@ async function compileForBuild(root, buildDir) {
145
168
  logger.info(`Found ${routes.length} routes`);
146
169
  }
147
170
 
148
- await compileBuildDirectory(srcDir, buildDir, root);
171
+ // PASS ENV VARS TO COMPILATION
172
+ await compileBuildDirectory(srcDir, buildDir, root, envVars);
149
173
 
150
174
  if (routes.length > 0) {
151
175
  await generateBuildRouter(routes, buildDir);
@@ -343,7 +367,8 @@ ${routeConfigs}
343
367
  await Bun.write(join(buildDir, 'router.js'), routerCode);
344
368
  }
345
369
 
346
- async function compileBuildDirectory(srcDir, buildDir, root) {
370
+ // ACCEPT ENV VARS PARAMETER
371
+ async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
347
372
  const files = readdirSync(srcDir);
348
373
 
349
374
  for (const file of files) {
@@ -353,19 +378,20 @@ async function compileBuildDirectory(srcDir, buildDir, root) {
353
378
  if (stat.isDirectory()) {
354
379
  const subBuildDir = join(buildDir, file);
355
380
  mkdirSync(subBuildDir, { recursive: true });
356
- await compileBuildDirectory(srcPath, subBuildDir, root);
381
+ await compileBuildDirectory(srcPath, subBuildDir, root, envVars); // ✅ PASS IT DOWN
357
382
  } else {
358
383
  const ext = extname(file);
359
384
 
360
385
  if (ext === '.css') continue;
361
386
 
362
387
  if (['.jsx', '.tsx', '.ts'].includes(ext)) {
363
- await compileBuildFile(srcPath, buildDir, file, root);
388
+ await compileBuildFile(srcPath, buildDir, file, root, envVars); // ✅ PASS IT HERE
364
389
  } else if (ext === '.js') {
365
390
  const outPath = join(buildDir, file);
366
391
  let code = await Bun.file(srcPath).text();
367
392
 
368
393
  code = removeCSSImports(code);
394
+ code = replaceEnvInCode(code, envVars); // ✅ REPLACE ENV VARS!
369
395
  code = fixBuildImports(code, srcPath, outPath, root);
370
396
 
371
397
  await Bun.write(outPath, code);
@@ -374,7 +400,8 @@ async function compileBuildDirectory(srcDir, buildDir, root) {
374
400
  }
375
401
  }
376
402
 
377
- async function compileBuildFile(srcPath, buildDir, filename, root) {
403
+ // ACCEPT ENV VARS PARAMETER
404
+ async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
378
405
  const ext = extname(filename);
379
406
  const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
380
407
 
@@ -382,6 +409,7 @@ async function compileBuildFile(srcPath, buildDir, filename, root) {
382
409
  let code = await Bun.file(srcPath).text();
383
410
 
384
411
  code = removeCSSImports(code);
412
+ code = replaceEnvInCode(code, envVars); // ✅ REPLACE ENV VARS BEFORE TRANSPILATION!
385
413
 
386
414
  const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
387
415
  const outPath = join(buildDir, outFilename);
@@ -448,41 +476,90 @@ function fixRelativeImports(code) {
448
476
  return code;
449
477
  }
450
478
 
451
- // IMPROVED: Extract meta using regex (works on raw source code)
452
479
  function extractMetaFromSource(code) {
453
480
  try {
454
- // Match: export const meta = { ... };
455
- const metaRegex = /export\s+const\s+meta\s*=\s*\{([^}]+)\}/s;
456
- const match = code.match(metaRegex);
481
+ const metaMatch = code.match(/export\s+const\s+meta\s*=\s*\{/);
482
+ if (!metaMatch) return null;
483
+
484
+ const startIndex = metaMatch.index + metaMatch[0].length - 1;
485
+ let braceCount = 0;
486
+ let endIndex = startIndex;
487
+
488
+ for (let i = startIndex; i < code.length; i++) {
489
+ if (code[i] === '{') braceCount++;
490
+ if (code[i] === '}') {
491
+ braceCount--;
492
+ if (braceCount === 0) {
493
+ endIndex = i;
494
+ break;
495
+ }
496
+ }
497
+ }
457
498
 
458
- if (!match) return null;
499
+ if (endIndex === startIndex) return null;
459
500
 
460
- const metaContent = match[1];
501
+ const metaString = code.substring(startIndex, endIndex + 1);
461
502
  const meta = {};
503
+ const pairRegex = /(\w+)\s*:\s*(['"`])((?:(?!\2).)*)\2/g;
504
+ let match;
462
505
 
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
- });
506
+ while ((match = pairRegex.exec(metaString)) !== null) {
507
+ const key = match[1];
508
+ const value = match[3];
509
+ meta[key] = value;
510
+ }
472
511
 
473
- return meta;
512
+ return Object.keys(meta).length > 0 ? meta : null;
474
513
  } catch (error) {
514
+ logger.warn(`Could not extract meta: ${error.message}`);
475
515
  return null;
476
516
  }
477
517
  }
478
518
 
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
519
+ async function generateProductionHTML(root, outDir, buildResult, routes) {
520
+ const mainBundle = buildResult.outputs.find(o =>
521
+ o.path.includes('main') && o.kind === 'entry-point'
522
+ );
523
+
524
+ if (!mainBundle) {
525
+ throw new Error('Could not find main bundle in build output');
526
+ }
483
527
 
484
- const html = `<!DOCTYPE html>
485
- <html lang="en">
528
+ const bundlePath = relative(outDir, mainBundle.path).replace(/\\/g, '/');
529
+ logger.info(`Main bundle path: ${bundlePath}`);
530
+
531
+ const srcStylesDir = join(root, 'src', 'styles');
532
+ let userStylesheets = '';
533
+
534
+ if (existsSync(srcStylesDir)) {
535
+ const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
536
+ userStylesheets = cssFiles.map(f =>
537
+ ` <link rel="stylesheet" href="/styles/${f.replace('.css', '.min.css')}">`
538
+ ).join('\n');
539
+ }
540
+
541
+ const { loadConfig } = await import('./config/loadConfig.js');
542
+ const config = await loadConfig(root);
543
+ const defaultMeta = config.meta || {};
544
+
545
+ logger.info('Generating SEO-optimized HTML files...');
546
+
547
+ for (const route of routes) {
548
+ if (route.type === 'dynamic') {
549
+ logger.info(`Skipping dynamic route: ${route.route}`);
550
+ continue;
551
+ }
552
+
553
+ const sourceCode = await Bun.file(route.path).text();
554
+ const pageMeta = extractMetaFromSource(sourceCode);
555
+ const meta = { ...defaultMeta, ...pageMeta };
556
+
557
+ if (pageMeta) {
558
+ logger.info(`Extracted meta for ${route.route}: ${JSON.stringify(pageMeta)}`);
559
+ }
560
+
561
+ const html = `<!DOCTYPE html>
562
+ <html lang="${meta.lang || 'en'}">
486
563
  <head>
487
564
  <meta charset="UTF-8">
488
565
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -497,7 +574,7 @@ async function renderComponentToHTML(route, bundlePath, userStylesheets, meta) {
497
574
  <meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
498
575
  ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
499
576
  <meta property="og:type" content="website">
500
- <meta property="og:url" content="${route}">
577
+ <meta property="og:url" content="${route.route}">
501
578
 
502
579
  <meta name="twitter:card" content="summary_large_image">
503
580
  <meta name="twitter:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
@@ -505,7 +582,7 @@ async function renderComponentToHTML(route, bundlePath, userStylesheets, meta) {
505
582
  ${meta.ogImage ? `<meta name="twitter:image" content="${meta.ogImage}">` : ''}
506
583
 
507
584
  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
508
- <link rel="canonical" href="${route}">
585
+ <link rel="canonical" href="${route.route}">
509
586
 
510
587
  ${userStylesheets}
511
588
 
@@ -519,67 +596,13 @@ ${userStylesheets}
519
596
  }
520
597
  }
521
598
  </script>
522
-
523
- <!-- SEO Preload Hints -->
524
- <link rel="preconnect" href="https://esm.sh">
525
- <link rel="dns-prefetch" href="https://esm.sh">
526
599
  </head>
527
600
  <body>
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>
601
+ <div id="root"></div>
537
602
  <script type="module" src="/${bundlePath}"></script>
538
603
  </body>
539
604
  </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);
581
605
 
582
- // Determine output path
583
606
  let htmlPath;
584
607
  if (route.route === '/') {
585
608
  htmlPath = join(outDir, 'index.html');
@@ -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 } from 'fs';
4
+ import { existsSync, readdirSync } from 'fs'; // ✅ FIXED: Import properly
5
5
  import logger from '../logger/logger.js';
6
6
  import { compileProject } from '../client/compiler.js';
7
7
  import { loadConfig } from '../config/loadConfig.js';
@@ -50,7 +50,7 @@ export async function startDevServer(options = {}) {
50
50
  }
51
51
  }
52
52
 
53
- // FIXED: Handle CSS files from .bertui/styles
53
+ // Handle CSS files from .bertui/styles
54
54
  if (path.startsWith('styles/') && path.endsWith('.css')) {
55
55
  const cssPath = join(stylesDir, path.replace('styles/', ''));
56
56
  const file = Bun.file(cssPath);
@@ -148,7 +148,6 @@ ws.onclose = () => {
148
148
  });
149
149
  })
150
150
 
151
- // FIXED: Serve CSS from .bertui/styles
152
151
  .get('/styles/*', async ({ params, set }) => {
153
152
  const filepath = join(stylesDir, params['*']);
154
153
  const file = Bun.file(filepath);
@@ -166,7 +165,6 @@ ws.onclose = () => {
166
165
  });
167
166
  })
168
167
 
169
- // Around line 60, update the route handler:
170
168
  .get('/public/*', async ({ params, set }) => {
171
169
  const publicDir = join(root, 'public');
172
170
  const filepath = join(publicDir, params['*']);
@@ -200,17 +198,20 @@ ws.onclose = () => {
200
198
  function serveHTML(root, hasRouter, config) {
201
199
  const meta = config.meta || {};
202
200
 
203
- // Find user's CSS files
201
+ // FIXED: Proper ESM import for fs
204
202
  const srcStylesDir = join(root, 'src', 'styles');
205
203
  let userStylesheets = '';
206
204
 
207
205
  if (existsSync(srcStylesDir)) {
208
- const cssFiles = require('fs').readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
209
- userStylesheets = cssFiles.map(f => ` <link rel="stylesheet" href="/styles/${f}">`).join('\n');
206
+ try {
207
+ const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
208
+ userStylesheets = cssFiles.map(f => ` <link rel="stylesheet" href="/styles/${f}">`).join('\n');
209
+ } catch (error) {
210
+ logger.warn(`Could not read styles directory: ${error.message}`);
211
+ }
210
212
  }
211
213
 
212
- const html = `
213
- <!DOCTYPE html>
214
+ const html = `<!DOCTYPE html>
214
215
  <html lang="${meta.lang || 'en'}">
215
216
  <head>
216
217
  <meta charset="UTF-8">
@@ -226,9 +227,9 @@ function serveHTML(root, hasRouter, config) {
226
227
  ${meta.ogDescription ? `<meta property="og:description" content="${meta.ogDescription || meta.description}">` : ''}
227
228
  ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
228
229
 
229
- <link rel="icon" type="image/svg+xml" href="/favicon.svg">
230
+ <link rel="icon" type="image/svg+xml" href="/public/favicon.svg">
230
231
 
231
- ${userStylesheets}
232
+ ${userStylesheets}
232
233
 
233
234
  <script type="importmap">
234
235
  {
package/src/utils/env.js CHANGED
@@ -11,7 +11,7 @@ export function loadEnvVariables(root) {
11
11
 
12
12
  // Load from process.env (already loaded by Bun/Node)
13
13
  for (const [key, value] of Object.entries(process.env)) {
14
- // Only expose variables that start with VITE_ or PUBLIC_
14
+ // Only expose variables that start with BERTUI_ or PUBLIC_
15
15
  if (key.startsWith('BERTUI_') || key.startsWith('PUBLIC_')) {
16
16
  envVars[key] = value;
17
17
  }
@@ -28,13 +28,12 @@ export function generateEnvCode(envVars) {
28
28
  .map(([key, value]) => ` "${key}": ${JSON.stringify(value)}`)
29
29
  .join(',\n');
30
30
 
31
- return `
32
- // Environment variables injected at build time
31
+ return `// Environment variables injected at build time
33
32
  export const env = {
34
33
  ${envObject}
35
34
  };
36
35
 
37
- Make it available globally (optional)
36
+ // Make it available globally (optional)
38
37
  if (typeof window !== 'undefined') {
39
38
  window.__BERTUI_ENV__ = env;
40
39
  }
@@ -42,7 +41,8 @@ if (typeof window !== 'undefined') {
42
41
  }
43
42
 
44
43
  /**
45
- * Replace process.env references in code with actual values
44
+ * ✅ CRITICAL FIX: Replace ALL process.env references with actual values
45
+ * This prevents "process is not defined" errors in the browser
46
46
  */
47
47
  export function replaceEnvInCode(code, envVars) {
48
48
  let result = code;
@@ -53,5 +53,16 @@ export function replaceEnvInCode(code, envVars) {
53
53
  result = result.replace(regex, JSON.stringify(value));
54
54
  }
55
55
 
56
+ // ✅ NEW: Also replace generic process.env.NODE_ENV
57
+ // This is commonly used by React and other libraries
58
+ result = result.replace(
59
+ /process\.env\.NODE_ENV/g,
60
+ JSON.stringify('production')
61
+ );
62
+
63
+ // ✅ NEW: Remove any remaining process references that might cause errors
64
+ // Replace with undefined to avoid runtime errors
65
+ result = result.replace(/\bprocess\b/g, 'undefined');
66
+
56
67
  return result;
57
68
  }