@useavalon/avalon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +54 -0
  2. package/mod.ts +301 -0
  3. package/package.json +85 -0
  4. package/src/build/README.md +310 -0
  5. package/src/build/integration-bundler-plugin.ts +116 -0
  6. package/src/build/integration-config.ts +168 -0
  7. package/src/build/integration-detection-plugin.ts +117 -0
  8. package/src/build/integration-resolver-plugin.ts +90 -0
  9. package/src/build/island-manifest.ts +269 -0
  10. package/src/build/island-types-generator.ts +476 -0
  11. package/src/build/mdx-island-transform.ts +464 -0
  12. package/src/build/mdx-plugin.ts +98 -0
  13. package/src/build/page-island-transform.ts +598 -0
  14. package/src/build/prop-extractors/index.ts +21 -0
  15. package/src/build/prop-extractors/lit.ts +140 -0
  16. package/src/build/prop-extractors/qwik.ts +16 -0
  17. package/src/build/prop-extractors/solid.ts +125 -0
  18. package/src/build/prop-extractors/svelte.ts +194 -0
  19. package/src/build/prop-extractors/vue.ts +111 -0
  20. package/src/build/sidecar-file-manager.ts +104 -0
  21. package/src/build/sidecar-renderer.ts +30 -0
  22. package/src/client/adapters/index.ts +13 -0
  23. package/src/client/adapters/lit-adapter.ts +654 -0
  24. package/src/client/adapters/preact-adapter.ts +331 -0
  25. package/src/client/adapters/qwik-adapter.ts +345 -0
  26. package/src/client/adapters/react-adapter.ts +353 -0
  27. package/src/client/adapters/solid-adapter.ts +451 -0
  28. package/src/client/adapters/svelte-adapter.ts +524 -0
  29. package/src/client/adapters/vue-adapter.ts +467 -0
  30. package/src/client/components.ts +35 -0
  31. package/src/client/css-hmr-handler.ts +344 -0
  32. package/src/client/framework-adapter.ts +462 -0
  33. package/src/client/hmr-coordinator.ts +396 -0
  34. package/src/client/hmr-error-overlay.js +533 -0
  35. package/src/client/main.js +816 -0
  36. package/src/client/tests/css-hmr-handler.test.ts +360 -0
  37. package/src/client/tests/framework-adapter.test.ts +519 -0
  38. package/src/client/tests/hmr-coordinator.test.ts +176 -0
  39. package/src/client/tests/hydration-option-parsing.test.ts +107 -0
  40. package/src/client/tests/lit-adapter.test.ts +427 -0
  41. package/src/client/tests/preact-adapter.test.ts +353 -0
  42. package/src/client/tests/qwik-adapter.test.ts +343 -0
  43. package/src/client/tests/react-adapter.test.ts +317 -0
  44. package/src/client/tests/solid-adapter.test.ts +396 -0
  45. package/src/client/tests/svelte-adapter.test.ts +387 -0
  46. package/src/client/tests/vue-adapter.test.ts +407 -0
  47. package/src/client/types/framework-runtime.d.ts +68 -0
  48. package/src/client/types/vite-hmr.d.ts +46 -0
  49. package/src/client/types/vite-virtual-modules.d.ts +60 -0
  50. package/src/components/Image.tsx +123 -0
  51. package/src/components/IslandErrorBoundary.tsx +145 -0
  52. package/src/components/LayoutDataErrorBoundary.tsx +141 -0
  53. package/src/components/LayoutErrorBoundary.tsx +127 -0
  54. package/src/components/PersistentIsland.tsx +52 -0
  55. package/src/components/StreamingErrorBoundary.tsx +233 -0
  56. package/src/components/StreamingLayout.tsx +538 -0
  57. package/src/components/tests/component-analyzer.test.ts +96 -0
  58. package/src/components/tests/component-detection.test.ts +347 -0
  59. package/src/components/tests/persistent-islands.test.ts +398 -0
  60. package/src/core/components/component-analyzer.ts +192 -0
  61. package/src/core/components/component-detection.ts +508 -0
  62. package/src/core/components/enhanced-framework-detector.ts +500 -0
  63. package/src/core/components/framework-registry.ts +563 -0
  64. package/src/core/components/tests/enhanced-framework-detector.test.ts +577 -0
  65. package/src/core/components/tests/framework-registry.test.ts +465 -0
  66. package/src/core/content/mdx-processor.ts +46 -0
  67. package/src/core/integrations/README.md +282 -0
  68. package/src/core/integrations/index.ts +19 -0
  69. package/src/core/integrations/loader.ts +125 -0
  70. package/src/core/integrations/registry.ts +195 -0
  71. package/src/core/islands/island-persistence.ts +325 -0
  72. package/src/core/islands/island-state-serializer.ts +258 -0
  73. package/src/core/islands/persistent-island-context.tsx +80 -0
  74. package/src/core/islands/use-persistent-state.ts +68 -0
  75. package/src/core/layout/enhanced-layout-resolver.ts +322 -0
  76. package/src/core/layout/layout-cache-manager.ts +485 -0
  77. package/src/core/layout/layout-composer.ts +357 -0
  78. package/src/core/layout/layout-data-loader.ts +516 -0
  79. package/src/core/layout/layout-discovery.ts +243 -0
  80. package/src/core/layout/layout-matcher.ts +299 -0
  81. package/src/core/layout/layout-types.ts +110 -0
  82. package/src/core/layout/tests/enhanced-layout-resolver.test.ts +477 -0
  83. package/src/core/layout/tests/layout-cache-optimization.test.ts +149 -0
  84. package/src/core/layout/tests/layout-composer.test.ts +486 -0
  85. package/src/core/layout/tests/layout-data-loader.test.ts +443 -0
  86. package/src/core/layout/tests/layout-discovery.test.ts +253 -0
  87. package/src/core/layout/tests/layout-matcher.test.ts +480 -0
  88. package/src/core/modules/framework-module-resolver.ts +273 -0
  89. package/src/core/modules/tests/framework-module-resolver.test.ts +263 -0
  90. package/src/core/modules/tests/module-resolution-integration.test.ts +117 -0
  91. package/src/islands/component-analysis.ts +213 -0
  92. package/src/islands/css-utils.ts +565 -0
  93. package/src/islands/discovery/index.ts +80 -0
  94. package/src/islands/discovery/registry.ts +340 -0
  95. package/src/islands/discovery/resolver.ts +477 -0
  96. package/src/islands/discovery/scanner.ts +386 -0
  97. package/src/islands/discovery/tests/island-discovery.test.ts +881 -0
  98. package/src/islands/discovery/types.ts +117 -0
  99. package/src/islands/discovery/validator.ts +544 -0
  100. package/src/islands/discovery/watcher.ts +368 -0
  101. package/src/islands/framework-detection.ts +428 -0
  102. package/src/islands/integration-loader.ts +490 -0
  103. package/src/islands/island.tsx +565 -0
  104. package/src/islands/render-cache.ts +550 -0
  105. package/src/islands/types.ts +80 -0
  106. package/src/islands/universal-css-collector.ts +157 -0
  107. package/src/islands/universal-head-collector.ts +137 -0
  108. package/src/layout-system.d.ts +592 -0
  109. package/src/layout-system.ts +218 -0
  110. package/src/middleware/__tests__/discovery.test.ts +107 -0
  111. package/src/middleware/discovery.ts +268 -0
  112. package/src/middleware/executor.ts +315 -0
  113. package/src/middleware/index.ts +76 -0
  114. package/src/middleware/types.ts +99 -0
  115. package/src/nitro/build-config.ts +576 -0
  116. package/src/nitro/config.ts +483 -0
  117. package/src/nitro/error-handler.ts +636 -0
  118. package/src/nitro/index.ts +173 -0
  119. package/src/nitro/island-manifest.ts +584 -0
  120. package/src/nitro/middleware-adapter.ts +260 -0
  121. package/src/nitro/renderer.ts +1458 -0
  122. package/src/nitro/route-discovery.ts +439 -0
  123. package/src/nitro/types.ts +321 -0
  124. package/src/render/collect-css.ts +198 -0
  125. package/src/render/error-pages.ts +79 -0
  126. package/src/render/isolated-ssr-renderer.ts +654 -0
  127. package/src/render/ssr.ts +1030 -0
  128. package/src/schemas/api.ts +30 -0
  129. package/src/schemas/core.ts +64 -0
  130. package/src/schemas/index.ts +212 -0
  131. package/src/schemas/layout.ts +279 -0
  132. package/src/schemas/routing/index.ts +38 -0
  133. package/src/schemas/routing.ts +376 -0
  134. package/src/types/as-island.ts +20 -0
  135. package/src/types/image.d.ts +106 -0
  136. package/src/types/index.d.ts +22 -0
  137. package/src/types/island-jsx.d.ts +33 -0
  138. package/src/types/island-prop.d.ts +20 -0
  139. package/src/types/layout.ts +285 -0
  140. package/src/types/mdx.d.ts +6 -0
  141. package/src/types/routing.ts +555 -0
  142. package/src/types/tests/layout-types.test.ts +197 -0
  143. package/src/types/types.ts +5 -0
  144. package/src/types/urlpattern.d.ts +49 -0
  145. package/src/types/vite-env.d.ts +11 -0
  146. package/src/utils/dev-logger.ts +299 -0
  147. package/src/utils/fs.ts +151 -0
  148. package/src/vite-plugin/auto-discover.ts +551 -0
  149. package/src/vite-plugin/config.ts +266 -0
  150. package/src/vite-plugin/errors.ts +127 -0
  151. package/src/vite-plugin/image-optimization.ts +151 -0
  152. package/src/vite-plugin/integration-activator.ts +126 -0
  153. package/src/vite-plugin/island-sidecar-plugin.ts +176 -0
  154. package/src/vite-plugin/module-discovery.ts +189 -0
  155. package/src/vite-plugin/nitro-integration.ts +1334 -0
  156. package/src/vite-plugin/plugin.ts +329 -0
  157. package/src/vite-plugin/tests/image-optimization.test.ts +54 -0
  158. package/src/vite-plugin/types.ts +327 -0
  159. package/src/vite-plugin/validation.ts +228 -0
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Preact HMR Adapter
3
+ *
4
+ * Provides Hot Module Replacement support for Preact components.
5
+ * Integrates with @preact/preset-vite to preserve hooks state and component tree during updates.
6
+ *
7
+ * Requirements: 2.2
8
+ */
9
+
10
+ /// <reference lib="dom" />
11
+
12
+ import { BaseFrameworkAdapter, type StateSnapshot } from '../framework-adapter.ts';
13
+
14
+ /**
15
+ * Preact component type
16
+ * Can be a function component or class component
17
+ */
18
+ type PreactComponent<P = Record<string, unknown>> =
19
+ | ((props: P) => PreactVNode | null)
20
+ | (new (props: P) => PreactClassComponent);
21
+
22
+ /**
23
+ * Preact class component interface
24
+ */
25
+ interface PreactClassComponent {
26
+ render(): PreactVNode | null;
27
+ isReactComponent?: boolean; // Preact uses same marker as React
28
+ }
29
+
30
+ /**
31
+ * Preact VNode (Virtual Node) type
32
+ */
33
+ interface PreactVNode {
34
+ type: string | PreactComponent;
35
+ props: Record<string, unknown>;
36
+ key: string | number | null;
37
+ }
38
+
39
+ /**
40
+ * Preact module interface
41
+ */
42
+ interface PreactModule {
43
+ h<P = Record<string, unknown>>(
44
+ component: PreactComponent<P> | string,
45
+ props: P | null,
46
+ ...children: unknown[]
47
+ ): PreactVNode;
48
+ render(vnode: PreactVNode, container: HTMLElement): void;
49
+ hydrate(vnode: PreactVNode, container: HTMLElement): void;
50
+ }
51
+
52
+ /**
53
+ * Preact-specific state snapshot
54
+ * Extends base snapshot with Preact-specific state like hooks
55
+ */
56
+ interface PreactStateSnapshot extends StateSnapshot {
57
+ framework: 'preact';
58
+ data: {
59
+ /**
60
+ * Preact component tree data (if accessible)
61
+ * Used by Preact HMR to preserve hooks state
62
+ */
63
+ componentData?: unknown;
64
+
65
+ /**
66
+ * Component display name for debugging
67
+ */
68
+ componentName?: string;
69
+
70
+ /**
71
+ * Props at time of state capture
72
+ */
73
+ capturedProps?: Record<string, unknown>;
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Preact HMR Adapter
79
+ *
80
+ * Leverages Preact's HMR capabilities (provided by @preact/preset-vite) to:
81
+ * - Preserve hooks state across updates
82
+ * - Maintain component tree structure
83
+ * - Handle component updates without full remount
84
+ *
85
+ * Preact HMR works similarly to React Fast Refresh:
86
+ * 1. Detecting when a component module is updated
87
+ * 2. Preserving the component tree (internal state representation)
88
+ * 3. Re-rendering with the new component definition
89
+ * 4. Restoring hooks state from the preserved tree
90
+ */
91
+ export class PreactHMRAdapter extends BaseFrameworkAdapter {
92
+ readonly name = 'preact';
93
+
94
+ /**
95
+ * Store Preact component instances for each island to enable proper updates
96
+ */
97
+ private instances: WeakMap<HTMLElement, unknown> = new WeakMap();
98
+
99
+ /**
100
+ * Check if a component is a Preact component
101
+ *
102
+ * Preact components can be:
103
+ * - Function components (including hooks)
104
+ * - Class components (extending Preact Component)
105
+ * - Forward refs
106
+ * - Memoized components
107
+ *
108
+ * Preact is API-compatible with React, so detection is similar
109
+ */
110
+ canHandle(component: unknown): boolean {
111
+ if (!component) return false;
112
+
113
+ // Check if it's a function (function component or class)
114
+ if (typeof component === 'function') {
115
+ // Check for Preact/React-specific properties on the function
116
+ const comp = component as unknown as Record<string, unknown>;
117
+
118
+ // Class components have isReactComponent on prototype
119
+ // (Preact uses the same marker for compatibility)
120
+ const proto = (component as { prototype?: Record<string, unknown> }).prototype;
121
+ if (proto && proto.isReactComponent) {
122
+ return true;
123
+ }
124
+
125
+ // Check for Preact element symbol (for wrapped components)
126
+ if (comp.$typeof) {
127
+ return true;
128
+ }
129
+
130
+ // Assume any function could be a Preact component
131
+ // Preact HMR will handle validation
132
+ return true;
133
+ }
134
+
135
+ // Check for Preact VNode types
136
+ if (typeof component !== 'object') {
137
+ return false;
138
+ }
139
+
140
+ const obj = component as Record<string, unknown>;
141
+
142
+ // Check for Preact VNode symbol
143
+ if (obj.$typeof) {
144
+ return true;
145
+ }
146
+
147
+ // Check for wrapped components (HOCs, memo, forwardRef)
148
+ if (obj.type && typeof obj.type === 'function') {
149
+ return true;
150
+ }
151
+
152
+ return false;
153
+ }
154
+
155
+ /**
156
+ * Preserve Preact component state before HMR update
157
+ *
158
+ * Preact HMR handles most state preservation automatically through
159
+ * the component tree. We capture additional DOM state and props for fallback.
160
+ */
161
+ override preserveState(island: HTMLElement): PreactStateSnapshot | null {
162
+ try {
163
+ // Get base DOM state
164
+ const baseSnapshot = super.preserveState(island);
165
+ if (!baseSnapshot) return null;
166
+
167
+ // Get Preact-specific data
168
+ const propsAttr = island.getAttribute('data-props');
169
+ const capturedProps = propsAttr ? JSON.parse(propsAttr) : {};
170
+
171
+ // Try to get component name from the island
172
+ const src = island.getAttribute('data-src') || '';
173
+ const componentName = this.extractComponentName(src);
174
+
175
+ const preactSnapshot: PreactStateSnapshot = {
176
+ ...baseSnapshot,
177
+ framework: 'preact',
178
+ data: {
179
+ componentName,
180
+ capturedProps,
181
+ },
182
+ };
183
+
184
+ return preactSnapshot;
185
+ } catch (error) {
186
+ console.warn('Failed to preserve Preact state:', error);
187
+ return null;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Update Preact component with HMR
193
+ *
194
+ * This method integrates with Preact HMR:
195
+ * 1. Preact HMR is automatically enabled by @preact/preset-vite
196
+ * 2. When a module updates, Vite sends an HMR event
197
+ * 3. We re-hydrate the component with the new definition
198
+ * 4. Preact HMR preserves hooks state automatically
199
+ * 5. The component re-renders with preserved state
200
+ */
201
+ async update(
202
+ island: HTMLElement,
203
+ newComponent: unknown,
204
+ props: Record<string, unknown>
205
+ ): Promise<void> {
206
+ if (!this.canHandle(newComponent)) {
207
+ throw new Error('Component is not a valid Preact component');
208
+ }
209
+
210
+ const Component = newComponent as PreactComponent;
211
+
212
+ try {
213
+ // Dynamically import Preact at runtime
214
+ // This is resolved by Vite in the browser
215
+ const preactModule = await import('preact') as PreactModule;
216
+ const { h, hydrate } = preactModule;
217
+
218
+ // Check if we have an existing instance
219
+ const existingInstance = this.instances.get(island);
220
+
221
+ if (existingInstance) {
222
+ // Update existing component
223
+ // Preact HMR will preserve hooks state automatically
224
+ const vnode = h(Component, props);
225
+ hydrate(vnode, island);
226
+ } else {
227
+ // Create new instance and hydrate
228
+ // This happens on first HMR update or if instance was lost
229
+ const vnode = h(Component, props);
230
+ hydrate(vnode, island);
231
+
232
+ // Store instance for future updates
233
+ this.instances.set(island, Component);
234
+ }
235
+
236
+ // Mark as hydrated
237
+ island.setAttribute('data-hydrated', 'true');
238
+ island.setAttribute('data-hydration-status', 'success');
239
+
240
+ } catch (error) {
241
+ console.error('Preact HMR update failed:', error);
242
+ island.setAttribute('data-hydration-status', 'error');
243
+ throw error;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Restore Preact component state after HMR update
249
+ *
250
+ * Preact HMR handles most state restoration automatically.
251
+ * We restore DOM state (scroll, focus, form values) as a supplement.
252
+ */
253
+ override restoreState(island: HTMLElement, state: StateSnapshot): void {
254
+ try {
255
+ // Restore DOM state (scroll, focus, form values)
256
+ super.restoreState(island, state);
257
+
258
+ // Preact HMR handles hooks state restoration automatically
259
+ // through the component tree, so we don't need to do anything special here
260
+
261
+ } catch (error) {
262
+ console.warn('Failed to restore Preact state:', error);
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Handle errors during Preact HMR update
268
+ *
269
+ * Provides Preact-specific error handling with helpful messages
270
+ */
271
+ override handleError(island: HTMLElement, error: Error): void {
272
+ console.error('Preact HMR error:', error);
273
+
274
+ // Use base error handling
275
+ super.handleError(island, error);
276
+
277
+ // Add Preact-specific error information
278
+ const errorIndicator = island.querySelector('.hmr-error-indicator');
279
+ if (errorIndicator) {
280
+ const errorMessage = error.message;
281
+
282
+ // Provide helpful hints for common Preact errors
283
+ let hint = '';
284
+ if (errorMessage.includes('hooks')) {
285
+ hint = ' (Hint: Check hooks usage - hooks must be called in the same order)';
286
+ } else if (errorMessage.includes('render')) {
287
+ hint = ' (Hint: Check component render method for errors)';
288
+ } else if (errorMessage.includes('hydration') || errorMessage.includes('hydrate')) {
289
+ hint = ' (Hint: Server and client render must match)';
290
+ }
291
+
292
+ errorIndicator.textContent = `Preact HMR Error: ${errorMessage}${hint}`;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Extract component name from source path
298
+ * Used for debugging and error messages
299
+ */
300
+ private extractComponentName(src: string): string {
301
+ const parts = src.split('/');
302
+ const filename = parts[parts.length - 1];
303
+ return filename.replace(/\.(tsx?|jsx?)$/, '');
304
+ }
305
+
306
+ /**
307
+ * Clean up Preact component when island is removed
308
+ * This should be called when an island is unmounted
309
+ */
310
+ unmount(island: HTMLElement): void {
311
+ const instance = this.instances.get(island);
312
+ if (instance) {
313
+ try {
314
+ // Preact doesn't have an explicit unmount API like React
315
+ // We just clear the instance reference
316
+ this.instances.delete(island);
317
+
318
+ // Clear the island content to trigger cleanup
319
+ // Preact will handle component lifecycle cleanup
320
+ island.innerHTML = '';
321
+ } catch (error) {
322
+ console.warn('Failed to unmount Preact component:', error);
323
+ }
324
+ }
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Create and export a singleton instance of the Preact HMR adapter
330
+ */
331
+ export const preactAdapter = new PreactHMRAdapter();
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Qwik HMR Adapter
3
+ *
4
+ * Provides Hot Module Replacement support for Qwik components.
5
+ * Qwik is fundamentally different from other frameworks — it uses resumability
6
+ * instead of hydration. Components are serialized on the server and resumed
7
+ * on the client without replaying application logic.
8
+ *
9
+ * Key Qwik concepts:
10
+ * - Resumability: No hydration step — the app resumes from serialized state
11
+ * - Lazy loading via $: Code is split at $ boundaries and loaded on demand
12
+ * - Containers: Qwik apps run inside container elements with q:container attribute
13
+ * - Qwikloader: A tiny (~1KB) script that sets up global event listeners
14
+ *
15
+ * Requirements: 2.1
16
+ */
17
+
18
+ /// <reference lib="dom" />
19
+
20
+ import { BaseFrameworkAdapter, type StateSnapshot } from '../framework-adapter.ts';
21
+
22
+ /**
23
+ * Qwik component type
24
+ * Qwik components are created with component$() which returns a QRL-wrapped component
25
+ */
26
+ type QwikComponent<P = Record<string, unknown>> = {
27
+ (props: P): unknown;
28
+ __brand?: 'QwikComponent';
29
+ };
30
+
31
+ /**
32
+ * Qwik module interface for SSR/client rendering
33
+ */
34
+ interface QwikModule {
35
+ renderToString?: (opts: Record<string, unknown>) => Promise<string>;
36
+ render?: (parent: Element, jsxNode: unknown) => Promise<void>;
37
+ }
38
+
39
+ /**
40
+ * Qwik state snapshot extending base state
41
+ */
42
+ interface QwikStateSnapshot extends StateSnapshot {
43
+ framework: 'qwik';
44
+ data: {
45
+ componentName: string;
46
+ capturedProps: Record<string, unknown>;
47
+ containerState: string | null;
48
+ qContainerAttrs: Record<string, string>;
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Qwik HMR Adapter
54
+ *
55
+ * Unlike other frameworks, Qwik doesn't hydrate — it resumes. This adapter
56
+ * handles HMR by re-rendering the container rather than performing traditional
57
+ * hydration-based hot updates.
58
+ *
59
+ * During development, Qwik's optimizer (via vite-plugin-qwik) handles most
60
+ * of the HMR heavy lifting. This adapter provides the island-level integration
61
+ * for Avalon's HMR coordinator.
62
+ */
63
+ export class QwikHMRAdapter extends BaseFrameworkAdapter {
64
+ readonly name = 'qwik';
65
+
66
+ /**
67
+ * Track container elements for cleanup
68
+ */
69
+ private containers: WeakMap<HTMLElement, { cleanup?: () => void }> = new WeakMap();
70
+
71
+ /**
72
+ * Check if a component is a Qwik component
73
+ *
74
+ * Qwik components are created with component$() and have distinctive markers:
75
+ * - __brand or __qrl property from the Qwik optimizer
76
+ * - $ suffix convention in source
77
+ * - QRL (Qwik Resource Locator) references
78
+ */
79
+ canHandle(component: unknown): boolean {
80
+ if (!component) return false;
81
+
82
+ // Check if it's a function (Qwik components are functions)
83
+ if (typeof component === 'function') {
84
+ const comp = component as unknown as Record<string, unknown>;
85
+
86
+ // Check for Qwik-specific markers set by the optimizer
87
+ if (comp.__brand === 'QwikComponent' || comp.__qrl) {
88
+ return true;
89
+ }
90
+
91
+ // Check for QRL wrapper pattern
92
+ if (comp.getSymbol || comp.getHash) {
93
+ return true;
94
+ }
95
+
96
+ try {
97
+ const funcStr = component.toString();
98
+
99
+ // Look for Qwik-specific compiled patterns
100
+ if (
101
+ funcStr.includes('component$') ||
102
+ funcStr.includes('qrl') ||
103
+ funcStr.includes('useSignal') ||
104
+ funcStr.includes('useStore') ||
105
+ funcStr.includes('useTask$') ||
106
+ funcStr.includes('useVisibleTask$') ||
107
+ funcStr.includes('_qrl') ||
108
+ funcStr.includes('qwik')
109
+ ) {
110
+ return true;
111
+ }
112
+ } catch {
113
+ // Ignore errors from toString()
114
+ }
115
+ }
116
+
117
+ // Check if it's a Qwik component object (wrapped or exported)
118
+ if (typeof component === 'object' && component !== null) {
119
+ const obj = component as Record<string, unknown>;
120
+
121
+ // Check for default export pattern
122
+ if (obj.default && typeof obj.default === 'function') {
123
+ return this.canHandle(obj.default);
124
+ }
125
+
126
+ // Check for Qwik QRL markers
127
+ if (obj.__qrl || obj.__brand === 'QwikComponent') {
128
+ return true;
129
+ }
130
+ }
131
+
132
+ return false;
133
+ }
134
+
135
+ /**
136
+ * Preserve Qwik component state before HMR update
137
+ *
138
+ * Qwik serializes state into the DOM via q:container attributes and
139
+ * inline <script type="qwik/json"> blocks. We capture this serialized
140
+ * state so it can be used during re-rendering.
141
+ */
142
+ override preserveState(island: HTMLElement): QwikStateSnapshot | null {
143
+ try {
144
+ const baseSnapshot = super.preserveState(island);
145
+ if (!baseSnapshot) return null;
146
+
147
+ // Get props from the island
148
+ const propsAttr = island.getAttribute('data-props');
149
+ const capturedProps = propsAttr ? JSON.parse(propsAttr) : {};
150
+
151
+ const src = island.getAttribute('data-src') || '';
152
+ const componentName = this.extractComponentName(src);
153
+
154
+ // Capture Qwik container state — serialized in the DOM
155
+ const containerEl = island.closest('[q\\:container]') || island;
156
+ const containerState = containerEl.querySelector('script[type="qwik/json"]')?.textContent || null;
157
+
158
+ // Capture q: attributes that hold container metadata
159
+ const qContainerAttrs: Record<string, string> = {};
160
+ const attrs = containerEl.attributes;
161
+ if (attrs) {
162
+ for (let i = 0; i < attrs.length; i++) {
163
+ const attr = attrs[i];
164
+ if (attr.name.startsWith('q:')) {
165
+ qContainerAttrs[attr.name] = attr.value;
166
+ }
167
+ }
168
+ }
169
+
170
+ return {
171
+ ...baseSnapshot,
172
+ framework: 'qwik',
173
+ data: {
174
+ componentName,
175
+ capturedProps,
176
+ containerState,
177
+ qContainerAttrs,
178
+ },
179
+ };
180
+ } catch (error) {
181
+ console.warn('Failed to preserve Qwik state:', error);
182
+ return null;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Update Qwik component with HMR
188
+ *
189
+ * Qwik's resumability model means we don't hydrate in the traditional sense.
190
+ * Instead, during HMR:
191
+ * 1. The Qwik optimizer (vite-plugin-qwik) invalidates the affected QRLs
192
+ * 2. We re-render the component into the island container
193
+ * 3. Qwikloader picks up the new event bindings automatically
194
+ * 4. Serialized state is restored from the container
195
+ */
196
+ async update(
197
+ island: HTMLElement,
198
+ newComponent: unknown,
199
+ props: Record<string, unknown>
200
+ ): Promise<void> {
201
+ if (!this.canHandle(newComponent)) {
202
+ throw new Error('Component is not a valid Qwik component');
203
+ }
204
+
205
+ // Extract the actual component function
206
+ let Component: QwikComponent;
207
+ if (typeof newComponent === 'object' && newComponent !== null) {
208
+ const obj = newComponent as Record<string, unknown>;
209
+ if (obj.default && typeof obj.default === 'function') {
210
+ Component = obj.default as QwikComponent;
211
+ } else {
212
+ throw new Error('Qwik component object must have a default export');
213
+ }
214
+ } else if (typeof newComponent === 'function') {
215
+ Component = newComponent as QwikComponent;
216
+ } else {
217
+ throw new Error('Invalid Qwik component type');
218
+ }
219
+
220
+ try {
221
+ // Clean up existing container tracking
222
+ const existing = this.containers.get(island);
223
+ if (existing?.cleanup) {
224
+ try {
225
+ existing.cleanup();
226
+ } catch (error) {
227
+ console.warn('Failed to clean up existing Qwik container:', error);
228
+ }
229
+ }
230
+
231
+ // Dynamically import Qwik's client render API
232
+ // In development, vite-plugin-qwik makes this available
233
+ // Use a variable to prevent Vite's static import analysis from resolving this at build time
234
+ const qwikId = '@builder.io/qwik';
235
+ const qwikModule = await import(/* @vite-ignore */ qwikId) as QwikModule & Record<string, unknown>;
236
+
237
+ if (qwikModule.render) {
238
+ // Use Qwik's client-side render to mount the component
239
+ // Qwik's render handles setting up the container and resumability
240
+ const jsxNode = typeof qwikModule.jsx === 'function'
241
+ ? qwikModule.jsx(Component, props)
242
+ : Component(props);
243
+
244
+ await qwikModule.render(island, jsxNode);
245
+ } else {
246
+ // Fallback: replace innerHTML and let Qwikloader resume
247
+ // This works because Qwik's event listeners are declarative in the DOM
248
+ console.warn('Qwik render API not available, using innerHTML fallback');
249
+ const result = Component(props);
250
+ if (typeof result === 'string') {
251
+ island.innerHTML = result;
252
+ }
253
+ }
254
+
255
+ // Track the container for future cleanup
256
+ this.containers.set(island, {});
257
+
258
+ // Mark as hydrated (resumed, in Qwik terms)
259
+ island.setAttribute('data-hydrated', 'true');
260
+ island.setAttribute('data-hydration-status', 'success');
261
+
262
+ } catch (error) {
263
+ console.error('Qwik HMR update failed:', error);
264
+ island.setAttribute('data-hydration-status', 'error');
265
+ throw error;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Restore Qwik component state after HMR update
271
+ *
272
+ * Qwik's serialized state lives in the DOM, so restoring the container
273
+ * state script block and q: attributes is sufficient for the framework
274
+ * to resume correctly.
275
+ */
276
+ override restoreState(island: HTMLElement, state: StateSnapshot): void {
277
+ try {
278
+ // Restore DOM state (scroll, focus, form values)
279
+ super.restoreState(island, state);
280
+
281
+ // Qwik's resumability handles reactive state automatically
282
+ // through its serialized container state in the DOM
283
+
284
+ } catch (error) {
285
+ console.warn('Failed to restore Qwik state:', error);
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Handle errors during Qwik HMR update
291
+ */
292
+ override handleError(island: HTMLElement, error: Error): void {
293
+ console.error('Qwik HMR error:', error);
294
+
295
+ super.handleError(island, error);
296
+
297
+ const errorIndicator = island.querySelector('.hmr-error-indicator');
298
+ if (errorIndicator) {
299
+ const errorMessage = error.message;
300
+
301
+ let hint = '';
302
+ if (errorMessage.includes('component$') || errorMessage.includes('component\\$')) {
303
+ hint = ' (Hint: Ensure component is wrapped with component$())';
304
+ } else if (errorMessage.includes('useSignal') || errorMessage.includes('useStore')) {
305
+ hint = ' (Hint: Qwik hooks must be called inside component$() body)';
306
+ } else if (errorMessage.includes('QRL') || errorMessage.includes('qrl')) {
307
+ hint = ' (Hint: Check that lazy-loaded boundaries use $ correctly)';
308
+ } else if (errorMessage.includes('serialize') || errorMessage.includes('container')) {
309
+ hint = ' (Hint: Ensure all state is serializable — Qwik serializes state to the DOM)';
310
+ } else if (errorMessage.includes('resumable') || errorMessage.includes('resume')) {
311
+ hint = ' (Hint: Server and client container state must match for resumability)';
312
+ }
313
+
314
+ errorIndicator.textContent = `Qwik HMR Error: ${errorMessage}${hint}`;
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Extract component name from source path
320
+ */
321
+ private extractComponentName(src: string): string {
322
+ const parts = src.split('/');
323
+ const filename = parts[parts.length - 1];
324
+ return filename.replace(/\.qwik\.(tsx?|jsx?)$/, '').replace(/\.(tsx?|jsx?)$/, '');
325
+ }
326
+
327
+ /**
328
+ * Clean up Qwik component when island is removed
329
+ */
330
+ unmount(island: HTMLElement): void {
331
+ const container = this.containers.get(island);
332
+ if (container) {
333
+ try {
334
+ if (container.cleanup) {
335
+ container.cleanup();
336
+ }
337
+ this.containers.delete(island);
338
+ } catch (error) {
339
+ console.warn('Failed to unmount Qwik component:', error);
340
+ }
341
+ }
342
+ }
343
+ }
344
+
345
+ export const qwikAdapter = new QwikHMRAdapter();