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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/plugin.js +82 -56
  3. package/serve.js +116 -106
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.7.28",
3
+ "version": "0.7.30",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/HeapVoid/bimba.git"
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
- // clear previous cached version
120
- const glob = new Glob(Bun.hash(path + ':' + target) + '_' + "*.js");
121
- for await (const file of glob.scan(cache)) if (fs.existsSync(dir.join(cache, file))) unlink(dir.join(cache, file));
122
-
123
- // if no cached version read and compile it with the imba compiler
124
- const file = await Bun.file(path).text();
125
- const platform = target === 'node' || target === 'bun' ? 'node' : 'browser';
126
- let out
127
- try {
128
- out = compiler.compile(file, {
129
- sourcePath: path,
130
- platform: platform,
131
- comments: false
132
- })
133
- } catch (error) {
134
- out = { js: '', errors: [error] }
135
- }
136
-
137
- // the file has been successfully compiled
138
- if (!out.errors?.length) {
139
- clearCompileError(path);
140
- console.log(theme.action("compiling: ") + theme.folder(dir.join(f.dir,'/')) + theme.filename(f.base) + " - " + theme.success("compiled"));
141
- stats.bundled++;
142
- stats.compiled++;
143
- contents = out.js;
144
- await Bun.write(cached, contents);
145
- }
146
- // there were errors during compilation
147
- else {
148
- const shouldPrint = shouldPrintCompileError(path, out.errors);
149
- if (shouldPrint) console.log(theme.action("compiling: ") + theme.folder(dir.join(f.dir,'/')) + theme.filename(f.base) + " - " + theme.failure(" fail "));
150
- stats.failed++;
151
- if (shouldPrint) {
152
- stats.reported++;
153
- for (let i = 0; i < out.errors.length; i++) {
154
- if(out.errors[i]) printCompileError(out.errors[i]);
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
- stats.errors++;
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
- const file = Bun.file(abs)
422
- let stat
423
- try {
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
- const cached = _compileCache.get(abs)
432
- if (cached && cached.stamp === stamp) return _normalizeResult(cached.result, { changeType: 'cached' })
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
- let code
435
- try {
436
- code = await file.text()
437
- } catch (error) {
438
- if (isMissingFileError(error)) return missingCompileResult(abs)
439
- throw error
440
- }
441
- const result = compiler.compile(code, {
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
- const errors = result.errors || []
448
- if (!errors.length && result.js) {
449
- const { js, slotCount } = stabilizeSymbols(result.js, abs)
450
- result.js = rewriteBareImports(js)
451
- const prev = _prevSlots.get(abs)
452
- result.slots = (prev === undefined || prev === slotCount) ? 'stable' : 'shifted'
453
- _prevSlots.set(abs, slotCount)
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
- // Bake errors as an own property so caching/spreading preserves them.
457
- const baked = { js: result.js, errors, slots: result.slots }
458
- const changeType = _prevJs.get(abs) === baked.js ? 'none' : 'full'
459
- _prevJs.set(abs, baked.js)
460
- _compileCache.set(abs, { stamp, result: baked })
461
- return _normalizeResult(baked, { changeType })
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
- if (errors?.length) {
666
- process.stdout.write(` ${theme.folder(now)} ${theme.filename(file)} ${status}\n`)
667
- for (const err of errors) {
668
- try { printerr(err) } catch(_) { process.stdout.write(' ' + err.message + '\n') }
669
- }
670
- } else {
671
- const myId = ++_fadeId
672
- const plainLine = ` ${now} ${file} ok `
673
- const total = plainLine.length
674
- process.stdout.write(` ${theme.folder(now)} ${theme.filename(file)} ${status}`)
675
- for (let i = 1; i <= total; i++) {
676
- _fadeTimers.push(setTimeout(() => {
677
- if (_fadeId !== myId) return
678
- process.stdout.write('\x1b[1D \x1b[1D')
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
- 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 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
- if (_isTTY) renderErrorPanel()
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
- let showedNext = false
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
- if (!_isTTY && wasStatusFile && _activeErrors.size) {
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 && !active
919
- if (shouldPrint) printStatus(key, 'ok')
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