bimba-cli 0.7.18 → 0.7.20

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 +22 -4
  2. package/package.json +1 -1
  3. package/plugin.js +214 -162
  4. package/serve.js +123 -28
package/index.js CHANGED
@@ -90,6 +90,8 @@ if(flags.help) {
90
90
 
91
91
 
92
92
  let bundling = false;
93
+ let rebuildQueued = false;
94
+ let watchTimer = null;
93
95
 
94
96
  // typecheck mode
95
97
  if (flags.typecheck || flags.tscheck) {
@@ -131,7 +133,13 @@ else {
131
133
 
132
134
  function watch(callback) {
133
135
  if (flags.watch) {
134
- const watcher = fs.watch(path.dirname(entrypoint), {recursive: true}, async (event, filename) => ( callback() ));
136
+ const watcher = fs.watch(path.dirname(entrypoint), {recursive: true}, () => {
137
+ if (watchTimer) clearTimeout(watchTimer);
138
+ watchTimer = setTimeout(() => {
139
+ watchTimer = null;
140
+ callback();
141
+ }, 150);
142
+ });
135
143
 
136
144
  process.on("SIGINT", () => {
137
145
  if(flags.clearcache) rmSync(cache, { recursive: true, force: true });
@@ -146,7 +154,10 @@ function watch(callback) {
146
154
 
147
155
 
148
156
  async function bundle() {
149
- if (bundling) return;
157
+ if (bundling) {
158
+ rebuildQueued = true;
159
+ return false;
160
+ }
150
161
  bundling = true;
151
162
 
152
163
  if (!fs.existsSync(entrypoint)) {
@@ -157,6 +168,7 @@ async function bundle() {
157
168
  stats.failed = 0
158
169
  stats.compiled = 0
159
170
  stats.errors = 0
171
+ stats.reported = 0
160
172
  stats.bundled = 0
161
173
 
162
174
  const start = Date.now();
@@ -197,8 +209,10 @@ async function bundle() {
197
209
  }
198
210
  }
199
211
 
200
- if(stats.failed)
201
- 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
+ }
202
216
  else
203
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)}`));
204
218
 
@@ -220,5 +234,9 @@ async function bundle() {
220
234
  }
221
235
  finally {
222
236
  bundling = false;
237
+ if (rebuildQueued) {
238
+ rebuildQueued = false;
239
+ queueMicrotask(bundle);
240
+ }
223
241
  };
224
242
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.7.18",
3
+ "version": "0.7.20",
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), compileErrorSnippet(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
 
@@ -216,7 +216,16 @@ const hmrClient = `
216
216
 
217
217
  // ── Error overlay ──────────────────────────────────────────────────────────
218
218
 
219
- function showError(file, errors) {
219
+ function normalizeFile(file) {
220
+ let value = String(file || '').split(String.fromCharCode(92)).join('/');
221
+ while (value.startsWith('./')) value = value.slice(2);
222
+ while (value.startsWith('/')) value = value.slice(1);
223
+ return value;
224
+ }
225
+
226
+ function showError(file, errors, time) {
227
+ const displayFile = normalizeFile(file);
228
+ const displayTime = time || new Date().toLocaleTimeString();
220
229
  let overlay = document.getElementById('__bimba_error__');
221
230
  if (!overlay) {
222
231
  overlay = document.createElement('div');
@@ -225,11 +234,11 @@ const hmrClient = `
225
234
  overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
226
235
  document.body.appendChild(overlay);
227
236
  }
228
- overlay.dataset.file = file;
237
+ overlay.dataset.file = displayFile;
229
238
  overlay.innerHTML = \`
230
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)">
231
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">
232
- <span>Compile error — \${file}</span>
241
+ <span>Compile error — \${displayFile} <span style="opacity:.75;font-weight:400">\${displayTime}</span></span>
233
242
  <span onclick="document.getElementById('__bimba_error__').remove()" style="cursor:pointer;opacity:.7;font-size:16px">✕</span>
234
243
  </div>
235
244
  \${errors.map(err => \`
@@ -244,7 +253,10 @@ const hmrClient = `
244
253
 
245
254
  function clearError(file) {
246
255
  const overlay = document.getElementById('__bimba_error__');
247
- if (overlay && (!file || overlay.dataset.file === file)) overlay.remove();
256
+ if (!overlay) return;
257
+
258
+ const activeFile = overlay.dataset.file;
259
+ if (!file || !activeFile || activeFile === normalizeFile(file)) overlay.remove();
248
260
  }
249
261
 
250
262
  connect();
@@ -598,14 +610,98 @@ export function serve(entrypoint, flags) {
598
610
 
599
611
  // ── File watcher ───────────────────────────────────────────────────────────
600
612
 
613
+ const _activeErrors = new Map()
614
+
601
615
  function broadcast(payload) {
602
616
  const msg = JSON.stringify(payload)
603
617
  for (const socket of sockets) socket.send(msg)
604
618
  }
605
619
 
620
+ function normalizeFile(file) {
621
+ let value = String(file || '')
622
+ if (path.isAbsolute(value)) {
623
+ const rel = path.relative(process.cwd(), value)
624
+ if (!rel.startsWith('..')) value = rel
625
+ }
626
+ value = value.replaceAll('\\', '/')
627
+ while (value.startsWith('./')) value = value.slice(2)
628
+ while (value.startsWith('/')) value = value.slice(1)
629
+ return value
630
+ }
631
+
632
+ function errorMessage(error) {
633
+ return error?.message || String(error)
634
+ }
635
+
636
+ function errorLine(error) {
637
+ return error?.range?.start?.line ?? error?.line
638
+ }
639
+
640
+ function errorSnippet(error) {
641
+ try {
642
+ return error?.toSnippet?.() || error?.snippet || error?.stack || errorMessage(error)
643
+ } catch(_) {
644
+ return error?.snippet || error?.stack || errorMessage(error)
645
+ }
646
+ }
647
+
648
+ function serializeErrors(errors) {
649
+ return errors.map(error => ({
650
+ message: errorMessage(error),
651
+ line: errorLine(error),
652
+ snippet: errorSnippet(error),
653
+ }))
654
+ }
655
+
656
+ function errorSignature(errors) {
657
+ return serializeErrors(errors)
658
+ .map(error => [error.message, error.line || '', error.snippet || ''].join('\n'))
659
+ .join('\n---\n')
660
+ }
661
+
662
+ function showTrackedError(file, item) {
663
+ printStatus(file, 'fail', item.errors)
664
+ broadcast({ type: 'error', file, time: item.time, errors: item.payload })
665
+ }
666
+
667
+ function reportError(file, errors) {
668
+ const key = normalizeFile(file)
669
+ const list = Array.isArray(errors) ? errors : [errors]
670
+ const signature = errorSignature(list)
671
+ const previous = _activeErrors.get(key)
672
+
673
+ const item = {
674
+ signature,
675
+ errors: list,
676
+ payload: serializeErrors(list),
677
+ time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
678
+ }
679
+ _activeErrors.set(key, item)
680
+ if (previous?.signature === signature && !_isTTY) {
681
+ broadcast({ type: 'error', file: key, time: item.time, errors: item.payload })
682
+ return
683
+ }
684
+
685
+ showTrackedError(key, item)
686
+ }
687
+
606
688
  function clearError(file) {
607
- clearStatus(file)
608
- broadcast({ type: 'clear-error', file })
689
+ const key = file ? normalizeFile(file) : null
690
+ const wasStatusFile = key && _statusFile === key
691
+ const hadError = key ? _activeErrors.has(key) : _activeErrors.size > 0
692
+
693
+ if (key) _activeErrors.delete(key)
694
+ else _activeErrors.clear()
695
+
696
+ if (key && !hadError && !wasStatusFile) return
697
+
698
+ clearStatus(key)
699
+ broadcast({ type: 'clear-error', file: key })
700
+
701
+ if (wasStatusFile && _activeErrors.size) {
702
+ const [nextFile, nextItem] = Array.from(_activeErrors.entries()).at(-1)
703
+ showTrackedError(nextFile, nextItem)
704
+ }
609
705
  }
610
706
 
611
707
  const _debounce = new Map()
@@ -633,7 +729,7 @@ export function serve(entrypoint, flags) {
633
729
 
634
730
  async function compileChangedFile(filename, version) {
635
731
  const filepath = path.join(srcDir, filename)
636
- const rel = path.join(path.relative('.', srcDir), filename).replaceAll('\\', '/')
732
+ const rel = normalizeFile(path.join(path.relative('.', srcDir), filename))
637
733
 
638
734
  try {
639
735
  if (!existsSync(filepath)) {
@@ -648,12 +744,7 @@ export function serve(entrypoint, flags) {
648
744
  if (!isCurrentChange(filename, version)) return
649
745
 
650
746
  if (out.errors?.length) {
651
- printStatus(rel, 'fail', out.errors)
652
- broadcast({ type: 'error', file: rel, errors: out.errors.map(e => ({
653
- message: e.message,
654
- line: e.range?.start?.line,
655
- snippet: e.toSnippet(),
656
- })) })
747
+ reportError(rel, out.errors)
657
748
  return
658
749
  }
659
750
 
@@ -666,8 +757,7 @@ export function serve(entrypoint, flags) {
666
757
  broadcast({ type: 'update', file: rel, slots: out.slots || 'shifted' })
667
758
  } catch(e) {
668
759
  if (!isCurrentChange(filename, version)) return
669
- printStatus(rel, 'fail', [{ message: e.message }])
670
- broadcast({ type: 'error', file: rel, errors: [{ message: e.message, snippet: e.stack || e.message }] })
760
+ reportError(rel, [{ message: e.message, snippet: e.stack || e.message }])
671
761
  }
672
762
  }
673
763
 
@@ -712,23 +802,17 @@ export function serve(entrypoint, flags) {
712
802
  // Imba files: compile on demand and serve as JS
713
803
  if (pathname.endsWith('.imba')) {
714
804
  const filepath = '.' + pathname
805
+ const file = normalizeFile(pathname)
715
806
  try {
716
807
  const out = await compileFile(filepath)
717
808
  if (out.errors?.length) {
718
- const file = pathname.replace(/^\//, '')
719
- printStatus(file, 'fail', out.errors)
720
- broadcast({ type: 'error', file, errors: out.errors.map(e => ({
721
- message: e.message,
722
- line: e.range?.start?.line,
723
- snippet: e.toSnippet(),
724
- })) })
809
+ reportError(file, out.errors)
725
810
  return new Response(out.errors.map(e => e.message).join('\n'), { status: 500 })
726
811
  }
812
+ clearError(file)
727
813
  return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
728
814
  } catch(e) {
729
- const file = pathname.replace(/^\//, '')
730
- printStatus(file, 'fail', [{ message: e.message }])
731
- broadcast({ type: 'error', file, errors: [{ message: e.message, snippet: e.stack || e.message }] })
815
+ reportError(file, [{ message: e.message, snippet: e.stack || e.message }])
732
816
  return new Response(e.message, { status: 500 })
733
817
  }
734
818
  }
@@ -768,7 +852,12 @@ export function serve(entrypoint, flags) {
768
852
  const resolved = resolveFileCandidate('.' + pathname)
769
853
  if (resolved?.endsWith('.imba')) {
770
854
  const out = await compileFile(resolved)
771
- if (out.errors?.length) return new Response(out.errors.map(e => e.message).join('\n'), { status: 500 })
855
+ const file = normalizeFile(resolved)
856
+ if (out.errors?.length) {
857
+ reportError(file, out.errors)
858
+ return new Response(out.errors.map(e => e.message).join('\n'), { status: 500 })
859
+ }
860
+ clearError(file)
772
861
  return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
773
862
  }
774
863
 
@@ -794,7 +883,13 @@ export function serve(entrypoint, flags) {
794
883
  const imbaPath = '.' + pathname + '.imba'
795
884
  if (existsSync(imbaPath)) {
796
885
  const out = await compileFile(imbaPath)
797
- if (!out.errors?.length) return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
886
+ const file = normalizeFile(imbaPath)
887
+ if (out.errors?.length) {
888
+ reportError(file, out.errors)
889
+ return new Response(out.errors.map(e => e.message).join('\n'), { status: 500 })
890
+ }
891
+ clearError(file)
892
+ return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
798
893
  }
799
894
  for (const ext of ['.js', '.mjs']) {
800
895
  const withExt = '.' + pathname + ext