diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts index 7216edafb..57094e1aa 100644 --- a/packages/runtime-core/src/apiOptions.ts +++ b/packages/runtime-core/src/apiOptions.ts @@ -64,7 +64,12 @@ export interface ComponentOptionsBase< // type. render?: Function // SSR only. This is produced by compiler-ssr and attached in compiler-sfc - ssrRender?: Function + // not user facing, so the typing is lax and for test only. + ssrRender?: ( + ctx: any, + push: (item: any) => void, + parentInstance: ComponentInternalInstance + ) => void components?: Record< string, Component | { new (): ComponentPublicInstance } diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts index 3a7d76d4d..ffab23a07 100644 --- a/packages/server-renderer/__tests__/renderToString.spec.ts +++ b/packages/server-renderer/__tests__/renderToString.spec.ts @@ -1,7 +1,261 @@ -// import { renderToString, renderComponent } from '../src' +import { createApp, h } from 'vue' +import { renderToString, renderComponent, renderSlot } from '../src' describe('ssr: renderToString', () => { - describe('elements', () => { + describe('components', () => { + test('vnode components', async () => { + expect( + await renderToString( + createApp({ + data() { + return { msg: 'hello' } + }, + render(this: any) { + return h('div', this.msg) + } + }) + ) + ).toBe(`
hello
`) + }) + + test('optimized components', async () => { + expect( + await renderToString( + createApp({ + data() { + return { msg: 'hello' } + }, + ssrRender(ctx, push) { + push(`
${ctx.msg}
`) + } + }) + ) + ).toBe(`
hello
`) + }) + + test('nested vnode components', async () => { + const Child = { + props: ['msg'], + render(this: any) { + return h('div', this.msg) + } + } + + expect( + await renderToString( + createApp({ + render() { + return h('div', ['parent', h(Child, { msg: 'hello' })]) + } + }) + ) + ).toBe(`
parent
hello
`) + }) + + test('nested optimized components', async () => { + const Child = { + props: ['msg'], + ssrRender(ctx: any, push: any) { + push(`
${ctx.msg}
`) + } + } + + expect( + await renderToString( + createApp({ + ssrRender(_ctx, push, parent) { + push(`
parent`) + push(renderComponent(Child, { msg: 'hello' }, null, parent)) + push(`
`) + } + }) + ) + ).toBe(`
parent
hello
`) + }) + + test('mixing optimized / vnode components', async () => { + const OptimizedChild = { + props: ['msg'], + ssrRender(ctx: any, push: any) { + push(`
${ctx.msg}
`) + } + } + + const VNodeChild = { + props: ['msg'], + render(this: any) { + return h('div', this.msg) + } + } + + expect( + await renderToString( + createApp({ + ssrRender(_ctx, push, parent) { + push(`
parent`) + push( + renderComponent(OptimizedChild, { msg: 'opt' }, null, parent) + ) + push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent)) + push(`
`) + } + }) + ) + ).toBe(`
parent
opt
vnode
`) + }) + + test('nested components with optimized slots', async () => { + const Child = { + props: ['msg'], + ssrRender(ctx: any, push: any, parent: any) { + push(`
`) + renderSlot(ctx.$slots.default, { msg: 'from slot' }, push, parent) + push(`
`) + } + } + + expect( + await renderToString( + createApp({ + ssrRender(_ctx, push, parent) { + push(`
parent`) + push( + renderComponent( + Child, + { msg: 'hello' }, + { + // optimized slot using string push + default: ({ msg }: any, push: any) => { + push(`${msg}`) + }, + _compiled: true // important to avoid slots being normalized + }, + parent + ) + ) + push(`
`) + } + }) + ) + ).toBe( + `
parent
` + + `from slot` + + `
` + ) + }) + + test('nested components with vnode slots', async () => { + const Child = { + props: ['msg'], + ssrRender(ctx: any, push: any, parent: any) { + push(`
`) + renderSlot(ctx.$slots.default, { msg: 'from slot' }, push, parent) + push(`
`) + } + } + + expect( + await renderToString( + createApp({ + ssrRender(_ctx, push, parent) { + push(`
parent`) + push( + renderComponent( + Child, + { msg: 'hello' }, + { + // bailed slots returning raw vnodes + default: ({ msg }: any) => { + return h('span', msg) + } + }, + parent + ) + ) + push(`
`) + } + }) + ) + ).toBe( + `
parent
` + + `from slot` + + `
` + ) + }) + + test('async components', async () => { + const Child = { + // should wait for resovled render context from setup() + async setup() { + return { + msg: 'hello' + } + }, + ssrRender(ctx: any, push: any) { + push(`
${ctx.msg}
`) + } + } + + expect( + await renderToString( + createApp({ + ssrRender(_ctx, push, parent) { + push(`
parent`) + push(renderComponent(Child, null, null, parent)) + push(`
`) + } + }) + ) + ).toBe(`
parent
hello
`) + }) + + test('parallel async components', async () => { + const OptimizedChild = { + props: ['msg'], + async setup(props: any) { + return { + localMsg: props.msg + '!' + } + }, + ssrRender(ctx: any, push: any) { + push(`
${ctx.localMsg}
`) + } + } + + const VNodeChild = { + props: ['msg'], + async setup(props: any) { + return { + localMsg: props.msg + '!' + } + }, + render(this: any) { + return h('div', this.localMsg) + } + } + + expect( + await renderToString( + createApp({ + ssrRender(_ctx, push, parent) { + push(`
parent`) + push( + renderComponent(OptimizedChild, { msg: 'opt' }, null, parent) + ) + push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent)) + push(`
`) + } + }) + ) + ).toBe(`
parent
opt!
vnode!
`) + }) + }) + + describe('scopeId', () => { + // TODO + }) + + describe('vnode', () => { test('text children', () => {}) test('array children', () => {}) @@ -14,18 +268,4 @@ describe('ssr: renderToString', () => { test('textarea value', () => {}) }) - - describe('components', () => { - test('nested components', () => {}) - - test('nested components with optimized slots', () => {}) - - test('mixing optimized / vnode components', () => {}) - - test('nested components with vnode slots', () => {}) - - test('async components', () => {}) - - test('parallel async components', () => {}) - }) }) diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index a88c62c35..1cc3706ca 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -125,7 +125,7 @@ function renderComponentSubTree( } else { if (comp.ssrRender) { // optimized - comp.ssrRender(push, instance.proxy) + comp.ssrRender(instance.proxy, push, instance) } else if (comp.render) { renderVNode(push, renderComponentRoot(instance), instance) } else { @@ -260,8 +260,8 @@ export function renderSlot( ) { // template-compiled slots are always rendered as fragments push(``) - if (slotFn.length > 2) { - // only ssr-optimized slot fns accept 3 arguments + if (slotFn.length > 1) { + // only ssr-optimized slot fns accept more than 1 arguments slotFn(slotProps, push, parentComponent) } else { // normal slot diff --git a/tsconfig.json b/tsconfig.json index 62c98dbdb..4a61ca375 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ "types": ["jest", "puppeteer", "node"], "rootDir": ".", "paths": { - "@vue/*": ["packages/*/src"] + "@vue/*": ["packages/*/src"], + "vue": ["packages/vue/src"] } }, "include": [