bimba-cli 0.7.19 → 0.7.21

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 (4) hide show
  1. package/index.js +5 -2
  2. package/package.json +1 -1
  3. package/plugin.js +214 -162
  4. package/serve.js +277 -56
package/index.js CHANGED
@@ -168,6 +168,7 @@ async function bundle() {
168
168
  stats.failed = 0
169
169
  stats.compiled = 0
170
170
  stats.errors = 0
171
+ stats.reported = 0
171
172
  stats.bundled = 0
172
173
 
173
174
  const start = Date.now();
@@ -208,8 +209,10 @@ async function bundle() {
208
209
  }
209
210
  }
210
211
 
211
- if(stats.failed)
212
- console.log(theme.start(theme.failure(" Failure ") + theme.filename(` Imba compiler failed to proceed ${stats.failed} file${stats.failed > 1 ? 's' : ''}`)));
212
+ if(stats.failed) {
213
+ if (stats.reported)
214
+ console.log(theme.start(theme.failure(" Failure ") + theme.filename(` Imba compiler failed to proceed ${stats.failed} file${stats.failed > 1 ? 's' : ''}`)));
215
+ }
213
216
  else
214
217
  console.log(theme.start(theme.success("Success") +` It took ${theme.time(Date.now() - start)} ms to bundle ${theme.count(stats.bundled)} file${stats.bundled > 1 ? 's' : ''} to the folder: ${theme.filedir(flags.outdir)}`));
215
218
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.7.19",
3
+ "version": "0.7.21",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/HeapVoid/bimba.git"
package/plugin.js CHANGED
@@ -1,162 +1,214 @@
1
- import { plugin } from "bun";
2
- import {theme} from './utils.js';
3
- import * as compiler from 'imba/compiler'
4
- import dir from 'path'
5
- import fs from 'fs'
6
- import { Glob } from "bun";
7
- import { unlink } from "node:fs/promises";
8
-
9
- export const cache = dir.join(process.cwd(), '.cache')
10
- if (!fs.existsSync(cache)){ fs.mkdirSync(cache);}
11
-
12
- // this should be reset from outside to get results of entrypoint building
13
- export let stats = {
14
- failed: 0,
15
- compiled: 0,
16
- cached: 0,
17
- bundled: 0,
18
- errors: 0,
19
- };
20
-
21
- // Target platform for the Imba compiler: 'browser' or 'node'
22
- // Set via setTarget() from the CLI before building
23
- export let target = 'browser';
24
- export function setTarget(t) { target = t; }
25
-
26
- export const imbaPlugin = {
27
- name: "imba",
28
- async setup(build) {
29
-
30
- // when an .imba file is imported...
31
- build.onLoad({ filter: /\.imba$/ }, async ({ path }) => {
32
-
33
- const f = dir.parse(path)
34
- let contents = '';
35
-
36
- // return the cached version if exists (include target in hash to avoid cross-platform cache hits)
37
- const cached = dir.join(cache, Bun.hash(path + ':' + target) + '_' + fs.statSync(path).mtimeMs + '.js');
38
- if (fs.existsSync(cached)) {
39
- stats.bundled++;
40
- stats.cached++;
41
- return {
42
- contents: await Bun.file(cached).text(),
43
- loader: "js",
44
- };
45
- }
46
-
47
- // clear previous cached version
48
- const glob = new Glob(Bun.hash(path + ':' + target) + '_' + "*.js");
49
- for await (const file of glob.scan(cache)) if (fs.existsSync(dir.join(cache, file))) unlink(dir.join(cache, file));
50
-
51
- // if no cached version read and compile it with the imba compiler
52
- const file = await Bun.file(path).text();
53
- const platform = target === 'node' || target === 'bun' ? 'node' : 'browser';
54
- const out = compiler.compile(file, {
55
- sourcePath: path,
56
- platform: platform,
57
- comments: false
58
- })
59
-
60
- // the file has been successfully compiled
61
- if (!out.errors?.length) {
62
- console.log(theme.action("compiling: ") + theme.folder(dir.join(f.dir,'/')) + theme.filename(f.base) + " - " + theme.success("compiled"));
63
- stats.bundled++;
64
- stats.compiled++;
65
- contents = out.js;
66
- await Bun.write(cached, contents);
67
- }
68
- // there were errors during compilation
69
- else {
70
- console.log(theme.action("compiling: ") + theme.folder(dir.join(f.dir,'/')) + theme.filename(f.base) + " - " + theme.failure(" fail "));
71
- stats.failed++;
72
- for (let i = 0; i < out.errors.length; i++) {
73
- if(out.errors[i]) printerr(out.errors[i]);
74
- }
75
- stats.errors++;
76
- }
77
-
78
- // and return the compiled source code as "js"
79
- return {
80
- contents,
81
- loader: "js",
82
- };
83
- });
84
- }
85
- };
86
-
87
- plugin(imbaPlugin);
88
-
89
-
90
- // -------------------------------------------------------------------------------
91
- // print pretty messages produced by the imba compiler
92
- // -------------------------------------------------------------------------------
93
-
94
- // print an error generated by the imba compiler
95
- export function printerr(err) {
96
-
97
- // halper function to produce empty strings
98
- const fill = (len = 0) => {return new Array(len + 1).join(' ')}
99
-
100
- // gather the needed information from the compiler error
101
- const snippet = err.toSnippet().split("\n");
102
- const errs = snippet[2] ? snippet[2].indexOf('^') : -1;
103
-
104
- // no source context available — print compact fallback
105
- if (!snippet[1] || errs === -1) {
106
- console.log('');
107
- console.log(fill(10) + theme.error(" " + err.message + " "));
108
- console.log('');
109
- return;
110
- }
111
-
112
- const display = {
113
- error: " " + err.message + " ",
114
- outdent: fill(10),
115
- source: snippet[1] + " ",
116
- margin: " line " + (err.range.start.line + 1) + " ",
117
- errs: errs,
118
- erre: snippet[2].lastIndexOf('^') + 1,
119
- };
120
-
121
- // calculate parameters for priniting a message
122
- const center = display.margin.length + display.errs + Math.floor((display.erre - display.errs) / 2);
123
- const half = Math.ceil((display.error.length - 1) / 2);
124
- const start = Math.max(0, center - half);
125
- const end = start + display.error.length;
126
- const total = Math.max(display.margin.length + display.source.length, end);
127
-
128
- // print emtpy line
129
- console.log('');
130
-
131
- // print line with the error message
132
- console.log(
133
- display.outdent +
134
- theme.margin(fill(Math.min(start, display.margin.length))) +
135
- theme.code(fill(Math.max(0, start - display.margin.length))) +
136
- theme.error(display.error) +
137
- theme.margin(fill(Math.max(0, display.margin.length - end))) +
138
- theme.code(fill(Math.min(total - display.margin.length, total - end)))
139
- );
140
-
141
- // print line with the source code
142
- console.log(
143
- display.outdent +
144
- theme.margin(display.margin) +
145
- theme.code(display.source.slice(0,display.errs)) +
146
- theme.error(display.source.slice(display.errs,display.erre)) +
147
- theme.code(display.source.slice(display.erre)) +
148
- theme.code(fill(total - display.source.length - display.margin.length))
149
- );
150
-
151
- // print empty line to balance the view
152
- // later we can put something usefull here
153
- // for example a link to online docs about the error
154
- console.log(
155
- display.outdent +
156
- theme.margin(fill(display.margin.length)) +
157
- theme.code(fill(total - display.margin.length))
158
- );
159
-
160
- // print emtpy line
161
- console.log('');
162
- }
1
+ import { plugin } from "bun";
2
+ import {theme} from './utils.js';
3
+ import * as compiler from 'imba/compiler'
4
+ import dir from 'path'
5
+ import fs from 'fs'
6
+ import { Glob } from "bun";
7
+ import { unlink } from "node:fs/promises";
8
+
9
+ export const cache = dir.join(process.cwd(), '.cache')
10
+ if (!fs.existsSync(cache)){ fs.mkdirSync(cache);}
11
+
12
+ // this should be reset from outside to get results of entrypoint building
13
+ export let stats = {
14
+ failed: 0,
15
+ compiled: 0,
16
+ cached: 0,
17
+ bundled: 0,
18
+ errors: 0,
19
+ reported: 0,
20
+ };
21
+
22
+ const _activeCompileErrors = new Map();
23
+
24
+ function normalizeCompilePath(filepath) {
25
+ let value = String(filepath || '');
26
+ if (dir.isAbsolute(value)) {
27
+ const rel = dir.relative(process.cwd(), value);
28
+ if (!rel.startsWith('..')) value = rel;
29
+ }
30
+ return value.replaceAll('\\', '/');
31
+ }
32
+
33
+ function compileErrorMessage(error) {
34
+ return error?.message || String(error);
35
+ }
36
+
37
+ function compileErrorLine(error) {
38
+ return error?.range?.start?.line ?? error?.line ?? '';
39
+ }
40
+
41
+ function compileErrorSnippet(error) {
42
+ try {
43
+ return error?.toSnippet?.() || error?.snippet || error?.stack || compileErrorMessage(error);
44
+ } catch(_) {
45
+ return error?.snippet || error?.stack || compileErrorMessage(error);
46
+ }
47
+ }
48
+
49
+ function compileErrorSignature(errors) {
50
+ return errors
51
+ .map(error => [compileErrorMessage(error), compileErrorLine(error)].join('\n'))
52
+ .join('\n---\n');
53
+ }
54
+
55
+ function shouldPrintCompileError(filepath, errors) {
56
+ const key = normalizeCompilePath(filepath);
57
+ const signature = compileErrorSignature(errors);
58
+ const previous = _activeCompileErrors.get(key);
59
+ _activeCompileErrors.set(key, { signature, time: Date.now() });
60
+ return previous?.signature !== signature;
61
+ }
62
+
63
+ function clearCompileError(filepath) {
64
+ _activeCompileErrors.delete(normalizeCompilePath(filepath));
65
+ }
66
+
67
+ // Target platform for the Imba compiler: 'browser' or 'node'
68
+ // Set via setTarget() from the CLI before building
69
+ export let target = 'browser';
70
+ export function setTarget(t) { target = t; }
71
+
72
+ export const imbaPlugin = {
73
+ name: "imba",
74
+ async setup(build) {
75
+
76
+ // when an .imba file is imported...
77
+ build.onLoad({ filter: /\.imba$/ }, async ({ path }) => {
78
+
79
+ const f = dir.parse(path)
80
+ let contents = '';
81
+
82
+ // return the cached version if exists (include target in hash to avoid cross-platform cache hits)
83
+ const cached = dir.join(cache, Bun.hash(path + ':' + target) + '_' + fs.statSync(path).mtimeMs + '.js');
84
+ if (fs.existsSync(cached)) {
85
+ clearCompileError(path);
86
+ stats.bundled++;
87
+ stats.cached++;
88
+ return {
89
+ contents: await Bun.file(cached).text(),
90
+ loader: "js",
91
+ };
92
+ }
93
+
94
+ // clear previous cached version
95
+ const glob = new Glob(Bun.hash(path + ':' + target) + '_' + "*.js");
96
+ for await (const file of glob.scan(cache)) if (fs.existsSync(dir.join(cache, file))) unlink(dir.join(cache, file));
97
+
98
+ // if no cached version read and compile it with the imba compiler
99
+ const file = await Bun.file(path).text();
100
+ const platform = target === 'node' || target === 'bun' ? 'node' : 'browser';
101
+ const out = compiler.compile(file, {
102
+ sourcePath: path,
103
+ platform: platform,
104
+ comments: false
105
+ })
106
+
107
+ // the file has been successfully compiled
108
+ if (!out.errors?.length) {
109
+ clearCompileError(path);
110
+ console.log(theme.action("compiling: ") + theme.folder(dir.join(f.dir,'/')) + theme.filename(f.base) + " - " + theme.success("compiled"));
111
+ stats.bundled++;
112
+ stats.compiled++;
113
+ contents = out.js;
114
+ await Bun.write(cached, contents);
115
+ }
116
+ // there were errors during compilation
117
+ else {
118
+ const shouldPrint = shouldPrintCompileError(path, out.errors);
119
+ if (shouldPrint) console.log(theme.action("compiling: ") + theme.folder(dir.join(f.dir,'/')) + theme.filename(f.base) + " - " + theme.failure(" fail "));
120
+ stats.failed++;
121
+ if (shouldPrint) {
122
+ stats.reported++;
123
+ for (let i = 0; i < out.errors.length; i++) {
124
+ if(out.errors[i]) printerr(out.errors[i]);
125
+ }
126
+ }
127
+ stats.errors++;
128
+ }
129
+
130
+ // and return the compiled source code as "js"
131
+ return {
132
+ contents,
133
+ loader: "js",
134
+ };
135
+ });
136
+ }
137
+ };
138
+
139
+ plugin(imbaPlugin);
140
+
141
+
142
+ // -------------------------------------------------------------------------------
143
+ // print pretty messages produced by the imba compiler
144
+ // -------------------------------------------------------------------------------
145
+
146
+ // print an error generated by the imba compiler
147
+ export function printerr(err) {
148
+
149
+ // halper function to produce empty strings
150
+ const fill = (len = 0) => {return new Array(len + 1).join(' ')}
151
+
152
+ // gather the needed information from the compiler error
153
+ const snippet = err.toSnippet().split("\n");
154
+ const errs = snippet[2] ? snippet[2].indexOf('^') : -1;
155
+
156
+ // no source context available — print compact fallback
157
+ if (!snippet[1] || errs === -1) {
158
+ console.log('');
159
+ console.log(fill(10) + theme.error(" " + err.message + " "));
160
+ console.log('');
161
+ return;
162
+ }
163
+
164
+ const display = {
165
+ error: " " + err.message + " ",
166
+ outdent: fill(10),
167
+ source: snippet[1] + " ",
168
+ margin: " line " + (err.range.start.line + 1) + " ",
169
+ errs: errs,
170
+ erre: snippet[2].lastIndexOf('^') + 1,
171
+ };
172
+
173
+ // calculate parameters for priniting a message
174
+ const center = display.margin.length + display.errs + Math.floor((display.erre - display.errs) / 2);
175
+ const half = Math.ceil((display.error.length - 1) / 2);
176
+ const start = Math.max(0, center - half);
177
+ const end = start + display.error.length;
178
+ const total = Math.max(display.margin.length + display.source.length, end);
179
+
180
+ // print emtpy line
181
+ console.log('');
182
+
183
+ // print line with the error message
184
+ console.log(
185
+ display.outdent +
186
+ theme.margin(fill(Math.min(start, display.margin.length))) +
187
+ theme.code(fill(Math.max(0, start - display.margin.length))) +
188
+ theme.error(display.error) +
189
+ theme.margin(fill(Math.max(0, display.margin.length - end))) +
190
+ theme.code(fill(Math.min(total - display.margin.length, total - end)))
191
+ );
192
+
193
+ // print line with the source code
194
+ console.log(
195
+ display.outdent +
196
+ theme.margin(display.margin) +
197
+ theme.code(display.source.slice(0,display.errs)) +
198
+ theme.error(display.source.slice(display.errs,display.erre)) +
199
+ theme.code(display.source.slice(display.erre)) +
200
+ theme.code(fill(total - display.source.length - display.margin.length))
201
+ );
202
+
203
+ // print empty line to balance the view
204
+ // later we can put something usefull here
205
+ // for example a link to online docs about the error
206
+ console.log(
207
+ display.outdent +
208
+ theme.margin(fill(display.margin.length)) +
209
+ theme.code(fill(total - display.margin.length))
210
+ );
211
+
212
+ // print emtpy line
213
+ console.log('');
214
+ }
package/serve.js CHANGED
@@ -207,7 +207,7 @@ const hmrClient = `
207
207
  const msg = JSON.parse(e.data);
208
208
  if (msg.type === 'update') _applyUpdate(msg.file, msg.slots);
209
209
  else if (msg.type === 'reload') location.reload();
210
- else if (msg.type === 'error') showError(msg.file, msg.errors);
210
+ else if (msg.type === 'error') showError(msg.file, msg.errors, msg.time);
211
211
  else if (msg.type === 'clear-error') clearError(msg.file);
212
212
  };
213
213
 
@@ -223,8 +223,9 @@ const hmrClient = `
223
223
  return value;
224
224
  }
225
225
 
226
- function showError(file, errors) {
226
+ function showError(file, errors, time) {
227
227
  const displayFile = normalizeFile(file);
228
+ const displayTime = time || new Date().toLocaleTimeString();
228
229
  let overlay = document.getElementById('__bimba_error__');
229
230
  if (!overlay) {
230
231
  overlay = document.createElement('div');
@@ -237,7 +238,7 @@ const hmrClient = `
237
238
  overlay.innerHTML = \`
238
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)">
239
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">
240
- <span>Compile error — \${displayFile}</span>
241
+ <span>Compile error — \${displayFile} <span style="opacity:.75;font-weight:400">\${displayTime}</span></span>
241
242
  <span onclick="document.getElementById('__bimba_error__').remove()" style="cursor:pointer;opacity:.7;font-size:16px">✕</span>
242
243
  </div>
243
244
  \${errors.map(err => \`
@@ -353,16 +354,39 @@ function rewriteBareImports(js) {
353
354
  return js
354
355
  }
355
356
 
357
+ function isMissingFileError(error) {
358
+ return error?.code === 'ENOENT' || String(error?.message || error).includes('ENOENT: no such file or directory')
359
+ }
360
+
361
+ function missingCompileResult(filepath) {
362
+ dropFileState(filepath)
363
+ return { js: '', errors: [], slots: null, changeType: 'missing', missing: true }
364
+ }
365
+
356
366
  async function compileFile(filepath) {
357
367
  const abs = path.resolve(filepath)
368
+ if (!existsSync(abs)) return missingCompileResult(abs)
369
+
358
370
  const file = Bun.file(filepath)
359
- const stat = await file.stat()
371
+ let stat
372
+ try {
373
+ stat = await file.stat()
374
+ } catch (error) {
375
+ if (isMissingFileError(error)) return missingCompileResult(abs)
376
+ throw error
377
+ }
360
378
  const mtime = stat.mtime.getTime()
361
379
 
362
380
  const cached = _compileCache.get(abs)
363
381
  if (cached && cached.mtime === mtime) return _normalizeResult(cached.result, { changeType: 'cached' })
364
382
 
365
- const code = await file.text()
383
+ let code
384
+ try {
385
+ code = await file.text()
386
+ } catch (error) {
387
+ if (isMissingFileError(error)) return missingCompileResult(abs)
388
+ throw error
389
+ }
366
390
  const result = compiler.compile(code, {
367
391
  sourcePath: filepath,
368
392
  platform: 'browser',
@@ -609,14 +633,108 @@ export function serve(entrypoint, flags) {
609
633
 
610
634
  // ── File watcher ───────────────────────────────────────────────────────────
611
635
 
636
+ const _activeErrors = new Map()
637
+
612
638
  function broadcast(payload) {
613
639
  const msg = JSON.stringify(payload)
614
640
  for (const socket of sockets) socket.send(msg)
615
641
  }
616
642
 
643
+ function normalizeFile(file) {
644
+ let value = String(file || '')
645
+ if (path.isAbsolute(value)) {
646
+ const rel = path.relative(process.cwd(), value)
647
+ if (!rel.startsWith('..')) value = rel
648
+ }
649
+ value = value.replaceAll('\\', '/')
650
+ while (value.startsWith('./')) value = value.slice(2)
651
+ while (value.startsWith('/')) value = value.slice(1)
652
+ return value
653
+ }
654
+
655
+ function errorMessage(error) {
656
+ return error?.message || String(error)
657
+ }
658
+
659
+ function errorLine(error) {
660
+ return error?.range?.start?.line ?? error?.line
661
+ }
662
+
663
+ function errorSnippet(error) {
664
+ try {
665
+ return error?.toSnippet?.() || error?.snippet || error?.stack || errorMessage(error)
666
+ } catch(_) {
667
+ return error?.snippet || error?.stack || errorMessage(error)
668
+ }
669
+ }
670
+
671
+ function serializeErrors(errors) {
672
+ return errors.map(error => ({
673
+ message: errorMessage(error),
674
+ line: errorLine(error),
675
+ snippet: errorSnippet(error),
676
+ }))
677
+ }
678
+
679
+ function errorSignature(errors) {
680
+ return serializeErrors(errors)
681
+ .map(error => [error.message, error.line || ''].join('\n'))
682
+ .join('\n---\n')
683
+ }
684
+
685
+ function showTrackedError(file, item) {
686
+ printStatus(file, 'fail', item.errors)
687
+ broadcast({ type: 'error', file, time: item.time, errors: item.payload })
688
+ }
689
+
690
+ function reportError(file, errors) {
691
+ const key = normalizeFile(file)
692
+ const list = Array.isArray(errors) ? errors : [errors]
693
+ const signature = errorSignature(list)
694
+ const previous = _activeErrors.get(key)
695
+
696
+ const item = {
697
+ signature,
698
+ errors: list,
699
+ payload: serializeErrors(list),
700
+ time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
701
+ }
702
+ _activeErrors.set(key, item)
703
+ if (previous?.signature === signature && !_isTTY) {
704
+ broadcast({ type: 'error', file: key, time: item.time, errors: item.payload })
705
+ return
706
+ }
707
+
708
+ showTrackedError(key, item)
709
+ }
710
+
711
+ function errorText(errors) {
712
+ const list = Array.isArray(errors) ? errors : [errors]
713
+ return list.map(errorMessage).join('\n')
714
+ }
715
+
716
+ function errorResponse(file, errors, status = 500) {
717
+ reportError(file, errors)
718
+ return new Response(errorText(errors), { status })
719
+ }
720
+
617
721
  function clearError(file) {
618
- clearStatus(file)
619
- broadcast({ type: 'clear-error', file })
722
+ const key = file ? normalizeFile(file) : null
723
+ const wasStatusFile = key && _statusFile === key
724
+ const hadError = key ? _activeErrors.has(key) : _activeErrors.size > 0
725
+
726
+ if (key) _activeErrors.delete(key)
727
+ else _activeErrors.clear()
728
+
729
+ if (key && !hadError && !wasStatusFile) return
730
+
731
+ clearStatus(key)
732
+ broadcast({ type: 'clear-error', file: key })
733
+
734
+ if (wasStatusFile && _activeErrors.size) {
735
+ const [nextFile, nextItem] = Array.from(_activeErrors.entries()).at(-1)
736
+ showTrackedError(nextFile, nextItem)
737
+ }
620
738
  }
621
739
 
622
740
  const _debounce = new Map()
@@ -644,7 +762,7 @@ export function serve(entrypoint, flags) {
644
762
 
645
763
  async function compileChangedFile(filename, version) {
646
764
  const filepath = path.join(srcDir, filename)
647
- const rel = path.join(path.relative('.', srcDir), filename).replaceAll('\\', '/')
765
+ const rel = normalizeFile(path.join(path.relative('.', srcDir), filename))
648
766
 
649
767
  try {
650
768
  if (!existsSync(filepath)) {
@@ -657,14 +775,13 @@ export function serve(entrypoint, flags) {
657
775
  const out = await compileFile(filepath)
658
776
 
659
777
  if (!isCurrentChange(filename, version)) return
778
+ if (out.missing) {
779
+ clearError(rel)
780
+ return
781
+ }
660
782
 
661
783
  if (out.errors?.length) {
662
- printStatus(rel, 'fail', out.errors)
663
- broadcast({ type: 'error', file: rel, errors: out.errors.map(e => ({
664
- message: e.message,
665
- line: e.range?.start?.line,
666
- snippet: e.toSnippet(),
667
- })) })
784
+ reportError(rel, out.errors)
668
785
  return
669
786
  }
670
787
 
@@ -677,8 +794,12 @@ export function serve(entrypoint, flags) {
677
794
  broadcast({ type: 'update', file: rel, slots: out.slots || 'shifted' })
678
795
  } catch(e) {
679
796
  if (!isCurrentChange(filename, version)) return
680
- printStatus(rel, 'fail', [{ message: e.message }])
681
- broadcast({ type: 'error', file: rel, errors: [{ message: e.message, snippet: e.stack || e.message }] })
797
+ if (isMissingFileError(e)) {
798
+ dropFileState(filepath)
799
+ clearError(rel)
800
+ return
801
+ }
802
+ reportError(rel, [{ message: e.message, snippet: e.stack || e.message }])
682
803
  }
683
804
  }
684
805
 
@@ -695,6 +816,7 @@ export function serve(entrypoint, flags) {
695
816
  fetch: async (req, server) => {
696
817
  const url = new URL(req.url)
697
818
  const pathname = url.pathname
819
+ try {
698
820
 
699
821
  // WebSocket upgrade for HMR
700
822
  if (pathname === '/__hmr__') {
@@ -703,44 +825,57 @@ export function serve(entrypoint, flags) {
703
825
 
704
826
  if (pathname.startsWith('/__bimba_vendor__/')) {
705
827
  const specifier = vendorSpecifierFromPath(pathname)
828
+ const file = 'vendor:' + (specifier || pathname)
706
829
  const bundled = specifier ? await bundleVendor(specifier) : null
707
830
  if (bundled?.code) {
831
+ clearError(file)
708
832
  return new Response(bundled.code, { headers: { 'Content-Type': 'application/javascript' } })
709
833
  }
710
834
 
711
- return new Response((bundled?.errors || [`Could not bundle vendor module: ${specifier}`]).join('\n'), { status: 500 })
835
+ return errorResponse(file, bundled?.errors || [`Could not bundle vendor module: ${specifier}`])
712
836
  }
713
837
 
714
838
  // HTML: index or any .html file
715
839
  if (pathname === '/' || pathname.endsWith('.html')) {
716
840
  const htmlFile = pathname === '/' ? htmlPath : '.' + pathname
717
- let html = await Bun.file(htmlFile).text()
718
- return new Response(transformHtml(html, entrypoint), {
719
- headers: { 'Content-Type': 'text/html' },
720
- })
841
+ const file = 'html:' + normalizeFile(htmlFile)
842
+ try {
843
+ let html = await Bun.file(htmlFile).text()
844
+ clearError(file)
845
+ return new Response(transformHtml(html, entrypoint), {
846
+ headers: { 'Content-Type': 'text/html' },
847
+ })
848
+ } catch (error) {
849
+ if (isMissingFileError(error)) {
850
+ clearError(file)
851
+ return new Response('Not Found', { status: 404 })
852
+ }
853
+ return errorResponse(file, [error])
854
+ }
721
855
  }
722
856
 
723
857
  // Imba files: compile on demand and serve as JS
724
858
  if (pathname.endsWith('.imba')) {
725
859
  const filepath = '.' + pathname
860
+ const file = normalizeFile(pathname)
726
861
  try {
727
862
  const out = await compileFile(filepath)
863
+ if (out.missing) {
864
+ clearError(file)
865
+ return new Response('Not Found', { status: 404 })
866
+ }
728
867
  if (out.errors?.length) {
729
- const file = pathname.replace(/^\//, '')
730
- printStatus(file, 'fail', out.errors)
731
- broadcast({ type: 'error', file, errors: out.errors.map(e => ({
732
- message: e.message,
733
- line: e.range?.start?.line,
734
- snippet: e.toSnippet(),
735
- })) })
736
- return new Response(out.errors.map(e => e.message).join('\n'), { status: 500 })
868
+ return errorResponse(file, out.errors)
737
869
  }
870
+ clearError(file)
738
871
  return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
739
872
  } catch(e) {
740
- const file = pathname.replace(/^\//, '')
741
- printStatus(file, 'fail', [{ message: e.message }])
742
- broadcast({ type: 'error', file, errors: [{ message: e.message, snippet: e.stack || e.message }] })
743
- return new Response(e.message, { status: 500 })
873
+ if (isMissingFileError(e)) {
874
+ dropFileState(filepath)
875
+ clearError(file)
876
+ return new Response('Not Found', { status: 404 })
877
+ }
878
+ return errorResponse(file, [{ message: e.message, snippet: e.stack || e.message }])
744
879
  }
745
880
  }
746
881
 
@@ -750,26 +885,50 @@ export function serve(entrypoint, flags) {
750
885
  if (pathname.endsWith('.css')) {
751
886
  const cssPath = resolveFileCandidate(path.join(htmlDir, pathname)) || resolveFileCandidate('.' + pathname)
752
887
  const cssFile = cssPath ? Bun.file(cssPath) : null
753
- if (cssFile && await cssFile.exists()) {
754
- if (req.headers.get('sec-fetch-dest') === 'style') {
755
- return new Response(cssFile, { headers: { 'Content-Type': 'text/css' } })
756
- }
888
+ const file = 'css:' + normalizeFile(cssPath || pathname)
889
+ try {
890
+ if (cssFile && await cssFile.exists()) {
891
+ if (req.headers.get('sec-fetch-dest') === 'style') {
892
+ clearError(file)
893
+ return new Response(cssFile, { headers: { 'Content-Type': 'text/css' } })
894
+ }
757
895
 
758
- const css = await cssFile.text()
759
- const id = JSON.stringify(pathname)
760
- const js = [
761
- `const id = ${id};`,
762
- `let el = document.querySelector('style[data-bimba-css=' + JSON.stringify(id) + ']');`,
763
- `if (!el) { el = document.createElement('style'); el.setAttribute('data-bimba-css', id); document.head.appendChild(el); }`,
764
- `el.textContent = ${JSON.stringify(css)};`,
765
- ].join('\n')
766
- return new Response(js, { headers: { 'Content-Type': 'application/javascript' } })
896
+ const css = await cssFile.text()
897
+ const id = JSON.stringify(pathname)
898
+ const js = [
899
+ `const id = ${id};`,
900
+ `let el = document.querySelector('style[data-bimba-css=' + JSON.stringify(id) + ']');`,
901
+ `if (!el) { el = document.createElement('style'); el.setAttribute('data-bimba-css', id); document.head.appendChild(el); }`,
902
+ `el.textContent = ${JSON.stringify(css)};`,
903
+ ].join('\n')
904
+ clearError(file)
905
+ return new Response(js, { headers: { 'Content-Type': 'application/javascript' } })
906
+ }
907
+ } catch (error) {
908
+ if (isMissingFileError(error)) {
909
+ clearError(file)
910
+ return new Response('Not Found', { status: 404 })
911
+ }
912
+ return errorResponse(file, [error])
767
913
  }
768
914
  }
769
915
 
770
916
  if (!pathname.startsWith('/node_modules/') && (pathname.endsWith('.js') || pathname.endsWith('.mjs'))) {
771
917
  const jsFile = resolveFileCandidate(path.join(htmlDir, pathname)) || resolveFileCandidate('.' + pathname)
772
- if (jsFile) return serveJavaScriptFile(jsFile)
918
+ if (jsFile) {
919
+ const file = 'js:' + normalizeFile(jsFile)
920
+ try {
921
+ const response = await serveJavaScriptFile(jsFile)
922
+ clearError(file)
923
+ return response
924
+ } catch (error) {
925
+ if (isMissingFileError(error)) {
926
+ clearError(file)
927
+ return new Response('Not Found', { status: 404 })
928
+ }
929
+ return errorResponse(file, [error])
930
+ }
931
+ }
773
932
  }
774
933
 
775
934
  // Direct node_modules URLs (from user import maps or explicit imports)
@@ -779,24 +938,46 @@ export function serve(entrypoint, flags) {
779
938
  const resolved = resolveFileCandidate('.' + pathname)
780
939
  if (resolved?.endsWith('.imba')) {
781
940
  const out = await compileFile(resolved)
782
- if (out.errors?.length) return new Response(out.errors.map(e => e.message).join('\n'), { status: 500 })
941
+ const file = normalizeFile(resolved)
942
+ if (out.missing) {
943
+ clearError(file)
944
+ return new Response('Not Found', { status: 404 })
945
+ }
946
+ if (out.errors?.length) {
947
+ return errorResponse(file, out.errors)
948
+ }
949
+ clearError(file)
783
950
  return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
784
951
  }
785
952
 
786
953
  if (resolved) {
954
+ const file = 'vendor:' + normalizeFile(pathname)
787
955
  const bundled = await bundleVendor(path.resolve(resolved))
788
956
  if (bundled?.code) {
957
+ clearError(file)
789
958
  return new Response(bundled.code, { headers: { 'Content-Type': 'application/javascript' } })
790
959
  }
791
- return new Response((bundled?.errors || [`Could not bundle ${pathname}`]).join('\n'), { status: 500 })
960
+ return errorResponse(file, bundled?.errors || [`Could not bundle ${pathname}`])
792
961
  }
793
962
  }
794
963
 
795
964
  // Static files: check htmlDir first (for assets relative to HTML), then root
796
- const inHtmlDir = Bun.file(path.join(htmlDir, pathname))
797
- if (await inHtmlDir.exists()) return new Response(inHtmlDir)
798
- const inRoot = Bun.file('.' + pathname)
799
- if (await inRoot.exists()) return new Response(inRoot)
965
+ try {
966
+ const inHtmlDirPath = path.join(htmlDir, pathname)
967
+ const inHtmlDir = Bun.file(inHtmlDirPath)
968
+ if (await inHtmlDir.exists()) {
969
+ clearError('static:' + normalizeFile(inHtmlDirPath))
970
+ return new Response(inHtmlDir)
971
+ }
972
+ const inRootPath = '.' + pathname
973
+ const inRoot = Bun.file(inRootPath)
974
+ if (await inRoot.exists()) {
975
+ clearError('static:' + normalizeFile(inRootPath))
976
+ return new Response(inRoot)
977
+ }
978
+ } catch (error) {
979
+ if (!isMissingFileError(error)) return errorResponse('static:' + normalizeFile(pathname), [error])
980
+ }
800
981
 
801
982
  // Try extensions for extensionless paths (e.g. node_modules imports)
802
983
  const lastSegment = pathname.split('/').pop()
@@ -805,23 +986,63 @@ export function serve(entrypoint, flags) {
805
986
  const imbaPath = '.' + pathname + '.imba'
806
987
  if (existsSync(imbaPath)) {
807
988
  const out = await compileFile(imbaPath)
808
- if (!out.errors?.length) return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
989
+ const file = normalizeFile(imbaPath)
990
+ if (out.missing) {
991
+ clearError(file)
992
+ return new Response('Not Found', { status: 404 })
993
+ }
994
+ if (out.errors?.length) {
995
+ return errorResponse(file, out.errors)
996
+ }
997
+ clearError(file)
998
+ return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
809
999
  }
810
1000
  for (const ext of ['.js', '.mjs']) {
811
1001
  const withExt = '.' + pathname + ext
812
- if (existsSync(withExt)) return serveJavaScriptFile(withExt)
1002
+ if (existsSync(withExt)) {
1003
+ const file = 'js:' + normalizeFile(withExt)
1004
+ try {
1005
+ const response = await serveJavaScriptFile(withExt)
1006
+ clearError(file)
1007
+ return response
1008
+ } catch (error) {
1009
+ if (isMissingFileError(error)) {
1010
+ clearError(file)
1011
+ return new Response('Not Found', { status: 404 })
1012
+ }
1013
+ return errorResponse(file, [error])
1014
+ }
1015
+ }
813
1016
  }
814
1017
  }
815
1018
 
816
1019
  // SPA fallback for extension-less paths
817
- if (!lastSegment.includes('.')) {
1020
+ if (!lastSegment.includes('.')) {
1021
+ const file = 'html:' + normalizeFile(htmlPath)
1022
+ try {
818
1023
  let html = await Bun.file(htmlPath).text()
1024
+ clearError(file)
819
1025
  return new Response(transformHtml(html, entrypoint), {
820
1026
  headers: { 'Content-Type': 'text/html' },
821
1027
  })
1028
+ } catch (error) {
1029
+ if (isMissingFileError(error)) {
1030
+ clearError(file)
1031
+ return new Response('Not Found', { status: 404 })
1032
+ }
1033
+ return errorResponse(file, [error])
822
1034
  }
1035
+ }
823
1036
 
824
1037
  return new Response('Not Found', { status: 404 })
1038
+ } catch (error) {
1039
+ const file = 'server:' + normalizeFile(pathname || req.url)
1040
+ if (isMissingFileError(error)) {
1041
+ clearError(file)
1042
+ return new Response('Not Found', { status: 404 })
1043
+ }
1044
+ return errorResponse(file, [error])
1045
+ }
825
1046
  },
826
1047
 
827
1048
  websocket: {