bimba-cli 0.7.30 → 0.7.32
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 +185 -70
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,29 +627,69 @@ 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
|
|
637
|
-
let
|
|
635
|
+
let _statusInline = false
|
|
636
|
+
let _statusWidth = 0
|
|
637
|
+
let _eraseTimers = []
|
|
638
638
|
const _isTTY = process.stdout.isTTY
|
|
639
639
|
|
|
640
|
-
function
|
|
641
|
-
|
|
642
|
-
|
|
640
|
+
function stripAnsi(text) {
|
|
641
|
+
return String(text).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '')
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function renderedRows(lines) {
|
|
645
|
+
const columns = Math.max(1, process.stdout.columns || 80)
|
|
646
|
+
return lines.reduce((total, line) => {
|
|
647
|
+
const length = stripAnsi(line).length
|
|
648
|
+
return total + Math.max(1, Math.ceil(length / columns))
|
|
649
|
+
}, 0)
|
|
643
650
|
}
|
|
644
651
|
|
|
645
652
|
function clearStatus(file) {
|
|
646
|
-
if (file && _statusFile && _statusFile !== file) return
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
653
|
+
if (file && _statusFile && _statusFile !== file) return false
|
|
654
|
+
if (_statusTimer) {
|
|
655
|
+
clearTimeout(_statusTimer)
|
|
656
|
+
_statusTimer = null
|
|
657
|
+
}
|
|
658
|
+
_eraseTimers.forEach(timer => clearTimeout(timer))
|
|
659
|
+
_eraseTimers = []
|
|
660
|
+
if (_isTTY && _statusInline) {
|
|
661
|
+
process.stdout.write('\r\x1b[J')
|
|
662
|
+
} else if (_isTTY && _statusRows) {
|
|
663
|
+
process.stdout.write(`\x1b[${_statusRows}A\r\x1b[J`)
|
|
651
664
|
}
|
|
665
|
+
_statusRows = 0
|
|
652
666
|
_statusFile = null
|
|
653
|
-
|
|
667
|
+
_statusInline = false
|
|
668
|
+
_statusWidth = 0
|
|
669
|
+
return true
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function formatErrorLines(errors) {
|
|
673
|
+
const lines = []
|
|
674
|
+
for (const err of errors || []) {
|
|
675
|
+
const message = errorMessage(err)
|
|
676
|
+
const line = errorLine(err)
|
|
677
|
+
lines.push(` ${theme.error(' ' + message + ' ')}${line != null ? theme.margin(` line ${line + 1} `) : ''}`)
|
|
678
|
+
const snippet = errorSnippet(err)
|
|
679
|
+
if (snippet && snippet !== message) {
|
|
680
|
+
lines.push(...String(snippet).split('\n').slice(0, 6).map(item => ` ${theme.code(item)}`))
|
|
681
|
+
}
|
|
682
|
+
lines.push('')
|
|
683
|
+
}
|
|
684
|
+
return lines
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function statusLines(file, state, errors) {
|
|
688
|
+
const now = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
689
|
+
const status = state === 'ok' ? theme.success(' ok ') : theme.failure(' fail ')
|
|
690
|
+
const lines = [` ${theme.folder(now)} ${theme.filename(file)} ${status}`]
|
|
691
|
+
if (errors?.length) lines.push('', ...formatErrorLines(errors))
|
|
692
|
+
return lines
|
|
654
693
|
}
|
|
655
694
|
|
|
656
695
|
function printStatus(file, state, errors, options = {}) {
|
|
@@ -670,45 +709,46 @@ export function serve(entrypoint, flags) {
|
|
|
670
709
|
return
|
|
671
710
|
}
|
|
672
711
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
712
|
+
clearStatus()
|
|
713
|
+
_statusFile = file
|
|
714
|
+
const lines = statusLines(file, state, errors)
|
|
715
|
+
if (options.fadeAfter && lines.length === 1) {
|
|
716
|
+
const plain = stripAnsi(lines[0])
|
|
717
|
+
process.stdout.write(lines[0])
|
|
718
|
+
_statusRows = 1
|
|
719
|
+
_statusInline = true
|
|
720
|
+
_statusWidth = plain.length
|
|
721
|
+
_statusTimer = setTimeout(() => eraseStatus(file), options.fadeAfter)
|
|
722
|
+
return
|
|
677
723
|
}
|
|
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
724
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
725
|
+
process.stdout.write(lines.join('\n') + '\n')
|
|
726
|
+
_statusRows = renderedRows(lines)
|
|
727
|
+
|
|
728
|
+
if (options.clearAfter) _statusTimer = setTimeout(() => clearStatus(file), options.clearAfter)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function eraseStatus(file) {
|
|
732
|
+
if (file && _statusFile && _statusFile !== file) return
|
|
733
|
+
_statusTimer = null
|
|
734
|
+
if (!_isTTY || !_statusInline) {
|
|
735
|
+
clearStatus(file)
|
|
690
736
|
return
|
|
691
737
|
}
|
|
692
738
|
|
|
693
|
-
|
|
694
|
-
_statusSaved = true
|
|
695
|
-
_statusFile = file
|
|
696
|
-
_statusKind = state
|
|
697
|
-
|
|
698
|
-
const myId = ++_fadeId
|
|
699
|
-
const plainLine = ` ${now} ${file} ok `
|
|
700
|
-
const total = plainLine.length
|
|
701
|
-
process.stdout.write(` ${theme.folder(now)} ${theme.filename(file)} ${status}`)
|
|
739
|
+
const total = _statusWidth
|
|
702
740
|
for (let i = 1; i <= total; i++) {
|
|
703
|
-
|
|
704
|
-
if (
|
|
741
|
+
_eraseTimers.push(setTimeout(() => {
|
|
742
|
+
if (!_statusInline || _statusFile !== file) return
|
|
705
743
|
process.stdout.write('\x1b[1D \x1b[1D')
|
|
706
744
|
if (i === total) {
|
|
707
|
-
|
|
745
|
+
_statusRows = 0
|
|
708
746
|
_statusFile = null
|
|
709
|
-
|
|
747
|
+
_statusInline = false
|
|
748
|
+
_statusWidth = 0
|
|
749
|
+
_eraseTimers = []
|
|
710
750
|
}
|
|
711
|
-
},
|
|
751
|
+
}, i * 22))
|
|
712
752
|
}
|
|
713
753
|
}
|
|
714
754
|
|
|
@@ -853,9 +893,72 @@ export function serve(entrypoint, flags) {
|
|
|
853
893
|
.join('\n---\n')
|
|
854
894
|
}
|
|
855
895
|
|
|
896
|
+
function renderActiveErrors() {
|
|
897
|
+
if (!_isTTY) return false
|
|
898
|
+
if (!_activeErrors.size) {
|
|
899
|
+
clearStatus()
|
|
900
|
+
return false
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
clearStatus()
|
|
904
|
+
const lines = []
|
|
905
|
+
for (const item of _activeErrors.values()) {
|
|
906
|
+
if (lines.length) lines.push('')
|
|
907
|
+
lines.push(...statusLines(item.file, 'fail', item.errors))
|
|
908
|
+
}
|
|
909
|
+
process.stdout.write(lines.join('\n') + '\n')
|
|
910
|
+
_statusFile = null
|
|
911
|
+
_statusRows = renderedRows(lines)
|
|
912
|
+
return true
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function existingFileForError(file) {
|
|
916
|
+
for (const candidate of fileCandidates(file)) {
|
|
917
|
+
try {
|
|
918
|
+
const stat = statSync(candidate)
|
|
919
|
+
if (stat.isFile()) return candidate
|
|
920
|
+
} catch(_) {
|
|
921
|
+
// ignore aliases that no longer exist
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return null
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function reconcileActiveErrors() {
|
|
928
|
+
if (!_activeErrors.size) return false
|
|
929
|
+
|
|
930
|
+
let changed = false
|
|
931
|
+
const items = Array.from(_activeErrors.values())
|
|
932
|
+
for (const item of items) {
|
|
933
|
+
const filepath = existingFileForError(item.file)
|
|
934
|
+
if (!filepath) {
|
|
935
|
+
if (takeError(item.file)) {
|
|
936
|
+
_terminalErrors.delete(terminalErrorKey(item.file))
|
|
937
|
+
broadcast({ type: 'clear-error', file: item.file })
|
|
938
|
+
changed = true
|
|
939
|
+
}
|
|
940
|
+
continue
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (!filepath.endsWith('.imba')) continue
|
|
944
|
+
|
|
945
|
+
const out = await compileFile(filepath)
|
|
946
|
+
if (out.errors?.length) continue
|
|
947
|
+
|
|
948
|
+
if (takeError(item.file)) {
|
|
949
|
+
_terminalErrors.delete(terminalErrorKey(item.file))
|
|
950
|
+
broadcast({ type: 'clear-error', file: item.file })
|
|
951
|
+
changed = true
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return changed
|
|
956
|
+
}
|
|
957
|
+
|
|
856
958
|
function showTrackedError(item) {
|
|
857
959
|
const file = item.file
|
|
858
|
-
|
|
960
|
+
if (_isTTY) renderActiveErrors()
|
|
961
|
+
else printStatus(file, 'fail', item.errors)
|
|
859
962
|
broadcast({ type: 'error', file, time: item.time, errors: item.payload })
|
|
860
963
|
}
|
|
861
964
|
|
|
@@ -884,9 +987,10 @@ export function serve(entrypoint, flags) {
|
|
|
884
987
|
_activeErrors.set(key, item)
|
|
885
988
|
_terminalErrors.set(terminalKey, { signature: printSignature, time: now })
|
|
886
989
|
|
|
887
|
-
//
|
|
888
|
-
//
|
|
990
|
+
// Repeated reports of the same active error update the live status and
|
|
991
|
+
// browser overlay, but do not create another terminal entry.
|
|
889
992
|
if (duplicate) {
|
|
993
|
+
if (_isTTY) renderActiveErrors()
|
|
890
994
|
broadcast({ type: 'error', file: display, time: item.time, errors: item.payload })
|
|
891
995
|
return
|
|
892
996
|
}
|
|
@@ -909,25 +1013,36 @@ export function serve(entrypoint, flags) {
|
|
|
909
1013
|
const wasStatusFile = key && _statusFile && sameFile(_statusFile, key)
|
|
910
1014
|
const hadError = key ? !!takeError(key) : _activeErrors.size > 0
|
|
911
1015
|
|
|
912
|
-
if (!key)
|
|
1016
|
+
if (!key) {
|
|
1017
|
+
_activeErrors.clear()
|
|
1018
|
+
_terminalErrors.clear()
|
|
1019
|
+
} else if (hadError) {
|
|
1020
|
+
_terminalErrors.delete(terminalErrorKey(key))
|
|
1021
|
+
}
|
|
913
1022
|
|
|
914
|
-
|
|
1023
|
+
let showedNext = false
|
|
1024
|
+
if (_isTTY && _activeErrors.size) showedNext = renderActiveErrors()
|
|
1025
|
+
else if (!key || hadError || wasStatusFile) clearStatus(key)
|
|
915
1026
|
|
|
916
1027
|
broadcast({ type: 'clear-error', file: key })
|
|
917
1028
|
|
|
918
|
-
return { cleared: hadError || wasStatusFile, file: key, showedNext
|
|
1029
|
+
return { cleared: hadError || wasStatusFile, file: key, showedNext }
|
|
919
1030
|
}
|
|
920
1031
|
|
|
921
|
-
function markSuccess(file) {
|
|
1032
|
+
async function markSuccess(file) {
|
|
922
1033
|
const key = normalizeFile(file)
|
|
923
1034
|
const result = clearError(key)
|
|
1035
|
+
const reconciled = await reconcileActiveErrors()
|
|
924
1036
|
const active = _activeErrors.size
|
|
925
|
-
|
|
1037
|
+
let showedNext = false
|
|
1038
|
+
if (active && _isTTY) showedNext = renderActiveErrors()
|
|
1039
|
+
else if (_isTTY && (reconciled || result.showedNext)) clearStatus()
|
|
1040
|
+
else showedNext = result.showedNext
|
|
1041
|
+
const shouldPrint = (result?.cleared || reconciled) && !showedNext && !active
|
|
926
1042
|
if (shouldPrint) {
|
|
927
|
-
|
|
928
|
-
printStatus(key, 'ok', null, { sticky: true })
|
|
1043
|
+
printStatus(key, 'ok', null, { fadeAfter: 3500 })
|
|
929
1044
|
}
|
|
930
|
-
return { cleared: !!result?.cleared, printed: !!shouldPrint, showedNext: !!
|
|
1045
|
+
return { cleared: !!result?.cleared || reconciled, printed: !!shouldPrint, showedNext: !!showedNext, active }
|
|
931
1046
|
}
|
|
932
1047
|
|
|
933
1048
|
const _debounce = new Map()
|
|
@@ -995,12 +1110,12 @@ export function serve(entrypoint, flags) {
|
|
|
995
1110
|
return
|
|
996
1111
|
}
|
|
997
1112
|
|
|
998
|
-
const success = markSuccess(rel)
|
|
1113
|
+
const success = await markSuccess(rel)
|
|
999
1114
|
|
|
1000
1115
|
// No change at all — skip
|
|
1001
1116
|
if (out.changeType === 'none' || out.changeType === 'cached') return
|
|
1002
1117
|
|
|
1003
|
-
if (!success.printed && !success.showedNext && !success.active) printStatus(rel, 'ok')
|
|
1118
|
+
if (!success.printed && !success.showedNext && !success.active) printStatus(rel, 'ok', null, { fadeAfter: 3500 })
|
|
1004
1119
|
broadcast({ type: 'update', file: rel, slots: out.slots || 'shifted' })
|
|
1005
1120
|
} catch(e) {
|
|
1006
1121
|
if (!isCurrentChange(file, version)) return
|
|
@@ -1038,7 +1153,7 @@ export function serve(entrypoint, flags) {
|
|
|
1038
1153
|
const file = 'vendor:' + (specifier || pathname)
|
|
1039
1154
|
const bundled = specifier ? await bundleVendor(specifier) : null
|
|
1040
1155
|
if (bundled?.code) {
|
|
1041
|
-
markSuccess(file)
|
|
1156
|
+
await markSuccess(file)
|
|
1042
1157
|
return new Response(bundled.code, { headers: { 'Content-Type': 'application/javascript' } })
|
|
1043
1158
|
}
|
|
1044
1159
|
|
|
@@ -1051,7 +1166,7 @@ export function serve(entrypoint, flags) {
|
|
|
1051
1166
|
const file = 'html:' + normalizeFile(htmlFile)
|
|
1052
1167
|
try {
|
|
1053
1168
|
let html = await Bun.file(htmlFile).text()
|
|
1054
|
-
markSuccess(file)
|
|
1169
|
+
await markSuccess(file)
|
|
1055
1170
|
return new Response(transformHtml(html, entrypoint), {
|
|
1056
1171
|
headers: { 'Content-Type': 'text/html' },
|
|
1057
1172
|
})
|
|
@@ -1077,7 +1192,7 @@ export function serve(entrypoint, flags) {
|
|
|
1077
1192
|
if (out.errors?.length) {
|
|
1078
1193
|
return errorResponse(file, out.errors)
|
|
1079
1194
|
}
|
|
1080
|
-
markSuccess(file)
|
|
1195
|
+
await markSuccess(file)
|
|
1081
1196
|
return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
|
|
1082
1197
|
} catch(e) {
|
|
1083
1198
|
if (isMissingFileError(e)) {
|
|
@@ -1099,7 +1214,7 @@ export function serve(entrypoint, flags) {
|
|
|
1099
1214
|
try {
|
|
1100
1215
|
if (cssFile && await cssFile.exists()) {
|
|
1101
1216
|
if (req.headers.get('sec-fetch-dest') === 'style') {
|
|
1102
|
-
markSuccess(file)
|
|
1217
|
+
await markSuccess(file)
|
|
1103
1218
|
return new Response(cssFile, { headers: { 'Content-Type': 'text/css' } })
|
|
1104
1219
|
}
|
|
1105
1220
|
|
|
@@ -1111,7 +1226,7 @@ export function serve(entrypoint, flags) {
|
|
|
1111
1226
|
`if (!el) { el = document.createElement('style'); el.setAttribute('data-bimba-css', id); document.head.appendChild(el); }`,
|
|
1112
1227
|
`el.textContent = ${JSON.stringify(css)};`,
|
|
1113
1228
|
].join('\n')
|
|
1114
|
-
markSuccess(file)
|
|
1229
|
+
await markSuccess(file)
|
|
1115
1230
|
return new Response(js, { headers: { 'Content-Type': 'application/javascript' } })
|
|
1116
1231
|
}
|
|
1117
1232
|
} catch (error) {
|
|
@@ -1129,7 +1244,7 @@ export function serve(entrypoint, flags) {
|
|
|
1129
1244
|
const file = 'js:' + normalizeFile(jsFile)
|
|
1130
1245
|
try {
|
|
1131
1246
|
const response = await serveJavaScriptFile(jsFile)
|
|
1132
|
-
markSuccess(file)
|
|
1247
|
+
await markSuccess(file)
|
|
1133
1248
|
return response
|
|
1134
1249
|
} catch (error) {
|
|
1135
1250
|
if (isMissingFileError(error)) {
|
|
@@ -1156,7 +1271,7 @@ export function serve(entrypoint, flags) {
|
|
|
1156
1271
|
if (out.errors?.length) {
|
|
1157
1272
|
return errorResponse(file, out.errors)
|
|
1158
1273
|
}
|
|
1159
|
-
markSuccess(file)
|
|
1274
|
+
await markSuccess(file)
|
|
1160
1275
|
return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
|
|
1161
1276
|
}
|
|
1162
1277
|
|
|
@@ -1164,7 +1279,7 @@ export function serve(entrypoint, flags) {
|
|
|
1164
1279
|
const file = 'vendor:' + normalizeFile(pathname)
|
|
1165
1280
|
const bundled = await bundleVendor(path.resolve(resolved))
|
|
1166
1281
|
if (bundled?.code) {
|
|
1167
|
-
markSuccess(file)
|
|
1282
|
+
await markSuccess(file)
|
|
1168
1283
|
return new Response(bundled.code, { headers: { 'Content-Type': 'application/javascript' } })
|
|
1169
1284
|
}
|
|
1170
1285
|
return errorResponse(file, bundled?.errors || [`Could not bundle ${pathname}`])
|
|
@@ -1176,13 +1291,13 @@ export function serve(entrypoint, flags) {
|
|
|
1176
1291
|
const inHtmlDirPath = path.join(htmlDir, pathname)
|
|
1177
1292
|
const inHtmlDir = Bun.file(inHtmlDirPath)
|
|
1178
1293
|
if (await inHtmlDir.exists()) {
|
|
1179
|
-
markSuccess('static:' + normalizeFile(inHtmlDirPath))
|
|
1294
|
+
await markSuccess('static:' + normalizeFile(inHtmlDirPath))
|
|
1180
1295
|
return new Response(inHtmlDir)
|
|
1181
1296
|
}
|
|
1182
1297
|
const inRootPath = '.' + pathname
|
|
1183
1298
|
const inRoot = Bun.file(inRootPath)
|
|
1184
1299
|
if (await inRoot.exists()) {
|
|
1185
|
-
markSuccess('static:' + normalizeFile(inRootPath))
|
|
1300
|
+
await markSuccess('static:' + normalizeFile(inRootPath))
|
|
1186
1301
|
return new Response(inRoot)
|
|
1187
1302
|
}
|
|
1188
1303
|
} catch (error) {
|
|
@@ -1204,7 +1319,7 @@ export function serve(entrypoint, flags) {
|
|
|
1204
1319
|
if (out.errors?.length) {
|
|
1205
1320
|
return errorResponse(file, out.errors)
|
|
1206
1321
|
}
|
|
1207
|
-
markSuccess(file)
|
|
1322
|
+
await markSuccess(file)
|
|
1208
1323
|
return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
|
|
1209
1324
|
}
|
|
1210
1325
|
for (const ext of ['.js', '.mjs']) {
|
|
@@ -1213,7 +1328,7 @@ export function serve(entrypoint, flags) {
|
|
|
1213
1328
|
const file = 'js:' + normalizeFile(withExt)
|
|
1214
1329
|
try {
|
|
1215
1330
|
const response = await serveJavaScriptFile(withExt)
|
|
1216
|
-
markSuccess(file)
|
|
1331
|
+
await markSuccess(file)
|
|
1217
1332
|
return response
|
|
1218
1333
|
} catch (error) {
|
|
1219
1334
|
if (isMissingFileError(error)) {
|
|
@@ -1231,7 +1346,7 @@ export function serve(entrypoint, flags) {
|
|
|
1231
1346
|
const file = 'html:' + normalizeFile(htmlPath)
|
|
1232
1347
|
try {
|
|
1233
1348
|
let html = await Bun.file(htmlPath).text()
|
|
1234
|
-
markSuccess(file)
|
|
1349
|
+
await markSuccess(file)
|
|
1235
1350
|
return new Response(transformHtml(html, entrypoint), {
|
|
1236
1351
|
headers: { 'Content-Type': 'text/html' },
|
|
1237
1352
|
})
|