what-compiler 0.5.0 → 0.5.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-compiler",
3
- "version": "0.5.0",
3
+ "version": "0.5.3",
4
4
  "description": "JSX compiler for What Framework - transforms JSX to optimized DOM operations",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  "license": "MIT",
24
24
  "peerDependencies": {
25
25
  "@babel/core": "^7.0.0",
26
- "what-core": "^0.5.0"
26
+ "what-core": "^0.5.3"
27
27
  },
28
28
  "files": [
29
29
  "src"
@@ -31,5 +31,13 @@
31
31
  "devDependencies": {
32
32
  "@babel/core": "^7.23.0",
33
33
  "@babel/preset-env": "^7.23.0"
34
- }
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/zvndev/what-fw"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/zvndev/what-fw/issues"
41
+ },
42
+ "homepage": "https://whatframework.dev"
35
43
  }
@@ -20,6 +20,22 @@
20
20
 
21
21
  const EVENT_MODIFIERS = new Set(['preventDefault', 'stopPropagation', 'once', 'capture', 'passive', 'self']);
22
22
  const EVENT_OPTION_MODIFIERS = new Set(['once', 'capture', 'passive']);
23
+ const VOID_HTML_ELEMENTS = new Set([
24
+ 'area',
25
+ 'base',
26
+ 'br',
27
+ 'col',
28
+ 'embed',
29
+ 'hr',
30
+ 'img',
31
+ 'input',
32
+ 'link',
33
+ 'meta',
34
+ 'param',
35
+ 'source',
36
+ 'track',
37
+ 'wbr'
38
+ ]);
23
39
 
24
40
  export default function whatBabelPlugin({ types: t }) {
25
41
  const mode = 'fine-grained'; // Can be overridden via plugin options
@@ -47,6 +63,10 @@ export default function whatBabelPlugin({ types: t }) {
47
63
  return /^[A-Z]/.test(name);
48
64
  }
49
65
 
66
+ function isVoidHtmlElement(name) {
67
+ return VOID_HTML_ELEMENTS.has(String(name).toLowerCase());
68
+ }
69
+
50
70
  function getAttributeValue(value) {
51
71
  if (!value) return t.booleanLiteral(true);
52
72
  if (t.isJSXExpressionContainer(value)) return value.expression;
@@ -54,6 +74,12 @@ export default function whatBabelPlugin({ types: t }) {
54
74
  return t.stringLiteral(value.value || '');
55
75
  }
56
76
 
77
+ function normalizeAttrName(attrName) {
78
+ if (attrName === 'className') return 'class';
79
+ if (attrName === 'htmlFor') return 'for';
80
+ return attrName;
81
+ }
82
+
57
83
  function createEventHandler(handler, modifiers) {
58
84
  if (modifiers.length === 0) return handler;
59
85
 
@@ -456,6 +482,14 @@ export default function whatBabelPlugin({ types: t }) {
456
482
  if (t.isTemplateLiteral(expr)) {
457
483
  return expr.expressions.some(isPotentiallyReactive);
458
484
  }
485
+ if (t.isObjectExpression(expr)) {
486
+ return expr.properties.some(prop =>
487
+ t.isObjectProperty(prop) && isPotentiallyReactive(prop.value)
488
+ );
489
+ }
490
+ if (t.isArrayExpression(expr)) {
491
+ return expr.elements.some(el => el && isPotentiallyReactive(el));
492
+ }
459
493
  return false;
460
494
  }
461
495
 
@@ -467,8 +501,9 @@ export default function whatBabelPlugin({ types: t }) {
467
501
  }
468
502
 
469
503
  if (t.isJSXExpressionContainer(node)) {
470
- // Dynamic leave a placeholder
471
- return '';
504
+ // Dynamic child marker so insert() can preserve source ordering
505
+ if (t.isJSXEmptyExpression(node.expression)) return '';
506
+ return '<!--$-->';
472
507
  }
473
508
 
474
509
  if (!t.isJSXElement(node)) return '';
@@ -501,9 +536,13 @@ export default function whatBabelPlugin({ types: t }) {
501
536
  }
502
537
 
503
538
  const selfClosing = node.openingElement.selfClosing;
539
+ if (selfClosing && isVoidHtmlElement(tagName)) {
540
+ html += '>';
541
+ return html;
542
+ }
543
+
504
544
  if (selfClosing) {
505
- // Void elements
506
- html += '/>';
545
+ html += `></${tagName}>`;
507
546
  return html;
508
547
  }
509
548
 
@@ -515,11 +554,12 @@ export default function whatBabelPlugin({ types: t }) {
515
554
  const text = child.value.replace(/\n\s+/g, ' ').trim();
516
555
  if (text) html += escapeHTML(text);
517
556
  } else if (t.isJSXExpressionContainer(child)) {
518
- // Dynamic child — placeholder will be handled by insert()
519
- // Skip entirely from template
557
+ if (!t.isJSXEmptyExpression(child.expression)) {
558
+ html += '<!--$-->';
559
+ }
520
560
  } else if (t.isJSXElement(child)) {
521
561
  if (isComponent(child.openingElement.name.name)) {
522
- // Component — skip from template
562
+ html += '<!--$-->';
523
563
  } else {
524
564
  html += extractStaticHTML(child);
525
565
  }
@@ -615,6 +655,15 @@ export default function whatBabelPlugin({ types: t }) {
615
655
  }
616
656
 
617
657
  function applyDynamicAttrs(statements, elId, attributes, state) {
658
+ function buildSetPropCall(propName, valueExpr) {
659
+ state.needsSetProp = true;
660
+ return t.callExpression(t.identifier('_$setProp'), [
661
+ t.identifier(elId),
662
+ t.stringLiteral(propName),
663
+ valueExpr
664
+ ]);
665
+ }
666
+
618
667
  for (const attr of attributes) {
619
668
  if (t.isJSXSpreadAttribute(attr)) {
620
669
  // spread(el, props) — use runtime spread
@@ -718,75 +767,21 @@ export default function whatBabelPlugin({ types: t }) {
718
767
  // Dynamic attribute (expression)
719
768
  if (t.isJSXExpressionContainer(attr.value)) {
720
769
  const expr = attr.value.expression;
721
- let domName = attrName;
722
- if (attrName === 'className') domName = 'class';
723
- if (attrName === 'htmlFor') domName = 'for';
770
+ const domName = normalizeAttrName(attrName);
724
771
 
725
772
  if (isPotentiallyReactive(expr)) {
726
773
  // Reactive attribute — wrap in effect
727
774
  state.needsEffect = true;
728
- if (domName === 'class') {
729
- statements.push(
730
- t.expressionStatement(
731
- t.callExpression(t.identifier('_$effect'), [
732
- t.arrowFunctionExpression([], t.assignmentExpression('=',
733
- t.memberExpression(t.identifier(elId), t.identifier('className')),
734
- t.logicalExpression('||', expr, t.stringLiteral(''))
735
- ))
736
- ])
737
- )
738
- );
739
- } else if (domName === 'style') {
740
- statements.push(
741
- t.expressionStatement(
742
- t.callExpression(t.identifier('_$effect'), [
743
- t.arrowFunctionExpression([], t.blockStatement([
744
- t.expressionStatement(
745
- t.callExpression(
746
- t.memberExpression(
747
- t.memberExpression(t.identifier('Object'), t.identifier('assign')),
748
- t.identifier('call')
749
- ),
750
- [t.nullLiteral(), t.memberExpression(t.identifier(elId), t.identifier('style')), expr]
751
- )
752
- )
753
- ]))
754
- ])
755
- )
756
- );
757
- } else {
758
- statements.push(
759
- t.expressionStatement(
760
- t.callExpression(t.identifier('_$effect'), [
761
- t.arrowFunctionExpression([], t.callExpression(
762
- t.memberExpression(t.identifier(elId), t.identifier('setAttribute')),
763
- [t.stringLiteral(domName), expr]
764
- ))
765
- ])
766
- )
767
- );
768
- }
775
+ statements.push(
776
+ t.expressionStatement(
777
+ t.callExpression(t.identifier('_$effect'), [
778
+ t.arrowFunctionExpression([], buildSetPropCall(domName, expr))
779
+ ])
780
+ )
781
+ );
769
782
  } else {
770
783
  // Static expression (no signal calls) — set once
771
- if (domName === 'class') {
772
- statements.push(
773
- t.expressionStatement(
774
- t.assignmentExpression('=',
775
- t.memberExpression(t.identifier(elId), t.identifier('className')),
776
- t.logicalExpression('||', expr, t.stringLiteral(''))
777
- )
778
- )
779
- );
780
- } else {
781
- statements.push(
782
- t.expressionStatement(
783
- t.callExpression(
784
- t.memberExpression(t.identifier(elId), t.identifier('setAttribute')),
785
- [t.stringLiteral(domName), expr]
786
- )
787
- )
788
- );
789
- }
784
+ statements.push(t.expressionStatement(buildSetPropCall(domName, expr)));
790
785
  }
791
786
  }
792
787
  // Static string/boolean attributes already in template
@@ -809,16 +804,16 @@ export default function whatBabelPlugin({ types: t }) {
809
804
  if (t.isJSXEmptyExpression(child.expression)) continue;
810
805
 
811
806
  const expr = child.expression;
807
+ const marker = buildChildAccess(elId, childIndex);
812
808
  state.needsInsert = true;
813
809
 
814
- // insert(parent, () => expr, marker?)
815
- // For now use simple insert without marker — appends
816
810
  if (isPotentiallyReactive(expr)) {
817
811
  statements.push(
818
812
  t.expressionStatement(
819
813
  t.callExpression(t.identifier('_$insert'), [
820
814
  t.identifier(elId),
821
- t.arrowFunctionExpression([], expr)
815
+ t.arrowFunctionExpression([], expr),
816
+ marker
822
817
  ])
823
818
  )
824
819
  );
@@ -827,11 +822,13 @@ export default function whatBabelPlugin({ types: t }) {
827
822
  t.expressionStatement(
828
823
  t.callExpression(t.identifier('_$insert'), [
829
824
  t.identifier(elId),
830
- expr
825
+ expr,
826
+ marker
831
827
  ])
832
828
  )
833
829
  );
834
830
  }
831
+ childIndex++;
835
832
  continue;
836
833
  }
837
834
 
@@ -840,15 +837,18 @@ export default function whatBabelPlugin({ types: t }) {
840
837
  if (isComponent(childTag) || childTag === 'For' || childTag === 'Show') {
841
838
  // Component/control-flow — transform and insert
842
839
  const transformed = transformElementFineGrained({ node: child }, state);
840
+ const marker = buildChildAccess(elId, childIndex);
843
841
  state.needsInsert = true;
844
842
  statements.push(
845
843
  t.expressionStatement(
846
844
  t.callExpression(t.identifier('_$insert'), [
847
845
  t.identifier(elId),
848
- transformed
846
+ transformed,
847
+ marker
849
848
  ])
850
849
  )
851
850
  );
851
+ childIndex++;
852
852
  } else {
853
853
  // Static child element — already in template
854
854
  // But check if it has dynamic children/attrs that need effects
@@ -907,10 +907,10 @@ export default function whatBabelPlugin({ types: t }) {
907
907
  }
908
908
 
909
909
  function buildChildAccess(elId, index) {
910
- // Build _el$.children[index] or _el$.firstChild / .firstChild.nextSibling chain
911
- // Use children[n] for simplicity and readability
910
+ // Use childNodes[n] (not children[n]) so indices remain stable when text/comment
911
+ // placeholders are present in the static template.
912
912
  return t.memberExpression(
913
- t.memberExpression(t.identifier(elId), t.identifier('children')),
913
+ t.memberExpression(t.identifier(elId), t.identifier('childNodes')),
914
914
  t.numericLiteral(index),
915
915
  true // computed
916
916
  );
@@ -1022,6 +1022,7 @@ export default function whatBabelPlugin({ types: t }) {
1022
1022
  state.needsEffect = false;
1023
1023
  state.needsMapArray = false;
1024
1024
  state.needsSpread = false;
1025
+ state.needsSetProp = false;
1025
1026
  state.templates = [];
1026
1027
  state.templateCount = 0;
1027
1028
  state._varCounter = 0;
@@ -1069,6 +1070,11 @@ export default function whatBabelPlugin({ types: t }) {
1069
1070
  t.importSpecifier(t.identifier('_$spread'), t.identifier('spread'))
1070
1071
  );
1071
1072
  }
1073
+ if (state.needsSetProp) {
1074
+ fgSpecifiers.push(
1075
+ t.importSpecifier(t.identifier('_$setProp'), t.identifier('setProp'))
1076
+ );
1077
+ }
1072
1078
 
1073
1079
  // Also include h/Fragment/Island if vdom mode used for components
1074
1080
  const coreSpecifiers = [];
@@ -0,0 +1,357 @@
1
+ /**
2
+ * What Framework — Vite Error Overlay
3
+ *
4
+ * Custom error overlay injected during dev mode. Shows compiler transform errors
5
+ * and runtime signal errors with What Framework branding and helpful context.
6
+ *
7
+ * This is client-side code that Vite injects into the page during development.
8
+ */
9
+
10
+ // CSS for the overlay — scoped to avoid style conflicts
11
+ const OVERLAY_STYLES = `
12
+ :host {
13
+ position: fixed;
14
+ inset: 0;
15
+ z-index: 99999;
16
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
17
+ }
18
+
19
+ .backdrop {
20
+ position: fixed;
21
+ inset: 0;
22
+ background: rgba(0, 0, 0, 0.66);
23
+ }
24
+
25
+ .panel {
26
+ position: fixed;
27
+ inset: 2rem;
28
+ overflow: auto;
29
+ background: #1a1a2e;
30
+ border: 1px solid #2a2a4a;
31
+ border-radius: 12px;
32
+ box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
33
+ color: #e0e0e0;
34
+ }
35
+
36
+ .header {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: space-between;
40
+ padding: 1rem 1.5rem;
41
+ border-bottom: 1px solid #2a2a4a;
42
+ background: #16163a;
43
+ border-radius: 12px 12px 0 0;
44
+ }
45
+
46
+ .header-left {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 0.75rem;
50
+ }
51
+
52
+ .logo {
53
+ width: 28px;
54
+ height: 28px;
55
+ background: linear-gradient(135deg, #2563eb, #1d4ed8);
56
+ border-radius: 6px;
57
+ display: grid;
58
+ place-items: center;
59
+ font-weight: 800;
60
+ font-size: 14px;
61
+ color: #fff;
62
+ }
63
+
64
+ .brand {
65
+ font-size: 14px;
66
+ font-weight: 600;
67
+ color: #a0a0c0;
68
+ }
69
+
70
+ .tag {
71
+ font-size: 11px;
72
+ padding: 2px 8px;
73
+ border-radius: 4px;
74
+ font-weight: 600;
75
+ }
76
+
77
+ .tag-error {
78
+ background: #3b1219;
79
+ color: #f87171;
80
+ }
81
+
82
+ .tag-warning {
83
+ background: #3b2f19;
84
+ color: #fbbf24;
85
+ }
86
+
87
+ .close-btn {
88
+ background: none;
89
+ border: 1px solid #3a3a5a;
90
+ color: #a0a0c0;
91
+ border-radius: 6px;
92
+ padding: 4px 12px;
93
+ cursor: pointer;
94
+ font-family: inherit;
95
+ font-size: 12px;
96
+ }
97
+
98
+ .close-btn:hover {
99
+ background: #2a2a4a;
100
+ color: #fff;
101
+ }
102
+
103
+ .body {
104
+ padding: 1.5rem;
105
+ }
106
+
107
+ .error-title {
108
+ font-size: 16px;
109
+ font-weight: 700;
110
+ color: #f87171;
111
+ margin: 0 0 0.5rem;
112
+ }
113
+
114
+ .error-message {
115
+ font-size: 14px;
116
+ color: #e0e0e0;
117
+ margin: 0 0 1rem;
118
+ line-height: 1.6;
119
+ white-space: pre-wrap;
120
+ }
121
+
122
+ .file-path {
123
+ display: inline-flex;
124
+ align-items: center;
125
+ gap: 0.5rem;
126
+ font-size: 12px;
127
+ color: #818cf8;
128
+ margin-bottom: 1rem;
129
+ padding: 0.25rem 0;
130
+ }
131
+
132
+ .code-frame {
133
+ background: #0d0d1a;
134
+ border: 1px solid #2a2a4a;
135
+ border-radius: 8px;
136
+ overflow-x: auto;
137
+ margin-bottom: 1rem;
138
+ }
139
+
140
+ .code-line {
141
+ display: flex;
142
+ padding: 0 1rem;
143
+ font-size: 13px;
144
+ line-height: 1.7;
145
+ }
146
+
147
+ .code-line.highlight {
148
+ background: rgba(248, 113, 113, 0.1);
149
+ }
150
+
151
+ .line-number {
152
+ color: #4a4a6a;
153
+ min-width: 3ch;
154
+ text-align: right;
155
+ margin-right: 1rem;
156
+ user-select: none;
157
+ }
158
+
159
+ .line-content {
160
+ white-space: pre;
161
+ }
162
+
163
+ .tip {
164
+ margin-top: 1rem;
165
+ padding: 0.75rem 1rem;
166
+ background: #1a2744;
167
+ border: 1px solid #1e3a5f;
168
+ border-radius: 8px;
169
+ font-size: 13px;
170
+ color: #93c5fd;
171
+ line-height: 1.5;
172
+ }
173
+
174
+ .tip-label {
175
+ font-weight: 700;
176
+ color: #60a5fa;
177
+ }
178
+
179
+ .stack {
180
+ margin-top: 1rem;
181
+ font-size: 12px;
182
+ color: #6a6a8a;
183
+ white-space: pre-wrap;
184
+ line-height: 1.5;
185
+ }
186
+ `;
187
+
188
+ /**
189
+ * Build the overlay HTML for an error
190
+ */
191
+ function buildOverlayHTML(err) {
192
+ const isCompilerError = err._isCompilerError || err.plugin === 'vite-plugin-what';
193
+ const type = isCompilerError ? 'Compiler Error' : 'Runtime Error';
194
+ const tagClass = isCompilerError ? 'tag-error' : 'tag-warning';
195
+
196
+ let codeFrame = '';
197
+ if (err.frame || err._frame) {
198
+ const frame = err.frame || err._frame;
199
+ const lines = frame.split('\n');
200
+ codeFrame = `<div class="code-frame">${
201
+ lines.map(line => {
202
+ const isHighlight = line.trimStart().startsWith('>');
203
+ const cleaned = line.replace(/^\s*>\s?/, ' ').replace(/^\s{2}/, '');
204
+ const match = cleaned.match(/^(\s*\d+)\s*\|(.*)$/);
205
+ if (match) {
206
+ return `<div class="code-line${isHighlight ? ' highlight' : ''}"><span class="line-number">${match[1].trim()}</span><span class="line-content">${escapeHTML(match[2])}</span></div>`;
207
+ }
208
+ // Caret line (^^^)
209
+ if (cleaned.trim().startsWith('|')) {
210
+ return `<div class="code-line highlight"><span class="line-number"></span><span class="line-content" style="color:#f87171">${escapeHTML(cleaned.replace(/^\s*\|/, ''))}</span></div>`;
211
+ }
212
+ return '';
213
+ }).join('')
214
+ }</div>`;
215
+ }
216
+
217
+ const filePath = err.id || err.loc?.file || '';
218
+ const line = err.loc?.line ?? '';
219
+ const col = err.loc?.column ?? '';
220
+ const location = filePath
221
+ ? `<div class="file-path">${escapeHTML(filePath)}${line ? `:${line}` : ''}${col ? `:${col}` : ''}</div>`
222
+ : '';
223
+
224
+ const tip = getTip(err);
225
+ const tipHTML = tip ? `<div class="tip"><span class="tip-label">Tip: </span>${escapeHTML(tip)}</div>` : '';
226
+
227
+ const stack = err.stack && !isCompilerError
228
+ ? `<div class="stack">${escapeHTML(cleanStack(err.stack))}</div>`
229
+ : '';
230
+
231
+ return `
232
+ <div class="backdrop"></div>
233
+ <div class="panel">
234
+ <div class="header">
235
+ <div class="header-left">
236
+ <div class="logo">W</div>
237
+ <span class="brand">What Framework</span>
238
+ <span class="tag ${tagClass}">${type}</span>
239
+ </div>
240
+ <button class="close-btn">Dismiss (Esc)</button>
241
+ </div>
242
+ <div class="body">
243
+ <h2 class="error-title">${escapeHTML(err.name || 'Error')}</h2>
244
+ ${location}
245
+ <pre class="error-message">${escapeHTML(err.message || String(err))}</pre>
246
+ ${codeFrame}
247
+ ${tipHTML}
248
+ ${stack}
249
+ </div>
250
+ </div>
251
+ `;
252
+ }
253
+
254
+ /**
255
+ * Context-aware tips for common What Framework errors
256
+ */
257
+ function getTip(err) {
258
+ const msg = (err.message || '').toLowerCase();
259
+
260
+ if (msg.includes('infinite') && msg.includes('effect')) {
261
+ return 'An effect is writing to a signal it also reads. Use untrack() to read without subscribing, or move the write to a different effect.';
262
+ }
263
+ if (msg.includes('jsx') && msg.includes('unexpected')) {
264
+ return 'Make sure your vite.config includes the What compiler plugin: import what from "what-compiler/vite"';
265
+ }
266
+ if (msg.includes('not a function') && msg.includes('signal')) {
267
+ return 'Signals are functions: call sig() to read, sig(value) to write. Check you\'re not destructuring a signal.';
268
+ }
269
+ if (msg.includes('hydrat')) {
270
+ return 'Hydration mismatches happen when SSR output differs from client render. Ensure server and client see the same initial state.';
271
+ }
272
+ return '';
273
+ }
274
+
275
+ function escapeHTML(str) {
276
+ return str
277
+ .replace(/&/g, '&amp;')
278
+ .replace(/</g, '&lt;')
279
+ .replace(/>/g, '&gt;')
280
+ .replace(/"/g, '&quot;');
281
+ }
282
+
283
+ function cleanStack(stack) {
284
+ return stack
285
+ .split('\n')
286
+ .filter(line => !line.includes('node_modules'))
287
+ .slice(0, 10)
288
+ .join('\n');
289
+ }
290
+
291
+ /**
292
+ * Client-side overlay component — injected as a custom element
293
+ * to avoid style conflicts with the user's application.
294
+ */
295
+ const OVERLAY_ELEMENT = `
296
+ class WhatErrorOverlay extends HTMLElement {
297
+ constructor(err) {
298
+ super();
299
+ this.root = this.attachShadow({ mode: 'open' });
300
+ this.root.innerHTML = \`<style>${OVERLAY_STYLES}</style>\`;
301
+ this.show(err);
302
+ }
303
+
304
+ show(err) {
305
+ const template = document.createElement('template');
306
+ template.innerHTML = (${buildOverlayHTML.toString()})(err);
307
+ this.root.appendChild(template.content.cloneNode(true));
308
+
309
+ // Close handlers
310
+ this.root.querySelector('.close-btn')?.addEventListener('click', () => this.close());
311
+ this.root.querySelector('.backdrop')?.addEventListener('click', () => this.close());
312
+ document.addEventListener('keydown', this._onKey = (e) => {
313
+ if (e.key === 'Escape') this.close();
314
+ });
315
+ }
316
+
317
+ close() {
318
+ document.removeEventListener('keydown', this._onKey);
319
+ this.remove();
320
+ }
321
+ }
322
+
323
+ // Helper functions bundled into the overlay element
324
+ ${escapeHTML.toString()}
325
+ ${cleanStack.toString()}
326
+ ${getTip.toString()}
327
+
328
+ if (!customElements.get('what-error-overlay')) {
329
+ customElements.define('what-error-overlay', WhatErrorOverlay);
330
+ }
331
+ `;
332
+
333
+ /**
334
+ * Generate the client-side error overlay injection script.
335
+ * Called by the Vite plugin to inject into the dev server.
336
+ */
337
+ export function getErrorOverlayCode() {
338
+ return OVERLAY_ELEMENT;
339
+ }
340
+
341
+ /**
342
+ * Create the error overlay middleware for Vite's dev server.
343
+ * Intercepts Vite's error events and shows a custom What-branded overlay.
344
+ */
345
+ export function setupErrorOverlay(server) {
346
+ // Listen for Vite errors and enrich with What Framework context
347
+ const origSend = server.ws.send.bind(server.ws);
348
+ server.ws.send = function (payload) {
349
+ if (payload?.type === 'error') {
350
+ // Tag compiler errors
351
+ if (payload.err?.plugin === 'vite-plugin-what') {
352
+ payload.err._isCompilerError = true;
353
+ }
354
+ }
355
+ return origSend(payload);
356
+ };
357
+ }
@@ -10,6 +10,7 @@ import path from 'path';
10
10
  import { transformSync } from '@babel/core';
11
11
  import whatBabelPlugin from './babel-plugin.js';
12
12
  import { generateRoutesModule, scanPages } from './file-router.js';
13
+ import { setupErrorOverlay } from './error-overlay.js';
13
14
 
14
15
  const VIRTUAL_ROUTES_ID = 'virtual:what-routes';
15
16
  const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_ROUTES_ID;
@@ -43,6 +44,9 @@ export default function whatVitePlugin(options = {}) {
43
44
  configureServer(devServer) {
44
45
  server = devServer;
45
46
 
47
+ // Set up What-branded error overlay
48
+ setupErrorOverlay(devServer);
49
+
46
50
  // Watch the pages directory for file additions/removals
47
51
  devServer.watcher.on('add', (file) => {
48
52
  if (file.startsWith(pagesDir)) {
@@ -107,6 +111,12 @@ export default function whatVitePlugin(options = {}) {
107
111
  map: result.map
108
112
  };
109
113
  } catch (error) {
114
+ // Enrich Babel errors with file context for the error overlay
115
+ error.plugin = 'vite-plugin-what';
116
+ if (!error.id) error.id = id;
117
+ if (error.loc === undefined && error._loc) {
118
+ error.loc = { file: id, line: error._loc.line, column: error._loc.column };
119
+ }
110
120
  console.error(`[what] Error transforming ${id}:`, error.message);
111
121
  throw error;
112
122
  }