@absolutejs/absolute 0.15.0 → 0.15.1
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/dev/client/cssUtils.ts +288 -0
- package/dist/dev/client/domDiff.ts +261 -0
- package/dist/dev/client/domState.ts +271 -0
- package/dist/dev/client/errorOverlay.ts +145 -0
- package/dist/dev/client/frameworkDetect.ts +63 -0
- package/dist/dev/client/handlers/html.ts +415 -0
- package/dist/dev/client/handlers/htmx.ts +248 -0
- package/dist/dev/client/handlers/react.ts +80 -0
- package/dist/dev/client/handlers/rebuild.ts +147 -0
- package/dist/dev/client/handlers/svelte.ts +129 -0
- package/dist/dev/client/handlers/vue.ts +254 -0
- package/dist/dev/client/headPatch.ts +213 -0
- package/dist/dev/client/hmrClient.ts +204 -0
- package/dist/dev/client/moduleVersions.ts +57 -0
- package/dist/dev/client/reactRefreshSetup.ts +21 -0
- package/dist/index.js +44 -23
- package/dist/index.js.map +5 -5
- package/package.json +3 -3
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* React HMR update handler
|
|
2
|
+
Uses React Fast Refresh to hot-swap components while preserving state.
|
|
3
|
+
Code splitting ensures React lives in a shared chunk that stays cached,
|
|
4
|
+
so dynamic import of the rebuilt entry reuses the same React instance. */
|
|
5
|
+
|
|
6
|
+
import { detectCurrentFramework } from '../frameworkDetect';
|
|
7
|
+
|
|
8
|
+
export const handleReactUpdate = (message: {
|
|
9
|
+
data: {
|
|
10
|
+
hasCSSChanges?: boolean;
|
|
11
|
+
hasComponentChanges?: boolean;
|
|
12
|
+
manifest?: Record<string, string>;
|
|
13
|
+
primarySource?: string;
|
|
14
|
+
};
|
|
15
|
+
}) => {
|
|
16
|
+
const currentFramework = detectCurrentFramework();
|
|
17
|
+
if (currentFramework !== 'react') return;
|
|
18
|
+
|
|
19
|
+
const hasComponentChanges = message.data.hasComponentChanges !== false;
|
|
20
|
+
const hasCSSChanges = message.data.hasCSSChanges === true;
|
|
21
|
+
const cssPath =
|
|
22
|
+
message.data.manifest && message.data.manifest.ReactExampleCSS;
|
|
23
|
+
|
|
24
|
+
// CSS-only change: hot-swap the stylesheet link without reloading
|
|
25
|
+
if (!hasComponentChanges && hasCSSChanges && cssPath) {
|
|
26
|
+
reloadReactCSS(cssPath);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Component change: use React Fast Refresh to preserve state
|
|
31
|
+
const win = window as unknown as Record<string, unknown>;
|
|
32
|
+
const componentKey = win.__REACT_COMPONENT_KEY__ as string | undefined;
|
|
33
|
+
const newUrl = componentKey && message.data.manifest?.[componentKey];
|
|
34
|
+
const refreshRuntime = win.$RefreshRuntime$ as
|
|
35
|
+
| { performReactRefresh: () => void }
|
|
36
|
+
| undefined;
|
|
37
|
+
|
|
38
|
+
if (newUrl && refreshRuntime) {
|
|
39
|
+
import(newUrl + '?t=' + Date.now())
|
|
40
|
+
.then(() => {
|
|
41
|
+
refreshRuntime.performReactRefresh();
|
|
42
|
+
})
|
|
43
|
+
.catch((err) => {
|
|
44
|
+
console.warn(
|
|
45
|
+
'[HMR] React Fast Refresh failed, falling back to reload:',
|
|
46
|
+
err
|
|
47
|
+
);
|
|
48
|
+
window.location.reload();
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fallback: full page reload
|
|
54
|
+
window.location.reload();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const reloadReactCSS = (cssPath: string) => {
|
|
58
|
+
const existingCSSLinks = document.head.querySelectorAll(
|
|
59
|
+
'link[rel="stylesheet"]'
|
|
60
|
+
);
|
|
61
|
+
existingCSSLinks.forEach(function (link) {
|
|
62
|
+
const href = (link as HTMLLinkElement).getAttribute('href');
|
|
63
|
+
if (href) {
|
|
64
|
+
const hrefBase = href.split('?')[0]!.split('/').pop() || '';
|
|
65
|
+
const cssPathBase = cssPath.split('?')[0]!.split('/').pop() || '';
|
|
66
|
+
if (
|
|
67
|
+
hrefBase === cssPathBase ||
|
|
68
|
+
href.includes('react-example') ||
|
|
69
|
+
cssPathBase.includes(hrefBase)
|
|
70
|
+
) {
|
|
71
|
+
const newHref =
|
|
72
|
+
cssPath +
|
|
73
|
+
(cssPath.includes('?') ? '&' : '?') +
|
|
74
|
+
't=' +
|
|
75
|
+
Date.now();
|
|
76
|
+
(link as HTMLLinkElement).href = newHref;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/* Rebuild, manifest, module-update, and error handlers */
|
|
2
|
+
|
|
3
|
+
import { hideErrorOverlay, showErrorOverlay } from '../errorOverlay';
|
|
4
|
+
|
|
5
|
+
export const handleManifest = (message: {
|
|
6
|
+
data: {
|
|
7
|
+
manifest?: Record<string, string>;
|
|
8
|
+
serverVersions?: Record<string, number>;
|
|
9
|
+
};
|
|
10
|
+
}) => {
|
|
11
|
+
window.__HMR_MANIFEST__ =
|
|
12
|
+
message.data.manifest ||
|
|
13
|
+
(message.data as unknown as Record<string, string>);
|
|
14
|
+
|
|
15
|
+
if (message.data.serverVersions) {
|
|
16
|
+
window.__HMR_SERVER_VERSIONS__ = message.data.serverVersions;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!window.__HMR_MODULE_VERSIONS__) {
|
|
20
|
+
window.__HMR_MODULE_VERSIONS__ = {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
window.__HMR_MODULE_UPDATES__ = [];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const handleRebuildComplete = (message: {
|
|
27
|
+
data: {
|
|
28
|
+
affectedFrameworks?: string[];
|
|
29
|
+
manifest?: Record<string, string>;
|
|
30
|
+
};
|
|
31
|
+
}) => {
|
|
32
|
+
hideErrorOverlay();
|
|
33
|
+
if (window.__HMR_MANIFEST__) {
|
|
34
|
+
window.__HMR_MANIFEST__ = message.data.manifest;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
message.data.affectedFrameworks &&
|
|
39
|
+
!message.data.affectedFrameworks.includes('react') &&
|
|
40
|
+
!message.data.affectedFrameworks.includes('html') &&
|
|
41
|
+
!message.data.affectedFrameworks.includes('htmx') &&
|
|
42
|
+
!message.data.affectedFrameworks.includes('vue') &&
|
|
43
|
+
!message.data.affectedFrameworks.includes('svelte')
|
|
44
|
+
) {
|
|
45
|
+
const url = new URL(window.location.href);
|
|
46
|
+
url.searchParams.set('_cb', Date.now().toString());
|
|
47
|
+
window.location.href = url.toString();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const handleModuleUpdate = (message: {
|
|
52
|
+
data: {
|
|
53
|
+
framework?: string;
|
|
54
|
+
manifest?: Record<string, string>;
|
|
55
|
+
moduleVersions?: Record<string, number>;
|
|
56
|
+
serverVersions?: Record<string, number>;
|
|
57
|
+
};
|
|
58
|
+
}) => {
|
|
59
|
+
const hasHMRHandler =
|
|
60
|
+
message.data.framework === 'react' ||
|
|
61
|
+
message.data.framework === 'vue' ||
|
|
62
|
+
message.data.framework === 'svelte' ||
|
|
63
|
+
message.data.framework === 'html' ||
|
|
64
|
+
message.data.framework === 'htmx';
|
|
65
|
+
|
|
66
|
+
if (hasHMRHandler) {
|
|
67
|
+
if (message.data.serverVersions) {
|
|
68
|
+
const serverVersions = window.__HMR_SERVER_VERSIONS__ || {};
|
|
69
|
+
for (const key in message.data.serverVersions) {
|
|
70
|
+
if (
|
|
71
|
+
Object.prototype.hasOwnProperty.call(
|
|
72
|
+
message.data.serverVersions,
|
|
73
|
+
key
|
|
74
|
+
)
|
|
75
|
+
) {
|
|
76
|
+
serverVersions[key] = message.data.serverVersions[key]!;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
window.__HMR_SERVER_VERSIONS__ = serverVersions;
|
|
80
|
+
}
|
|
81
|
+
if (message.data.moduleVersions) {
|
|
82
|
+
const moduleVersions = window.__HMR_MODULE_VERSIONS__ || {};
|
|
83
|
+
for (const key in message.data.moduleVersions) {
|
|
84
|
+
if (
|
|
85
|
+
Object.prototype.hasOwnProperty.call(
|
|
86
|
+
message.data.moduleVersions,
|
|
87
|
+
key
|
|
88
|
+
)
|
|
89
|
+
) {
|
|
90
|
+
moduleVersions[key] = message.data.moduleVersions[key]!;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
window.__HMR_MODULE_VERSIONS__ = moduleVersions;
|
|
94
|
+
}
|
|
95
|
+
if (message.data.manifest) {
|
|
96
|
+
const manifest = window.__HMR_MANIFEST__ || {};
|
|
97
|
+
for (const key in message.data.manifest) {
|
|
98
|
+
if (
|
|
99
|
+
Object.prototype.hasOwnProperty.call(
|
|
100
|
+
message.data.manifest,
|
|
101
|
+
key
|
|
102
|
+
)
|
|
103
|
+
) {
|
|
104
|
+
manifest[key] = message.data.manifest[key]!;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
window.__HMR_MANIFEST__ = manifest;
|
|
108
|
+
}
|
|
109
|
+
if (!window.__HMR_MODULE_UPDATES__) {
|
|
110
|
+
window.__HMR_MODULE_UPDATES__ = [];
|
|
111
|
+
}
|
|
112
|
+
window.__HMR_MODULE_UPDATES__.push(message.data);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
window.location.reload();
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const handleRebuildError = (message: {
|
|
120
|
+
data: {
|
|
121
|
+
affectedFrameworks?: string[];
|
|
122
|
+
column?: number;
|
|
123
|
+
error?: string;
|
|
124
|
+
file?: string;
|
|
125
|
+
framework?: string;
|
|
126
|
+
line?: number;
|
|
127
|
+
lineText?: string;
|
|
128
|
+
};
|
|
129
|
+
}) => {
|
|
130
|
+
const errData = message.data || {};
|
|
131
|
+
showErrorOverlay({
|
|
132
|
+
column: errData.column,
|
|
133
|
+
file: errData.file,
|
|
134
|
+
framework:
|
|
135
|
+
errData.framework ||
|
|
136
|
+
(errData.affectedFrameworks && errData.affectedFrameworks[0]),
|
|
137
|
+
line: errData.line,
|
|
138
|
+
lineText: errData.lineText,
|
|
139
|
+
message: errData.error || 'Build failed'
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const handleFullReload = () => {
|
|
144
|
+
setTimeout(function () {
|
|
145
|
+
window.location.reload();
|
|
146
|
+
}, 200);
|
|
147
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* Svelte HMR update handler */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
saveDOMState,
|
|
5
|
+
restoreDOMState,
|
|
6
|
+
saveScrollState,
|
|
7
|
+
restoreScrollState
|
|
8
|
+
} from '../domState';
|
|
9
|
+
import { detectCurrentFramework, findIndexPath } from '../frameworkDetect';
|
|
10
|
+
|
|
11
|
+
/* Swap a stylesheet link by matching cssBaseName or framework name */
|
|
12
|
+
const swapStylesheet = (
|
|
13
|
+
cssUrl: string,
|
|
14
|
+
cssBaseName: string,
|
|
15
|
+
framework: string
|
|
16
|
+
): void => {
|
|
17
|
+
let existingLink: HTMLLinkElement | null = null;
|
|
18
|
+
document
|
|
19
|
+
.querySelectorAll('link[rel="stylesheet"]')
|
|
20
|
+
.forEach(function (link) {
|
|
21
|
+
const href = (link as HTMLLinkElement).getAttribute('href') || '';
|
|
22
|
+
if (href.includes(cssBaseName) || href.includes(framework)) {
|
|
23
|
+
existingLink = link as HTMLLinkElement;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (existingLink) {
|
|
28
|
+
const capturedExisting = existingLink as HTMLLinkElement;
|
|
29
|
+
const newLink = document.createElement('link');
|
|
30
|
+
newLink.rel = 'stylesheet';
|
|
31
|
+
newLink.href = cssUrl + '?t=' + Date.now();
|
|
32
|
+
newLink.onload = function () {
|
|
33
|
+
if (capturedExisting && capturedExisting.parentNode) {
|
|
34
|
+
capturedExisting.remove();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
document.head.appendChild(newLink);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const handleSvelteUpdate = (message: {
|
|
42
|
+
data: {
|
|
43
|
+
cssBaseName?: string;
|
|
44
|
+
cssUrl?: string;
|
|
45
|
+
html?: string;
|
|
46
|
+
manifest?: Record<string, string>;
|
|
47
|
+
sourceFile?: string;
|
|
48
|
+
updateType?: string;
|
|
49
|
+
};
|
|
50
|
+
}) => {
|
|
51
|
+
const svelteFrameworkCheck = detectCurrentFramework();
|
|
52
|
+
if (svelteFrameworkCheck !== 'svelte') return;
|
|
53
|
+
|
|
54
|
+
/* CSS-only update: hot-swap stylesheet, no remount needed */
|
|
55
|
+
if (message.data.updateType === 'css-only' && message.data.cssUrl) {
|
|
56
|
+
console.log('[HMR] Svelte CSS-only update');
|
|
57
|
+
swapStylesheet(
|
|
58
|
+
message.data.cssUrl,
|
|
59
|
+
message.data.cssBaseName || '',
|
|
60
|
+
'svelte'
|
|
61
|
+
);
|
|
62
|
+
console.log('[HMR] Svelte CSS updated');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Component update: preserve state, re-import (bootstrap handles unmount + mount) */
|
|
67
|
+
console.log('[HMR] Svelte update - remounting component');
|
|
68
|
+
|
|
69
|
+
/* Save DOM state and scroll position */
|
|
70
|
+
const domState = saveDOMState(document.body);
|
|
71
|
+
const scrollState = saveScrollState();
|
|
72
|
+
|
|
73
|
+
/* Extract state from DOM (Svelte 5 $state is not externally accessible) */
|
|
74
|
+
const preservedState: Record<string, unknown> = {};
|
|
75
|
+
const countButton = document.querySelector('button');
|
|
76
|
+
if (countButton && countButton.textContent) {
|
|
77
|
+
const countMatch = countButton.textContent.match(/count is (\d+)/i);
|
|
78
|
+
if (countMatch) {
|
|
79
|
+
preservedState.initialCount = parseInt(countMatch[1]!, 10);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Set preserved state on window + backup to sessionStorage */
|
|
84
|
+
window.__HMR_PRESERVED_STATE__ = preservedState;
|
|
85
|
+
try {
|
|
86
|
+
sessionStorage.setItem(
|
|
87
|
+
'__SVELTE_HMR_STATE__',
|
|
88
|
+
JSON.stringify(preservedState)
|
|
89
|
+
);
|
|
90
|
+
} catch (_err) {
|
|
91
|
+
/* ignore */
|
|
92
|
+
}
|
|
93
|
+
console.log(
|
|
94
|
+
'[HMR] Svelte state preserved:',
|
|
95
|
+
JSON.stringify(preservedState)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
/* CSS pre-update: swap stylesheet BEFORE unmounting to prevent FOUC */
|
|
99
|
+
if (message.data.cssUrl) {
|
|
100
|
+
swapStylesheet(
|
|
101
|
+
message.data.cssUrl,
|
|
102
|
+
message.data.cssBaseName || '',
|
|
103
|
+
'svelte'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const indexPath = findIndexPath(
|
|
108
|
+
message.data.manifest,
|
|
109
|
+
message.data.sourceFile,
|
|
110
|
+
'svelte'
|
|
111
|
+
);
|
|
112
|
+
if (!indexPath) {
|
|
113
|
+
console.warn('[HMR] Svelte index path not found, reloading');
|
|
114
|
+
window.location.reload();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const modulePath = indexPath + '?t=' + Date.now();
|
|
119
|
+
import(/* @vite-ignore */ modulePath)
|
|
120
|
+
.then(function () {
|
|
121
|
+
restoreDOMState(document.body, domState);
|
|
122
|
+
restoreScrollState(scrollState);
|
|
123
|
+
console.log('[HMR] Svelte component updated (state preserved)');
|
|
124
|
+
})
|
|
125
|
+
.catch(function (err: unknown) {
|
|
126
|
+
console.warn('[HMR] Svelte import failed, reloading:', err);
|
|
127
|
+
window.location.reload();
|
|
128
|
+
});
|
|
129
|
+
};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/* Vue HMR update handler */
|
|
2
|
+
|
|
3
|
+
import { saveDOMState, restoreDOMState } from '../domState';
|
|
4
|
+
import { detectCurrentFramework, findIndexPath } from '../frameworkDetect';
|
|
5
|
+
|
|
6
|
+
/* Local Vue internal types (avoids importing Vue) */
|
|
7
|
+
interface VueVNode {
|
|
8
|
+
children?: VueVNode[];
|
|
9
|
+
component?: VueComponentInstance;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface VueComponentInstance {
|
|
13
|
+
setupState?: Record<string, unknown>;
|
|
14
|
+
subTree?: VueVNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Extract state from child Vue component instances recursively */
|
|
18
|
+
const extractChildComponentState = (
|
|
19
|
+
instance: VueComponentInstance,
|
|
20
|
+
state: Record<string, unknown>
|
|
21
|
+
): void => {
|
|
22
|
+
if (!instance || !instance.subTree) return;
|
|
23
|
+
|
|
24
|
+
const walkVNode = (vnode: VueVNode | undefined): void => {
|
|
25
|
+
if (!vnode) return;
|
|
26
|
+
|
|
27
|
+
if (vnode.component && vnode.component.setupState) {
|
|
28
|
+
const childState = vnode.component.setupState;
|
|
29
|
+
const keys = Object.keys(childState);
|
|
30
|
+
for (let idx = 0; idx < keys.length; idx++) {
|
|
31
|
+
const key = keys[idx]!;
|
|
32
|
+
const value = childState[key];
|
|
33
|
+
if (
|
|
34
|
+
value &&
|
|
35
|
+
typeof value === 'object' &&
|
|
36
|
+
'value' in (value as Record<string, unknown>)
|
|
37
|
+
) {
|
|
38
|
+
state[key] = (value as { value: unknown }).value;
|
|
39
|
+
} else if (typeof value !== 'function') {
|
|
40
|
+
state[key] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (vnode.children && Array.isArray(vnode.children)) {
|
|
46
|
+
for (let jdx = 0; jdx < vnode.children.length; jdx++) {
|
|
47
|
+
walkVNode(vnode.children[jdx]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (vnode.component && vnode.component.subTree) {
|
|
51
|
+
walkVNode(vnode.component.subTree);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
walkVNode(instance.subTree);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const handleVueUpdate = (message: {
|
|
59
|
+
data: {
|
|
60
|
+
cssBaseName?: string;
|
|
61
|
+
cssUrl?: string;
|
|
62
|
+
html?: string;
|
|
63
|
+
manifest?: Record<string, string>;
|
|
64
|
+
sourceFile?: string;
|
|
65
|
+
updateType?: string;
|
|
66
|
+
};
|
|
67
|
+
}) => {
|
|
68
|
+
const vueFrameworkCheck = detectCurrentFramework();
|
|
69
|
+
if (vueFrameworkCheck !== 'vue') return;
|
|
70
|
+
|
|
71
|
+
if (message.data.updateType === 'css-only' && message.data.cssUrl) {
|
|
72
|
+
console.log('[HMR] Vue CSS-only update (state preserved)');
|
|
73
|
+
const cssBaseName = message.data.cssBaseName || '';
|
|
74
|
+
let existingLink: HTMLLinkElement | null = null;
|
|
75
|
+
document
|
|
76
|
+
.querySelectorAll('link[rel="stylesheet"]')
|
|
77
|
+
.forEach(function (link) {
|
|
78
|
+
const href =
|
|
79
|
+
(link as HTMLLinkElement).getAttribute('href') || '';
|
|
80
|
+
if (href.includes(cssBaseName) || href.includes('vue')) {
|
|
81
|
+
existingLink = link as HTMLLinkElement;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (existingLink) {
|
|
86
|
+
const capturedExisting = existingLink as HTMLLinkElement;
|
|
87
|
+
const newLink = document.createElement('link');
|
|
88
|
+
newLink.rel = 'stylesheet';
|
|
89
|
+
newLink.href = message.data.cssUrl + '?t=' + Date.now();
|
|
90
|
+
newLink.onload = function () {
|
|
91
|
+
if (capturedExisting && capturedExisting.parentNode) {
|
|
92
|
+
capturedExisting.remove();
|
|
93
|
+
}
|
|
94
|
+
console.log('[HMR] Vue CSS updated');
|
|
95
|
+
};
|
|
96
|
+
document.head.appendChild(newLink);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('[HMR] Vue update - remounting component');
|
|
102
|
+
sessionStorage.setItem('__HMR_ACTIVE__', 'true');
|
|
103
|
+
|
|
104
|
+
const vueRoot = document.getElementById('root');
|
|
105
|
+
const vueDomState = vueRoot ? saveDOMState(vueRoot) : null;
|
|
106
|
+
|
|
107
|
+
/* Extract Vue reactive state from app instance (not DOM) */
|
|
108
|
+
const vuePreservedState: Record<string, unknown> = {};
|
|
109
|
+
|
|
110
|
+
if (window.__VUE_APP__ && window.__VUE_APP__._instance) {
|
|
111
|
+
const instance = window.__VUE_APP__._instance;
|
|
112
|
+
|
|
113
|
+
if (instance.setupState) {
|
|
114
|
+
const setupKeys = Object.keys(instance.setupState);
|
|
115
|
+
for (let idx = 0; idx < setupKeys.length; idx++) {
|
|
116
|
+
const key = setupKeys[idx]!;
|
|
117
|
+
const value = instance.setupState[key];
|
|
118
|
+
if (
|
|
119
|
+
value &&
|
|
120
|
+
typeof value === 'object' &&
|
|
121
|
+
'value' in (value as Record<string, unknown>)
|
|
122
|
+
) {
|
|
123
|
+
vuePreservedState[key] = (
|
|
124
|
+
value as { value: unknown }
|
|
125
|
+
).value;
|
|
126
|
+
} else if (typeof value !== 'function') {
|
|
127
|
+
vuePreservedState[key] = value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
extractChildComponentState(
|
|
133
|
+
instance as VueComponentInstance,
|
|
134
|
+
vuePreservedState
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* DOM fallback if app instance not available */
|
|
139
|
+
if (Object.keys(vuePreservedState).length === 0) {
|
|
140
|
+
const countButton = document.querySelector('button');
|
|
141
|
+
if (countButton && countButton.textContent) {
|
|
142
|
+
const countMatch = countButton.textContent.match(/count is (\d+)/i);
|
|
143
|
+
if (countMatch) {
|
|
144
|
+
vuePreservedState.initialCount = parseInt(countMatch[1]!, 10);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Map count -> initialCount for prop-based state (used by CountButton) */
|
|
150
|
+
if (
|
|
151
|
+
vuePreservedState.count !== undefined &&
|
|
152
|
+
vuePreservedState.initialCount === undefined
|
|
153
|
+
) {
|
|
154
|
+
vuePreservedState.initialCount = vuePreservedState.count;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Backup to sessionStorage for resilience */
|
|
158
|
+
try {
|
|
159
|
+
sessionStorage.setItem(
|
|
160
|
+
'__VUE_HMR_STATE__',
|
|
161
|
+
JSON.stringify(vuePreservedState)
|
|
162
|
+
);
|
|
163
|
+
} catch (_err) {
|
|
164
|
+
/* ignore */
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
window.__HMR_PRESERVED_STATE__ = vuePreservedState;
|
|
168
|
+
console.log(
|
|
169
|
+
'[HMR] Vue state preserved:',
|
|
170
|
+
JSON.stringify(vuePreservedState)
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
/* CSS pre-update: swap stylesheet BEFORE unmounting to prevent FOUC */
|
|
174
|
+
if (message.data.cssUrl) {
|
|
175
|
+
const vueCssBaseName = message.data.cssBaseName || '';
|
|
176
|
+
let vueExistingLink: HTMLLinkElement | null = null;
|
|
177
|
+
document
|
|
178
|
+
.querySelectorAll('link[rel="stylesheet"]')
|
|
179
|
+
.forEach(function (link) {
|
|
180
|
+
const href =
|
|
181
|
+
(link as HTMLLinkElement).getAttribute('href') || '';
|
|
182
|
+
if (href.includes(vueCssBaseName) || href.includes('vue')) {
|
|
183
|
+
vueExistingLink = link as HTMLLinkElement;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
if (vueExistingLink) {
|
|
187
|
+
const capturedVueLink = vueExistingLink as HTMLLinkElement;
|
|
188
|
+
const vueCssLink = document.createElement('link');
|
|
189
|
+
vueCssLink.rel = 'stylesheet';
|
|
190
|
+
vueCssLink.href = message.data.cssUrl + '?t=' + Date.now();
|
|
191
|
+
vueCssLink.onload = function () {
|
|
192
|
+
if (capturedVueLink && capturedVueLink.parentNode) {
|
|
193
|
+
capturedVueLink.remove();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
document.head.appendChild(vueCssLink);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Unmount old Vue app */
|
|
201
|
+
if (window.__VUE_APP__) {
|
|
202
|
+
window.__VUE_APP__.unmount();
|
|
203
|
+
window.__VUE_APP__ = null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const newHTML = message.data.html;
|
|
207
|
+
if (!newHTML) {
|
|
208
|
+
window.location.reload();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const tempDiv = document.createElement('div');
|
|
213
|
+
tempDiv.innerHTML = newHTML;
|
|
214
|
+
const newRootDiv = tempDiv.querySelector('#root');
|
|
215
|
+
let innerContent = newRootDiv ? newRootDiv.innerHTML : newHTML;
|
|
216
|
+
|
|
217
|
+
/* Pre-apply preserved state to HTML (prevents flicker showing count=0) */
|
|
218
|
+
if (vuePreservedState.initialCount !== undefined) {
|
|
219
|
+
innerContent = innerContent.replace(
|
|
220
|
+
/count is 0/g,
|
|
221
|
+
'count is ' + vuePreservedState.initialCount
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (vueRoot) {
|
|
226
|
+
vueRoot.innerHTML = innerContent;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const indexPath = findIndexPath(
|
|
230
|
+
message.data.manifest,
|
|
231
|
+
message.data.sourceFile,
|
|
232
|
+
'vue'
|
|
233
|
+
);
|
|
234
|
+
if (!indexPath) {
|
|
235
|
+
console.warn('[HMR] Vue index path not found, reloading');
|
|
236
|
+
window.location.reload();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const modulePath = indexPath + '?t=' + Date.now();
|
|
241
|
+
import(/* @vite-ignore */ modulePath)
|
|
242
|
+
.then(function () {
|
|
243
|
+
if (vueRoot && vueDomState) {
|
|
244
|
+
restoreDOMState(vueRoot, vueDomState);
|
|
245
|
+
}
|
|
246
|
+
sessionStorage.removeItem('__HMR_ACTIVE__');
|
|
247
|
+
console.log('[HMR] Vue updated (state preserved)');
|
|
248
|
+
})
|
|
249
|
+
.catch(function (err: unknown) {
|
|
250
|
+
console.warn('[HMR] Vue import failed:', err);
|
|
251
|
+
sessionStorage.removeItem('__HMR_ACTIVE__');
|
|
252
|
+
window.location.reload();
|
|
253
|
+
});
|
|
254
|
+
};
|