bertui 0.1.5 → 0.1.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/index.js +1 -1
- package/package.json +3 -2
- package/src/client/compiler.js +201 -66
- package/src/router/router.js +105 -192
- package/src/server/dev-server.js +97 -23
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bertui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Lightning-fast React dev server powered by Bun and Elysia",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./index.js",
|
|
12
12
|
"./styles": "./src/styles/bertui.css",
|
|
13
|
-
"./logger": "./src/logger/logger.js"
|
|
13
|
+
"./logger": "./src/logger/logger.js",
|
|
14
|
+
"./router": "./src/router/Router.jsx"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"bin",
|
package/src/client/compiler.js
CHANGED
|
@@ -1,91 +1,226 @@
|
|
|
1
1
|
// src/client/compiler.js
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
3
|
+
import { join, extname, relative, sep } from 'path';
|
|
4
4
|
import logger from '../logger/logger.js';
|
|
5
|
-
import { generateRoutes, generateRouterCode, generateMainWithRouter, logRoutes } from '../router/router.js';
|
|
6
5
|
|
|
7
6
|
export async function compileProject(root) {
|
|
8
|
-
|
|
9
|
-
const routerDir = join(root, '.bertui');
|
|
7
|
+
logger.bigLog('COMPILING PROJECT', { color: 'blue' });
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
const srcDir = join(root, 'src');
|
|
10
|
+
const pagesDir = join(srcDir, 'pages');
|
|
11
|
+
const outDir = join(root, '.bertui', 'compiled');
|
|
12
|
+
|
|
13
|
+
// Check if src exists
|
|
14
|
+
if (!existsSync(srcDir)) {
|
|
15
|
+
logger.error('src/ directory not found!');
|
|
16
|
+
process.exit(1);
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
// Create output directory
|
|
20
|
+
if (!existsSync(outDir)) {
|
|
21
|
+
mkdirSync(outDir, { recursive: true });
|
|
22
|
+
logger.info('Created .bertui/compiled/');
|
|
23
|
+
}
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
// Discover routes if pages directory exists
|
|
26
|
+
let routes = [];
|
|
27
|
+
if (existsSync(pagesDir)) {
|
|
28
|
+
routes = await discoverRoutes(pagesDir);
|
|
29
|
+
logger.info(`Discovered ${routes.length} routes`);
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
// Display routes table
|
|
32
|
+
if (routes.length > 0) {
|
|
33
|
+
logger.bigLog('ROUTES DISCOVERED', { color: 'blue' });
|
|
34
|
+
logger.table(routes.map((r, i) => ({
|
|
35
|
+
'': i,
|
|
36
|
+
route: r.route,
|
|
37
|
+
file: r.file,
|
|
38
|
+
type: r.type
|
|
39
|
+
})));
|
|
32
40
|
|
|
33
|
-
// Generate router
|
|
34
|
-
|
|
35
|
-
const routerPath = join(routerDir, 'router.js');
|
|
36
|
-
writeFileSync(routerPath, routerCode);
|
|
41
|
+
// Generate router file
|
|
42
|
+
await generateRouter(routes, outDir);
|
|
37
43
|
logger.info('Generated router.js');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Compile all files
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
const stats = await compileDirectory(srcDir, outDir, root);
|
|
50
|
+
const duration = Date.now() - startTime;
|
|
51
|
+
|
|
52
|
+
logger.success(`Compiled ${stats.files} files in ${duration}ms`);
|
|
53
|
+
logger.info(`Output: ${outDir}`);
|
|
54
|
+
|
|
55
|
+
return { outDir, stats, routes };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function discoverRoutes(pagesDir) {
|
|
59
|
+
const routes = [];
|
|
60
|
+
|
|
61
|
+
async function scanDirectory(dir, basePath = '') {
|
|
62
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
63
|
+
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const fullPath = join(dir, entry.name);
|
|
66
|
+
const relativePath = join(basePath, entry.name);
|
|
38
67
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
// Recursively scan subdirectories
|
|
70
|
+
await scanDirectory(fullPath, relativePath);
|
|
71
|
+
} else if (entry.isFile()) {
|
|
72
|
+
const ext = extname(entry.name);
|
|
73
|
+
if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
|
|
74
|
+
const fileName = entry.name.replace(ext, '');
|
|
75
|
+
|
|
76
|
+
// Generate route path
|
|
77
|
+
let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
|
|
78
|
+
|
|
79
|
+
// Handle index files
|
|
80
|
+
if (fileName === 'index') {
|
|
81
|
+
route = route.replace('/index', '') || '/';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Determine route type
|
|
85
|
+
const isDynamic = fileName.includes('[') && fileName.includes(']');
|
|
86
|
+
const type = isDynamic ? 'dynamic' : 'static';
|
|
87
|
+
|
|
88
|
+
routes.push({
|
|
89
|
+
route: route === '' ? '/' : route,
|
|
90
|
+
file: relativePath,
|
|
91
|
+
path: fullPath,
|
|
92
|
+
type
|
|
93
|
+
});
|
|
94
|
+
}
|
|
51
95
|
}
|
|
52
96
|
}
|
|
53
|
-
|
|
54
|
-
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await scanDirectory(pagesDir);
|
|
100
|
+
|
|
101
|
+
// Sort routes: static routes first, then dynamic
|
|
102
|
+
routes.sort((a, b) => {
|
|
103
|
+
if (a.type === b.type) {
|
|
104
|
+
return a.route.localeCompare(b.route);
|
|
105
|
+
}
|
|
106
|
+
return a.type === 'static' ? -1 : 1;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return routes;
|
|
110
|
+
}
|
|
55
111
|
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
112
|
+
async function generateRouter(routes, outDir) {
|
|
113
|
+
const imports = routes.map((route, i) => {
|
|
114
|
+
const componentName = `Page${i}`;
|
|
115
|
+
const importPath = `./pages/${route.file.replace(/\\/g, '/')}`;
|
|
116
|
+
return `import ${componentName} from '${importPath}';`;
|
|
117
|
+
}).join('\n');
|
|
118
|
+
|
|
119
|
+
const routeConfigs = routes.map((route, i) => {
|
|
120
|
+
const componentName = `Page${i}`;
|
|
121
|
+
return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
|
|
122
|
+
}).join(',\n');
|
|
123
|
+
|
|
124
|
+
const routerCode = `// Auto-generated router - DO NOT EDIT
|
|
125
|
+
${imports}
|
|
126
|
+
|
|
127
|
+
export const routes = [
|
|
128
|
+
${routeConfigs}
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
export function matchRoute(pathname) {
|
|
132
|
+
// Try exact match first
|
|
133
|
+
for (const route of routes) {
|
|
134
|
+
if (route.type === 'static' && route.path === pathname) {
|
|
135
|
+
return route;
|
|
136
|
+
}
|
|
68
137
|
}
|
|
69
|
-
|
|
138
|
+
|
|
139
|
+
// Try dynamic routes
|
|
140
|
+
for (const route of routes) {
|
|
141
|
+
if (route.type === 'dynamic') {
|
|
142
|
+
const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
|
|
143
|
+
const regex = new RegExp('^' + pattern + '$');
|
|
144
|
+
const match = pathname.match(regex);
|
|
145
|
+
|
|
146
|
+
if (match) {
|
|
147
|
+
// Extract params
|
|
148
|
+
const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
|
|
149
|
+
const params = {};
|
|
150
|
+
paramNames.forEach((name, i) => {
|
|
151
|
+
params[name] = match[i + 1];
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return { ...route, params };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
const routerPath = join(outDir, 'router.js');
|
|
164
|
+
await Bun.write(routerPath, routerCode);
|
|
165
|
+
}
|
|
70
166
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
167
|
+
async function compileDirectory(srcDir, outDir, root) {
|
|
168
|
+
const stats = { files: 0, skipped: 0 };
|
|
169
|
+
|
|
170
|
+
const files = readdirSync(srcDir);
|
|
171
|
+
|
|
172
|
+
for (const file of files) {
|
|
173
|
+
const srcPath = join(srcDir, file);
|
|
174
|
+
const stat = statSync(srcPath);
|
|
175
|
+
|
|
176
|
+
if (stat.isDirectory()) {
|
|
177
|
+
// Recursively compile subdirectories
|
|
178
|
+
const subOutDir = join(outDir, file);
|
|
179
|
+
mkdirSync(subOutDir, { recursive: true });
|
|
180
|
+
const subStats = await compileDirectory(srcPath, subOutDir, root);
|
|
181
|
+
stats.files += subStats.files;
|
|
182
|
+
stats.skipped += subStats.skipped;
|
|
183
|
+
} else {
|
|
184
|
+
// Compile file
|
|
185
|
+
const ext = extname(file);
|
|
186
|
+
const relativePath = relative(join(root, 'src'), srcPath);
|
|
187
|
+
|
|
188
|
+
if (['.jsx', '.tsx', '.ts'].includes(ext)) {
|
|
189
|
+
await compileFile(srcPath, outDir, file, relativePath);
|
|
190
|
+
stats.files++;
|
|
191
|
+
} else if (ext === '.js' || ext === '.css') {
|
|
192
|
+
// Copy as-is
|
|
193
|
+
const outPath = join(outDir, file);
|
|
194
|
+
await Bun.write(outPath, Bun.file(srcPath));
|
|
195
|
+
logger.debug(`Copied: ${relativePath}`);
|
|
196
|
+
stats.files++;
|
|
197
|
+
} else {
|
|
198
|
+
logger.debug(`Skipped: ${relativePath}`);
|
|
199
|
+
stats.skipped++;
|
|
81
200
|
}
|
|
82
|
-
}
|
|
201
|
+
}
|
|
83
202
|
}
|
|
84
203
|
|
|
85
|
-
|
|
204
|
+
return stats;
|
|
86
205
|
}
|
|
206
|
+
|
|
207
|
+
async function compileFile(srcPath, outDir, filename, relativePath) {
|
|
208
|
+
const ext = extname(filename);
|
|
209
|
+
const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const transpiler = new Bun.Transpiler({ loader });
|
|
213
|
+
const code = await Bun.file(srcPath).text();
|
|
214
|
+
const compiled = await transpiler.transform(code);
|
|
215
|
+
|
|
216
|
+
// Change extension to .js
|
|
217
|
+
const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
|
|
218
|
+
const outPath = join(outDir, outFilename);
|
|
219
|
+
|
|
220
|
+
await Bun.write(outPath, compiled);
|
|
221
|
+
logger.debug(`Compiled: ${relativePath} → ${outFilename}`);
|
|
87
222
|
} catch (error) {
|
|
88
|
-
logger.error(`
|
|
223
|
+
logger.error(`Failed to compile ${relativePath}: ${error.message}`);
|
|
89
224
|
throw error;
|
|
90
225
|
}
|
|
91
226
|
}
|
package/src/router/router.js
CHANGED
|
@@ -1,216 +1,129 @@
|
|
|
1
|
-
// src/router/
|
|
2
|
-
import {
|
|
3
|
-
import { readdirSync, statSync, existsSync } from 'fs';
|
|
4
|
-
import logger from 'ernest-logger'; // Assuming Ernest Logger is used
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Scans the pages directory and generates route definitions
|
|
8
|
-
*/
|
|
9
|
-
export function generateRoutes(root) {
|
|
10
|
-
const pagesDir = join(root, 'src', 'pages');
|
|
11
|
-
|
|
12
|
-
if (!existsSync(pagesDir)) {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const routes = [];
|
|
17
|
-
|
|
18
|
-
function scanDirectory(dir, basePath = '') {
|
|
19
|
-
const entries = readdirSync(dir);
|
|
20
|
-
|
|
21
|
-
for (const entry of entries) {
|
|
22
|
-
const fullPath = join(dir, entry);
|
|
23
|
-
const stat = statSync(fullPath);
|
|
24
|
-
|
|
25
|
-
if (stat.isDirectory()) {
|
|
26
|
-
// Recursively scan subdirectories
|
|
27
|
-
scanDirectory(fullPath, join(basePath, entry));
|
|
28
|
-
} else if (stat.isFile() && /\.(jsx?|tsx?)$/.test(entry)) {
|
|
29
|
-
const parsed = parse(entry);
|
|
30
|
-
let fileName = parsed.name;
|
|
31
|
-
|
|
32
|
-
// Skip non-page files (e.g., those starting with a dot)
|
|
33
|
-
if (fileName.startsWith('.') || fileName.startsWith('~')) {
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// --- ROUTE PATH GENERATION ---
|
|
38
|
-
|
|
39
|
-
// Handle dynamic routes: _filename -> :filename
|
|
40
|
-
let isDynamic = false;
|
|
41
|
-
if (fileName.startsWith('_')) {
|
|
42
|
-
fileName = fileName.slice(1); // Remove the underscore
|
|
43
|
-
isDynamic = true;
|
|
44
|
-
}
|
|
1
|
+
// src/router/Router.jsx
|
|
2
|
+
import { useState, useEffect, createContext, useContext } from 'react';
|
|
45
3
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
routePath = '/' + routePath.replace(/\\/g, '/'); // Use forward slashes
|
|
49
|
-
|
|
50
|
-
// Apply dynamic parameter if detected
|
|
51
|
-
if (isDynamic) {
|
|
52
|
-
// Replace the last part of the route path with the dynamic parameter (e.g., /blog/slug -> /blog/:slug)
|
|
53
|
-
routePath = routePath.replace(new RegExp(`/${fileName}$`), `/:${fileName}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Handle the root path, ensuring it's just '/'
|
|
57
|
-
if (routePath === '//') {
|
|
58
|
-
routePath = '/';
|
|
59
|
-
}
|
|
4
|
+
// Router context
|
|
5
|
+
const RouterContext = createContext(null);
|
|
60
6
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
path: routePath,
|
|
66
|
-
file: relativePath,
|
|
67
|
-
component: fullPath,
|
|
68
|
-
isDynamic: isDynamic || routePath.includes(':')
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
}
|
|
7
|
+
export function useRouter() {
|
|
8
|
+
const context = useContext(RouterContext);
|
|
9
|
+
if (!context) {
|
|
10
|
+
throw new Error('useRouter must be used within a Router component');
|
|
72
11
|
}
|
|
73
|
-
|
|
74
|
-
scanDirectory(pagesDir);
|
|
75
|
-
|
|
76
|
-
// Sort routes: Root path first, then static, then dynamic
|
|
77
|
-
routes.sort((a, b) => {
|
|
78
|
-
if (a.path === '/') return -1;
|
|
79
|
-
if (b.path === '/') return 1;
|
|
80
|
-
if (a.isDynamic && !b.isDynamic) return 1;
|
|
81
|
-
if (!a.isDynamic && b.isDynamic) return -1;
|
|
82
|
-
return a.path.localeCompare(b.path);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
return routes;
|
|
12
|
+
return context;
|
|
86
13
|
}
|
|
87
14
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
path: route.path,
|
|
98
|
-
component: `Page${index}`
|
|
99
|
-
}));
|
|
100
|
-
|
|
101
|
-
return `
|
|
102
|
-
// Auto-generated router code - DO NOT EDIT MANUALLY
|
|
103
|
-
import React, { useState, useEffect } from 'react';
|
|
104
|
-
${imports}
|
|
105
|
-
|
|
106
|
-
const routes = ${JSON.stringify(routeConfigs, null, 2).replace(/"Page(\d+)"/g, 'Page$1')};
|
|
107
|
-
|
|
108
|
-
export function Router() {
|
|
109
|
-
const [currentPath, setCurrentPath] = useState(window.location.pathname);
|
|
110
|
-
|
|
15
|
+
export function useParams() {
|
|
16
|
+
const { params } = useRouter();
|
|
17
|
+
return params;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Router({ routes, children }) {
|
|
21
|
+
const [currentRoute, setCurrentRoute] = useState(null);
|
|
22
|
+
const [params, setParams] = useState({});
|
|
23
|
+
|
|
111
24
|
useEffect(() => {
|
|
25
|
+
// Match initial route
|
|
26
|
+
matchAndSetRoute(window.location.pathname);
|
|
27
|
+
|
|
28
|
+
// Handle browser navigation
|
|
112
29
|
const handlePopState = () => {
|
|
113
|
-
|
|
30
|
+
matchAndSetRoute(window.location.pathname);
|
|
114
31
|
};
|
|
115
|
-
|
|
32
|
+
|
|
116
33
|
window.addEventListener('popstate', handlePopState);
|
|
117
34
|
return () => window.removeEventListener('popstate', handlePopState);
|
|
118
35
|
}, []);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
routeParts.forEach((part, i) => {
|
|
151
|
-
if (part.startsWith(':')) {
|
|
152
|
-
const paramName = part.slice(1);
|
|
153
|
-
params[paramName] = pathParts[i];
|
|
36
|
+
|
|
37
|
+
function matchAndSetRoute(pathname) {
|
|
38
|
+
// Try exact match first (static routes)
|
|
39
|
+
for (const route of routes) {
|
|
40
|
+
if (route.type === 'static' && route.path === pathname) {
|
|
41
|
+
setCurrentRoute(route);
|
|
42
|
+
setParams({});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Try dynamic routes
|
|
48
|
+
for (const route of routes) {
|
|
49
|
+
if (route.type === 'dynamic') {
|
|
50
|
+
const pattern = route.path.replace(/\[([^\]]+)\]/g, '([^/]+)');
|
|
51
|
+
const regex = new RegExp('^' + pattern + '$');
|
|
52
|
+
const match = pathname.match(regex);
|
|
53
|
+
|
|
54
|
+
if (match) {
|
|
55
|
+
// Extract params
|
|
56
|
+
const paramNames = [...route.path.matchAll(/\[([^\]]+)\]/g)].map(m => m[1]);
|
|
57
|
+
const extractedParams = {};
|
|
58
|
+
paramNames.forEach((name, i) => {
|
|
59
|
+
extractedParams[name] = match[i + 1];
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
setCurrentRoute(route);
|
|
63
|
+
setParams(extractedParams);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
154
66
|
}
|
|
155
|
-
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// No match found - 404
|
|
70
|
+
setCurrentRoute(null);
|
|
71
|
+
setParams({});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function navigate(path) {
|
|
75
|
+
window.history.pushState({}, '', path);
|
|
76
|
+
matchAndSetRoute(path);
|
|
156
77
|
}
|
|
157
|
-
|
|
158
|
-
const Component = matchedRoute.component;
|
|
159
|
-
return <Component params={params} />;
|
|
160
|
-
}
|
|
161
78
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
79
|
+
const routerValue = {
|
|
80
|
+
currentRoute,
|
|
81
|
+
params,
|
|
82
|
+
navigate,
|
|
83
|
+
pathname: window.location.pathname
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<RouterContext.Provider value={routerValue}>
|
|
88
|
+
{currentRoute ? (
|
|
89
|
+
<currentRoute.component />
|
|
90
|
+
) : (
|
|
91
|
+
children || <NotFound />
|
|
92
|
+
)}
|
|
93
|
+
</RouterContext.Provider>
|
|
94
|
+
);
|
|
166
95
|
}
|
|
167
96
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
97
|
+
export function Link({ to, children, className, ...props }) {
|
|
98
|
+
const { navigate } = useRouter();
|
|
99
|
+
|
|
100
|
+
function handleClick(e) {
|
|
171
101
|
e.preventDefault();
|
|
172
|
-
navigate(
|
|
173
|
-
}
|
|
174
|
-
|
|
102
|
+
navigate(to);
|
|
103
|
+
}
|
|
104
|
+
|
|
175
105
|
return (
|
|
176
|
-
<a href={
|
|
106
|
+
<a href={to} onClick={handleClick} className={className} {...props}>
|
|
177
107
|
{children}
|
|
178
108
|
</a>
|
|
179
109
|
);
|
|
180
110
|
}
|
|
181
|
-
`;
|
|
182
|
-
}
|
|
183
111
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (routes.length === 0) {
|
|
202
|
-
logger.warn('No routes found in src/pages/');
|
|
203
|
-
logger.info('Create files in src/pages/ to define routes:');
|
|
204
|
-
logger.info(' src/pages/index.jsx → /');
|
|
205
|
-
logger.info(' src/pages/about.jsx → /about');
|
|
206
|
-
logger.info(' src/pages/user/_id.jsx → /user/:id'); // Updated tip
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
logger.bigLog('ROUTES DISCOVERED', { color: 'cyan' });
|
|
211
|
-
logger.table(routes.map(r => ({
|
|
212
|
-
route: r.path,
|
|
213
|
-
file: r.file,
|
|
214
|
-
type: r.isDynamic ? 'dynamic' : 'static'
|
|
215
|
-
})));
|
|
112
|
+
function NotFound() {
|
|
113
|
+
return (
|
|
114
|
+
<div style={{
|
|
115
|
+
display: 'flex',
|
|
116
|
+
flexDirection: 'column',
|
|
117
|
+
alignItems: 'center',
|
|
118
|
+
justifyContent: 'center',
|
|
119
|
+
minHeight: '100vh',
|
|
120
|
+
fontFamily: 'system-ui, sans-serif'
|
|
121
|
+
}}>
|
|
122
|
+
<h1 style={{ fontSize: '6rem', margin: 0 }}>404</h1>
|
|
123
|
+
<p style={{ fontSize: '1.5rem', color: '#666' }}>Page not found</p>
|
|
124
|
+
<a href="/" style={{ color: '#10b981', textDecoration: 'none', fontSize: '1.2rem' }}>
|
|
125
|
+
Go home
|
|
126
|
+
</a>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
216
129
|
}
|
package/src/server/dev-server.js
CHANGED
|
@@ -12,28 +12,49 @@ export async function startDevServer(options = {}) {
|
|
|
12
12
|
const compiledDir = join(root, '.bertui', 'compiled');
|
|
13
13
|
|
|
14
14
|
const clients = new Set();
|
|
15
|
+
let hasRouter = false;
|
|
16
|
+
|
|
17
|
+
// Check if router exists
|
|
18
|
+
const routerPath = join(compiledDir, 'router.js');
|
|
19
|
+
if (existsSync(routerPath)) {
|
|
20
|
+
hasRouter = true;
|
|
21
|
+
logger.info('Router-based routing enabled');
|
|
22
|
+
}
|
|
15
23
|
|
|
16
24
|
const app = new Elysia()
|
|
25
|
+
// Main HTML route - serves all pages
|
|
17
26
|
.get('/', async () => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<title>BertUI App - Dev</title>
|
|
25
|
-
<link rel="stylesheet" href="/styles/bertui.css">
|
|
26
|
-
</head>
|
|
27
|
-
<body>
|
|
28
|
-
<div id="root"></div>
|
|
29
|
-
<script type="module" src="/hmr-client.js"></script>
|
|
30
|
-
<script type="module" src="/compiled/main.js"></script>
|
|
31
|
-
</body>
|
|
32
|
-
</html>`;
|
|
27
|
+
return serveHTML(root, hasRouter);
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Catch-all route for SPA routing
|
|
31
|
+
.get('/*', async ({ params, set }) => {
|
|
32
|
+
const path = params['*'];
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
// Check if it's a file request
|
|
35
|
+
if (path.includes('.')) {
|
|
36
|
+
// Try to serve as static file
|
|
37
|
+
const filePath = join(compiledDir, path);
|
|
38
|
+
const file = Bun.file(filePath);
|
|
39
|
+
|
|
40
|
+
if (await file.exists()) {
|
|
41
|
+
const ext = extname(path);
|
|
42
|
+
const contentType = getContentType(ext);
|
|
43
|
+
|
|
44
|
+
return new Response(await file.text(), {
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': contentType,
|
|
47
|
+
'Cache-Control': 'no-store'
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
set.status = 404;
|
|
53
|
+
return 'File not found';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// For non-file routes, serve the main HTML (SPA mode)
|
|
57
|
+
return serveHTML(root, hasRouter);
|
|
37
58
|
})
|
|
38
59
|
|
|
39
60
|
.get('/hmr-client.js', () => {
|
|
@@ -111,9 +132,7 @@ ws.onclose = () => {
|
|
|
111
132
|
}
|
|
112
133
|
|
|
113
134
|
const ext = extname(filepath);
|
|
114
|
-
const contentType = ext
|
|
115
|
-
ext === '.css' ? 'text/css' :
|
|
116
|
-
'text/plain';
|
|
135
|
+
const contentType = getContentType(ext);
|
|
117
136
|
|
|
118
137
|
return new Response(await file.text(), {
|
|
119
138
|
headers: {
|
|
@@ -148,12 +167,62 @@ ws.onclose = () => {
|
|
|
148
167
|
logger.info(`📁 Serving: ${root}`);
|
|
149
168
|
|
|
150
169
|
// Watch for file changes
|
|
151
|
-
setupWatcher(root, compiledDir, clients)
|
|
170
|
+
setupWatcher(root, compiledDir, clients, () => {
|
|
171
|
+
// Check router status on recompile
|
|
172
|
+
hasRouter = existsSync(join(compiledDir, 'router.js'));
|
|
173
|
+
});
|
|
152
174
|
|
|
153
175
|
return app;
|
|
154
176
|
}
|
|
155
177
|
|
|
156
|
-
function
|
|
178
|
+
function serveHTML(root, hasRouter) {
|
|
179
|
+
const html = `
|
|
180
|
+
<!DOCTYPE html>
|
|
181
|
+
<html lang="en">
|
|
182
|
+
<head>
|
|
183
|
+
<meta charset="UTF-8">
|
|
184
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
185
|
+
<title>BertUI App - Dev</title>
|
|
186
|
+
<link rel="stylesheet" href="/styles/bertui.css">
|
|
187
|
+
</head>
|
|
188
|
+
<body>
|
|
189
|
+
<div id="root"></div>
|
|
190
|
+
<script type="module" src="/hmr-client.js"></script>
|
|
191
|
+
${hasRouter
|
|
192
|
+
? '<script type="module" src="/compiled/router.js"></script>'
|
|
193
|
+
: ''
|
|
194
|
+
}
|
|
195
|
+
<script type="module" src="/compiled/main.js"></script>
|
|
196
|
+
</body>
|
|
197
|
+
</html>`;
|
|
198
|
+
|
|
199
|
+
return new Response(html, {
|
|
200
|
+
headers: { 'Content-Type': 'text/html' }
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getContentType(ext) {
|
|
205
|
+
const types = {
|
|
206
|
+
'.js': 'application/javascript',
|
|
207
|
+
'.css': 'text/css',
|
|
208
|
+
'.html': 'text/html',
|
|
209
|
+
'.json': 'application/json',
|
|
210
|
+
'.png': 'image/png',
|
|
211
|
+
'.jpg': 'image/jpeg',
|
|
212
|
+
'.jpeg': 'image/jpeg',
|
|
213
|
+
'.gif': 'image/gif',
|
|
214
|
+
'.svg': 'image/svg+xml',
|
|
215
|
+
'.ico': 'image/x-icon',
|
|
216
|
+
'.woff': 'font/woff',
|
|
217
|
+
'.woff2': 'font/woff2',
|
|
218
|
+
'.ttf': 'font/ttf',
|
|
219
|
+
'.eot': 'application/vnd.ms-fontobject'
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return types[ext] || 'text/plain';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function setupWatcher(root, compiledDir, clients, onRecompile) {
|
|
157
226
|
const srcDir = join(root, 'src');
|
|
158
227
|
|
|
159
228
|
if (!existsSync(srcDir)) {
|
|
@@ -183,6 +252,11 @@ function setupWatcher(root, compiledDir, clients) {
|
|
|
183
252
|
try {
|
|
184
253
|
await compileProject(root);
|
|
185
254
|
|
|
255
|
+
// Call callback to update router status
|
|
256
|
+
if (onRecompile) {
|
|
257
|
+
onRecompile();
|
|
258
|
+
}
|
|
259
|
+
|
|
186
260
|
// Notify clients to reload
|
|
187
261
|
for (const client of clients) {
|
|
188
262
|
try {
|