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.
- package/README.md +165 -293
- package/index.js +70 -31
- package/package.json +47 -29
- package/src/build/image-optimizer.js +102 -51
- package/src/client/compiler.js +46 -3
- package/src/client/fast-refresh.js +72 -0
- package/src/client/hmr-runtime.js +59 -0
- package/src/compiler/index.js +25 -0
- package/src/compiler/router-generator-pure.js +104 -0
- package/src/compiler/transform.js +149 -0
- package/src/config/index.js +2 -0
- package/src/css/index.js +46 -0
- package/src/css/processor.js +127 -0
- package/src/image-optimizer/index.js +103 -0
- package/src/images/index.js +102 -0
- package/src/images/processor.js +169 -0
- package/src/router/SSRRouter.js +1 -1
- package/src/router/index.js +3 -0
- package/src/server/dev-handler.js +254 -0
- package/src/server/dev-server-utils.js +289 -0
- package/src/server/dev-server.js +10 -456
- package/src/server/hmr-handler.js +148 -0
- package/src/server/index.js +3 -0
- package/src/server/request-handler.js +36 -0
- package/src/server-islands/extractor.js +198 -0
- package/src/server-islands/index.js +59 -0
- package/src/utils/index.js +11 -0
|
@@ -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
|
+
}
|