@useavalon/avalon 0.1.9 → 0.1.11

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.
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Vue HMR Adapter
3
+ *
4
+ * Provides Hot Module Replacement support for Vue 3 components.
5
+ * Integrates with @vitejs/plugin-vue to preserve reactive state during updates.
6
+ * Uses Vue's __VUE_HMR_RUNTIME__ API for hot updates.
7
+ *
8
+ * Requirements: 2.3
9
+ */
10
+ /// <reference lib="dom" />
11
+ import { BaseFrameworkAdapter } from "../framework-adapter.js";
12
+ /**
13
+ * Vue HMR Adapter
14
+ *
15
+ * Leverages Vue's HMR capabilities (provided by @vitejs/plugin-vue) to:
16
+ * - Preserve reactive state across updates
17
+ * - Maintain computed properties
18
+ * - Handle component updates without full remount
19
+ *
20
+ * Vue HMR works through the __VUE_HMR_RUNTIME__ API:
21
+ * 1. When a component module is updated, Vite sends an HMR event
22
+ * 2. We use __VUE_HMR_RUNTIME__.reload() to hot-reload the component
23
+ * 3. Vue preserves reactive state automatically through its reactivity system
24
+ * 4. The component re-renders with preserved state
25
+ */
26
+ export class VueHMRAdapter extends BaseFrameworkAdapter {
27
+ name = "vue";
28
+ /**
29
+ * Store Vue app instances for each island to enable proper updates
30
+ */
31
+ apps = new WeakMap();
32
+ /**
33
+ * Store component IDs for HMR runtime
34
+ */
35
+ componentIds = new WeakMap();
36
+ /**
37
+ * Check if a component is a Vue component
38
+ *
39
+ * Vue components can be:
40
+ * - Component options objects (with setup, data, render, template, etc.)
41
+ * - Setup functions (Composition API)
42
+ * - SFC compiled components
43
+ */
44
+ canHandle(component) {
45
+ if (!component) return false;
46
+ // Check if it's a function (setup function or render function)
47
+ if (typeof component === "function") {
48
+ // Vue setup functions or render functions
49
+ return true;
50
+ }
51
+ // Check if it's a component options object
52
+ if (typeof component !== "object") {
53
+ return false;
54
+ }
55
+ const obj = component;
56
+ // Check for Vue-specific properties
57
+ const hasVueProperties = "setup" in obj || "data" in obj || "render" in obj || "template" in obj || "props" in obj || "computed" in obj || "methods" in obj || "components" in obj || "emits" in obj || "mounted" in obj || "created" in obj || "beforeMount" in obj || "beforeCreate" in obj;
58
+ if (hasVueProperties) {
59
+ return true;
60
+ }
61
+ // Check for __vccOpts (Vue SFC compiled component marker)
62
+ if ("__vccOpts" in obj) {
63
+ return true;
64
+ }
65
+ return false;
66
+ }
67
+ /**
68
+ * Preserve Vue component state before HMR update
69
+ *
70
+ * Vue's reactivity system handles most state preservation automatically.
71
+ * We capture additional DOM state and props for fallback.
72
+ */
73
+ preserveState(island) {
74
+ try {
75
+ // Get base DOM state
76
+ const baseSnapshot = super.preserveState(island);
77
+ if (!baseSnapshot) return null;
78
+ // Get Vue-specific data
79
+ const propsAttr = island.getAttribute("data-props");
80
+ const capturedProps = propsAttr ? JSON.parse(propsAttr) : {};
81
+ // Try to get component name from the island
82
+ const src = island.getAttribute("data-src") || "";
83
+ const componentName = this.extractComponentName(src);
84
+ // Try to capture reactive data from the Vue instance
85
+ // Note: This is best-effort, as Vue's internal state is not easily accessible
86
+ // Vue's HMR runtime will handle most state preservation automatically
87
+ const reactiveData = this.captureReactiveData(island);
88
+ const vueSnapshot = {
89
+ ...baseSnapshot,
90
+ framework: "vue",
91
+ data: {
92
+ componentName,
93
+ capturedProps,
94
+ reactiveData
95
+ }
96
+ };
97
+ return vueSnapshot;
98
+ } catch (error) {
99
+ console.warn("Failed to preserve Vue state:", error);
100
+ return null;
101
+ }
102
+ }
103
+ /**
104
+ * Update Vue component with HMR
105
+ *
106
+ * This method integrates with Vue's HMR API:
107
+ * 1. Vue HMR is automatically enabled by @vitejs/plugin-vue
108
+ * 2. When a module updates, Vite sends an HMR event
109
+ * 3. We use __VUE_HMR_RUNTIME__ to reload the component
110
+ * 4. Vue preserves reactive state automatically
111
+ * 5. The component re-renders with preserved state
112
+ */
113
+ async update(island, newComponent, props) {
114
+ if (!this.canHandle(newComponent)) {
115
+ throw new Error("Component is not a valid Vue component");
116
+ }
117
+ const Component = newComponent;
118
+ try {
119
+ // Dynamically import Vue at runtime
120
+ // This is resolved by Vite in the browser
121
+ const vueModule = await import("vue");
122
+ const { createApp } = vueModule;
123
+ // Check if we have an existing app
124
+ const existingApp = this.apps.get(island);
125
+ const componentId = this.componentIds.get(island);
126
+ // Try to use Vue HMR runtime if available
127
+ const hmrRuntime = globalThis.__VUE_HMR_RUNTIME__;
128
+ if (hmrRuntime && componentId) {
129
+ // Use Vue's HMR runtime for hot reload
130
+ // This preserves reactive state automatically
131
+ try {
132
+ hmrRuntime.reload(componentId, Component);
133
+ // If we have an existing app, we're done
134
+ // Vue HMR runtime handles the update
135
+ if (existingApp) {
136
+ return;
137
+ }
138
+ } catch (error) {
139
+ console.warn("Vue HMR runtime reload failed, falling back to full remount:", error);
140
+ }
141
+ }
142
+ if (existingApp) {
143
+ // Unmount existing app
144
+ try {
145
+ existingApp.unmount();
146
+ } catch (error) {
147
+ console.warn("Failed to unmount existing Vue app:", error);
148
+ }
149
+ }
150
+ // Create new app and mount
151
+ const app = createApp(Component, props);
152
+ // Configure error handling
153
+ app.config.errorHandler = (err, _instance, info) => {
154
+ console.error("Vue component error during HMR:", err, info);
155
+ };
156
+ // Mount with hydration
157
+ app.mount(island, true);
158
+ // Store app and component ID for future updates
159
+ this.apps.set(island, app);
160
+ // Generate component ID for HMR runtime
161
+ const src = island.getAttribute("data-src") || "";
162
+ const newComponentId = this.generateComponentId(src);
163
+ this.componentIds.set(island, newComponentId);
164
+ // Register with HMR runtime if available
165
+ if (hmrRuntime) {
166
+ hmrRuntime.createRecord(newComponentId, Component);
167
+ }
168
+ // Mark as hydrated
169
+ island.setAttribute("data-hydrated", "true");
170
+ island.setAttribute("data-hydration-status", "success");
171
+ } catch (error) {
172
+ console.error("Vue HMR update failed:", error);
173
+ island.setAttribute("data-hydration-status", "error");
174
+ throw error;
175
+ }
176
+ }
177
+ /**
178
+ * Restore Vue component state after HMR update
179
+ *
180
+ * Vue's reactivity system handles most state restoration automatically.
181
+ * We restore DOM state (scroll, focus, form values) as a supplement.
182
+ */
183
+ restoreState(island, state) {
184
+ try {
185
+ // Restore DOM state (scroll, focus, form values)
186
+ super.restoreState(island, state);
187
+ } catch (error) {
188
+ console.warn("Failed to restore Vue state:", error);
189
+ }
190
+ }
191
+ /**
192
+ * Handle errors during Vue HMR update
193
+ *
194
+ * Provides Vue-specific error handling with helpful messages
195
+ */
196
+ handleError(island, error) {
197
+ console.error("Vue HMR error:", error);
198
+ // Use base error handling
199
+ super.handleError(island, error);
200
+ // Add Vue-specific error information
201
+ const errorIndicator = island.querySelector(".hmr-error-indicator");
202
+ if (errorIndicator) {
203
+ const errorMessage = error.message;
204
+ // Provide helpful hints for common Vue errors
205
+ let hint = "";
206
+ if (errorMessage.includes("reactive") || errorMessage.includes("ref")) {
207
+ hint = " (Hint: Check reactive state usage - refs must be accessed with .value)";
208
+ } else if (errorMessage.includes("render")) {
209
+ hint = " (Hint: Check component render function or template for errors)";
210
+ } else if (errorMessage.includes("hydration") || errorMessage.includes("mismatch")) {
211
+ hint = " (Hint: Server and client render must match)";
212
+ } else if (errorMessage.includes("setup")) {
213
+ hint = " (Hint: Check setup function - it should return render function or object)";
214
+ }
215
+ errorIndicator.textContent = `Vue HMR Error: ${errorMessage}${hint}`;
216
+ }
217
+ }
218
+ /**
219
+ * Extract component name from source path
220
+ * Used for debugging and error messages
221
+ */
222
+ extractComponentName(src) {
223
+ const parts = src.split("/");
224
+ const filename = parts[parts.length - 1];
225
+ return filename.replace(/\.(vue|tsx?|jsx?)$/, "");
226
+ }
227
+ /**
228
+ * Generate a unique component ID for HMR runtime
229
+ */
230
+ generateComponentId(src) {
231
+ // Use the source path as the component ID
232
+ // This ensures consistency across HMR updates
233
+ return src.replace(/[^a-zA-Z0-9]/g, "_");
234
+ }
235
+ /**
236
+ * Attempt to capture reactive data from Vue instance
237
+ * This is best-effort and may not work in all cases
238
+ */
239
+ captureReactiveData(island) {
240
+ try {
241
+ // Vue 3 stores instance data on the element's __vueParentComponent
242
+ // This is internal API and may change, so we wrap in try-catch
243
+ const vueInstance = island.__vueParentComponent;
244
+ if (vueInstance && typeof vueInstance === "object") {
245
+ // Try to extract data from the instance
246
+ // This is very fragile and depends on Vue internals
247
+ const data = vueInstance.data;
248
+ if (data && typeof data === "object") {
249
+ return { ...data };
250
+ }
251
+ }
252
+ } catch (error) {
253
+ // Silently fail - this is best-effort
254
+ console.debug("Could not capture Vue reactive data:", error);
255
+ }
256
+ return undefined;
257
+ }
258
+ /**
259
+ * Clean up Vue app when island is removed
260
+ * This should be called when an island is unmounted
261
+ */
262
+ unmount(island) {
263
+ const app = this.apps.get(island);
264
+ if (app) {
265
+ try {
266
+ app.unmount();
267
+ this.apps.delete(island);
268
+ this.componentIds.delete(island);
269
+ } catch (error) {
270
+ console.warn("Failed to unmount Vue app:", error);
271
+ }
272
+ }
273
+ }
274
+ }
275
+ /**
276
+ * Create and export a singleton instance of the Vue HMR adapter
277
+ */
278
+ export const vueAdapter = new VueHMRAdapter();
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Client-safe component exports for use in island components.
3
+ *
4
+ * Import from '@useavalon/avalon/client' instead of '@useavalon/avalon' when
5
+ * you need framework components inside islands. The main entry point
6
+ * re-exports server-only code (nitro, h3, vite plugins) that can't
7
+ * be bundled for the browser.
8
+ */
9
+ // Persistent islands
10
+ export { PersistentIsland } from "../components/PersistentIsland.tsx";
11
+ export { usePersistentIslandContext, PersistentIslandProvider, createPersistentIslandContext } from "../core/islands/persistent-island-context.tsx";
12
+ export { usePersistentState } from "../core/islands/use-persistent-state.js";
13
+ export { IslandPersistence, defaultIslandPersistence } from "../core/islands/island-persistence.js";
14
+ export { IslandStateSerializer } from "../core/islands/island-state-serializer.js";
15
+ // Error boundaries
16
+ export { IslandErrorBoundary, withIslandErrorBoundary } from "../components/IslandErrorBoundary.tsx";
17
+ export { LayoutErrorBoundary } from "../components/LayoutErrorBoundary.tsx";
18
+ export { LayoutDataErrorBoundary } from "../components/LayoutDataErrorBoundary.tsx";
19
+ export { StreamingErrorBoundary, withStreamingErrorBoundary } from "../components/StreamingErrorBoundary.tsx";
20
+ // Streaming
21
+ export { StreamingLayout, StreamingSuspense, withStreaming, useStreamingState } from "../components/StreamingLayout.tsx";
22
+ // Image optimization
23
+ export { Image } from "../components/Image.tsx";
@@ -0,0 +1,263 @@
1
+ /**
2
+ * CSS HMR Handler class
3
+ * Manages CSS hot updates without page reload
4
+ */
5
+ export class CSSHMRHandler {
6
+ cssModuleMap = new Map();
7
+ styleElementMap = new Map();
8
+ /**
9
+ * Handle CSS update from Vite HMR
10
+ */
11
+ handleCSSUpdate(update) {
12
+ const updateInfo = this.classifyCSSUpdate(update);
13
+ switch (updateInfo.type) {
14
+ case "global":
15
+ this.handleGlobalCSSUpdate(updateInfo);
16
+ break;
17
+ case "module":
18
+ this.handleCSSModuleUpdate(updateInfo);
19
+ break;
20
+ case "scoped":
21
+ this.handleScopedCSSUpdate(updateInfo);
22
+ break;
23
+ }
24
+ }
25
+ /**
26
+ * Classify the type of CSS update
27
+ */
28
+ classifyCSSUpdate(update) {
29
+ const path = update.path || update.acceptedPath;
30
+ // Check if it's a CSS module
31
+ if (path.includes(".module.css") || path.includes(".module.scss")) {
32
+ return {
33
+ type: "module",
34
+ path,
35
+ timestamp: update.timestamp
36
+ };
37
+ }
38
+ // Check if it's a scoped style (Svelte or Vue)
39
+ if (path.includes(".svelte") || path.includes(".vue")) {
40
+ return {
41
+ type: "scoped",
42
+ path,
43
+ timestamp: update.timestamp
44
+ };
45
+ }
46
+ // Default to global CSS
47
+ return {
48
+ type: "global",
49
+ path,
50
+ timestamp: update.timestamp
51
+ };
52
+ }
53
+ /**
54
+ * Handle global CSS file update
55
+ * Injects updated styles without page reload
56
+ */
57
+ handleGlobalCSSUpdate(info) {
58
+ try {
59
+ // Find existing style element for this CSS file
60
+ const existingStyle = this.styleElementMap.get(info.path);
61
+ if (existingStyle) {
62
+ // Remove old style element
63
+ existingStyle.remove();
64
+ this.styleElementMap.delete(info.path);
65
+ }
66
+ // Vite automatically injects the updated CSS via its HMR mechanism
67
+ // We just need to ensure old styles are removed
68
+ // The new styles will be injected by Vite's CSS handling
69
+ // Find the newly injected style element
70
+ // Vite adds a data-vite-dev-id attribute to style elements
71
+ const newStyle = document.querySelector(`style[data-vite-dev-id*="${this.getFileId(info.path)}"]`);
72
+ if (newStyle) {
73
+ this.styleElementMap.set(info.path, newStyle);
74
+ }
75
+ // Dispatch event for feedback
76
+ this.dispatchCSSUpdateEvent(info, true);
77
+ } catch (error) {
78
+ console.error(`Failed to update global CSS: ${info.path}`, error);
79
+ this.dispatchCSSUpdateEvent(info, false, error);
80
+ throw error;
81
+ }
82
+ }
83
+ /**
84
+ * Handle CSS module update
85
+ * Re-renders components using the updated CSS module
86
+ */
87
+ handleCSSModuleUpdate(info) {
88
+ try {
89
+ const affectedIslands = this.findIslandsUsingCSSModule(info.path);
90
+ if (affectedIslands.length === 0) {
91
+ return;
92
+ }
93
+ for (const island of affectedIslands) {
94
+ this.triggerIslandRerender(island, info);
95
+ }
96
+ this.dispatchCSSUpdateEvent(info, true);
97
+ } catch (error) {
98
+ console.error(`[HMR] Failed to update CSS module: ${info.path}`, error);
99
+ this.dispatchCSSUpdateEvent(info, false, error);
100
+ throw error;
101
+ }
102
+ }
103
+ /**
104
+ * Handle scoped CSS update (Svelte/Vue)
105
+ * Updates only the affected component's styles
106
+ */
107
+ handleScopedCSSUpdate(info) {
108
+ try {
109
+ const affectedIslands = this.findIslandsUsingComponent(info.path);
110
+ if (affectedIslands.length === 0) {
111
+ return;
112
+ }
113
+ this.dispatchCSSUpdateEvent(info, true);
114
+ } catch (error) {
115
+ console.error(`[HMR] Failed to update scoped CSS: ${info.path}`, error);
116
+ this.dispatchCSSUpdateEvent(info, false, error);
117
+ throw error;
118
+ }
119
+ }
120
+ /**
121
+ * Find islands that use a specific CSS module
122
+ */
123
+ findIslandsUsingCSSModule(cssPath) {
124
+ const islands = [];
125
+ const normalizedPath = this.normalizePath(cssPath);
126
+ // Check cached mapping first
127
+ const cached = this.cssModuleMap.get(normalizedPath);
128
+ if (cached) {
129
+ return Array.from(cached);
130
+ }
131
+ // Find all islands
132
+ const allIslands = document.querySelectorAll("[data-src]");
133
+ for (const island of allIslands) {
134
+ const src = island.getAttribute("data-src");
135
+ if (!src) continue;
136
+ // Check if the island's component likely imports this CSS module
137
+ // This is a heuristic - we look for islands in the same directory
138
+ const componentDir = this.getDirectory(src);
139
+ const cssDir = this.getDirectory(cssPath);
140
+ if (componentDir === cssDir) {
141
+ islands.push(island);
142
+ }
143
+ }
144
+ // Cache the mapping
145
+ if (islands.length > 0) {
146
+ this.cssModuleMap.set(normalizedPath, new Set(islands));
147
+ }
148
+ return islands;
149
+ }
150
+ /**
151
+ * Find islands using a specific component file
152
+ */
153
+ findIslandsUsingComponent(componentPath) {
154
+ const islands = [];
155
+ const normalizedPath = this.normalizePath(componentPath);
156
+ const allIslands = document.querySelectorAll("[data-src]");
157
+ for (const island of allIslands) {
158
+ const src = island.getAttribute("data-src");
159
+ if (!src) continue;
160
+ const normalizedSrc = this.normalizePath(src);
161
+ if (normalizedSrc === normalizedPath || normalizedSrc.includes(normalizedPath)) {
162
+ islands.push(island);
163
+ }
164
+ }
165
+ return islands;
166
+ }
167
+ /**
168
+ * Trigger re-render of an island to apply new CSS module classes
169
+ */
170
+ triggerIslandRerender(island, info) {
171
+ // Dispatch event to trigger island re-render
172
+ // The HMR coordinator will handle the actual re-render
173
+ island.dispatchEvent(new CustomEvent("css-module-update", {
174
+ detail: {
175
+ cssPath: info.path,
176
+ timestamp: info.timestamp
177
+ },
178
+ bubbles: true
179
+ }));
180
+ // Also trigger a standard HMR update event
181
+ // This will be picked up by the HMR coordinator
182
+ const src = island.getAttribute("data-src");
183
+ if (src) {
184
+ island.dispatchEvent(new CustomEvent("hmr-update-required", {
185
+ detail: {
186
+ src,
187
+ reason: "css-module-update",
188
+ cssPath: info.path
189
+ },
190
+ bubbles: true
191
+ }));
192
+ }
193
+ }
194
+ /**
195
+ * Dispatch CSS update event for feedback
196
+ */
197
+ dispatchCSSUpdateEvent(info, success, error) {
198
+ const event = new CustomEvent("css-hmr-update", {
199
+ detail: {
200
+ type: info.type,
201
+ path: info.path,
202
+ timestamp: info.timestamp,
203
+ success,
204
+ error: error?.message
205
+ },
206
+ bubbles: true
207
+ });
208
+ document.dispatchEvent(event);
209
+ }
210
+ /**
211
+ * Normalize a file path for comparison
212
+ */
213
+ normalizePath(path) {
214
+ return path.replace(/\\/g, "/").replace(/^\//, "").replace(/\?.*$/, "").replace(/#.*$/, "");
215
+ }
216
+ /**
217
+ * Get directory from a file path
218
+ */
219
+ getDirectory(path) {
220
+ const normalized = this.normalizePath(path);
221
+ const lastSlash = normalized.lastIndexOf("/");
222
+ return lastSlash >= 0 ? normalized.substring(0, lastSlash) : "";
223
+ }
224
+ /**
225
+ * Get file ID from path (for Vite's data-vite-dev-id)
226
+ */
227
+ getFileId(path) {
228
+ const normalized = this.normalizePath(path);
229
+ // Vite uses the file path as the ID
230
+ return normalized;
231
+ }
232
+ /**
233
+ * Clear cached mappings
234
+ */
235
+ clearCache() {
236
+ this.cssModuleMap.clear();
237
+ this.styleElementMap.clear();
238
+ }
239
+ /**
240
+ * Register an island as using a CSS module
241
+ * Useful for explicit tracking
242
+ */
243
+ registerCSSModuleUsage(cssPath, island) {
244
+ const normalized = this.normalizePath(cssPath);
245
+ if (!this.cssModuleMap.has(normalized)) {
246
+ this.cssModuleMap.set(normalized, new Set());
247
+ }
248
+ this.cssModuleMap.get(normalized).add(island);
249
+ }
250
+ }
251
+ /**
252
+ * Global CSS HMR handler instance
253
+ */
254
+ let cssHandlerInstance = null;
255
+ /**
256
+ * Get or create the global CSS HMR handler instance
257
+ */
258
+ export function getCSSHMRHandler() {
259
+ if (!cssHandlerInstance) {
260
+ cssHandlerInstance = new CSSHMRHandler();
261
+ }
262
+ return cssHandlerInstance;
263
+ }