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.
- package/package.json +1 -1
- package/serve.js +103 -9
package/package.json
CHANGED
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(
|
|
332
|
-
if (cached && cached.mtime === mtime) return
|
|
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
|
-
|
|
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(
|
|
421
|
+
const prev = _prevSlots.get(abs)
|
|
345
422
|
result.slots = (prev === undefined || prev === slotCount) ? 'stable' : 'shifted'
|
|
346
|
-
_prevSlots.set(
|
|
423
|
+
_prevSlots.set(abs, slotCount)
|
|
424
|
+
updateImportGraph(abs, extractImports(js, abs))
|
|
347
425
|
}
|
|
348
426
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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 }] })
|