bertui 0.2.0 → 0.2.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/index.js +1 -1
- package/package.json +7 -6
- package/src/client/compiler.js +138 -40
- package/src/router/Router.js +4 -1
- package/src/server/dev-server.js +3 -7
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bertui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Lightning-fast React dev server powered by Bun and Elysia",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"bertui": "./bin/bertui.js"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
".": "./index.js",
|
|
12
|
+
"./styles": "./src/styles/bertui.css",
|
|
13
|
+
"./logger": "./src/logger/logger.js",
|
|
14
|
+
"./router": "./src/router/Router.jsx"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bin",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"build-tool",
|
|
35
35
|
"bundler",
|
|
36
36
|
"fast",
|
|
37
|
-
"hmr"
|
|
37
|
+
"hmr",
|
|
38
|
+
"file-based-routing"
|
|
38
39
|
],
|
|
39
40
|
"author": "Pease Ernest",
|
|
40
41
|
"license": "MIT",
|
package/src/client/compiler.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// src/client/compiler.js
|
|
2
1
|
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
3
2
|
import { join, extname, relative } from 'path';
|
|
4
3
|
import logger from '../logger/logger.js';
|
|
@@ -40,7 +39,6 @@ export async function compileProject(root) {
|
|
|
40
39
|
const stats = await compileDirectory(srcDir, outDir, root);
|
|
41
40
|
const duration = Date.now() - startTime;
|
|
42
41
|
|
|
43
|
-
// Generate router AFTER compilation
|
|
44
42
|
if (routes.length > 0) {
|
|
45
43
|
await generateRouter(routes, outDir, root);
|
|
46
44
|
logger.info('Generated router.js');
|
|
@@ -113,44 +111,131 @@ async function generateRouter(routes, outDir, root) {
|
|
|
113
111
|
return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
|
|
114
112
|
}).join(',\n');
|
|
115
113
|
|
|
116
|
-
const
|
|
117
|
-
|
|
114
|
+
const routerComponentCode = `
|
|
115
|
+
import { useState, useEffect, createContext, useContext } from 'react';
|
|
118
116
|
|
|
119
|
-
|
|
120
|
-
${routeConfigs}
|
|
121
|
-
];
|
|
117
|
+
const RouterContext = createContext(null);
|
|
122
118
|
|
|
123
|
-
export function
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
119
|
+
export function useRouter() {
|
|
120
|
+
const context = useContext(RouterContext);
|
|
121
|
+
if (!context) {
|
|
122
|
+
throw new Error('useRouter must be used within a Router component');
|
|
128
123
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
124
|
+
return context;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function Router({ routes }) {
|
|
128
|
+
const [currentRoute, setCurrentRoute] = useState(null);
|
|
129
|
+
const [params, setParams] = useState({});
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
matchAndSetRoute(window.location.pathname);
|
|
133
|
+
|
|
134
|
+
const handlePopState = () => {
|
|
135
|
+
matchAndSetRoute(window.location.pathname);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
window.addEventListener('popstate', handlePopState);
|
|
139
|
+
return () => window.removeEventListener('popstate', handlePopState);
|
|
140
|
+
}, [routes]);
|
|
141
|
+
|
|
142
|
+
function matchAndSetRoute(pathname) {
|
|
143
|
+
for (const route of routes) {
|
|
144
|
+
if (route.type === 'static' && route.path === pathname) {
|
|
145
|
+
setCurrentRoute(route);
|
|
146
|
+
setParams({});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const route of routes) {
|
|
152
|
+
if (route.type === 'dynamic') {
|
|
153
|
+
const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
|
|
154
|
+
const regex = new RegExp('^' + pattern + '$');
|
|
155
|
+
const match = pathname.match(regex);
|
|
156
|
+
|
|
157
|
+
if (match) {
|
|
158
|
+
const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
|
|
159
|
+
const extractedParams = {};
|
|
160
|
+
paramNames.forEach((name, i) => {
|
|
161
|
+
extractedParams[name] = match[i + 1];
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
setCurrentRoute(route);
|
|
165
|
+
setParams(extractedParams);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
144
168
|
}
|
|
145
169
|
}
|
|
170
|
+
|
|
171
|
+
setCurrentRoute(null);
|
|
172
|
+
setParams({});
|
|
146
173
|
}
|
|
147
|
-
|
|
148
|
-
|
|
174
|
+
|
|
175
|
+
function navigate(path) {
|
|
176
|
+
window.history.pushState({}, '', path);
|
|
177
|
+
matchAndSetRoute(path);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const routerValue = {
|
|
181
|
+
currentRoute,
|
|
182
|
+
params,
|
|
183
|
+
navigate,
|
|
184
|
+
pathname: window.location.pathname
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const Component = currentRoute?.component;
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<RouterContext.Provider value={routerValue}>
|
|
191
|
+
{Component ? <Component params={params} /> : <NotFound />}
|
|
192
|
+
</RouterContext.Provider>
|
|
193
|
+
);
|
|
149
194
|
}
|
|
195
|
+
|
|
196
|
+
export function Link({ to, children, ...props }) {
|
|
197
|
+
const { navigate } = useRouter();
|
|
198
|
+
|
|
199
|
+
function handleClick(e) {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
navigate(to);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<a href={to} onClick={handleClick} {...props}>
|
|
206
|
+
{children}
|
|
207
|
+
</a>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function NotFound() {
|
|
212
|
+
return (
|
|
213
|
+
<div style={{
|
|
214
|
+
display: 'flex',
|
|
215
|
+
flexDirection: 'column',
|
|
216
|
+
alignItems: 'center',
|
|
217
|
+
justifyContent: 'center',
|
|
218
|
+
minHeight: '100vh',
|
|
219
|
+
fontFamily: 'system-ui'
|
|
220
|
+
}}>
|
|
221
|
+
<h1 style={{ fontSize: '6rem', margin: 0 }}>404</h1>
|
|
222
|
+
<p style={{ fontSize: '1.5rem', color: '#666' }}>Page not found</p>
|
|
223
|
+
<a href="/" style={{ color: '#10b981', textDecoration: 'none', fontSize: '1.2rem' }}>
|
|
224
|
+
Go home
|
|
225
|
+
</a>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
${imports}
|
|
231
|
+
|
|
232
|
+
export const routes = [
|
|
233
|
+
${routeConfigs}
|
|
234
|
+
];
|
|
150
235
|
`;
|
|
151
236
|
|
|
152
237
|
const routerPath = join(outDir, 'router.js');
|
|
153
|
-
await Bun.write(routerPath,
|
|
238
|
+
await Bun.write(routerPath, routerComponentCode);
|
|
154
239
|
}
|
|
155
240
|
|
|
156
241
|
async function compileDirectory(srcDir, outDir, root) {
|
|
@@ -175,9 +260,13 @@ async function compileDirectory(srcDir, outDir, root) {
|
|
|
175
260
|
if (['.jsx', '.tsx', '.ts'].includes(ext)) {
|
|
176
261
|
await compileFile(srcPath, outDir, file, relativePath);
|
|
177
262
|
stats.files++;
|
|
178
|
-
} else if (ext === '.js'
|
|
263
|
+
} else if (ext === '.js') {
|
|
179
264
|
const outPath = join(outDir, file);
|
|
180
|
-
await Bun.
|
|
265
|
+
let code = await Bun.file(srcPath).text();
|
|
266
|
+
|
|
267
|
+
code = fixImports(code);
|
|
268
|
+
|
|
269
|
+
await Bun.write(outPath, code);
|
|
181
270
|
logger.debug(`Copied: ${relativePath}`);
|
|
182
271
|
stats.files++;
|
|
183
272
|
} else {
|
|
@@ -197,13 +286,11 @@ async function compileFile(srcPath, outDir, filename, relativePath) {
|
|
|
197
286
|
try {
|
|
198
287
|
let code = await Bun.file(srcPath).text();
|
|
199
288
|
|
|
200
|
-
|
|
201
|
-
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
289
|
+
code = fixImports(code);
|
|
202
290
|
|
|
203
291
|
const transpiler = new Bun.Transpiler({ loader });
|
|
204
292
|
let compiled = await transpiler.transform(code);
|
|
205
293
|
|
|
206
|
-
// CRITICAL FIX: Add .js extensions to all relative imports
|
|
207
294
|
compiled = fixRelativeImports(compiled);
|
|
208
295
|
|
|
209
296
|
const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
|
|
@@ -217,15 +304,26 @@ async function compileFile(srcPath, outDir, filename, relativePath) {
|
|
|
217
304
|
}
|
|
218
305
|
}
|
|
219
306
|
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
// Matches: import X from './path' or import X from '../path'
|
|
223
|
-
// But NOT: import X from './path.js' or import X from 'package'
|
|
307
|
+
function fixImports(code) {
|
|
308
|
+
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
224
309
|
|
|
310
|
+
code = code.replace(
|
|
311
|
+
/from\s+['"]bertui\/router['"]/g,
|
|
312
|
+
"from '/compiled/router.js'"
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
code = code.replace(
|
|
316
|
+
/from\s+['"]\.\.\/\.bertui\/compiled\/([^'"]+)['"]/g,
|
|
317
|
+
"from '/compiled/$1'"
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
return code;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function fixRelativeImports(code) {
|
|
225
324
|
const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
|
|
226
325
|
|
|
227
326
|
code = code.replace(importRegex, (match, prefix, path) => {
|
|
228
|
-
// Don't add .js if path already has an extension or ends with /
|
|
229
327
|
if (path.endsWith('/') || /\.\w+$/.test(path)) {
|
|
230
328
|
return match;
|
|
231
329
|
}
|
package/src/router/Router.js
CHANGED
|
@@ -23,9 +23,10 @@ export function Router({ routes }) {
|
|
|
23
23
|
|
|
24
24
|
window.addEventListener('popstate', handlePopState);
|
|
25
25
|
return () => window.removeEventListener('popstate', handlePopState);
|
|
26
|
-
}, []);
|
|
26
|
+
}, [routes]);
|
|
27
27
|
|
|
28
28
|
function matchAndSetRoute(pathname) {
|
|
29
|
+
// Try static routes first
|
|
29
30
|
for (const route of routes) {
|
|
30
31
|
if (route.type === 'static' && route.path === pathname) {
|
|
31
32
|
setCurrentRoute(route);
|
|
@@ -34,6 +35,7 @@ export function Router({ routes }) {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
// Try dynamic routes
|
|
37
39
|
for (const route of routes) {
|
|
38
40
|
if (route.type === 'dynamic') {
|
|
39
41
|
const pattern = route.path.replace(/\[([^\]]+)\]/g, '([^/]+)');
|
|
@@ -54,6 +56,7 @@ export function Router({ routes }) {
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
// No match found
|
|
57
60
|
setCurrentRoute(null);
|
|
58
61
|
setParams({});
|
|
59
62
|
}
|
package/src/server/dev-server.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// src/server/dev-server.js
|
|
2
1
|
import { Elysia } from 'elysia';
|
|
3
2
|
import { watch } from 'fs';
|
|
4
3
|
import { join, extname } from 'path';
|
|
@@ -17,7 +16,7 @@ export async function startDevServer(options = {}) {
|
|
|
17
16
|
const routerPath = join(compiledDir, 'router.js');
|
|
18
17
|
if (existsSync(routerPath)) {
|
|
19
18
|
hasRouter = true;
|
|
20
|
-
logger.info('
|
|
19
|
+
logger.info('File-based routing enabled');
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
const app = new Elysia()
|
|
@@ -158,7 +157,6 @@ function serveHTML(root, hasRouter) {
|
|
|
158
157
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
159
158
|
<title>BertUI App - Dev</title>
|
|
160
159
|
|
|
161
|
-
<!-- Import Map for React and dependencies -->
|
|
162
160
|
<script type="importmap">
|
|
163
161
|
{
|
|
164
162
|
"imports": {
|
|
@@ -171,7 +169,6 @@ function serveHTML(root, hasRouter) {
|
|
|
171
169
|
</script>
|
|
172
170
|
|
|
173
171
|
<style>
|
|
174
|
-
/* Inline basic styles since we're skipping CSS for now */
|
|
175
172
|
* {
|
|
176
173
|
margin: 0;
|
|
177
174
|
padding: 0;
|
|
@@ -186,10 +183,9 @@ function serveHTML(root, hasRouter) {
|
|
|
186
183
|
<div id="root"></div>
|
|
187
184
|
<script type="module" src="/hmr-client.js"></script>
|
|
188
185
|
${hasRouter
|
|
189
|
-
? '<script type="module" src="/compiled/
|
|
190
|
-
: ''
|
|
186
|
+
? '<script type="module" src="/compiled/main.js"></script>'
|
|
187
|
+
: '<script type="module" src="/compiled/main.js"></script>'
|
|
191
188
|
}
|
|
192
|
-
<script type="module" src="/compiled/main.js"></script>
|
|
193
189
|
</body>
|
|
194
190
|
</html>`;
|
|
195
191
|
|