bertui 1.2.1 → 1.2.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 +152 -197
- package/index.js +16 -8
- package/package.json +1 -1
- package/src/build/compiler/file-transpiler.js +92 -141
- package/src/build/compiler/index.js +23 -15
- package/src/build.js +147 -92
- package/src/client/compiler.js +169 -157
- package/src/config/defaultConfig.js +13 -4
- package/src/config/loadConfig.js +47 -32
- package/src/dev.js +222 -49
- package/src/logger/logger.js +294 -16
- package/src/server/dev-handler.js +11 -0
- package/src/server/dev-server-utils.js +262 -160
- package/src/utils/importhow.js +52 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
// bertui/src/server/dev-server-utils.js
|
|
1
|
+
// bertui/src/server/dev-server-utils.js
|
|
2
2
|
import { join, extname } from 'path';
|
|
3
|
-
import { existsSync, readdirSync, watch } from 'fs';
|
|
3
|
+
import { existsSync, readdirSync, watch, statSync } from 'fs';
|
|
4
4
|
import logger from '../logger/logger.js';
|
|
5
5
|
import { compileProject } from '../client/compiler.js';
|
|
6
|
-
import { globalCache } from '../utils/cache.js';
|
|
6
|
+
import { globalCache } from '../utils/cache.js';
|
|
7
7
|
|
|
8
8
|
// Image content type mapping
|
|
9
9
|
export function getImageContentType(ext) {
|
|
10
10
|
const types = {
|
|
11
|
-
'.jpg':
|
|
11
|
+
'.jpg': 'image/jpeg',
|
|
12
12
|
'.jpeg': 'image/jpeg',
|
|
13
|
-
'.png':
|
|
14
|
-
'.gif':
|
|
15
|
-
'.svg':
|
|
13
|
+
'.png': 'image/png',
|
|
14
|
+
'.gif': 'image/gif',
|
|
15
|
+
'.svg': 'image/svg+xml',
|
|
16
16
|
'.webp': 'image/webp',
|
|
17
17
|
'.avif': 'image/avif',
|
|
18
|
-
'.ico':
|
|
18
|
+
'.ico': 'image/x-icon',
|
|
19
19
|
};
|
|
20
20
|
return types[ext] || 'application/octet-stream';
|
|
21
21
|
}
|
|
@@ -23,278 +23,380 @@ export function getImageContentType(ext) {
|
|
|
23
23
|
// General content type mapping
|
|
24
24
|
export function getContentType(ext) {
|
|
25
25
|
const types = {
|
|
26
|
-
'.js':
|
|
27
|
-
'.jsx':
|
|
28
|
-
'.css':
|
|
26
|
+
'.js': 'application/javascript',
|
|
27
|
+
'.jsx': 'application/javascript',
|
|
28
|
+
'.css': 'text/css',
|
|
29
29
|
'.html': 'text/html',
|
|
30
30
|
'.json': 'application/json',
|
|
31
|
-
'.png':
|
|
32
|
-
'.jpg':
|
|
31
|
+
'.png': 'image/png',
|
|
32
|
+
'.jpg': 'image/jpeg',
|
|
33
33
|
'.jpeg': 'image/jpeg',
|
|
34
|
-
'.gif':
|
|
35
|
-
'.svg':
|
|
34
|
+
'.gif': 'image/gif',
|
|
35
|
+
'.svg': 'image/svg+xml',
|
|
36
36
|
'.webp': 'image/webp',
|
|
37
37
|
'.avif': 'image/avif',
|
|
38
|
-
'.ico':
|
|
38
|
+
'.ico': 'image/x-icon',
|
|
39
39
|
'.woff': 'font/woff',
|
|
40
|
-
'.woff2':
|
|
41
|
-
'.ttf':
|
|
42
|
-
'.otf':
|
|
43
|
-
'.mp4':
|
|
40
|
+
'.woff2':'font/woff2',
|
|
41
|
+
'.ttf': 'font/ttf',
|
|
42
|
+
'.otf': 'font/otf',
|
|
43
|
+
'.mp4': 'video/mp4',
|
|
44
44
|
'.webm': 'video/webm',
|
|
45
|
-
'.mp3':
|
|
45
|
+
'.mp3': 'audio/mpeg',
|
|
46
46
|
};
|
|
47
47
|
return types[ext] || 'text/plain';
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// Import map builder — scans node_modules at runtime so newly installed
|
|
52
|
+
// packages are picked up without restarting the dev server.
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
// Cached importmap + the package.json mtime it was built from
|
|
56
|
+
let _cachedImportMap = null;
|
|
57
|
+
let _cachedPkgMtime = null;
|
|
58
|
+
|
|
59
|
+
export async function buildDevImportMap(root) {
|
|
60
|
+
const pkgJsonPath = join(root, 'package.json');
|
|
61
|
+
const nodeModulesDir = join(root, 'node_modules');
|
|
62
|
+
|
|
63
|
+
// Invalidate cache when package.json changes (new install happened)
|
|
64
|
+
let currentMtime = null;
|
|
65
|
+
try {
|
|
66
|
+
currentMtime = statSync(pkgJsonPath).mtimeMs;
|
|
67
|
+
} catch { /* package.json missing — fine */ }
|
|
68
|
+
|
|
69
|
+
if (_cachedImportMap && currentMtime === _cachedPkgMtime) {
|
|
70
|
+
return _cachedImportMap;
|
|
59
71
|
}
|
|
60
|
-
|
|
72
|
+
|
|
73
|
+
logger.info('🔄 Rebuilding dev import map (new packages detected)...');
|
|
74
|
+
|
|
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
|
+
'react/jsx-runtime': 'https://esm.sh/react@18.2.0/jsx-runtime',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const SKIP = new Set(['react', 'react-dom', '.bin', '.cache', '.package-lock.json', '.yarn']);
|
|
83
|
+
|
|
84
|
+
if (existsSync(nodeModulesDir)) {
|
|
85
|
+
try {
|
|
86
|
+
const packages = readdirSync(nodeModulesDir);
|
|
87
|
+
|
|
88
|
+
for (const pkg of packages) {
|
|
89
|
+
if (SKIP.has(pkg) || pkg.startsWith('.') || pkg.startsWith('_')) continue;
|
|
90
|
+
|
|
91
|
+
// Handle scoped packages (@org/pkg)
|
|
92
|
+
let pkgNames = [pkg];
|
|
93
|
+
if (pkg.startsWith('@')) {
|
|
94
|
+
const scopeDir = join(nodeModulesDir, pkg);
|
|
95
|
+
try {
|
|
96
|
+
if (statSync(scopeDir).isDirectory()) {
|
|
97
|
+
pkgNames = readdirSync(scopeDir).map(sub => `${pkg}/${sub}`);
|
|
98
|
+
}
|
|
99
|
+
} catch { continue; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const pkgName of pkgNames) {
|
|
103
|
+
const pkgDir = join(nodeModulesDir, pkgName);
|
|
104
|
+
const pkgJsonFile = join(pkgDir, 'package.json');
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
if (!statSync(pkgDir).isDirectory()) continue;
|
|
108
|
+
} catch { continue; }
|
|
109
|
+
|
|
110
|
+
if (!existsSync(pkgJsonFile)) continue;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const pkgJson = JSON.parse(await Bun.file(pkgJsonFile).text());
|
|
114
|
+
|
|
115
|
+
const possibleEntries = [
|
|
116
|
+
pkgJson.module,
|
|
117
|
+
pkgJson.browser,
|
|
118
|
+
pkgJson.main,
|
|
119
|
+
'dist/index.js',
|
|
120
|
+
'lib/index.js',
|
|
121
|
+
'index.js',
|
|
122
|
+
].filter(Boolean);
|
|
123
|
+
|
|
124
|
+
for (const entry of possibleEntries) {
|
|
125
|
+
const fullPath = join(pkgDir, entry);
|
|
126
|
+
if (existsSync(fullPath)) {
|
|
127
|
+
importMap[pkgName] = `/node_modules/${pkgName}/${entry}`;
|
|
128
|
+
logger.debug(`📦 Dev map: ${pkgName} → ${importMap[pkgName]}`);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch { continue; }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
logger.warn(`Failed to scan node_modules: ${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_cachedImportMap = importMap;
|
|
141
|
+
_cachedPkgMtime = currentMtime;
|
|
142
|
+
|
|
143
|
+
logger.success(`✅ Import map ready (${Object.keys(importMap).length} packages)`);
|
|
144
|
+
return importMap;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
148
|
+
// HTML generator — uses the live importmap so newly installed packages
|
|
149
|
+
// appear in the next page load without a server restart.
|
|
150
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
151
|
+
export async function serveHTML(root, hasRouter, config, port) {
|
|
152
|
+
// Don't cache HTML anymore — importmap can change between requests
|
|
61
153
|
const meta = config.meta || {};
|
|
62
|
-
|
|
154
|
+
|
|
63
155
|
const srcStylesDir = join(root, 'src', 'styles');
|
|
64
156
|
let userStylesheets = '';
|
|
65
|
-
|
|
157
|
+
|
|
66
158
|
if (existsSync(srcStylesDir)) {
|
|
67
159
|
try {
|
|
68
160
|
const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
|
|
69
|
-
userStylesheets = cssFiles
|
|
161
|
+
userStylesheets = cssFiles
|
|
162
|
+
.map(f => ` <link rel="stylesheet" href="/styles/${f}">`)
|
|
163
|
+
.join('\n');
|
|
70
164
|
} catch (error) {
|
|
71
165
|
logger.warn(`Could not read styles directory: ${error.message}`);
|
|
72
166
|
}
|
|
73
167
|
}
|
|
74
|
-
|
|
168
|
+
|
|
75
169
|
// Auto-detect bertui-animate CSS
|
|
76
170
|
let bertuiAnimateStylesheet = '';
|
|
77
171
|
const bertuiAnimatePath = join(root, 'node_modules/bertui-animate/dist/bertui-animate.min.css');
|
|
78
172
|
if (existsSync(bertuiAnimatePath)) {
|
|
79
173
|
bertuiAnimateStylesheet = ' <link rel="stylesheet" href="/bertui-animate.css">';
|
|
80
174
|
}
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
const importMap =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client"
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// Auto-detect bertui-* JavaScript packages
|
|
90
|
-
const nodeModulesDir = join(root, 'node_modules');
|
|
91
|
-
|
|
92
|
-
if (existsSync(nodeModulesDir)) {
|
|
93
|
-
try {
|
|
94
|
-
const packages = readdirSync(nodeModulesDir);
|
|
95
|
-
|
|
96
|
-
for (const pkg of packages) {
|
|
97
|
-
if (!pkg.startsWith('bertui-')) continue;
|
|
98
|
-
|
|
99
|
-
const pkgDir = join(nodeModulesDir, pkg);
|
|
100
|
-
const pkgJsonPath = join(pkgDir, 'package.json');
|
|
101
|
-
|
|
102
|
-
if (!existsSync(pkgJsonPath)) continue;
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const pkgJsonContent = await Bun.file(pkgJsonPath).text();
|
|
106
|
-
const pkgJson = JSON.parse(pkgJsonContent);
|
|
107
|
-
|
|
108
|
-
let mainFile = null;
|
|
109
|
-
|
|
110
|
-
if (pkgJson.exports) {
|
|
111
|
-
const rootExport = pkgJson.exports['.'];
|
|
112
|
-
if (typeof rootExport === 'string') {
|
|
113
|
-
mainFile = rootExport;
|
|
114
|
-
} else if (typeof rootExport === 'object') {
|
|
115
|
-
mainFile = rootExport.browser || rootExport.default || rootExport.import;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (!mainFile) {
|
|
120
|
-
mainFile = pkgJson.main || 'index.js';
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const fullPath = join(pkgDir, mainFile);
|
|
124
|
-
if (existsSync(fullPath)) {
|
|
125
|
-
importMap[pkg] = `/node_modules/${pkg}/${mainFile}`;
|
|
126
|
-
logger.debug(`✅ ${pkg} available`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
} catch (error) {
|
|
130
|
-
logger.warn(`⚠️ Failed to parse ${pkg}/package.json: ${error.message}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
} catch (error) {
|
|
134
|
-
logger.warn(`Failed to scan node_modules: ${error.message}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const html = `<!DOCTYPE html>
|
|
175
|
+
|
|
176
|
+
// ✅ Always get the latest importmap (cached until package.json changes)
|
|
177
|
+
const importMap = await buildDevImportMap(root);
|
|
178
|
+
|
|
179
|
+
return `<!DOCTYPE html>
|
|
139
180
|
<html lang="${meta.lang || 'en'}">
|
|
140
181
|
<head>
|
|
141
182
|
<meta charset="UTF-8">
|
|
142
183
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
143
184
|
<title>${meta.title || 'BertUI App'}</title>
|
|
144
|
-
|
|
145
|
-
${meta.description
|
|
146
|
-
${meta.keywords
|
|
147
|
-
${meta.author
|
|
148
|
-
${meta.themeColor
|
|
149
|
-
|
|
150
|
-
${meta.ogTitle
|
|
185
|
+
|
|
186
|
+
${meta.description ? `<meta name="description" content="${meta.description}">` : ''}
|
|
187
|
+
${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
|
|
188
|
+
${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
|
|
189
|
+
${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
|
|
190
|
+
|
|
191
|
+
${meta.ogTitle ? `<meta property="og:title" content="${meta.ogTitle || meta.title}">` : ''}
|
|
151
192
|
${meta.ogDescription ? `<meta property="og:description" content="${meta.ogDescription || meta.description}">` : ''}
|
|
152
|
-
${meta.ogImage
|
|
153
|
-
|
|
193
|
+
${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
|
|
194
|
+
|
|
154
195
|
<link rel="icon" type="image/svg+xml" href="/public/favicon.svg">
|
|
155
|
-
|
|
196
|
+
|
|
156
197
|
${userStylesheets}
|
|
157
198
|
${bertuiAnimateStylesheet}
|
|
158
|
-
|
|
199
|
+
|
|
159
200
|
<script type="importmap">
|
|
160
201
|
${JSON.stringify({ imports: importMap }, null, 2)}
|
|
161
202
|
</script>
|
|
162
|
-
|
|
203
|
+
|
|
163
204
|
<style>
|
|
164
|
-
* {
|
|
165
|
-
|
|
166
|
-
padding: 0;
|
|
167
|
-
box-sizing: border-box;
|
|
168
|
-
}
|
|
169
|
-
body {
|
|
170
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
171
|
-
}
|
|
205
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
206
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
|
|
172
207
|
</style>
|
|
173
208
|
</head>
|
|
174
209
|
<body>
|
|
175
210
|
<div id="root"></div>
|
|
176
|
-
|
|
211
|
+
|
|
177
212
|
<script type="module">
|
|
178
213
|
const ws = new WebSocket('ws://localhost:${port}/__hmr');
|
|
179
|
-
|
|
214
|
+
|
|
180
215
|
ws.onopen = () => {
|
|
181
216
|
console.log('%c🔥 BertUI HMR connected', 'color: #10b981; font-weight: bold');
|
|
182
217
|
};
|
|
183
|
-
|
|
218
|
+
|
|
184
219
|
ws.onmessage = (event) => {
|
|
185
220
|
const data = JSON.parse(event.data);
|
|
186
|
-
|
|
221
|
+
|
|
187
222
|
if (data.type === 'reload') {
|
|
188
223
|
console.log('%c🔄 Reloading...', 'color: #f59e0b; font-weight: bold');
|
|
224
|
+
if (window.__BERTUI_HIDE_ERROR__) window.__BERTUI_HIDE_ERROR__();
|
|
189
225
|
window.location.reload();
|
|
190
226
|
}
|
|
191
|
-
|
|
227
|
+
|
|
192
228
|
if (data.type === 'recompiling') {
|
|
193
229
|
console.log('%c⚙️ Recompiling...', 'color: #3b82f6');
|
|
194
230
|
}
|
|
195
|
-
|
|
231
|
+
|
|
196
232
|
if (data.type === 'compiled') {
|
|
197
233
|
console.log('%c✅ Compilation complete', 'color: #10b981');
|
|
234
|
+
if (window.__BERTUI_HIDE_ERROR__) window.__BERTUI_HIDE_ERROR__();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ✅ New: server tells browser the importmap changed → full reload
|
|
238
|
+
if (data.type === 'importmap-updated') {
|
|
239
|
+
console.log('%c📦 New packages detected — reloading...', 'color: #8b5cf6; font-weight: bold');
|
|
240
|
+
window.location.reload();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (data.type === 'compilation-error') {
|
|
244
|
+
if (window.__BERTUI_SHOW_ERROR__) {
|
|
245
|
+
window.__BERTUI_SHOW_ERROR__({
|
|
246
|
+
type: 'Compilation Error',
|
|
247
|
+
message: data.message,
|
|
248
|
+
stack: data.stack,
|
|
249
|
+
file: data.file,
|
|
250
|
+
line: data.line,
|
|
251
|
+
column: data.column,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
198
254
|
}
|
|
199
255
|
};
|
|
200
|
-
|
|
256
|
+
|
|
201
257
|
ws.onerror = (error) => {
|
|
202
258
|
console.error('%c❌ HMR connection error', 'color: #ef4444', error);
|
|
203
259
|
};
|
|
204
|
-
|
|
260
|
+
|
|
205
261
|
ws.onclose = () => {
|
|
206
262
|
console.log('%c⚠️ HMR disconnected. Refresh to reconnect.', 'color: #f59e0b');
|
|
207
263
|
};
|
|
208
264
|
</script>
|
|
209
|
-
|
|
265
|
+
|
|
266
|
+
<script src="/error-overlay.js"></script>
|
|
210
267
|
<script type="module" src="/compiled/main.js"></script>
|
|
211
268
|
</body>
|
|
212
269
|
</html>`;
|
|
213
|
-
|
|
214
|
-
// Cache the HTML
|
|
215
|
-
globalCache.set(cacheKey, html, { ttl: 1000 });
|
|
216
|
-
|
|
217
|
-
return html;
|
|
218
270
|
}
|
|
219
271
|
|
|
220
|
-
//
|
|
272
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
273
|
+
// File watcher — watches src/ for code changes AND package.json for
|
|
274
|
+
// new installs. When package.json changes it invalidates the importmap
|
|
275
|
+
// cache and sends an `importmap-updated` message so the browser reloads
|
|
276
|
+
// with the new packages — no server restart needed.
|
|
277
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
221
278
|
export function setupFileWatcher(root, compiledDir, clients, onRecompile) {
|
|
222
|
-
const srcDir
|
|
279
|
+
const srcDir = join(root, 'src');
|
|
280
|
+
const pkgJson = join(root, 'package.json');
|
|
223
281
|
const configPath = join(root, 'bertui.config.js');
|
|
224
|
-
|
|
282
|
+
|
|
225
283
|
if (!existsSync(srcDir)) {
|
|
226
284
|
logger.warn('src/ directory not found');
|
|
227
285
|
return () => {};
|
|
228
286
|
}
|
|
229
|
-
|
|
287
|
+
|
|
230
288
|
logger.debug(`👀 Watching: ${srcDir}`);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
let
|
|
234
|
-
|
|
235
|
-
|
|
289
|
+
logger.debug(`👀 Watching: ${pkgJson} (package installs)`);
|
|
290
|
+
|
|
291
|
+
let isRecompiling = false;
|
|
292
|
+
let recompileTimeout = null;
|
|
293
|
+
const watchedExtensions = [
|
|
294
|
+
'.js', '.jsx', '.ts', '.tsx', '.css',
|
|
295
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif',
|
|
296
|
+
];
|
|
297
|
+
|
|
236
298
|
function notifyClients(message) {
|
|
237
299
|
for (const client of clients) {
|
|
238
300
|
try {
|
|
239
301
|
client.send(JSON.stringify(message));
|
|
240
|
-
} catch
|
|
302
|
+
} catch {
|
|
241
303
|
clients.delete(client);
|
|
242
304
|
}
|
|
243
305
|
}
|
|
244
306
|
}
|
|
245
|
-
|
|
246
|
-
|
|
307
|
+
|
|
308
|
+
// ── Source file watcher ──────────────────────────────────────────────────
|
|
309
|
+
const srcWatcher = watch(srcDir, { recursive: true }, async (eventType, filename) => {
|
|
247
310
|
if (!filename) return;
|
|
248
|
-
|
|
249
311
|
const ext = extname(filename);
|
|
250
312
|
if (!watchedExtensions.includes(ext)) return;
|
|
251
|
-
|
|
313
|
+
|
|
252
314
|
logger.debug(`📝 File changed: ${filename}`);
|
|
253
|
-
|
|
254
315
|
clearTimeout(recompileTimeout);
|
|
255
|
-
|
|
316
|
+
|
|
256
317
|
recompileTimeout = setTimeout(async () => {
|
|
257
318
|
if (isRecompiling) return;
|
|
258
|
-
|
|
259
319
|
isRecompiling = true;
|
|
260
320
|
notifyClients({ type: 'recompiling' });
|
|
261
|
-
|
|
321
|
+
|
|
262
322
|
try {
|
|
263
323
|
await compileProject(root);
|
|
264
|
-
|
|
265
|
-
if (onRecompile) {
|
|
266
|
-
await onRecompile();
|
|
267
|
-
}
|
|
268
|
-
|
|
324
|
+
if (onRecompile) await onRecompile();
|
|
269
325
|
logger.success('✅ Recompiled successfully');
|
|
270
326
|
notifyClients({ type: 'compiled' });
|
|
271
|
-
|
|
272
|
-
setTimeout(() => {
|
|
273
|
-
notifyClients({ type: 'reload' });
|
|
274
|
-
}, 100);
|
|
275
|
-
|
|
327
|
+
setTimeout(() => notifyClients({ type: 'reload' }), 100);
|
|
276
328
|
} catch (error) {
|
|
277
329
|
logger.error(`Recompilation failed: ${error.message}`);
|
|
330
|
+
notifyClients({
|
|
331
|
+
type: 'compilation-error',
|
|
332
|
+
message: error.message,
|
|
333
|
+
stack: error.stack || null,
|
|
334
|
+
file: error.file || null,
|
|
335
|
+
line: error.line || null,
|
|
336
|
+
column: error.column || null,
|
|
337
|
+
});
|
|
278
338
|
} finally {
|
|
279
339
|
isRecompiling = false;
|
|
280
340
|
}
|
|
281
341
|
}, 150);
|
|
282
342
|
});
|
|
283
|
-
|
|
284
|
-
//
|
|
343
|
+
|
|
344
|
+
// ── package.json watcher — detects new npm/bun installs ─────────────────
|
|
345
|
+
let pkgWatcher = null;
|
|
346
|
+
let lastPkgMtime = null;
|
|
347
|
+
|
|
348
|
+
if (existsSync(pkgJson)) {
|
|
349
|
+
try {
|
|
350
|
+
lastPkgMtime = statSync(pkgJson).mtimeMs;
|
|
351
|
+
} catch { /* ignore */ }
|
|
352
|
+
|
|
353
|
+
pkgWatcher = watch(pkgJson, async (eventType) => {
|
|
354
|
+
if (eventType !== 'change') return;
|
|
355
|
+
|
|
356
|
+
// Debounce — installs can trigger multiple change events
|
|
357
|
+
clearTimeout(pkgWatcher._debounce);
|
|
358
|
+
pkgWatcher._debounce = setTimeout(async () => {
|
|
359
|
+
try {
|
|
360
|
+
const newMtime = statSync(pkgJson).mtimeMs;
|
|
361
|
+
if (newMtime === lastPkgMtime) return; // spurious event
|
|
362
|
+
lastPkgMtime = newMtime;
|
|
363
|
+
|
|
364
|
+
logger.info('📦 package.json changed — refreshing import map...');
|
|
365
|
+
|
|
366
|
+
// Bust the importmap cache so next serveHTML call rebuilds it
|
|
367
|
+
_cachedImportMap = null;
|
|
368
|
+
_cachedPkgMtime = null;
|
|
369
|
+
|
|
370
|
+
// Wait briefly for node_modules to finish writing
|
|
371
|
+
await new Promise(r => setTimeout(r, 800));
|
|
372
|
+
|
|
373
|
+
// Rebuild and notify browser
|
|
374
|
+
await buildDevImportMap(root);
|
|
375
|
+
notifyClients({ type: 'importmap-updated' });
|
|
376
|
+
|
|
377
|
+
logger.success('✅ Import map updated — browser reloading');
|
|
378
|
+
} catch (err) {
|
|
379
|
+
logger.warn(`package.json watch error: ${err.message}`);
|
|
380
|
+
}
|
|
381
|
+
}, 500);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── bertui.config.js watcher ─────────────────────────────────────────────
|
|
285
386
|
let configWatcher = null;
|
|
286
387
|
if (existsSync(configPath)) {
|
|
287
|
-
configWatcher = watch(configPath,
|
|
388
|
+
configWatcher = watch(configPath, (eventType) => {
|
|
288
389
|
if (eventType === 'change') {
|
|
289
390
|
logger.debug('📝 Config changed, reloading...');
|
|
290
391
|
notifyClients({ type: 'reload' });
|
|
291
392
|
}
|
|
292
393
|
});
|
|
293
394
|
}
|
|
294
|
-
|
|
395
|
+
|
|
295
396
|
// Return cleanup function
|
|
296
397
|
return () => {
|
|
297
|
-
|
|
398
|
+
srcWatcher.close();
|
|
399
|
+
if (pkgWatcher) pkgWatcher.close();
|
|
298
400
|
if (configWatcher) configWatcher.close();
|
|
299
401
|
};
|
|
300
402
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// bertui/src/utils/importhow.js
|
|
2
|
+
import { join, relative, dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {Object} importhow - { alias: relPath } from bertui.config.js
|
|
6
|
+
* @param {string} projectRoot - absolute project root
|
|
7
|
+
* @param {string} compiledDir - if set, aliases resolve to compiledDir/<alias>
|
|
8
|
+
* pass .bertui/compiled in dev mode
|
|
9
|
+
* leave null in build mode (uses raw source paths)
|
|
10
|
+
*/
|
|
11
|
+
export function buildAliasMap(importhow = {}, projectRoot, compiledDir = null) {
|
|
12
|
+
const map = new Map();
|
|
13
|
+
for (const [alias, relPath] of Object.entries(importhow)) {
|
|
14
|
+
const abs = compiledDir
|
|
15
|
+
? join(compiledDir, alias) // dev: .bertui/compiled/amani
|
|
16
|
+
: join(projectRoot, relPath); // build: /project/src/components
|
|
17
|
+
map.set(alias, abs);
|
|
18
|
+
}
|
|
19
|
+
return map;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Rewrite alias import specifiers in compiled code.
|
|
24
|
+
* 'amani/button' → '../components/button.js'
|
|
25
|
+
*/
|
|
26
|
+
export function rewriteAliasImports(code, currentFile, aliasMap) {
|
|
27
|
+
if (!aliasMap || aliasMap.size === 0) return code;
|
|
28
|
+
|
|
29
|
+
const currentDir = dirname(currentFile);
|
|
30
|
+
const importRe = /(?:import|export)(?:\s+[\w*{},\s]+\s+from)?\s+['"]([^'"]+)['"]/g;
|
|
31
|
+
|
|
32
|
+
return code.replace(importRe, (match, specifier) => {
|
|
33
|
+
const slashIdx = specifier.indexOf('/');
|
|
34
|
+
const alias = slashIdx === -1 ? specifier : specifier.slice(0, slashIdx);
|
|
35
|
+
const rest = slashIdx === -1 ? '' : specifier.slice(slashIdx);
|
|
36
|
+
|
|
37
|
+
const absBase = aliasMap.get(alias);
|
|
38
|
+
if (!absBase) return match;
|
|
39
|
+
|
|
40
|
+
let rel = relative(currentDir, absBase + rest).replace(/\\/g, '/');
|
|
41
|
+
if (!rel.startsWith('.')) rel = './' + rel;
|
|
42
|
+
if (rest && !/\.\w+$/.test(rest)) rel += '.js';
|
|
43
|
+
|
|
44
|
+
return match.replace(`'${specifier}'`, `'${rel}'`).replace(`"${specifier}"`, `"${rel}"`);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getAliasDirs(aliasMap) {
|
|
49
|
+
const dirs = new Set();
|
|
50
|
+
for (const absPath of aliasMap.values()) dirs.add(absPath);
|
|
51
|
+
return dirs;
|
|
52
|
+
}
|