bimba-cli 0.7.28 → 0.7.30
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/plugin.js +82 -56
- package/serve.js +116 -106
package/package.json
CHANGED
package/plugin.js
CHANGED
|
@@ -45,6 +45,16 @@ function compileErrorKey(filepath) {
|
|
|
45
45
|
return physicalCompileKey(filepath) || `path:${normalizeCompilePath(filepath)}`;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function compileFileStamp(filepath) {
|
|
49
|
+
try {
|
|
50
|
+
const stat = fs.statSync(filepath);
|
|
51
|
+
if (!stat.isFile()) return null;
|
|
52
|
+
return `${stat.mtimeMs ?? stat.mtime?.getTime?.() ?? 0}_${stat.size ?? 0}`;
|
|
53
|
+
} catch(_) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
function compileErrorMessage(error) {
|
|
49
59
|
return error?.message || String(error);
|
|
50
60
|
}
|
|
@@ -102,66 +112,82 @@ export const imbaPlugin = {
|
|
|
102
112
|
build.onLoad({ filter: /\.imba$/ }, async ({ path }) => {
|
|
103
113
|
|
|
104
114
|
const f = dir.parse(path)
|
|
105
|
-
let contents = '';
|
|
106
|
-
|
|
107
|
-
// return the cached version if exists (include target in hash to avoid cross-platform cache hits)
|
|
108
|
-
const cached = dir.join(cache, Bun.hash(path + ':' + target) + '_' + fs.statSync(path).mtimeMs + '.js');
|
|
109
|
-
if (fs.existsSync(cached)) {
|
|
110
|
-
clearCompileError(path);
|
|
111
|
-
stats.bundled++;
|
|
112
|
-
stats.cached++;
|
|
113
|
-
return {
|
|
114
|
-
contents: await Bun.file(cached).text(),
|
|
115
|
-
loader: "js",
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
115
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
116
|
+
while (true) {
|
|
117
|
+
const stamp = compileFileStamp(path);
|
|
118
|
+
if (!stamp) {
|
|
119
|
+
clearCompileError(path);
|
|
120
|
+
return { contents: '', loader: "js" };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let contents = '';
|
|
124
|
+
|
|
125
|
+
// return the cached version if exists (include target in hash to avoid cross-platform cache hits)
|
|
126
|
+
const cached = dir.join(cache, Bun.hash(path + ':' + target) + '_' + stamp + '.js');
|
|
127
|
+
if (fs.existsSync(cached)) {
|
|
128
|
+
const cachedContents = await Bun.file(cached).text();
|
|
129
|
+
if (compileFileStamp(path) !== stamp) continue;
|
|
130
|
+
clearCompileError(path);
|
|
131
|
+
stats.bundled++;
|
|
132
|
+
stats.cached++;
|
|
133
|
+
return {
|
|
134
|
+
contents: cachedContents,
|
|
135
|
+
loader: "js",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// clear previous cached version
|
|
140
|
+
const glob = new Glob(Bun.hash(path + ':' + target) + '_' + "*.js");
|
|
141
|
+
for await (const file of glob.scan(cache)) if (fs.existsSync(dir.join(cache, file))) unlink(dir.join(cache, file));
|
|
142
|
+
|
|
143
|
+
// if no cached version read and compile it with the imba compiler
|
|
144
|
+
const file = await Bun.file(path).text();
|
|
145
|
+
if (compileFileStamp(path) !== stamp) continue;
|
|
146
|
+
|
|
147
|
+
const platform = target === 'node' || target === 'bun' ? 'node' : 'browser';
|
|
148
|
+
let out
|
|
149
|
+
try {
|
|
150
|
+
out = compiler.compile(file, {
|
|
151
|
+
sourcePath: path,
|
|
152
|
+
platform: platform,
|
|
153
|
+
comments: false
|
|
154
|
+
})
|
|
155
|
+
} catch (error) {
|
|
156
|
+
out = { js: '', errors: [error] }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Never report or cache a result from an older source snapshot.
|
|
160
|
+
if (compileFileStamp(path) !== stamp) continue;
|
|
161
|
+
|
|
162
|
+
// the file has been successfully compiled
|
|
163
|
+
if (!out.errors?.length) {
|
|
164
|
+
clearCompileError(path);
|
|
165
|
+
console.log(theme.action("compiling: ") + theme.folder(dir.join(f.dir,'/')) + theme.filename(f.base) + " - " + theme.success("compiled"));
|
|
166
|
+
stats.bundled++;
|
|
167
|
+
stats.compiled++;
|
|
168
|
+
contents = out.js;
|
|
169
|
+
await Bun.write(cached, contents);
|
|
170
|
+
}
|
|
171
|
+
// there were errors during compilation
|
|
172
|
+
else {
|
|
173
|
+
const shouldPrint = shouldPrintCompileError(path, out.errors);
|
|
174
|
+
if (shouldPrint) console.log(theme.action("compiling: ") + theme.folder(dir.join(f.dir,'/')) + theme.filename(f.base) + " - " + theme.failure(" fail "));
|
|
175
|
+
stats.failed++;
|
|
176
|
+
if (shouldPrint) {
|
|
177
|
+
stats.reported++;
|
|
178
|
+
for (let i = 0; i < out.errors.length; i++) {
|
|
179
|
+
if(out.errors[i]) printCompileError(out.errors[i]);
|
|
180
|
+
}
|
|
155
181
|
}
|
|
182
|
+
stats.errors++;
|
|
156
183
|
}
|
|
157
|
-
|
|
184
|
+
|
|
185
|
+
// and return the compiled source code as "js"
|
|
186
|
+
return {
|
|
187
|
+
contents,
|
|
188
|
+
loader: "js",
|
|
189
|
+
};
|
|
158
190
|
}
|
|
159
|
-
|
|
160
|
-
// and return the compiled source code as "js"
|
|
161
|
-
return {
|
|
162
|
-
contents,
|
|
163
|
-
loader: "js",
|
|
164
|
-
};
|
|
165
191
|
});
|
|
166
192
|
}
|
|
167
193
|
};
|
package/serve.js
CHANGED
|
@@ -409,6 +409,17 @@ function isMissingFileError(error) {
|
|
|
409
409
|
return error?.code === 'ENOENT' || String(error?.message || error).includes('ENOENT: no such file or directory')
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
function fileStamp(abs) {
|
|
413
|
+
try {
|
|
414
|
+
const stat = statSync(abs)
|
|
415
|
+
if (!stat.isFile()) return null
|
|
416
|
+
return `${stat.mtimeMs ?? stat.mtime?.getTime?.() ?? 0}:${stat.size ?? 0}`
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (isMissingFileError(error)) return null
|
|
419
|
+
throw error
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
412
423
|
function missingCompileResult(filepath) {
|
|
413
424
|
dropFileState(filepath)
|
|
414
425
|
return { js: '', errors: [], slots: null, changeType: 'missing', missing: true }
|
|
@@ -416,49 +427,59 @@ function missingCompileResult(filepath) {
|
|
|
416
427
|
|
|
417
428
|
async function compileFile(filepath) {
|
|
418
429
|
const abs = path.resolve(filepath)
|
|
419
|
-
if (!existsSync(abs)) return missingCompileResult(abs)
|
|
420
430
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
stat = await file.stat()
|
|
425
|
-
} catch (error) {
|
|
426
|
-
if (isMissingFileError(error)) return missingCompileResult(abs)
|
|
427
|
-
throw error
|
|
428
|
-
}
|
|
429
|
-
const stamp = `${stat.mtimeMs ?? stat.mtime?.getTime?.() ?? 0}:${stat.size ?? 0}`
|
|
431
|
+
while (true) {
|
|
432
|
+
const stamp = fileStamp(abs)
|
|
433
|
+
if (!stamp) return missingCompileResult(abs)
|
|
430
434
|
|
|
431
|
-
|
|
432
|
-
|
|
435
|
+
const cached = _compileCache.get(abs)
|
|
436
|
+
if (cached && cached.stamp === stamp) {
|
|
437
|
+
if (fileStamp(abs) !== stamp) continue
|
|
438
|
+
return _normalizeResult(cached.result, { changeType: 'cached' })
|
|
439
|
+
}
|
|
433
440
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
sourcePath: filepath,
|
|
443
|
-
platform: 'browser',
|
|
444
|
-
sourcemap: 'inline',
|
|
445
|
-
})
|
|
441
|
+
const file = Bun.file(abs)
|
|
442
|
+
let code
|
|
443
|
+
try {
|
|
444
|
+
code = await file.text()
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (isMissingFileError(error)) return missingCompileResult(abs)
|
|
447
|
+
throw error
|
|
448
|
+
}
|
|
446
449
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
450
|
+
// A save can land while an older request is compiling. Never publish,
|
|
451
|
+
// cache, or report a result unless it still matches the current file.
|
|
452
|
+
if (fileStamp(abs) !== stamp) continue
|
|
453
|
+
|
|
454
|
+
let result
|
|
455
|
+
try {
|
|
456
|
+
result = compiler.compile(code, {
|
|
457
|
+
sourcePath: filepath,
|
|
458
|
+
platform: 'browser',
|
|
459
|
+
sourcemap: 'inline',
|
|
460
|
+
})
|
|
461
|
+
} catch (error) {
|
|
462
|
+
result = { js: '', errors: [error], slots: null }
|
|
463
|
+
}
|
|
455
464
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
465
|
+
if (fileStamp(abs) !== stamp) continue
|
|
466
|
+
|
|
467
|
+
const errors = result.errors || []
|
|
468
|
+
if (!errors.length && result.js) {
|
|
469
|
+
const { js, slotCount } = stabilizeSymbols(result.js, abs)
|
|
470
|
+
result.js = rewriteBareImports(js)
|
|
471
|
+
const prev = _prevSlots.get(abs)
|
|
472
|
+
result.slots = (prev === undefined || prev === slotCount) ? 'stable' : 'shifted'
|
|
473
|
+
_prevSlots.set(abs, slotCount)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Bake errors as an own property so caching/spreading preserves them.
|
|
477
|
+
const baked = { js: result.js, errors, slots: result.slots }
|
|
478
|
+
const changeType = _prevJs.get(abs) === baked.js ? 'none' : 'full'
|
|
479
|
+
_prevJs.set(abs, baked.js)
|
|
480
|
+
_compileCache.set(abs, { stamp, result: baked })
|
|
481
|
+
return _normalizeResult(baked, { changeType })
|
|
482
|
+
}
|
|
462
483
|
}
|
|
463
484
|
|
|
464
485
|
// ─── HTML helpers ─────────────────────────────────────────────────────────────
|
|
@@ -632,7 +653,7 @@ export function serve(entrypoint, flags) {
|
|
|
632
653
|
_statusKind = null
|
|
633
654
|
}
|
|
634
655
|
|
|
635
|
-
function printStatus(file, state, errors) {
|
|
656
|
+
function printStatus(file, state, errors, options = {}) {
|
|
636
657
|
// non-TTY (pipes, Claude Code bash, CI): plain newline-terminated output,
|
|
637
658
|
// no ANSI cursor tricks, no fade-out — so logs stay readable.
|
|
638
659
|
if (!_isTTY) {
|
|
@@ -657,66 +678,45 @@ export function serve(entrypoint, flags) {
|
|
|
657
678
|
const now = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
658
679
|
const status = state === 'ok' ? theme.success(' ok ') : theme.failure(' fail ')
|
|
659
680
|
|
|
681
|
+
if (errors?.length || options.sticky) {
|
|
682
|
+
process.stdout.write(` ${theme.folder(now)} ${theme.filename(file)} ${status}\n`)
|
|
683
|
+
if (errors?.length) {
|
|
684
|
+
for (const err of errors) {
|
|
685
|
+
try { printerr(err) } catch(_) { process.stdout.write(' ' + err.message + '\n') }
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
_statusFile = null
|
|
689
|
+
_statusKind = null
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
|
|
660
693
|
process.stdout.write('\x1b[s')
|
|
661
694
|
_statusSaved = true
|
|
662
695
|
_statusFile = file
|
|
663
696
|
_statusKind = state
|
|
664
697
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
if (i === total) {
|
|
680
|
-
_statusSaved = false
|
|
681
|
-
_statusFile = null
|
|
682
|
-
_statusKind = null
|
|
683
|
-
}
|
|
684
|
-
}, 5000 + i * 22))
|
|
685
|
-
}
|
|
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}`)
|
|
702
|
+
for (let i = 1; i <= total; i++) {
|
|
703
|
+
_fadeTimers.push(setTimeout(() => {
|
|
704
|
+
if (_fadeId !== myId) return
|
|
705
|
+
process.stdout.write('\x1b[1D \x1b[1D')
|
|
706
|
+
if (i === total) {
|
|
707
|
+
_statusSaved = false
|
|
708
|
+
_statusFile = null
|
|
709
|
+
_statusKind = null
|
|
710
|
+
}
|
|
711
|
+
}, 5000 + i * 22))
|
|
686
712
|
}
|
|
687
713
|
}
|
|
688
714
|
|
|
689
715
|
// ── File watcher ───────────────────────────────────────────────────────────
|
|
690
716
|
|
|
691
717
|
const _activeErrors = new Map()
|
|
692
|
-
|
|
693
|
-
|
|
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 item of _activeErrors.values()) {
|
|
711
|
-
const file = item.file
|
|
712
|
-
process.stdout.write(` ${theme.folder(item.time)} ${theme.filename(file)} ${theme.failure(' fail ')}\n`)
|
|
713
|
-
for (const err of item.errors) {
|
|
714
|
-
try { printerr(err) } catch(_) { process.stdout.write(' ' + err.message + '\n') }
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
return true
|
|
719
|
-
}
|
|
718
|
+
const _terminalErrors = new Map()
|
|
719
|
+
const TERMINAL_DUPLICATE_MS = 5000
|
|
720
720
|
|
|
721
721
|
function broadcast(payload) {
|
|
722
722
|
const msg = JSON.stringify(payload)
|
|
@@ -753,6 +753,12 @@ export function serve(entrypoint, flags) {
|
|
|
753
753
|
return Array.from(variants).filter(Boolean)
|
|
754
754
|
}
|
|
755
755
|
|
|
756
|
+
function terminalErrorKey(file) {
|
|
757
|
+
const variants = fileVariants(file)
|
|
758
|
+
const rooted = variants.find(variant => srcRel && variant.startsWith(srcRel + '/'))
|
|
759
|
+
return `path:${rooted || variants[0] || normalizeFile(file)}`
|
|
760
|
+
}
|
|
761
|
+
|
|
756
762
|
function fileCandidates(file) {
|
|
757
763
|
const candidates = []
|
|
758
764
|
for (const variant of fileVariants(file)) {
|
|
@@ -841,29 +847,42 @@ export function serve(entrypoint, flags) {
|
|
|
841
847
|
.join('\n---\n')
|
|
842
848
|
}
|
|
843
849
|
|
|
850
|
+
function terminalErrorSignature(errors) {
|
|
851
|
+
return serializeErrors(errors)
|
|
852
|
+
.map(error => error.message)
|
|
853
|
+
.join('\n---\n')
|
|
854
|
+
}
|
|
855
|
+
|
|
844
856
|
function showTrackedError(item) {
|
|
845
857
|
const file = item.file
|
|
846
|
-
|
|
847
|
-
else printStatus(file, 'fail', item.errors)
|
|
858
|
+
printStatus(file, 'fail', item.errors)
|
|
848
859
|
broadcast({ type: 'error', file, time: item.time, errors: item.payload })
|
|
849
860
|
}
|
|
850
861
|
|
|
851
862
|
function reportError(file, errors) {
|
|
852
863
|
const display = normalizeFile(file)
|
|
853
864
|
const key = errorKey(display)
|
|
865
|
+
const terminalKey = terminalErrorKey(display)
|
|
854
866
|
const list = Array.isArray(errors) ? errors : [errors]
|
|
855
867
|
const signature = errorSignature(list)
|
|
868
|
+
const printSignature = terminalErrorSignature(list)
|
|
856
869
|
const previous = takeError(display)
|
|
870
|
+
const now = Date.now()
|
|
871
|
+
const recent = _terminalErrors.get(terminalKey)
|
|
857
872
|
const duplicate = previous?.signature === signature
|
|
873
|
+
|| previous?.printSignature === printSignature
|
|
874
|
+
|| (recent?.signature === printSignature && now - recent.time < TERMINAL_DUPLICATE_MS)
|
|
858
875
|
|
|
859
876
|
const item = {
|
|
860
877
|
file: display,
|
|
861
878
|
signature,
|
|
879
|
+
printSignature,
|
|
862
880
|
errors: list,
|
|
863
881
|
payload: serializeErrors(list),
|
|
864
882
|
time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
|
|
865
883
|
}
|
|
866
884
|
_activeErrors.set(key, item)
|
|
885
|
+
_terminalErrors.set(terminalKey, { signature: printSignature, time: now })
|
|
867
886
|
|
|
868
887
|
// The terminal is append-only in many real shells. Repeated reports of the
|
|
869
888
|
// same active error update the browser overlay, but must not print again.
|
|
@@ -887,36 +906,27 @@ export function serve(entrypoint, flags) {
|
|
|
887
906
|
|
|
888
907
|
function clearError(file) {
|
|
889
908
|
const key = file ? normalizeFile(file) : null
|
|
890
|
-
const hadPanel = _statusKind === 'errors'
|
|
891
909
|
const wasStatusFile = key && _statusFile && sameFile(_statusFile, key)
|
|
892
910
|
const hadError = key ? !!takeError(key) : _activeErrors.size > 0
|
|
893
911
|
|
|
894
912
|
if (!key) _activeErrors.clear()
|
|
895
913
|
|
|
896
|
-
|
|
897
|
-
if (_isTTY && (hadError || wasStatusFile || hadPanel)) {
|
|
898
|
-
showedNext = renderErrorPanel()
|
|
899
|
-
} else if (!key || hadError || wasStatusFile) {
|
|
900
|
-
clearStatus(key)
|
|
901
|
-
}
|
|
914
|
+
if (!key || hadError || wasStatusFile) clearStatus(key)
|
|
902
915
|
|
|
903
916
|
broadcast({ type: 'clear-error', file: key })
|
|
904
917
|
|
|
905
|
-
|
|
906
|
-
const nextItem = Array.from(_activeErrors.values()).at(-1)
|
|
907
|
-
showTrackedError(nextItem)
|
|
908
|
-
showedNext = true
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
return { cleared: hadError || wasStatusFile, file: key, showedNext }
|
|
918
|
+
return { cleared: hadError || wasStatusFile, file: key, showedNext: false }
|
|
912
919
|
}
|
|
913
920
|
|
|
914
921
|
function markSuccess(file) {
|
|
915
922
|
const key = normalizeFile(file)
|
|
916
923
|
const result = clearError(key)
|
|
917
924
|
const active = _activeErrors.size
|
|
918
|
-
const shouldPrint = result?.cleared && !result.showedNext
|
|
919
|
-
if (shouldPrint)
|
|
925
|
+
const shouldPrint = result?.cleared && !result.showedNext
|
|
926
|
+
if (shouldPrint) {
|
|
927
|
+
_terminalErrors.delete(terminalErrorKey(key))
|
|
928
|
+
printStatus(key, 'ok', null, { sticky: true })
|
|
929
|
+
}
|
|
920
930
|
return { cleared: !!result?.cleared, printed: !!shouldPrint, showedNext: !!result?.showedNext, active }
|
|
921
931
|
}
|
|
922
932
|
|