bimba-cli 0.5.10 → 0.5.12

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 +82 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
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
@@ -324,11 +386,12 @@ function stabilizeSymbols(js, filepath) {
324
386
  }
325
387
 
326
388
  async function compileFile(filepath) {
389
+ const abs = path.resolve(filepath)
327
390
  const file = Bun.file(filepath)
328
391
  const stat = await file.stat()
329
392
  const mtime = stat.mtime.getTime()
330
393
 
331
- const cached = _compileCache.get(filepath)
394
+ const cached = _compileCache.get(abs)
332
395
  if (cached && cached.mtime === mtime) return { ...cached.result, changeType: 'cached', slots: cached.slots }
333
396
 
334
397
  const code = await file.text()
@@ -344,11 +407,12 @@ async function compileFile(filepath) {
344
407
  const prev = _prevSlots.get(filepath)
345
408
  result.slots = (prev === undefined || prev === slotCount) ? 'stable' : 'shifted'
346
409
  _prevSlots.set(filepath, slotCount)
410
+ updateImportGraph(abs, extractImports(js, abs))
347
411
  }
348
412
 
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 })
413
+ const changeType = _prevJs.get(abs) === result.js ? 'none' : 'full'
414
+ _prevJs.set(abs, result.js)
415
+ _compileCache.set(abs, { mtime, result, slots: result.slots })
352
416
  return { ...result, changeType }
353
417
  }
354
418
 
@@ -505,6 +569,20 @@ export function serve(entrypoint, flags) {
505
569
  printStatus(rel, 'ok')
506
570
  broadcast({ type: 'clear-error' })
507
571
  broadcast({ type: 'update', file: rel, slots: out.slots || 'shifted' })
572
+
573
+ // Cascade: re-import every module transitively importing this file.
574
+ // They don't need recompilation (their source didn't change), but
575
+ // their captured references to the changed module are stale, so we
576
+ // tell the client to re-import them. The client's HMR queue
577
+ // processes these in order; tag classes get re-patched, plain
578
+ // utility modules get fresh top-level state.
579
+ const ups = transitiveImporters(path.resolve(filepath))
580
+ for (const upAbs of ups) {
581
+ const upRel = path.relative('.', upAbs).replaceAll('\\', '/')
582
+ const cached = _compileCache.get(upAbs)
583
+ const slots = cached?.slots || 'shifted'
584
+ broadcast({ type: 'update', file: upRel, slots })
585
+ }
508
586
  } catch(e) {
509
587
  printStatus(rel, 'fail', [{ message: e.message }])
510
588
  broadcast({ type: 'error', file: rel, errors: [{ message: e.message, snippet: e.stack || e.message }] })