what-compiler 0.5.4 → 0.6.0

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.
@@ -5,6 +5,10 @@
5
5
  * and runtime signal errors with What Framework branding and helpful context.
6
6
  *
7
7
  * This is client-side code that Vite injects into the page during development.
8
+ *
9
+ * Architecture: The overlay HTML template and all helper functions are inlined as
10
+ * string literals into the custom element code. This avoids function-to-string
11
+ * serialization (which is fragile with minifiers and bundlers).
8
12
  */
9
13
 
10
14
  // CSS for the overlay — scoped to avoid style conflicts
@@ -49,6 +53,12 @@ const OVERLAY_STYLES = `
49
53
  gap: 0.75rem;
50
54
  }
51
55
 
56
+ .header-right {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 0.5rem;
60
+ }
61
+
52
62
  .logo {
53
63
  width: 28px;
54
64
  height: 28px;
@@ -84,7 +94,7 @@ const OVERLAY_STYLES = `
84
94
  color: #fbbf24;
85
95
  }
86
96
 
87
- .close-btn {
97
+ .close-btn, .copy-btn {
88
98
  background: none;
89
99
  border: 1px solid #3a3a5a;
90
100
  color: #a0a0c0;
@@ -95,11 +105,16 @@ const OVERLAY_STYLES = `
95
105
  font-size: 12px;
96
106
  }
97
107
 
98
- .close-btn:hover {
108
+ .close-btn:hover, .copy-btn:hover {
99
109
  background: #2a2a4a;
100
110
  color: #fff;
101
111
  }
102
112
 
113
+ .copy-btn.copied {
114
+ border-color: #22c55e;
115
+ color: #22c55e;
116
+ }
117
+
103
118
  .body {
104
119
  padding: 1.5rem;
105
120
  }
@@ -186,132 +201,193 @@ const OVERLAY_STYLES = `
186
201
  `;
187
202
 
188
203
  /**
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.
204
+ * Client-side overlay component injected as a custom element string literal.
205
+ * All helper functions are inlined directly to avoid function.toString() fragility.
294
206
  */
295
207
  const OVERLAY_ELEMENT = `
296
208
  class WhatErrorOverlay extends HTMLElement {
297
209
  constructor(err) {
298
210
  super();
299
211
  this.root = this.attachShadow({ mode: 'open' });
300
- this.root.innerHTML = \`<style>${OVERLAY_STYLES}</style>\`;
212
+ this.root.innerHTML = '<style>${OVERLAY_STYLES}</style>';
213
+ this._err = err;
301
214
  this.show(err);
302
215
  }
303
216
 
217
+ // --- Inlined helper: escapeHTML ---
218
+ _escapeHTML(str) {
219
+ return String(str)
220
+ .replace(/&/g, '&amp;')
221
+ .replace(/</g, '&lt;')
222
+ .replace(/>/g, '&gt;')
223
+ .replace(/"/g, '&quot;');
224
+ }
225
+
226
+ // --- Inlined helper: cleanStack ---
227
+ _cleanStack(stack) {
228
+ return stack
229
+ .split('\\n')
230
+ .filter(function(line) { return line.indexOf('node_modules') === -1; })
231
+ .slice(0, 10)
232
+ .join('\\n');
233
+ }
234
+
235
+ // --- Inlined helper: getTip ---
236
+ _getTip(err) {
237
+ var msg = (err.message || '').toLowerCase();
238
+
239
+ if (msg.indexOf('infinite') !== -1 && msg.indexOf('effect') !== -1) {
240
+ 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.';
241
+ }
242
+ if (msg.indexOf('jsx') !== -1 && msg.indexOf('unexpected') !== -1) {
243
+ return 'Make sure your vite.config includes the What compiler plugin: import what from "what-compiler/vite"';
244
+ }
245
+ if (msg.indexOf('not a function') !== -1 && msg.indexOf('signal') !== -1) {
246
+ return 'Signals are functions: call sig() to read, sig(value) to write. Check you are not destructuring a signal.';
247
+ }
248
+ if (msg.indexOf('hydrat') !== -1) {
249
+ return 'Hydration mismatches happen when SSR output differs from client render. Ensure server and client see the same initial state.';
250
+ }
251
+ // New tips for common mistakes
252
+ if (msg.indexOf('signal') !== -1 && msg.indexOf('without') !== -1 && msg.indexOf('call') !== -1) {
253
+ return 'Signals must be called to read their value. Use {count()} in JSX, not {count}. The parentheses trigger the reactive subscription.';
254
+ }
255
+ if (msg.indexOf('innerhtml') !== -1 && msg.indexOf('__html') !== -1) {
256
+ return 'Raw innerHTML is blocked for security. Use innerHTML={{ __html: trustedString }} or dangerouslySetInnerHTML={{ __html: trustedString }} instead.';
257
+ }
258
+ if ((msg.indexOf('innerhtml') !== -1 || msg.indexOf('xss') !== -1) && msg.indexOf('raw string') !== -1) {
259
+ return 'Raw innerHTML is a security risk (XSS). Wrap your HTML in an object: innerHTML={{ __html: yourString }}.';
260
+ }
261
+ if (msg.indexOf('cleanup') !== -1 && (msg.indexOf('effect') !== -1 || msg.indexOf('listener') !== -1)) {
262
+ return 'Effects that add event listeners or timers should return a cleanup function: effect(() => { el.addEventListener(...); return () => el.removeEventListener(...); })';
263
+ }
264
+ if (msg.indexOf('route') !== -1 && (msg.indexOf('not found') !== -1 || msg.indexOf('404') !== -1 || msg.indexOf('no match') !== -1)) {
265
+ return 'No route matched the current URL. Check that your route paths are correct and you have a catch-all or 404 route defined.';
266
+ }
267
+ if (msg.indexOf('key') !== -1 && (msg.indexOf('missing') !== -1 || msg.indexOf('list') !== -1 || msg.indexOf('each') !== -1)) {
268
+ return 'Lists need unique keys for efficient DOM updates. Add a key prop: items.map(item => <Item key={item.id} />)';
269
+ }
270
+ return '';
271
+ }
272
+
273
+ // --- Build overlay HTML ---
274
+ _buildHTML(err) {
275
+ var isCompilerError = err._isCompilerError || err.plugin === 'vite-plugin-what';
276
+ var type = isCompilerError ? 'Compiler Error' : 'Runtime Error';
277
+ var tagClass = isCompilerError ? 'tag-error' : 'tag-warning';
278
+
279
+ var codeFrame = '';
280
+ var rawFrame = err.frame || err._frame;
281
+ if (rawFrame) {
282
+ var lines = rawFrame.split('\\n');
283
+ var frameLines = '';
284
+ for (var i = 0; i < lines.length; i++) {
285
+ var line = lines[i];
286
+ var isHighlight = line.trimStart().startsWith('>');
287
+ var cleaned = line.replace(/^\\s*>\\s?/, ' ').replace(/^\\s{2}/, '');
288
+ var match = cleaned.match(/^(\\s*\\d+)\\s*\\|(.*)$/);
289
+ if (match) {
290
+ frameLines += '<div class="code-line' + (isHighlight ? ' highlight' : '') + '"><span class="line-number">' + match[1].trim() + '</span><span class="line-content">' + this._escapeHTML(match[2]) + '</span></div>';
291
+ } else if (cleaned.trim().startsWith('|')) {
292
+ frameLines += '<div class="code-line highlight"><span class="line-number"></span><span class="line-content" style="color:#f87171">' + this._escapeHTML(cleaned.replace(/^\\s*\\|/, '')) + '</span></div>';
293
+ }
294
+ }
295
+ if (frameLines) {
296
+ codeFrame = '<div class="code-frame">' + frameLines + '</div>';
297
+ }
298
+ }
299
+
300
+ var filePath = err.id || (err.loc && err.loc.file) || '';
301
+ var lineNum = (err.loc && err.loc.line != null) ? err.loc.line : '';
302
+ var col = (err.loc && err.loc.column != null) ? err.loc.column : '';
303
+ var location = filePath
304
+ ? '<div class="file-path">' + this._escapeHTML(filePath) + (lineNum ? ':' + lineNum : '') + (col ? ':' + col : '') + '</div>'
305
+ : '';
306
+
307
+ var tip = this._getTip(err);
308
+ var tipHTML = tip ? '<div class="tip"><span class="tip-label">Tip: </span>' + this._escapeHTML(tip) + '</div>' : '';
309
+
310
+ var stack = (err.stack && !isCompilerError)
311
+ ? '<div class="stack">' + this._escapeHTML(this._cleanStack(err.stack)) + '</div>'
312
+ : '';
313
+
314
+ return '<div class="backdrop"></div>'
315
+ + '<div class="panel">'
316
+ + '<div class="header">'
317
+ + '<div class="header-left">'
318
+ + '<div class="logo">W</div>'
319
+ + '<span class="brand">What Framework</span>'
320
+ + '<span class="tag ' + tagClass + '">' + type + '</span>'
321
+ + '</div>'
322
+ + '<div class="header-right">'
323
+ + '<button class="copy-btn">Copy Error</button>'
324
+ + '<button class="close-btn">Dismiss (Esc)</button>'
325
+ + '</div>'
326
+ + '</div>'
327
+ + '<div class="body">'
328
+ + '<h2 class="error-title">' + this._escapeHTML(err.name || 'Error') + '</h2>'
329
+ + location
330
+ + '<pre class="error-message">' + this._escapeHTML(err.message || String(err)) + '</pre>'
331
+ + codeFrame
332
+ + tipHTML
333
+ + stack
334
+ + '</div>'
335
+ + '</div>';
336
+ }
337
+
304
338
  show(err) {
305
- const template = document.createElement('template');
306
- template.innerHTML = (${buildOverlayHTML.toString()})(err);
339
+ var template = document.createElement('template');
340
+ template.innerHTML = this._buildHTML(err);
307
341
  this.root.appendChild(template.content.cloneNode(true));
308
342
 
309
343
  // 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();
344
+ var self = this;
345
+ var closeBtn = this.root.querySelector('.close-btn');
346
+ if (closeBtn) closeBtn.addEventListener('click', function() { self.close(); });
347
+ var backdrop = this.root.querySelector('.backdrop');
348
+ if (backdrop) backdrop.addEventListener('click', function() { self.close(); });
349
+ document.addEventListener('keydown', this._onKey = function(e) {
350
+ if (e.key === 'Escape') self.close();
314
351
  });
352
+
353
+ // Copy Error button
354
+ var copyBtn = this.root.querySelector('.copy-btn');
355
+ if (copyBtn) {
356
+ copyBtn.addEventListener('click', function() {
357
+ self._copyError(copyBtn);
358
+ });
359
+ }
360
+ }
361
+
362
+ _copyError(btn) {
363
+ var err = this._err;
364
+ var data = {
365
+ name: err.name || 'Error',
366
+ message: err.message || String(err),
367
+ file: err.id || (err.loc && err.loc.file) || null,
368
+ line: (err.loc && err.loc.line != null) ? err.loc.line : null,
369
+ column: (err.loc && err.loc.column != null) ? err.loc.column : null,
370
+ stack: err.stack ? this._cleanStack(err.stack) : null,
371
+ framework: 'What Framework',
372
+ timestamp: new Date().toISOString()
373
+ };
374
+
375
+ var text = JSON.stringify(data, null, 2);
376
+ if (navigator.clipboard && navigator.clipboard.writeText) {
377
+ navigator.clipboard.writeText(text).then(function() {
378
+ btn.textContent = 'Copied!';
379
+ btn.classList.add('copied');
380
+ setTimeout(function() {
381
+ btn.textContent = 'Copy Error';
382
+ btn.classList.remove('copied');
383
+ }, 2000);
384
+ }).catch(function() {
385
+ // Fallback: select text
386
+ prompt('Copy error details:', text);
387
+ });
388
+ } else {
389
+ prompt('Copy error details:', text);
390
+ }
315
391
  }
316
392
 
317
393
  close() {
@@ -320,11 +396,6 @@ class WhatErrorOverlay extends HTMLElement {
320
396
  }
321
397
  }
322
398
 
323
- // Helper functions bundled into the overlay element
324
- ${escapeHTML.toString()}
325
- ${cleanStack.toString()}
326
- ${getTip.toString()}
327
-
328
399
  if (!customElements.get('what-error-overlay')) {
329
400
  customElements.define('what-error-overlay', WhatErrorOverlay);
330
401
  }
@@ -150,7 +150,8 @@ function fileNameToSegment(name) {
150
150
  const dynamic = name.match(/^\[(\w+)\]$/);
151
151
  if (dynamic) return ':' + dynamic[1];
152
152
 
153
- return name;
153
+ // Lowercase page names for URL consistency (About.jsx → /about)
154
+ return name.toLowerCase();
154
155
  }
155
156
 
156
157
  /**
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * 1. Transforms JSX via the What babel plugin
5
5
  * 2. Provides file-based routing via virtual:what-routes
6
- * 3. Watches pages directory for HMR
6
+ * 3. Watches pages directory for route changes
7
+ * 4. HMR support: component files get granular hot-module replacement,
8
+ * signal/utility files trigger full reload
7
9
  */
8
10
 
9
11
  import path from 'path';
@@ -15,6 +17,11 @@ import { setupErrorOverlay } from './error-overlay.js';
15
17
  const VIRTUAL_ROUTES_ID = 'virtual:what-routes';
16
18
  const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_ROUTES_ID;
17
19
 
20
+ // Pattern: exported function starting with uppercase = component
21
+ const COMPONENT_EXPORT_RE = /export\s+(?:default\s+)?function\s+([A-Z]\w*)/;
22
+ // Pattern: files that are likely signal/store/utility files
23
+ const UTILITY_FILE_RE = /(?:store|signal|state|context|util|helper|lib|config)\b/i;
24
+
18
25
  export default function whatVitePlugin(options = {}) {
19
26
  const {
20
27
  // File extensions to process
@@ -27,11 +34,14 @@ export default function whatVitePlugin(options = {}) {
27
34
  production = process.env.NODE_ENV === 'production',
28
35
  // Pages directory (relative to project root)
29
36
  pages = 'src/pages',
37
+ // HMR: enabled by default in dev, disabled in production
38
+ hot = !production,
30
39
  } = options;
31
40
 
32
41
  let rootDir = '';
33
42
  let pagesDir = '';
34
43
  let server = null;
44
+ let isDevMode = false;
35
45
 
36
46
  return {
37
47
  name: 'vite-plugin-what',
@@ -39,6 +49,7 @@ export default function whatVitePlugin(options = {}) {
39
49
  configResolved(config) {
40
50
  rootDir = config.root;
41
51
  pagesDir = path.resolve(rootDir, pages);
52
+ isDevMode = config.command === 'serve';
42
53
  },
43
54
 
44
55
  configureServer(devServer) {
@@ -106,8 +117,19 @@ export default function whatVitePlugin(options = {}) {
106
117
  return null;
107
118
  }
108
119
 
120
+ let outputCode = result.code;
121
+
122
+ // HMR: append hot boundary code for component files in dev mode
123
+ if (hot && isDevMode && !production) {
124
+ const isComponentFile = isComponentModule(code, id);
125
+
126
+ if (isComponentFile) {
127
+ outputCode += generateHMRBoundary(id);
128
+ }
129
+ }
130
+
109
131
  return {
110
- code: result.code,
132
+ code: outputCode,
111
133
  map: result.map
112
134
  };
113
135
  } catch (error) {
@@ -122,11 +144,31 @@ export default function whatVitePlugin(options = {}) {
122
144
  }
123
145
  },
124
146
 
147
+ // HMR: detect component vs utility files and handle accordingly
148
+ handleHotUpdate({ file, server: devServer, modules }) {
149
+ if (!hot) return;
150
+
151
+ // Only handle files we process
152
+ if (!include.test(file)) return;
153
+ if (exclude && exclude.test(file)) return;
154
+
155
+ // Utility/signal/store files: trigger full reload
156
+ // These files may export signals used across multiple components
157
+ if (isUtilityFile(file)) {
158
+ devServer.ws.send({ type: 'full-reload' });
159
+ return [];
160
+ }
161
+
162
+ // Component files: let Vite handle HMR normally (our boundary code handles it)
163
+ // Return undefined to let Vite's default HMR proceed
164
+ return;
165
+ },
166
+
125
167
  // Configure for development
126
168
  config(config, { mode }) {
127
169
  return {
128
170
  esbuild: {
129
- // Preserve JSX so our babel plugin handles it don't let esbuild transform it
171
+ // Preserve JSX so our babel plugin handles it -- don't let esbuild transform it
130
172
  jsx: 'preserve',
131
173
  },
132
174
  optimizeDeps: {
@@ -138,5 +180,46 @@ export default function whatVitePlugin(options = {}) {
138
180
  };
139
181
  }
140
182
 
183
+ /**
184
+ * Check if a file likely contains a component (has exported function starting with uppercase)
185
+ */
186
+ function isComponentModule(source, filePath) {
187
+ // .jsx/.tsx files with component exports
188
+ if (COMPONENT_EXPORT_RE.test(source)) return true;
189
+ // Pages are always component files
190
+ if (filePath.includes('/pages/') || filePath.includes('\\pages\\')) return true;
191
+ return false;
192
+ }
193
+
194
+ /**
195
+ * Check if a file is a utility/signal/store file (should trigger full reload)
196
+ */
197
+ function isUtilityFile(filePath) {
198
+ const basename = path.basename(filePath, path.extname(filePath));
199
+ return UTILITY_FILE_RE.test(basename);
200
+ }
201
+
202
+ /**
203
+ * Generate HMR boundary code for a component file.
204
+ * When the module is updated, Vite's HMR runtime calls import.meta.hot.accept(),
205
+ * which re-runs the module. The component re-renders in place.
206
+ */
207
+ function generateHMRBoundary(filePath) {
208
+ return `
209
+
210
+ // --- What Framework HMR Boundary ---
211
+ if (import.meta.hot) {
212
+ import.meta.hot.accept((newModule) => {
213
+ if (newModule) {
214
+ // Signal to the What runtime that this module was hot-updated
215
+ if (window.__WHAT_HMR_ACCEPT__) {
216
+ window.__WHAT_HMR_ACCEPT__(${JSON.stringify(filePath)}, newModule);
217
+ }
218
+ }
219
+ });
220
+ }
221
+ `;
222
+ }
223
+
141
224
  // Named export for compatibility
142
225
  export { whatVitePlugin as what };