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/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
- const hmrClient = `
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,'&lt;')}</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().split('\n').slice(1).join('\n').trim() })) })
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 })