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.
- package/README.md +1 -1
- package/dist/babel-plugin.js +1238 -0
- package/dist/babel-plugin.js.map +7 -0
- package/dist/babel-plugin.min.js +2 -0
- package/dist/babel-plugin.min.js.map +7 -0
- package/dist/file-router.js +195 -0
- package/dist/file-router.js.map +7 -0
- package/dist/file-router.min.js +3 -0
- package/dist/file-router.min.js.map +7 -0
- package/dist/index.js +1996 -0
- package/dist/index.js.map +7 -0
- package/dist/index.min.js +397 -0
- package/dist/index.min.js.map +7 -0
- package/dist/runtime.js +9 -0
- package/dist/runtime.js.map +7 -0
- package/dist/runtime.min.js +2 -0
- package/dist/runtime.min.js.map +7 -0
- package/dist/vite-plugin.js +1985 -0
- package/dist/vite-plugin.js.map +7 -0
- package/dist/vite-plugin.min.js +397 -0
- package/dist/vite-plugin.min.js.map +7 -0
- package/package.json +27 -11
- package/src/babel-plugin.js +818 -520
- package/src/error-overlay.js +190 -119
- package/src/file-router.js +2 -1
- package/src/vite-plugin.js +86 -3
package/src/error-overlay.js
CHANGED
|
@@ -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
|
-
*
|
|
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, '&')
|
|
278
|
-
.replace(/</g, '<')
|
|
279
|
-
.replace(/>/g, '>')
|
|
280
|
-
.replace(/"/g, '"');
|
|
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 =
|
|
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, '&')
|
|
221
|
+
.replace(/</g, '<')
|
|
222
|
+
.replace(/>/g, '>')
|
|
223
|
+
.replace(/"/g, '"');
|
|
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
|
-
|
|
306
|
-
template.innerHTML =
|
|
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
|
-
|
|
311
|
-
this.root.querySelector('.
|
|
312
|
-
|
|
313
|
-
|
|
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
|
}
|
package/src/file-router.js
CHANGED
|
@@ -150,7 +150,8 @@ function fileNameToSegment(name) {
|
|
|
150
150
|
const dynamic = name.match(/^\[(\w+)\]$/);
|
|
151
151
|
if (dynamic) return ':' + dynamic[1];
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
// Lowercase page names for URL consistency (About.jsx → /about)
|
|
154
|
+
return name.toLowerCase();
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
/**
|
package/src/vite-plugin.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
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 };
|