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,3 +1,4 @@
1
+ // bertui/src/client/compiler.js - FIXED NODE_MODULES IMPORTS
1
2
  import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
2
3
  import { join, extname, relative, dirname } from 'path';
3
4
  import logger from '../logger/logger.js';
@@ -73,12 +74,10 @@ async function discoverRoutes(pagesDir) {
73
74
  await scanDirectory(fullPath, relativePath);
74
75
  } else if (entry.isFile()) {
75
76
  const ext = extname(entry.name);
76
-
77
77
  if (ext === '.css') continue;
78
78
 
79
79
  if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
80
80
  const fileName = entry.name.replace(ext, '');
81
-
82
81
  let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
83
82
 
84
83
  if (fileName === 'index') {
@@ -100,7 +99,6 @@ async function discoverRoutes(pagesDir) {
100
99
  }
101
100
 
102
101
  await scanDirectory(pagesDir);
103
-
104
102
  routes.sort((a, b) => {
105
103
  if (a.type === b.type) {
106
104
  return a.route.localeCompare(b.route);
@@ -141,11 +139,7 @@ export function Router({ routes }) {
141
139
 
142
140
  useEffect(() => {
143
141
  matchAndSetRoute(window.location.pathname);
144
-
145
- const handlePopState = () => {
146
- matchAndSetRoute(window.location.pathname);
147
- };
148
-
142
+ const handlePopState = () => matchAndSetRoute(window.location.pathname);
149
143
  window.addEventListener('popstate', handlePopState);
150
144
  return () => window.removeEventListener('popstate', handlePopState);
151
145
  }, [routes]);
@@ -158,27 +152,21 @@ export function Router({ routes }) {
158
152
  return;
159
153
  }
160
154
  }
161
-
162
155
  for (const route of routes) {
163
156
  if (route.type === 'dynamic') {
164
157
  const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
165
158
  const regex = new RegExp('^' + pattern + '$');
166
159
  const match = pathname.match(regex);
167
-
168
160
  if (match) {
169
161
  const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
170
162
  const extractedParams = {};
171
- paramNames.forEach((name, i) => {
172
- extractedParams[name] = match[i + 1];
173
- });
174
-
163
+ paramNames.forEach((name, i) => { extractedParams[name] = match[i + 1]; });
175
164
  setCurrentRoute(route);
176
165
  setParams(extractedParams);
177
166
  return;
178
167
  }
179
168
  }
180
169
  }
181
-
182
170
  setCurrentRoute(null);
183
171
  setParams({});
184
172
  }
@@ -188,31 +176,21 @@ export function Router({ routes }) {
188
176
  matchAndSetRoute(path);
189
177
  }
190
178
 
191
- const routerValue = {
192
- currentRoute,
193
- params,
194
- navigate,
195
- pathname: window.location.pathname
196
- };
197
-
198
179
  const Component = currentRoute?.component;
199
-
200
180
  return React.createElement(
201
181
  RouterContext.Provider,
202
- { value: routerValue },
203
- Component ? React.createElement(Component, { params }) : React.createElement(NotFound, null)
182
+ { value: { currentRoute, params, navigate, pathname: window.location.pathname } },
183
+ Component ? React.createElement(Component, { params }) : React.createElement(NotFound)
204
184
  );
205
185
  }
206
186
 
207
187
  export function Link({ to, children, ...props }) {
208
188
  const { navigate } = useRouter();
209
-
210
- function handleClick(e) {
211
- e.preventDefault();
212
- navigate(to);
213
- }
214
-
215
- return React.createElement('a', { href: to, onClick: handleClick, ...props }, children);
189
+ return React.createElement('a', {
190
+ href: to,
191
+ onClick: (e) => { e.preventDefault(); navigate(to); },
192
+ ...props
193
+ }, children);
216
194
  }
217
195
 
218
196
  function NotFound() {
@@ -250,7 +228,6 @@ ${routeConfigs}
250
228
 
251
229
  async function compileDirectory(srcDir, outDir, root, envVars) {
252
230
  const stats = { files: 0, skipped: 0 };
253
-
254
231
  const files = readdirSync(srcDir);
255
232
 
256
233
  for (const file of files) {
@@ -287,7 +264,6 @@ async function compileDirectory(srcDir, outDir, root, envVars) {
287
264
  code = replaceEnvInCode(code, envVars);
288
265
  code = fixRouterImports(code, outPath, root);
289
266
 
290
- // ✅ CRITICAL FIX: Ensure React import for .js files with JSX
291
267
  if (usesJSX(code) && !code.includes('import React')) {
292
268
  code = `import React from 'react';\n${code}`;
293
269
  }
@@ -331,11 +307,11 @@ async function compileFile(srcPath, outDir, filename, relativePath, root, envVar
331
307
  });
332
308
  let compiled = await transpiler.transform(code);
333
309
 
334
- // ✅ CRITICAL FIX: Always add React import if JSX is present
335
310
  if (usesJSX(compiled) && !compiled.includes('import React')) {
336
311
  compiled = `import React from 'react';\n${compiled}`;
337
312
  }
338
313
 
314
+ // ✅ CRITICAL FIX: Don't touch node_modules imports
339
315
  compiled = fixRelativeImports(compiled);
340
316
 
341
317
  await Bun.write(outPath, compiled);
@@ -346,13 +322,12 @@ async function compileFile(srcPath, outDir, filename, relativePath, root, envVar
346
322
  }
347
323
  }
348
324
 
349
- // ✅ NEW: Detect if code uses JSX
350
325
  function usesJSX(code) {
351
326
  return code.includes('React.createElement') ||
352
327
  code.includes('React.Fragment') ||
353
- /<[A-Z]/.test(code) || // Detects JSX tags like <Component>
354
- code.includes('jsx(') || // Runtime JSX
355
- code.includes('jsxs('); // Runtime JSX
328
+ /<[A-Z]/.test(code) ||
329
+ code.includes('jsx(') ||
330
+ code.includes('jsxs(');
356
331
  }
357
332
 
358
333
  function removeCSSImports(code) {
@@ -384,13 +359,19 @@ function fixRouterImports(code, outPath, root) {
384
359
  }
385
360
 
386
361
  function fixRelativeImports(code) {
387
- const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
362
+ // CRITICAL FIX: Only fix relative imports, NOT bare specifiers like 'bertui-icons'
363
+ // Regex explanation:
364
+ // - Match: from './file' or from '../file'
365
+ // - DON'T match: from 'bertui-icons' or from 'react'
366
+
367
+ const importRegex = /from\s+['"](\.\.?\/[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json)['"]/g;
388
368
 
389
- code = code.replace(importRegex, (match, prefix, path) => {
369
+ code = code.replace(importRegex, (match, path) => {
370
+ // Don't add .js if path already has an extension or ends with /
390
371
  if (path.endsWith('/') || /\.\w+$/.test(path)) {
391
372
  return match;
392
373
  }
393
- return `from '${prefix}${path}.js';`;
374
+ return `from '${path}.js'`;
394
375
  });
395
376
 
396
377
  return code;
@@ -1,16 +1,36 @@
1
+ // bertui/src/config/defaultConfig.js
2
+ // Default configuration used when bertui.config.js is not present
3
+
1
4
  export const defaultConfig = {
5
+ // Site information (used for sitemap generation)
6
+ siteName: "BertUI App",
7
+ baseUrl: "http://localhost:3000", // Default to localhost
8
+
9
+ // HTML Meta Tags (SEO)
2
10
  meta: {
3
- title: "BertUI App",
4
- description: "Built with BertUI - Lightning fast React development",
5
- keywords: "react, bun, bertui",
11
+ title: "BertUI - Lightning Fast React",
12
+ description: "Build lightning-fast React applications with file-based routing powered by Bun",
13
+ keywords: "react, bun, bertui, fast, file-based routing",
6
14
  author: "Pease Ernest",
7
- ogImage: "/og-image.png",
8
- themeColor: "#f30606ff",
9
- lang: "en"
15
+ themeColor: "#667eea",
16
+ lang: "en",
17
+
18
+ // Open Graph for social sharing
19
+ ogTitle: "BertUI - Lightning Fast React Framework",
20
+ ogDescription: "Build lightning-fast React apps with zero config",
21
+ ogImage: "/og-image.png"
10
22
  },
23
+
24
+ // App Shell Configuration
11
25
  appShell: {
12
26
  loading: true,
13
27
  loadingText: "Loading...",
14
28
  backgroundColor: "#ffffff"
29
+ },
30
+
31
+ // robots.txt Configuration
32
+ robots: {
33
+ disallow: [], // No paths blocked by default
34
+ crawlDelay: null // No crawl delay by default
15
35
  }
16
36
  };
@@ -1,4 +1,4 @@
1
- // src/config/loadConfig.js
1
+ // src/config/loadConfig.js - COMPLETE CORRECTED VERSION
2
2
  import { join } from 'path';
3
3
  import { existsSync } from 'fs';
4
4
  import { defaultConfig } from './defaultConfig.js';
@@ -13,6 +13,13 @@ export async function loadConfig(root) {
13
13
  const userConfig = await import(configPath);
14
14
  logger.success('Loaded bertui.config.js');
15
15
 
16
+ // DEBUG: Show what we loaded
17
+ logger.info(`📋 Config loaded: ${JSON.stringify({
18
+ hasSiteName: !!(userConfig.default?.siteName || userConfig.siteName),
19
+ hasBaseUrl: !!(userConfig.default?.baseUrl || userConfig.baseUrl),
20
+ hasRobots: !!(userConfig.default?.robots || userConfig.robots)
21
+ })}`);
22
+
16
23
  // Merge user config with defaults
17
24
  return mergeConfig(defaultConfig, userConfig.default || userConfig);
18
25
  } catch (error) {
@@ -26,8 +33,17 @@ export async function loadConfig(root) {
26
33
  }
27
34
 
28
35
  function mergeConfig(defaults, user) {
29
- return {
30
- meta: { ...defaults.meta, ...(user.meta || {}) },
31
- appShell: { ...defaults.appShell, ...(user.appShell || {}) }
32
- };
36
+ // Start with user config (so user values override defaults)
37
+ const merged = { ...user };
38
+
39
+ // Deep merge for nested objects
40
+ merged.meta = { ...defaults.meta, ...(user.meta || {}) };
41
+ merged.appShell = { ...defaults.appShell, ...(user.appShell || {}) };
42
+ merged.robots = { ...defaults.robots, ...(user.robots || {}) };
43
+
44
+ // Ensure we have required top-level fields
45
+ if (!merged.siteName) merged.siteName = defaults.siteName;
46
+ if (!merged.baseUrl) merged.baseUrl = defaults.baseUrl;
47
+
48
+ return merged;
33
49
  }
@@ -0,0 +1,191 @@
1
+ // bertui/src/pagebuilder/core.js
2
+ import { join } from 'path';
3
+ import { existsSync, mkdirSync } from 'fs';
4
+ import logger from '../logger/logger.js';
5
+
6
+ /**
7
+ * Run page builder to generate pages from config
8
+ * @param {string} root - Project root directory
9
+ * @param {Object} config - BertUI configuration
10
+ */
11
+ export async function runPageBuilder(root, config) {
12
+ const pagesDir = join(root, 'src', 'pages');
13
+
14
+ if (!config.pageBuilder || typeof config.pageBuilder !== 'object') {
15
+ logger.debug('No page builder configuration found');
16
+ return;
17
+ }
18
+
19
+ const { pages } = config.pageBuilder;
20
+
21
+ if (!pages || !Array.isArray(pages) || pages.length === 0) {
22
+ logger.debug('No pages defined in page builder');
23
+ return;
24
+ }
25
+
26
+ logger.info(`📄 Page Builder: Generating ${pages.length} page(s)...`);
27
+
28
+ // Ensure pages directory exists
29
+ const generatedDir = join(pagesDir, 'generated');
30
+ if (!existsSync(generatedDir)) {
31
+ mkdirSync(generatedDir, { recursive: true });
32
+ }
33
+
34
+ for (const page of pages) {
35
+ try {
36
+ await generatePage(page, generatedDir);
37
+ logger.success(`✅ Generated: ${page.name}`);
38
+ } catch (error) {
39
+ logger.error(`❌ Failed to generate ${page.name}: ${error.message}`);
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Generate a single page from configuration
46
+ * @param {Object} pageConfig - Page configuration
47
+ * @param {string} outputDir - Output directory
48
+ */
49
+ async function generatePage(pageConfig, outputDir) {
50
+ const { name, type, data } = pageConfig;
51
+
52
+ if (!name) {
53
+ throw new Error('Page name is required');
54
+ }
55
+
56
+ let pageContent = '';
57
+
58
+ switch (type) {
59
+ case 'markdown':
60
+ pageContent = generateMarkdownPage(name, data);
61
+ break;
62
+
63
+ case 'json':
64
+ pageContent = generateJsonPage(name, data);
65
+ break;
66
+
67
+ case 'custom':
68
+ pageContent = data.template || generateDefaultPage(name, data);
69
+ break;
70
+
71
+ default:
72
+ pageContent = generateDefaultPage(name, data);
73
+ }
74
+
75
+ // Write the generated page
76
+ const filename = name.toLowerCase().replace(/\s+/g, '-') + '.jsx';
77
+ const filepath = join(outputDir, filename);
78
+
79
+ await Bun.write(filepath, pageContent);
80
+ }
81
+
82
+ /**
83
+ * Generate a default React page
84
+ */
85
+ function generateDefaultPage(name, data) {
86
+ const title = data?.title || name;
87
+ const content = data?.content || `<p>Welcome to ${name}</p>`;
88
+
89
+ return `// Auto-generated page: ${name}
90
+ import React from 'react';
91
+
92
+ export const title = "${title}";
93
+ export const description = "${data?.description || `${name} page`}";
94
+
95
+ export default function ${sanitizeComponentName(name)}() {
96
+ return (
97
+ <div>
98
+ <h1>${title}</h1>
99
+ ${content}
100
+ </div>
101
+ );
102
+ }
103
+ `;
104
+ }
105
+
106
+ /**
107
+ * Generate a page from Markdown data
108
+ */
109
+ function generateMarkdownPage(name, data) {
110
+ const title = data?.title || name;
111
+ const markdown = data?.markdown || '';
112
+
113
+ // Simple markdown to JSX conversion
114
+ const jsxContent = convertMarkdownToJSX(markdown);
115
+
116
+ return `// Auto-generated markdown page: ${name}
117
+ import React from 'react';
118
+
119
+ export const title = "${title}";
120
+ export const description = "${data?.description || `${name} page`}";
121
+
122
+ export default function ${sanitizeComponentName(name)}() {
123
+ return (
124
+ <div className="markdown-content">
125
+ ${jsxContent}
126
+ </div>
127
+ );
128
+ }
129
+ `;
130
+ }
131
+
132
+ /**
133
+ * Generate a page from JSON data
134
+ */
135
+ function generateJsonPage(name, data) {
136
+ const title = data?.title || name;
137
+ const items = data?.items || [];
138
+
139
+ return `// Auto-generated JSON page: ${name}
140
+ import React from 'react';
141
+
142
+ export const title = "${title}";
143
+ export const description = "${data?.description || `${name} page`}";
144
+
145
+ const items = ${JSON.stringify(items, null, 2)};
146
+
147
+ export default function ${sanitizeComponentName(name)}() {
148
+ return (
149
+ <div>
150
+ <h1>${title}</h1>
151
+ <ul>
152
+ {items.map((item, index) => (
153
+ <li key={index}>{item.title || item.name || item}</li>
154
+ ))}
155
+ </ul>
156
+ </div>
157
+ );
158
+ }
159
+ `;
160
+ }
161
+
162
+ /**
163
+ * Convert markdown to JSX (basic implementation)
164
+ */
165
+ function convertMarkdownToJSX(markdown) {
166
+ let jsx = markdown
167
+ // Headers
168
+ .replace(/^### (.*$)/gm, '<h3>$1</h3>')
169
+ .replace(/^## (.*$)/gm, '<h2>$1</h2>')
170
+ .replace(/^# (.*$)/gm, '<h1>$1</h1>')
171
+ // Bold
172
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
173
+ // Italic
174
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
175
+ // Paragraphs
176
+ .split('\n\n')
177
+ .map(para => para.trim() ? `<p>${para}</p>` : '')
178
+ .join('\n ');
179
+
180
+ return jsx;
181
+ }
182
+
183
+ /**
184
+ * Sanitize component name (must be valid React component name)
185
+ */
186
+ function sanitizeComponentName(name) {
187
+ return name
188
+ .replace(/[^a-zA-Z0-9]/g, '')
189
+ .replace(/^[0-9]/, 'Page$&')
190
+ .replace(/^./, c => c.toUpperCase());
191
+ }