bertui 0.1.9 → 0.2.1

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.1.9"
35
+ version: "0.2.1"
36
36
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "Lightning-fast React dev server powered by Bun and Elysia",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -40,7 +40,6 @@ export async function compileProject(root) {
40
40
  const stats = await compileDirectory(srcDir, outDir, root);
41
41
  const duration = Date.now() - startTime;
42
42
 
43
- // Generate router AFTER compilation
44
43
  if (routes.length > 0) {
45
44
  await generateRouter(routes, outDir, root);
46
45
  logger.info('Generated router.js');
@@ -113,44 +112,132 @@ async function generateRouter(routes, outDir, root) {
113
112
  return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
114
113
  }).join(',\n');
115
114
 
116
- const routerCode = `// Auto-generated router - DO NOT EDIT
117
- ${imports}
115
+ // CRITICAL: Copy Router component into compiled folder
116
+ const routerComponentCode = `
117
+ import { useState, useEffect, createContext, useContext } from 'react';
118
118
 
119
- export const routes = [
120
- ${routeConfigs}
121
- ];
119
+ const RouterContext = createContext(null);
122
120
 
123
- export function matchRoute(pathname) {
124
- for (const route of routes) {
125
- if (route.type === 'static' && route.path === pathname) {
126
- return route;
127
- }
121
+ export function useRouter() {
122
+ const context = useContext(RouterContext);
123
+ if (!context) {
124
+ throw new Error('useRouter must be used within a Router component');
128
125
  }
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 };
126
+ return context;
127
+ }
128
+
129
+ export function Router({ routes }) {
130
+ const [currentRoute, setCurrentRoute] = useState(null);
131
+ const [params, setParams] = useState({});
132
+
133
+ useEffect(() => {
134
+ matchAndSetRoute(window.location.pathname);
135
+
136
+ const handlePopState = () => {
137
+ matchAndSetRoute(window.location.pathname);
138
+ };
139
+
140
+ window.addEventListener('popstate', handlePopState);
141
+ return () => window.removeEventListener('popstate', handlePopState);
142
+ }, []);
143
+
144
+ function matchAndSetRoute(pathname) {
145
+ for (const route of routes) {
146
+ if (route.type === 'static' && route.path === pathname) {
147
+ setCurrentRoute(route);
148
+ setParams({});
149
+ return;
150
+ }
151
+ }
152
+
153
+ for (const route of routes) {
154
+ if (route.type === 'dynamic') {
155
+ const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
156
+ const regex = new RegExp('^' + pattern + '$');
157
+ const match = pathname.match(regex);
158
+
159
+ if (match) {
160
+ const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
161
+ const extractedParams = {};
162
+ paramNames.forEach((name, i) => {
163
+ extractedParams[name] = match[i + 1];
164
+ });
165
+
166
+ setCurrentRoute(route);
167
+ setParams(extractedParams);
168
+ return;
169
+ }
144
170
  }
145
171
  }
172
+
173
+ setCurrentRoute(null);
174
+ setParams({});
146
175
  }
147
-
148
- return null;
176
+
177
+ function navigate(path) {
178
+ window.history.pushState({}, '', path);
179
+ matchAndSetRoute(path);
180
+ }
181
+
182
+ const routerValue = {
183
+ currentRoute,
184
+ params,
185
+ navigate,
186
+ pathname: window.location.pathname
187
+ };
188
+
189
+ const Component = currentRoute?.component;
190
+
191
+ return (
192
+ <RouterContext.Provider value={routerValue}>
193
+ {Component ? <Component params={params} /> : <NotFound />}
194
+ </RouterContext.Provider>
195
+ );
149
196
  }
197
+
198
+ export function Link({ to, children, ...props }) {
199
+ const { navigate } = useRouter();
200
+
201
+ function handleClick(e) {
202
+ e.preventDefault();
203
+ navigate(to);
204
+ }
205
+
206
+ return (
207
+ <a href={to} onClick={handleClick} {...props}>
208
+ {children}
209
+ </a>
210
+ );
211
+ }
212
+
213
+ function NotFound() {
214
+ return (
215
+ <div style={{
216
+ display: 'flex',
217
+ flexDirection: 'column',
218
+ alignItems: 'center',
219
+ justifyContent: 'center',
220
+ minHeight: '100vh',
221
+ fontFamily: 'system-ui'
222
+ }}>
223
+ <h1 style={{ fontSize: '6rem', margin: 0 }}>404</h1>
224
+ <p style={{ fontSize: '1.5rem', color: '#666' }}>Page not found</p>
225
+ <a href="/" style={{ color: '#10b981', textDecoration: 'none', fontSize: '1.2rem' }}>
226
+ Go home
227
+ </a>
228
+ </div>
229
+ );
230
+ }
231
+
232
+ ${imports}
233
+
234
+ export const routes = [
235
+ ${routeConfigs}
236
+ ];
150
237
  `;
151
238
 
152
239
  const routerPath = join(outDir, 'router.js');
153
- await Bun.write(routerPath, routerCode);
240
+ await Bun.write(routerPath, routerComponentCode);
154
241
  }
155
242
 
156
243
  async function compileDirectory(srcDir, outDir, root) {
@@ -175,9 +262,14 @@ async function compileDirectory(srcDir, outDir, root) {
175
262
  if (['.jsx', '.tsx', '.ts'].includes(ext)) {
176
263
  await compileFile(srcPath, outDir, file, relativePath);
177
264
  stats.files++;
178
- } else if (ext === '.js' || ext === '.css') {
265
+ } else if (ext === '.js') {
179
266
  const outPath = join(outDir, file);
180
- await Bun.write(outPath, Bun.file(srcPath));
267
+ let code = await Bun.file(srcPath).text();
268
+
269
+ // Fix imports in .js files too
270
+ code = fixImports(code);
271
+
272
+ await Bun.write(outPath, code);
181
273
  logger.debug(`Copied: ${relativePath}`);
182
274
  stats.files++;
183
275
  } else {
@@ -197,13 +289,13 @@ async function compileFile(srcPath, outDir, filename, relativePath) {
197
289
  try {
198
290
  let code = await Bun.file(srcPath).text();
199
291
 
200
- // Remove bertui/styles imports
201
- code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
292
+ // Fix imports BEFORE transpiling
293
+ code = fixImports(code);
202
294
 
203
295
  const transpiler = new Bun.Transpiler({ loader });
204
296
  let compiled = await transpiler.transform(code);
205
297
 
206
- // CRITICAL FIX: Add .js extensions to all relative imports
298
+ // Add .js extensions to relative imports
207
299
  compiled = fixRelativeImports(compiled);
208
300
 
209
301
  const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
@@ -217,15 +309,29 @@ async function compileFile(srcPath, outDir, filename, relativePath) {
217
309
  }
218
310
  }
219
311
 
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'
312
+ function fixImports(code) {
313
+ // Remove bertui/styles imports
314
+ code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
224
315
 
316
+ // Replace bertui/router with /compiled/router.js
317
+ code = code.replace(
318
+ /from\s+['"]bertui\/router['"]/g,
319
+ "from '/compiled/router.js'"
320
+ );
321
+
322
+ // Fix ../.bertui/compiled paths to /compiled
323
+ code = code.replace(
324
+ /from\s+['"]\.\.\/\.bertui\/compiled\/([^'"]+)['"]/g,
325
+ "from '/compiled/$1'"
326
+ );
327
+
328
+ return code;
329
+ }
330
+
331
+ function fixRelativeImports(code) {
225
332
  const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
226
333
 
227
334
  code = code.replace(importRegex, (match, prefix, path) => {
228
- // Don't add .js if path already has an extension or ends with /
229
335
  if (path.endsWith('/') || /\.\w+$/.test(path)) {
230
336
  return match;
231
337
  }
@@ -1,7 +1,5 @@
1
- // src/router/Router.jsx
2
1
  import { useState, useEffect, createContext, useContext } from 'react';
3
2
 
4
- // Router context
5
3
  const RouterContext = createContext(null);
6
4
 
7
5
  export function useRouter() {
@@ -12,20 +10,13 @@ export function useRouter() {
12
10
  return context;
13
11
  }
14
12
 
15
- export function useParams() {
16
- const { params } = useRouter();
17
- return params;
18
- }
19
-
20
- export function Router({ routes, children }) {
13
+ export function Router({ routes }) {
21
14
  const [currentRoute, setCurrentRoute] = useState(null);
22
15
  const [params, setParams] = useState({});
23
16
 
24
17
  useEffect(() => {
25
- // Match initial route
26
18
  matchAndSetRoute(window.location.pathname);
27
19
 
28
- // Handle browser navigation
29
20
  const handlePopState = () => {
30
21
  matchAndSetRoute(window.location.pathname);
31
22
  };
@@ -35,7 +26,6 @@ export function Router({ routes, children }) {
35
26
  }, []);
36
27
 
37
28
  function matchAndSetRoute(pathname) {
38
- // Try exact match first (static routes)
39
29
  for (const route of routes) {
40
30
  if (route.type === 'static' && route.path === pathname) {
41
31
  setCurrentRoute(route);
@@ -44,7 +34,6 @@ export function Router({ routes, children }) {
44
34
  }
45
35
  }
46
36
 
47
- // Try dynamic routes
48
37
  for (const route of routes) {
49
38
  if (route.type === 'dynamic') {
50
39
  const pattern = route.path.replace(/\[([^\]]+)\]/g, '([^/]+)');
@@ -52,7 +41,6 @@ export function Router({ routes, children }) {
52
41
  const match = pathname.match(regex);
53
42
 
54
43
  if (match) {
55
- // Extract params
56
44
  const paramNames = [...route.path.matchAll(/\[([^\]]+)\]/g)].map(m => m[1]);
57
45
  const extractedParams = {};
58
46
  paramNames.forEach((name, i) => {
@@ -66,7 +54,6 @@ export function Router({ routes, children }) {
66
54
  }
67
55
  }
68
56
 
69
- // No match found - 404
70
57
  setCurrentRoute(null);
71
58
  setParams({});
72
59
  }
@@ -83,18 +70,16 @@ export function Router({ routes, children }) {
83
70
  pathname: window.location.pathname
84
71
  };
85
72
 
73
+ const Component = currentRoute?.component;
74
+
86
75
  return (
87
76
  <RouterContext.Provider value={routerValue}>
88
- {currentRoute ? (
89
- <currentRoute.component />
90
- ) : (
91
- children || <NotFound />
92
- )}
77
+ {Component ? <Component params={params} /> : <NotFound />}
93
78
  </RouterContext.Provider>
94
79
  );
95
80
  }
96
81
 
97
- export function Link({ to, children, className, ...props }) {
82
+ export function Link({ to, children, ...props }) {
98
83
  const { navigate } = useRouter();
99
84
 
100
85
  function handleClick(e) {
@@ -103,7 +88,7 @@ export function Link({ to, children, className, ...props }) {
103
88
  }
104
89
 
105
90
  return (
106
- <a href={to} onClick={handleClick} className={className} {...props}>
91
+ <a href={to} onClick={handleClick} {...props}>
107
92
  {children}
108
93
  </a>
109
94
  );
@@ -117,7 +102,7 @@ function NotFound() {
117
102
  alignItems: 'center',
118
103
  justifyContent: 'center',
119
104
  minHeight: '100vh',
120
- fontFamily: 'system-ui, sans-serif'
105
+ fontFamily: 'system-ui'
121
106
  }}>
122
107
  <h1 style={{ fontSize: '6rem', margin: 0 }}>404</h1>
123
108
  <p style={{ fontSize: '1.5rem', color: '#666' }}>Page not found</p>