@useavalon/avalon 0.1.11 → 0.1.13
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/README.md +54 -54
- package/mod.ts +302 -302
- package/package.json +49 -26
- package/src/build/integration-bundler-plugin.ts +116 -116
- package/src/build/integration-config.ts +168 -168
- package/src/build/integration-detection-plugin.ts +117 -117
- package/src/build/integration-resolver-plugin.ts +90 -90
- package/src/build/island-manifest.ts +269 -269
- package/src/build/island-types-generator.ts +476 -476
- package/src/build/mdx-island-transform.ts +464 -464
- package/src/build/mdx-plugin.ts +98 -98
- package/src/build/page-island-transform.ts +598 -598
- package/src/build/prop-extractors/index.ts +21 -21
- package/src/build/prop-extractors/lit.ts +140 -140
- package/src/build/prop-extractors/qwik.ts +16 -16
- package/src/build/prop-extractors/solid.ts +125 -125
- package/src/build/prop-extractors/svelte.ts +194 -194
- package/src/build/prop-extractors/vue.ts +111 -111
- package/src/build/sidecar-file-manager.ts +104 -104
- package/src/build/sidecar-renderer.ts +30 -30
- package/src/client/adapters/index.ts +21 -13
- package/src/client/components.ts +35 -35
- package/src/client/css-hmr-handler.ts +344 -344
- package/src/client/framework-adapter.ts +462 -462
- package/src/client/hmr-coordinator.ts +396 -396
- package/src/client/hmr-error-overlay.js +533 -533
- package/src/client/main.js +824 -816
- package/src/client/types/framework-runtime.d.ts +68 -68
- package/src/client/types/vite-hmr.d.ts +46 -46
- package/src/client/types/vite-virtual-modules.d.ts +70 -60
- package/src/components/Image.tsx +123 -123
- package/src/components/IslandErrorBoundary.tsx +145 -145
- package/src/components/LayoutDataErrorBoundary.tsx +141 -141
- package/src/components/LayoutErrorBoundary.tsx +127 -127
- package/src/components/PersistentIsland.tsx +52 -52
- package/src/components/StreamingErrorBoundary.tsx +233 -233
- package/src/components/StreamingLayout.tsx +538 -538
- package/src/core/components/component-analyzer.ts +192 -192
- package/src/core/components/component-detection.ts +508 -508
- package/src/core/components/enhanced-framework-detector.ts +500 -500
- package/src/core/components/framework-registry.ts +563 -563
- package/src/core/content/mdx-processor.ts +46 -46
- package/src/core/integrations/index.ts +19 -19
- package/src/core/integrations/loader.ts +125 -125
- package/src/core/integrations/registry.ts +175 -175
- package/src/core/islands/island-persistence.ts +325 -325
- package/src/core/islands/island-state-serializer.ts +258 -258
- package/src/core/islands/persistent-island-context.tsx +80 -80
- package/src/core/islands/use-persistent-state.ts +68 -68
- package/src/core/layout/enhanced-layout-resolver.ts +322 -322
- package/src/core/layout/layout-cache-manager.ts +485 -485
- package/src/core/layout/layout-composer.ts +357 -357
- package/src/core/layout/layout-data-loader.ts +516 -516
- package/src/core/layout/layout-discovery.ts +243 -243
- package/src/core/layout/layout-matcher.ts +299 -299
- package/src/core/layout/layout-types.ts +110 -110
- package/src/core/modules/framework-module-resolver.ts +273 -273
- package/src/islands/component-analysis.ts +213 -213
- package/src/islands/css-utils.ts +565 -565
- package/src/islands/discovery/index.ts +80 -80
- package/src/islands/discovery/registry.ts +340 -340
- package/src/islands/discovery/resolver.ts +477 -477
- package/src/islands/discovery/scanner.ts +386 -386
- package/src/islands/discovery/types.ts +117 -117
- package/src/islands/discovery/validator.ts +544 -544
- package/src/islands/discovery/watcher.ts +368 -368
- package/src/islands/framework-detection.ts +428 -428
- package/src/islands/integration-loader.ts +490 -490
- package/src/islands/island.tsx +565 -565
- package/src/islands/render-cache.ts +550 -550
- package/src/islands/types.ts +80 -80
- package/src/islands/universal-css-collector.ts +157 -157
- package/src/islands/universal-head-collector.ts +137 -137
- package/src/layout-system.d.ts +592 -592
- package/src/layout-system.ts +218 -218
- package/src/middleware/discovery.ts +268 -268
- package/src/middleware/executor.ts +315 -315
- package/src/middleware/index.ts +76 -76
- package/src/middleware/types.ts +99 -99
- package/src/nitro/build-config.ts +575 -575
- package/src/nitro/config.ts +483 -483
- package/src/nitro/error-handler.ts +636 -636
- package/src/nitro/index.ts +173 -173
- package/src/nitro/island-manifest.ts +584 -584
- package/src/nitro/middleware-adapter.ts +260 -260
- package/src/nitro/renderer.ts +1471 -1471
- package/src/nitro/route-discovery.ts +439 -439
- package/src/nitro/types.ts +321 -321
- package/src/render/collect-css.ts +198 -198
- package/src/render/error-pages.ts +79 -79
- package/src/render/isolated-ssr-renderer.ts +654 -654
- package/src/render/ssr.ts +1030 -1030
- package/src/schemas/api.ts +30 -30
- package/src/schemas/core.ts +64 -64
- package/src/schemas/index.ts +212 -212
- package/src/schemas/layout.ts +279 -279
- package/src/schemas/routing/index.ts +38 -38
- package/src/schemas/routing.ts +376 -376
- package/src/types/as-island.ts +20 -20
- package/src/types/image.d.ts +106 -106
- package/src/types/index.d.ts +22 -22
- package/src/types/island-jsx.d.ts +33 -33
- package/src/types/island-prop.d.ts +20 -20
- package/src/types/layout.ts +285 -285
- package/src/types/mdx.d.ts +6 -6
- package/src/types/routing.ts +555 -555
- package/src/types/types.ts +5 -5
- package/src/types/urlpattern.d.ts +49 -49
- package/src/types/vite-env.d.ts +11 -11
- package/src/utils/dev-logger.ts +299 -299
- package/src/utils/fs.ts +151 -151
- package/src/vite-plugin/auto-discover.ts +551 -551
- package/src/vite-plugin/config.ts +266 -266
- package/src/vite-plugin/errors.ts +127 -127
- package/src/vite-plugin/image-optimization.ts +156 -156
- package/src/vite-plugin/integration-activator.ts +126 -126
- package/src/vite-plugin/island-sidecar-plugin.ts +176 -176
- package/src/vite-plugin/module-discovery.ts +189 -189
- package/src/vite-plugin/nitro-integration.ts +1354 -1354
- package/src/vite-plugin/plugin.ts +403 -409
- package/src/vite-plugin/types.ts +327 -327
- package/src/vite-plugin/validation.ts +228 -228
- package/src/client/adapters/index.js +0 -12
- package/src/client/adapters/lit-adapter.js +0 -467
- package/src/client/adapters/lit-adapter.ts +0 -654
- package/src/client/adapters/preact-adapter.js +0 -223
- package/src/client/adapters/preact-adapter.ts +0 -331
- package/src/client/adapters/qwik-adapter.js +0 -259
- package/src/client/adapters/qwik-adapter.ts +0 -345
- package/src/client/adapters/react-adapter.js +0 -220
- package/src/client/adapters/react-adapter.ts +0 -353
- package/src/client/adapters/solid-adapter.js +0 -295
- package/src/client/adapters/solid-adapter.ts +0 -451
- package/src/client/adapters/svelte-adapter.js +0 -368
- package/src/client/adapters/svelte-adapter.ts +0 -524
- package/src/client/adapters/vue-adapter.js +0 -278
- package/src/client/adapters/vue-adapter.ts +0 -467
- package/src/client/components.js +0 -23
- package/src/client/css-hmr-handler.js +0 -263
- package/src/client/framework-adapter.js +0 -283
- package/src/client/hmr-coordinator.js +0 -274
|
@@ -1,462 +1,462 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Framework HMR Adapter Interface
|
|
3
|
-
*
|
|
4
|
-
* Defines the contract for framework-specific HMR adapters.
|
|
5
|
-
* Each supported framework (React, Preact, Vue, Svelte, Solid, Lit) implements this interface
|
|
6
|
-
* to provide framework-specific hot module replacement behavior.
|
|
7
|
-
*
|
|
8
|
-
* Requirements: 2.1-2.7
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/// <reference lib="dom" />
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* State snapshot for preserving component state across HMR updates
|
|
15
|
-
*/
|
|
16
|
-
export interface StateSnapshot {
|
|
17
|
-
/**
|
|
18
|
-
* Framework name (e.g., 'react', 'vue', 'svelte')
|
|
19
|
-
*/
|
|
20
|
-
framework: string;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Timestamp when state was captured
|
|
24
|
-
*/
|
|
25
|
-
timestamp: number;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Framework-specific state data
|
|
29
|
-
* - React: hooks state, context values
|
|
30
|
-
* - Vue: reactive data, computed properties
|
|
31
|
-
* - Svelte: component state, store subscriptions
|
|
32
|
-
* - Solid: signal values, reactive computations
|
|
33
|
-
* - Lit: element properties, attributes
|
|
34
|
-
*/
|
|
35
|
-
data: Record<string, unknown>;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* DOM state (scroll, focus, form values)
|
|
39
|
-
*/
|
|
40
|
-
dom?: {
|
|
41
|
-
scrollPosition?: { x: number; y: number };
|
|
42
|
-
focusedElement?: string;
|
|
43
|
-
formValues?: Record<string, unknown>;
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Framework-specific HMR adapter interface
|
|
49
|
-
*
|
|
50
|
-
* Each framework adapter implements this interface to provide:
|
|
51
|
-
* - Component identification (canHandle)
|
|
52
|
-
* - State preservation (preserveState, restoreState)
|
|
53
|
-
* - Hot updates (update)
|
|
54
|
-
* - Error handling (handleError)
|
|
55
|
-
*/
|
|
56
|
-
export interface FrameworkHMRAdapter {
|
|
57
|
-
/**
|
|
58
|
-
* Framework name (e.g., 'react', 'preact', 'vue', 'svelte', 'solid', 'lit')
|
|
59
|
-
*/
|
|
60
|
-
readonly name: string;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Check if this adapter can handle HMR for a given component
|
|
64
|
-
*
|
|
65
|
-
* @param component - The component to check
|
|
66
|
-
* @returns true if this adapter can handle the component
|
|
67
|
-
*
|
|
68
|
-
* Example:
|
|
69
|
-
* - React: Check for React.Component or function with hooks
|
|
70
|
-
* - Vue: Check for Vue component options object
|
|
71
|
-
* - Svelte: Check for Svelte component class
|
|
72
|
-
*/
|
|
73
|
-
canHandle(component: unknown): boolean;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Preserve component state before HMR update
|
|
77
|
-
*
|
|
78
|
-
* Captures the current state of the component so it can be restored after update.
|
|
79
|
-
* Returns null if state preservation is not supported or fails.
|
|
80
|
-
*
|
|
81
|
-
* @param island - The island DOM element containing the component
|
|
82
|
-
* @returns State snapshot or null if preservation fails
|
|
83
|
-
*
|
|
84
|
-
* Requirements: 1.3, 4.1-4.5
|
|
85
|
-
*/
|
|
86
|
-
preserveState(island: HTMLElement): StateSnapshot | null;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Update the component with a new module
|
|
90
|
-
*
|
|
91
|
-
* Performs the actual hot module replacement:
|
|
92
|
-
* - Unmounts old component (if needed)
|
|
93
|
-
* - Mounts new component with same props
|
|
94
|
-
* - Integrates with framework-specific HMR APIs
|
|
95
|
-
*
|
|
96
|
-
* @param island - The island DOM element
|
|
97
|
-
* @param newComponent - The new component class/function
|
|
98
|
-
* @param props - Component props
|
|
99
|
-
*
|
|
100
|
-
* Requirements: 2.1-2.6
|
|
101
|
-
*/
|
|
102
|
-
update(island: HTMLElement, newComponent: unknown, props: Record<string, unknown>): Promise<void>;
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Restore component state after HMR update
|
|
106
|
-
*
|
|
107
|
-
* Applies the previously captured state to the updated component.
|
|
108
|
-
* Should handle cases where state structure has changed.
|
|
109
|
-
*
|
|
110
|
-
* @param island - The island DOM element
|
|
111
|
-
* @param state - Previously captured state snapshot
|
|
112
|
-
*
|
|
113
|
-
* Requirements: 1.3, 4.1-4.5
|
|
114
|
-
*/
|
|
115
|
-
restoreState(island: HTMLElement, state: StateSnapshot): void;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Handle errors during HMR update
|
|
119
|
-
*
|
|
120
|
-
* Provides framework-specific error handling:
|
|
121
|
-
* - Display error overlay
|
|
122
|
-
* - Preserve SSR HTML as fallback
|
|
123
|
-
* - Log diagnostic information
|
|
124
|
-
*
|
|
125
|
-
* @param island - The island DOM element
|
|
126
|
-
* @param error - The error that occurred
|
|
127
|
-
*
|
|
128
|
-
* Requirements: 1.4, 7.1-7.5
|
|
129
|
-
*/
|
|
130
|
-
handleError(island: HTMLElement, error: Error): void;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Adapter registry for managing framework-specific HMR adapters
|
|
135
|
-
*/
|
|
136
|
-
export class AdapterRegistry {
|
|
137
|
-
private adapters: Map<string, FrameworkHMRAdapter> = new Map();
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Register a framework-specific HMR adapter
|
|
141
|
-
*
|
|
142
|
-
* @param framework - Framework name (case-insensitive)
|
|
143
|
-
* @param adapter - The adapter implementation
|
|
144
|
-
* @throws Error if adapter is invalid or already registered
|
|
145
|
-
*
|
|
146
|
-
* Requirements: 2.1-2.7
|
|
147
|
-
*/
|
|
148
|
-
register(framework: string, adapter: FrameworkHMRAdapter): void {
|
|
149
|
-
const normalizedName = framework.toLowerCase();
|
|
150
|
-
|
|
151
|
-
// Validate adapter
|
|
152
|
-
if (!adapter) {
|
|
153
|
-
throw new Error(`Cannot register null/undefined adapter for framework: ${framework}`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!adapter.name) {
|
|
157
|
-
throw new Error(`Adapter for framework ${framework} must have a name property`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (typeof adapter.canHandle !== 'function') {
|
|
161
|
-
throw new Error(`Adapter for framework ${framework} must implement canHandle method`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (typeof adapter.preserveState !== 'function') {
|
|
165
|
-
throw new Error(`Adapter for framework ${framework} must implement preserveState method`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (typeof adapter.update !== 'function') {
|
|
169
|
-
throw new Error(`Adapter for framework ${framework} must implement update method`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (typeof adapter.restoreState !== 'function') {
|
|
173
|
-
throw new Error(`Adapter for framework ${framework} must implement restoreState method`);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (typeof adapter.handleError !== 'function') {
|
|
177
|
-
throw new Error(`Adapter for framework ${framework} must implement handleError method`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Check if already registered
|
|
181
|
-
if (this.adapters.has(normalizedName)) {
|
|
182
|
-
console.warn(`Overwriting existing HMR adapter for framework: ${framework}`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
this.adapters.set(normalizedName, adapter);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Get an adapter for a specific framework
|
|
190
|
-
*
|
|
191
|
-
* @param framework - Framework name (case-insensitive)
|
|
192
|
-
* @returns The adapter or undefined if not found
|
|
193
|
-
*/
|
|
194
|
-
get(framework: string): FrameworkHMRAdapter | undefined {
|
|
195
|
-
return this.adapters.get(framework.toLowerCase());
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Check if an adapter is registered for a framework
|
|
200
|
-
*
|
|
201
|
-
* @param framework - Framework name (case-insensitive)
|
|
202
|
-
* @returns true if adapter is registered
|
|
203
|
-
*/
|
|
204
|
-
has(framework: string): boolean {
|
|
205
|
-
return this.adapters.has(framework.toLowerCase());
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Get all registered framework names
|
|
210
|
-
*
|
|
211
|
-
* @returns Array of registered framework names
|
|
212
|
-
*/
|
|
213
|
-
getRegisteredFrameworks(): string[] {
|
|
214
|
-
return Array.from(this.adapters.keys());
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Find an adapter that can handle a specific component
|
|
219
|
-
*
|
|
220
|
-
* Iterates through all registered adapters and returns the first one
|
|
221
|
-
* that can handle the component.
|
|
222
|
-
*
|
|
223
|
-
* @param component - The component to check
|
|
224
|
-
* @returns The adapter that can handle the component, or undefined
|
|
225
|
-
*/
|
|
226
|
-
findAdapter(component: unknown): FrameworkHMRAdapter | undefined {
|
|
227
|
-
for (const adapter of this.adapters.values()) {
|
|
228
|
-
if (adapter.canHandle(component)) {
|
|
229
|
-
return adapter;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return undefined;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Unregister an adapter
|
|
237
|
-
*
|
|
238
|
-
* @param framework - Framework name (case-insensitive)
|
|
239
|
-
* @returns true if adapter was removed, false if not found
|
|
240
|
-
*/
|
|
241
|
-
unregister(framework: string): boolean {
|
|
242
|
-
const normalizedName = framework.toLowerCase();
|
|
243
|
-
const removed = this.adapters.delete(normalizedName);
|
|
244
|
-
|
|
245
|
-
return removed;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Clear all registered adapters
|
|
250
|
-
*/
|
|
251
|
-
clear(): void {
|
|
252
|
-
this.adapters.clear();
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Get the number of registered adapters
|
|
257
|
-
*/
|
|
258
|
-
get size(): number {
|
|
259
|
-
return this.adapters.size;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Base adapter class with common functionality
|
|
265
|
-
*
|
|
266
|
-
* Framework-specific adapters can extend this class to inherit common behavior
|
|
267
|
-
* and only override framework-specific methods.
|
|
268
|
-
*/
|
|
269
|
-
export abstract class BaseFrameworkAdapter implements FrameworkHMRAdapter {
|
|
270
|
-
abstract readonly name: string;
|
|
271
|
-
|
|
272
|
-
abstract canHandle(component: unknown): boolean;
|
|
273
|
-
|
|
274
|
-
abstract update(island: HTMLElement, newComponent: unknown, props: Record<string, unknown>): Promise<void>;
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Default state preservation implementation
|
|
278
|
-
* Captures DOM state (scroll, focus, form values)
|
|
279
|
-
*
|
|
280
|
-
* Subclasses should override to add framework-specific state
|
|
281
|
-
*/
|
|
282
|
-
preserveState(island: HTMLElement): StateSnapshot | null {
|
|
283
|
-
try {
|
|
284
|
-
const snapshot: StateSnapshot = {
|
|
285
|
-
framework: this.name,
|
|
286
|
-
timestamp: Date.now(),
|
|
287
|
-
data: {},
|
|
288
|
-
dom: this.captureDOMState(island),
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
return snapshot;
|
|
292
|
-
} catch (error) {
|
|
293
|
-
console.warn(`Failed to preserve state for ${this.name}:`, error);
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Default state restoration implementation
|
|
300
|
-
* Restores DOM state (scroll, focus, form values)
|
|
301
|
-
*
|
|
302
|
-
* Subclasses should override to add framework-specific state restoration
|
|
303
|
-
*/
|
|
304
|
-
restoreState(island: HTMLElement, state: StateSnapshot): void {
|
|
305
|
-
try {
|
|
306
|
-
if (state.dom) {
|
|
307
|
-
this.restoreDOMState(island, state.dom);
|
|
308
|
-
}
|
|
309
|
-
} catch (error) {
|
|
310
|
-
console.warn(`Failed to restore state for ${this.name}:`, error);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Default error handling implementation
|
|
316
|
-
* Shows error indicator and preserves SSR HTML
|
|
317
|
-
*/
|
|
318
|
-
handleError(island: HTMLElement, error: Error): void {
|
|
319
|
-
console.error(`HMR error in ${this.name} island:`, error);
|
|
320
|
-
|
|
321
|
-
// Add error indicator
|
|
322
|
-
const errorIndicator = document.createElement('div');
|
|
323
|
-
errorIndicator.className = 'hmr-error-indicator';
|
|
324
|
-
errorIndicator.style.cssText = `
|
|
325
|
-
position: absolute;
|
|
326
|
-
top: 0;
|
|
327
|
-
left: 0;
|
|
328
|
-
right: 0;
|
|
329
|
-
background: #ff4444;
|
|
330
|
-
color: white;
|
|
331
|
-
padding: 8px;
|
|
332
|
-
font-size: 12px;
|
|
333
|
-
font-family: monospace;
|
|
334
|
-
z-index: 10000;
|
|
335
|
-
border-bottom: 2px solid #cc0000;
|
|
336
|
-
`;
|
|
337
|
-
errorIndicator.textContent = `HMR Error: ${error.message}`;
|
|
338
|
-
|
|
339
|
-
// Remove existing error indicators
|
|
340
|
-
const existing = island.querySelector('.hmr-error-indicator');
|
|
341
|
-
if (existing) {
|
|
342
|
-
existing.remove();
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
island.style.position = 'relative';
|
|
346
|
-
island.insertBefore(errorIndicator, island.firstChild);
|
|
347
|
-
|
|
348
|
-
// Mark island as having error
|
|
349
|
-
island.setAttribute('data-hmr-error', 'true');
|
|
350
|
-
island.setAttribute('data-hmr-error-message', error.message);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Capture DOM state (scroll, focus, form values)
|
|
355
|
-
*/
|
|
356
|
-
protected captureDOMState(island: HTMLElement): StateSnapshot['dom'] {
|
|
357
|
-
const dom: StateSnapshot['dom'] = {};
|
|
358
|
-
|
|
359
|
-
// Capture scroll position
|
|
360
|
-
const scrollableElements = island.querySelectorAll('[data-preserve-scroll]');
|
|
361
|
-
if (scrollableElements.length > 0 || island.scrollTop > 0 || island.scrollLeft > 0) {
|
|
362
|
-
dom.scrollPosition = {
|
|
363
|
-
x: island.scrollLeft,
|
|
364
|
-
y: island.scrollTop,
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Capture focused element
|
|
369
|
-
const activeElement = document.activeElement;
|
|
370
|
-
if (activeElement && island.contains(activeElement)) {
|
|
371
|
-
const selector = this.getElementSelector(activeElement as HTMLElement);
|
|
372
|
-
if (selector) {
|
|
373
|
-
dom.focusedElement = selector;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Capture form values
|
|
378
|
-
const formElements = island.querySelectorAll('input, textarea, select');
|
|
379
|
-
if (formElements.length > 0) {
|
|
380
|
-
dom.formValues = {};
|
|
381
|
-
formElements.forEach((element, index) => {
|
|
382
|
-
const input = element as HTMLInputElement;
|
|
383
|
-
const name = input.name || input.id || `element-${index}`;
|
|
384
|
-
|
|
385
|
-
if (input.type === 'checkbox' || input.type === 'radio') {
|
|
386
|
-
dom.formValues![name] = input.checked;
|
|
387
|
-
} else {
|
|
388
|
-
dom.formValues![name] = input.value;
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return dom;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Restore DOM state (scroll, focus, form values)
|
|
398
|
-
*/
|
|
399
|
-
protected restoreDOMState(island: HTMLElement, dom: StateSnapshot['dom']): void {
|
|
400
|
-
if (!dom) return;
|
|
401
|
-
|
|
402
|
-
// Restore scroll position
|
|
403
|
-
if (dom.scrollPosition) {
|
|
404
|
-
island.scrollLeft = dom.scrollPosition.x;
|
|
405
|
-
island.scrollTop = dom.scrollPosition.y;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Restore focused element
|
|
409
|
-
if (dom.focusedElement) {
|
|
410
|
-
try {
|
|
411
|
-
const element = island.querySelector(dom.focusedElement) as HTMLElement;
|
|
412
|
-
if (element && typeof element.focus === 'function') {
|
|
413
|
-
element.focus();
|
|
414
|
-
}
|
|
415
|
-
} catch (error) {
|
|
416
|
-
console.warn('Failed to restore focus:', error);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Restore form values
|
|
421
|
-
if (dom.formValues) {
|
|
422
|
-
const formElements = island.querySelectorAll('input, textarea, select');
|
|
423
|
-
formElements.forEach((element, index) => {
|
|
424
|
-
const input = element as HTMLInputElement;
|
|
425
|
-
const name = input.name || input.id || `element-${index}`;
|
|
426
|
-
const value = dom.formValues![name];
|
|
427
|
-
|
|
428
|
-
if (value !== undefined) {
|
|
429
|
-
if (input.type === 'checkbox' || input.type === 'radio') {
|
|
430
|
-
input.checked = value as boolean;
|
|
431
|
-
} else {
|
|
432
|
-
input.value = value as string;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Get a CSS selector for an element
|
|
441
|
-
*/
|
|
442
|
-
protected getElementSelector(element: HTMLElement): string | null {
|
|
443
|
-
if (element.id) {
|
|
444
|
-
return `#${element.id}`;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Check if element has name attribute (for form elements)
|
|
448
|
-
const nameAttr = element.getAttribute('name');
|
|
449
|
-
if (nameAttr) {
|
|
450
|
-
return `[name="${nameAttr}"]`;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Fallback to nth-child selector
|
|
454
|
-
const parent = element.parentElement;
|
|
455
|
-
if (parent) {
|
|
456
|
-
const index = Array.from(parent.children).indexOf(element);
|
|
457
|
-
return `${element.tagName.toLowerCase()}:nth-child(${index + 1})`;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Framework HMR Adapter Interface
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract for framework-specific HMR adapters.
|
|
5
|
+
* Each supported framework (React, Preact, Vue, Svelte, Solid, Lit) implements this interface
|
|
6
|
+
* to provide framework-specific hot module replacement behavior.
|
|
7
|
+
*
|
|
8
|
+
* Requirements: 2.1-2.7
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/// <reference lib="dom" />
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* State snapshot for preserving component state across HMR updates
|
|
15
|
+
*/
|
|
16
|
+
export interface StateSnapshot {
|
|
17
|
+
/**
|
|
18
|
+
* Framework name (e.g., 'react', 'vue', 'svelte')
|
|
19
|
+
*/
|
|
20
|
+
framework: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Timestamp when state was captured
|
|
24
|
+
*/
|
|
25
|
+
timestamp: number;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Framework-specific state data
|
|
29
|
+
* - React: hooks state, context values
|
|
30
|
+
* - Vue: reactive data, computed properties
|
|
31
|
+
* - Svelte: component state, store subscriptions
|
|
32
|
+
* - Solid: signal values, reactive computations
|
|
33
|
+
* - Lit: element properties, attributes
|
|
34
|
+
*/
|
|
35
|
+
data: Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* DOM state (scroll, focus, form values)
|
|
39
|
+
*/
|
|
40
|
+
dom?: {
|
|
41
|
+
scrollPosition?: { x: number; y: number };
|
|
42
|
+
focusedElement?: string;
|
|
43
|
+
formValues?: Record<string, unknown>;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Framework-specific HMR adapter interface
|
|
49
|
+
*
|
|
50
|
+
* Each framework adapter implements this interface to provide:
|
|
51
|
+
* - Component identification (canHandle)
|
|
52
|
+
* - State preservation (preserveState, restoreState)
|
|
53
|
+
* - Hot updates (update)
|
|
54
|
+
* - Error handling (handleError)
|
|
55
|
+
*/
|
|
56
|
+
export interface FrameworkHMRAdapter {
|
|
57
|
+
/**
|
|
58
|
+
* Framework name (e.g., 'react', 'preact', 'vue', 'svelte', 'solid', 'lit')
|
|
59
|
+
*/
|
|
60
|
+
readonly name: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if this adapter can handle HMR for a given component
|
|
64
|
+
*
|
|
65
|
+
* @param component - The component to check
|
|
66
|
+
* @returns true if this adapter can handle the component
|
|
67
|
+
*
|
|
68
|
+
* Example:
|
|
69
|
+
* - React: Check for React.Component or function with hooks
|
|
70
|
+
* - Vue: Check for Vue component options object
|
|
71
|
+
* - Svelte: Check for Svelte component class
|
|
72
|
+
*/
|
|
73
|
+
canHandle(component: unknown): boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Preserve component state before HMR update
|
|
77
|
+
*
|
|
78
|
+
* Captures the current state of the component so it can be restored after update.
|
|
79
|
+
* Returns null if state preservation is not supported or fails.
|
|
80
|
+
*
|
|
81
|
+
* @param island - The island DOM element containing the component
|
|
82
|
+
* @returns State snapshot or null if preservation fails
|
|
83
|
+
*
|
|
84
|
+
* Requirements: 1.3, 4.1-4.5
|
|
85
|
+
*/
|
|
86
|
+
preserveState(island: HTMLElement): StateSnapshot | null;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Update the component with a new module
|
|
90
|
+
*
|
|
91
|
+
* Performs the actual hot module replacement:
|
|
92
|
+
* - Unmounts old component (if needed)
|
|
93
|
+
* - Mounts new component with same props
|
|
94
|
+
* - Integrates with framework-specific HMR APIs
|
|
95
|
+
*
|
|
96
|
+
* @param island - The island DOM element
|
|
97
|
+
* @param newComponent - The new component class/function
|
|
98
|
+
* @param props - Component props
|
|
99
|
+
*
|
|
100
|
+
* Requirements: 2.1-2.6
|
|
101
|
+
*/
|
|
102
|
+
update(island: HTMLElement, newComponent: unknown, props: Record<string, unknown>): Promise<void>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Restore component state after HMR update
|
|
106
|
+
*
|
|
107
|
+
* Applies the previously captured state to the updated component.
|
|
108
|
+
* Should handle cases where state structure has changed.
|
|
109
|
+
*
|
|
110
|
+
* @param island - The island DOM element
|
|
111
|
+
* @param state - Previously captured state snapshot
|
|
112
|
+
*
|
|
113
|
+
* Requirements: 1.3, 4.1-4.5
|
|
114
|
+
*/
|
|
115
|
+
restoreState(island: HTMLElement, state: StateSnapshot): void;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Handle errors during HMR update
|
|
119
|
+
*
|
|
120
|
+
* Provides framework-specific error handling:
|
|
121
|
+
* - Display error overlay
|
|
122
|
+
* - Preserve SSR HTML as fallback
|
|
123
|
+
* - Log diagnostic information
|
|
124
|
+
*
|
|
125
|
+
* @param island - The island DOM element
|
|
126
|
+
* @param error - The error that occurred
|
|
127
|
+
*
|
|
128
|
+
* Requirements: 1.4, 7.1-7.5
|
|
129
|
+
*/
|
|
130
|
+
handleError(island: HTMLElement, error: Error): void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Adapter registry for managing framework-specific HMR adapters
|
|
135
|
+
*/
|
|
136
|
+
export class AdapterRegistry {
|
|
137
|
+
private adapters: Map<string, FrameworkHMRAdapter> = new Map();
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Register a framework-specific HMR adapter
|
|
141
|
+
*
|
|
142
|
+
* @param framework - Framework name (case-insensitive)
|
|
143
|
+
* @param adapter - The adapter implementation
|
|
144
|
+
* @throws Error if adapter is invalid or already registered
|
|
145
|
+
*
|
|
146
|
+
* Requirements: 2.1-2.7
|
|
147
|
+
*/
|
|
148
|
+
register(framework: string, adapter: FrameworkHMRAdapter): void {
|
|
149
|
+
const normalizedName = framework.toLowerCase();
|
|
150
|
+
|
|
151
|
+
// Validate adapter
|
|
152
|
+
if (!adapter) {
|
|
153
|
+
throw new Error(`Cannot register null/undefined adapter for framework: ${framework}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!adapter.name) {
|
|
157
|
+
throw new Error(`Adapter for framework ${framework} must have a name property`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof adapter.canHandle !== 'function') {
|
|
161
|
+
throw new Error(`Adapter for framework ${framework} must implement canHandle method`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (typeof adapter.preserveState !== 'function') {
|
|
165
|
+
throw new Error(`Adapter for framework ${framework} must implement preserveState method`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (typeof adapter.update !== 'function') {
|
|
169
|
+
throw new Error(`Adapter for framework ${framework} must implement update method`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (typeof adapter.restoreState !== 'function') {
|
|
173
|
+
throw new Error(`Adapter for framework ${framework} must implement restoreState method`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (typeof adapter.handleError !== 'function') {
|
|
177
|
+
throw new Error(`Adapter for framework ${framework} must implement handleError method`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check if already registered
|
|
181
|
+
if (this.adapters.has(normalizedName)) {
|
|
182
|
+
console.warn(`Overwriting existing HMR adapter for framework: ${framework}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.adapters.set(normalizedName, adapter);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get an adapter for a specific framework
|
|
190
|
+
*
|
|
191
|
+
* @param framework - Framework name (case-insensitive)
|
|
192
|
+
* @returns The adapter or undefined if not found
|
|
193
|
+
*/
|
|
194
|
+
get(framework: string): FrameworkHMRAdapter | undefined {
|
|
195
|
+
return this.adapters.get(framework.toLowerCase());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if an adapter is registered for a framework
|
|
200
|
+
*
|
|
201
|
+
* @param framework - Framework name (case-insensitive)
|
|
202
|
+
* @returns true if adapter is registered
|
|
203
|
+
*/
|
|
204
|
+
has(framework: string): boolean {
|
|
205
|
+
return this.adapters.has(framework.toLowerCase());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get all registered framework names
|
|
210
|
+
*
|
|
211
|
+
* @returns Array of registered framework names
|
|
212
|
+
*/
|
|
213
|
+
getRegisteredFrameworks(): string[] {
|
|
214
|
+
return Array.from(this.adapters.keys());
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Find an adapter that can handle a specific component
|
|
219
|
+
*
|
|
220
|
+
* Iterates through all registered adapters and returns the first one
|
|
221
|
+
* that can handle the component.
|
|
222
|
+
*
|
|
223
|
+
* @param component - The component to check
|
|
224
|
+
* @returns The adapter that can handle the component, or undefined
|
|
225
|
+
*/
|
|
226
|
+
findAdapter(component: unknown): FrameworkHMRAdapter | undefined {
|
|
227
|
+
for (const adapter of this.adapters.values()) {
|
|
228
|
+
if (adapter.canHandle(component)) {
|
|
229
|
+
return adapter;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Unregister an adapter
|
|
237
|
+
*
|
|
238
|
+
* @param framework - Framework name (case-insensitive)
|
|
239
|
+
* @returns true if adapter was removed, false if not found
|
|
240
|
+
*/
|
|
241
|
+
unregister(framework: string): boolean {
|
|
242
|
+
const normalizedName = framework.toLowerCase();
|
|
243
|
+
const removed = this.adapters.delete(normalizedName);
|
|
244
|
+
|
|
245
|
+
return removed;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Clear all registered adapters
|
|
250
|
+
*/
|
|
251
|
+
clear(): void {
|
|
252
|
+
this.adapters.clear();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get the number of registered adapters
|
|
257
|
+
*/
|
|
258
|
+
get size(): number {
|
|
259
|
+
return this.adapters.size;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Base adapter class with common functionality
|
|
265
|
+
*
|
|
266
|
+
* Framework-specific adapters can extend this class to inherit common behavior
|
|
267
|
+
* and only override framework-specific methods.
|
|
268
|
+
*/
|
|
269
|
+
export abstract class BaseFrameworkAdapter implements FrameworkHMRAdapter {
|
|
270
|
+
abstract readonly name: string;
|
|
271
|
+
|
|
272
|
+
abstract canHandle(component: unknown): boolean;
|
|
273
|
+
|
|
274
|
+
abstract update(island: HTMLElement, newComponent: unknown, props: Record<string, unknown>): Promise<void>;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Default state preservation implementation
|
|
278
|
+
* Captures DOM state (scroll, focus, form values)
|
|
279
|
+
*
|
|
280
|
+
* Subclasses should override to add framework-specific state
|
|
281
|
+
*/
|
|
282
|
+
preserveState(island: HTMLElement): StateSnapshot | null {
|
|
283
|
+
try {
|
|
284
|
+
const snapshot: StateSnapshot = {
|
|
285
|
+
framework: this.name,
|
|
286
|
+
timestamp: Date.now(),
|
|
287
|
+
data: {},
|
|
288
|
+
dom: this.captureDOMState(island),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return snapshot;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.warn(`Failed to preserve state for ${this.name}:`, error);
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Default state restoration implementation
|
|
300
|
+
* Restores DOM state (scroll, focus, form values)
|
|
301
|
+
*
|
|
302
|
+
* Subclasses should override to add framework-specific state restoration
|
|
303
|
+
*/
|
|
304
|
+
restoreState(island: HTMLElement, state: StateSnapshot): void {
|
|
305
|
+
try {
|
|
306
|
+
if (state.dom) {
|
|
307
|
+
this.restoreDOMState(island, state.dom);
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.warn(`Failed to restore state for ${this.name}:`, error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Default error handling implementation
|
|
316
|
+
* Shows error indicator and preserves SSR HTML
|
|
317
|
+
*/
|
|
318
|
+
handleError(island: HTMLElement, error: Error): void {
|
|
319
|
+
console.error(`HMR error in ${this.name} island:`, error);
|
|
320
|
+
|
|
321
|
+
// Add error indicator
|
|
322
|
+
const errorIndicator = document.createElement('div');
|
|
323
|
+
errorIndicator.className = 'hmr-error-indicator';
|
|
324
|
+
errorIndicator.style.cssText = `
|
|
325
|
+
position: absolute;
|
|
326
|
+
top: 0;
|
|
327
|
+
left: 0;
|
|
328
|
+
right: 0;
|
|
329
|
+
background: #ff4444;
|
|
330
|
+
color: white;
|
|
331
|
+
padding: 8px;
|
|
332
|
+
font-size: 12px;
|
|
333
|
+
font-family: monospace;
|
|
334
|
+
z-index: 10000;
|
|
335
|
+
border-bottom: 2px solid #cc0000;
|
|
336
|
+
`;
|
|
337
|
+
errorIndicator.textContent = `HMR Error: ${error.message}`;
|
|
338
|
+
|
|
339
|
+
// Remove existing error indicators
|
|
340
|
+
const existing = island.querySelector('.hmr-error-indicator');
|
|
341
|
+
if (existing) {
|
|
342
|
+
existing.remove();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
island.style.position = 'relative';
|
|
346
|
+
island.insertBefore(errorIndicator, island.firstChild);
|
|
347
|
+
|
|
348
|
+
// Mark island as having error
|
|
349
|
+
island.setAttribute('data-hmr-error', 'true');
|
|
350
|
+
island.setAttribute('data-hmr-error-message', error.message);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Capture DOM state (scroll, focus, form values)
|
|
355
|
+
*/
|
|
356
|
+
protected captureDOMState(island: HTMLElement): StateSnapshot['dom'] {
|
|
357
|
+
const dom: StateSnapshot['dom'] = {};
|
|
358
|
+
|
|
359
|
+
// Capture scroll position
|
|
360
|
+
const scrollableElements = island.querySelectorAll('[data-preserve-scroll]');
|
|
361
|
+
if (scrollableElements.length > 0 || island.scrollTop > 0 || island.scrollLeft > 0) {
|
|
362
|
+
dom.scrollPosition = {
|
|
363
|
+
x: island.scrollLeft,
|
|
364
|
+
y: island.scrollTop,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Capture focused element
|
|
369
|
+
const activeElement = document.activeElement;
|
|
370
|
+
if (activeElement && island.contains(activeElement)) {
|
|
371
|
+
const selector = this.getElementSelector(activeElement as HTMLElement);
|
|
372
|
+
if (selector) {
|
|
373
|
+
dom.focusedElement = selector;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Capture form values
|
|
378
|
+
const formElements = island.querySelectorAll('input, textarea, select');
|
|
379
|
+
if (formElements.length > 0) {
|
|
380
|
+
dom.formValues = {};
|
|
381
|
+
formElements.forEach((element, index) => {
|
|
382
|
+
const input = element as HTMLInputElement;
|
|
383
|
+
const name = input.name || input.id || `element-${index}`;
|
|
384
|
+
|
|
385
|
+
if (input.type === 'checkbox' || input.type === 'radio') {
|
|
386
|
+
dom.formValues![name] = input.checked;
|
|
387
|
+
} else {
|
|
388
|
+
dom.formValues![name] = input.value;
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return dom;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Restore DOM state (scroll, focus, form values)
|
|
398
|
+
*/
|
|
399
|
+
protected restoreDOMState(island: HTMLElement, dom: StateSnapshot['dom']): void {
|
|
400
|
+
if (!dom) return;
|
|
401
|
+
|
|
402
|
+
// Restore scroll position
|
|
403
|
+
if (dom.scrollPosition) {
|
|
404
|
+
island.scrollLeft = dom.scrollPosition.x;
|
|
405
|
+
island.scrollTop = dom.scrollPosition.y;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Restore focused element
|
|
409
|
+
if (dom.focusedElement) {
|
|
410
|
+
try {
|
|
411
|
+
const element = island.querySelector(dom.focusedElement) as HTMLElement;
|
|
412
|
+
if (element && typeof element.focus === 'function') {
|
|
413
|
+
element.focus();
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.warn('Failed to restore focus:', error);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Restore form values
|
|
421
|
+
if (dom.formValues) {
|
|
422
|
+
const formElements = island.querySelectorAll('input, textarea, select');
|
|
423
|
+
formElements.forEach((element, index) => {
|
|
424
|
+
const input = element as HTMLInputElement;
|
|
425
|
+
const name = input.name || input.id || `element-${index}`;
|
|
426
|
+
const value = dom.formValues![name];
|
|
427
|
+
|
|
428
|
+
if (value !== undefined) {
|
|
429
|
+
if (input.type === 'checkbox' || input.type === 'radio') {
|
|
430
|
+
input.checked = value as boolean;
|
|
431
|
+
} else {
|
|
432
|
+
input.value = value as string;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Get a CSS selector for an element
|
|
441
|
+
*/
|
|
442
|
+
protected getElementSelector(element: HTMLElement): string | null {
|
|
443
|
+
if (element.id) {
|
|
444
|
+
return `#${element.id}`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Check if element has name attribute (for form elements)
|
|
448
|
+
const nameAttr = element.getAttribute('name');
|
|
449
|
+
if (nameAttr) {
|
|
450
|
+
return `[name="${nameAttr}"]`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Fallback to nth-child selector
|
|
454
|
+
const parent = element.parentElement;
|
|
455
|
+
if (parent) {
|
|
456
|
+
const index = Array.from(parent.children).indexOf(element);
|
|
457
|
+
return `${element.tagName.toLowerCase()}:nth-child(${index + 1})`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
}
|