diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 12267adb..bbd4dc98 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -104,13 +104,14 @@ export { registerRuntimeCompiler } from './component' // SSR ------------------------------------------------------------------------- import { createComponentInstance, setupComponent } from './component' import { renderComponentRoot } from './componentRenderUtils' -import { normalizeVNode } from './vnode' +import { isVNode, normalizeVNode } from './vnode' // SSR utils are only exposed in cjs builds. const _ssrUtils = { createComponentInstance, setupComponent, renderComponentRoot, + isVNode, normalizeVNode } diff --git a/packages/server-renderer/__tests__/renderProps.spec.ts b/packages/server-renderer/__tests__/renderProps.spec.ts index f5a6f60d..25efd80f 100644 --- a/packages/server-renderer/__tests__/renderProps.spec.ts +++ b/packages/server-renderer/__tests__/renderProps.spec.ts @@ -55,6 +55,17 @@ describe('ssr: renderProps', () => { }) ).toBe(` readonly for="foobar"`) }) + + test('preserve name on custom element', () => { + expect( + renderProps( + { + fooBar: 'ok' + }, + 'my-el' + ) + ).toBe(` fooBar="ok"`) + }) }) describe('ssr: renderClass', () => { diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts index ffab23a0..a41c4eed 100644 --- a/packages/server-renderer/__tests__/renderToString.spec.ts +++ b/packages/server-renderer/__tests__/renderToString.spec.ts @@ -1,5 +1,5 @@ -import { createApp, h } from 'vue' -import { renderToString, renderComponent, renderSlot } from '../src' +import { createApp, h, createCommentVNode } from 'vue' +import { renderToString, renderComponent, renderSlot, escapeHtml } from '../src' describe('ssr: renderToString', () => { describe('components', () => { @@ -251,21 +251,82 @@ describe('ssr: renderToString', () => { }) }) + describe('vnode element', () => { + test('props', async () => { + expect( + await renderToString( + h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello') + ) + ).toBe(`
hello
`) + }) + + test('text children', async () => { + expect(await renderToString(h('div', 'hello'))).toBe(`
hello
`) + }) + + test('array children', async () => { + expect( + await renderToString( + h('div', [ + 'foo', + h('span', 'bar'), + [h('span', 'baz')], + createCommentVNode('qux') + ]) + ) + ).toBe( + `
foobarbaz
` + ) + }) + + test('void elements', async () => { + expect(await renderToString(h('input'))).toBe(``) + }) + + test('innerHTML', async () => { + expect( + await renderToString( + h( + 'div', + { + innerHTML: `hello` + }, + 'ignored' + ) + ) + ).toBe(`
hello
`) + }) + + test('textContent', async () => { + expect( + await renderToString( + h( + 'div', + { + textContent: `hello` + }, + 'ignored' + ) + ) + ).toBe(`
${escapeHtml(`hello`)}
`) + }) + + test('textarea value', async () => { + expect( + await renderToString( + h( + 'textarea', + { + value: `hello` + }, + 'ignored' + ) + ) + ).toBe(``) + }) + }) + describe('scopeId', () => { // TODO }) - - describe('vnode', () => { - test('text children', () => {}) - - test('array children', () => {}) - - test('void elements', () => {}) - - test('innerHTML', () => {}) - - test('textContent', () => {}) - - test('textarea value', () => {}) - }) }) diff --git a/packages/server-renderer/src/renderProps.ts b/packages/server-renderer/src/renderProps.ts index 112055d8..53536f70 100644 --- a/packages/server-renderer/src/renderProps.ts +++ b/packages/server-renderer/src/renderProps.ts @@ -8,16 +8,23 @@ import { isNoUnitNumericStyleProp, isOn, isSSRSafeAttrName, - isBooleanAttr + isBooleanAttr, + makeMap } from '@vue/shared' +const shouldIgnoreProp = makeMap(`key,ref,innerHTML,textContent`) + export function renderProps( props: Record, - isCustomElement: boolean = false + tag?: string ): string { let ret = '' for (const key in props) { - if (key === 'key' || key === 'ref' || isOn(key)) { + if ( + shouldIgnoreProp(key) || + isOn(key) || + (tag === 'textarea' && key === 'value') + ) { continue } const value = props[key] @@ -26,9 +33,10 @@ export function renderProps( } else if (key === 'style') { ret += ` style="${renderStyle(value)}"` } else if (value != null) { - const attrKey = isCustomElement - ? key - : propsToAttrMap[key] || key.toLowerCase() + const attrKey = + tag && tag.indexOf('-') > 0 + ? key // preserve raw name on custom elements + : propsToAttrMap[key] || key.toLowerCase() if (isBooleanAttr(attrKey)) { if (value !== false) { ret += ` ${attrKey}` diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index 1cc3706c..7cd7c25c 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -12,7 +12,8 @@ import { Portal, ShapeFlags, ssrUtils, - Slot + Slot, + createApp } from 'vue' import { isString, @@ -25,6 +26,7 @@ import { renderProps } from './renderProps' import { escapeHtml } from './ssrUtils' const { + isVNode, createComponentInstance, setupComponent, renderComponentRoot, @@ -81,7 +83,15 @@ function unrollBuffer(buffer: ResolvedSSRBuffer): string { return ret } -export async function renderToString(app: App): Promise { +export async function renderToString(input: App | VNode): Promise { + if (isVNode(input)) { + return renderAppToString(createApp({ render: () => input })) + } else { + return renderAppToString(input) + } +} + +async function renderAppToString(app: App): Promise { const resolvedBuffer = await renderComponent(app._component, app._props, null) return unrollBuffer(resolvedBuffer) } @@ -143,7 +153,7 @@ function renderComponentSubTree( return hasAsync() ? Promise.all(buffer) : (buffer as ResolvedSSRBuffer) } -export function renderVNode( +function renderVNode( push: PushFn, vnode: VNode, parentComponent: ComponentInternalInstance | null = null @@ -203,7 +213,7 @@ function renderElement( // TODO directives if (props !== null) { - openTag += renderProps(props, tag.indexOf(`-`) > 0) + openTag += renderProps(props, tag) } if (scopeId !== null) {