@absolutejs/absolute 0.19.0-beta.706 → 0.19.0-beta.708
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/dist/angular/browser.js +1 -19
- package/dist/angular/browser.js.map +3 -3
- package/dist/angular/components/constants.js +78 -0
- package/dist/angular/components/core/streamingSlotRegistrar.js +58 -0
- package/dist/angular/components/core/streamingSlotRegistry.js +114 -0
- package/dist/angular/components/defer-slot-payload.js +6 -0
- package/dist/angular/components/defer-slot-templates.directive.js +44 -0
- package/dist/angular/components/defer-slot.component.js +149 -0
- package/dist/angular/components/image.component.js +202 -0
- package/dist/angular/components/index.js +4 -0
- package/dist/angular/components/stream-slot.component.js +103 -0
- package/dist/angular/index.js +91 -36
- package/dist/angular/index.js.map +6 -6
- package/dist/angular/server.js +91 -36
- package/dist/angular/server.js.map +6 -6
- package/dist/build.js +242 -162
- package/dist/build.js.map +12 -12
- package/dist/cli/index.js +214 -142
- package/dist/client/index.js +86 -31
- package/dist/client/index.js.map +4 -4
- package/dist/core/streamingSlotRegistrar.js +1 -19
- package/dist/core/streamingSlotRegistrar.js.map +2 -2
- package/dist/core/streamingSlotRegistry.js +1 -19
- package/dist/core/streamingSlotRegistry.js.map +2 -2
- package/dist/dev/client/constants.ts +26 -0
- package/dist/dev/client/cssUtils.ts +307 -0
- package/dist/dev/client/domDiff.ts +226 -0
- package/dist/dev/client/domState.ts +421 -0
- package/dist/dev/client/domTracker.ts +61 -0
- package/dist/dev/client/errorOverlay.ts +184 -0
- package/dist/dev/client/frameworkDetect.ts +63 -0
- package/dist/dev/client/handlers/angular.ts +578 -0
- package/dist/dev/client/handlers/angularRuntime.ts +231 -0
- package/dist/dev/client/handlers/html.ts +364 -0
- package/dist/dev/client/handlers/htmx.ts +278 -0
- package/dist/dev/client/handlers/react.ts +108 -0
- package/dist/dev/client/handlers/rebuild.ts +153 -0
- package/dist/dev/client/handlers/svelte.ts +334 -0
- package/dist/dev/client/handlers/vue.ts +292 -0
- package/dist/dev/client/headPatch.ts +233 -0
- package/dist/dev/client/hmrClient.ts +273 -0
- package/dist/dev/client/hmrState.ts +14 -0
- package/dist/dev/client/moduleVersions.ts +62 -0
- package/dist/dev/client/reactRefreshSetup.ts +31 -0
- package/dist/index.js +282 -187
- package/dist/index.js.map +15 -15
- package/dist/islands/browser.js +1 -19
- package/dist/islands/browser.js.map +2 -2
- package/dist/islands/index.js +80 -26
- package/dist/islands/index.js.map +5 -5
- package/dist/react/browser.js +7 -25
- package/dist/react/browser.js.map +2 -2
- package/dist/react/components/browser/index.js +101 -101
- package/dist/react/components/index.js +104 -122
- package/dist/react/components/index.js.map +3 -3
- package/dist/react/hooks/index.js +1 -19
- package/dist/react/hooks/index.js.map +2 -2
- package/dist/react/index.js +101 -46
- package/dist/react/index.js.map +6 -6
- package/dist/react/jsxDevRuntimeCompat.js +1 -19
- package/dist/react/jsxDevRuntimeCompat.js.map +2 -2
- package/dist/react/server.js +13 -30
- package/dist/react/server.js.map +4 -4
- package/dist/src/angular/components/constants.d.ts +75 -0
- package/dist/src/angular/components/defer-slot-templates.directive.d.ts +7 -0
- package/dist/src/angular/components/defer-slot.component.d.ts +5 -2
- package/dist/src/angular/components/image.component.d.ts +5 -2
- package/dist/src/angular/components/index.d.ts +4 -4
- package/dist/src/angular/components/stream-slot.component.d.ts +3 -0
- package/dist/src/client/streamSwap.d.ts +0 -10
- package/dist/src/constants.d.ts +1 -0
- package/dist/src/dev/rebuildTrigger.d.ts +1 -1
- package/dist/src/svelte/renderToPipeableStream.d.ts +2 -2
- package/dist/src/svelte/renderToReadableStream.d.ts +2 -2
- package/dist/src/svelte/renderToString.d.ts +2 -2
- package/dist/src/vue/components/Image.d.ts +3 -3
- package/dist/svelte/browser.js +1 -19
- package/dist/svelte/browser.js.map +2 -2
- package/dist/svelte/components/AwaitSlot.svelte +39 -0
- package/dist/svelte/components/AwaitSlot.svelte.d.ts +2 -0
- package/dist/svelte/components/Head.svelte +144 -0
- package/dist/svelte/components/Head.svelte.d.ts +2 -0
- package/dist/svelte/components/Image.svelte +164 -0
- package/dist/svelte/components/Image.svelte.d.ts +5 -0
- package/dist/svelte/components/Island.svelte +71 -0
- package/dist/svelte/components/Island.svelte.d.ts +5 -0
- package/dist/svelte/components/JsonLd.svelte +21 -0
- package/dist/svelte/components/JsonLd.svelte.d.ts +2 -0
- package/dist/svelte/components/StreamSlot.svelte +41 -0
- package/dist/svelte/components/StreamSlot.svelte.d.ts +2 -0
- package/dist/svelte/index.js +93 -37
- package/dist/svelte/index.js.map +7 -7
- package/dist/svelte/server.js +16 -32
- package/dist/svelte/server.js.map +5 -5
- package/dist/types/globals.d.ts +130 -0
- package/dist/vue/browser.js +1 -19
- package/dist/vue/browser.js.map +2 -2
- package/dist/vue/components/Image.js +1 -19
- package/dist/vue/components/Image.js.map +3 -3
- package/dist/vue/components/index.js +1 -19
- package/dist/vue/components/index.js.map +3 -3
- package/dist/vue/index.js +91 -36
- package/dist/vue/index.js.map +7 -7
- package/dist/vue/server.js +13 -30
- package/dist/vue/server.js.map +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import type {} from '../../../types/globals';
|
|
2
|
+
/* Svelte HMR update handler */
|
|
3
|
+
|
|
4
|
+
import { SVELTE_CSS_LOAD_TIMEOUT_MS } from '../constants';
|
|
5
|
+
import {
|
|
6
|
+
saveDOMState,
|
|
7
|
+
restoreDOMState,
|
|
8
|
+
saveScrollState,
|
|
9
|
+
restoreScrollState
|
|
10
|
+
} from '../domState';
|
|
11
|
+
import { detectCurrentFramework, findIndexPath } from '../frameworkDetect';
|
|
12
|
+
|
|
13
|
+
type SvelteHmrWindow = Window & {
|
|
14
|
+
__SVELTE_HMR_ACCEPT__?: Record<string, (mod: unknown) => void>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/* Swap a stylesheet link by matching cssBaseName or framework name */
|
|
18
|
+
const swapStylesheet = (
|
|
19
|
+
cssUrl: string,
|
|
20
|
+
cssBaseName: string,
|
|
21
|
+
framework: string
|
|
22
|
+
) => {
|
|
23
|
+
let existingLink: HTMLLinkElement | null = null;
|
|
24
|
+
document
|
|
25
|
+
.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]')
|
|
26
|
+
.forEach((link) => {
|
|
27
|
+
const href = link.getAttribute('href') ?? '';
|
|
28
|
+
if (href.includes(cssBaseName) || href.includes(framework)) {
|
|
29
|
+
existingLink = link;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!existingLink) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const capturedExisting: HTMLLinkElement = existingLink;
|
|
38
|
+
const newLink = document.createElement('link');
|
|
39
|
+
newLink.rel = 'stylesheet';
|
|
40
|
+
newLink.href = `${cssUrl}?t=${Date.now()}`;
|
|
41
|
+
newLink.onload = () => {
|
|
42
|
+
if (capturedExisting && capturedExisting.parentNode) {
|
|
43
|
+
capturedExisting.remove();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
document.head.appendChild(newLink);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const extractCountFromDOM = () => {
|
|
50
|
+
const countButton = document.querySelector('button');
|
|
51
|
+
if (!countButton || !countButton.textContent) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const countMatch = countButton.textContent.match(/(\d+)/);
|
|
56
|
+
if (!countMatch) {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { initialCount: parseInt(countMatch[1] ?? '0', 10) };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const loadStateFromSession = () => {
|
|
64
|
+
try {
|
|
65
|
+
const stored = sessionStorage.getItem('__SVELTE_HMR_STATE__');
|
|
66
|
+
if (!stored) {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parsed: Record<string, unknown> = JSON.parse(stored);
|
|
71
|
+
if (parsed && Object.keys(parsed).length > 0) {
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {};
|
|
76
|
+
} catch {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const saveStateToSession = (preservedState: Record<string, unknown>) => {
|
|
82
|
+
if (Object.keys(preservedState).length === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
sessionStorage.setItem(
|
|
88
|
+
'__SVELTE_HMR_STATE__',
|
|
89
|
+
JSON.stringify(preservedState)
|
|
90
|
+
);
|
|
91
|
+
} catch {
|
|
92
|
+
/* ignore */
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const collectCssRules = (sheet: CSSStyleSheet) => {
|
|
97
|
+
let rules = '';
|
|
98
|
+
for (let idx = 0; idx < sheet.cssRules.length; idx++) {
|
|
99
|
+
const rule = sheet.cssRules[idx];
|
|
100
|
+
if (!rule) continue;
|
|
101
|
+
rules += `${rule.cssText}\n`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return rules;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const preserveLinkAsInlineStyle = (link: HTMLLinkElement) => {
|
|
108
|
+
try {
|
|
109
|
+
const { sheet } = link;
|
|
110
|
+
if (!sheet || sheet.cssRules.length === 0) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const style = document.createElement('style');
|
|
115
|
+
style.dataset.hmrPreserved = 'true';
|
|
116
|
+
style.textContent = collectCssRules(sheet);
|
|
117
|
+
document.head.appendChild(style);
|
|
118
|
+
|
|
119
|
+
return style;
|
|
120
|
+
} catch {
|
|
121
|
+
/* Cross-origin sheets (e.g. Google Fonts) — clone as fallback */
|
|
122
|
+
const clone = document.createElement('link');
|
|
123
|
+
clone.rel = link.rel;
|
|
124
|
+
clone.href = link.href;
|
|
125
|
+
clone.dataset.hmrPreserved = 'true';
|
|
126
|
+
document.head.appendChild(clone);
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const preserveAllStylesheets = () => {
|
|
133
|
+
const preservedStyles: HTMLStyleElement[] = [];
|
|
134
|
+
document
|
|
135
|
+
.querySelectorAll<HTMLLinkElement>('head link[rel="stylesheet"]')
|
|
136
|
+
.forEach((link) => {
|
|
137
|
+
const style = preserveLinkAsInlineStyle(link);
|
|
138
|
+
if (style) {
|
|
139
|
+
preservedStyles.push(style);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/* Also preserve Svelte injected <style> tags (css: 'injected' mode) */
|
|
144
|
+
document
|
|
145
|
+
.querySelectorAll<HTMLStyleElement>(
|
|
146
|
+
'head style:not([data-hmr-preserved])'
|
|
147
|
+
)
|
|
148
|
+
.forEach((style) => {
|
|
149
|
+
const clone = document.createElement('style');
|
|
150
|
+
clone.dataset.hmrPreserved = 'true';
|
|
151
|
+
clone.textContent = style.textContent;
|
|
152
|
+
document.head.appendChild(clone);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return preservedStyles;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const buildLinkLoadPromise = (link: HTMLLinkElement) => {
|
|
159
|
+
if (link.sheet && link.sheet.cssRules.length > 0) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
164
|
+
link.onload = () => {
|
|
165
|
+
resolve();
|
|
166
|
+
};
|
|
167
|
+
link.onerror = () => {
|
|
168
|
+
resolve();
|
|
169
|
+
};
|
|
170
|
+
setTimeout(resolve, SVELTE_CSS_LOAD_TIMEOUT_MS);
|
|
171
|
+
|
|
172
|
+
return promise;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const cleanupAfterImport = (
|
|
176
|
+
domState: ReturnType<typeof saveDOMState>,
|
|
177
|
+
scrollState: ReturnType<typeof saveScrollState>
|
|
178
|
+
) => {
|
|
179
|
+
document
|
|
180
|
+
.querySelectorAll('[data-hmr-preserved="true"]')
|
|
181
|
+
.forEach((element) => {
|
|
182
|
+
element.remove();
|
|
183
|
+
});
|
|
184
|
+
restoreDOMState(document.body, domState);
|
|
185
|
+
restoreScrollState(scrollState);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const waitForStylesAndCleanup = (
|
|
189
|
+
domState: ReturnType<typeof saveDOMState>,
|
|
190
|
+
scrollState: ReturnType<typeof saveScrollState>
|
|
191
|
+
) => {
|
|
192
|
+
const newLinks = document.querySelectorAll<HTMLLinkElement>(
|
|
193
|
+
'head link[rel="stylesheet"]:not([data-hmr-preserved])'
|
|
194
|
+
);
|
|
195
|
+
const loadPromises: Promise<void>[] = [];
|
|
196
|
+
newLinks.forEach((link) => {
|
|
197
|
+
const promise = buildLinkLoadPromise(link);
|
|
198
|
+
if (promise) {
|
|
199
|
+
loadPromises.push(promise);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const cleanup = () => {
|
|
204
|
+
cleanupAfterImport(domState, scrollState);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
if (loadPromises.length > 0) {
|
|
208
|
+
void Promise.all(loadPromises).then(cleanup);
|
|
209
|
+
} else {
|
|
210
|
+
cleanup();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const handleSvelteUpdate = (message: {
|
|
215
|
+
data: {
|
|
216
|
+
cssBaseName?: string;
|
|
217
|
+
cssUrl?: string;
|
|
218
|
+
html?: string;
|
|
219
|
+
manifest?: Record<string, string>;
|
|
220
|
+
pageModuleUrl?: string;
|
|
221
|
+
serverDuration?: number;
|
|
222
|
+
sourceFile?: string;
|
|
223
|
+
updateType?: string;
|
|
224
|
+
};
|
|
225
|
+
}) => {
|
|
226
|
+
const svelteFrameworkCheck = detectCurrentFramework();
|
|
227
|
+
if (svelteFrameworkCheck !== 'svelte') return;
|
|
228
|
+
|
|
229
|
+
/* CSS-only update: hot-swap stylesheet, no remount needed */
|
|
230
|
+
if (message.data.updateType === 'css-only' && message.data.cssUrl) {
|
|
231
|
+
swapStylesheet(
|
|
232
|
+
message.data.cssUrl,
|
|
233
|
+
message.data.cssBaseName || '',
|
|
234
|
+
'svelte'
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* Component update: preserve state, re-import (bootstrap handles unmount + mount) */
|
|
241
|
+
|
|
242
|
+
/* Save DOM state and scroll position */
|
|
243
|
+
const domState = saveDOMState(document.body);
|
|
244
|
+
const scrollState = saveScrollState();
|
|
245
|
+
|
|
246
|
+
let preservedState: Record<string, unknown> = extractCountFromDOM();
|
|
247
|
+
|
|
248
|
+
if (Object.keys(preservedState).length === 0) {
|
|
249
|
+
preservedState = loadStateFromSession();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* Set preserved state on window + backup to sessionStorage */
|
|
253
|
+
window.__HMR_PRESERVED_STATE__ = preservedState;
|
|
254
|
+
saveStateToSession(preservedState);
|
|
255
|
+
|
|
256
|
+
/* CSS pre-update: swap stylesheet BEFORE importing to prevent FOUC */
|
|
257
|
+
if (message.data.cssUrl) {
|
|
258
|
+
swapStylesheet(
|
|
259
|
+
message.data.cssUrl,
|
|
260
|
+
message.data.cssBaseName || '',
|
|
261
|
+
'svelte'
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// O(1) Svelte 5 HMR: import the changed module, then call its
|
|
266
|
+
// accept callback. Svelte's $.hmr() reactive wrapper swaps the
|
|
267
|
+
// component in place — parent state and DOM survive untouched.
|
|
268
|
+
const { pageModuleUrl } = message.data;
|
|
269
|
+
if (pageModuleUrl) {
|
|
270
|
+
const clientStart = performance.now();
|
|
271
|
+
const modulePath = `${pageModuleUrl}?t=${Date.now()}`;
|
|
272
|
+
|
|
273
|
+
const svelteWindow: SvelteHmrWindow = window;
|
|
274
|
+
const acceptRegistry = svelteWindow.__SVELTE_HMR_ACCEPT__;
|
|
275
|
+
|
|
276
|
+
// Save the OLD module's accept callback BEFORE importing.
|
|
277
|
+
const acceptFn = acceptRegistry?.[pageModuleUrl];
|
|
278
|
+
|
|
279
|
+
import(modulePath)
|
|
280
|
+
.then((newModule) => {
|
|
281
|
+
if (acceptFn) {
|
|
282
|
+
acceptFn(newModule);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (
|
|
286
|
+
window.__HMR_WS__ &&
|
|
287
|
+
message.data.serverDuration !== undefined
|
|
288
|
+
) {
|
|
289
|
+
const clientMs = Math.round(
|
|
290
|
+
performance.now() - clientStart
|
|
291
|
+
);
|
|
292
|
+
const total = (message.data.serverDuration ?? 0) + clientMs;
|
|
293
|
+
window.__HMR_WS__.send(
|
|
294
|
+
JSON.stringify({ duration: total, type: 'hmr-timing' })
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return undefined;
|
|
299
|
+
})
|
|
300
|
+
.catch((err: unknown) => {
|
|
301
|
+
console.warn('[HMR] Svelte HMR failed, reloading:', err);
|
|
302
|
+
window.location.reload();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Bundled fallback: re-import the index file
|
|
309
|
+
const indexPath = findIndexPath(
|
|
310
|
+
message.data.manifest,
|
|
311
|
+
message.data.sourceFile,
|
|
312
|
+
'svelte'
|
|
313
|
+
);
|
|
314
|
+
if (!indexPath) {
|
|
315
|
+
console.warn('[HMR] Svelte index path not found, reloading');
|
|
316
|
+
window.location.reload();
|
|
317
|
+
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
preserveAllStylesheets();
|
|
322
|
+
|
|
323
|
+
const modulePath = `${indexPath}?t=${Date.now()}`;
|
|
324
|
+
import(modulePath)
|
|
325
|
+
.then(() => {
|
|
326
|
+
waitForStylesAndCleanup(domState, scrollState);
|
|
327
|
+
|
|
328
|
+
return undefined;
|
|
329
|
+
})
|
|
330
|
+
.catch((err: unknown) => {
|
|
331
|
+
console.warn('[HMR] Svelte import failed, reloading:', err);
|
|
332
|
+
window.location.reload();
|
|
333
|
+
});
|
|
334
|
+
};
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import type {} from '../../../types/globals';
|
|
2
|
+
/* Vue HMR update handler */
|
|
3
|
+
|
|
4
|
+
import type { VueComponentInstance, VueVNode } from '../../../types/vue';
|
|
5
|
+
import { saveDOMState, restoreDOMState } from '../domState';
|
|
6
|
+
import { detectCurrentFramework, findIndexPath } from '../frameworkDetect';
|
|
7
|
+
|
|
8
|
+
/* Collect reactive value from a setup state entry into the target record */
|
|
9
|
+
const collectSetupValue = (
|
|
10
|
+
target: Record<string, unknown>,
|
|
11
|
+
key: string,
|
|
12
|
+
value: unknown
|
|
13
|
+
) => {
|
|
14
|
+
if (value && typeof value === 'object' && 'value' in value) {
|
|
15
|
+
target[key] = value.value;
|
|
16
|
+
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof value !== 'function') {
|
|
21
|
+
target[key] = value;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/* Copy all setup state entries from a record into the target */
|
|
26
|
+
const collectSetupState = (
|
|
27
|
+
target: Record<string, unknown>,
|
|
28
|
+
setupState: Record<string, unknown>
|
|
29
|
+
) => {
|
|
30
|
+
const keys = Object.keys(setupState);
|
|
31
|
+
for (let idx = 0; idx < keys.length; idx++) {
|
|
32
|
+
const key = keys[idx];
|
|
33
|
+
if (key === undefined) continue;
|
|
34
|
+
collectSetupValue(target, key, setupState[key]);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/* Walk a VNode tree and collect setup state from all child components */
|
|
39
|
+
const walkVNode = (
|
|
40
|
+
vnode: VueVNode | undefined,
|
|
41
|
+
state: Record<string, unknown>
|
|
42
|
+
) => {
|
|
43
|
+
if (!vnode) return;
|
|
44
|
+
|
|
45
|
+
if (vnode.component && vnode.component.setupState) {
|
|
46
|
+
collectSetupState(state, vnode.component.setupState);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (vnode.children && Array.isArray(vnode.children)) {
|
|
50
|
+
vnode.children.forEach((child) => {
|
|
51
|
+
walkVNode(child, state);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (vnode.component && vnode.component.subTree) {
|
|
56
|
+
walkVNode(vnode.component.subTree, state);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/* Extract state from child Vue component instances recursively */
|
|
61
|
+
const extractChildComponentState = (
|
|
62
|
+
instance: VueComponentInstance,
|
|
63
|
+
state: Record<string, unknown>
|
|
64
|
+
) => {
|
|
65
|
+
if (!instance || !instance.subTree) return;
|
|
66
|
+
|
|
67
|
+
walkVNode(instance.subTree, state);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/* Find an existing stylesheet link matching the given base name */
|
|
71
|
+
const findMatchingStylesheetLink = (cssBaseName: string) => {
|
|
72
|
+
let found: HTMLLinkElement | null = null;
|
|
73
|
+
document
|
|
74
|
+
.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]')
|
|
75
|
+
.forEach((link) => {
|
|
76
|
+
const href = link.getAttribute('href') ?? '';
|
|
77
|
+
if (cssBaseName && href.includes(cssBaseName)) {
|
|
78
|
+
found = link;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return found;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/* Swap a stylesheet link with a new one, removing the old on load */
|
|
86
|
+
const swapStylesheet = (cssUrl: string, cssBaseName: string) => {
|
|
87
|
+
const existingLink = findMatchingStylesheetLink(cssBaseName);
|
|
88
|
+
if (!existingLink) return;
|
|
89
|
+
|
|
90
|
+
const capturedExisting: HTMLLinkElement = existingLink;
|
|
91
|
+
const newLink = document.createElement('link');
|
|
92
|
+
newLink.rel = 'stylesheet';
|
|
93
|
+
newLink.href = `${cssUrl}?t=${Date.now()}`;
|
|
94
|
+
newLink.onload = function () {
|
|
95
|
+
if (capturedExisting && capturedExisting.parentNode) {
|
|
96
|
+
capturedExisting.remove();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
document.head.appendChild(newLink);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/* Extract Vue reactive state from app instance */
|
|
103
|
+
const extractVueAppState = (vuePreservedState: Record<string, unknown>) => {
|
|
104
|
+
if (!window.__VUE_APP__ || !window.__VUE_APP__._instance) return;
|
|
105
|
+
|
|
106
|
+
const instance = window.__VUE_APP__._instance;
|
|
107
|
+
|
|
108
|
+
if (instance.setupState) {
|
|
109
|
+
collectSetupState(vuePreservedState, instance.setupState);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
extractChildComponentState(instance, vuePreservedState);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/* DOM fallback: extract count from button text when app instance is unavailable */
|
|
116
|
+
const extractCountFromDOM = (vuePreservedState: Record<string, unknown>) => {
|
|
117
|
+
if (Object.keys(vuePreservedState).length > 0) return;
|
|
118
|
+
|
|
119
|
+
const countButton = document.querySelector('button');
|
|
120
|
+
if (!countButton || !countButton.textContent) return;
|
|
121
|
+
|
|
122
|
+
const countMatch = countButton.textContent.match(/count is (\d+)/i);
|
|
123
|
+
if (!countMatch) return;
|
|
124
|
+
|
|
125
|
+
vuePreservedState.initialCount = parseInt(countMatch[1] ?? '0', 10);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/* Handle completion of Vue module reimport */
|
|
129
|
+
const handleVueImportSuccess = (
|
|
130
|
+
vueRoot: HTMLElement | null,
|
|
131
|
+
vueDomState: ReturnType<typeof saveDOMState> | null
|
|
132
|
+
) => {
|
|
133
|
+
if (vueRoot && vueDomState) {
|
|
134
|
+
restoreDOMState(vueRoot, vueDomState);
|
|
135
|
+
}
|
|
136
|
+
sessionStorage.removeItem('__HMR_ACTIVE__');
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/* Force-reload a Vue component via HMR runtime when setup() must re-run */
|
|
140
|
+
const forceReloadVueComponent = (mod: Record<string, unknown>) => {
|
|
141
|
+
const hmrRuntime = window.__VUE_HMR_RUNTIME__;
|
|
142
|
+
if (!hmrRuntime) return;
|
|
143
|
+
|
|
144
|
+
const component = mod?.default ?? Object.values(mod ?? {})[0];
|
|
145
|
+
if (!component || typeof component !== 'object') return;
|
|
146
|
+
if (!('__hmrId' in component)) return;
|
|
147
|
+
|
|
148
|
+
const { __hmrId: hmrId } = component;
|
|
149
|
+
if (typeof hmrId === 'string') {
|
|
150
|
+
hmrRuntime.reload(hmrId, component);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const handleVueUpdate = (message: {
|
|
155
|
+
data: {
|
|
156
|
+
cssBaseName?: string;
|
|
157
|
+
cssUrl?: string;
|
|
158
|
+
forceReload?: boolean;
|
|
159
|
+
html?: string;
|
|
160
|
+
manifest?: Record<string, string>;
|
|
161
|
+
pageModuleUrl?: string;
|
|
162
|
+
serverDuration?: number;
|
|
163
|
+
sourceFile?: string;
|
|
164
|
+
updateType?: string;
|
|
165
|
+
};
|
|
166
|
+
}) => {
|
|
167
|
+
const vueFrameworkCheck = detectCurrentFramework();
|
|
168
|
+
if (vueFrameworkCheck !== 'vue') return;
|
|
169
|
+
|
|
170
|
+
if (message.data.updateType === 'css-only' && message.data.cssUrl) {
|
|
171
|
+
swapStylesheet(message.data.cssUrl, message.data.cssBaseName || '');
|
|
172
|
+
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
sessionStorage.setItem('__HMR_ACTIVE__', 'true');
|
|
177
|
+
|
|
178
|
+
const vueRoot = document.getElementById('root');
|
|
179
|
+
const vueDomState = vueRoot ? saveDOMState(vueRoot) : null;
|
|
180
|
+
|
|
181
|
+
/* Extract Vue reactive state from app instance (not DOM) */
|
|
182
|
+
const vuePreservedState: Record<string, unknown> = {};
|
|
183
|
+
|
|
184
|
+
extractVueAppState(vuePreservedState);
|
|
185
|
+
|
|
186
|
+
/* DOM fallback if app instance not available */
|
|
187
|
+
extractCountFromDOM(vuePreservedState);
|
|
188
|
+
|
|
189
|
+
/* Map count -> initialCount for prop-based state (used by CountButton) */
|
|
190
|
+
if (
|
|
191
|
+
vuePreservedState.count !== undefined &&
|
|
192
|
+
vuePreservedState.initialCount === undefined
|
|
193
|
+
) {
|
|
194
|
+
vuePreservedState.initialCount = vuePreservedState.count;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Backup to sessionStorage for resilience */
|
|
198
|
+
try {
|
|
199
|
+
sessionStorage.setItem(
|
|
200
|
+
'__VUE_HMR_STATE__',
|
|
201
|
+
JSON.stringify(vuePreservedState)
|
|
202
|
+
);
|
|
203
|
+
} catch {
|
|
204
|
+
/* ignore */
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
window.__HMR_PRESERVED_STATE__ = vuePreservedState;
|
|
208
|
+
|
|
209
|
+
// O(1) Vue HMR: import the changed module directly.
|
|
210
|
+
// __VUE_HMR_RUNTIME__.reload() inside the module hot-swaps the
|
|
211
|
+
// component in place — same pattern as React Fast Refresh.
|
|
212
|
+
const { pageModuleUrl } = message.data;
|
|
213
|
+
if (pageModuleUrl) {
|
|
214
|
+
const clientStart = performance.now();
|
|
215
|
+
const modulePath = `${pageModuleUrl}?t=${Date.now()}`;
|
|
216
|
+
|
|
217
|
+
import(modulePath)
|
|
218
|
+
.then((mod) => {
|
|
219
|
+
// When a composable/utility file changed (not the .vue file itself),
|
|
220
|
+
// force reload via __VUE_HMR_RUNTIME__ so setup() re-runs.
|
|
221
|
+
// Vue's rerender only swaps the template, not the setup closure.
|
|
222
|
+
if (message.data.forceReload) {
|
|
223
|
+
forceReloadVueComponent(mod);
|
|
224
|
+
}
|
|
225
|
+
sessionStorage.removeItem('__HMR_ACTIVE__');
|
|
226
|
+
|
|
227
|
+
if (
|
|
228
|
+
window.__HMR_WS__ &&
|
|
229
|
+
message.data.serverDuration !== undefined
|
|
230
|
+
) {
|
|
231
|
+
const clientMs = Math.round(
|
|
232
|
+
performance.now() - clientStart
|
|
233
|
+
);
|
|
234
|
+
const total = (message.data.serverDuration ?? 0) + clientMs;
|
|
235
|
+
window.__HMR_WS__.send(
|
|
236
|
+
JSON.stringify({ duration: total, type: 'hmr-timing' })
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return undefined;
|
|
241
|
+
})
|
|
242
|
+
.catch((err: unknown) => {
|
|
243
|
+
console.warn('[HMR] Vue HMR failed, reloading:', err);
|
|
244
|
+
sessionStorage.removeItem('__HMR_ACTIVE__');
|
|
245
|
+
window.location.reload();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* CSS pre-update: swap stylesheet BEFORE unmounting to prevent FOUC */
|
|
252
|
+
if (message.data.cssUrl) {
|
|
253
|
+
swapStylesheet(message.data.cssUrl, message.data.cssBaseName || '');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* Unmount old Vue app but keep DOM visually intact during async import.
|
|
257
|
+
unmount() clears the container — snapshot and restore synchronously. */
|
|
258
|
+
const savedHTML = vueRoot ? vueRoot.innerHTML : '';
|
|
259
|
+
if (window.__VUE_APP__) {
|
|
260
|
+
window.__VUE_APP__.unmount();
|
|
261
|
+
window.__VUE_APP__ = null;
|
|
262
|
+
}
|
|
263
|
+
if (vueRoot) {
|
|
264
|
+
vueRoot.innerHTML = savedHTML;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Bundled fallback: re-import the index file
|
|
268
|
+
const indexPath = findIndexPath(
|
|
269
|
+
message.data.manifest,
|
|
270
|
+
message.data.sourceFile,
|
|
271
|
+
'vue'
|
|
272
|
+
);
|
|
273
|
+
if (!indexPath) {
|
|
274
|
+
console.warn('[HMR] Vue index path not found, reloading');
|
|
275
|
+
window.location.reload();
|
|
276
|
+
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const modulePath = `${indexPath}?t=${Date.now()}`;
|
|
281
|
+
import(modulePath)
|
|
282
|
+
.then(() => {
|
|
283
|
+
handleVueImportSuccess(vueRoot, vueDomState);
|
|
284
|
+
|
|
285
|
+
return undefined;
|
|
286
|
+
})
|
|
287
|
+
.catch((err: unknown) => {
|
|
288
|
+
console.warn('[HMR] Vue import failed:', err);
|
|
289
|
+
sessionStorage.removeItem('__HMR_ACTIVE__');
|
|
290
|
+
window.location.reload();
|
|
291
|
+
});
|
|
292
|
+
};
|