bertui 0.1.0 ā 0.1.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/README.md +192 -0
- package/index.js +32 -0
- package/package.json +15 -6
- package/src/client/compiler.js +72 -74
- package/src/router/router.js +197 -0
- package/src/server/dev-server.js +109 -10
package/README.md
CHANGED
|
@@ -47,6 +47,198 @@ bertui build # Build for production
|
|
|
47
47
|
- `.pulse` - Pulse animation
|
|
48
48
|
- `.shake` - Shake animation
|
|
49
49
|
|
|
50
|
+
# š BertUI Now Has File-Based Routing!
|
|
51
|
+
|
|
52
|
+
## What We Built
|
|
53
|
+
|
|
54
|
+
I've added a **complete file-based routing system** to BertUI. Here's what's included:
|
|
55
|
+
|
|
56
|
+
### š New Files to Add
|
|
57
|
+
|
|
58
|
+
1. **`src/router/router.js`** - Core routing logic
|
|
59
|
+
- Scans `src/pages/` directory
|
|
60
|
+
- Generates route definitions
|
|
61
|
+
- Creates React Router component
|
|
62
|
+
- Provides `Link` and `navigate` utilities
|
|
63
|
+
|
|
64
|
+
2. **`src/client/compiler.js`** (updated) - Enhanced compiler
|
|
65
|
+
- Detects `pages/` directory
|
|
66
|
+
- Auto-generates router code
|
|
67
|
+
- Supports both routing and non-routing modes
|
|
68
|
+
|
|
69
|
+
3. **`src/server/dev-server.js`** (updated) - Enhanced dev server
|
|
70
|
+
- Serves SPA-style HTML for all routes
|
|
71
|
+
- Notifies clients of route changes
|
|
72
|
+
- Better HMR integration
|
|
73
|
+
|
|
74
|
+
## š Features
|
|
75
|
+
|
|
76
|
+
### ā
File-Based Routing
|
|
77
|
+
```
|
|
78
|
+
src/pages/index.jsx ā /
|
|
79
|
+
src/pages/about.jsx ā /about
|
|
80
|
+
src/pages/blog/index.jsx ā /blog
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### ā
Dynamic Routes
|
|
84
|
+
```
|
|
85
|
+
src/pages/user/[id].jsx ā /user/:id
|
|
86
|
+
src/pages/blog/[slug].jsx ā /blog/:slug
|
|
87
|
+
src/pages/shop/[cat]/[prod].jsx ā /shop/:cat/:prod
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### ā
Navigation Components
|
|
91
|
+
```jsx
|
|
92
|
+
import { Link, navigate } from '../.bertui/router';
|
|
93
|
+
|
|
94
|
+
// Link component
|
|
95
|
+
<Link href="/about">About</Link>
|
|
96
|
+
|
|
97
|
+
// Programmatic navigation
|
|
98
|
+
navigate('/dashboard');
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### ā
Route Parameters
|
|
102
|
+
```jsx
|
|
103
|
+
export default function UserProfile({ params }) {
|
|
104
|
+
return <div>User ID: {params.id}</div>;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### ā
Backward Compatible
|
|
109
|
+
- Still works with `src/main.jsx` if no `pages/` directory
|
|
110
|
+
- Automatically detects routing mode
|
|
111
|
+
- No breaking changes!
|
|
112
|
+
|
|
113
|
+
## š How It Works
|
|
114
|
+
|
|
115
|
+
1. **Developer creates pages:**
|
|
116
|
+
```bash
|
|
117
|
+
src/pages/
|
|
118
|
+
āāā index.jsx
|
|
119
|
+
āāā about.jsx
|
|
120
|
+
āāā user/[id].jsx
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
2. **BertUI scans and generates routes:**
|
|
124
|
+
```javascript
|
|
125
|
+
[
|
|
126
|
+
{ path: '/', file: 'index.jsx' },
|
|
127
|
+
{ path: '/about', file: 'about.jsx' },
|
|
128
|
+
{ path: '/user/:id', file: 'user/[id].jsx', isDynamic: true }
|
|
129
|
+
]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
3. **Router code is auto-generated:**
|
|
133
|
+
- Creates `.bertui/router.js`
|
|
134
|
+
- Imports all page components
|
|
135
|
+
- Provides routing logic
|
|
136
|
+
|
|
137
|
+
4. **Dev server serves SPA:**
|
|
138
|
+
- All routes serve the same HTML
|
|
139
|
+
- Client-side routing handles navigation
|
|
140
|
+
- HMR updates routes on file changes
|
|
141
|
+
|
|
142
|
+
## š§ Integration Steps
|
|
143
|
+
|
|
144
|
+
### 1. Add Router Files
|
|
145
|
+
Copy these files to your BertUI project:
|
|
146
|
+
- `src/router/router.js` (new)
|
|
147
|
+
- `src/client/compiler.js` (replace)
|
|
148
|
+
- `src/server/dev-server.js` (replace)
|
|
149
|
+
|
|
150
|
+
### 2. Update Dependencies
|
|
151
|
+
No new dependencies needed! Uses existing React ecosystem.
|
|
152
|
+
|
|
153
|
+
### 3. Test It
|
|
154
|
+
```bash
|
|
155
|
+
# Create example pages
|
|
156
|
+
mkdir -p src/pages
|
|
157
|
+
echo 'export default () => <h1>Home</h1>' > src/pages/index.jsx
|
|
158
|
+
echo 'export default () => <h1>About</h1>' > src/pages/about.jsx
|
|
159
|
+
|
|
160
|
+
# Start dev server
|
|
161
|
+
bertui dev
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 4. Watch the Magic
|
|
165
|
+
- Navigate to `http://localhost:3000` ā Home page
|
|
166
|
+
- Click links ā No page reload!
|
|
167
|
+
- Edit files ā Instant HMR!
|
|
168
|
+
- Check console ā Route discovery logs
|
|
169
|
+
|
|
170
|
+
## šØ Works with BertUI Animations
|
|
171
|
+
|
|
172
|
+
```jsx
|
|
173
|
+
export default function Home() {
|
|
174
|
+
return (
|
|
175
|
+
<div>
|
|
176
|
+
<h1 className="split fadein" data-text="Welcome!">
|
|
177
|
+
Welcome!
|
|
178
|
+
</h1>
|
|
179
|
+
<p className="moveright">Lightning fast! ā”</p>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## š Performance
|
|
186
|
+
|
|
187
|
+
- **Fast compilation:** Bun's speed + code splitting
|
|
188
|
+
- **Small bundles:** Each route is a separate chunk
|
|
189
|
+
- **Quick HMR:** Only recompiles changed files
|
|
190
|
+
- **Smart routing:** Static routes matched first
|
|
191
|
+
|
|
192
|
+
## š Error Handling
|
|
193
|
+
|
|
194
|
+
- Missing routes ā Auto 404 page
|
|
195
|
+
- Invalid pages ā Compilation error with details
|
|
196
|
+
- Runtime errors ā Preserved in dev mode
|
|
197
|
+
|
|
198
|
+
## šÆ Next Steps
|
|
199
|
+
|
|
200
|
+
### Recommended Enhancements:
|
|
201
|
+
1. **Layouts** - Wrap pages with shared layouts
|
|
202
|
+
2. **Middleware** - Auth, logging, etc.
|
|
203
|
+
3. **Data Loading** - Fetch data before rendering
|
|
204
|
+
4. **API Routes** - Backend API in `pages/api/`
|
|
205
|
+
5. **Static Generation** - Pre-render at build time
|
|
206
|
+
|
|
207
|
+
### Production Build
|
|
208
|
+
Update `build.js` to:
|
|
209
|
+
- Generate static HTML for each route
|
|
210
|
+
- Create optimized bundles per route
|
|
211
|
+
- Handle dynamic routes appropriately
|
|
212
|
+
|
|
213
|
+
## š Usage Example
|
|
214
|
+
|
|
215
|
+
```jsx
|
|
216
|
+
// src/pages/index.jsx
|
|
217
|
+
import { Link } from '../.bertui/router';
|
|
218
|
+
|
|
219
|
+
export default function Home() {
|
|
220
|
+
return (
|
|
221
|
+
<div className="fadein">
|
|
222
|
+
<h1>Welcome to My App!</h1>
|
|
223
|
+
<nav>
|
|
224
|
+
<Link href="/about">About</Link>
|
|
225
|
+
<Link href="/blog">Blog</Link>
|
|
226
|
+
<Link href="/user/123">My Profile</Link>
|
|
227
|
+
</nav>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/pages/user/[id].jsx
|
|
233
|
+
export default function UserProfile({ params }) {
|
|
234
|
+
return (
|
|
235
|
+
<div className="scalein">
|
|
236
|
+
<h1>User {params.id}</h1>
|
|
237
|
+
<p>Profile page for user {params.id}</p>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
50
242
|
## License
|
|
51
243
|
|
|
52
244
|
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// index.js - Main BertUI package exports
|
|
2
|
+
import logger from "./src/logger/logger.js";
|
|
3
|
+
import { defaultConfig } from "./src/config/defaultConfig.js";
|
|
4
|
+
import { startDev } from "./src/dev.js";
|
|
5
|
+
import { buildProduction } from "./src/build.js";
|
|
6
|
+
import { compileProject } from "./src/client/compiler.js";
|
|
7
|
+
import { program } from "./src/cli.js";
|
|
8
|
+
|
|
9
|
+
// Router exports - these will be available after compilation
|
|
10
|
+
// Users import these from 'bertui/router'
|
|
11
|
+
export { Link, navigate, Router } from './src/router/router.js';
|
|
12
|
+
|
|
13
|
+
// Named exports for CLI and build tools
|
|
14
|
+
export {
|
|
15
|
+
logger,
|
|
16
|
+
defaultConfig,
|
|
17
|
+
startDev,
|
|
18
|
+
buildProduction,
|
|
19
|
+
compileProject,
|
|
20
|
+
program
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Default export
|
|
24
|
+
export default {
|
|
25
|
+
logger,
|
|
26
|
+
defaultConfig,
|
|
27
|
+
startDev,
|
|
28
|
+
buildProduction,
|
|
29
|
+
compileProject,
|
|
30
|
+
program,
|
|
31
|
+
version: "0.1.2"
|
|
32
|
+
};
|
package/package.json
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bertui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Lightning-fast React dev server powered by Bun and Elysia",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./index.
|
|
6
|
+
"main": "./index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"bertui": "./bin/bertui.js"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
|
-
".":
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./index.js",
|
|
13
|
+
"require": "./index.js"
|
|
14
|
+
},
|
|
12
15
|
"./styles": "./src/styles/bertui.css",
|
|
13
|
-
"./logger": "./src/logger/logger.js"
|
|
16
|
+
"./logger": "./src/logger/logger.js",
|
|
17
|
+
"./router": {
|
|
18
|
+
"import": "./src/router/client-exports.js",
|
|
19
|
+
"require": "./src/router/client-exports.js"
|
|
20
|
+
}
|
|
14
21
|
},
|
|
15
22
|
"files": [
|
|
16
23
|
"bin",
|
|
17
24
|
"src",
|
|
18
|
-
"index.
|
|
25
|
+
"index.js",
|
|
19
26
|
"README.md"
|
|
20
27
|
],
|
|
21
28
|
"scripts": {
|
|
@@ -30,7 +37,9 @@
|
|
|
30
37
|
"vite-alternative",
|
|
31
38
|
"elysia",
|
|
32
39
|
"build-tool",
|
|
33
|
-
"bundler"
|
|
40
|
+
"bundler",
|
|
41
|
+
"routing",
|
|
42
|
+
"file-based-routing"
|
|
34
43
|
],
|
|
35
44
|
"author": "Your Name",
|
|
36
45
|
"license": "MIT",
|
package/src/client/compiler.js
CHANGED
|
@@ -1,94 +1,92 @@
|
|
|
1
1
|
// src/client/compiler.js
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
4
4
|
import logger from '../logger/logger.js';
|
|
5
|
+
import { generateRoutes, generateRouterCode, generateMainWithRouter, logRoutes } from '../router/router.js';
|
|
5
6
|
|
|
6
7
|
export async function compileProject(root) {
|
|
7
|
-
|
|
8
|
+
const compiledDir = join(root, '.bertui', 'compiled');
|
|
9
|
+
const routerDir = join(root, '.bertui');
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// Check if src exists
|
|
13
|
-
if (!existsSync(srcDir)) {
|
|
14
|
-
logger.error('src/ directory not found!');
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Create output directory
|
|
19
|
-
if (!existsSync(outDir)) {
|
|
20
|
-
mkdirSync(outDir, { recursive: true });
|
|
21
|
-
logger.info('Created .bertui/compiled/');
|
|
11
|
+
// Ensure compiled directory exists
|
|
12
|
+
if (!existsSync(compiledDir)) {
|
|
13
|
+
mkdirSync(compiledDir, { recursive: true });
|
|
22
14
|
}
|
|
23
15
|
|
|
24
|
-
// Compile all files
|
|
25
16
|
const startTime = Date.now();
|
|
26
|
-
const stats = await compileDirectory(srcDir, outDir, root);
|
|
27
|
-
const duration = Date.now() - startTime;
|
|
28
|
-
|
|
29
|
-
logger.success(`Compiled ${stats.files} files in ${duration}ms`);
|
|
30
|
-
logger.info(`Output: ${outDir}`);
|
|
31
|
-
|
|
32
|
-
return { outDir, stats };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function compileDirectory(srcDir, outDir, root) {
|
|
36
|
-
const stats = { files: 0, skipped: 0 };
|
|
37
|
-
|
|
38
|
-
const files = readdirSync(srcDir);
|
|
39
17
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
18
|
+
try {
|
|
19
|
+
// Check if routing is enabled (pages directory exists)
|
|
20
|
+
const pagesDir = join(root, 'src', 'pages');
|
|
21
|
+
const useRouting = existsSync(pagesDir);
|
|
22
|
+
|
|
23
|
+
let entryPoint;
|
|
24
|
+
let routes = [];
|
|
43
25
|
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
26
|
+
if (useRouting) {
|
|
27
|
+
logger.info('š File-based routing enabled');
|
|
28
|
+
|
|
29
|
+
// Generate routes
|
|
30
|
+
routes = generateRoutes(root);
|
|
31
|
+
logRoutes(routes);
|
|
32
|
+
|
|
33
|
+
// Generate router code
|
|
34
|
+
const routerCode = generateRouterCode(routes);
|
|
35
|
+
const routerPath = join(routerDir, 'router.js');
|
|
36
|
+
writeFileSync(routerPath, routerCode);
|
|
37
|
+
logger.info('Generated router.js');
|
|
38
|
+
|
|
39
|
+
// Generate main entry with router
|
|
40
|
+
const mainCode = generateMainWithRouter(routes);
|
|
41
|
+
const mainPath = join(routerDir, 'main-entry.js');
|
|
42
|
+
writeFileSync(mainPath, mainCode);
|
|
43
|
+
|
|
44
|
+
entryPoint = mainPath;
|
|
51
45
|
} else {
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
const relativePath = relative(join(root, 'src'), srcPath);
|
|
46
|
+
// Use regular main.jsx if no pages directory
|
|
47
|
+
entryPoint = join(root, 'src/main.jsx');
|
|
55
48
|
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
stats.files++;
|
|
59
|
-
} else if (ext === '.js' || ext === '.css') {
|
|
60
|
-
// Copy as-is
|
|
61
|
-
const outPath = join(outDir, file);
|
|
62
|
-
await Bun.write(outPath, Bun.file(srcPath));
|
|
63
|
-
logger.debug(`Copied: ${relativePath}`);
|
|
64
|
-
stats.files++;
|
|
65
|
-
} else {
|
|
66
|
-
logger.debug(`Skipped: ${relativePath}`);
|
|
67
|
-
stats.skipped++;
|
|
49
|
+
if (!existsSync(entryPoint)) {
|
|
50
|
+
throw new Error('src/main.jsx not found. Create either src/main.jsx or src/pages/ directory.');
|
|
68
51
|
}
|
|
69
52
|
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return stats;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function compileFile(srcPath, outDir, filename, relativePath) {
|
|
76
|
-
const ext = extname(filename);
|
|
77
|
-
const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const transpiler = new Bun.Transpiler({ loader });
|
|
81
|
-
const code = await Bun.file(srcPath).text();
|
|
82
|
-
const compiled = await transpiler.transform(code);
|
|
83
53
|
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
|
|
54
|
+
// Transpile with Bun
|
|
55
|
+
const result = await Bun.build({
|
|
56
|
+
entrypoints: [entryPoint],
|
|
57
|
+
outdir: compiledDir,
|
|
58
|
+
target: 'browser',
|
|
59
|
+
format: 'esm',
|
|
60
|
+
splitting: true,
|
|
61
|
+
naming: {
|
|
62
|
+
entry: '[name].js',
|
|
63
|
+
chunk: 'chunks/[name]-[hash].js'
|
|
64
|
+
},
|
|
65
|
+
external: ['react', 'react-dom'],
|
|
66
|
+
define: {
|
|
67
|
+
'process.env.NODE_ENV': '"development"'
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!result.success) {
|
|
72
|
+
logger.error('Compilation failed!');
|
|
73
|
+
result.logs.forEach(log => logger.error(log.message));
|
|
74
|
+
throw new Error('Build failed');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Copy BertUI CSS
|
|
78
|
+
const bertuiCss = join(import.meta.dir, '../styles/bertui.css');
|
|
79
|
+
if (existsSync(bertuiCss)) {
|
|
80
|
+
await Bun.write(join(compiledDir, 'bertui.css'), Bun.file(bertuiCss));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const duration = Date.now() - startTime;
|
|
84
|
+
logger.success(`Compiled in ${duration}ms`);
|
|
85
|
+
|
|
86
|
+
return { success: true, routes };
|
|
87
87
|
|
|
88
|
-
await Bun.write(outPath, compiled);
|
|
89
|
-
logger.debug(`Compiled: ${relativePath} ā ${outFilename}`);
|
|
90
88
|
} catch (error) {
|
|
91
|
-
logger.error(`
|
|
89
|
+
logger.error(`Compilation error: ${error.message}`);
|
|
92
90
|
throw error;
|
|
93
91
|
}
|
|
94
92
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// src/router/router.js
|
|
2
|
+
import { join, relative, parse } from 'path';
|
|
3
|
+
import { readdirSync, statSync, existsSync } from 'fs';
|
|
4
|
+
import logger from '../logger/logger.js';
|
|
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
|
+
const fileName = parsed.name;
|
|
31
|
+
|
|
32
|
+
// Skip non-page files
|
|
33
|
+
if (fileName.startsWith('_') || fileName.startsWith('.')) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Generate route path
|
|
38
|
+
let routePath = join(basePath, fileName === 'index' ? '' : fileName);
|
|
39
|
+
routePath = '/' + routePath.replace(/\\/g, '/');
|
|
40
|
+
|
|
41
|
+
// Handle dynamic routes [id] -> :id
|
|
42
|
+
routePath = routePath.replace(/\[([^\]]+)\]/g, ':$1');
|
|
43
|
+
|
|
44
|
+
// Get relative path from pages dir
|
|
45
|
+
const relativePath = relative(pagesDir, fullPath);
|
|
46
|
+
|
|
47
|
+
routes.push({
|
|
48
|
+
path: routePath,
|
|
49
|
+
file: relativePath,
|
|
50
|
+
component: fullPath,
|
|
51
|
+
isDynamic: routePath.includes(':')
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
scanDirectory(pagesDir);
|
|
58
|
+
|
|
59
|
+
// Sort routes: static routes first, then dynamic, then catch-all
|
|
60
|
+
routes.sort((a, b) => {
|
|
61
|
+
if (a.isDynamic && !b.isDynamic) return 1;
|
|
62
|
+
if (!a.isDynamic && b.isDynamic) return -1;
|
|
63
|
+
return a.path.localeCompare(b.path);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return routes;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generates the router client code
|
|
71
|
+
*/
|
|
72
|
+
export function generateRouterCode(routes) {
|
|
73
|
+
const imports = routes.map((route, index) =>
|
|
74
|
+
`import Page${index} from '../src/pages/${route.file.replace(/\\/g, '/')}';`
|
|
75
|
+
).join('\n');
|
|
76
|
+
|
|
77
|
+
const routeConfigs = routes.map((route, index) => ({
|
|
78
|
+
path: route.path,
|
|
79
|
+
component: `Page${index}`
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
return `
|
|
83
|
+
// Auto-generated router code - DO NOT EDIT MANUALLY
|
|
84
|
+
import React, { useState, useEffect } from 'react';
|
|
85
|
+
${imports}
|
|
86
|
+
|
|
87
|
+
const routes = ${JSON.stringify(routeConfigs, null, 2).replace(/"Page(\d+)"/g, 'Page$1')};
|
|
88
|
+
|
|
89
|
+
export function Router() {
|
|
90
|
+
const [currentPath, setCurrentPath] = useState(window.location.pathname);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const handlePopState = () => {
|
|
94
|
+
setCurrentPath(window.location.pathname);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
window.addEventListener('popstate', handlePopState);
|
|
98
|
+
return () => window.removeEventListener('popstate', handlePopState);
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
// Match current path to route
|
|
102
|
+
const matchedRoute = routes.find(route => {
|
|
103
|
+
if (route.path === currentPath) return true;
|
|
104
|
+
|
|
105
|
+
// Handle dynamic routes
|
|
106
|
+
const routeParts = route.path.split('/');
|
|
107
|
+
const pathParts = currentPath.split('/');
|
|
108
|
+
|
|
109
|
+
if (routeParts.length !== pathParts.length) return false;
|
|
110
|
+
|
|
111
|
+
return routeParts.every((part, i) => {
|
|
112
|
+
if (part.startsWith(':')) return true;
|
|
113
|
+
return part === pathParts[i];
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!matchedRoute) {
|
|
118
|
+
return <div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
119
|
+
<h1>404 - Page Not Found</h1>
|
|
120
|
+
<p>The page "{currentPath}" does not exist.</p>
|
|
121
|
+
<a href="/" onClick={(e) => { e.preventDefault(); navigate('/'); }}>Go Home</a>
|
|
122
|
+
</div>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Extract params from dynamic routes
|
|
126
|
+
const params = {};
|
|
127
|
+
if (matchedRoute.path.includes(':')) {
|
|
128
|
+
const routeParts = matchedRoute.path.split('/');
|
|
129
|
+
const pathParts = currentPath.split('/');
|
|
130
|
+
|
|
131
|
+
routeParts.forEach((part, i) => {
|
|
132
|
+
if (part.startsWith(':')) {
|
|
133
|
+
const paramName = part.slice(1);
|
|
134
|
+
params[paramName] = pathParts[i];
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const Component = matchedRoute.component;
|
|
140
|
+
return <Component params={params} />;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Client-side navigation
|
|
144
|
+
export function navigate(path) {
|
|
145
|
+
window.history.pushState({}, '', path);
|
|
146
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Link component for navigation
|
|
150
|
+
export function Link({ href, children, ...props }) {
|
|
151
|
+
const handleClick = (e) => {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
navigate(href);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<a href={href} onClick={handleClick} {...props}>
|
|
158
|
+
{children}
|
|
159
|
+
</a>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Generates the main entry point with router
|
|
167
|
+
*/
|
|
168
|
+
export function generateMainWithRouter(routes) {
|
|
169
|
+
return `
|
|
170
|
+
import 'bertui/styles';
|
|
171
|
+
import React from 'react';
|
|
172
|
+
import ReactDOM from 'react-dom/client';
|
|
173
|
+
import { Router } from './.bertui/router.js';
|
|
174
|
+
|
|
175
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
176
|
+
<Router />
|
|
177
|
+
);
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function logRoutes(routes) {
|
|
182
|
+
if (routes.length === 0) {
|
|
183
|
+
logger.warn('No routes found in src/pages/');
|
|
184
|
+
logger.info('Create files in src/pages/ to define routes:');
|
|
185
|
+
logger.info(' src/pages/index.jsx ā /');
|
|
186
|
+
logger.info(' src/pages/about.jsx ā /about');
|
|
187
|
+
logger.info(' src/pages/user/[id].jsx ā /user/:id');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
logger.bigLog('ROUTES DISCOVERED', { color: 'cyan' });
|
|
192
|
+
logger.table(routes.map(r => ({
|
|
193
|
+
route: r.path,
|
|
194
|
+
file: r.file,
|
|
195
|
+
type: r.isDynamic ? 'dynamic' : 'static'
|
|
196
|
+
})));
|
|
197
|
+
}
|
package/src/server/dev-server.js
CHANGED
|
@@ -12,9 +12,20 @@ export async function startDevServer(options = {}) {
|
|
|
12
12
|
const compiledDir = join(root, '.bertui', 'compiled');
|
|
13
13
|
|
|
14
14
|
const clients = new Set();
|
|
15
|
+
let currentRoutes = [];
|
|
15
16
|
|
|
16
17
|
const app = new Elysia()
|
|
17
|
-
.
|
|
18
|
+
// Serve index.html for all routes (SPA mode)
|
|
19
|
+
.get('/*', async ({ params }) => {
|
|
20
|
+
// Check if it's requesting a file
|
|
21
|
+
const path = params['*'] || '';
|
|
22
|
+
|
|
23
|
+
if (path.includes('.')) {
|
|
24
|
+
// It's a file request, handle it separately
|
|
25
|
+
return await serveFile(compiledDir, path);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Serve the SPA HTML for all routes
|
|
18
29
|
const html = `
|
|
19
30
|
<!DOCTYPE html>
|
|
20
31
|
<html lang="en">
|
|
@@ -22,11 +33,19 @@ export async function startDevServer(options = {}) {
|
|
|
22
33
|
<meta charset="UTF-8">
|
|
23
34
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
24
35
|
<title>BertUI App</title>
|
|
36
|
+
<link rel="stylesheet" href="/compiled/bertui.css">
|
|
25
37
|
</head>
|
|
26
38
|
<body>
|
|
27
39
|
<div id="root"></div>
|
|
28
40
|
<script type="module" src="/hmr-client.js"></script>
|
|
29
|
-
<script type="module"
|
|
41
|
+
<script type="module">
|
|
42
|
+
// Provide React and ReactDOM from CDN for dev
|
|
43
|
+
import React from 'https://esm.sh/react@18.2.0';
|
|
44
|
+
import ReactDOM from 'https://esm.sh/react-dom@18.2.0';
|
|
45
|
+
window.React = React;
|
|
46
|
+
window.ReactDOM = ReactDOM;
|
|
47
|
+
</script>
|
|
48
|
+
<script type="module" src="/compiled/main-entry.js"></script>
|
|
30
49
|
</body>
|
|
31
50
|
</html>`;
|
|
32
51
|
|
|
@@ -47,13 +66,28 @@ ws.onmessage = (event) => {
|
|
|
47
66
|
const data = JSON.parse(event.data);
|
|
48
67
|
|
|
49
68
|
if (data.type === 'reload') {
|
|
50
|
-
console.log('%cš
|
|
69
|
+
console.log('%cš Hot reloading...', 'color: #f59e0b');
|
|
51
70
|
window.location.reload();
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
if (data.type === 'recompiling') {
|
|
55
74
|
console.log('%cāļø Recompiling...', 'color: #3b82f6');
|
|
56
75
|
}
|
|
76
|
+
|
|
77
|
+
if (data.type === 'routes-updated') {
|
|
78
|
+
console.log('%cš Routes updated:', 'color: #8b5cf6; font-weight: bold');
|
|
79
|
+
data.routes.forEach(r => {
|
|
80
|
+
console.log(\` \${r.path} ā \${r.file}\`);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
ws.onerror = (error) => {
|
|
86
|
+
console.error('HMR connection error:', error);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
ws.onclose = () => {
|
|
90
|
+
console.log('%cā HMR disconnected. Refresh to reconnect.', 'color: #ef4444');
|
|
57
91
|
};
|
|
58
92
|
`;
|
|
59
93
|
|
|
@@ -66,6 +100,14 @@ ws.onmessage = (event) => {
|
|
|
66
100
|
open(ws) {
|
|
67
101
|
clients.add(ws);
|
|
68
102
|
logger.info('Client connected to HMR');
|
|
103
|
+
|
|
104
|
+
// Send current routes on connection
|
|
105
|
+
if (currentRoutes.length > 0) {
|
|
106
|
+
ws.send(JSON.stringify({
|
|
107
|
+
type: 'routes-updated',
|
|
108
|
+
routes: currentRoutes
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
69
111
|
},
|
|
70
112
|
close(ws) {
|
|
71
113
|
clients.delete(ws);
|
|
@@ -83,7 +125,7 @@ ws.onmessage = (event) => {
|
|
|
83
125
|
}
|
|
84
126
|
|
|
85
127
|
const ext = extname(filepath);
|
|
86
|
-
const contentType = ext
|
|
128
|
+
const contentType = getContentType(ext);
|
|
87
129
|
|
|
88
130
|
return new Response(await file.text(), {
|
|
89
131
|
headers: {
|
|
@@ -101,14 +143,49 @@ ws.onmessage = (event) => {
|
|
|
101
143
|
}
|
|
102
144
|
|
|
103
145
|
logger.success(`Server running at http://localhost:${port}`);
|
|
146
|
+
logger.info('Press Ctrl+C to stop');
|
|
104
147
|
|
|
105
148
|
// Watch for file changes
|
|
106
|
-
setupWatcher(root,
|
|
149
|
+
setupWatcher(root, clients, (routes) => {
|
|
150
|
+
currentRoutes = routes;
|
|
151
|
+
});
|
|
107
152
|
|
|
108
153
|
return app;
|
|
109
154
|
}
|
|
110
155
|
|
|
111
|
-
function
|
|
156
|
+
async function serveFile(compiledDir, path) {
|
|
157
|
+
const filepath = join(compiledDir, path);
|
|
158
|
+
const file = Bun.file(filepath);
|
|
159
|
+
|
|
160
|
+
if (!await file.exists()) {
|
|
161
|
+
return new Response('File not found', { status: 404 });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const ext = extname(filepath);
|
|
165
|
+
const contentType = getContentType(ext);
|
|
166
|
+
|
|
167
|
+
return new Response(await file.text(), {
|
|
168
|
+
headers: {
|
|
169
|
+
'Content-Type': contentType,
|
|
170
|
+
'Cache-Control': 'no-store'
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getContentType(ext) {
|
|
176
|
+
const types = {
|
|
177
|
+
'.js': 'application/javascript',
|
|
178
|
+
'.css': 'text/css',
|
|
179
|
+
'.html': 'text/html',
|
|
180
|
+
'.json': 'application/json',
|
|
181
|
+
'.png': 'image/png',
|
|
182
|
+
'.jpg': 'image/jpeg',
|
|
183
|
+
'.svg': 'image/svg+xml'
|
|
184
|
+
};
|
|
185
|
+
return types[ext] || 'text/plain';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function setupWatcher(root, clients, onRoutesUpdate) {
|
|
112
189
|
const srcDir = join(root, 'src');
|
|
113
190
|
|
|
114
191
|
if (!existsSync(srcDir)) {
|
|
@@ -116,14 +193,18 @@ function setupWatcher(root, compiledDir, clients) {
|
|
|
116
193
|
return;
|
|
117
194
|
}
|
|
118
195
|
|
|
119
|
-
logger.info(
|
|
196
|
+
logger.info(`š Watching: ${srcDir}`);
|
|
197
|
+
|
|
198
|
+
let isRecompiling = false;
|
|
120
199
|
|
|
121
200
|
watch(srcDir, { recursive: true }, async (eventType, filename) => {
|
|
122
|
-
if (!filename) return;
|
|
201
|
+
if (!filename || isRecompiling) return;
|
|
123
202
|
|
|
124
203
|
const ext = extname(filename);
|
|
125
204
|
if (['.js', '.jsx', '.ts', '.tsx', '.css'].includes(ext)) {
|
|
126
|
-
|
|
205
|
+
isRecompiling = true;
|
|
206
|
+
|
|
207
|
+
logger.info(`š File changed: ${filename}`);
|
|
127
208
|
|
|
128
209
|
// Notify clients that recompilation is starting
|
|
129
210
|
for (const client of clients) {
|
|
@@ -136,7 +217,23 @@ function setupWatcher(root, compiledDir, clients) {
|
|
|
136
217
|
|
|
137
218
|
// Recompile the project
|
|
138
219
|
try {
|
|
139
|
-
await compileProject(root);
|
|
220
|
+
const result = await compileProject(root);
|
|
221
|
+
|
|
222
|
+
// Notify about route changes
|
|
223
|
+
if (result.routes && result.routes.length > 0) {
|
|
224
|
+
onRoutesUpdate(result.routes);
|
|
225
|
+
|
|
226
|
+
for (const client of clients) {
|
|
227
|
+
try {
|
|
228
|
+
client.send(JSON.stringify({
|
|
229
|
+
type: 'routes-updated',
|
|
230
|
+
routes: result.routes
|
|
231
|
+
}));
|
|
232
|
+
} catch (e) {
|
|
233
|
+
clients.delete(client);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
140
237
|
|
|
141
238
|
// Notify clients to reload
|
|
142
239
|
for (const client of clients) {
|
|
@@ -148,6 +245,8 @@ function setupWatcher(root, compiledDir, clients) {
|
|
|
148
245
|
}
|
|
149
246
|
} catch (error) {
|
|
150
247
|
logger.error(`Recompilation failed: ${error.message}`);
|
|
248
|
+
} finally {
|
|
249
|
+
isRecompiling = false;
|
|
151
250
|
}
|
|
152
251
|
}
|
|
153
252
|
});
|