bimba-cli 0.7.22 → 0.7.24

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 +145 -52
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.7.22",
3
+ "version": "0.7.24",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/HeapVoid/bimba.git"
package/serve.js CHANGED
@@ -98,7 +98,7 @@ const hmrClient = `
98
98
  }
99
99
 
100
100
  async function _doUpdate(file, slots) {
101
- clearError();
101
+ clearError(file);
102
102
 
103
103
  const bodyBefore = new Set(document.body.children);
104
104
  const tagsBefore = new Set();
@@ -216,6 +216,8 @@ const hmrClient = `
216
216
 
217
217
  // ── Error overlay ──────────────────────────────────────────────────────────
218
218
 
219
+ const _compileErrors = new Map();
220
+
219
221
  function normalizeFile(file) {
220
222
  let value = String(file || '').split(String.fromCharCode(92)).join('/');
221
223
  while (value.startsWith('./')) value = value.slice(2);
@@ -223,10 +225,24 @@ const hmrClient = `
223
225
  return value;
224
226
  }
225
227
 
226
- function showError(file, errors, time) {
227
- const displayFile = normalizeFile(file);
228
- const displayTime = time || new Date().toLocaleTimeString();
228
+ function escapeHtml(value) {
229
+ return String(value ?? '').replace(/[&<>"']/g, ch => ({
230
+ '&': '&amp;',
231
+ '<': '&lt;',
232
+ '>': '&gt;',
233
+ '"': '&quot;',
234
+ "'": '&#39;',
235
+ })[ch]);
236
+ }
237
+
238
+ function renderErrors() {
229
239
  let overlay = document.getElementById('__bimba_error__');
240
+
241
+ if (!_compileErrors.size) {
242
+ if (overlay) overlay.remove();
243
+ return;
244
+ }
245
+
230
246
  if (!overlay) {
231
247
  overlay = document.createElement('div');
232
248
  overlay.id = '__bimba_error__';
@@ -234,29 +250,48 @@ const hmrClient = `
234
250
  overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
235
251
  document.body.appendChild(overlay);
236
252
  }
237
- overlay.dataset.file = displayFile;
238
- overlay.innerHTML = \`
239
- <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)">
240
- <div style="background:#ff4444;color:#fff;padding:10px 16px;font-size:13px;font-weight:600;display:flex;justify-content:space-between;align-items:center">
241
- <span>Compile error — \${displayFile} <span style="opacity:.75;font-weight:400">\${displayTime}</span></span>
242
- <span onclick="document.getElementById('__bimba_error__').remove()" style="cursor:pointer;opacity:.7;font-size:16px">✕</span>
243
- </div>
244
- \${errors.map(err => \`
245
- <div style="padding:16px;border-bottom:1px solid #333">
246
- <div style="color:#ff8080;font-size:13px;margin-bottom:10px">\${err.message}\${err.line ? \` <span style="color:#888">line \${err.line}</span>\` : ''}</div>
247
- \${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>\` : ''}
248
- </div>
249
- \`).join('')}
250
- </div>
251
- \`;
253
+
254
+ const files = Array.from(_compileErrors.entries());
255
+ overlay.innerHTML =
256
+ '<div style="background:#1a1a1a;border:1px solid #ff4444;border-radius:8px;max-width:900px;width:100%;max-height:90vh;overflow:auto;box-shadow:0 0 40px rgba(255,68,68,.3)">' +
257
+ '<div style="background:#ff4444;color:#fff;padding:10px 16px;font-size:13px;font-weight:600;display:flex;justify-content:space-between;align-items:center">' +
258
+ '<span>Compile errors — ' + files.length + '</span>' +
259
+ '<span onclick="document.getElementById(\\'__bimba_error__\\').remove()" style="cursor:pointer;opacity:.7;font-size:16px">✕</span>' +
260
+ '</div>' +
261
+ files.map(([displayFile, item]) => {
262
+ const errors = item.errors || [];
263
+ return '<div style="border-bottom:1px solid #333">' +
264
+ '<div style="padding:10px 16px;background:#241616;color:#ffd1d1;font-size:13px;font-weight:600;display:flex;justify-content:space-between;gap:16px">' +
265
+ '<span>' + escapeHtml(displayFile) + '</span>' +
266
+ '<span style="opacity:.75;font-weight:400">' + escapeHtml(item.time || '') + '</span>' +
267
+ '</div>' +
268
+ errors.map(err =>
269
+ '<div style="padding:16px;border-top:1px solid #333">' +
270
+ '<div style="color:#ff8080;font-size:13px;margin-bottom:10px">' +
271
+ escapeHtml(err.message) +
272
+ (err.line ? ' <span style="color:#888">line ' + escapeHtml(err.line) + '</span>' : '') +
273
+ '</div>' +
274
+ (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">' + escapeHtml(err.snippet) + '</pre>' : '') +
275
+ '</div>'
276
+ ).join('') +
277
+ '</div>';
278
+ }).join('') +
279
+ '</div>';
252
280
  }
253
281
 
254
- function clearError(file) {
255
- const overlay = document.getElementById('__bimba_error__');
256
- if (!overlay) return;
282
+ function showError(file, errors, time) {
283
+ const displayFile = normalizeFile(file);
284
+ _compileErrors.set(displayFile, {
285
+ errors: Array.isArray(errors) ? errors : [errors],
286
+ time: time || new Date().toLocaleTimeString(),
287
+ });
288
+ renderErrors();
289
+ }
257
290
 
258
- const activeFile = overlay.dataset.file;
259
- if (!file || !activeFile || activeFile === normalizeFile(file)) overlay.remove();
291
+ function clearError(file) {
292
+ if (file) _compileErrors.delete(normalizeFile(file));
293
+ else _compileErrors.clear();
294
+ renderErrors();
260
295
  }
261
296
 
262
297
  connect();
@@ -265,7 +300,7 @@ const hmrClient = `
265
300
 
266
301
  // ─── Server-side compile cache ────────────────────────────────────────────────
267
302
 
268
- const _compileCache = new Map() // filepath → { mtime, result }
303
+ const _compileCache = new Map() // filepath → { stamp, result }
269
304
  const _prevJs = new Map() // filepath → compiled js — for change detection
270
305
  const _prevSlots = new Map() // filepath → previous symbol slot count
271
306
  const _importScanner = new Bun.Transpiler({ loader: 'js' })
@@ -367,7 +402,7 @@ async function compileFile(filepath) {
367
402
  const abs = path.resolve(filepath)
368
403
  if (!existsSync(abs)) return missingCompileResult(abs)
369
404
 
370
- const file = Bun.file(filepath)
405
+ const file = Bun.file(abs)
371
406
  let stat
372
407
  try {
373
408
  stat = await file.stat()
@@ -375,10 +410,10 @@ async function compileFile(filepath) {
375
410
  if (isMissingFileError(error)) return missingCompileResult(abs)
376
411
  throw error
377
412
  }
378
- const mtime = stat.mtime.getTime()
413
+ const stamp = `${stat.mtimeMs ?? stat.mtime?.getTime?.() ?? 0}:${stat.size ?? 0}`
379
414
 
380
415
  const cached = _compileCache.get(abs)
381
- if (cached && cached.mtime === mtime) return _normalizeResult(cached.result, { changeType: 'cached' })
416
+ if (cached && cached.stamp === stamp) return _normalizeResult(cached.result, { changeType: 'cached' })
382
417
 
383
418
  let code
384
419
  try {
@@ -406,7 +441,7 @@ async function compileFile(filepath) {
406
441
  const baked = { js: result.js, errors, slots: result.slots }
407
442
  const changeType = _prevJs.get(abs) === baked.js ? 'none' : 'full'
408
443
  _prevJs.set(abs, baked.js)
409
- _compileCache.set(abs, { mtime, result: baked })
444
+ _compileCache.set(abs, { stamp, result: baked })
410
445
  return _normalizeResult(baked, { changeType })
411
446
  }
412
447
 
@@ -562,6 +597,7 @@ export function serve(entrypoint, flags) {
562
597
  let _fadeId = 0
563
598
  let _statusFile = null
564
599
  let _statusSaved = false
600
+ let _statusKind = null
565
601
  const _isTTY = process.stdout.isTTY
566
602
 
567
603
  function cancelFade() {
@@ -577,6 +613,7 @@ export function serve(entrypoint, flags) {
577
613
  _statusSaved = false
578
614
  }
579
615
  _statusFile = null
616
+ _statusKind = null
580
617
  }
581
618
 
582
619
  function printStatus(file, state, errors) {
@@ -607,6 +644,7 @@ export function serve(entrypoint, flags) {
607
644
  process.stdout.write('\x1b[s')
608
645
  _statusSaved = true
609
646
  _statusFile = file
647
+ _statusKind = state
610
648
 
611
649
  if (errors?.length) {
612
650
  process.stdout.write(` ${theme.folder(now)} ${theme.filename(file)} ${status}\n`)
@@ -625,6 +663,7 @@ export function serve(entrypoint, flags) {
625
663
  if (i === total) {
626
664
  _statusSaved = false
627
665
  _statusFile = null
666
+ _statusKind = null
628
667
  }
629
668
  }, 5000 + i * 22))
630
669
  }
@@ -635,6 +674,33 @@ export function serve(entrypoint, flags) {
635
674
 
636
675
  const _activeErrors = new Map()
637
676
 
677
+ function renderErrorPanel() {
678
+ if (!_isTTY) return false
679
+
680
+ cancelFade()
681
+ if (_statusSaved) {
682
+ process.stdout.write('\x1b[u\x1b[J')
683
+ _statusSaved = false
684
+ }
685
+ _statusFile = null
686
+ _statusKind = null
687
+
688
+ if (!_activeErrors.size) return false
689
+
690
+ process.stdout.write('\x1b[s')
691
+ _statusSaved = true
692
+ _statusKind = 'errors'
693
+
694
+ for (const [file, item] of _activeErrors.entries()) {
695
+ process.stdout.write(` ${theme.folder(item.time)} ${theme.filename(file)} ${theme.failure(' fail ')}\n`)
696
+ for (const err of item.errors) {
697
+ try { printerr(err) } catch(_) { process.stdout.write(' ' + err.message + '\n') }
698
+ }
699
+ }
700
+
701
+ return true
702
+ }
703
+
638
704
  function broadcast(payload) {
639
705
  const msg = JSON.stringify(payload)
640
706
  for (const socket of sockets) socket.send(msg)
@@ -652,6 +718,9 @@ export function serve(entrypoint, flags) {
652
718
  return value
653
719
  }
654
720
 
721
+ const srcRoot = path.resolve(srcDir)
722
+ const srcRel = normalizeFile(srcRoot)
723
+
655
724
  function errorMessage(error) {
656
725
  return error?.message || String(error)
657
726
  }
@@ -683,7 +752,8 @@ export function serve(entrypoint, flags) {
683
752
  }
684
753
 
685
754
  function showTrackedError(file, item) {
686
- printStatus(file, 'fail', item.errors)
755
+ if (_isTTY) renderErrorPanel()
756
+ else printStatus(file, 'fail', item.errors)
687
757
  broadcast({ type: 'error', file, time: item.time, errors: item.payload })
688
758
  }
689
759
 
@@ -728,11 +798,16 @@ export function serve(entrypoint, flags) {
728
798
 
729
799
  if (key && !hadError && !wasStatusFile) return
730
800
 
731
- clearStatus(key)
801
+ let showedNext = false
802
+ if (_isTTY && (hadError || _statusKind === 'errors')) {
803
+ showedNext = renderErrorPanel()
804
+ } else {
805
+ clearStatus(key)
806
+ }
807
+
732
808
  broadcast({ type: 'clear-error', file: key })
733
809
 
734
- let showedNext = false
735
- if (wasStatusFile && _activeErrors.size) {
810
+ if (!_isTTY && wasStatusFile && _activeErrors.size) {
736
811
  const [nextFile, nextItem] = Array.from(_activeErrors.entries()).at(-1)
737
812
  showTrackedError(nextFile, nextItem)
738
813
  showedNext = true
@@ -744,41 +819,59 @@ export function serve(entrypoint, flags) {
744
819
  function markSuccess(file) {
745
820
  const key = normalizeFile(file)
746
821
  const result = clearError(key)
747
- const shouldPrint = result?.cleared && !result.showedNext
822
+ const active = _activeErrors.size
823
+ const shouldPrint = result?.cleared && !result.showedNext && !active
748
824
  if (shouldPrint) printStatus(key, 'ok')
749
- return { cleared: !!result?.cleared, printed: !!shouldPrint, showedNext: !!result?.showedNext }
825
+ return { cleared: !!result?.cleared, printed: !!shouldPrint, showedNext: !!result?.showedNext, active }
750
826
  }
751
827
 
752
828
  const _debounce = new Map()
753
829
  const _watchVersion = new Map()
754
830
 
755
- function scheduleCompile(filename) {
831
+ function watchedFile(filename) {
756
832
  filename = filename && String(filename)
757
- if (!filename || !filename.endsWith('.imba')) return
833
+ if (!filename) return null
834
+
835
+ let filepath
836
+ if (path.isAbsolute(filename)) {
837
+ filepath = path.resolve(filename)
838
+ } else {
839
+ const rel = normalizeFile(filename)
840
+ filepath = (rel === srcRel || rel.startsWith(srcRel + '/'))
841
+ ? path.resolve(filename)
842
+ : path.resolve(srcRoot, filename)
843
+ }
844
+
845
+ const rel = normalizeFile(filepath)
846
+ return { filepath, rel }
847
+ }
848
+
849
+ function scheduleCompile(filename) {
850
+ const file = watchedFile(filename)
851
+ if (!file || !file.rel.endsWith('.imba')) return
758
852
 
759
- const version = (_watchVersion.get(filename) || 0) + 1
760
- _watchVersion.set(filename, version)
853
+ const version = (_watchVersion.get(file.rel) || 0) + 1
854
+ _watchVersion.set(file.rel, version)
761
855
 
762
- const pending = _debounce.get(filename)
856
+ const pending = _debounce.get(file.rel)
763
857
  if (pending) clearTimeout(pending)
764
858
 
765
- _debounce.set(filename, setTimeout(() => {
766
- _debounce.delete(filename)
767
- compileChangedFile(filename, version)
859
+ _debounce.set(file.rel, setTimeout(() => {
860
+ _debounce.delete(file.rel)
861
+ compileChangedFile(file, version)
768
862
  }, 150))
769
863
  }
770
864
 
771
- function isCurrentChange(filename, version) {
772
- return _watchVersion.get(filename) === version
865
+ function isCurrentChange(file, version) {
866
+ return _watchVersion.get(file.rel) === version
773
867
  }
774
868
 
775
- async function compileChangedFile(filename, version) {
776
- const filepath = path.join(srcDir, filename)
777
- const rel = normalizeFile(path.join(path.relative('.', srcDir), filename))
869
+ async function compileChangedFile(file, version) {
870
+ const { filepath, rel } = file
778
871
 
779
872
  try {
780
873
  if (!existsSync(filepath)) {
781
- if (!isCurrentChange(filename, version)) return
874
+ if (!isCurrentChange(file, version)) return
782
875
  dropFileState(filepath)
783
876
  clearError(rel)
784
877
  return
@@ -786,7 +879,7 @@ export function serve(entrypoint, flags) {
786
879
 
787
880
  const out = await compileFile(filepath)
788
881
 
789
- if (!isCurrentChange(filename, version)) return
882
+ if (!isCurrentChange(file, version)) return
790
883
  if (out.missing) {
791
884
  clearError(rel)
792
885
  return
@@ -802,10 +895,10 @@ export function serve(entrypoint, flags) {
802
895
  // No change at all — skip
803
896
  if (out.changeType === 'none' || out.changeType === 'cached') return
804
897
 
805
- if (!success.printed && !success.showedNext) printStatus(rel, 'ok')
898
+ if (!success.printed && !success.showedNext && !success.active) printStatus(rel, 'ok')
806
899
  broadcast({ type: 'update', file: rel, slots: out.slots || 'shifted' })
807
900
  } catch(e) {
808
- if (!isCurrentChange(filename, version)) return
901
+ if (!isCurrentChange(file, version)) return
809
902
  if (isMissingFileError(e)) {
810
903
  dropFileState(filepath)
811
904
  clearError(rel)