bimba-cli 0.7.29 → 0.7.31
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 +108 -95
package/package.json
CHANGED
package/serve.js
CHANGED
|
@@ -3,7 +3,6 @@ import * as compiler from 'imba/compiler'
|
|
|
3
3
|
import { mkdirSync, watch, existsSync, statSync, writeFileSync, realpathSync } from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import { theme } from './utils.js'
|
|
6
|
-
import { printerr } from './plugin.js'
|
|
7
6
|
|
|
8
7
|
// ─── HMR Client (injected into browser) ──────────────────────────────────────
|
|
9
8
|
|
|
@@ -628,32 +627,63 @@ export function serve(entrypoint, flags) {
|
|
|
628
627
|
const srcDir = path.dirname(entrypoint)
|
|
629
628
|
const sockets = new Set()
|
|
630
629
|
|
|
631
|
-
// ──
|
|
630
|
+
// ── Live status block (shows only the current compile state) ───────────────
|
|
632
631
|
|
|
633
|
-
let
|
|
634
|
-
let
|
|
632
|
+
let _statusRows = 0
|
|
633
|
+
let _statusTimer = null
|
|
635
634
|
let _statusFile = null
|
|
636
|
-
let _statusSaved = false
|
|
637
|
-
let _statusKind = null
|
|
638
635
|
const _isTTY = process.stdout.isTTY
|
|
639
636
|
|
|
640
|
-
function
|
|
641
|
-
|
|
642
|
-
|
|
637
|
+
function stripAnsi(text) {
|
|
638
|
+
return String(text).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '')
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function renderedRows(lines) {
|
|
642
|
+
const columns = Math.max(1, process.stdout.columns || 80)
|
|
643
|
+
return lines.reduce((total, line) => {
|
|
644
|
+
const length = stripAnsi(line).length
|
|
645
|
+
return total + Math.max(1, Math.ceil(length / columns))
|
|
646
|
+
}, 0)
|
|
643
647
|
}
|
|
644
648
|
|
|
645
649
|
function clearStatus(file) {
|
|
646
|
-
if (file && _statusFile && _statusFile !== file) return
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
_statusSaved = false
|
|
650
|
+
if (file && _statusFile && _statusFile !== file) return false
|
|
651
|
+
if (_statusTimer) {
|
|
652
|
+
clearTimeout(_statusTimer)
|
|
653
|
+
_statusTimer = null
|
|
651
654
|
}
|
|
655
|
+
if (_isTTY && _statusRows) {
|
|
656
|
+
process.stdout.write(`\x1b[${_statusRows}A\r\x1b[J`)
|
|
657
|
+
}
|
|
658
|
+
_statusRows = 0
|
|
652
659
|
_statusFile = null
|
|
653
|
-
|
|
660
|
+
return true
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function formatErrorLines(errors) {
|
|
664
|
+
const lines = []
|
|
665
|
+
for (const err of errors || []) {
|
|
666
|
+
const message = errorMessage(err)
|
|
667
|
+
const line = errorLine(err)
|
|
668
|
+
lines.push(` ${theme.error(' ' + message + ' ')}${line != null ? theme.margin(` line ${line + 1} `) : ''}`)
|
|
669
|
+
const snippet = errorSnippet(err)
|
|
670
|
+
if (snippet && snippet !== message) {
|
|
671
|
+
lines.push(...String(snippet).split('\n').slice(0, 6).map(item => ` ${theme.code(item)}`))
|
|
672
|
+
}
|
|
673
|
+
lines.push('')
|
|
674
|
+
}
|
|
675
|
+
return lines
|
|
654
676
|
}
|
|
655
677
|
|
|
656
|
-
function
|
|
678
|
+
function statusLines(file, state, errors) {
|
|
679
|
+
const now = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
680
|
+
const status = state === 'ok' ? theme.success(' ok ') : theme.failure(' fail ')
|
|
681
|
+
const lines = [` ${theme.folder(now)} ${theme.filename(file)} ${status}`]
|
|
682
|
+
if (errors?.length) lines.push('', ...formatErrorLines(errors))
|
|
683
|
+
return lines
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function printStatus(file, state, errors, options = {}) {
|
|
657
687
|
// non-TTY (pipes, Claude Code bash, CI): plain newline-terminated output,
|
|
658
688
|
// no ANSI cursor tricks, no fade-out — so logs stay readable.
|
|
659
689
|
if (!_isTTY) {
|
|
@@ -670,74 +700,20 @@ export function serve(entrypoint, flags) {
|
|
|
670
700
|
return
|
|
671
701
|
}
|
|
672
702
|
|
|
673
|
-
|
|
674
|
-
if (_statusSaved) {
|
|
675
|
-
process.stdout.write('\x1b[u\x1b[J')
|
|
676
|
-
_statusSaved = false
|
|
677
|
-
}
|
|
678
|
-
const now = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
679
|
-
const status = state === 'ok' ? theme.success(' ok ') : theme.failure(' fail ')
|
|
680
|
-
|
|
681
|
-
process.stdout.write('\x1b[s')
|
|
682
|
-
_statusSaved = true
|
|
703
|
+
clearStatus()
|
|
683
704
|
_statusFile = file
|
|
684
|
-
|
|
705
|
+
const lines = statusLines(file, state, errors)
|
|
706
|
+
process.stdout.write(lines.join('\n') + '\n')
|
|
707
|
+
_statusRows = renderedRows(lines)
|
|
685
708
|
|
|
686
|
-
if (
|
|
687
|
-
process.stdout.write(` ${theme.folder(now)} ${theme.filename(file)} ${status}\n`)
|
|
688
|
-
for (const err of errors) {
|
|
689
|
-
try { printerr(err) } catch(_) { process.stdout.write(' ' + err.message + '\n') }
|
|
690
|
-
}
|
|
691
|
-
} else {
|
|
692
|
-
const myId = ++_fadeId
|
|
693
|
-
const plainLine = ` ${now} ${file} ok `
|
|
694
|
-
const total = plainLine.length
|
|
695
|
-
process.stdout.write(` ${theme.folder(now)} ${theme.filename(file)} ${status}`)
|
|
696
|
-
for (let i = 1; i <= total; i++) {
|
|
697
|
-
_fadeTimers.push(setTimeout(() => {
|
|
698
|
-
if (_fadeId !== myId) return
|
|
699
|
-
process.stdout.write('\x1b[1D \x1b[1D')
|
|
700
|
-
if (i === total) {
|
|
701
|
-
_statusSaved = false
|
|
702
|
-
_statusFile = null
|
|
703
|
-
_statusKind = null
|
|
704
|
-
}
|
|
705
|
-
}, 5000 + i * 22))
|
|
706
|
-
}
|
|
707
|
-
}
|
|
709
|
+
if (options.clearAfter) _statusTimer = setTimeout(() => clearStatus(file), options.clearAfter)
|
|
708
710
|
}
|
|
709
711
|
|
|
710
712
|
// ── File watcher ───────────────────────────────────────────────────────────
|
|
711
713
|
|
|
712
714
|
const _activeErrors = new Map()
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (!_isTTY) return false
|
|
716
|
-
|
|
717
|
-
cancelFade()
|
|
718
|
-
if (_statusSaved) {
|
|
719
|
-
process.stdout.write('\x1b[u\x1b[J')
|
|
720
|
-
_statusSaved = false
|
|
721
|
-
}
|
|
722
|
-
_statusFile = null
|
|
723
|
-
_statusKind = null
|
|
724
|
-
|
|
725
|
-
if (!_activeErrors.size) return false
|
|
726
|
-
|
|
727
|
-
process.stdout.write('\x1b[s')
|
|
728
|
-
_statusSaved = true
|
|
729
|
-
_statusKind = 'errors'
|
|
730
|
-
|
|
731
|
-
for (const item of _activeErrors.values()) {
|
|
732
|
-
const file = item.file
|
|
733
|
-
process.stdout.write(` ${theme.folder(item.time)} ${theme.filename(file)} ${theme.failure(' fail ')}\n`)
|
|
734
|
-
for (const err of item.errors) {
|
|
735
|
-
try { printerr(err) } catch(_) { process.stdout.write(' ' + err.message + '\n') }
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
return true
|
|
740
|
-
}
|
|
715
|
+
const _terminalErrors = new Map()
|
|
716
|
+
const TERMINAL_DUPLICATE_MS = 5000
|
|
741
717
|
|
|
742
718
|
function broadcast(payload) {
|
|
743
719
|
const msg = JSON.stringify(payload)
|
|
@@ -774,6 +750,12 @@ export function serve(entrypoint, flags) {
|
|
|
774
750
|
return Array.from(variants).filter(Boolean)
|
|
775
751
|
}
|
|
776
752
|
|
|
753
|
+
function terminalErrorKey(file) {
|
|
754
|
+
const variants = fileVariants(file)
|
|
755
|
+
const rooted = variants.find(variant => srcRel && variant.startsWith(srcRel + '/'))
|
|
756
|
+
return `path:${rooted || variants[0] || normalizeFile(file)}`
|
|
757
|
+
}
|
|
758
|
+
|
|
777
759
|
function fileCandidates(file) {
|
|
778
760
|
const candidates = []
|
|
779
761
|
for (const variant of fileVariants(file)) {
|
|
@@ -862,9 +844,34 @@ export function serve(entrypoint, flags) {
|
|
|
862
844
|
.join('\n---\n')
|
|
863
845
|
}
|
|
864
846
|
|
|
847
|
+
function terminalErrorSignature(errors) {
|
|
848
|
+
return serializeErrors(errors)
|
|
849
|
+
.map(error => error.message)
|
|
850
|
+
.join('\n---\n')
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function renderActiveErrors() {
|
|
854
|
+
if (!_isTTY) return false
|
|
855
|
+
if (!_activeErrors.size) {
|
|
856
|
+
clearStatus()
|
|
857
|
+
return false
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
clearStatus()
|
|
861
|
+
const lines = []
|
|
862
|
+
for (const item of _activeErrors.values()) {
|
|
863
|
+
if (lines.length) lines.push('')
|
|
864
|
+
lines.push(...statusLines(item.file, 'fail', item.errors))
|
|
865
|
+
}
|
|
866
|
+
process.stdout.write(lines.join('\n') + '\n')
|
|
867
|
+
_statusFile = null
|
|
868
|
+
_statusRows = renderedRows(lines)
|
|
869
|
+
return true
|
|
870
|
+
}
|
|
871
|
+
|
|
865
872
|
function showTrackedError(item) {
|
|
866
873
|
const file = item.file
|
|
867
|
-
if (_isTTY)
|
|
874
|
+
if (_isTTY) renderActiveErrors()
|
|
868
875
|
else printStatus(file, 'fail', item.errors)
|
|
869
876
|
broadcast({ type: 'error', file, time: item.time, errors: item.payload })
|
|
870
877
|
}
|
|
@@ -872,23 +879,32 @@ export function serve(entrypoint, flags) {
|
|
|
872
879
|
function reportError(file, errors) {
|
|
873
880
|
const display = normalizeFile(file)
|
|
874
881
|
const key = errorKey(display)
|
|
882
|
+
const terminalKey = terminalErrorKey(display)
|
|
875
883
|
const list = Array.isArray(errors) ? errors : [errors]
|
|
876
884
|
const signature = errorSignature(list)
|
|
885
|
+
const printSignature = terminalErrorSignature(list)
|
|
877
886
|
const previous = takeError(display)
|
|
887
|
+
const now = Date.now()
|
|
888
|
+
const recent = _terminalErrors.get(terminalKey)
|
|
878
889
|
const duplicate = previous?.signature === signature
|
|
890
|
+
|| previous?.printSignature === printSignature
|
|
891
|
+
|| (recent?.signature === printSignature && now - recent.time < TERMINAL_DUPLICATE_MS)
|
|
879
892
|
|
|
880
893
|
const item = {
|
|
881
894
|
file: display,
|
|
882
895
|
signature,
|
|
896
|
+
printSignature,
|
|
883
897
|
errors: list,
|
|
884
898
|
payload: serializeErrors(list),
|
|
885
899
|
time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
|
|
886
900
|
}
|
|
887
901
|
_activeErrors.set(key, item)
|
|
902
|
+
_terminalErrors.set(terminalKey, { signature: printSignature, time: now })
|
|
888
903
|
|
|
889
|
-
//
|
|
890
|
-
//
|
|
904
|
+
// Repeated reports of the same active error update the live status and
|
|
905
|
+
// browser overlay, but do not create another terminal entry.
|
|
891
906
|
if (duplicate) {
|
|
907
|
+
if (_isTTY) renderActiveErrors()
|
|
892
908
|
broadcast({ type: 'error', file: display, time: item.time, errors: item.payload })
|
|
893
909
|
return
|
|
894
910
|
}
|
|
@@ -908,27 +924,22 @@ export function serve(entrypoint, flags) {
|
|
|
908
924
|
|
|
909
925
|
function clearError(file) {
|
|
910
926
|
const key = file ? normalizeFile(file) : null
|
|
911
|
-
const hadPanel = _statusKind === 'errors'
|
|
912
927
|
const wasStatusFile = key && _statusFile && sameFile(_statusFile, key)
|
|
913
928
|
const hadError = key ? !!takeError(key) : _activeErrors.size > 0
|
|
914
929
|
|
|
915
|
-
if (!key)
|
|
930
|
+
if (!key) {
|
|
931
|
+
_activeErrors.clear()
|
|
932
|
+
_terminalErrors.clear()
|
|
933
|
+
} else if (hadError) {
|
|
934
|
+
_terminalErrors.delete(terminalErrorKey(key))
|
|
935
|
+
}
|
|
916
936
|
|
|
917
937
|
let showedNext = false
|
|
918
|
-
if (_isTTY &&
|
|
919
|
-
|
|
920
|
-
} else if (!key || hadError || wasStatusFile) {
|
|
921
|
-
clearStatus(key)
|
|
922
|
-
}
|
|
938
|
+
if (_isTTY && _activeErrors.size) showedNext = renderActiveErrors()
|
|
939
|
+
else if (!key || hadError || wasStatusFile) clearStatus(key)
|
|
923
940
|
|
|
924
941
|
broadcast({ type: 'clear-error', file: key })
|
|
925
942
|
|
|
926
|
-
if (!_isTTY && wasStatusFile && _activeErrors.size) {
|
|
927
|
-
const nextItem = Array.from(_activeErrors.values()).at(-1)
|
|
928
|
-
showTrackedError(nextItem)
|
|
929
|
-
showedNext = true
|
|
930
|
-
}
|
|
931
|
-
|
|
932
943
|
return { cleared: hadError || wasStatusFile, file: key, showedNext }
|
|
933
944
|
}
|
|
934
945
|
|
|
@@ -937,7 +948,9 @@ export function serve(entrypoint, flags) {
|
|
|
937
948
|
const result = clearError(key)
|
|
938
949
|
const active = _activeErrors.size
|
|
939
950
|
const shouldPrint = result?.cleared && !result.showedNext && !active
|
|
940
|
-
if (shouldPrint)
|
|
951
|
+
if (shouldPrint) {
|
|
952
|
+
printStatus(key, 'ok', null, { clearAfter: 3500 })
|
|
953
|
+
}
|
|
941
954
|
return { cleared: !!result?.cleared, printed: !!shouldPrint, showedNext: !!result?.showedNext, active }
|
|
942
955
|
}
|
|
943
956
|
|
|
@@ -1011,7 +1024,7 @@ export function serve(entrypoint, flags) {
|
|
|
1011
1024
|
// No change at all — skip
|
|
1012
1025
|
if (out.changeType === 'none' || out.changeType === 'cached') return
|
|
1013
1026
|
|
|
1014
|
-
if (!success.printed && !success.showedNext && !success.active) printStatus(rel, 'ok')
|
|
1027
|
+
if (!success.printed && !success.showedNext && !success.active) printStatus(rel, 'ok', null, { clearAfter: 3500 })
|
|
1015
1028
|
broadcast({ type: 'update', file: rel, slots: out.slots || 'shifted' })
|
|
1016
1029
|
} catch(e) {
|
|
1017
1030
|
if (!isCurrentChange(file, version)) return
|