@absolutejs/absolute 0.19.0-beta.706 → 0.19.0-beta.707

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/angular/components/constants.js +78 -0
  2. package/dist/angular/components/core/streamingSlotRegistrar.js +58 -0
  3. package/dist/angular/components/core/streamingSlotRegistry.js +114 -0
  4. package/dist/angular/components/defer-slot-payload.js +6 -0
  5. package/dist/angular/components/defer-slot-templates.directive.js +44 -0
  6. package/dist/angular/components/defer-slot.component.js +149 -0
  7. package/dist/angular/components/image.component.js +202 -0
  8. package/dist/angular/components/index.js +4 -0
  9. package/dist/angular/components/stream-slot.component.js +103 -0
  10. package/dist/angular/index.js +7 -9
  11. package/dist/angular/index.js.map +4 -4
  12. package/dist/angular/server.js +7 -9
  13. package/dist/angular/server.js.map +4 -4
  14. package/dist/build.js +19 -26
  15. package/dist/build.js.map +4 -4
  16. package/dist/cli/index.js +197 -144
  17. package/dist/client/index.js +2 -4
  18. package/dist/client/index.js.map +2 -2
  19. package/dist/dev/client/constants.ts +26 -0
  20. package/dist/dev/client/cssUtils.ts +307 -0
  21. package/dist/dev/client/domDiff.ts +226 -0
  22. package/dist/dev/client/domState.ts +421 -0
  23. package/dist/dev/client/domTracker.ts +61 -0
  24. package/dist/dev/client/errorOverlay.ts +184 -0
  25. package/dist/dev/client/frameworkDetect.ts +63 -0
  26. package/dist/dev/client/handlers/angular.ts +572 -0
  27. package/dist/dev/client/handlers/angularRuntime.ts +226 -0
  28. package/dist/dev/client/handlers/html.ts +364 -0
  29. package/dist/dev/client/handlers/htmx.ts +278 -0
  30. package/dist/dev/client/handlers/react.ts +108 -0
  31. package/dist/dev/client/handlers/rebuild.ts +153 -0
  32. package/dist/dev/client/handlers/svelte.ts +334 -0
  33. package/dist/dev/client/handlers/vue.ts +292 -0
  34. package/dist/dev/client/headPatch.ts +233 -0
  35. package/dist/dev/client/hmrClient.ts +273 -0
  36. package/dist/dev/client/hmrState.ts +14 -0
  37. package/dist/dev/client/moduleVersions.ts +62 -0
  38. package/dist/dev/client/reactRefreshSetup.ts +32 -0
  39. package/dist/index.js +29 -33
  40. package/dist/index.js.map +5 -5
  41. package/dist/islands/index.js +6 -8
  42. package/dist/islands/index.js.map +4 -4
  43. package/dist/react/browser.js +7 -7
  44. package/dist/react/browser.js.map +2 -2
  45. package/dist/react/components/browser/index.js +101 -101
  46. package/dist/react/components/index.js +104 -104
  47. package/dist/react/components/index.js.map +2 -2
  48. package/dist/react/index.js +17 -19
  49. package/dist/react/index.js.map +4 -4
  50. package/dist/react/server.js +3 -3
  51. package/dist/react/server.js.map +3 -3
  52. package/dist/src/angular/components/constants.d.ts +75 -0
  53. package/dist/src/angular/components/defer-slot-templates.directive.d.ts +7 -0
  54. package/dist/src/angular/components/defer-slot.component.d.ts +5 -2
  55. package/dist/src/angular/components/image.component.d.ts +5 -2
  56. package/dist/src/angular/components/index.d.ts +4 -4
  57. package/dist/src/angular/components/stream-slot.component.d.ts +3 -0
  58. package/dist/src/constants.d.ts +1 -0
  59. package/dist/svelte/components/AwaitSlot.svelte +39 -0
  60. package/dist/svelte/components/AwaitSlot.svelte.d.ts +2 -0
  61. package/dist/svelte/components/Head.svelte +144 -0
  62. package/dist/svelte/components/Head.svelte.d.ts +2 -0
  63. package/dist/svelte/components/Image.svelte +164 -0
  64. package/dist/svelte/components/Image.svelte.d.ts +5 -0
  65. package/dist/svelte/components/Island.svelte +71 -0
  66. package/dist/svelte/components/Island.svelte.d.ts +5 -0
  67. package/dist/svelte/components/JsonLd.svelte +21 -0
  68. package/dist/svelte/components/JsonLd.svelte.d.ts +2 -0
  69. package/dist/svelte/components/StreamSlot.svelte +41 -0
  70. package/dist/svelte/components/StreamSlot.svelte.d.ts +2 -0
  71. package/dist/svelte/index.js +7 -9
  72. package/dist/svelte/index.js.map +4 -4
  73. package/dist/svelte/server.js +4 -4
  74. package/dist/svelte/server.js.map +3 -3
  75. package/dist/types/globals.d.ts +122 -0
  76. package/dist/vue/index.js +7 -9
  77. package/dist/vue/index.js.map +4 -4
  78. package/dist/vue/server.js +3 -3
  79. package/dist/vue/server.js.map +3 -3
  80. 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
+ };