bimba-cli 0.7.23 → 0.7.25

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 +160 -38
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.7.23",
3
+ "version": "0.7.25",
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,17 +216,40 @@ const hmrClient = `
216
216
 
217
217
  // ── Error overlay ──────────────────────────────────────────────────────────
218
218
 
219
+ const _compileErrors = new Map();
220
+
219
221
  function normalizeFile(file) {
220
- let value = String(file || '').split(String.fromCharCode(92)).join('/');
222
+ let value = String(file || '').split(/[?#]/)[0].split(String.fromCharCode(92)).join('/');
221
223
  while (value.startsWith('./')) value = value.slice(2);
222
224
  while (value.startsWith('/')) value = value.slice(1);
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 sameFile(left, right) {
229
+ const a = normalizeFile(left);
230
+ const b = normalizeFile(right);
231
+ if (!a || !b) return false;
232
+ return a === b || a.endsWith('/' + b) || b.endsWith('/' + a);
233
+ }
234
+
235
+ function escapeHtml(value) {
236
+ return String(value ?? '').replace(/[&<>"']/g, ch => ({
237
+ '&': '&amp;',
238
+ '<': '&lt;',
239
+ '>': '&gt;',
240
+ '"': '&quot;',
241
+ "'": '&#39;',
242
+ })[ch]);
243
+ }
244
+
245
+ function renderErrors() {
229
246
  let overlay = document.getElementById('__bimba_error__');
247
+
248
+ if (!_compileErrors.size) {
249
+ if (overlay) overlay.remove();
250
+ return;
251
+ }
252
+
230
253
  if (!overlay) {
231
254
  overlay = document.createElement('div');
232
255
  overlay.id = '__bimba_error__';
@@ -234,29 +257,57 @@ const hmrClient = `
234
257
  overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
235
258
  document.body.appendChild(overlay);
236
259
  }
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
- \`;
260
+
261
+ const files = Array.from(_compileErrors.entries());
262
+ overlay.innerHTML =
263
+ '<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)">' +
264
+ '<div style="background:#ff4444;color:#fff;padding:10px 16px;font-size:13px;font-weight:600;display:flex;justify-content:space-between;align-items:center">' +
265
+ '<span>Compile errors — ' + files.length + '</span>' +
266
+ '<span onclick="document.getElementById(\\'__bimba_error__\\').remove()" style="cursor:pointer;opacity:.7;font-size:16px">✕</span>' +
267
+ '</div>' +
268
+ files.map(([displayFile, item]) => {
269
+ const errors = item.errors || [];
270
+ return '<div style="border-bottom:1px solid #333">' +
271
+ '<div style="padding:10px 16px;background:#241616;color:#ffd1d1;font-size:13px;font-weight:600;display:flex;justify-content:space-between;gap:16px">' +
272
+ '<span>' + escapeHtml(displayFile) + '</span>' +
273
+ '<span style="opacity:.75;font-weight:400">' + escapeHtml(item.time || '') + '</span>' +
274
+ '</div>' +
275
+ errors.map(err =>
276
+ '<div style="padding:16px;border-top:1px solid #333">' +
277
+ '<div style="color:#ff8080;font-size:13px;margin-bottom:10px">' +
278
+ escapeHtml(err.message) +
279
+ (err.line ? ' <span style="color:#888">line ' + escapeHtml(err.line) + '</span>' : '') +
280
+ '</div>' +
281
+ (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>' : '') +
282
+ '</div>'
283
+ ).join('') +
284
+ '</div>';
285
+ }).join('') +
286
+ '</div>';
252
287
  }
253
288
 
254
- function clearError(file) {
255
- const overlay = document.getElementById('__bimba_error__');
256
- if (!overlay) return;
289
+ function showError(file, errors, time) {
290
+ const displayFile = normalizeFile(file);
291
+ for (const key of Array.from(_compileErrors.keys())) {
292
+ if (sameFile(key, displayFile)) _compileErrors.delete(key);
293
+ }
294
+ _compileErrors.set(displayFile, {
295
+ errors: Array.isArray(errors) ? errors : [errors],
296
+ time: time || new Date().toLocaleTimeString(),
297
+ });
298
+ renderErrors();
299
+ }
257
300
 
258
- const activeFile = overlay.dataset.file;
259
- if (!file || !activeFile || activeFile === normalizeFile(file)) overlay.remove();
301
+ function clearError(file) {
302
+ if (file) {
303
+ const displayFile = normalizeFile(file);
304
+ for (const key of Array.from(_compileErrors.keys())) {
305
+ if (sameFile(key, displayFile)) _compileErrors.delete(key);
306
+ }
307
+ } else {
308
+ _compileErrors.clear();
309
+ }
310
+ renderErrors();
260
311
  }
261
312
 
262
313
  connect();
@@ -562,6 +613,7 @@ export function serve(entrypoint, flags) {
562
613
  let _fadeId = 0
563
614
  let _statusFile = null
564
615
  let _statusSaved = false
616
+ let _statusKind = null
565
617
  const _isTTY = process.stdout.isTTY
566
618
 
567
619
  function cancelFade() {
@@ -577,6 +629,7 @@ export function serve(entrypoint, flags) {
577
629
  _statusSaved = false
578
630
  }
579
631
  _statusFile = null
632
+ _statusKind = null
580
633
  }
581
634
 
582
635
  function printStatus(file, state, errors) {
@@ -607,6 +660,7 @@ export function serve(entrypoint, flags) {
607
660
  process.stdout.write('\x1b[s')
608
661
  _statusSaved = true
609
662
  _statusFile = file
663
+ _statusKind = state
610
664
 
611
665
  if (errors?.length) {
612
666
  process.stdout.write(` ${theme.folder(now)} ${theme.filename(file)} ${status}\n`)
@@ -625,6 +679,7 @@ export function serve(entrypoint, flags) {
625
679
  if (i === total) {
626
680
  _statusSaved = false
627
681
  _statusFile = null
682
+ _statusKind = null
628
683
  }
629
684
  }, 5000 + i * 22))
630
685
  }
@@ -635,6 +690,33 @@ export function serve(entrypoint, flags) {
635
690
 
636
691
  const _activeErrors = new Map()
637
692
 
693
+ function renderErrorPanel() {
694
+ if (!_isTTY) return false
695
+
696
+ cancelFade()
697
+ if (_statusSaved) {
698
+ process.stdout.write('\x1b[u\x1b[J')
699
+ _statusSaved = false
700
+ }
701
+ _statusFile = null
702
+ _statusKind = null
703
+
704
+ if (!_activeErrors.size) return false
705
+
706
+ process.stdout.write('\x1b[s')
707
+ _statusSaved = true
708
+ _statusKind = 'errors'
709
+
710
+ for (const [file, item] of _activeErrors.entries()) {
711
+ process.stdout.write(` ${theme.folder(item.time)} ${theme.filename(file)} ${theme.failure(' fail ')}\n`)
712
+ for (const err of item.errors) {
713
+ try { printerr(err) } catch(_) { process.stdout.write(' ' + err.message + '\n') }
714
+ }
715
+ }
716
+
717
+ return true
718
+ }
719
+
638
720
  function broadcast(payload) {
639
721
  const msg = JSON.stringify(payload)
640
722
  for (const socket of sockets) socket.send(msg)
@@ -642,6 +724,7 @@ export function serve(entrypoint, flags) {
642
724
 
643
725
  function normalizeFile(file) {
644
726
  let value = String(file || '')
727
+ value = value.split(/[?#]/)[0]
645
728
  if (path.isAbsolute(value)) {
646
729
  const rel = path.relative(process.cwd(), value)
647
730
  if (!rel.startsWith('..')) value = rel
@@ -655,6 +738,40 @@ export function serve(entrypoint, flags) {
655
738
  const srcRoot = path.resolve(srcDir)
656
739
  const srcRel = normalizeFile(srcRoot)
657
740
 
741
+ function fileVariants(file) {
742
+ const key = normalizeFile(file)
743
+ const variants = new Set([key])
744
+
745
+ if (srcRel && key.startsWith(srcRel + '/')) variants.add(key.slice(srcRel.length + 1))
746
+ else if (srcRel && key) variants.add(srcRel + '/' + key)
747
+
748
+ return Array.from(variants).filter(Boolean)
749
+ }
750
+
751
+ function sameFile(left, right) {
752
+ const lefts = fileVariants(left)
753
+ const rights = fileVariants(right)
754
+
755
+ for (const a of lefts) {
756
+ for (const b of rights) {
757
+ if (a === b || a.endsWith('/' + b) || b.endsWith('/' + a)) return true
758
+ }
759
+ }
760
+
761
+ return false
762
+ }
763
+
764
+ function takeError(file) {
765
+ let previous = null
766
+ const keys = Array.from(_activeErrors.keys())
767
+ for (const key of keys) {
768
+ if (!sameFile(key, file)) continue
769
+ previous ||= _activeErrors.get(key)
770
+ _activeErrors.delete(key)
771
+ }
772
+ return previous
773
+ }
774
+
658
775
  function errorMessage(error) {
659
776
  return error?.message || String(error)
660
777
  }
@@ -686,7 +803,8 @@ export function serve(entrypoint, flags) {
686
803
  }
687
804
 
688
805
  function showTrackedError(file, item) {
689
- printStatus(file, 'fail', item.errors)
806
+ if (_isTTY) renderErrorPanel()
807
+ else printStatus(file, 'fail', item.errors)
690
808
  broadcast({ type: 'error', file, time: item.time, errors: item.payload })
691
809
  }
692
810
 
@@ -694,7 +812,7 @@ export function serve(entrypoint, flags) {
694
812
  const key = normalizeFile(file)
695
813
  const list = Array.isArray(errors) ? errors : [errors]
696
814
  const signature = errorSignature(list)
697
- const previous = _activeErrors.get(key)
815
+ const previous = takeError(key)
698
816
 
699
817
  const item = {
700
818
  signature,
@@ -723,19 +841,22 @@ export function serve(entrypoint, flags) {
723
841
 
724
842
  function clearError(file) {
725
843
  const key = file ? normalizeFile(file) : null
726
- const wasStatusFile = key && _statusFile === key
727
- const hadError = key ? _activeErrors.has(key) : _activeErrors.size > 0
844
+ const hadPanel = _statusKind === 'errors'
845
+ const wasStatusFile = key && _statusFile && sameFile(_statusFile, key)
846
+ const hadError = key ? !!takeError(key) : _activeErrors.size > 0
728
847
 
729
- if (key) _activeErrors.delete(key)
730
- else _activeErrors.clear()
848
+ if (!key) _activeErrors.clear()
731
849
 
732
- if (key && !hadError && !wasStatusFile) return
850
+ let showedNext = false
851
+ if (_isTTY && (hadError || wasStatusFile || hadPanel)) {
852
+ showedNext = renderErrorPanel()
853
+ } else if (!key || hadError || wasStatusFile) {
854
+ clearStatus(key)
855
+ }
733
856
 
734
- clearStatus(key)
735
857
  broadcast({ type: 'clear-error', file: key })
736
858
 
737
- let showedNext = false
738
- if (wasStatusFile && _activeErrors.size) {
859
+ if (!_isTTY && wasStatusFile && _activeErrors.size) {
739
860
  const [nextFile, nextItem] = Array.from(_activeErrors.entries()).at(-1)
740
861
  showTrackedError(nextFile, nextItem)
741
862
  showedNext = true
@@ -747,9 +868,10 @@ export function serve(entrypoint, flags) {
747
868
  function markSuccess(file) {
748
869
  const key = normalizeFile(file)
749
870
  const result = clearError(key)
750
- const shouldPrint = result?.cleared && !result.showedNext
871
+ const active = _activeErrors.size
872
+ const shouldPrint = result?.cleared && !result.showedNext && !active
751
873
  if (shouldPrint) printStatus(key, 'ok')
752
- return { cleared: !!result?.cleared, printed: !!shouldPrint, showedNext: !!result?.showedNext }
874
+ return { cleared: !!result?.cleared, printed: !!shouldPrint, showedNext: !!result?.showedNext, active }
753
875
  }
754
876
 
755
877
  const _debounce = new Map()
@@ -822,7 +944,7 @@ export function serve(entrypoint, flags) {
822
944
  // No change at all — skip
823
945
  if (out.changeType === 'none' || out.changeType === 'cached') return
824
946
 
825
- if (!success.printed && !success.showedNext) printStatus(rel, 'ok')
947
+ if (!success.printed && !success.showedNext && !success.active) printStatus(rel, 'ok')
826
948
  broadcast({ type: 'update', file: rel, slots: out.slots || 'shifted' })
827
949
  } catch(e) {
828
950
  if (!isCurrentChange(file, version)) return