@useavalon/avalon 0.1.9 → 0.1.10
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/package.json +5 -1
- package/src/client/adapters/index.js +12 -0
- package/src/client/adapters/lit-adapter.js +467 -0
- package/src/client/adapters/preact-adapter.js +223 -0
- package/src/client/adapters/qwik-adapter.js +259 -0
- package/src/client/adapters/react-adapter.js +220 -0
- package/src/client/adapters/solid-adapter.js +295 -0
- package/src/client/adapters/svelte-adapter.js +368 -0
- package/src/client/adapters/vue-adapter.js +278 -0
- package/src/client/components.js +23 -0
- package/src/client/css-hmr-handler.js +263 -0
- package/src/client/framework-adapter.js +283 -0
- package/src/client/hmr-coordinator.js +274 -0
- package/src/client/main.js +8 -8
- package/src/vite-plugin/plugin.ts +17 -15
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter registry for managing framework-specific HMR adapters
|
|
3
|
+
*/
|
|
4
|
+
export class AdapterRegistry {
|
|
5
|
+
adapters = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Register a framework-specific HMR adapter
|
|
8
|
+
*
|
|
9
|
+
* @param framework - Framework name (case-insensitive)
|
|
10
|
+
* @param adapter - The adapter implementation
|
|
11
|
+
* @throws Error if adapter is invalid or already registered
|
|
12
|
+
*
|
|
13
|
+
* Requirements: 2.1-2.7
|
|
14
|
+
*/
|
|
15
|
+
register(framework, adapter) {
|
|
16
|
+
const normalizedName = framework.toLowerCase();
|
|
17
|
+
// Validate adapter
|
|
18
|
+
if (!adapter) {
|
|
19
|
+
throw new Error(`Cannot register null/undefined adapter for framework: ${framework}`);
|
|
20
|
+
}
|
|
21
|
+
if (!adapter.name) {
|
|
22
|
+
throw new Error(`Adapter for framework ${framework} must have a name property`);
|
|
23
|
+
}
|
|
24
|
+
if (typeof adapter.canHandle !== "function") {
|
|
25
|
+
throw new Error(`Adapter for framework ${framework} must implement canHandle method`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof adapter.preserveState !== "function") {
|
|
28
|
+
throw new Error(`Adapter for framework ${framework} must implement preserveState method`);
|
|
29
|
+
}
|
|
30
|
+
if (typeof adapter.update !== "function") {
|
|
31
|
+
throw new Error(`Adapter for framework ${framework} must implement update method`);
|
|
32
|
+
}
|
|
33
|
+
if (typeof adapter.restoreState !== "function") {
|
|
34
|
+
throw new Error(`Adapter for framework ${framework} must implement restoreState method`);
|
|
35
|
+
}
|
|
36
|
+
if (typeof adapter.handleError !== "function") {
|
|
37
|
+
throw new Error(`Adapter for framework ${framework} must implement handleError method`);
|
|
38
|
+
}
|
|
39
|
+
// Check if already registered
|
|
40
|
+
if (this.adapters.has(normalizedName)) {
|
|
41
|
+
console.warn(`Overwriting existing HMR adapter for framework: ${framework}`);
|
|
42
|
+
}
|
|
43
|
+
this.adapters.set(normalizedName, adapter);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get an adapter for a specific framework
|
|
47
|
+
*
|
|
48
|
+
* @param framework - Framework name (case-insensitive)
|
|
49
|
+
* @returns The adapter or undefined if not found
|
|
50
|
+
*/
|
|
51
|
+
get(framework) {
|
|
52
|
+
return this.adapters.get(framework.toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if an adapter is registered for a framework
|
|
56
|
+
*
|
|
57
|
+
* @param framework - Framework name (case-insensitive)
|
|
58
|
+
* @returns true if adapter is registered
|
|
59
|
+
*/
|
|
60
|
+
has(framework) {
|
|
61
|
+
return this.adapters.has(framework.toLowerCase());
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get all registered framework names
|
|
65
|
+
*
|
|
66
|
+
* @returns Array of registered framework names
|
|
67
|
+
*/
|
|
68
|
+
getRegisteredFrameworks() {
|
|
69
|
+
return Array.from(this.adapters.keys());
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Find an adapter that can handle a specific component
|
|
73
|
+
*
|
|
74
|
+
* Iterates through all registered adapters and returns the first one
|
|
75
|
+
* that can handle the component.
|
|
76
|
+
*
|
|
77
|
+
* @param component - The component to check
|
|
78
|
+
* @returns The adapter that can handle the component, or undefined
|
|
79
|
+
*/
|
|
80
|
+
findAdapter(component) {
|
|
81
|
+
for (const adapter of this.adapters.values()) {
|
|
82
|
+
if (adapter.canHandle(component)) {
|
|
83
|
+
return adapter;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Unregister an adapter
|
|
90
|
+
*
|
|
91
|
+
* @param framework - Framework name (case-insensitive)
|
|
92
|
+
* @returns true if adapter was removed, false if not found
|
|
93
|
+
*/
|
|
94
|
+
unregister(framework) {
|
|
95
|
+
const normalizedName = framework.toLowerCase();
|
|
96
|
+
const removed = this.adapters.delete(normalizedName);
|
|
97
|
+
return removed;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Clear all registered adapters
|
|
101
|
+
*/
|
|
102
|
+
clear() {
|
|
103
|
+
this.adapters.clear();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get the number of registered adapters
|
|
107
|
+
*/
|
|
108
|
+
get size() {
|
|
109
|
+
return this.adapters.size;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Base adapter class with common functionality
|
|
114
|
+
*
|
|
115
|
+
* Framework-specific adapters can extend this class to inherit common behavior
|
|
116
|
+
* and only override framework-specific methods.
|
|
117
|
+
*/
|
|
118
|
+
export class BaseFrameworkAdapter {
|
|
119
|
+
/**
|
|
120
|
+
* Default state preservation implementation
|
|
121
|
+
* Captures DOM state (scroll, focus, form values)
|
|
122
|
+
*
|
|
123
|
+
* Subclasses should override to add framework-specific state
|
|
124
|
+
*/
|
|
125
|
+
preserveState(island) {
|
|
126
|
+
try {
|
|
127
|
+
const snapshot = {
|
|
128
|
+
framework: this.name,
|
|
129
|
+
timestamp: Date.now(),
|
|
130
|
+
data: {},
|
|
131
|
+
dom: this.captureDOMState(island)
|
|
132
|
+
};
|
|
133
|
+
return snapshot;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.warn(`Failed to preserve state for ${this.name}:`, error);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Default state restoration implementation
|
|
141
|
+
* Restores DOM state (scroll, focus, form values)
|
|
142
|
+
*
|
|
143
|
+
* Subclasses should override to add framework-specific state restoration
|
|
144
|
+
*/
|
|
145
|
+
restoreState(island, state) {
|
|
146
|
+
try {
|
|
147
|
+
if (state.dom) {
|
|
148
|
+
this.restoreDOMState(island, state.dom);
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.warn(`Failed to restore state for ${this.name}:`, error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Default error handling implementation
|
|
156
|
+
* Shows error indicator and preserves SSR HTML
|
|
157
|
+
*/
|
|
158
|
+
handleError(island, error) {
|
|
159
|
+
console.error(`HMR error in ${this.name} island:`, error);
|
|
160
|
+
// Add error indicator
|
|
161
|
+
const errorIndicator = document.createElement("div");
|
|
162
|
+
errorIndicator.className = "hmr-error-indicator";
|
|
163
|
+
errorIndicator.style.cssText = `
|
|
164
|
+
position: absolute;
|
|
165
|
+
top: 0;
|
|
166
|
+
left: 0;
|
|
167
|
+
right: 0;
|
|
168
|
+
background: #ff4444;
|
|
169
|
+
color: white;
|
|
170
|
+
padding: 8px;
|
|
171
|
+
font-size: 12px;
|
|
172
|
+
font-family: monospace;
|
|
173
|
+
z-index: 10000;
|
|
174
|
+
border-bottom: 2px solid #cc0000;
|
|
175
|
+
`;
|
|
176
|
+
errorIndicator.textContent = `HMR Error: ${error.message}`;
|
|
177
|
+
// Remove existing error indicators
|
|
178
|
+
const existing = island.querySelector(".hmr-error-indicator");
|
|
179
|
+
if (existing) {
|
|
180
|
+
existing.remove();
|
|
181
|
+
}
|
|
182
|
+
island.style.position = "relative";
|
|
183
|
+
island.insertBefore(errorIndicator, island.firstChild);
|
|
184
|
+
// Mark island as having error
|
|
185
|
+
island.setAttribute("data-hmr-error", "true");
|
|
186
|
+
island.setAttribute("data-hmr-error-message", error.message);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Capture DOM state (scroll, focus, form values)
|
|
190
|
+
*/
|
|
191
|
+
captureDOMState(island) {
|
|
192
|
+
const dom = {};
|
|
193
|
+
// Capture scroll position
|
|
194
|
+
const scrollableElements = island.querySelectorAll("[data-preserve-scroll]");
|
|
195
|
+
if (scrollableElements.length > 0 || island.scrollTop > 0 || island.scrollLeft > 0) {
|
|
196
|
+
dom.scrollPosition = {
|
|
197
|
+
x: island.scrollLeft,
|
|
198
|
+
y: island.scrollTop
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// Capture focused element
|
|
202
|
+
const activeElement = document.activeElement;
|
|
203
|
+
if (activeElement && island.contains(activeElement)) {
|
|
204
|
+
const selector = this.getElementSelector(activeElement);
|
|
205
|
+
if (selector) {
|
|
206
|
+
dom.focusedElement = selector;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Capture form values
|
|
210
|
+
const formElements = island.querySelectorAll("input, textarea, select");
|
|
211
|
+
if (formElements.length > 0) {
|
|
212
|
+
dom.formValues = {};
|
|
213
|
+
formElements.forEach((element, index) => {
|
|
214
|
+
const input = element;
|
|
215
|
+
const name = input.name || input.id || `element-${index}`;
|
|
216
|
+
if (input.type === "checkbox" || input.type === "radio") {
|
|
217
|
+
dom.formValues[name] = input.checked;
|
|
218
|
+
} else {
|
|
219
|
+
dom.formValues[name] = input.value;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return dom;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Restore DOM state (scroll, focus, form values)
|
|
227
|
+
*/
|
|
228
|
+
restoreDOMState(island, dom) {
|
|
229
|
+
if (!dom) return;
|
|
230
|
+
// Restore scroll position
|
|
231
|
+
if (dom.scrollPosition) {
|
|
232
|
+
island.scrollLeft = dom.scrollPosition.x;
|
|
233
|
+
island.scrollTop = dom.scrollPosition.y;
|
|
234
|
+
}
|
|
235
|
+
// Restore focused element
|
|
236
|
+
if (dom.focusedElement) {
|
|
237
|
+
try {
|
|
238
|
+
const element = island.querySelector(dom.focusedElement);
|
|
239
|
+
if (element && typeof element.focus === "function") {
|
|
240
|
+
element.focus();
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.warn("Failed to restore focus:", error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Restore form values
|
|
247
|
+
if (dom.formValues) {
|
|
248
|
+
const formElements = island.querySelectorAll("input, textarea, select");
|
|
249
|
+
formElements.forEach((element, index) => {
|
|
250
|
+
const input = element;
|
|
251
|
+
const name = input.name || input.id || `element-${index}`;
|
|
252
|
+
const value = dom.formValues[name];
|
|
253
|
+
if (value !== undefined) {
|
|
254
|
+
if (input.type === "checkbox" || input.type === "radio") {
|
|
255
|
+
input.checked = value;
|
|
256
|
+
} else {
|
|
257
|
+
input.value = value;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get a CSS selector for an element
|
|
265
|
+
*/
|
|
266
|
+
getElementSelector(element) {
|
|
267
|
+
if (element.id) {
|
|
268
|
+
return `#${element.id}`;
|
|
269
|
+
}
|
|
270
|
+
// Check if element has name attribute (for form elements)
|
|
271
|
+
const nameAttr = element.getAttribute("name");
|
|
272
|
+
if (nameAttr) {
|
|
273
|
+
return `[name="${nameAttr}"]`;
|
|
274
|
+
}
|
|
275
|
+
// Fallback to nth-child selector
|
|
276
|
+
const parent = element.parentElement;
|
|
277
|
+
if (parent) {
|
|
278
|
+
const index = Array.from(parent.children).indexOf(element);
|
|
279
|
+
return `${element.tagName.toLowerCase()}:nth-child(${index + 1})`;
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HMR Coordinator
|
|
3
|
+
*
|
|
4
|
+
* Central orchestrator for all HMR operations in Avalon.
|
|
5
|
+
* Integrates with Vite's HMR API and coordinates island updates across frameworks.
|
|
6
|
+
*/
|
|
7
|
+
/// <reference lib="dom" />
|
|
8
|
+
/// <reference lib="dom.iterable" />
|
|
9
|
+
import { AdapterRegistry } from "./framework-adapter.js";
|
|
10
|
+
import { getCSSHMRHandler } from "./css-hmr-handler.js";
|
|
11
|
+
/**
|
|
12
|
+
* HMR Coordinator class
|
|
13
|
+
* Manages HMR lifecycle and coordinates updates across islands
|
|
14
|
+
*/
|
|
15
|
+
export class HMRCoordinator {
|
|
16
|
+
registry = new AdapterRegistry();
|
|
17
|
+
stateSnapshots = new Map();
|
|
18
|
+
updateQueue = new Set();
|
|
19
|
+
isProcessing = false;
|
|
20
|
+
/**
|
|
21
|
+
* Initialize the HMR coordinator
|
|
22
|
+
* Sets up Vite HMR listeners and accepts updates
|
|
23
|
+
*/
|
|
24
|
+
initialize() {
|
|
25
|
+
// @ts-ignore - Vite HMR is available in browser context
|
|
26
|
+
if (!import.meta.hot) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// @ts-ignore - Vite HMR API
|
|
30
|
+
import.meta.hot.accept();
|
|
31
|
+
// @ts-ignore - Vite HMR event types
|
|
32
|
+
import.meta.hot.on("vite:beforeUpdate", (payload) => {
|
|
33
|
+
this.handleUpdate(payload);
|
|
34
|
+
});
|
|
35
|
+
// @ts-ignore - Vite HMR event types
|
|
36
|
+
import.meta.hot.on("vite:beforeFullReload", () => {
|
|
37
|
+
this.handleBeforeFullReload();
|
|
38
|
+
});
|
|
39
|
+
// @ts-ignore - Vite HMR event types
|
|
40
|
+
import.meta.hot.on("vite:error", (payload) => {
|
|
41
|
+
console.error("[HMR] error:", payload);
|
|
42
|
+
this.handleError(payload);
|
|
43
|
+
});
|
|
44
|
+
document.addEventListener("hmr-update-required", (event) => {
|
|
45
|
+
const customEvent = event;
|
|
46
|
+
const { src, reason } = customEvent.detail;
|
|
47
|
+
if (reason === "css-module-update") {
|
|
48
|
+
this.updateQueue.add(this.normalizePath(src));
|
|
49
|
+
if (!this.isProcessing) {
|
|
50
|
+
this.processUpdateQueue().catch((error) => {
|
|
51
|
+
console.error("[HMR] Failed to process CSS module update:", error);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
registerAdapter(framework, adapter) {
|
|
58
|
+
this.registry.register(framework, adapter);
|
|
59
|
+
}
|
|
60
|
+
getRegistry() {
|
|
61
|
+
return this.registry;
|
|
62
|
+
}
|
|
63
|
+
async handleUpdate(payload) {
|
|
64
|
+
if (payload.type !== "update" || !payload.updates) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const cssUpdates = [];
|
|
68
|
+
const jsUpdates = [];
|
|
69
|
+
for (const update of payload.updates) {
|
|
70
|
+
if (update.type === "css-update") {
|
|
71
|
+
cssUpdates.push(update);
|
|
72
|
+
} else {
|
|
73
|
+
jsUpdates.push(update);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
this.processCSSUpdates(cssUpdates);
|
|
77
|
+
this.queueJSUpdates(jsUpdates);
|
|
78
|
+
if (!this.isProcessing && this.updateQueue.size > 0) {
|
|
79
|
+
await this.processUpdateQueue();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
processCSSUpdates(cssUpdates) {
|
|
83
|
+
if (cssUpdates.length === 0) return;
|
|
84
|
+
const cssHandler = getCSSHMRHandler();
|
|
85
|
+
for (const cssUpdate of cssUpdates) {
|
|
86
|
+
try {
|
|
87
|
+
cssHandler.handleCSSUpdate(cssUpdate);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("[HMR] CSS update failed:", error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
queueJSUpdates(jsUpdates) {
|
|
94
|
+
for (const update of jsUpdates) {
|
|
95
|
+
const normalizedPath = this.normalizePath(update.path || update.acceptedPath);
|
|
96
|
+
if (this.isIslandModule(normalizedPath)) {
|
|
97
|
+
this.updateQueue.add(normalizedPath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async processUpdateQueue() {
|
|
102
|
+
if (this.updateQueue.size === 0) return;
|
|
103
|
+
this.isProcessing = true;
|
|
104
|
+
try {
|
|
105
|
+
const paths = Array.from(this.updateQueue);
|
|
106
|
+
this.updateQueue.clear();
|
|
107
|
+
for (const path of paths) {
|
|
108
|
+
const islands = this.findAffectedIslands(path);
|
|
109
|
+
for (const island of islands) {
|
|
110
|
+
try {
|
|
111
|
+
await this.updateIsland(island);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error("[HMR] Failed to update island:", error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
this.isProcessing = false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
findAffectedIslands(modulePath) {
|
|
122
|
+
const islands = [];
|
|
123
|
+
const normalizedPath = this.normalizePath(modulePath);
|
|
124
|
+
const allIslands = document.querySelectorAll("[data-src]");
|
|
125
|
+
for (const island of allIslands) {
|
|
126
|
+
const src = island.dataset.src;
|
|
127
|
+
if (!src) continue;
|
|
128
|
+
const normalizedSrc = this.normalizePath(src);
|
|
129
|
+
const isMatch = normalizedSrc === normalizedPath || normalizedSrc.endsWith(normalizedPath) || normalizedPath.endsWith(normalizedSrc) || normalizedSrc.split("/").pop() === normalizedPath.split("/").pop();
|
|
130
|
+
if (isMatch) {
|
|
131
|
+
islands.push(island);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return islands;
|
|
135
|
+
}
|
|
136
|
+
async updateIsland(island) {
|
|
137
|
+
const framework = island.dataset.framework;
|
|
138
|
+
const src = island.dataset.src;
|
|
139
|
+
const propsAttr = island.dataset.props;
|
|
140
|
+
if (!framework || !src) {
|
|
141
|
+
console.warn("[HMR] Island missing framework or src attribute", island);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const adapter = this.registry.get(framework.toLowerCase());
|
|
145
|
+
if (!adapter) {
|
|
146
|
+
console.warn(`[HMR] No adapter registered for framework: ${framework}`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const props = propsAttr ? JSON.parse(propsAttr) : {};
|
|
151
|
+
const state = adapter.preserveState(island);
|
|
152
|
+
if (state) {
|
|
153
|
+
this.stateSnapshots.set(this.getIslandId(island), state);
|
|
154
|
+
}
|
|
155
|
+
delete island.dataset.hydrated;
|
|
156
|
+
delete island.dataset.hydrationStatus;
|
|
157
|
+
island.querySelector(".hydration-error-indicator, .hmr-error-indicator")?.remove();
|
|
158
|
+
const timestamp = Date.now();
|
|
159
|
+
const freshSrc = src.includes("?") ? `${src}&t=${timestamp}` : `${src}?t=${timestamp}`;
|
|
160
|
+
const componentModule = await import(
|
|
161
|
+
/* @vite-ignore */
|
|
162
|
+
freshSrc
|
|
163
|
+
);
|
|
164
|
+
const Component = this.resolveComponent(componentModule, src);
|
|
165
|
+
await adapter.update(island, Component, props);
|
|
166
|
+
if (state) {
|
|
167
|
+
adapter.restoreState(island, state);
|
|
168
|
+
this.stateSnapshots.delete(this.getIslandId(island));
|
|
169
|
+
}
|
|
170
|
+
island.dataset.hydrated = "true";
|
|
171
|
+
island.dispatchEvent(new CustomEvent("hmr-update", {
|
|
172
|
+
detail: {
|
|
173
|
+
framework,
|
|
174
|
+
src,
|
|
175
|
+
timestamp: Date.now(),
|
|
176
|
+
success: true
|
|
177
|
+
},
|
|
178
|
+
bubbles: true
|
|
179
|
+
}));
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(`[HMR] Failed to update ${framework} island ${src}:`, error);
|
|
182
|
+
adapter.handleError(island, error);
|
|
183
|
+
island.dispatchEvent(new CustomEvent("hmr-error", {
|
|
184
|
+
detail: {
|
|
185
|
+
framework,
|
|
186
|
+
src,
|
|
187
|
+
error: error.message,
|
|
188
|
+
timestamp: Date.now()
|
|
189
|
+
},
|
|
190
|
+
bubbles: true
|
|
191
|
+
}));
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
resolveComponent(componentModule, src) {
|
|
196
|
+
if (componentModule.default) return componentModule.default;
|
|
197
|
+
for (const key of Object.keys(componentModule)) {
|
|
198
|
+
if (key === "default") continue;
|
|
199
|
+
const value = componentModule[key];
|
|
200
|
+
if (typeof value === "function" && value.prototype) {
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
throw new Error(`Component ${src} has no default export`);
|
|
205
|
+
}
|
|
206
|
+
handleBeforeFullReload() {
|
|
207
|
+
const islands = document.querySelectorAll("[data-hydrated=\"true\"]");
|
|
208
|
+
const states = {};
|
|
209
|
+
for (const island of islands) {
|
|
210
|
+
const framework = island.dataset.framework;
|
|
211
|
+
const src = island.dataset.src;
|
|
212
|
+
if (!framework || !src) continue;
|
|
213
|
+
const adapter = this.registry.get(framework.toLowerCase());
|
|
214
|
+
if (!adapter) continue;
|
|
215
|
+
const state = adapter.preserveState(island);
|
|
216
|
+
if (state) {
|
|
217
|
+
states[src] = state;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
sessionStorage.setItem("__avalon_hmr_states__", JSON.stringify(states));
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.warn("[HMR] Failed to save states:", error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
handleError(payload) {
|
|
227
|
+
const error = new Error(payload.err.message);
|
|
228
|
+
error.stack = payload.err.stack;
|
|
229
|
+
console.error("[HMR] Error:", error);
|
|
230
|
+
if (globalThis.window !== undefined) {
|
|
231
|
+
// @ts-ignore - dynamic import of JS file
|
|
232
|
+
import("./hmr-error-overlay.js").then(({ showHMRErrorOverlay }) => {
|
|
233
|
+
showHMRErrorOverlay({
|
|
234
|
+
framework: "unknown",
|
|
235
|
+
src: "unknown",
|
|
236
|
+
error,
|
|
237
|
+
filePath: payload.err.id || payload.err.loc?.file || "unknown",
|
|
238
|
+
line: payload.err.loc?.line,
|
|
239
|
+
column: payload.err.loc?.column
|
|
240
|
+
});
|
|
241
|
+
}).catch(() => {
|
|
242
|
+
console.error("[HMR] Failed to show error overlay");
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
normalizePath(path) {
|
|
247
|
+
return path.replaceAll("\\", "/").replace(/^\//, "").replace(/\?.*$/, "").replace(/#.*$/, "").replace(/^src\//, "");
|
|
248
|
+
}
|
|
249
|
+
isIslandModule(path) {
|
|
250
|
+
return path.includes("/islands/") || path.includes("\\islands\\");
|
|
251
|
+
}
|
|
252
|
+
getIslandId(island) {
|
|
253
|
+
const src = island.dataset.src ?? "";
|
|
254
|
+
const framework = island.dataset.framework ?? "";
|
|
255
|
+
const index = Array.from(document.querySelectorAll(`[data-src="${src}"]`)).indexOf(island);
|
|
256
|
+
return `${framework}:${src}:${index}`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Global HMR coordinator instance
|
|
261
|
+
*/
|
|
262
|
+
let coordinatorInstance = null;
|
|
263
|
+
export function getHMRCoordinator() {
|
|
264
|
+
coordinatorInstance ??= new HMRCoordinator();
|
|
265
|
+
return coordinatorInstance;
|
|
266
|
+
}
|
|
267
|
+
export function initializeHMR() {
|
|
268
|
+
// @ts-ignore - Vite HMR is available in browser context
|
|
269
|
+
if (!import.meta.hot) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const coordinator = getHMRCoordinator();
|
|
273
|
+
coordinator.initialize();
|
|
274
|
+
}
|
package/src/client/main.js
CHANGED
|
@@ -608,7 +608,7 @@ if (import.meta.hot) {
|
|
|
608
608
|
import.meta.hot.accept();
|
|
609
609
|
|
|
610
610
|
// Lazy HMR adapter registration - only load adapters for frameworks used on the page
|
|
611
|
-
import('./hmr-coordinator.
|
|
611
|
+
import('./hmr-coordinator.js')
|
|
612
612
|
.then(async ({ initializeHMR, getHMRCoordinator }) => {
|
|
613
613
|
initializeHMR();
|
|
614
614
|
|
|
@@ -623,13 +623,13 @@ if (import.meta.hot) {
|
|
|
623
623
|
|
|
624
624
|
// Only register adapters for frameworks that are used
|
|
625
625
|
const adapterLoaders = {
|
|
626
|
-
react: () => import('./adapters/react-adapter.
|
|
627
|
-
preact: () => import('./adapters/preact-adapter.
|
|
628
|
-
vue: () => import('./adapters/vue-adapter.
|
|
629
|
-
svelte: () => import('./adapters/svelte-adapter.
|
|
630
|
-
solid: () => import('./adapters/solid-adapter.
|
|
631
|
-
lit: () => import('./adapters/lit-adapter.
|
|
632
|
-
qwik: () => import('./adapters/qwik-adapter.
|
|
626
|
+
react: () => import('./adapters/react-adapter.js').then(m => m.reactAdapter),
|
|
627
|
+
preact: () => import('./adapters/preact-adapter.js').then(m => m.preactAdapter),
|
|
628
|
+
vue: () => import('./adapters/vue-adapter.js').then(m => m.vueAdapter),
|
|
629
|
+
svelte: () => import('./adapters/svelte-adapter.js').then(m => m.svelteAdapter),
|
|
630
|
+
solid: () => import('./adapters/solid-adapter.js').then(m => m.solidAdapter),
|
|
631
|
+
lit: () => import('./adapters/lit-adapter.js').then(m => m.litAdapter),
|
|
632
|
+
qwik: () => import('./adapters/qwik-adapter.js').then(m => m.qwikAdapter),
|
|
633
633
|
};
|
|
634
634
|
|
|
635
635
|
for (const framework of usedFrameworks) {
|
|
@@ -282,20 +282,16 @@ export async function avalon(config?: AvalonPluginConfig): Promise<PluginOption[
|
|
|
282
282
|
enforce: "pre",
|
|
283
283
|
|
|
284
284
|
config() {
|
|
285
|
-
// @useavalon packages ship raw .ts source
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
// strips TypeScript without applying any JSX config.
|
|
285
|
+
// @useavalon packages ship raw .ts source for SSR (handled by our
|
|
286
|
+
// transform hook below) and pre-compiled .js for client-side code.
|
|
287
|
+
// Client .js files are excluded from OXC by default (/\.js$/), so
|
|
288
|
+
// integration plugins' jsx: 'automatic' config doesn't affect them.
|
|
289
|
+
// noExternal ensures Vite processes @useavalon packages through the
|
|
290
|
+
// SSR transform pipeline instead of treating them as external CJS.
|
|
292
291
|
return {
|
|
293
292
|
ssr: {
|
|
294
293
|
noExternal: [/^@useavalon\//],
|
|
295
294
|
},
|
|
296
|
-
oxc: {
|
|
297
|
-
exclude: [/node_modules\/@useavalon\//],
|
|
298
|
-
},
|
|
299
295
|
};
|
|
300
296
|
},
|
|
301
297
|
|
|
@@ -325,11 +321,17 @@ export async function avalon(config?: AvalonPluginConfig): Promise<PluginOption[
|
|
|
325
321
|
},
|
|
326
322
|
|
|
327
323
|
async transform(code: string, id: string) {
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
// plain .ts files. We
|
|
332
|
-
|
|
324
|
+
// For SSR: strip TypeScript from @useavalon packages ourselves.
|
|
325
|
+
// Integration plugins (react, preact) set jsx: 'automatic' which Vite's
|
|
326
|
+
// OXC applies to all files — causing "Invalid jsx option" errors on
|
|
327
|
+
// plain .ts files during SSR. We intercept and strip TS without JSX config.
|
|
328
|
+
// For client-side: main.js imports pre-compiled .js files that OXC
|
|
329
|
+
// skips entirely (default exclude: /\.js$/), avoiding the jsx conflict.
|
|
330
|
+
if (
|
|
331
|
+
this.environment?.config?.consumer === 'server' &&
|
|
332
|
+
id.includes('@useavalon/') &&
|
|
333
|
+
/\.tsx?$/.test(id)
|
|
334
|
+
) {
|
|
333
335
|
const { transform: oxcTransform } = await import('oxc-transform');
|
|
334
336
|
const result = await oxcTransform(id, code, {
|
|
335
337
|
sourcemap: true,
|