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 CHANGED
@@ -32,5 +32,5 @@ export default {
32
32
  buildCSS,
33
33
  copyCSS,
34
34
  program,
35
- version: "0.2.0"
35
+ version: "0.2.1"
36
36
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "0.2.0",
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
- ".": "./index.js",
12
- "./styles": "./src/styles/bertui.css",
13
- "./logger": "./src/logger/logger.js",
14
- "./router": "./src/router/Router.jsx"
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",
@@ -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 routerCode = `// Auto-generated router - DO NOT EDIT
117
- ${imports}
114
+ const routerComponentCode = `
115
+ import { useState, useEffect, createContext, useContext } from 'react';
118
116
 
119
- export const routes = [
120
- ${routeConfigs}
121
- ];
117
+ const RouterContext = createContext(null);
122
118
 
123
- export function matchRoute(pathname) {
124
- for (const route of routes) {
125
- if (route.type === 'static' && route.path === pathname) {
126
- return route;
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
- for (const route of routes) {
131
- if (route.type === 'dynamic') {
132
- const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
133
- const regex = new RegExp('^' + pattern + '$');
134
- const match = pathname.match(regex);
135
-
136
- if (match) {
137
- const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
138
- const params = {};
139
- paramNames.forEach((name, i) => {
140
- params[name] = match[i + 1];
141
- });
142
-
143
- return { ...route, params };
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
- return null;
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, routerCode);
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' || ext === '.css') {
263
+ } else if (ext === '.js') {
179
264
  const outPath = join(outDir, file);
180
- await Bun.write(outPath, Bun.file(srcPath));
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
- // Remove bertui/styles imports
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 fixRelativeImports(code) {
221
- // Match import statements with relative paths that don't already have extensions
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
  }
@@ -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
  }
@@ -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('Router-based routing enabled');
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/router.js"></script>'
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