@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useavalon/avalon",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Multi-framework islands architecture for the modern web",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,6 +11,10 @@
11
11
  },
12
12
  "homepage": "https://useavalon.dev",
13
13
  "keywords": ["avalon", "islands", "ssr", "vite", "preact", "multi-framework", "hydration"],
14
+ "scripts": {
15
+ "build:client": "bun run scripts/build-client.ts",
16
+ "prepublishOnly": "bun run build:client"
17
+ },
14
18
  "exports": {
15
19
  ".": "./mod.ts",
16
20
  "./client": "./src/client/components.ts",
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Framework HMR Adapters
3
+ *
4
+ * Exports all framework-specific HMR adapters for easy registration
5
+ */
6
+ export { ReactHMRAdapter, reactAdapter } from "./react-adapter.js";
7
+ export { PreactHMRAdapter, preactAdapter } from "./preact-adapter.js";
8
+ export { VueHMRAdapter, vueAdapter } from "./vue-adapter.js";
9
+ export { SvelteHMRAdapter, svelteAdapter } from "./svelte-adapter.js";
10
+ export { SolidHMRAdapter, solidAdapter } from "./solid-adapter.js";
11
+ export { LitHMRAdapter, litAdapter } from "./lit-adapter.js";
12
+ export { QwikHMRAdapter, qwikAdapter } from "./qwik-adapter.js";
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Lit HMR Adapter
3
+ *
4
+ * Provides Hot Module Replacement support for Lit components.
5
+ * Handles custom element re-registration and updates all element instances.
6
+ * Preserves element properties across updates.
7
+ *
8
+ * Requirements: 2.6
9
+ */
10
+ /// <reference lib="dom" />
11
+ import { BaseFrameworkAdapter } from "../framework-adapter.js";
12
+ /**
13
+ * Lit HMR Adapter
14
+ *
15
+ * Handles HMR for Lit components by:
16
+ * 1. Re-registering the custom element with a new definition
17
+ * 2. Updating all existing instances of the element
18
+ * 3. Preserving element properties and attributes
19
+ *
20
+ * Lit components are Web Components (custom elements), so HMR requires:
21
+ * - Undefining the old custom element (if possible)
22
+ * - Defining the new custom element
23
+ * - Updating all instances in the DOM
24
+ * - Preserving reactive properties
25
+ */
26
+ export class LitHMRAdapter extends BaseFrameworkAdapter {
27
+ name = "lit";
28
+ /**
29
+ * Store element constructors for each island
30
+ */
31
+ elementConstructors = new WeakMap();
32
+ /**
33
+ * Store tag names for each island
34
+ */
35
+ tagNames = new WeakMap();
36
+ /**
37
+ * Check if a component is a Lit component
38
+ *
39
+ * Lit components are:
40
+ * - Classes that extend LitElement
41
+ * - Typically decorated with @customElement
42
+ * - Have .lit.ts or .lit.js extension (but not always)
43
+ */
44
+ canHandle(component) {
45
+ if (!component) return false;
46
+ // Check if it's a class (Lit components are classes)
47
+ if (typeof component === "function") {
48
+ const comp = component;
49
+ // Check for Lit-specific markers
50
+ // Lit elements may have __litElement marker
51
+ if (comp.__litElement) {
52
+ return true;
53
+ }
54
+ // Check if it extends LitElement by looking at the prototype chain
55
+ try {
56
+ const proto = component.prototype;
57
+ if (proto) {
58
+ // Check for Lit-specific methods on prototype
59
+ if ("render" in proto && "requestUpdate" in proto && "updateComplete" in proto) {
60
+ return true;
61
+ }
62
+ // Check for LitElement in prototype chain
63
+ let currentProto = proto;
64
+ while (currentProto && currentProto !== Object.prototype) {
65
+ const constructor = currentProto.constructor;
66
+ if (constructor && constructor.name === "LitElement") {
67
+ return true;
68
+ }
69
+ currentProto = Object.getPrototypeOf(currentProto);
70
+ }
71
+ }
72
+ } catch {}
73
+ // Check for @customElement decorator metadata
74
+ // The decorator adds metadata to the class
75
+ if (comp.elementName || comp.tagName) {
76
+ return true;
77
+ }
78
+ // Check function signature - Lit components typically have specific patterns
79
+ try {
80
+ const funcStr = component.toString();
81
+ // Look for Lit-specific patterns
82
+ if (funcStr.includes("LitElement") || funcStr.includes("customElement") || funcStr.includes("html`") || funcStr.includes("css`") || funcStr.includes("render()") || funcStr.includes("requestUpdate")) {
83
+ return true;
84
+ }
85
+ } catch {}
86
+ }
87
+ // Check if it's a Lit component object (wrapped or exported)
88
+ if (typeof component !== "object") {
89
+ return false;
90
+ }
91
+ const obj = component;
92
+ // Check for default export pattern
93
+ if (obj.default && typeof obj.default === "function") {
94
+ return this.canHandle(obj.default);
95
+ }
96
+ // Check for Lit component markers
97
+ if (obj.__litElement) {
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+ /**
103
+ * Preserve Lit element state before HMR update
104
+ *
105
+ * Captures:
106
+ * - Element properties (reactive properties defined with @property)
107
+ * - Element attributes
108
+ * - DOM state (scroll, focus, form values)
109
+ */
110
+ preserveState(island) {
111
+ try {
112
+ // Get base DOM state
113
+ const baseSnapshot = super.preserveState(island);
114
+ if (!baseSnapshot) return null;
115
+ // Get Lit-specific data
116
+ const propsAttr = island.getAttribute("data-props");
117
+ const capturedProps = propsAttr ? JSON.parse(propsAttr) : {};
118
+ // Try to get component name from the island
119
+ const src = island.getAttribute("data-src") || "";
120
+ const componentName = this.extractComponentName(src);
121
+ // Get tag name
122
+ const tagName = island.getAttribute("data-tag-name") || this.tagNames.get(island);
123
+ // Find the Lit element inside the island
124
+ const litElement = tagName ? island.querySelector(tagName) : island.querySelector("[data-lit-element]");
125
+ // Capture element properties and attributes
126
+ const elementProperties = {};
127
+ const elementAttributes = {};
128
+ if (litElement) {
129
+ // Capture all properties
130
+ // Lit stores reactive properties on the element instance
131
+ for (const key in litElement) {
132
+ if (litElement.hasOwnProperty(key) && !key.startsWith("_")) {
133
+ try {
134
+ const value = litElement[key];
135
+ // Only capture serializable values
136
+ if (value !== undefined && value !== null && typeof value !== "function" && typeof value !== "symbol") {
137
+ elementProperties[key] = value;
138
+ }
139
+ } catch {}
140
+ }
141
+ }
142
+ // Capture all attributes
143
+ for (let i = 0; i < litElement.attributes.length; i++) {
144
+ const attr = litElement.attributes[i];
145
+ elementAttributes[attr.name] = attr.value;
146
+ }
147
+ }
148
+ const litSnapshot = {
149
+ ...baseSnapshot,
150
+ framework: "lit",
151
+ data: {
152
+ componentName,
153
+ tagName: tagName || undefined,
154
+ capturedProps,
155
+ elementProperties,
156
+ elementAttributes
157
+ }
158
+ };
159
+ return litSnapshot;
160
+ } catch (error) {
161
+ console.warn("Failed to preserve Lit state:", error);
162
+ return null;
163
+ }
164
+ }
165
+ /**
166
+ * Update Lit component with HMR
167
+ *
168
+ * This method handles custom element re-registration:
169
+ * 1. Extract the tag name from the component or island
170
+ * 2. Find all instances of the element in the DOM
171
+ * 3. Preserve their properties and attributes
172
+ * 4. Undefine the old custom element (if possible)
173
+ * 5. Define the new custom element
174
+ * 6. Update all instances with the new definition
175
+ * 7. Restore properties and attributes
176
+ */
177
+ async update(island, newComponent, props) {
178
+ if (!this.canHandle(newComponent)) {
179
+ throw new Error("Component is not a valid Lit component");
180
+ }
181
+ // Extract the actual component class
182
+ let ElementClass;
183
+ if (typeof newComponent === "object" && newComponent !== null) {
184
+ const obj = newComponent;
185
+ if (obj.default && typeof obj.default === "function") {
186
+ ElementClass = obj.default;
187
+ } else {
188
+ throw new Error("Lit component object must have a default export");
189
+ }
190
+ } else if (typeof newComponent === "function") {
191
+ ElementClass = newComponent;
192
+ } else {
193
+ throw new Error("Invalid Lit component type");
194
+ }
195
+ try {
196
+ // Determine the tag name
197
+ // Priority: data-tag-name attribute > elementName property > derive from class name
198
+ let tagName = island.getAttribute("data-tag-name");
199
+ if (!tagName) {
200
+ // Try to get from the class
201
+ tagName = ElementClass.elementName;
202
+ }
203
+ if (!tagName) {
204
+ // Derive from class name (convert PascalCase to kebab-case)
205
+ tagName = ElementClass.name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
206
+ }
207
+ if (!tagName || !tagName.includes("-")) {
208
+ throw new Error("Invalid custom element tag name: " + tagName);
209
+ }
210
+ // Store tag name for future updates
211
+ this.tagNames.set(island, tagName);
212
+ island.setAttribute("data-tag-name", tagName);
213
+ // Find all instances of this element in the island
214
+ const elements = Array.from(island.querySelectorAll(tagName));
215
+ // If no elements exist, create one
216
+ if (elements.length === 0) {
217
+ // Check if the custom element is already defined
218
+ const existingDefinition = customElements.get(tagName);
219
+ if (!existingDefinition) {
220
+ // Define the custom element
221
+ customElements.define(tagName, ElementClass);
222
+ } else if (existingDefinition !== ElementClass) {
223
+ // Element is already defined with a different class
224
+ // We need to re-register it
225
+ await this.reregisterCustomElement(tagName, ElementClass);
226
+ }
227
+ // Create a new element
228
+ const newElement = document.createElement(tagName);
229
+ // Set properties from props
230
+ Object.entries(props).forEach(([key, value]) => {
231
+ try {
232
+ newElement[key] = value;
233
+ } catch (error) {
234
+ console.warn(`Failed to set property ${key} on Lit element:`, error);
235
+ }
236
+ });
237
+ // Append to island
238
+ island.appendChild(newElement);
239
+ // Store constructor
240
+ this.elementConstructors.set(island, ElementClass);
241
+ // Mark as hydrated
242
+ island.setAttribute("data-hydrated", "true");
243
+ island.setAttribute("data-hydration-status", "success");
244
+ return;
245
+ }
246
+ // Update existing elements
247
+ // First, check if we need to re-register the custom element
248
+ const existingDefinition = customElements.get(tagName);
249
+ if (existingDefinition && existingDefinition !== ElementClass) {
250
+ // Re-register the custom element with the new definition
251
+ await this.reregisterCustomElement(tagName, ElementClass);
252
+ // After re-registration, we need to replace all existing elements
253
+ // because they're instances of the old class
254
+ for (const oldElement of elements) {
255
+ // Preserve state
256
+ const properties = {};
257
+ const attributes = {};
258
+ // Capture properties
259
+ for (const key in oldElement) {
260
+ if (oldElement.hasOwnProperty(key) && !key.startsWith("_")) {
261
+ try {
262
+ const value = oldElement[key];
263
+ if (value !== undefined && value !== null && typeof value !== "function" && typeof value !== "symbol") {
264
+ properties[key] = value;
265
+ }
266
+ } catch {}
267
+ }
268
+ }
269
+ // Capture attributes
270
+ for (let i = 0; i < oldElement.attributes.length; i++) {
271
+ const attr = oldElement.attributes[i];
272
+ attributes[attr.name] = attr.value;
273
+ }
274
+ // Create new element
275
+ const newElement = document.createElement(tagName);
276
+ // Restore attributes
277
+ Object.entries(attributes).forEach(([name, value]) => {
278
+ newElement.setAttribute(name, value);
279
+ });
280
+ // Restore properties
281
+ Object.entries(properties).forEach(([key, value]) => {
282
+ try {
283
+ newElement[key] = value;
284
+ } catch (error) {
285
+ console.warn(`Failed to restore property ${key}:`, error);
286
+ }
287
+ });
288
+ // Replace old element with new element
289
+ oldElement.parentNode?.replaceChild(newElement, oldElement);
290
+ }
291
+ } else if (!existingDefinition) {
292
+ // Define the custom element for the first time
293
+ customElements.define(tagName, ElementClass);
294
+ // Trigger update on all elements
295
+ for (const element of elements) {
296
+ if (element.requestUpdate) {
297
+ element.requestUpdate();
298
+ }
299
+ }
300
+ } else {
301
+ // Same class, just trigger updates
302
+ for (const element of elements) {
303
+ // Update properties from props
304
+ Object.entries(props).forEach(([key, value]) => {
305
+ try {
306
+ element[key] = value;
307
+ } catch (error) {
308
+ console.warn(`Failed to update property ${key}:`, error);
309
+ }
310
+ });
311
+ // Request update
312
+ if (element.requestUpdate) {
313
+ element.requestUpdate();
314
+ }
315
+ }
316
+ }
317
+ // Store constructor
318
+ this.elementConstructors.set(island, ElementClass);
319
+ // Mark as hydrated
320
+ island.setAttribute("data-hydrated", "true");
321
+ island.setAttribute("data-hydration-status", "success");
322
+ } catch (error) {
323
+ console.error("Lit HMR update failed:", error);
324
+ island.setAttribute("data-hydration-status", "error");
325
+ throw error;
326
+ }
327
+ }
328
+ /**
329
+ * Re-register a custom element with a new definition
330
+ *
331
+ * Custom elements cannot be undefined once defined, so we need to:
332
+ * 1. Create a new tag name (with a version suffix)
333
+ * 2. Define the new element with the new tag name
334
+ * 3. Update all references to use the new tag name
335
+ *
336
+ * Alternative approach (used here):
337
+ * 1. Use a wrapper element that delegates to the actual element
338
+ * 2. Update the wrapper to use the new element class
339
+ */
340
+ async reregisterCustomElement(tagName, ElementClass) {
341
+ // Unfortunately, custom elements cannot be truly undefined once defined
342
+ // The best we can do is define a new version with a suffix
343
+ // However, for HMR purposes, we can use a different approach:
344
+ // We'll just replace all instances of the old element with new instances
345
+ // This is handled in the update() method above
346
+ // For now, we'll just log a warning
347
+ console.warn(`Custom element ${tagName} is already defined. ` + `Replacing all instances with new definition.`);
348
+ // Note: In a production HMR system, you might want to:
349
+ // 1. Use a versioned tag name (e.g., my-element-v2)
350
+ // 2. Use a proxy element that delegates to the actual element
351
+ // 3. Use a custom element registry that supports re-registration
352
+ // For this implementation, we rely on replacing element instances
353
+ // which is handled in the update() method
354
+ }
355
+ /**
356
+ * Restore Lit element state after HMR update
357
+ *
358
+ * Restores:
359
+ * - Element properties
360
+ * - Element attributes
361
+ * - DOM state (scroll, focus, form values)
362
+ */
363
+ restoreState(island, state) {
364
+ try {
365
+ // Restore DOM state (scroll, focus, form values)
366
+ super.restoreState(island, state);
367
+ // Restore Lit-specific state
368
+ const litState = state;
369
+ const tagName = litState.data.tagName;
370
+ if (tagName) {
371
+ const litElement = island.querySelector(tagName);
372
+ if (litElement) {
373
+ // Restore element properties
374
+ if (litState.data.elementProperties) {
375
+ Object.entries(litState.data.elementProperties).forEach(([key, value]) => {
376
+ try {
377
+ litElement[key] = value;
378
+ } catch (error) {
379
+ console.warn(`Failed to restore property ${key}:`, error);
380
+ }
381
+ });
382
+ }
383
+ // Restore element attributes
384
+ if (litState.data.elementAttributes) {
385
+ Object.entries(litState.data.elementAttributes).forEach(([name, value]) => {
386
+ try {
387
+ litElement.setAttribute(name, value);
388
+ } catch (error) {
389
+ console.warn(`Failed to restore attribute ${name}:`, error);
390
+ }
391
+ });
392
+ }
393
+ // Request update to apply changes
394
+ if (litElement.requestUpdate) {
395
+ litElement.requestUpdate();
396
+ }
397
+ }
398
+ }
399
+ } catch (error) {
400
+ console.warn("Failed to restore Lit state:", error);
401
+ }
402
+ }
403
+ /**
404
+ * Handle errors during Lit HMR update
405
+ *
406
+ * Provides Lit-specific error handling with helpful messages
407
+ */
408
+ handleError(island, error) {
409
+ console.error("Lit HMR error:", error);
410
+ // Use base error handling
411
+ super.handleError(island, error);
412
+ // Add Lit-specific error information
413
+ const errorIndicator = island.querySelector(".hmr-error-indicator");
414
+ if (errorIndicator) {
415
+ const errorMessage = error.message;
416
+ // Provide helpful hints for common Lit errors
417
+ let hint = "";
418
+ if (errorMessage.includes("custom element") || errorMessage.includes("define")) {
419
+ hint = " (Hint: Check that your element has a valid tag name with a hyphen)";
420
+ } else if (errorMessage.includes("tag name")) {
421
+ hint = " (Hint: Custom element tag names must contain a hyphen)";
422
+ } else if (errorMessage.includes("property") || errorMessage.includes("attribute")) {
423
+ hint = " (Hint: Check @property decorators and attribute names)";
424
+ } else if (errorMessage.includes("render")) {
425
+ hint = " (Hint: Check the render() method for errors)";
426
+ } else if (errorMessage.includes("shadow")) {
427
+ hint = " (Hint: Check Shadow DOM usage and styles)";
428
+ }
429
+ errorIndicator.textContent = `Lit HMR Error: ${errorMessage}${hint}`;
430
+ }
431
+ }
432
+ /**
433
+ * Extract component name from source path
434
+ * Used for debugging and error messages
435
+ */
436
+ extractComponentName(src) {
437
+ const parts = src.split("/");
438
+ const filename = parts[parts.length - 1];
439
+ return filename.replace(/\.lit\.(ts|js)$/, "").replace(/\.(ts|js)$/, "");
440
+ }
441
+ /**
442
+ * Clean up Lit element when island is removed
443
+ * This should be called when an island is unmounted
444
+ */
445
+ unmount(island) {
446
+ try {
447
+ const tagName = this.tagNames.get(island);
448
+ if (tagName) {
449
+ // Find all elements and disconnect them
450
+ const elements = island.querySelectorAll(tagName);
451
+ elements.forEach((element) => {
452
+ // Lit elements clean up automatically when disconnected
453
+ // but we can help by removing them from the DOM
454
+ element.remove();
455
+ });
456
+ this.tagNames.delete(island);
457
+ }
458
+ this.elementConstructors.delete(island);
459
+ } catch (error) {
460
+ console.warn("Failed to unmount Lit element:", error);
461
+ }
462
+ }
463
+ }
464
+ /**
465
+ * Create and export a singleton instance of the Lit HMR adapter
466
+ */
467
+ export const litAdapter = new LitHMRAdapter();