bertui 1.1.9 → 1.2.0
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 +65 -45
- package/package.json +1 -1
- package/src/analyzer/index.js +370 -0
- package/src/build/compiler/route-discoverer.js +2 -0
- package/src/build.js +90 -184
- package/src/cli.js +77 -27
- package/src/client/compiler.js +168 -67
- package/src/dev.js +47 -9
- package/src/hydration/index.js +151 -0
- package/src/layouts/index.js +165 -0
- package/src/loading/index.js +210 -0
- package/src/middleware/index.js +182 -0
- package/src/scaffolder/index.js +310 -0
- package/src/server/dev-handler.js +78 -148
- package/src/server-islands/index.js +1 -1
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// bertui/src/scaffolder/index.js
|
|
2
|
+
// CLI component/page/layout scaffolder
|
|
3
|
+
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
6
|
+
import logger from '../logger/logger.js';
|
|
7
|
+
// ─── TEMPLATES ─────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const TEMPLATES = {
|
|
10
|
+
component: (name) => `import React, { useState } from 'react';
|
|
11
|
+
|
|
12
|
+
interface ${name}Props {
|
|
13
|
+
className?: string;
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function ${name}({ className = '', children }: ${name}Props) {
|
|
18
|
+
return (
|
|
19
|
+
<div className={className}>
|
|
20
|
+
{children ?? <p>${name} component</p>}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
`,
|
|
25
|
+
|
|
26
|
+
page: (name, route) => `// Route: ${route}
|
|
27
|
+
import React from 'react';
|
|
28
|
+
import { Link } from 'bertui/router';
|
|
29
|
+
|
|
30
|
+
export const title = '${name}';
|
|
31
|
+
export const description = '${name} page';
|
|
32
|
+
|
|
33
|
+
export default function ${name}Page() {
|
|
34
|
+
return (
|
|
35
|
+
<main style={{ padding: '2rem', fontFamily: 'system-ui' }}>
|
|
36
|
+
<h1>${name}</h1>
|
|
37
|
+
<p>Welcome to ${name}</p>
|
|
38
|
+
<Link to="/">← Back home</Link>
|
|
39
|
+
</main>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
`,
|
|
43
|
+
|
|
44
|
+
layout: (name) => `import React from 'react';
|
|
45
|
+
|
|
46
|
+
interface ${name}LayoutProps {
|
|
47
|
+
children: React.ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// This layout wraps all pages${name === 'default' ? '' : ` under /${name.toLowerCase()}/`}
|
|
51
|
+
export default function ${name}Layout({ children }: ${name}LayoutProps) {
|
|
52
|
+
return (
|
|
53
|
+
<div style={{ minHeight: '100vh', fontFamily: 'system-ui' }}>
|
|
54
|
+
<header style={{
|
|
55
|
+
padding: '1rem 2rem',
|
|
56
|
+
borderBottom: '1px solid #e5e7eb',
|
|
57
|
+
display: 'flex',
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
justifyContent: 'space-between',
|
|
60
|
+
}}>
|
|
61
|
+
<a href="/" style={{ fontWeight: 700, textDecoration: 'none', color: 'inherit' }}>
|
|
62
|
+
My App
|
|
63
|
+
</a>
|
|
64
|
+
<nav style={{ display: 'flex', gap: '1.5rem' }}>
|
|
65
|
+
<a href="/" style={{ color: '#4b5563', textDecoration: 'none' }}>Home</a>
|
|
66
|
+
<a href="/about" style={{ color: '#4b5563', textDecoration: 'none' }}>About</a>
|
|
67
|
+
</nav>
|
|
68
|
+
</header>
|
|
69
|
+
<main style={{ padding: '2rem' }}>
|
|
70
|
+
{children}
|
|
71
|
+
</main>
|
|
72
|
+
<footer style={{
|
|
73
|
+
padding: '1rem 2rem',
|
|
74
|
+
borderTop: '1px solid #e5e7eb',
|
|
75
|
+
color: '#9ca3af',
|
|
76
|
+
fontSize: '14px',
|
|
77
|
+
textAlign: 'center',
|
|
78
|
+
}}>
|
|
79
|
+
Built with BertUI ⚡
|
|
80
|
+
</footer>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
`,
|
|
85
|
+
|
|
86
|
+
loading: (name) => `import React from 'react';
|
|
87
|
+
|
|
88
|
+
// Loading state for ${name} route
|
|
89
|
+
// This shows while the page JavaScript loads
|
|
90
|
+
export default function ${name}Loading() {
|
|
91
|
+
return (
|
|
92
|
+
<div style={{
|
|
93
|
+
display: 'flex',
|
|
94
|
+
flexDirection: 'column',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
justifyContent: 'center',
|
|
97
|
+
minHeight: '60vh',
|
|
98
|
+
gap: '1rem',
|
|
99
|
+
fontFamily: 'system-ui',
|
|
100
|
+
}}>
|
|
101
|
+
<div style={{
|
|
102
|
+
width: '40px',
|
|
103
|
+
height: '40px',
|
|
104
|
+
border: '3px solid #e5e7eb',
|
|
105
|
+
borderTopColor: '#10b981',
|
|
106
|
+
borderRadius: '50%',
|
|
107
|
+
animation: 'spin 0.7s linear infinite',
|
|
108
|
+
}} />
|
|
109
|
+
<style>{'@keyframes spin { to { transform: rotate(360deg); } }'}</style>
|
|
110
|
+
<p style={{ color: '#6b7280', fontSize: '14px' }}>Loading ${name}...</p>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
`,
|
|
115
|
+
|
|
116
|
+
middleware: () => `import type { MiddlewareContext } from 'bertui/middleware';
|
|
117
|
+
|
|
118
|
+
// Runs before EVERY page request
|
|
119
|
+
// Use ctx.redirect(), ctx.respond(), or ctx.locals to pass data
|
|
120
|
+
|
|
121
|
+
export async function onRequest(ctx: MiddlewareContext) {
|
|
122
|
+
// Example: protect /dashboard
|
|
123
|
+
// if (ctx.pathname.startsWith('/dashboard')) {
|
|
124
|
+
// const token = ctx.headers['authorization'];
|
|
125
|
+
// if (!token) return ctx.redirect('/login');
|
|
126
|
+
// }
|
|
127
|
+
|
|
128
|
+
// Example: add custom headers
|
|
129
|
+
// ctx.setHeader('X-Powered-By', 'BertUI');
|
|
130
|
+
|
|
131
|
+
// Example: pass data to pages via locals
|
|
132
|
+
// ctx.locals.user = await getUser(ctx.headers.cookie);
|
|
133
|
+
|
|
134
|
+
console.log('[Middleware]', ctx.method, ctx.pathname);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Optional: handle middleware errors
|
|
138
|
+
export async function onError(ctx: MiddlewareContext, error: Error) {
|
|
139
|
+
console.error('[Middleware Error]', error.message);
|
|
140
|
+
}
|
|
141
|
+
`,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// ─── SCAFFOLDER ─────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
export async function scaffold(type, name, options = {}) {
|
|
147
|
+
const root = options.root || process.cwd();
|
|
148
|
+
const ts = options.ts !== false; // Default: TypeScript
|
|
149
|
+
|
|
150
|
+
const ext = ts ? '.tsx' : '.jsx';
|
|
151
|
+
|
|
152
|
+
switch (type) {
|
|
153
|
+
case 'component':
|
|
154
|
+
return createComponent(name, root, ext);
|
|
155
|
+
case 'page':
|
|
156
|
+
return createPage(name, root, ext);
|
|
157
|
+
case 'layout':
|
|
158
|
+
return createLayout(name, root, ext);
|
|
159
|
+
case 'loading':
|
|
160
|
+
return createLoading(name, root, ext);
|
|
161
|
+
case 'middleware':
|
|
162
|
+
return createMiddleware(root, ts);
|
|
163
|
+
default:
|
|
164
|
+
logger.error(`Unknown scaffold type: ${type}`);
|
|
165
|
+
logger.info('Available types: component, page, layout, loading, middleware');
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function createComponent(name, root, ext) {
|
|
171
|
+
const pascal = toPascalCase(name);
|
|
172
|
+
const dir = join(root, 'src', 'components');
|
|
173
|
+
|
|
174
|
+
fsMkdir(dir, { recursive: true });
|
|
175
|
+
|
|
176
|
+
const filePath = join(dir, `${pascal}${ext}`);
|
|
177
|
+
|
|
178
|
+
if (fsExists(filePath)) {
|
|
179
|
+
logger.warn(`Component already exists: src/components/${pascal}${ext}`);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await Bun.write(filePath, TEMPLATES.component(pascal));
|
|
184
|
+
logger.success(`✅ Created component: src/components/${pascal}${ext}`);
|
|
185
|
+
return filePath;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function createPage(name, root, ext) {
|
|
189
|
+
const pascal = toPascalCase(name);
|
|
190
|
+
const route = `/${name.toLowerCase().replace(/\s+/g, '-')}`;
|
|
191
|
+
|
|
192
|
+
// Handle nested routes like "blog/[slug]"
|
|
193
|
+
const parts = name.split('/');
|
|
194
|
+
const pageName = toPascalCase(parts[parts.length - 1]);
|
|
195
|
+
const dir = join(root, 'src', 'pages', ...parts.slice(0, -1));
|
|
196
|
+
|
|
197
|
+
fsMkdir(dir, { recursive: true });
|
|
198
|
+
|
|
199
|
+
// Handle index pages
|
|
200
|
+
const fileName = pageName.toLowerCase() === 'index' ? 'index' : pageName.toLowerCase();
|
|
201
|
+
const filePath = join(dir, `${fileName}${ext}`);
|
|
202
|
+
|
|
203
|
+
if (fsExists(filePath)) {
|
|
204
|
+
logger.warn(`Page already exists: src/pages/${name}${ext}`);
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await Bun.write(filePath, TEMPLATES.page(pascal, route));
|
|
209
|
+
logger.success(`✅ Created page: src/pages/${name}${ext}`);
|
|
210
|
+
logger.info(` Route: ${route}`);
|
|
211
|
+
return filePath;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function createLayout(name, root, ext) {
|
|
215
|
+
const pascal = toPascalCase(name);
|
|
216
|
+
const dir = join(root, 'src', 'layouts');
|
|
217
|
+
|
|
218
|
+
fsMkdir(dir, { recursive: true });
|
|
219
|
+
|
|
220
|
+
const filePath = join(dir, `${name.toLowerCase()}${ext}`);
|
|
221
|
+
|
|
222
|
+
if (fsExists(filePath)) {
|
|
223
|
+
logger.warn(`Layout already exists: src/layouts/${name.toLowerCase()}${ext}`);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await Bun.write(filePath, TEMPLATES.layout(pascal));
|
|
228
|
+
logger.success(`✅ Created layout: src/layouts/${name.toLowerCase()}${ext}`);
|
|
229
|
+
|
|
230
|
+
if (name.toLowerCase() === 'default') {
|
|
231
|
+
logger.info(' → This layout will wrap ALL pages automatically');
|
|
232
|
+
} else {
|
|
233
|
+
logger.info(` → This layout will wrap pages under /${name.toLowerCase()}/`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return filePath;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function createLoading(name, root, ext) {
|
|
240
|
+
const pascal = toPascalCase(name);
|
|
241
|
+
|
|
242
|
+
// Place loading.tsx next to the relevant page/route
|
|
243
|
+
const isRoot = name.toLowerCase() === 'root' || name === '/';
|
|
244
|
+
const dir = isRoot
|
|
245
|
+
? join(root, 'src', 'pages')
|
|
246
|
+
: join(root, 'src', 'pages', name.toLowerCase());
|
|
247
|
+
|
|
248
|
+
fsMkdir(dir, { recursive: true });
|
|
249
|
+
|
|
250
|
+
const filePath = join(dir, `loading${ext}`);
|
|
251
|
+
|
|
252
|
+
if (fsExists(filePath)) {
|
|
253
|
+
logger.warn(`Loading component already exists at ${dir}/loading${ext}`);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await Bun.write(filePath, TEMPLATES.loading(pascal));
|
|
258
|
+
logger.success(`✅ Created loading state: ${filePath.replace(root, '')}`);
|
|
259
|
+
logger.info(` → Shows while ${isRoot ? 'root' : `/${name.toLowerCase()}`} route loads`);
|
|
260
|
+
return filePath;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function createMiddleware(root, ts) {
|
|
264
|
+
const ext = ts ? '.ts' : '.js';
|
|
265
|
+
const filePath = join(root, 'src', `middleware${ext}`);
|
|
266
|
+
|
|
267
|
+
if (fsExists(filePath)) {
|
|
268
|
+
logger.warn(`Middleware already exists: src/middleware${ext}`);
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await Bun.write(filePath, TEMPLATES.middleware());
|
|
273
|
+
logger.success(`✅ Created middleware: src/middleware${ext}`);
|
|
274
|
+
logger.info(' → Runs before every page request');
|
|
275
|
+
return filePath;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─── HELPERS ────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
function toPascalCase(str) {
|
|
281
|
+
return str
|
|
282
|
+
.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
|
|
283
|
+
.replace(/^(.)/, c => c.toUpperCase())
|
|
284
|
+
.replace(/[[\]]/g, ''); // Remove bracket from dynamic routes
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Parse CLI args for the create command
|
|
289
|
+
* Usage: bertui create component Button
|
|
290
|
+
* bertui create page About
|
|
291
|
+
* bertui create layout default
|
|
292
|
+
* bertui create loading blog
|
|
293
|
+
* bertui create middleware
|
|
294
|
+
*/
|
|
295
|
+
export function parseCreateArgs(args) {
|
|
296
|
+
const [type, name] = args;
|
|
297
|
+
|
|
298
|
+
if (!type) {
|
|
299
|
+
logger.error('Usage: bertui create <type> [name]');
|
|
300
|
+
logger.info('Types: component, page, layout, loading, middleware');
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (type !== 'middleware' && !name) {
|
|
305
|
+
logger.error(`Usage: bertui create ${type} <name>`);
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { type, name: name || type };
|
|
310
|
+
}
|
|
@@ -1,45 +1,28 @@
|
|
|
1
|
-
// bertui/src/server/dev-handler.js -
|
|
2
|
-
// Headless dev server handler for Bunny integration
|
|
3
|
-
|
|
1
|
+
// bertui/src/server/dev-handler.js - WITH MIDDLEWARE + LAYOUTS + LOADING
|
|
4
2
|
import { join, extname, dirname } from 'path';
|
|
5
|
-
import { existsSync
|
|
3
|
+
import { existsSync } from 'fs';
|
|
6
4
|
import logger from '../logger/logger.js';
|
|
7
5
|
import { compileProject } from '../client/compiler.js';
|
|
8
6
|
import { loadConfig } from '../config/loadConfig.js';
|
|
9
7
|
import { getContentType, getImageContentType, serveHTML, setupFileWatcher } from './dev-server-utils.js';
|
|
10
8
|
|
|
11
|
-
/**
|
|
12
|
-
* Create a headless dev server handler for integration with Elysia/Bunny
|
|
13
|
-
* @param {Object} options
|
|
14
|
-
* @param {string} options.root - Project root directory
|
|
15
|
-
* @param {number} options.port - Port number (for HMR WebSocket URL)
|
|
16
|
-
* @param {Object} options.elysiaApp - Optional Elysia app instance to mount on
|
|
17
|
-
* @returns {Promise<Object>} Handler object with request handler and utilities
|
|
18
|
-
*/
|
|
19
9
|
export async function createDevHandler(options = {}) {
|
|
20
10
|
const root = options.root || process.cwd();
|
|
21
11
|
const port = parseInt(options.port) || 3000;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
const middlewareManager = options.middleware || null;
|
|
13
|
+
const layouts = options.layouts || {};
|
|
14
|
+
const loadingComponents = options.loadingComponents || {};
|
|
15
|
+
|
|
25
16
|
const compiledDir = join(root, '.bertui', 'compiled');
|
|
26
17
|
const stylesDir = join(root, '.bertui', 'styles');
|
|
27
18
|
const srcDir = join(root, 'src');
|
|
28
19
|
const publicDir = join(root, 'public');
|
|
29
|
-
|
|
20
|
+
|
|
30
21
|
const config = await loadConfig(root);
|
|
31
|
-
|
|
32
|
-
let hasRouter =
|
|
33
|
-
const routerPath = join(compiledDir, 'router.js');
|
|
34
|
-
if (existsSync(routerPath)) {
|
|
35
|
-
hasRouter = true;
|
|
36
|
-
logger.info('File-based routing enabled');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Clients set for HMR
|
|
22
|
+
|
|
23
|
+
let hasRouter = existsSync(join(compiledDir, 'router.js'));
|
|
40
24
|
const clients = new Set();
|
|
41
|
-
|
|
42
|
-
// WebSocket handler for HMR
|
|
25
|
+
|
|
43
26
|
const websocketHandler = {
|
|
44
27
|
open(ws) {
|
|
45
28
|
clients.add(ws);
|
|
@@ -47,208 +30,155 @@ export async function createDevHandler(options = {}) {
|
|
|
47
30
|
},
|
|
48
31
|
close(ws) {
|
|
49
32
|
clients.delete(ws);
|
|
50
|
-
|
|
51
|
-
}
|
|
33
|
+
},
|
|
52
34
|
};
|
|
53
|
-
|
|
54
|
-
// Notify all HMR clients
|
|
35
|
+
|
|
55
36
|
function notifyClients(message) {
|
|
56
37
|
for (const client of clients) {
|
|
57
|
-
try {
|
|
58
|
-
|
|
59
|
-
} catch (e) {
|
|
60
|
-
clients.delete(client);
|
|
61
|
-
}
|
|
38
|
+
try { client.send(JSON.stringify(message)); }
|
|
39
|
+
catch (e) { clients.delete(client); }
|
|
62
40
|
}
|
|
63
41
|
}
|
|
64
|
-
|
|
65
|
-
// Setup file watcher if we have a root
|
|
42
|
+
|
|
66
43
|
let watcherCleanup = null;
|
|
67
44
|
if (root) {
|
|
68
45
|
watcherCleanup = setupFileWatcher(root, compiledDir, clients, async () => {
|
|
69
46
|
hasRouter = existsSync(join(compiledDir, 'router.js'));
|
|
70
47
|
});
|
|
71
48
|
}
|
|
72
|
-
|
|
73
|
-
// MAIN REQUEST HANDLER
|
|
49
|
+
|
|
74
50
|
async function handleRequest(request) {
|
|
75
51
|
const url = new URL(request.url);
|
|
76
|
-
|
|
77
|
-
//
|
|
52
|
+
|
|
53
|
+
// WebSocket upgrade for HMR
|
|
78
54
|
if (url.pathname === '/__hmr' && request.headers.get('upgrade') === 'websocket') {
|
|
79
|
-
// This will be handled by Elysia/Bun.serve upgrade mechanism
|
|
80
55
|
return { type: 'websocket', handler: websocketHandler };
|
|
81
56
|
}
|
|
82
|
-
|
|
83
|
-
//
|
|
57
|
+
|
|
58
|
+
// ✅ Run middleware BEFORE every page request
|
|
59
|
+
if (middlewareManager && isPageRequest(url.pathname)) {
|
|
60
|
+
const middlewareResponse = await middlewareManager.run(request, {
|
|
61
|
+
route: url.pathname,
|
|
62
|
+
});
|
|
63
|
+
if (middlewareResponse) {
|
|
64
|
+
logger.debug(`🛡️ Middleware handled: ${url.pathname}`);
|
|
65
|
+
return middlewareResponse;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Serve page HTML
|
|
84
70
|
if (url.pathname === '/' || (!url.pathname.includes('.') && !url.pathname.startsWith('/compiled'))) {
|
|
85
71
|
const html = await serveHTML(root, hasRouter, config, port);
|
|
86
72
|
return new Response(html, {
|
|
87
|
-
headers: { 'Content-Type': 'text/html' }
|
|
73
|
+
headers: { 'Content-Type': 'text/html' },
|
|
88
74
|
});
|
|
89
75
|
}
|
|
90
|
-
|
|
91
|
-
//
|
|
76
|
+
|
|
77
|
+
// Compiled JS (includes layouts and loading components)
|
|
92
78
|
if (url.pathname.startsWith('/compiled/')) {
|
|
93
79
|
const filepath = join(compiledDir, url.pathname.replace('/compiled/', ''));
|
|
94
80
|
const file = Bun.file(filepath);
|
|
95
|
-
|
|
96
|
-
if (await file.exists()) {
|
|
97
|
-
const ext = extname(filepath).toLowerCase();
|
|
98
|
-
const contentType = ext === '.js' ? 'application/javascript; charset=utf-8' : 'text/plain';
|
|
99
|
-
|
|
100
|
-
return new Response(file, {
|
|
101
|
-
headers: {
|
|
102
|
-
'Content-Type': contentType,
|
|
103
|
-
'Cache-Control': 'no-store, no-cache, must-revalidate'
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Serve bertui-animate CSS
|
|
110
|
-
if (url.pathname === '/bertui-animate.css') {
|
|
111
|
-
const bertuiAnimatePath = join(root, 'node_modules/bertui-animate/dist/bertui-animate.min.css');
|
|
112
|
-
const file = Bun.file(bertuiAnimatePath);
|
|
113
|
-
|
|
114
81
|
if (await file.exists()) {
|
|
115
82
|
return new Response(file, {
|
|
116
|
-
headers: {
|
|
117
|
-
'Content-Type': '
|
|
118
|
-
'Cache-Control': 'no-store'
|
|
119
|
-
}
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
85
|
+
'Cache-Control': 'no-store',
|
|
86
|
+
},
|
|
120
87
|
});
|
|
121
88
|
}
|
|
122
89
|
}
|
|
123
|
-
|
|
124
|
-
//
|
|
90
|
+
|
|
91
|
+
// CSS
|
|
125
92
|
if (url.pathname.startsWith('/styles/')) {
|
|
126
93
|
const filepath = join(stylesDir, url.pathname.replace('/styles/', ''));
|
|
127
94
|
const file = Bun.file(filepath);
|
|
128
|
-
|
|
129
95
|
if (await file.exists()) {
|
|
130
96
|
return new Response(file, {
|
|
131
|
-
headers: {
|
|
132
|
-
'Content-Type': 'text/css',
|
|
133
|
-
'Cache-Control': 'no-store'
|
|
134
|
-
}
|
|
97
|
+
headers: { 'Content-Type': 'text/css', 'Cache-Control': 'no-store' },
|
|
135
98
|
});
|
|
136
99
|
}
|
|
137
100
|
}
|
|
138
|
-
|
|
139
|
-
//
|
|
101
|
+
|
|
102
|
+
// bertui-animate CSS
|
|
103
|
+
if (url.pathname === '/bertui-animate.css') {
|
|
104
|
+
const animPath = join(root, 'node_modules/bertui-animate/dist/bertui-animate.min.css');
|
|
105
|
+
const file = Bun.file(animPath);
|
|
106
|
+
if (await file.exists()) {
|
|
107
|
+
return new Response(file, { headers: { 'Content-Type': 'text/css' } });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Images
|
|
140
112
|
if (url.pathname.startsWith('/images/')) {
|
|
141
113
|
const filepath = join(srcDir, 'images', url.pathname.replace('/images/', ''));
|
|
142
114
|
const file = Bun.file(filepath);
|
|
143
|
-
|
|
144
115
|
if (await file.exists()) {
|
|
145
116
|
const ext = extname(filepath).toLowerCase();
|
|
146
|
-
const contentType = getImageContentType(ext);
|
|
147
|
-
|
|
148
117
|
return new Response(file, {
|
|
149
|
-
headers: {
|
|
150
|
-
'Content-Type': contentType,
|
|
151
|
-
'Cache-Control': 'no-cache'
|
|
152
|
-
}
|
|
118
|
+
headers: { 'Content-Type': getImageContentType(ext), 'Cache-Control': 'no-cache' },
|
|
153
119
|
});
|
|
154
120
|
}
|
|
155
121
|
}
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
if (url.pathname.startsWith('/public/')) {
|
|
122
|
+
|
|
123
|
+
// Public directory
|
|
124
|
+
if (url.pathname.startsWith('/public/') || existsSync(join(publicDir, url.pathname.slice(1)))) {
|
|
159
125
|
const filepath = join(publicDir, url.pathname.replace('/public/', ''));
|
|
160
126
|
const file = Bun.file(filepath);
|
|
161
|
-
|
|
162
127
|
if (await file.exists()) {
|
|
163
|
-
return new Response(file, {
|
|
164
|
-
headers: { 'Cache-Control': 'no-cache' }
|
|
165
|
-
});
|
|
128
|
+
return new Response(file, { headers: { 'Cache-Control': 'no-cache' } });
|
|
166
129
|
}
|
|
167
130
|
}
|
|
168
|
-
|
|
169
|
-
//
|
|
131
|
+
|
|
132
|
+
// node_modules
|
|
170
133
|
if (url.pathname.startsWith('/node_modules/')) {
|
|
171
134
|
const filepath = join(root, 'node_modules', url.pathname.replace('/node_modules/', ''));
|
|
172
135
|
const file = Bun.file(filepath);
|
|
173
|
-
|
|
174
136
|
if (await file.exists()) {
|
|
175
137
|
const ext = extname(filepath).toLowerCase();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
contentType = 'text/css';
|
|
180
|
-
} else if (ext === '.js' || ext === '.mjs') {
|
|
181
|
-
contentType = 'application/javascript; charset=utf-8';
|
|
182
|
-
} else {
|
|
183
|
-
contentType = getContentType(ext);
|
|
184
|
-
}
|
|
185
|
-
|
|
138
|
+
const contentType = ext === '.css' ? 'text/css' :
|
|
139
|
+
['.js', '.mjs'].includes(ext) ? 'application/javascript; charset=utf-8' :
|
|
140
|
+
getContentType(ext);
|
|
186
141
|
return new Response(file, {
|
|
187
|
-
headers: {
|
|
188
|
-
'Content-Type': contentType,
|
|
189
|
-
'Cache-Control': 'no-cache'
|
|
190
|
-
}
|
|
142
|
+
headers: { 'Content-Type': contentType, 'Cache-Control': 'no-cache' },
|
|
191
143
|
});
|
|
192
144
|
}
|
|
193
145
|
}
|
|
194
|
-
|
|
195
|
-
// Not a BertUI route
|
|
146
|
+
|
|
196
147
|
return null;
|
|
197
148
|
}
|
|
198
|
-
|
|
199
|
-
// Standalone server starter (for backward compatibility)
|
|
149
|
+
|
|
200
150
|
async function start() {
|
|
201
151
|
const server = Bun.serve({
|
|
202
152
|
port,
|
|
203
153
|
async fetch(req, server) {
|
|
204
154
|
const url = new URL(req.url);
|
|
205
|
-
|
|
206
|
-
// Handle WebSocket upgrade
|
|
207
155
|
if (url.pathname === '/__hmr') {
|
|
208
156
|
const success = server.upgrade(req);
|
|
209
157
|
if (success) return undefined;
|
|
210
158
|
return new Response('WebSocket upgrade failed', { status: 500 });
|
|
211
159
|
}
|
|
212
|
-
|
|
213
|
-
// Handle normal requests
|
|
214
160
|
const response = await handleRequest(req);
|
|
215
161
|
if (response) return response;
|
|
216
|
-
|
|
217
162
|
return new Response('Not found', { status: 404 });
|
|
218
163
|
},
|
|
219
|
-
websocket: websocketHandler
|
|
164
|
+
websocket: websocketHandler,
|
|
220
165
|
});
|
|
221
|
-
|
|
222
|
-
logger.success(`🚀 BertUI
|
|
166
|
+
|
|
167
|
+
logger.success(`🚀 BertUI running at http://localhost:${port}`);
|
|
223
168
|
return server;
|
|
224
169
|
}
|
|
225
|
-
|
|
226
|
-
// Recompile project
|
|
227
|
-
async function recompile() {
|
|
228
|
-
return await compileProject(root);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Cleanup
|
|
170
|
+
|
|
232
171
|
function dispose() {
|
|
233
|
-
if (watcherCleanup
|
|
234
|
-
watcherCleanup();
|
|
235
|
-
}
|
|
172
|
+
if (watcherCleanup) watcherCleanup();
|
|
236
173
|
clients.clear();
|
|
174
|
+
if (middlewareManager) middlewareManager.dispose();
|
|
237
175
|
}
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
handleRequest,
|
|
241
|
-
start,
|
|
242
|
-
recompile,
|
|
243
|
-
dispose,
|
|
244
|
-
notifyClients,
|
|
245
|
-
config,
|
|
246
|
-
hasRouter,
|
|
247
|
-
// For Elysia integration
|
|
248
|
-
getElysiaApp: () => elysiaApp,
|
|
249
|
-
websocketHandler
|
|
250
|
-
};
|
|
176
|
+
|
|
177
|
+
return { handleRequest, start, notifyClients, dispose, config, hasRouter, websocketHandler };
|
|
251
178
|
}
|
|
252
179
|
|
|
253
|
-
|
|
254
|
-
|
|
180
|
+
function isPageRequest(pathname) {
|
|
181
|
+
// Skip asset requests
|
|
182
|
+
return !pathname.includes('.') ||
|
|
183
|
+
pathname.endsWith('.html');
|
|
184
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { validateServerIsland } from '../build/server-island-validator.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function extractStaticHTML(componentCode) {
|
|
7
7
|
// For now, use existing regex-based extractor
|
|
8
8
|
// TODO: Replace with proper AST parser
|
|
9
9
|
return extractJSXFromReturn(componentCode);
|