From 6bc0420b6805dd3acf52f688dd288b108c506d83 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 29 Oct 2025 15:54:45 +0800 Subject: [PATCH] wip: add SSR support --- .../src/apiDefineVaporCustomElement.ts | 11 +- .../e2e/ssr-vapor-custom-element.spec.ts | 184 ++++++++++++++++++ 2 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 packages/vue/__tests__/e2e/ssr-vapor-custom-element.spec.ts diff --git a/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts b/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts index a3158cc3ab..7624b66cee 100644 --- a/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts +++ b/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts @@ -2,6 +2,7 @@ import { extend, isPlainObject } from '@vue/shared' import { createComponent, createVaporApp, + createVaporSSRApp, defineVaporComponent, isFragment, } from '.' @@ -17,6 +18,7 @@ import type { VaporComponentInstance, } from './component' import type { Block } from './block' +import { withHydration } from './dom/hydration' export type VaporElementConstructor

= { new (initialProps?: Record): VaporElement & P @@ -50,7 +52,6 @@ export const defineVaporSSRCustomElement = (( options: any, extraOptions?: Omit, ) => { - // @ts-expect-error return defineVaporCustomElement(options, extraOptions, createVaporSSRApp) }) as typeof defineVaporCustomElement @@ -93,7 +94,13 @@ export class VaporElement extends VueElementBase< this._def.configureApp(this._app) } - this._createComponent() + // For SSR custom elements, we need to create component in hydration context + if (this._createApp === createVaporSSRApp) { + withHydration(this._root, this._createComponent.bind(this)) + } else { + this._createComponent() + } + this._app!.mount(this._root) // Render slots immediately after mount for shadowRoot: false diff --git a/packages/vue/__tests__/e2e/ssr-vapor-custom-element.spec.ts b/packages/vue/__tests__/e2e/ssr-vapor-custom-element.spec.ts new file mode 100644 index 0000000000..065cb0fd68 --- /dev/null +++ b/packages/vue/__tests__/e2e/ssr-vapor-custom-element.spec.ts @@ -0,0 +1,184 @@ +import path from 'node:path' +import fs from 'node:fs' +import { setupPuppeteer } from './e2eUtils' + +const { page, click, text } = setupPuppeteer() + +let vaporDataUrl: string + +beforeAll(() => { + // Read the vapor ESM module once + const vaporPath = path.resolve( + __dirname, + '../../dist/vue.runtime-with-vapor.esm-browser.js', + ) + const vaporCode = fs.readFileSync(vaporPath, 'utf-8') + + // Create a data URL for the ESM module + vaporDataUrl = `data:text/javascript;base64,${Buffer.from(vaporCode).toString('base64')}` +}) + +async function loadVaporModule() { + // Load module and expose to window + await page().addScriptTag({ + content: ` + import('${vaporDataUrl}').then(module => { + window.VueVapor = module; + }); + `, + type: 'module', + }) + + // Wait for VueVapor to be available + await page().waitForFunction( + () => typeof (window as any).VueVapor !== 'undefined', + { timeout: 10000 }, + ) +} + +async function setContent(html: string) { + // For SSR content with declarative shadow DOM, we need to use setContent + // which causes the browser to parse the HTML properly + await page().setContent(` + + + +

${html}
+ + + `) + + // load the vapor module after setting content + await loadVaporModule() +} + +// this must be tested in actual Chrome because jsdom does not support +// declarative shadow DOM +test('ssr vapor custom element hydration', async () => { + await setContent( + ``, + ) + + await page().evaluate(() => { + const { + ref, + defineVaporSSRCustomElement, + defineVaporAsyncComponent, + onMounted, + useHost, + template, + child, + setText, + renderEffect, + delegateEvents, + } = (window as any).VueVapor + + delegateEvents('click') + + const def = { + setup() { + const count = ref(1) + const el = useHost() + onMounted(() => (el.style.border = '1px solid red')) + + const n0 = template('')() + const x0 = child(n0) + n0.$evtclick = () => count.value++ + renderEffect(() => setText(x0, count.value)) + return n0 + }, + } + + customElements.define('my-element', defineVaporSSRCustomElement(def)) + customElements.define( + 'my-element-async', + defineVaporSSRCustomElement( + defineVaporAsyncComponent( + () => + new Promise(r => { + ;(window as any).resolve = () => r(def) + }), + ), + ), + ) + }) + + function getColor() { + return page().evaluate(() => { + return [ + (document.querySelector('my-element') as any).style.border, + (document.querySelector('my-element-async') as any).style.border, + ] + }) + } + + expect(await getColor()).toMatchObject(['1px solid red', '']) + await page().evaluate(() => (window as any).resolve()) // exposed by test + expect(await getColor()).toMatchObject(['1px solid red', '1px solid red']) + + async function assertInteraction(el: string) { + const selector = `${el} >>> button` + expect(await text(selector)).toBe('1') + await click(selector) + expect(await text(selector)).toBe('2') + } + + await assertInteraction('my-element') + await assertInteraction('my-element-async') +}) + +// test('work with Teleport (shadowRoot: false)', async () => { +// await setContent( +// `
`, +// ) + +// await page().evaluate(() => { +// const { +// defineVaporSSRCustomElement, +// createComponent, +// createSlot, +// VaporTeleport, +// createComponentWithFallback, +// template, +// } = (window as any).VueVapor +// const Y = defineVaporSSRCustomElement( +// { +// setup() { +// const n1 = createComponent( +// VaporTeleport, +// { to: () => '#test' }, +// { +// default: () => { +// const n0 = createSlot('default', null) +// return n0 +// }, +// }, +// true, +// ) +// return n1 +// }, +// }, +// { shadowRoot: false }, +// ) +// customElements.define('my-y', Y) +// const P = defineVaporSSRCustomElement( +// { +// setup() { +// return createComponentWithFallback('my-y', null, { +// default: () => template('default')(), +// }) +// }, +// }, +// { shadowRoot: false }, +// ) +// customElements.define('my-p', P) +// }) + +// function getInnerHTML() { +// return page().evaluate(() => { +// return (document.querySelector('#test') as any).innerHTML +// }) +// } + +// expect(await getInnerHTML()).toBe('default') +// }) -- 2.47.3