@useavalon/vue 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ import{BaseFrameworkAdapter as e}from"@useavalon/avalon/client/hmr";export class VueHMRAdapter extends e{name=`vue`;apps=new WeakMap;componentIds=new WeakMap;canHandle(e){if(!e)return!1;if(typeof e==`function`)return!0;if(typeof e!=`object`)return!1;let t=e;return`setup`in t||`data`in t||`render`in t||`template`in t||`props`in t||`computed`in t||`methods`in t||`components`in t||`emits`in t||`mounted`in t||`created`in t||`beforeMount`in t||`beforeCreate`in t||`__vccOpts`in t}preserveState(e){try{let t=super.preserveState(e);if(!t)return null;let n=e.dataset.props?JSON.parse(e.dataset.props):{},r=this.extractComponentName(e.dataset.src||``),i=this.captureReactiveData(e);return{...t,framework:`vue`,data:{componentName:r,capturedProps:n,reactiveData:i}}}catch(e){return console.warn(`Failed to preserve Vue state:`,e),null}}async update(e,t,n){if(!this.canHandle(t))throw Error(`Component is not a valid Vue component`);let r=t;try{let{createApp:t}=await import(`vue`),i=this.apps.get(e),a=this.componentIds.get(e),o=globalThis.__VUE_HMR_RUNTIME__;if(o&&a)try{if(o.reload(a,r),i)return}catch(e){console.warn(`Vue HMR runtime reload failed, falling back to full remount:`,e)}if(i)try{i.unmount()}catch(e){console.warn(`Failed to unmount existing Vue app:`,e)}let s=t(r,n);s.config.errorHandler=(e,t,n)=>{console.error(`Vue component error during HMR:`,e,n)},s.mount(e,!0),this.apps.set(e,s);let c=e.dataset.src||``,l=this.generateComponentId(c);this.componentIds.set(e,l),o&&o.createRecord(l,r),e.dataset.hydrated=`true`,e.dataset.hydrationStatus=`success`}catch(t){throw console.error(`Vue HMR update failed:`,t),e.dataset.hydrationStatus=`error`,t}}restoreState(e,t){try{super.restoreState(e,t)}catch(e){console.warn(`Failed to restore Vue state:`,e)}}handleError(e,t){console.error(`Vue HMR error:`,t),super.handleError(e,t);let n=e.querySelector(`.hmr-error-indicator`);if(n){let e=t.message,r=``;e.includes(`reactive`)||e.includes(`ref`)?r=` (Hint: Check reactive state usage - refs must be accessed with .value)`:e.includes(`render`)?r=` (Hint: Check component render function or template for errors)`:e.includes(`hydration`)||e.includes(`mismatch`)?r=` (Hint: Server and client render must match)`:e.includes(`setup`)&&(r=` (Hint: Check setup function - it should return render function or object)`),n.textContent=`Vue HMR Error: ${e}${r}`}}extractComponentName(e){return(e.split(`/`).at(-1)??``).replace(/\.(vue|tsx?|jsx?)$/,``)}generateComponentId(e){return e.replaceAll(/[^a-zA-Z0-9]/g,`_`)}captureReactiveData(e){try{let t=e.__vueParentComponent;if(t&&typeof t==`object`){let e=t.data;if(e&&typeof e==`object`)return{...e}}}catch(e){console.debug(`Could not capture Vue reactive data:`,e)}}unmount(e){let t=this.apps.get(e);if(t)try{t.unmount(),this.apps.delete(e),this.componentIds.delete(e)}catch(e){console.warn(`Failed to unmount Vue app:`,e)}}}export const vueAdapter=new VueHMRAdapter;
@@ -1,124 +1,76 @@
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
+ import{createApp as e}from"vue";export function hydrate(t,n,r={}){try{e(n,r).mount(t,!0)}catch(e){throw console.error(`Vue hydration failed:`,e),e}}export function getHydrationScript(){return`
2
+ import { createApp } from 'vue';
3
+
4
+ // Auto-hydrate all Vue islands
5
+ document.querySelectorAll('[data-framework="vue"]').forEach(async (el) => {
6
+ const src = el.getAttribute('data-src');
7
+ const propsJson = el.getAttribute('data-props') || '{}';
8
+ const condition = el.getAttribute('data-condition') || 'on:client';
9
+
10
+ // Check hydration condition
11
+ if (!shouldHydrate(el, condition)) {
12
+ return;
13
+ }
14
+
15
+ try {
16
+ // Dynamic import the component
17
+ const module = await import(src);
18
+ const Component = module.default || module;
19
+
20
+ // Parse props
21
+ const props = JSON.parse(propsJson);
22
+
23
+ // Create and mount Vue app
24
+ const app = createApp(Component, props);
25
+ app.mount(el, true);
26
+ } catch (error) {
27
+ console.error('Failed to hydrate Vue island:', error);
28
+ }
29
+ });
30
+
31
+ function shouldHydrate(element, condition) {
32
+ if (!condition || condition === 'on:client') {
33
+ return true;
34
+ }
35
+
36
+ if (condition === 'on:visible') {
37
+ return new Promise((resolve) => {
38
+ const observer = new IntersectionObserver((entries) => {
39
+ if (entries[0].isIntersecting) {
40
+ observer.disconnect();
41
+ resolve(true);
42
+ }
43
+ });
44
+ observer.observe(element);
45
+ });
46
+ }
47
+
48
+ if (condition === 'on:interaction') {
49
+ return new Promise((resolve) => {
50
+ const events = ['click', 'mouseenter', 'focusin', 'touchstart'];
51
+ const handler = () => {
52
+ events.forEach(e => element.removeEventListener(e, handler));
53
+ resolve(true);
54
+ };
55
+ events.forEach(e => element.addEventListener(e, handler, { once: true }));
56
+ });
57
+ }
58
+
59
+ if (condition === 'on:idle') {
60
+ return new Promise((resolve) => {
61
+ if ('requestIdleCallback' in window) {
62
+ requestIdleCallback(() => resolve(true));
63
+ } else {
64
+ setTimeout(() => resolve(true), 200);
65
+ }
66
+ });
67
+ }
68
+
69
+ if (condition.startsWith('media:')) {
70
+ const query = condition.slice(6);
71
+ return window.matchMedia(query).matches;
72
+ }
73
+
74
+ return true;
75
+ }
76
+ `}
@@ -0,0 +1 @@
1
+ export{hydrate,getHydrationScript}from"./hydration.js";
package/dist/mod.js ADDED
@@ -0,0 +1 @@
1
+ import{render as e}from"./server/renderer.js";import{getHydrationScript as t}from"./client/hydration.js";export const vueIntegration={name:`vue`,version:`0.1.0`,render:e,getHydrationScript:t,config(){return{name:`vue`,fileExtensions:[`.vue`],jsxImportSources:[],detectionPatterns:{imports:[/^vue$/,/^vue\//,/from\s+['"]vue['"]/],content:[/<template>/,/<script.*setup>/,/\bdefineComponent\b/,/\bref\b/,/\breactive\b/,/\bcomputed\b/]}}},async vitePlugin(){let{default:e}=await import(`@vitejs/plugin-vue`);return e({template:{compilerOptions:{isCustomElement:e=>e===`avalon-island`}}})}};export{render}from"./server/renderer.js";export{hydrate,getHydrationScript}from"./client/hydration.js";export{extractCSS,applyScopedCSS,generateScopeId}from"./server/css-extractor.js";
@@ -0,0 +1 @@
1
+ import{readFile as e}from"node:fs/promises";export async function extractCSS(t,a={}){let o=[t.startsWith(`/`)?`src${t}`:t,t.replace(`/islands/`,`/src/islands/`),t.startsWith(`/`)?t.substring(1):t],s=``;for(let t of o)try{s=await e(t,`utf-8`);break}catch{continue}if(!s)throw Error(`Vue file not found in any of the attempted paths: ${o.join(`, `)}`);let c=r(s);if(c.length===0)return``;let l=a.scopeId||generateScopeId(t),u=``;for(let e of c)e.scoped?u+=applyScopedCSS(e.content,l):u+=e.content;return u}export function applyScopedCSS(e,t){return e.replace(/([^{}]+){/g,(e,n)=>{let r=n.trim();return r.startsWith(`@`)?e:`${r.split(`,`).map(e=>`${e.trim()}[${t}]`).join(`, `)} {`})}function r(e){let t=/<style([^>]*)>([\s\S]*?)<\/style>/gi,n=[],r;for(;(r=t.exec(e))!==null;){let e=r[1],t=r[2].trim(),i=e.includes(`scoped`);n.push({content:t,scoped:i,attributes:e})}return n}export function generateScopeId(e){return`data-v-${e.replace(/[^a-zA-Z0-9]/g,``).toLowerCase()}`}export function applyScopeToHTML(e,t){return e.replace(/<([a-zA-Z][^>]*?)>/g,(e,n)=>n.startsWith(`/`)||n.endsWith(`/`)?e:`<${n} ${t}>`)}
@@ -0,0 +1 @@
1
+ import{createSSRApp as e}from"vue";import{renderToString as t}from"vue/server-renderer";import{extractCSS as n,generateScopeId as r,applyScopeToHTML as i}from"./css-extractor.js";import{toImportSpecifier as a}from"@useavalon/core/utils";import{resolveIslandPath as o}from"@useavalon/avalon/islands/framework-detection";export async function render(a){let{component:o,props:s={},src:l,condition:u=`on:client`,ssrOnly:d=!1}=a;try{let a=await t(e(await c(l),s)),o=``,f=``;try{f=r(l),o=await n(l,{scopeId:f})}catch{}let p=a;return o&&(p=i(a,f)),{html:p,css:o||void 0,scopeId:f||void 0,hydrationData:{src:l,props:s,framework:`vue`,condition:u,ssrOnly:d}}}catch(e){throw Error(`Vue SSR rendering failed: ${e}`)}}async function c(e){if(process.env.NODE_ENV!==`production`&&globalThis.__viteDevServer){let t=globalThis.__viteDevServer,n=await o(e),r=await t.ssrLoadModule(n);return r.default||r}let t=await import(a(e.replace(`/islands/`,`/dist/ssr/islands/`).replace(`.vue`,`.js`)));return t.default||t}export function getComponentMetadata(e){return typeof e==`object`&&e?{name:e.name||`Anonymous`,type:`component`,hasSetup:`setup`in e,hasTemplate:`template`in e,hasRender:`render`in e}:{type:typeof e}}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export{};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useavalon/vue",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Vue integration for Avalon islands architecture",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,22 +18,23 @@
18
18
  "hydration"
19
19
  ],
20
20
  "exports": {
21
- ".": "./mod.ts",
22
- "./server": "./server/renderer.ts",
23
- "./client": "./client/index.ts",
24
- "./client/hmr": "./client/hmr-adapter.ts",
25
- "./types": "./types.ts"
21
+ ".": "./dist/mod.js",
22
+ "./server": "./dist/server/renderer.js",
23
+ "./client": "./dist/client/index.js",
24
+ "./client/hmr": "./dist/client/hmr-adapter.js",
25
+ "./types": "./dist/types.js"
26
+ },
27
+ "scripts": {
28
+ "build": "bun run ../../../scripts/build-package.ts",
29
+ "prepublishOnly": "bun run build"
26
30
  },
27
31
  "files": [
28
- "**/*.ts",
29
- "**/*.tsx",
30
- "!**/__tests__/**",
31
- "!**/*.test.ts",
32
- "!vitest.config.ts",
32
+ "dist/**/*.js",
33
+ "dist/**/*.d.ts",
33
34
  "README.md"
34
35
  ],
35
36
  "dependencies": {
36
- "@useavalon/avalon": "^0.1.12",
37
+ "@useavalon/avalon": "^0.1.15",
37
38
  "@useavalon/core": "^0.1.3",
38
39
  "@vitejs/plugin-vue": "^5.2.4"
39
40
  },
@@ -1,226 +0,0 @@
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();
package/client/index.ts DELETED
@@ -1,7 +0,0 @@
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";
package/mod.ts DELETED
@@ -1,59 +0,0 @@
1
- /**
2
- * Vue Integration for Avalon
3
- *
4
- * Provides Vue 3 support with SSR, hydration, and scoped CSS extraction.
5
- * This integration enables Vue Single File Components (.vue) to work
6
- * seamlessly with Avalon's islands architecture.
7
- */
8
-
9
- import type { Plugin } from 'vite';
10
- import type { Integration, IntegrationConfig } from '@useavalon/core/types';
11
- import { render } from './server/renderer.ts';
12
- import { getHydrationScript } from './client/hydration.ts';
13
-
14
- /**
15
- * Vue integration instance
16
- *
17
- * Implements the standard Integration interface for Vue components.
18
- */
19
- export const vueIntegration: Integration = {
20
- name: 'vue',
21
- version: '0.1.0',
22
-
23
- render,
24
- getHydrationScript,
25
-
26
- config(): IntegrationConfig {
27
- return {
28
- name: 'vue',
29
- fileExtensions: ['.vue'],
30
- jsxImportSources: [],
31
- detectionPatterns: {
32
- imports: [/^vue$/, /^vue\//, /from\s+['"]vue['"]/],
33
- content: [/<template>/, /<script.*setup>/, /\bdefineComponent\b/, /\bref\b/, /\breactive\b/, /\bcomputed\b/],
34
- },
35
- };
36
- },
37
-
38
- /**
39
- * Provides the @vitejs/plugin-vue Vite plugin with avalon-island custom element configuration.
40
- * This allows Vue components to work seamlessly with Avalon's islands architecture.
41
- */
42
- async vitePlugin(): Promise<Plugin | Plugin[]> {
43
- const { default: vue } = await import('@vitejs/plugin-vue');
44
- return vue({
45
- template: {
46
- compilerOptions: {
47
- // Treat avalon-island as a custom element so Vue doesn't try to resolve it
48
- isCustomElement: (tag: string) => tag === 'avalon-island',
49
- },
50
- },
51
- });
52
- },
53
- };
54
-
55
- // Re-export public API
56
- export { render } from './server/renderer.ts';
57
- export { hydrate, getHydrationScript } from './client/hydration.ts';
58
- export { extractCSS, applyScopedCSS, generateScopeId } from './server/css-extractor.ts';
59
- export type * from './types.ts';
@@ -1,175 +0,0 @@
1
- /**
2
- * Vue CSS Extractor
3
- *
4
- * Utilities for extracting and processing CSS from Vue Single File Components.
5
- * Handles both scoped and global styles with proper attribute application.
6
- */
7
-
8
- import { readFile } from "node:fs/promises";
9
- import type { CSSExtractionOptions, StyleBlock } from "../types.ts";
10
-
11
- /**
12
- * Extract CSS from Vue Single File Component
13
- *
14
- * Parses <style> blocks from .vue files and applies scoping if needed.
15
- * Supports both scoped and global styles.
16
- *
17
- * @param src - Path to the Vue component file
18
- * @param options - CSS extraction options
19
- * @returns Extracted and processed CSS string
20
- */
21
- export async function extractCSS(
22
- src: string,
23
- options: CSSExtractionOptions = {},
24
- ) {
25
- // Try different path variations to find the Vue file
26
- const pathVariations = [
27
- // Standard framework paths
28
- src.startsWith("/") ? `src${src}` : src,
29
- src.replace("/islands/", "/src/islands/"),
30
- // Remove leading slash variations
31
- src.startsWith("/") ? src.substring(1) : src,
32
- ];
33
-
34
- let vueContent = "";
35
-
36
- for (const path of pathVariations) {
37
- try {
38
- vueContent = await readFile(path, "utf-8");
39
- break;
40
- } catch {
41
- continue;
42
- }
43
- }
44
-
45
- if (!vueContent) {
46
- throw new Error(
47
- `Vue file not found in any of the attempted paths: ${
48
- pathVariations.join(", ")
49
- }`,
50
- );
51
- }
52
-
53
- // Extract all style blocks
54
- const styleBlocks = extractStyleBlocks(vueContent);
55
-
56
- if (styleBlocks.length === 0) {
57
- return "";
58
- }
59
-
60
- // Generate scope ID if not provided
61
- const scopeId = options.scopeId || generateScopeId(src);
62
-
63
- // Process each style block
64
- let componentCSS = "";
65
-
66
- for (const block of styleBlocks) {
67
- if (block.scoped) {
68
- componentCSS += applyScopedCSS(block.content, scopeId);
69
- } else {
70
- componentCSS += block.content;
71
- }
72
- }
73
-
74
- return componentCSS;
75
- }
76
-
77
- /**
78
- * Apply scoped CSS transformation
79
- *
80
- * Adds scope attributes to CSS selectors for Vue's scoped styles.
81
- * Skips at-rules like @media, @keyframes, etc.
82
- *
83
- * @param css - CSS content to scope
84
- * @param scopeId - Scope identifier (e.g., "data-v-abc123")
85
- * @returns Scoped CSS string
86
- */
87
- export function applyScopedCSS(css: string, scopeId: string) {
88
- return css.replace(/([^{}]+){/g, (match, selector) => {
89
- const trimmedSelector = selector.trim();
90
-
91
- // Skip at-rules (@media, @keyframes, @supports, etc.)
92
- if (trimmedSelector.startsWith("@")) {
93
- return match;
94
- }
95
-
96
- // Add scope attribute to each selector
97
- // Handle multiple selectors separated by commas
98
- const scopedSelectors = trimmedSelector
99
- .split(",")
100
- .map((s: string) => `${s.trim()}[${scopeId}]`)
101
- .join(", ");
102
-
103
- return `${scopedSelectors} {`;
104
- });
105
- }
106
-
107
- /**
108
- * Extract style blocks from Vue SFC content
109
- *
110
- * Parses <style> tags and extracts their content and attributes.
111
- *
112
- * @param vueContent - Vue SFC file content
113
- * @returns Array of style blocks with metadata
114
- */
115
- function extractStyleBlocks(vueContent: string) {
116
- const styleRegex = /<style([^>]*)>([\s\S]*?)<\/style>/gi;
117
- const blocks: StyleBlock[] = [];
118
- let match;
119
-
120
- while ((match = styleRegex.exec(vueContent)) !== null) {
121
- const attributes = match[1];
122
- const content = match[2].trim();
123
- const isScoped = attributes.includes("scoped");
124
-
125
- blocks.push({
126
- content,
127
- scoped: isScoped,
128
- attributes,
129
- });
130
- }
131
-
132
- return blocks;
133
- }
134
-
135
- /**
136
- * Generate a consistent scope ID for a component
137
- *
138
- * Creates a deterministic scope ID based on the component path.
139
- * Format: "data-v-{hash}" where hash is derived from the path.
140
- *
141
- * @param src - Component source path
142
- * @returns Scope ID string
143
- */
144
- export function generateScopeId(src: string) {
145
- // Remove special characters and convert to lowercase for consistency
146
- const hash = src.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
147
- return `data-v-${hash}`;
148
- }
149
-
150
- /**
151
- * Apply scope attributes to HTML elements
152
- *
153
- * Adds scope attributes to HTML tags for matching with scoped CSS.
154
- * Skips closing tags and self-closing tags.
155
- *
156
- * @param html - HTML string to process
157
- * @param scopeId - Scope identifier
158
- * @returns HTML with scope attributes
159
- */
160
- export function applyScopeToHTML(html: string, scopeId: string) {
161
- return html.replace(/<([a-zA-Z][^>]*?)>/g, (match, tagContent) => {
162
- // Skip closing tags
163
- if (tagContent.startsWith("/")) {
164
- return match;
165
- }
166
-
167
- // Skip self-closing tags (already have /)
168
- if (tagContent.endsWith("/")) {
169
- return match;
170
- }
171
-
172
- // Add scope attribute
173
- return `<${tagContent} ${scopeId}>`;
174
- });
175
- }
@@ -1,116 +0,0 @@
1
- /**
2
- * Vue Server Renderer
3
- *
4
- * Provides server-side rendering capabilities for Vue components.
5
- * Uses Vue's official SSR API with proper hydration support.
6
- *
7
- * Migrated from src/islands/renderers/vue-renderer.ts
8
- */
9
-
10
- import { createSSRApp } from 'vue';
11
- import { renderToString as vueRenderToString } from 'vue/server-renderer';
12
- import type { RenderParams, RenderResult } from '@useavalon/core/types';
13
- import { extractCSS, generateScopeId, applyScopeToHTML } from './css-extractor.ts';
14
- import { toImportSpecifier } from '@useavalon/core/utils';
15
- import { resolveIslandPath } from '@useavalon/avalon/islands/framework-detection';
16
-
17
- /**
18
- * Render a Vue component to HTML string with SSR
19
- *
20
- * Creates a Vue SSR app instance and renders it to string.
21
- * Extracts and applies scoped CSS from the component.
22
- *
23
- * Based on Vue.js SSR documentation and Astro's Vue integration:
24
- * - Creates proper SSR app with createSSRApp
25
- * - Wraps SSR HTML in a div with data-server-rendered="true"
26
- * - Uses consistent container structure for client hydration
27
- *
28
- * @param params - Render parameters including component, props, and source path
29
- * @returns Render result with HTML, CSS, and hydration data
30
- */
31
- export async function render(params: RenderParams): Promise<RenderResult> {
32
- const { component: _component, props = {}, src, condition = 'on:client', ssrOnly = false } = params;
33
-
34
- try {
35
- const VueComponent = await loadComponent(src);
36
-
37
- const app = createSSRApp(VueComponent as any, props);
38
- const ssrHtml = await vueRenderToString(app);
39
-
40
- let componentCSS = '';
41
- let scopeId = '';
42
-
43
- try {
44
- scopeId = generateScopeId(src);
45
- componentCSS = await extractCSS(src, { scopeId });
46
- } catch {
47
- // CSS extraction failed, continue without CSS
48
- }
49
-
50
- let finalHtml = ssrHtml;
51
- if (componentCSS) {
52
- finalHtml = applyScopeToHTML(ssrHtml, scopeId);
53
- }
54
-
55
- return {
56
- html: finalHtml,
57
- css: componentCSS || undefined,
58
- scopeId: scopeId || undefined,
59
- hydrationData: { src, props, framework: 'vue', condition, ssrOnly },
60
- };
61
- } catch (error) {
62
- throw new Error(`Vue SSR rendering failed: ${error}`);
63
- }
64
- }
65
-
66
- /**
67
- * Load a Vue component module
68
- *
69
- * Handles both development (via Vite) and production (pre-built) scenarios.
70
- *
71
- * @param src - Component source path
72
- * @returns Vue component module
73
- */
74
- async function loadComponent(src: string) {
75
- const isDev = process.env.NODE_ENV !== 'production';
76
-
77
- if (isDev && (globalThis as any).__viteDevServer) {
78
- // Development: use Vite's SSR module loading
79
-
80
- const viteServer = (globalThis as any).__viteDevServer;
81
- const resolvedPath = await resolveIslandPath(src);
82
- const module = await viteServer.ssrLoadModule(resolvedPath);
83
- return module.default || module;
84
- }
85
-
86
- // Production: load from build output
87
- const ssrPath = src.replace('/islands/', '/dist/ssr/islands/').replace('.vue', '.js');
88
-
89
- const module = await import(
90
- /* @vite-ignore */
91
- toImportSpecifier(ssrPath)
92
- );
93
- return module.default || module;
94
- }
95
-
96
- /**
97
- * Get component metadata for debugging
98
- *
99
- * @param component - Vue component
100
- * @returns Component metadata object
101
- */
102
- export function getComponentMetadata(component: unknown) {
103
- if (typeof component === 'object' && component !== null) {
104
- return {
105
- name: (component as { name?: string }).name || 'Anonymous',
106
- type: 'component',
107
- hasSetup: 'setup' in component,
108
- hasTemplate: 'template' in component,
109
- hasRender: 'render' in component,
110
- };
111
- }
112
-
113
- return {
114
- type: typeof component,
115
- };
116
- }
package/types.ts DELETED
@@ -1,80 +0,0 @@
1
- /**
2
- * Vue Integration Types
3
- *
4
- * Type definitions specific to the Vue integration package.
5
- */
6
-
7
- import type { RenderParams, RenderResult } from '@useavalon/core/types';
8
-
9
- /**
10
- * Vue-specific render parameters
11
- */
12
- export interface VueRenderParams extends RenderParams {
13
- /**
14
- * Vue app context for SSR
15
- */
16
- context?: Map<string, unknown>;
17
- }
18
-
19
- /**
20
- * Vue-specific render result with CSS extraction
21
- */
22
- export interface VueRenderResult extends RenderResult {
23
- /**
24
- * Extracted CSS from Vue SFC <style> blocks
25
- */
26
- css?: string;
27
-
28
- /**
29
- * Head content (e.g., meta tags, title)
30
- */
31
- head?: string;
32
-
33
- /**
34
- * Scope ID for scoped styles
35
- */
36
- scopeId?: string;
37
- }
38
-
39
- /**
40
- * Vue component module structure
41
- */
42
- export interface VueComponentModule {
43
- default?: unknown;
44
- [key: string]: unknown;
45
- }
46
-
47
- /**
48
- * CSS extraction options
49
- */
50
- export interface CSSExtractionOptions {
51
- /**
52
- * Whether to apply scoping to CSS
53
- */
54
- scoped?: boolean;
55
-
56
- /**
57
- * Custom scope ID (generated if not provided)
58
- */
59
- scopeId?: string;
60
- }
61
-
62
- /**
63
- * Style block metadata from Vue SFC
64
- */
65
- export interface StyleBlock {
66
- /**
67
- * CSS content
68
- */
69
- content: string;
70
-
71
- /**
72
- * Whether the style is scoped
73
- */
74
- scoped: boolean;
75
-
76
- /**
77
- * Style attributes (e.g., lang, scoped)
78
- */
79
- attributes: string;
80
- }