bertui 1.1.0 → 1.1.2

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.
@@ -1,4 +1,4 @@
1
- // src/server/dev-server.js - FIXED IMAGE SERVING
1
+ // src/server/dev-server.js - FIXED: bertui-icons Support + Import Map
2
2
  import { Elysia } from 'elysia';
3
3
  import { watch } from 'fs';
4
4
  import { join, extname } from 'path';
@@ -28,10 +28,10 @@ export async function startDevServer(options = {}) {
28
28
 
29
29
  const app = new Elysia()
30
30
  .get('/', async () => {
31
- return serveHTML(root, hasRouter, config);
31
+ return await serveHTML(root, hasRouter, config); // ✅ FIX 1/3: Added await
32
32
  })
33
33
 
34
- // ✅ FIX: Serve images from src/images/ (CRITICAL)
34
+ // ✅ Serve images from src/images/
35
35
  .get('/images/*', async ({ params, set }) => {
36
36
  const srcImagesDir = join(srcDir, 'images');
37
37
  const filepath = join(srcImagesDir, params['*']);
@@ -48,7 +48,7 @@ export async function startDevServer(options = {}) {
48
48
  return new Response(file, {
49
49
  headers: {
50
50
  'Content-Type': contentType,
51
- 'Cache-Control': 'no-cache' // Dev server = no cache
51
+ 'Cache-Control': 'no-cache'
52
52
  }
53
53
  });
54
54
  })
@@ -89,6 +89,27 @@ export async function startDevServer(options = {}) {
89
89
  });
90
90
  })
91
91
 
92
+ // ✅ CRITICAL FIX: Serve node_modules with correct MIME type
93
+ .get('/node_modules/*', async ({ params, set }) => {
94
+ const filepath = join(root, 'node_modules', params['*']);
95
+ const file = Bun.file(filepath);
96
+
97
+ if (!await file.exists()) {
98
+ set.status = 404;
99
+ return 'Module not found';
100
+ }
101
+
102
+ const ext = extname(filepath).toLowerCase();
103
+ const contentType = ext === '.js' ? 'application/javascript; charset=utf-8' : getContentType(ext);
104
+
105
+ return new Response(file, {
106
+ headers: {
107
+ 'Content-Type': contentType,
108
+ 'Cache-Control': 'no-cache'
109
+ }
110
+ });
111
+ })
112
+
92
113
  .get('/*', async ({ params, set }) => {
93
114
  const path = params['*'];
94
115
 
@@ -128,7 +149,7 @@ export async function startDevServer(options = {}) {
128
149
  return 'File not found';
129
150
  }
130
151
 
131
- return serveHTML(root, hasRouter, config);
152
+ return await serveHTML(root, hasRouter, config); // ✅ FIX 2/3: Added await
132
153
  })
133
154
 
134
155
  .get('/hmr-client.js', () => {
@@ -405,26 +426,43 @@ ws.onclose = () => {
405
426
  }
406
427
  })
407
428
 
408
- .get('/compiled/*', async ({ params, set }) => {
409
- const filepath = join(compiledDir, params['*']);
410
- const file = Bun.file(filepath);
411
-
412
- if (!await file.exists()) {
413
- set.status = 404;
414
- return 'File not found';
415
- }
416
-
417
- const ext = extname(filepath);
418
- const contentType = ext === '.js' ? 'application/javascript' : getContentType(ext);
419
-
420
- return new Response(await file.text(), {
421
- headers: {
422
- 'Content-Type': contentType,
423
- 'Cache-Control': 'no-store'
424
- }
425
- });
426
- })
427
-
429
+ // Replace the /compiled/* handler in bertui/src/server/dev-server.js
430
+
431
+ .get('/compiled/*', async ({ params, set }) => {
432
+ const requestedPath = params['*'];
433
+ const filepath = join(compiledDir, requestedPath);
434
+
435
+ // Check if file exists
436
+ const file = Bun.file(filepath);
437
+ if (!await file.exists()) {
438
+ logger.warn(`File not found: /compiled/${requestedPath}`);
439
+ set.status = 404;
440
+ return 'File not found';
441
+ }
442
+
443
+ // CRITICAL FIX: Always return JavaScript files with correct MIME type
444
+ const ext = extname(filepath).toLowerCase();
445
+
446
+ let contentType;
447
+ if (ext === '.js' || ext === '.jsx' || ext === '.mjs') {
448
+ contentType = 'application/javascript; charset=utf-8';
449
+ } else if (ext === '.json') {
450
+ contentType = 'application/json; charset=utf-8';
451
+ } else if (ext === '.css') {
452
+ contentType = 'text/css; charset=utf-8';
453
+ } else {
454
+ contentType = 'text/plain; charset=utf-8';
455
+ }
456
+
457
+ logger.debug(`Serving: /compiled/${requestedPath} (${contentType})`);
458
+
459
+ return new Response(file, {
460
+ headers: {
461
+ 'Content-Type': contentType,
462
+ 'Cache-Control': 'no-store, no-cache, must-revalidate'
463
+ }
464
+ });
465
+ })
428
466
  .get('/styles/*', async ({ params, set }) => {
429
467
  const filepath = join(stylesDir, params['*']);
430
468
  const file = Bun.file(filepath);
@@ -453,6 +491,7 @@ ws.onclose = () => {
453
491
  logger.info(`📁 Serving: ${root}`);
454
492
  logger.info(`🖼️ Images: /images/* → src/images/`);
455
493
  logger.info(`📦 Public: /public/* → public/`);
494
+ logger.info(`⚡ BertUI Packages: /node_modules/* → node_modules/`);
456
495
 
457
496
  setupWatcher(root, compiledDir, clients, async () => {
458
497
  hasRouter = existsSync(join(compiledDir, 'router.js'));
@@ -460,8 +499,10 @@ ws.onclose = () => {
460
499
 
461
500
  return app;
462
501
  }
502
+ // Update serveHTML function in bertui/src/server/dev-server.js
503
+ // This version auto-detects ALL bertui-* packages
463
504
 
464
- function serveHTML(root, hasRouter, config) {
505
+ async function serveHTML(root, hasRouter, config) { // ✅ FIX 3/3: Added async
465
506
  const meta = config.meta || {};
466
507
 
467
508
  const srcStylesDir = join(root, 'src', 'styles');
@@ -476,6 +517,72 @@ function serveHTML(root, hasRouter, config) {
476
517
  }
477
518
  }
478
519
 
520
+ // Build import map
521
+ const importMap = {
522
+ "react": "https://esm.sh/react@18.2.0",
523
+ "react-dom": "https://esm.sh/react-dom@18.2.0",
524
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0/client"
525
+ };
526
+
527
+ // ✅ AUTO-DETECT ALL bertui-* PACKAGES
528
+ const nodeModulesDir = join(root, 'node_modules');
529
+
530
+ if (existsSync(nodeModulesDir)) {
531
+ try {
532
+ const packages = readdirSync(nodeModulesDir);
533
+
534
+ for (const pkg of packages) {
535
+ // Skip non-bertui packages
536
+ if (!pkg.startsWith('bertui-')) continue;
537
+
538
+ const pkgDir = join(nodeModulesDir, pkg);
539
+ const pkgJsonPath = join(pkgDir, 'package.json');
540
+
541
+ // Skip if no package.json
542
+ if (!existsSync(pkgJsonPath)) continue;
543
+
544
+ try {
545
+ const pkgJsonContent = await Bun.file(pkgJsonPath).text();
546
+ const pkgJson = JSON.parse(pkgJsonContent);
547
+
548
+ // Resolve main entry point
549
+ let mainFile = null;
550
+
551
+ // Try exports field first (modern)
552
+ if (pkgJson.exports) {
553
+ const rootExport = pkgJson.exports['.'];
554
+ if (typeof rootExport === 'string') {
555
+ mainFile = rootExport;
556
+ } else if (typeof rootExport === 'object') {
557
+ // Prefer browser build, fallback to default
558
+ mainFile = rootExport.browser || rootExport.default || rootExport.import;
559
+ }
560
+ }
561
+
562
+ // Fallback to main field
563
+ if (!mainFile) {
564
+ mainFile = pkgJson.main || 'index.js';
565
+ }
566
+
567
+ // Verify file exists
568
+ const fullPath = join(pkgDir, mainFile);
569
+ if (existsSync(fullPath)) {
570
+ importMap[pkg] = `/node_modules/${pkg}/${mainFile}`;
571
+ logger.info(`✅ ${pkg} available`);
572
+ } else {
573
+ logger.warn(`⚠️ ${pkg} main file not found: ${mainFile}`);
574
+ }
575
+
576
+ } catch (error) {
577
+ logger.warn(`⚠️ Failed to parse ${pkg}/package.json: ${error.message}`);
578
+ }
579
+ }
580
+
581
+ } catch (error) {
582
+ logger.warn(`Failed to scan node_modules: ${error.message}`);
583
+ }
584
+ }
585
+
479
586
  const html = `<!DOCTYPE html>
480
587
  <html lang="${meta.lang || 'en'}">
481
588
  <head>
@@ -497,13 +604,7 @@ function serveHTML(root, hasRouter, config) {
497
604
  ${userStylesheets}
498
605
 
499
606
  <script type="importmap">
500
- {
501
- "imports": {
502
- "react": "https://esm.sh/react@18.2.0",
503
- "react-dom": "https://esm.sh/react-dom@18.2.0",
504
- "react-dom/client": "https://esm.sh/react-dom@18.2.0/client"
505
- }
506
- }
607
+ ${JSON.stringify({ imports: importMap }, null, 2)}
507
608
  </script>
508
609
 
509
610
  <style>
@@ -529,7 +630,6 @@ ${userStylesheets}
529
630
  headers: { 'Content-Type': 'text/html' }
530
631
  });
531
632
  }
532
-
533
633
  function getImageContentType(ext) {
534
634
  const types = {
535
635
  '.jpg': 'image/jpeg',
@@ -592,7 +692,6 @@ function setupWatcher(root, compiledDir, clients, onRecompile) {
592
692
  if (watchedExtensions.includes(ext)) {
593
693
  logger.info(`📝 File changed: ${filename}`);
594
694
 
595
- // For images, just reload
596
695
  if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif'].includes(ext)) {
597
696
  for (const client of clients) {
598
697
  try {
@@ -604,7 +703,6 @@ function setupWatcher(root, compiledDir, clients, onRecompile) {
604
703
  return;
605
704
  }
606
705
 
607
- // For code/CSS, recompile
608
706
  for (const client of clients) {
609
707
  try {
610
708
  client.send(JSON.stringify({ type: 'recompiling' }));
package/src/utils/env.js CHANGED
@@ -1,19 +1,49 @@
1
- // bertui/src/utils/env.js
2
- import { existsSync } from 'fs';
1
+ // bertui/src/utils/env.js - FIXED
3
2
  import { join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import logger from '../logger/logger.js';
4
5
 
5
6
  /**
6
- * Load environment variables for BertUI
7
- * This runs at BUILD TIME (Node.js), not in the browser
7
+ * Load environment variables from .env file
8
+ * @param {string} root - Project root directory
9
+ * @returns {Object} Environment variables
8
10
  */
9
11
  export function loadEnvVariables(root) {
12
+ const envPath = join(root, '.env');
10
13
  const envVars = {};
11
14
 
12
- // Load from process.env (already loaded by Bun/Node)
13
- for (const [key, value] of Object.entries(process.env)) {
14
- // Only expose variables that start with BERTUI_ or PUBLIC_
15
- if (key.startsWith('BERTUI_') || key.startsWith('PUBLIC_')) {
16
- envVars[key] = value;
15
+ if (existsSync(envPath)) {
16
+ try {
17
+ // FIXED: Use synchronous read for consistency
18
+ const file = Bun.file(envPath);
19
+ const envContent = file.text();
20
+ const lines = envContent.split('\n');
21
+
22
+ for (const line of lines) {
23
+ // Skip empty lines and comments
24
+ if (!line.trim() || line.trim().startsWith('#')) continue;
25
+
26
+ // Parse KEY=VALUE
27
+ const match = line.match(/^([^=]+)=(.*)$/);
28
+ if (match) {
29
+ const key = match[1].trim();
30
+ let value = match[2].trim();
31
+
32
+ // Remove quotes if present
33
+ if ((value.startsWith('"') && value.endsWith('"')) ||
34
+ (value.startsWith("'") && value.endsWith("'"))) {
35
+ value = value.slice(1, -1);
36
+ }
37
+
38
+ envVars[key] = value;
39
+ }
40
+ }
41
+
42
+ if (Object.keys(envVars).length > 0) {
43
+ logger.debug(`Loaded ${Object.keys(envVars).length} environment variables from .env`);
44
+ }
45
+ } catch (error) {
46
+ logger.warn(`Failed to load .env: ${error.message}`);
17
47
  }
18
48
  }
19
49
 
@@ -21,48 +51,38 @@ export function loadEnvVariables(root) {
21
51
  }
22
52
 
23
53
  /**
24
- * Generate code to inject env variables into the browser
54
+ * Generate JavaScript code to expose environment variables
55
+ * @param {Object} envVars - Environment variables object
56
+ * @returns {string} JavaScript code
25
57
  */
26
58
  export function generateEnvCode(envVars) {
27
- const envObject = Object.entries(envVars)
28
- .map(([key, value]) => ` "${key}": ${JSON.stringify(value)}`)
29
- .join(',\n');
59
+ const exports = Object.entries(envVars)
60
+ .map(([key, value]) => `export const ${key} = ${JSON.stringify(value)};`)
61
+ .join('\n');
30
62
 
31
- return `// Environment variables injected at build time
32
- export const env = {
33
- ${envObject}
34
- };
63
+ return `// Auto-generated environment variables
64
+ // Do not edit this file manually
35
65
 
36
- // Make it available globally (optional)
37
- if (typeof window !== 'undefined') {
38
- window.__BERTUI_ENV__ = env;
39
- }
66
+ ${exports}
67
+
68
+ // Access via: import { VAR_NAME } from './env.js'
40
69
  `;
41
70
  }
42
71
 
43
72
  /**
44
- * ✅ CRITICAL FIX: Replace ALL process.env references with actual values
45
- * This prevents "process is not defined" errors in the browser
73
+ * Replace process.env references in code
74
+ * @param {string} code - Source code
75
+ * @param {Object} envVars - Environment variables
76
+ * @returns {string} Code with replaced env vars
46
77
  */
47
78
  export function replaceEnvInCode(code, envVars) {
48
- let result = code;
79
+ let modified = code;
49
80
 
50
- // Replace process.env.VARIABLE_NAME with actual values
51
81
  for (const [key, value] of Object.entries(envVars)) {
52
- const regex = new RegExp(`process\\.env\\.${key}`, 'g');
53
- result = result.replace(regex, JSON.stringify(value));
82
+ // Replace process.env.KEY with actual value
83
+ const regex = new RegExp(`process\\.env\\.${key}\\b`, 'g');
84
+ modified = modified.replace(regex, JSON.stringify(value));
54
85
  }
55
86
 
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
-
67
- return result;
87
+ return modified;
68
88
  }
@@ -0,0 +1,127 @@
1
+ // bertui/src/utils/meta-extractor.js
2
+
3
+ /**
4
+ * Extract meta information from page component source code
5
+ * @param {string} sourceCode - Component source code
6
+ * @returns {Object} Extracted meta information
7
+ */
8
+ export function extractMetaFromSource(sourceCode) {
9
+ const meta = {};
10
+
11
+ // Extract title
12
+ const titleMatch = sourceCode.match(/export\s+const\s+title\s*=\s*['"]([^'"]+)['"]/);
13
+ if (titleMatch) {
14
+ meta.title = titleMatch[1];
15
+ }
16
+
17
+ // Extract description
18
+ const descMatch = sourceCode.match(/export\s+const\s+description\s*=\s*['"]([^'"]+)['"]/);
19
+ if (descMatch) {
20
+ meta.description = descMatch[1];
21
+ }
22
+
23
+ // Extract keywords
24
+ const keywordsMatch = sourceCode.match(/export\s+const\s+keywords\s*=\s*['"]([^'"]+)['"]/);
25
+ if (keywordsMatch) {
26
+ meta.keywords = keywordsMatch[1];
27
+ }
28
+
29
+ // Extract author
30
+ const authorMatch = sourceCode.match(/export\s+const\s+author\s*=\s*['"]([^'"]+)['"]/);
31
+ if (authorMatch) {
32
+ meta.author = authorMatch[1];
33
+ }
34
+
35
+ // Extract og:title
36
+ const ogTitleMatch = sourceCode.match(/export\s+const\s+ogTitle\s*=\s*['"]([^'"]+)['"]/);
37
+ if (ogTitleMatch) {
38
+ meta.ogTitle = ogTitleMatch[1];
39
+ }
40
+
41
+ // Extract og:description
42
+ const ogDescMatch = sourceCode.match(/export\s+const\s+ogDescription\s*=\s*['"]([^'"]+)['"]/);
43
+ if (ogDescMatch) {
44
+ meta.ogDescription = ogDescMatch[1];
45
+ }
46
+
47
+ // Extract og:image
48
+ const ogImageMatch = sourceCode.match(/export\s+const\s+ogImage\s*=\s*['"]([^'"]+)['"]/);
49
+ if (ogImageMatch) {
50
+ meta.ogImage = ogImageMatch[1];
51
+ }
52
+
53
+ // Extract language
54
+ const langMatch = sourceCode.match(/export\s+const\s+lang\s*=\s*['"]([^'"]+)['"]/);
55
+ if (langMatch) {
56
+ meta.lang = langMatch[1];
57
+ }
58
+
59
+ // Extract theme color
60
+ const themeMatch = sourceCode.match(/export\s+const\s+themeColor\s*=\s*['"]([^'"]+)['"]/);
61
+ if (themeMatch) {
62
+ meta.themeColor = themeMatch[1];
63
+ }
64
+
65
+ return meta;
66
+ }
67
+
68
+ /**
69
+ * Generate HTML meta tags from meta object
70
+ * @param {Object} meta - Meta information object
71
+ * @returns {string} HTML meta tags
72
+ */
73
+ export function generateMetaTags(meta) {
74
+ const tags = [];
75
+
76
+ if (meta.title) {
77
+ tags.push(`<title>${escapeHtml(meta.title)}</title>`);
78
+ }
79
+
80
+ if (meta.description) {
81
+ tags.push(`<meta name="description" content="${escapeHtml(meta.description)}">`);
82
+ }
83
+
84
+ if (meta.keywords) {
85
+ tags.push(`<meta name="keywords" content="${escapeHtml(meta.keywords)}">`);
86
+ }
87
+
88
+ if (meta.author) {
89
+ tags.push(`<meta name="author" content="${escapeHtml(meta.author)}">`);
90
+ }
91
+
92
+ if (meta.themeColor) {
93
+ tags.push(`<meta name="theme-color" content="${escapeHtml(meta.themeColor)}">`);
94
+ }
95
+
96
+ // Open Graph tags
97
+ if (meta.ogTitle) {
98
+ tags.push(`<meta property="og:title" content="${escapeHtml(meta.ogTitle)}">`);
99
+ }
100
+
101
+ if (meta.ogDescription) {
102
+ tags.push(`<meta property="og:description" content="${escapeHtml(meta.ogDescription)}">`);
103
+ }
104
+
105
+ if (meta.ogImage) {
106
+ tags.push(`<meta property="og:image" content="${escapeHtml(meta.ogImage)}">`);
107
+ }
108
+
109
+ return tags.join('\n ');
110
+ }
111
+
112
+ /**
113
+ * Escape HTML special characters
114
+ * @param {string} text - Text to escape
115
+ * @returns {string} Escaped text
116
+ */
117
+ function escapeHtml(text) {
118
+ const map = {
119
+ '&': '&amp;',
120
+ '<': '&lt;',
121
+ '>': '&gt;',
122
+ '"': '&quot;',
123
+ "'": '&#039;'
124
+ };
125
+
126
+ return String(text).replace(/[&<>"']/g, m => map[m]);
127
+ }
@@ -0,0 +1,80 @@
1
+ // bertui/types/config.d.ts
2
+ declare module 'bertui/config' {
3
+ /**
4
+ * BertUI Configuration
5
+ */
6
+ export interface BertuiConfig {
7
+ /** Site name for SEO */
8
+ siteName?: string;
9
+
10
+ /** Base URL for sitemap generation (e.g., "https://example.com") */
11
+ baseUrl?: string;
12
+
13
+ /** HTML meta tags configuration */
14
+ meta?: {
15
+ /** Page title */
16
+ title?: string;
17
+ /** Meta description */
18
+ description?: string;
19
+ /** Meta keywords */
20
+ keywords?: string;
21
+ /** Author name */
22
+ author?: string;
23
+ /** Open Graph image URL */
24
+ ogImage?: string;
25
+ /** Open Graph title */
26
+ ogTitle?: string;
27
+ /** Open Graph description */
28
+ ogDescription?: string;
29
+ /** Theme color */
30
+ themeColor?: string;
31
+ /** Language code (e.g., "en") */
32
+ lang?: string;
33
+ };
34
+
35
+ /** App shell configuration */
36
+ appShell?: {
37
+ /** Show loading indicator */
38
+ loading?: boolean;
39
+ /** Loading text */
40
+ loadingText?: string;
41
+ /** Background color */
42
+ backgroundColor?: string;
43
+ };
44
+
45
+ /** robots.txt configuration */
46
+ robots?: {
47
+ /** Paths to disallow in robots.txt */
48
+ disallow?: string[];
49
+ /** Crawl delay in seconds */
50
+ crawlDelay?: number;
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Page meta configuration (exported from page files)
56
+ */
57
+ export interface PageMeta {
58
+ title?: string;
59
+ description?: string;
60
+ keywords?: string;
61
+ author?: string;
62
+ ogTitle?: string;
63
+ ogDescription?: string;
64
+ ogImage?: string;
65
+ themeColor?: string;
66
+ lang?: string;
67
+ publishedDate?: string;
68
+ updatedDate?: string;
69
+ }
70
+
71
+ /**
72
+ * Default BertUI configuration
73
+ */
74
+ export const defaultConfig: BertuiConfig;
75
+
76
+ /**
77
+ * Load BertUI configuration from bertui.config.js
78
+ */
79
+ export function loadConfig(root: string): Promise<BertuiConfig>;
80
+ }