bimba-cli 0.4.8 → 0.5.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/.claude/agents/kfc/spec-design.md +158 -0
- package/.claude/agents/kfc/spec-impl.md +39 -0
- package/.claude/agents/kfc/spec-judge.md +125 -0
- package/.claude/agents/kfc/spec-requirements.md +123 -0
- package/.claude/agents/kfc/spec-system-prompt-loader.md +38 -0
- package/.claude/agents/kfc/spec-tasks.md +183 -0
- package/.claude/agents/kfc/spec-test.md +108 -0
- package/.claude/settings/kfc-settings.json +24 -0
- package/.claude/system-prompts/spec-workflow-starter.md +306 -0
- package/README.md +8 -0
- package/index.js +11 -1
- package/package.json +1 -1
- package/serve.js +116 -7
package/serve.js
CHANGED
|
@@ -5,7 +5,78 @@ import path from 'path'
|
|
|
5
5
|
import { theme } from './utils.js'
|
|
6
6
|
import { printerr } from './plugin.js'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// HMR client for CSS-only mode (injects styles without reload)
|
|
9
|
+
const hmrClientCssOnly = `
|
|
10
|
+
<script>
|
|
11
|
+
let _connected = false;
|
|
12
|
+
let _styleEl = null;
|
|
13
|
+
|
|
14
|
+
function connect() {
|
|
15
|
+
const ws = new WebSocket('ws://' + location.host + '/__hmr__');
|
|
16
|
+
ws.onopen = () => {
|
|
17
|
+
if (_connected) {
|
|
18
|
+
location.reload();
|
|
19
|
+
} else {
|
|
20
|
+
_connected = true;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
ws.onmessage = (e) => {
|
|
24
|
+
const data = JSON.parse(e.data);
|
|
25
|
+
if (data.type === 'css-update') {
|
|
26
|
+
if (!_styleEl) {
|
|
27
|
+
_styleEl = document.createElement('style');
|
|
28
|
+
_styleEl.id = '__bimba_hmr_css__';
|
|
29
|
+
document.head.appendChild(_styleEl);
|
|
30
|
+
}
|
|
31
|
+
_styleEl.textContent = data.css;
|
|
32
|
+
} else if (data.type === 'reload') {
|
|
33
|
+
location.reload();
|
|
34
|
+
} else if (data.type === 'error') {
|
|
35
|
+
showError(data.file, data.errors);
|
|
36
|
+
} else if (data.type === 'clear-error') {
|
|
37
|
+
clearError();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
ws.onclose = () => {
|
|
41
|
+
setTimeout(connect, 1000);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function showError(file, errors) {
|
|
46
|
+
let overlay = document.getElementById('__bimba_error__');
|
|
47
|
+
if (!overlay) {
|
|
48
|
+
overlay = document.createElement('div');
|
|
49
|
+
overlay.id = '__bimba_error__';
|
|
50
|
+
overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;font-family:monospace;padding:24px;box-sizing:border-box';
|
|
51
|
+
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
|
52
|
+
document.body.appendChild(overlay);
|
|
53
|
+
}
|
|
54
|
+
overlay.innerHTML = \`
|
|
55
|
+
<div style="background:#1a1a1a;border:1px solid #ff4444;border-radius:8px;max-width:860px;width:100%;max-height:90vh;overflow:auto;box-shadow:0 0 40px rgba(255,68,68,.3)">
|
|
56
|
+
<div style="background:#ff4444;color:#fff;padding:10px 16px;font-size:13px;font-weight:600;display:flex;justify-content:space-between;align-items:center">
|
|
57
|
+
<span>Compile error — \${file}</span>
|
|
58
|
+
<span onclick="document.getElementById('__bimba_error__').remove()" style="cursor:pointer;opacity:.7;font-size:16px">✕</span>
|
|
59
|
+
</div>
|
|
60
|
+
\${errors.map(err => \`
|
|
61
|
+
<div style="padding:16px;border-bottom:1px solid #333">
|
|
62
|
+
<div style="color:#ff8080;font-size:13px;margin-bottom:10px">\${err.message}\${err.line ? \` <span style="color:#888">line \${err.line}</span>\` : ''}</div>
|
|
63
|
+
\${err.snippet ? \`<pre style="margin:0;padding:10px;background:#111;border-radius:4px;font-size:12px;line-height:1.6;color:#ccc;overflow-x:auto;white-space:pre">\${err.snippet.replace(/</g,'<')}</pre>\` : ''}
|
|
64
|
+
</div>
|
|
65
|
+
\`).join('')}
|
|
66
|
+
</div>
|
|
67
|
+
\`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function clearError() {
|
|
71
|
+
const overlay = document.getElementById('__bimba_error__');
|
|
72
|
+
if (overlay) overlay.remove();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
connect();
|
|
76
|
+
</script>`
|
|
77
|
+
|
|
78
|
+
// Full HMR client (swaps component prototypes and resets elements)
|
|
79
|
+
const hmrClientFull = `
|
|
9
80
|
<script>
|
|
10
81
|
const _originalDefine = customElements.define.bind(customElements);
|
|
11
82
|
const _registry = new Map();
|
|
@@ -113,6 +184,8 @@ const hmrClient = `
|
|
|
113
184
|
</script>`
|
|
114
185
|
|
|
115
186
|
const _compileCache = new Map()
|
|
187
|
+
// Store previous versions to detect what changed
|
|
188
|
+
const _versionHistory = new Map()
|
|
116
189
|
|
|
117
190
|
async function compileFile(filepath) {
|
|
118
191
|
const file = Bun.file(filepath)
|
|
@@ -122,8 +195,25 @@ async function compileFile(filepath) {
|
|
|
122
195
|
if (cached && cached.mtime === mtime) return { ...cached.result, cached: true }
|
|
123
196
|
const code = await file.text()
|
|
124
197
|
const result = compiler.compile(code, { sourcePath: filepath, platform: 'browser', sourcemap: 'inline' })
|
|
198
|
+
|
|
199
|
+
// Track what changed compared to previous version
|
|
200
|
+
const prev = _versionHistory.get(filepath)
|
|
201
|
+
let changeType = 'full' // default
|
|
202
|
+
if (prev) {
|
|
203
|
+
const cssChanged = prev.css !== result.css
|
|
204
|
+
const jsChanged = prev.js !== result.js
|
|
205
|
+
if (cssChanged && !jsChanged) {
|
|
206
|
+
changeType = 'css-only'
|
|
207
|
+
} else if (jsChanged) {
|
|
208
|
+
changeType = 'full'
|
|
209
|
+
} else {
|
|
210
|
+
changeType = 'none'
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_versionHistory.set(filepath, { css: result.css, js: result.js })
|
|
125
215
|
_compileCache.set(filepath, { mtime, result })
|
|
126
|
-
return result
|
|
216
|
+
return { ...result, changeType }
|
|
127
217
|
}
|
|
128
218
|
|
|
129
219
|
function findHtml(flagHtml) {
|
|
@@ -165,12 +255,14 @@ async function buildImportMap() {
|
|
|
165
255
|
// - removes existing importmap block
|
|
166
256
|
// - removes <script data-bimba> from its position
|
|
167
257
|
// - injects importmap + entrypoint script + HMR client before </head>
|
|
168
|
-
function transformHtml(html, entrypoint, importMapTag) {
|
|
258
|
+
function transformHtml(html, entrypoint, importMapTag, hmrMode) {
|
|
169
259
|
html = html.replace(/<script\s+type=["']importmap["'][^>]*>[\s\S]*?<\/script>/gi, '');
|
|
170
260
|
html = html.replace(/<script([^>]*)\bdata-entrypoint\b([^>]*)><\/script>/gi, '');
|
|
171
261
|
|
|
172
262
|
const entryUrl = '/' + entrypoint.replace(/^\.\//, '').replaceAll('\\', '/');
|
|
173
263
|
const entryScript = `\t\t<script type='module' src='${entryUrl}'></script>`;
|
|
264
|
+
|
|
265
|
+
const hmrClient = hmrMode === 'full' ? hmrClientFull : hmrClientCssOnly;
|
|
174
266
|
|
|
175
267
|
html = html.replace('</head>', `${importMapTag}\n${entryScript}\n${hmrClient}\n\t</head>`);
|
|
176
268
|
return html;
|
|
@@ -183,6 +275,7 @@ export function serve(entrypoint, flags) {
|
|
|
183
275
|
const srcDir = path.dirname(entrypoint)
|
|
184
276
|
const sockets = new Set()
|
|
185
277
|
let importMapTag = null
|
|
278
|
+
const hmrMode = flags.hmrMode || 'css'
|
|
186
279
|
|
|
187
280
|
let _fadeTimers = []
|
|
188
281
|
let _fadeId = 0
|
|
@@ -255,7 +348,7 @@ export function serve(entrypoint, flags) {
|
|
|
255
348
|
const htmlFile = pathname === '/' ? htmlPath : '.' + pathname
|
|
256
349
|
let html = await Bun.file(htmlFile).text()
|
|
257
350
|
if (!importMapTag) importMapTag = await buildImportMap()
|
|
258
|
-
html = transformHtml(html, entrypoint, importMapTag)
|
|
351
|
+
html = transformHtml(html, entrypoint, importMapTag, hmrMode)
|
|
259
352
|
return new Response(html, { headers: { 'Content-Type': 'text/html' } })
|
|
260
353
|
}
|
|
261
354
|
|
|
@@ -266,7 +359,7 @@ export function serve(entrypoint, flags) {
|
|
|
266
359
|
if (out.errors?.length) {
|
|
267
360
|
if (!out.cached) {
|
|
268
361
|
printStatus(file, 'fail', out.errors)
|
|
269
|
-
const payload = JSON.stringify({ type: 'error', file, errors: out.errors.map(e => ({ message: e.message, line: e.range?.start?.line, snippet: e.toSnippet()
|
|
362
|
+
const payload = JSON.stringify({ type: 'error', file, errors: out.errors.map(e => ({ message: e.message, line: e.range?.start?.line, snippet: e.toSnippet() })) })
|
|
270
363
|
for (const socket of sockets) socket.send(payload)
|
|
271
364
|
}
|
|
272
365
|
return new Response(out.errors.map(e => e.message).join('\n'), { status: 500 })
|
|
@@ -274,12 +367,28 @@ export function serve(entrypoint, flags) {
|
|
|
274
367
|
if (!out.cached) {
|
|
275
368
|
printStatus(file, 'ok')
|
|
276
369
|
for (const socket of sockets) socket.send(JSON.stringify({ type: 'clear-error' }))
|
|
370
|
+
|
|
371
|
+
// Send appropriate update type based on HMR mode and what changed
|
|
372
|
+
if (hmrMode === 'full') {
|
|
373
|
+
// Full mode: send 'update' for prototype swapping (CSS is included in JS via runtime)
|
|
374
|
+
for (const socket of sockets) socket.send(JSON.stringify({ type: 'update', file }))
|
|
375
|
+
} else {
|
|
376
|
+
// CSS-only mode
|
|
377
|
+
if (out.changeType === 'css-only') {
|
|
378
|
+
// Only CSS changed: inject styles without reload
|
|
379
|
+
for (const socket of sockets) socket.send(JSON.stringify({ type: 'css-update', file, css: out.css }))
|
|
380
|
+
} else if (out.changeType === 'full') {
|
|
381
|
+
// JS changed in CSS-only mode: full page reload
|
|
382
|
+
for (const socket of sockets) socket.send(JSON.stringify({ type: 'reload' }))
|
|
383
|
+
}
|
|
384
|
+
// 'none' change type: do nothing, already up-to-date
|
|
385
|
+
}
|
|
277
386
|
}
|
|
278
387
|
return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
|
|
279
388
|
} catch (e) {
|
|
280
389
|
const file = pathname.replace(/^\//, '')
|
|
281
390
|
printStatus(file, 'fail', [{ message: e.message }])
|
|
282
|
-
const payload = JSON.stringify({ type: 'error', file, errors: [{ message: e.message }] })
|
|
391
|
+
const payload = JSON.stringify({ type: 'error', file, errors: [{ message: e.message, snippet: e.stack || e.message }] })
|
|
283
392
|
for (const socket of sockets) socket.send(payload)
|
|
284
393
|
return new Response(e.message, { status: 500 })
|
|
285
394
|
}
|
|
@@ -296,7 +405,7 @@ export function serve(entrypoint, flags) {
|
|
|
296
405
|
if (!lastSegment.includes('.')) {
|
|
297
406
|
let html = await Bun.file(htmlPath).text()
|
|
298
407
|
if (!importMapTag) importMapTag = await buildImportMap()
|
|
299
|
-
html = transformHtml(html, entrypoint, importMapTag)
|
|
408
|
+
html = transformHtml(html, entrypoint, importMapTag, hmrMode)
|
|
300
409
|
return new Response(html, { headers: { 'Content-Type': 'text/html' } })
|
|
301
410
|
}
|
|
302
411
|
return new Response('Not Found', { status: 404 })
|