@useavalon/vue 0.1.2 → 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.
- package/README.md +42 -42
- package/dist/client/hmr-adapter.js +1 -0
- package/{client/hydration.ts → dist/client/hydration.js} +2 -50
- package/dist/client/index.js +1 -0
- package/dist/mod.js +1 -0
- package/dist/server/css-extractor.js +1 -0
- package/dist/server/renderer.js +1 -0
- package/dist/types.js +1 -0
- package/package.json +24 -9
- package/client/index.ts +0 -7
- package/mod.ts +0 -59
- package/server/css-extractor.ts +0 -175
- package/server/renderer.ts +0 -116
- package/types.ts +0 -80
- package/vitest.config.ts +0 -13
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 @@
|
|
|
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,51 +1,4 @@
|
|
|
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 `
|
|
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`
|
|
49
2
|
import { createApp } from 'vue';
|
|
50
3
|
|
|
51
4
|
// Auto-hydrate all Vue islands
|
|
@@ -120,5 +73,4 @@ export function getHydrationScript(): string {
|
|
|
120
73
|
|
|
121
74
|
return true;
|
|
122
75
|
}
|
|
123
|
-
|
|
124
|
-
}
|
|
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
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Vue integration for Avalon islands architecture",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,17 +10,32 @@
|
|
|
10
10
|
"directory": "packages/integrations/vue"
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://useavalon.dev/docs/frameworks/vue",
|
|
13
|
-
"keywords": [
|
|
13
|
+
"keywords": [
|
|
14
|
+
"avalon",
|
|
15
|
+
"islands",
|
|
16
|
+
"vue",
|
|
17
|
+
"ssr",
|
|
18
|
+
"hydration"
|
|
19
|
+
],
|
|
14
20
|
"exports": {
|
|
15
|
-
".": "./mod.
|
|
16
|
-
"./server": "./server/renderer.
|
|
17
|
-
"./client": "./client/index.
|
|
18
|
-
"./
|
|
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"
|
|
19
26
|
},
|
|
20
|
-
"
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "bun run ../../../scripts/build-package.ts",
|
|
29
|
+
"prepublishOnly": "bun run build"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist/**/*.js",
|
|
33
|
+
"dist/**/*.d.ts",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
21
36
|
"dependencies": {
|
|
22
|
-
"@useavalon/avalon": "^0.1.
|
|
23
|
-
"@useavalon/core": "^0.1.
|
|
37
|
+
"@useavalon/avalon": "^0.1.15",
|
|
38
|
+
"@useavalon/core": "^0.1.3",
|
|
24
39
|
"@vitejs/plugin-vue": "^5.2.4"
|
|
25
40
|
},
|
|
26
41
|
"peerDependencies": {
|
package/client/index.ts
DELETED
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';
|
package/server/css-extractor.ts
DELETED
|
@@ -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
|
-
}
|
package/server/renderer.ts
DELETED
|
@@ -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
|
-
}
|
package/vitest.config.ts
DELETED