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 +1 -1
- package/package.json +1 -1
- package/src/client/compiler.js +145 -39
- package/src/router/Router.js +7 -22
package/index.js
CHANGED
package/package.json
CHANGED
package/src/client/compiler.js
CHANGED
|
@@ -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
|
-
|
|
117
|
-
|
|
115
|
+
// CRITICAL: Copy Router component into compiled folder
|
|
116
|
+
const routerComponentCode = `
|
|
117
|
+
import { useState, useEffect, createContext, useContext } from 'react';
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
${routeConfigs}
|
|
121
|
-
];
|
|
119
|
+
const RouterContext = createContext(null);
|
|
122
120
|
|
|
123
|
-
export function
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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,
|
|
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'
|
|
265
|
+
} else if (ext === '.js') {
|
|
179
266
|
const outPath = join(outDir, file);
|
|
180
|
-
await Bun.
|
|
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
|
-
//
|
|
201
|
-
code = code
|
|
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
|
-
//
|
|
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
|
|
221
|
-
//
|
|
222
|
-
|
|
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
|
}
|
package/src/router/Router.js
CHANGED
|
@@ -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
|
|
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
|
-
{
|
|
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,
|
|
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}
|
|
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
|
|
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>
|