bimba-cli 0.5.11 → 0.5.13

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/serve.js +103 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/HeapVoid/bimba.git"
package/serve.js CHANGED
@@ -293,6 +293,68 @@ const _compileCache = new Map() // filepath → { mtime, result }
293
293
  const _prevJs = new Map() // filepath → compiled js — for change detection
294
294
  const _prevSlots = new Map() // filepath → previous symbol slot count
295
295
 
296
+ // ─── Import dependency graph ──────────────────────────────────────────────────
297
+ //
298
+ // When a non-tag module (utility functions, constants, shared state) is edited,
299
+ // the existing class-prototype patching does nothing for the modules that
300
+ // imported it — they hold their own captured references. To make those
301
+ // updates flow into the UI, we track who imports whom and, on every change,
302
+ // re-broadcast updates for the transitive importer set. The client's existing
303
+ // HMR queue then re-imports each in turn; their top-level code reruns, picks
304
+ // up the new symbols, and any tag re-registrations patch instances in place.
305
+ //
306
+ // Keys are absolute, normalized paths (path.resolve). Edges are added during
307
+ // compilation by scanning the produced JS for relative .imba imports.
308
+
309
+ const _imports = new Map() // absFile → Set<absFile> (what it imports)
310
+ const _importers = new Map() // absFile → Set<absFile> (who imports it)
311
+
312
+ function extractImports(js, fromAbs) {
313
+ const dir = path.dirname(fromAbs)
314
+ const out = new Set()
315
+ const re = /(?:^|[\s;])(?:import|from)\s*['"]([^'"]+)['"]/g
316
+ let m
317
+ while ((m = re.exec(js))) {
318
+ const spec = m[1]
319
+ if (!spec.startsWith('.') && !spec.startsWith('/')) continue
320
+ if (!spec.endsWith('.imba')) continue
321
+ const resolved = spec.startsWith('/')
322
+ ? path.resolve('.' + spec)
323
+ : path.resolve(dir, spec)
324
+ out.add(resolved)
325
+ }
326
+ return out
327
+ }
328
+
329
+ function updateImportGraph(fromAbs, newDeps) {
330
+ const old = _imports.get(fromAbs)
331
+ if (old) {
332
+ for (const d of old) {
333
+ if (newDeps.has(d)) continue
334
+ const set = _importers.get(d)
335
+ if (set) { set.delete(fromAbs); if (!set.size) _importers.delete(d) }
336
+ }
337
+ }
338
+ for (const d of newDeps) {
339
+ let set = _importers.get(d)
340
+ if (!set) { set = new Set(); _importers.set(d, set) }
341
+ set.add(fromAbs)
342
+ }
343
+ _imports.set(fromAbs, newDeps)
344
+ }
345
+
346
+ function transitiveImporters(absFile) {
347
+ const out = new Set()
348
+ const stack = [absFile]
349
+ while (stack.length) {
350
+ const cur = stack.pop()
351
+ const ups = _importers.get(cur)
352
+ if (!ups) continue
353
+ for (const u of ups) if (!out.has(u)) { out.add(u); stack.push(u) }
354
+ }
355
+ return out
356
+ }
357
+
296
358
  // Imba compiles tag render-cache slots as anonymous local Symbols at module top
297
359
  // level: `var $4 = Symbol(), $11 = Symbol(), ...; let c$0 = Symbol();`. Each
298
360
  // re-import of the file creates fresh Symbol objects, so old slot data on live
@@ -323,13 +385,27 @@ function stabilizeSymbols(js, filepath) {
323
385
  return { js: bootstrap + out, slotCount: count }
324
386
  }
325
387
 
388
+ // Imba's compile result puts `errors` on the prototype as a getter, so plain
389
+ // object spread (`{...result}`) silently strips it. We always normalize to a
390
+ // plain shape with `errors` as an own property — otherwise downstream callers
391
+ // see no errors and serve empty 200s for broken files.
392
+ function _normalizeResult(result, extras) {
393
+ return {
394
+ js: result.js,
395
+ errors: result.errors || [],
396
+ slots: result.slots,
397
+ ...extras,
398
+ }
399
+ }
400
+
326
401
  async function compileFile(filepath) {
402
+ const abs = path.resolve(filepath)
327
403
  const file = Bun.file(filepath)
328
404
  const stat = await file.stat()
329
405
  const mtime = stat.mtime.getTime()
330
406
 
331
- const cached = _compileCache.get(filepath)
332
- if (cached && cached.mtime === mtime) return { ...cached.result, changeType: 'cached', slots: cached.slots }
407
+ const cached = _compileCache.get(abs)
408
+ if (cached && cached.mtime === mtime) return _normalizeResult(cached.result, { changeType: 'cached' })
333
409
 
334
410
  const code = await file.text()
335
411
  const result = compiler.compile(code, {
@@ -338,18 +414,22 @@ async function compileFile(filepath) {
338
414
  sourcemap: 'inline',
339
415
  })
340
416
 
341
- if (!result.errors?.length && result.js) {
417
+ const errors = result.errors || []
418
+ if (!errors.length && result.js) {
342
419
  const { js, slotCount } = stabilizeSymbols(result.js, filepath)
343
420
  result.js = js
344
- const prev = _prevSlots.get(filepath)
421
+ const prev = _prevSlots.get(abs)
345
422
  result.slots = (prev === undefined || prev === slotCount) ? 'stable' : 'shifted'
346
- _prevSlots.set(filepath, slotCount)
423
+ _prevSlots.set(abs, slotCount)
424
+ updateImportGraph(abs, extractImports(js, abs))
347
425
  }
348
426
 
349
- const changeType = _prevJs.get(filepath) === result.js ? 'none' : 'full'
350
- _prevJs.set(filepath, result.js)
351
- _compileCache.set(filepath, { mtime, result, slots: result.slots })
352
- return { ...result, changeType }
427
+ // Bake errors as an own property so caching/spreading preserves them.
428
+ const baked = { js: result.js, errors, slots: result.slots }
429
+ const changeType = _prevJs.get(abs) === baked.js ? 'none' : 'full'
430
+ _prevJs.set(abs, baked.js)
431
+ _compileCache.set(abs, { mtime, result: baked })
432
+ return _normalizeResult(baked, { changeType })
353
433
  }
354
434
 
355
435
  // ─── HTML helpers ─────────────────────────────────────────────────────────────
@@ -505,6 +585,20 @@ export function serve(entrypoint, flags) {
505
585
  printStatus(rel, 'ok')
506
586
  broadcast({ type: 'clear-error' })
507
587
  broadcast({ type: 'update', file: rel, slots: out.slots || 'shifted' })
588
+
589
+ // Cascade: re-import every module transitively importing this file.
590
+ // They don't need recompilation (their source didn't change), but
591
+ // their captured references to the changed module are stale, so we
592
+ // tell the client to re-import them. The client's HMR queue
593
+ // processes these in order; tag classes get re-patched, plain
594
+ // utility modules get fresh top-level state.
595
+ const ups = transitiveImporters(path.resolve(filepath))
596
+ for (const upAbs of ups) {
597
+ const upRel = path.relative('.', upAbs).replaceAll('\\', '/')
598
+ const cached = _compileCache.get(upAbs)
599
+ const slots = cached?.result?.slots || 'shifted'
600
+ broadcast({ type: 'update', file: upRel, slots })
601
+ }
508
602
  } catch(e) {
509
603
  printStatus(rel, 'fail', [{ message: e.message }])
510
604
  broadcast({ type: 'error', file: rel, errors: [{ message: e.message, snippet: e.stack || e.message }] })