@useavalon/vue 0.1.1 → 0.1.3

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 CHANGED
@@ -1,42 +1,42 @@
1
- # @useavalon/vue
2
-
3
- Vue 3 integration for [Avalon](https://useavalon.dev). Server-side rendering and client-side hydration for Vue Single File Components as islands.
4
-
5
- ## Features
6
-
7
- - Vue 3 with Composition API and `<script setup>`
8
- - Server-side rendering via `@vue/server-renderer`
9
- - Scoped CSS extraction from SFCs
10
- - All hydration strategies (`on:client`, `on:visible`, `on:idle`, `on:interaction`)
11
-
12
- ## Usage
13
-
14
- ```vue
15
- <!-- components/Counter.vue -->
16
- <script setup>
17
- import { ref } from 'vue';
18
- const count = ref(0);
19
- </script>
20
-
21
- <template>
22
- <button @click="count++">Count: {{ count }}</button>
23
- </template>
24
- ```
25
-
26
- ```tsx
27
- // pages/index.tsx
28
- import Counter from '../components/Counter.vue';
29
-
30
- export default function Home() {
31
- return <Counter island={{ condition: 'on:visible' }} />;
32
- }
33
- ```
34
-
35
- ## Links
36
-
37
- - [Documentation](https://useavalon.dev/docs/frameworks/vue)
38
- - [GitHub](https://github.com/useAvalon/Avalon)
39
-
40
- ## License
41
-
42
- MIT
1
+ # @useavalon/vue
2
+
3
+ Vue 3 integration for [Avalon](https://useavalon.dev). Server-side rendering and client-side hydration for Vue Single File Components as islands.
4
+
5
+ ## Features
6
+
7
+ - Vue 3 with Composition API and `<script setup>`
8
+ - Server-side rendering via `@vue/server-renderer`
9
+ - Scoped CSS extraction from SFCs
10
+ - All hydration strategies (`on:client`, `on:visible`, `on:idle`, `on:interaction`)
11
+
12
+ ## Usage
13
+
14
+ ```vue
15
+ <!-- components/Counter.vue -->
16
+ <script setup>
17
+ import { ref } from 'vue';
18
+ const count = ref(0);
19
+ </script>
20
+
21
+ <template>
22
+ <button @click="count++">Count: {{ count }}</button>
23
+ </template>
24
+ ```
25
+
26
+ ```tsx
27
+ // pages/index.tsx
28
+ import Counter from '../components/Counter.vue';
29
+
30
+ export default function Home() {
31
+ return <Counter island={{ condition: 'on:visible' }} />;
32
+ }
33
+ ```
34
+
35
+ ## Links
36
+
37
+ - [Documentation](https://useavalon.dev/docs/frameworks/vue)
38
+ - [GitHub](https://github.com/useAvalon/Avalon)
39
+
40
+ ## License
41
+
42
+ MIT
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Vue HMR Adapter
3
+ *
4
+ * Provides Hot Module Replacement support for Vue 3 components.
5
+ * Integrates with @vitejs/plugin-vue to preserve reactive state during updates.
6
+ * Uses Vue's __VUE_HMR_RUNTIME__ API for hot updates.
7
+ */
8
+
9
+ /// <reference lib="dom" />
10
+
11
+ import { BaseFrameworkAdapter, type StateSnapshot } from '@useavalon/avalon/client/hmr';
12
+
13
+ type VueComponent<P = Record<string, unknown>> = VueComponentOptions<P> | ((props: P) => unknown);
14
+
15
+ interface VueComponentOptions<P = Record<string, unknown>> {
16
+ name?: string;
17
+ props?: string[] | Record<string, unknown>;
18
+ data?: () => Record<string, unknown>;
19
+ setup?: (props: P, context: unknown) => unknown;
20
+ render?: () => unknown;
21
+ template?: string;
22
+ components?: Record<string, VueComponent>;
23
+ computed?: Record<string, () => unknown>;
24
+ methods?: Record<string, (...args: unknown[]) => unknown>;
25
+ watch?: Record<string, unknown>;
26
+ emits?: string[] | Record<string, unknown>;
27
+ expose?: string[];
28
+ beforeCreate?: () => void;
29
+ created?: () => void;
30
+ beforeMount?: () => void;
31
+ mounted?: () => void;
32
+ beforeUpdate?: () => void;
33
+ updated?: () => void;
34
+ beforeUnmount?: () => void;
35
+ unmounted?: () => void;
36
+ }
37
+
38
+ interface VueApp {
39
+ mount(rootContainer: HTMLElement | string, isHydrate?: boolean): unknown;
40
+ unmount(): void;
41
+ use(plugin: unknown, ...options: unknown[]): this;
42
+ component(name: string, component: VueComponent): this;
43
+ directive(name: string, directive: unknown): this;
44
+ provide(key: string | symbol, value: unknown): this;
45
+ config: {
46
+ errorHandler?: (err: Error, instance: unknown, info: string) => void;
47
+ warnHandler?: (msg: string, instance: unknown, trace: string) => void;
48
+ };
49
+ }
50
+
51
+ interface VueModule {
52
+ createApp(rootComponent: VueComponent, rootProps?: Record<string, unknown>): VueApp;
53
+ version: string;
54
+ }
55
+
56
+ interface VueHMRRuntime {
57
+ createRecord(id: string, component: VueComponent): boolean;
58
+ reload(id: string, component: VueComponent): void;
59
+ rerender(id: string, render: () => unknown): void;
60
+ }
61
+
62
+ declare global {
63
+ var __VUE_HMR_RUNTIME__: VueHMRRuntime | undefined;
64
+ }
65
+
66
+ interface VueStateSnapshot extends StateSnapshot {
67
+ framework: 'vue';
68
+ data: {
69
+ reactiveData?: Record<string, unknown>;
70
+ componentName?: string;
71
+ capturedProps?: Record<string, unknown>;
72
+ computedValues?: Record<string, unknown>;
73
+ };
74
+ }
75
+
76
+ export class VueHMRAdapter extends BaseFrameworkAdapter {
77
+ readonly name = 'vue';
78
+ private apps: WeakMap<HTMLElement, VueApp> = new WeakMap();
79
+ private componentIds: WeakMap<HTMLElement, string> = new WeakMap();
80
+
81
+ canHandle(component: unknown): boolean {
82
+ if (!component) return false;
83
+ if (typeof component === 'function') return true;
84
+ if (typeof component !== 'object') return false;
85
+ const obj = component as Record<string, unknown>;
86
+ if (
87
+ 'setup' in obj ||
88
+ 'data' in obj ||
89
+ 'render' in obj ||
90
+ 'template' in obj ||
91
+ 'props' in obj ||
92
+ 'computed' in obj ||
93
+ 'methods' in obj ||
94
+ 'components' in obj ||
95
+ 'emits' in obj ||
96
+ 'mounted' in obj ||
97
+ 'created' in obj ||
98
+ 'beforeMount' in obj ||
99
+ 'beforeCreate' in obj
100
+ )
101
+ return true;
102
+ if ('__vccOpts' in obj) return true;
103
+ return false;
104
+ }
105
+
106
+ override preserveState(island: HTMLElement): VueStateSnapshot | null {
107
+ try {
108
+ const baseSnapshot = super.preserveState(island);
109
+ if (!baseSnapshot) return null;
110
+ const capturedProps = island.dataset.props ? JSON.parse(island.dataset.props) : {};
111
+ const componentName = this.extractComponentName(island.dataset.src || '');
112
+ const reactiveData = this.captureReactiveData(island);
113
+ return { ...baseSnapshot, framework: 'vue', data: { componentName, capturedProps, reactiveData } };
114
+ } catch (error) {
115
+ console.warn('Failed to preserve Vue state:', error);
116
+ return null;
117
+ }
118
+ }
119
+
120
+ async update(island: HTMLElement, newComponent: unknown, props: Record<string, unknown>): Promise<void> {
121
+ if (!this.canHandle(newComponent)) throw new Error('Component is not a valid Vue component');
122
+ const Component = newComponent as VueComponent;
123
+ try {
124
+ const vueModule = (await import('vue')) as VueModule;
125
+ const { createApp } = vueModule;
126
+ const existingApp = this.apps.get(island);
127
+ const componentId = this.componentIds.get(island);
128
+ const hmrRuntime = globalThis.__VUE_HMR_RUNTIME__;
129
+ if (hmrRuntime && componentId) {
130
+ try {
131
+ hmrRuntime.reload(componentId, Component);
132
+ if (existingApp) return;
133
+ } catch (error) {
134
+ console.warn('Vue HMR runtime reload failed, falling back to full remount:', error);
135
+ }
136
+ }
137
+ if (existingApp) {
138
+ try {
139
+ existingApp.unmount();
140
+ } catch (error) {
141
+ console.warn('Failed to unmount existing Vue app:', error);
142
+ }
143
+ }
144
+ const app = createApp(Component, props);
145
+ app.config.errorHandler = (err: Error, _instance: unknown, info: string) => {
146
+ console.error('Vue component error during HMR:', err, info);
147
+ };
148
+ app.mount(island, true);
149
+ this.apps.set(island, app);
150
+ const src = island.dataset.src || '';
151
+ const newComponentId = this.generateComponentId(src);
152
+ this.componentIds.set(island, newComponentId);
153
+ if (hmrRuntime) hmrRuntime.createRecord(newComponentId, Component);
154
+ island.dataset.hydrated = 'true';
155
+ island.dataset.hydrationStatus = 'success';
156
+ } catch (error) {
157
+ console.error('Vue HMR update failed:', error);
158
+ island.dataset.hydrationStatus = 'error';
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ override restoreState(island: HTMLElement, state: StateSnapshot): void {
164
+ try {
165
+ super.restoreState(island, state);
166
+ } catch (error) {
167
+ console.warn('Failed to restore Vue state:', error);
168
+ }
169
+ }
170
+
171
+ override handleError(island: HTMLElement, error: Error): void {
172
+ console.error('Vue HMR error:', error);
173
+ super.handleError(island, error);
174
+ const errorIndicator = island.querySelector('.hmr-error-indicator');
175
+ if (errorIndicator) {
176
+ const msg = error.message;
177
+ let hint = '';
178
+ if (msg.includes('reactive') || msg.includes('ref'))
179
+ hint = ' (Hint: Check reactive state usage - refs must be accessed with .value)';
180
+ else if (msg.includes('render')) hint = ' (Hint: Check component render function or template for errors)';
181
+ else if (msg.includes('hydration') || msg.includes('mismatch'))
182
+ hint = ' (Hint: Server and client render must match)';
183
+ else if (msg.includes('setup'))
184
+ hint = ' (Hint: Check setup function - it should return render function or object)';
185
+ errorIndicator.textContent = `Vue HMR Error: ${msg}${hint}`;
186
+ }
187
+ }
188
+
189
+ private extractComponentName(src: string): string {
190
+ const parts = src.split('/');
191
+ const filename = parts.at(-1) ?? '';
192
+ return filename.replace(/\.(vue|tsx?|jsx?)$/, '');
193
+ }
194
+
195
+ private generateComponentId(src: string): string {
196
+ return src.replaceAll(/[^a-zA-Z0-9]/g, '_');
197
+ }
198
+
199
+ private captureReactiveData(island: HTMLElement): Record<string, unknown> | undefined {
200
+ try {
201
+ const vueInstance = (island as unknown as { __vueParentComponent?: unknown }).__vueParentComponent;
202
+ if (vueInstance && typeof vueInstance === 'object') {
203
+ const data = (vueInstance as { data?: Record<string, unknown> }).data;
204
+ if (data && typeof data === 'object') return { ...data };
205
+ }
206
+ } catch (error) {
207
+ console.debug('Could not capture Vue reactive data:', error);
208
+ }
209
+ return undefined;
210
+ }
211
+
212
+ unmount(island: HTMLElement): void {
213
+ const app = this.apps.get(island);
214
+ if (app) {
215
+ try {
216
+ app.unmount();
217
+ this.apps.delete(island);
218
+ this.componentIds.delete(island);
219
+ } catch (error) {
220
+ console.warn('Failed to unmount Vue app:', error);
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ export const vueAdapter = new VueHMRAdapter();
@@ -1,124 +1,124 @@
1
- /**
2
- * Vue Client Hydration
3
- *
4
- * Provides client-side hydration for Vue components.
5
- * Attaches interactivity to server-rendered Vue components.
6
- */
7
-
8
- import { createApp } from "vue";
9
- import type { Component } from "vue";
10
-
11
- /**
12
- * Hydrate a Vue component on the client
13
- *
14
- * Creates a Vue app instance and mounts it to the container element,
15
- * hydrating the server-rendered HTML.
16
- *
17
- * @param container - DOM element containing server-rendered HTML
18
- * @param component - Vue component to hydrate
19
- * @param props - Component props
20
- */
21
- export function hydrate(
22
- container: Element,
23
- component: unknown,
24
- props: Record<string, unknown> = {},
25
- ): void {
26
- try {
27
- // Create Vue app with the component and props
28
- // Type assertion needed because component is dynamically loaded
29
- const app = createApp(component as Component, props);
30
-
31
- // Mount and hydrate
32
- app.mount(container, true);
33
- } catch (error) {
34
- console.error("Vue hydration failed:", error);
35
- throw error;
36
- }
37
- }
38
-
39
- /**
40
- * Get the hydration script for Vue islands
41
- *
42
- * Returns a script that will be injected into the page to handle
43
- * automatic hydration of Vue islands based on their conditions.
44
- *
45
- * @returns Hydration script as a string
46
- */
47
- export function getHydrationScript(): string {
48
- return `
49
- import { createApp } from 'vue';
50
-
51
- // Auto-hydrate all Vue islands
52
- document.querySelectorAll('[data-framework="vue"]').forEach(async (el) => {
53
- const src = el.getAttribute('data-src');
54
- const propsJson = el.getAttribute('data-props') || '{}';
55
- const condition = el.getAttribute('data-condition') || 'on:client';
56
-
57
- // Check hydration condition
58
- if (!shouldHydrate(el, condition)) {
59
- return;
60
- }
61
-
62
- try {
63
- // Dynamic import the component
64
- const module = await import(src);
65
- const Component = module.default || module;
66
-
67
- // Parse props
68
- const props = JSON.parse(propsJson);
69
-
70
- // Create and mount Vue app
71
- const app = createApp(Component, props);
72
- app.mount(el, true);
73
- } catch (error) {
74
- console.error('Failed to hydrate Vue island:', error);
75
- }
76
- });
77
-
78
- function shouldHydrate(element, condition) {
79
- if (!condition || condition === 'on:client') {
80
- return true;
81
- }
82
-
83
- if (condition === 'on:visible') {
84
- return new Promise((resolve) => {
85
- const observer = new IntersectionObserver((entries) => {
86
- if (entries[0].isIntersecting) {
87
- observer.disconnect();
88
- resolve(true);
89
- }
90
- });
91
- observer.observe(element);
92
- });
93
- }
94
-
95
- if (condition === 'on:interaction') {
96
- return new Promise((resolve) => {
97
- const events = ['click', 'mouseenter', 'focusin', 'touchstart'];
98
- const handler = () => {
99
- events.forEach(e => element.removeEventListener(e, handler));
100
- resolve(true);
101
- };
102
- events.forEach(e => element.addEventListener(e, handler, { once: true }));
103
- });
104
- }
105
-
106
- if (condition === 'on:idle') {
107
- return new Promise((resolve) => {
108
- if ('requestIdleCallback' in window) {
109
- requestIdleCallback(() => resolve(true));
110
- } else {
111
- setTimeout(() => resolve(true), 200);
112
- }
113
- });
114
- }
115
-
116
- if (condition.startsWith('media:')) {
117
- const query = condition.slice(6);
118
- return window.matchMedia(query).matches;
119
- }
120
-
121
- return true;
122
- }
123
- `;
124
- }
1
+ /**
2
+ * Vue Client Hydration
3
+ *
4
+ * Provides client-side hydration for Vue components.
5
+ * Attaches interactivity to server-rendered Vue components.
6
+ */
7
+
8
+ import { createApp } from "vue";
9
+ import type { Component } from "vue";
10
+
11
+ /**
12
+ * Hydrate a Vue component on the client
13
+ *
14
+ * Creates a Vue app instance and mounts it to the container element,
15
+ * hydrating the server-rendered HTML.
16
+ *
17
+ * @param container - DOM element containing server-rendered HTML
18
+ * @param component - Vue component to hydrate
19
+ * @param props - Component props
20
+ */
21
+ export function hydrate(
22
+ container: Element,
23
+ component: unknown,
24
+ props: Record<string, unknown> = {},
25
+ ): void {
26
+ try {
27
+ // Create Vue app with the component and props
28
+ // Type assertion needed because component is dynamically loaded
29
+ const app = createApp(component as Component, props);
30
+
31
+ // Mount and hydrate
32
+ app.mount(container, true);
33
+ } catch (error) {
34
+ console.error("Vue hydration failed:", error);
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Get the hydration script for Vue islands
41
+ *
42
+ * Returns a script that will be injected into the page to handle
43
+ * automatic hydration of Vue islands based on their conditions.
44
+ *
45
+ * @returns Hydration script as a string
46
+ */
47
+ export function getHydrationScript(): string {
48
+ return `
49
+ import { createApp } from 'vue';
50
+
51
+ // Auto-hydrate all Vue islands
52
+ document.querySelectorAll('[data-framework="vue"]').forEach(async (el) => {
53
+ const src = el.getAttribute('data-src');
54
+ const propsJson = el.getAttribute('data-props') || '{}';
55
+ const condition = el.getAttribute('data-condition') || 'on:client';
56
+
57
+ // Check hydration condition
58
+ if (!shouldHydrate(el, condition)) {
59
+ return;
60
+ }
61
+
62
+ try {
63
+ // Dynamic import the component
64
+ const module = await import(src);
65
+ const Component = module.default || module;
66
+
67
+ // Parse props
68
+ const props = JSON.parse(propsJson);
69
+
70
+ // Create and mount Vue app
71
+ const app = createApp(Component, props);
72
+ app.mount(el, true);
73
+ } catch (error) {
74
+ console.error('Failed to hydrate Vue island:', error);
75
+ }
76
+ });
77
+
78
+ function shouldHydrate(element, condition) {
79
+ if (!condition || condition === 'on:client') {
80
+ return true;
81
+ }
82
+
83
+ if (condition === 'on:visible') {
84
+ return new Promise((resolve) => {
85
+ const observer = new IntersectionObserver((entries) => {
86
+ if (entries[0].isIntersecting) {
87
+ observer.disconnect();
88
+ resolve(true);
89
+ }
90
+ });
91
+ observer.observe(element);
92
+ });
93
+ }
94
+
95
+ if (condition === 'on:interaction') {
96
+ return new Promise((resolve) => {
97
+ const events = ['click', 'mouseenter', 'focusin', 'touchstart'];
98
+ const handler = () => {
99
+ events.forEach(e => element.removeEventListener(e, handler));
100
+ resolve(true);
101
+ };
102
+ events.forEach(e => element.addEventListener(e, handler, { once: true }));
103
+ });
104
+ }
105
+
106
+ if (condition === 'on:idle') {
107
+ return new Promise((resolve) => {
108
+ if ('requestIdleCallback' in window) {
109
+ requestIdleCallback(() => resolve(true));
110
+ } else {
111
+ setTimeout(() => resolve(true), 200);
112
+ }
113
+ });
114
+ }
115
+
116
+ if (condition.startsWith('media:')) {
117
+ const query = condition.slice(6);
118
+ return window.matchMedia(query).matches;
119
+ }
120
+
121
+ return true;
122
+ }
123
+ `;
124
+ }
package/client/index.ts CHANGED
@@ -1,7 +1,7 @@
1
- /**
2
- * Vue Integration Client Entrypoint
3
- *
4
- * Main export for client-side Vue integration functionality.
5
- */
6
-
7
- export { hydrate, getHydrationScript } from "./hydration.ts";
1
+ /**
2
+ * Vue Integration Client Entrypoint
3
+ *
4
+ * Main export for client-side Vue integration functionality.
5
+ */
6
+
7
+ export { hydrate, getHydrationScript } from "./hydration.ts";