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.
- package/README.md +424 -435
- package/package.json +26 -6
- package/src/build/compiler/file-transpiler.js +171 -0
- package/src/build/compiler/index.js +45 -0
- package/src/build/compiler/route-discoverer.js +46 -0
- package/src/build/compiler/router-generator.js +104 -0
- package/src/build/generators/html-generator.js +293 -0
- package/src/build/generators/robots-generator.js +58 -0
- package/src/build/generators/sitemap-generator.js +63 -0
- package/src/build/processors/asset-processor.js +19 -0
- package/src/build/processors/css-builder.js +102 -0
- package/src/build.js +100 -680
- package/src/client/compiler.js +23 -42
- package/src/config/defaultConfig.js +26 -6
- package/src/config/loadConfig.js +21 -5
- package/src/pagebuilder/core.js +191 -0
- package/src/server/dev-server.js +134 -36
- package/src/utils/env.js +59 -39
- package/src/utils/meta-extractor.js +127 -0
- package/types/config.d.ts +80 -0
- package/types/index.d.ts +116 -0
- package/types/react.d.ts +13 -0
- package/types/router.d.ts +79 -0
package/src/client/compiler.js
CHANGED
|
@@ -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:
|
|
203
|
-
Component ? React.createElement(Component, { params }) : React.createElement(NotFound
|
|
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
|
-
|
|
211
|
-
e.preventDefault();
|
|
212
|
-
|
|
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) ||
|
|
354
|
-
code.includes('jsx(') ||
|
|
355
|
-
code.includes('jsxs(');
|
|
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
|
-
|
|
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,
|
|
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 '${
|
|
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
|
|
4
|
-
description: "
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
};
|
package/src/config/loadConfig.js
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
}
|