bertui 1.1.6 → 1.1.8

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.
@@ -0,0 +1,3 @@
1
+ // bertui/src/router/index.js
2
+ export { Router, Link, useRouter } from './Router.js';
3
+ export { SSRRouter } from './SSRRouter.js';
@@ -0,0 +1,254 @@
1
+ // bertui/src/server/dev-handler.js - NEW FILE
2
+ // Headless dev server handler for Bunny integration
3
+
4
+ import { join, extname, dirname } from 'path';
5
+ import { existsSync, readdirSync } from 'fs';
6
+ import logger from '../logger/logger.js';
7
+ import { compileProject } from '../client/compiler.js';
8
+ import { loadConfig } from '../config/loadConfig.js';
9
+ import { getContentType, getImageContentType, serveHTML, setupFileWatcher } from './dev-server-utils.js';
10
+
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
+ export async function createDevHandler(options = {}) {
20
+ const root = options.root || process.cwd();
21
+ const port = parseInt(options.port) || 3000;
22
+ const elysiaApp = options.elysiaApp || null;
23
+
24
+ // Initialize BertUI state
25
+ const compiledDir = join(root, '.bertui', 'compiled');
26
+ const stylesDir = join(root, '.bertui', 'styles');
27
+ const srcDir = join(root, 'src');
28
+ const publicDir = join(root, 'public');
29
+
30
+ const config = await loadConfig(root);
31
+
32
+ let hasRouter = false;
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
40
+ const clients = new Set();
41
+
42
+ // WebSocket handler for HMR
43
+ const websocketHandler = {
44
+ open(ws) {
45
+ clients.add(ws);
46
+ logger.debug(`HMR client connected (${clients.size} total)`);
47
+ },
48
+ close(ws) {
49
+ clients.delete(ws);
50
+ logger.debug(`HMR client disconnected (${clients.size} remaining)`);
51
+ }
52
+ };
53
+
54
+ // Notify all HMR clients
55
+ function notifyClients(message) {
56
+ for (const client of clients) {
57
+ try {
58
+ client.send(JSON.stringify(message));
59
+ } catch (e) {
60
+ clients.delete(client);
61
+ }
62
+ }
63
+ }
64
+
65
+ // Setup file watcher if we have a root
66
+ let watcherCleanup = null;
67
+ if (root) {
68
+ watcherCleanup = setupFileWatcher(root, compiledDir, clients, async () => {
69
+ hasRouter = existsSync(join(compiledDir, 'router.js'));
70
+ });
71
+ }
72
+
73
+ // MAIN REQUEST HANDLER
74
+ async function handleRequest(request) {
75
+ const url = new URL(request.url);
76
+
77
+ // Handle WebSocket upgrade for HMR
78
+ if (url.pathname === '/__hmr' && request.headers.get('upgrade') === 'websocket') {
79
+ // This will be handled by Elysia/Bun.serve upgrade mechanism
80
+ return { type: 'websocket', handler: websocketHandler };
81
+ }
82
+
83
+ // Serve HTML for routes
84
+ if (url.pathname === '/' || (!url.pathname.includes('.') && !url.pathname.startsWith('/compiled'))) {
85
+ const html = await serveHTML(root, hasRouter, config, port);
86
+ return new Response(html, {
87
+ headers: { 'Content-Type': 'text/html' }
88
+ });
89
+ }
90
+
91
+ // Serve compiled JavaScript
92
+ if (url.pathname.startsWith('/compiled/')) {
93
+ const filepath = join(compiledDir, url.pathname.replace('/compiled/', ''));
94
+ 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
+ if (await file.exists()) {
115
+ return new Response(file, {
116
+ headers: {
117
+ 'Content-Type': 'text/css',
118
+ 'Cache-Control': 'no-store'
119
+ }
120
+ });
121
+ }
122
+ }
123
+
124
+ // Serve CSS
125
+ if (url.pathname.startsWith('/styles/')) {
126
+ const filepath = join(stylesDir, url.pathname.replace('/styles/', ''));
127
+ const file = Bun.file(filepath);
128
+
129
+ if (await file.exists()) {
130
+ return new Response(file, {
131
+ headers: {
132
+ 'Content-Type': 'text/css',
133
+ 'Cache-Control': 'no-store'
134
+ }
135
+ });
136
+ }
137
+ }
138
+
139
+ // Serve images from src/images/
140
+ if (url.pathname.startsWith('/images/')) {
141
+ const filepath = join(srcDir, 'images', url.pathname.replace('/images/', ''));
142
+ const file = Bun.file(filepath);
143
+
144
+ if (await file.exists()) {
145
+ const ext = extname(filepath).toLowerCase();
146
+ const contentType = getImageContentType(ext);
147
+
148
+ return new Response(file, {
149
+ headers: {
150
+ 'Content-Type': contentType,
151
+ 'Cache-Control': 'no-cache'
152
+ }
153
+ });
154
+ }
155
+ }
156
+
157
+ // Serve from public/
158
+ if (url.pathname.startsWith('/public/')) {
159
+ const filepath = join(publicDir, url.pathname.replace('/public/', ''));
160
+ const file = Bun.file(filepath);
161
+
162
+ if (await file.exists()) {
163
+ return new Response(file, {
164
+ headers: { 'Cache-Control': 'no-cache' }
165
+ });
166
+ }
167
+ }
168
+
169
+ // Serve node_modules
170
+ if (url.pathname.startsWith('/node_modules/')) {
171
+ const filepath = join(root, 'node_modules', url.pathname.replace('/node_modules/', ''));
172
+ const file = Bun.file(filepath);
173
+
174
+ if (await file.exists()) {
175
+ const ext = extname(filepath).toLowerCase();
176
+ let contentType;
177
+
178
+ if (ext === '.css') {
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
+
186
+ return new Response(file, {
187
+ headers: {
188
+ 'Content-Type': contentType,
189
+ 'Cache-Control': 'no-cache'
190
+ }
191
+ });
192
+ }
193
+ }
194
+
195
+ // Not a BertUI route
196
+ return null;
197
+ }
198
+
199
+ // Standalone server starter (for backward compatibility)
200
+ async function start() {
201
+ const server = Bun.serve({
202
+ port,
203
+ async fetch(req, server) {
204
+ const url = new URL(req.url);
205
+
206
+ // Handle WebSocket upgrade
207
+ if (url.pathname === '/__hmr') {
208
+ const success = server.upgrade(req);
209
+ if (success) return undefined;
210
+ return new Response('WebSocket upgrade failed', { status: 500 });
211
+ }
212
+
213
+ // Handle normal requests
214
+ const response = await handleRequest(req);
215
+ if (response) return response;
216
+
217
+ return new Response('Not found', { status: 404 });
218
+ },
219
+ websocket: websocketHandler
220
+ });
221
+
222
+ logger.success(`šŸš€ BertUI standalone server running at http://localhost:${port}`);
223
+ return server;
224
+ }
225
+
226
+ // Recompile project
227
+ async function recompile() {
228
+ return await compileProject(root);
229
+ }
230
+
231
+ // Cleanup
232
+ function dispose() {
233
+ if (watcherCleanup && typeof watcherCleanup === 'function') {
234
+ watcherCleanup();
235
+ }
236
+ clients.clear();
237
+ }
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
+ };
251
+ }
252
+
253
+ // Re-export for convenience
254
+ export { handleRequest as standaloneHandler } from './request-handler.js';
@@ -0,0 +1,289 @@
1
+ // bertui/src/server/dev-server-utils.js - NEW FILE
2
+ // Shared utilities for dev server (extracted from dev-server.js)
3
+
4
+ import { join, extname } from 'path';
5
+ import { existsSync, readdirSync, watch } from 'fs';
6
+ import logger from '../logger/logger.js';
7
+ import { compileProject } from '../client/compiler.js';
8
+
9
+ // Image content type mapping
10
+ export function getImageContentType(ext) {
11
+ const types = {
12
+ '.jpg': 'image/jpeg',
13
+ '.jpeg': 'image/jpeg',
14
+ '.png': 'image/png',
15
+ '.gif': 'image/gif',
16
+ '.svg': 'image/svg+xml',
17
+ '.webp': 'image/webp',
18
+ '.avif': 'image/avif',
19
+ '.ico': 'image/x-icon'
20
+ };
21
+ return types[ext] || 'application/octet-stream';
22
+ }
23
+
24
+ // General content type mapping
25
+ export function getContentType(ext) {
26
+ const types = {
27
+ '.js': 'application/javascript',
28
+ '.jsx': 'application/javascript',
29
+ '.css': 'text/css',
30
+ '.html': 'text/html',
31
+ '.json': 'application/json',
32
+ '.png': 'image/png',
33
+ '.jpg': 'image/jpeg',
34
+ '.jpeg': 'image/jpeg',
35
+ '.gif': 'image/gif',
36
+ '.svg': 'image/svg+xml',
37
+ '.webp': 'image/webp',
38
+ '.avif': 'image/avif',
39
+ '.ico': 'image/x-icon',
40
+ '.woff': 'font/woff',
41
+ '.woff2': 'font/woff2',
42
+ '.ttf': 'font/ttf',
43
+ '.otf': 'font/otf',
44
+ '.mp4': 'video/mp4',
45
+ '.webm': 'video/webm',
46
+ '.mp3': 'audio/mpeg'
47
+ };
48
+ return types[ext] || 'text/plain';
49
+ }
50
+
51
+ // HTML generator
52
+ export async function serveHTML(root, hasRouter, config, port) {
53
+ const meta = config.meta || {};
54
+
55
+ const srcStylesDir = join(root, 'src', 'styles');
56
+ let userStylesheets = '';
57
+
58
+ if (existsSync(srcStylesDir)) {
59
+ try {
60
+ const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
61
+ userStylesheets = cssFiles.map(f => ` <link rel="stylesheet" href="/styles/${f}">`).join('\n');
62
+ } catch (error) {
63
+ logger.warn(`Could not read styles directory: ${error.message}`);
64
+ }
65
+ }
66
+
67
+ // Auto-detect bertui-animate CSS
68
+ let bertuiAnimateStylesheet = '';
69
+ const bertuiAnimatePath = join(root, 'node_modules/bertui-animate/dist/bertui-animate.min.css');
70
+ if (existsSync(bertuiAnimatePath)) {
71
+ bertuiAnimateStylesheet = ' <link rel="stylesheet" href="/bertui-animate.css">';
72
+ }
73
+
74
+ // Build import map
75
+ const importMap = {
76
+ "react": "https://esm.sh/react@18.2.0",
77
+ "react-dom": "https://esm.sh/react-dom@18.2.0",
78
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0/client"
79
+ };
80
+
81
+ // Auto-detect bertui-* JavaScript packages
82
+ const nodeModulesDir = join(root, 'node_modules');
83
+
84
+ if (existsSync(nodeModulesDir)) {
85
+ try {
86
+ const packages = readdirSync(nodeModulesDir);
87
+
88
+ for (const pkg of packages) {
89
+ if (!pkg.startsWith('bertui-')) continue;
90
+
91
+ const pkgDir = join(nodeModulesDir, pkg);
92
+ const pkgJsonPath = join(pkgDir, 'package.json');
93
+
94
+ if (!existsSync(pkgJsonPath)) continue;
95
+
96
+ try {
97
+ const pkgJsonContent = await Bun.file(pkgJsonPath).text();
98
+ const pkgJson = JSON.parse(pkgJsonContent);
99
+
100
+ let mainFile = null;
101
+
102
+ if (pkgJson.exports) {
103
+ const rootExport = pkgJson.exports['.'];
104
+ if (typeof rootExport === 'string') {
105
+ mainFile = rootExport;
106
+ } else if (typeof rootExport === 'object') {
107
+ mainFile = rootExport.browser || rootExport.default || rootExport.import;
108
+ }
109
+ }
110
+
111
+ if (!mainFile) {
112
+ mainFile = pkgJson.main || 'index.js';
113
+ }
114
+
115
+ const fullPath = join(pkgDir, mainFile);
116
+ if (existsSync(fullPath)) {
117
+ importMap[pkg] = `/node_modules/${pkg}/${mainFile}`;
118
+ logger.debug(`āœ… ${pkg} available`);
119
+ }
120
+
121
+ } catch (error) {
122
+ logger.warn(`āš ļø Failed to parse ${pkg}/package.json: ${error.message}`);
123
+ }
124
+ }
125
+ } catch (error) {
126
+ logger.warn(`Failed to scan node_modules: ${error.message}`);
127
+ }
128
+ }
129
+
130
+ const html = `<!DOCTYPE html>
131
+ <html lang="${meta.lang || 'en'}">
132
+ <head>
133
+ <meta charset="UTF-8">
134
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
135
+ <title>${meta.title || 'BertUI App'}</title>
136
+
137
+ ${meta.description ? `<meta name="description" content="${meta.description}">` : ''}
138
+ ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
139
+ ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
140
+ ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
141
+
142
+ ${meta.ogTitle ? `<meta property="og:title" content="${meta.ogTitle || meta.title}">` : ''}
143
+ ${meta.ogDescription ? `<meta property="og:description" content="${meta.ogDescription || meta.description}">` : ''}
144
+ ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
145
+
146
+ <link rel="icon" type="image/svg+xml" href="/public/favicon.svg">
147
+
148
+ ${userStylesheets}
149
+ ${bertuiAnimateStylesheet}
150
+
151
+ <script type="importmap">
152
+ ${JSON.stringify({ imports: importMap }, null, 2)}
153
+ </script>
154
+
155
+ <style>
156
+ * {
157
+ margin: 0;
158
+ padding: 0;
159
+ box-sizing: border-box;
160
+ }
161
+ body {
162
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
163
+ }
164
+ </style>
165
+ </head>
166
+ <body>
167
+ <div id="root"></div>
168
+
169
+ <script type="module">
170
+ const ws = new WebSocket('ws://localhost:${port}/__hmr');
171
+
172
+ ws.onopen = () => {
173
+ console.log('%cšŸ”„ BertUI HMR connected', 'color: #10b981; font-weight: bold');
174
+ };
175
+
176
+ ws.onmessage = (event) => {
177
+ const data = JSON.parse(event.data);
178
+
179
+ if (data.type === 'reload') {
180
+ console.log('%cšŸ”„ Reloading...', 'color: #f59e0b; font-weight: bold');
181
+ window.location.reload();
182
+ }
183
+
184
+ if (data.type === 'recompiling') {
185
+ console.log('%cāš™ļø Recompiling...', 'color: #3b82f6');
186
+ }
187
+
188
+ if (data.type === 'compiled') {
189
+ console.log('%cāœ… Compilation complete', 'color: #10b981');
190
+ }
191
+ };
192
+
193
+ ws.onerror = (error) => {
194
+ console.error('%cāŒ HMR connection error', 'color: #ef4444', error);
195
+ };
196
+
197
+ ws.onclose = () => {
198
+ console.log('%cāš ļø HMR disconnected. Refresh to reconnect.', 'color: #f59e0b');
199
+ };
200
+ </script>
201
+
202
+ <script type="module" src="/compiled/main.js"></script>
203
+ </body>
204
+ </html>`;
205
+
206
+ return html;
207
+ }
208
+
209
+ // File watcher setup
210
+ export function setupFileWatcher(root, compiledDir, clients, onRecompile) {
211
+ const srcDir = join(root, 'src');
212
+ const configPath = join(root, 'bertui.config.js');
213
+
214
+ if (!existsSync(srcDir)) {
215
+ logger.warn('src/ directory not found');
216
+ return () => {};
217
+ }
218
+
219
+ logger.debug(`šŸ‘€ Watching: ${srcDir}`);
220
+
221
+ let isRecompiling = false;
222
+ let recompileTimeout = null;
223
+ const watchedExtensions = ['.js', '.jsx', '.ts', '.tsx', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif'];
224
+
225
+ function notifyClients(message) {
226
+ for (const client of clients) {
227
+ try {
228
+ client.send(JSON.stringify(message));
229
+ } catch (e) {
230
+ clients.delete(client);
231
+ }
232
+ }
233
+ }
234
+
235
+ const watcher = watch(srcDir, { recursive: true }, async (eventType, filename) => {
236
+ if (!filename) return;
237
+
238
+ const ext = extname(filename);
239
+ if (!watchedExtensions.includes(ext)) return;
240
+
241
+ logger.debug(`šŸ“ File changed: ${filename}`);
242
+
243
+ clearTimeout(recompileTimeout);
244
+
245
+ recompileTimeout = setTimeout(async () => {
246
+ if (isRecompiling) return;
247
+
248
+ isRecompiling = true;
249
+ notifyClients({ type: 'recompiling' });
250
+
251
+ try {
252
+ await compileProject(root);
253
+
254
+ if (onRecompile) {
255
+ await onRecompile();
256
+ }
257
+
258
+ logger.success('āœ… Recompiled successfully');
259
+ notifyClients({ type: 'compiled' });
260
+
261
+ setTimeout(() => {
262
+ notifyClients({ type: 'reload' });
263
+ }, 100);
264
+
265
+ } catch (error) {
266
+ logger.error(`Recompilation failed: ${error.message}`);
267
+ } finally {
268
+ isRecompiling = false;
269
+ }
270
+ }, 150);
271
+ });
272
+
273
+ // Watch config file if it exists
274
+ let configWatcher = null;
275
+ if (existsSync(configPath)) {
276
+ configWatcher = watch(configPath, async (eventType) => {
277
+ if (eventType === 'change') {
278
+ logger.debug('šŸ“ Config changed, reloading...');
279
+ notifyClients({ type: 'reload' });
280
+ }
281
+ });
282
+ }
283
+
284
+ // Return cleanup function
285
+ return () => {
286
+ watcher.close();
287
+ if (configWatcher) configWatcher.close();
288
+ };
289
+ }