From a996cf4ad7c844719c444d47372bfa1324ed5300 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 31 Mar 2026 23:43:42 +0000 Subject: [PATCH] Add vue-vapor framework support for islands Introduce Vue Vapor as a new island framework, allowing .vapor.vue components to hydrate with the lightweight Vapor runtime (~6KB) instead of the full Vue runtime (~50KB). Vapor islands are treated as a separate framework (like Solid/Preact) with their own SSR prerenderer and client hydration adapter. - New hydration adapter: packages/hydration/vue-vapor.ts - Auto-detection of .vapor.vue file extension in Island.vue - SSR prerendering via createVaporApp + renderToString - Separate vendor chunk (vendor-vue-vapor) for bundle isolation - Support for explicit using="vue-vapor" prop on islands https://claude.ai/code/session_01Rn1bhLJcwHBfA2ewWEyWWE --- packages/hydration/package.json | 1 + packages/hydration/types.ts | 2 +- packages/hydration/vue-vapor.ts | 22 +++++++++++++++++++ .../iles/src/client/app/components/Island.vue | 2 ++ packages/iles/src/node/alias.ts | 2 +- packages/iles/src/node/build/chunks.ts | 6 ++++- packages/prerender/prerender.ts | 6 +++++ 7 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 packages/hydration/vue-vapor.ts diff --git a/packages/hydration/package.json b/packages/hydration/package.json index 16726405..4f76a6e8 100644 --- a/packages/hydration/package.json +++ b/packages/hydration/package.json @@ -27,6 +27,7 @@ "./svelte": "./dist/svelte.js", "./vanilla": "./dist/vanilla.js", "./vue": "./dist/vue.js", + "./vue-vapor": "./dist/vue-vapor.js", "./dist/*": "./dist/*", "./package.json": "./package.json" }, diff --git a/packages/hydration/types.ts b/packages/hydration/types.ts index ddf1e8d2..81b3fada 100644 --- a/packages/hydration/types.ts +++ b/packages/hydration/types.ts @@ -2,7 +2,7 @@ import type Vue from './vue' -export type Framework = 'vue' | 'preact' | 'solid' | 'svelte' | 'vanilla' +export type Framework = 'vue' | 'vue-vapor' | 'preact' | 'solid' | 'svelte' | 'vanilla' export type FrameworkFn = typeof Vue export type AsyncFrameworkFn = () => Promise export type Component = any diff --git a/packages/hydration/vue-vapor.ts b/packages/hydration/vue-vapor.ts new file mode 100644 index 00000000..7f0fe612 --- /dev/null +++ b/packages/hydration/vue-vapor.ts @@ -0,0 +1,22 @@ +import { createVaporApp } from 'vue/vapor' +import type { Props, Slots } from './types' +import { onDispose } from './hydration' + +// Internal: Creates a Vue Vapor app and mounts it on the specified island root. +// Vapor islands bypass the virtual DOM for significantly smaller bundle size. +export default function createVueVaporIsland (component: any, id: string, el: Element, props: Props, slots: Slots | undefined) { + const app = createVaporApp(component, { + ...props, + ...slots && Object.fromEntries(Object.entries(slots).map(([slotName, content]) => { + return [slotName, () => content] + })), + }) + + app.mount(el!) + + if (import.meta.env.DISPOSE_ISLANDS) + onDispose(id, app.unmount) + + if (import.meta.env.DEV) + (window as any).__ILE_DEVTOOLS__?.onHydration({ id, el, props, slots, component }) +} diff --git a/packages/iles/src/client/app/components/Island.vue b/packages/iles/src/client/app/components/Island.vue index d8978932..16dc01c1 100644 --- a/packages/iles/src/client/app/components/Island.vue +++ b/packages/iles/src/client/app/components/Island.vue @@ -51,11 +51,13 @@ export default defineComponent({ } const ext = props.importFrom.split('.').slice(-1)[0] + const isVaporVue = props.importFrom.endsWith('.vapor.vue') const appConfig = useAppConfig() const framework: Framework = props.using || (ext === 'svelte' && 'svelte') || ((ext === 'js' || ext === 'ts') && 'vanilla') || ((ext === 'jsx' || ext === 'tsx') && appConfig.jsx) + || (isVaporVue && 'vue-vapor') || 'vue' return { diff --git a/packages/iles/src/node/alias.ts b/packages/iles/src/node/alias.ts index aa5fa37d..db641d25 100644 --- a/packages/iles/src/node/alias.ts +++ b/packages/iles/src/node/alias.ts @@ -78,7 +78,7 @@ export function resolveAliases (root: string, userConfig: UserConfig): AliasOpti find: /^@islands\/hydration$/, replacement: require.resolve('@islands/hydration'), }, - ...['vue', 'vanilla', 'svelte', 'preact', 'solid'].map(name => ({ + ...['vue', 'vue-vapor', 'vanilla', 'svelte', 'preact', 'solid'].map(name => ({ find: new RegExp(`^@islands/hydration/${name}$`), replacement: require.resolve(`@islands/hydration/${name}`), })), diff --git a/packages/iles/src/node/build/chunks.ts b/packages/iles/src/node/build/chunks.ts index fb301425..f6964015 100644 --- a/packages/iles/src/node/build/chunks.ts +++ b/packages/iles/src/node/build/chunks.ts @@ -15,6 +15,9 @@ export function extendManualChunks (config: AppConfig): GetManualChunk { svelte: 'vendor-svelte', vue: 'vendor-vue', } + const chunkForSpecialExtension: Record = { + 'vapor.vue': 'vendor-vue-vapor', + } return (id, api) => { // Internal chunks must take priority to ensure hydration works correctly. // User manualChunks could inadvertently match hydration modules (e.g. @@ -56,7 +59,8 @@ function vendorPerFramework ( const queryIndex = id.lastIndexOf('?') const idWithoutQuery = queryIndex > -1 ? id.slice(0, queryIndex) : id const extension = idWithoutQuery.slice(idWithoutQuery.lastIndexOf('.') + 1) - const name = chunkForExtension[extension] + const specialExt = Object.keys(chunkForSpecialExtension).find(ext => idWithoutQuery.endsWith(`.${ext}`)) + const name = (specialExt && chunkForSpecialExtension[specialExt]) || chunkForExtension[extension] cache.set(id, name) return name } diff --git a/packages/prerender/prerender.ts b/packages/prerender/prerender.ts index aeb5b6ef..ff872cc9 100644 --- a/packages/prerender/prerender.ts +++ b/packages/prerender/prerender.ts @@ -36,6 +36,12 @@ export const renderers: Record = { const renderSvelteComponent = (await import('./svelte')).default return renderSvelteComponent(component, props, slots, renderId) }, + async 'vue-vapor' (component, props, slots) { + const { createVaporApp } = await import('vue/vapor') + const { renderToString } = await import('vue/server-renderer') + const app = createVaporApp(component, props) + return await renderToString(app) + }, async vanilla () { throw new Error('The vanilla strategy does not prerender islands.') },