diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts index c7a7cfb980c..051f59b4f03 100644 --- a/packages/server-renderer/__tests__/renderToString.spec.ts +++ b/packages/server-renderer/__tests__/renderToString.spec.ts @@ -6,10 +6,12 @@ import { resolveComponent, ComponentOptions } from 'vue' -import { escapeHtml } from '@vue/shared' +import { escapeHtml, mockWarn } from '@vue/shared' import { renderToString, renderComponent } from '../src/renderToString' import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot' +mockWarn() + describe('ssr: renderToString', () => { test('should apply app context', async () => { const app = createApp({ @@ -56,6 +58,31 @@ describe('ssr: renderToString', () => { ).toBe(`
hello
`) }) + describe('template components', () => { + test('render', async () => { + expect( + await renderToString( + createApp({ + data() { + return { msg: 'hello' } + }, + template: `
{{ msg }}
` + }) + ) + ).toBe(`
hello
`) + }) + + test('handle compiler errors', async () => { + await renderToString(createApp({ template: `<` })) + + expect( + '[Vue warn]: Template compilation error: Unexpected EOF in tag.\n' + + '1 | <\n' + + ' | ^' + ).toHaveBeenWarned() + }) + }) + test('nested vnode components', async () => { const Child = { props: ['msg'], @@ -96,7 +123,22 @@ describe('ssr: renderToString', () => { ).toBe(`
parent
hello
`) }) - test('mixing optimized / vnode components', async () => { + test('nested template components', async () => { + const Child = { + props: ['msg'], + template: `
{{ msg }}
` + } + const app = createApp({ + template: `
parent
` + }) + app.component('Child', Child) + + expect(await renderToString(app)).toBe( + `
parent
hello
` + ) + }) + + test('mixing optimized / vnode / template components', async () => { const OptimizedChild = { props: ['msg'], ssrRender(ctx: any, push: any) { @@ -111,6 +153,11 @@ describe('ssr: renderToString', () => { } } + const TemplateChild = { + props: ['msg'], + template: `
{{ msg }}
` + } + expect( await renderToString( createApp({ @@ -120,11 +167,21 @@ describe('ssr: renderToString', () => { renderComponent(OptimizedChild, { msg: 'opt' }, null, parent) ) push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent)) + push( + renderComponent( + TemplateChild, + { msg: 'template' }, + null, + parent + ) + ) push(``) } }) ) - ).toBe(`
parent
opt
vnode
`) + ).toBe( + `
parent
opt
vnode
template
` + ) }) test('nested components with optimized slots', async () => { @@ -236,6 +293,50 @@ describe('ssr: renderToString', () => { ) }) + test('nested components with template slots', async () => { + const Child = { + props: ['msg'], + template: `
` + } + + const app = createApp({ + template: `
parent{{ msg }}
` + }) + app.component('Child', Child) + + expect(await renderToString(app)).toBe( + `
parent
` + + `from slot` + + `
` + ) + }) + + test('nested render fn components with template slots', async () => { + const Child = { + props: ['msg'], + render(this: any) { + return h( + 'div', + { + class: 'child' + }, + this.$slots.default({ msg: 'from slot' }) + ) + } + } + + const app = createApp({ + template: `
parent{{ msg }}
` + }) + app.component('Child', Child) + + expect(await renderToString(app)).toBe( + `
parent
` + + `from slot` + + `
` + ) + }) + test('async components', async () => { const Child = { // should wait for resovled render context from setup() diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index b8b47c549fd..2d30373e0b4 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -28,5 +28,8 @@ "homepage": "https://github.com/vuejs/vue/tree/dev/packages/server-renderer#readme", "peerDependencies": { "vue": "3.0.0-alpha.4" + }, + "dependencies": { + "@vue/compiler-ssr": "3.0.0-alpha.4" } } diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index 1b0eff4cc3f..074d98b0423 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -11,7 +11,8 @@ import { Portal, ShapeFlags, ssrUtils, - Slots + Slots, + warn } from 'vue' import { isString, @@ -19,10 +20,14 @@ import { isArray, isFunction, isVoidTag, - escapeHtml + escapeHtml, + NO, + generateCodeFrame } from '@vue/shared' +import { compile } from '@vue/compiler-ssr' import { ssrRenderAttrs } from './helpers/ssrRenderAttrs' import { SSRSlots } from './helpers/ssrRenderSlot' +import { CompilerError } from '@vue/compiler-dom' const { isVNode, @@ -126,6 +131,44 @@ function renderComponentVNode( } } +type SSRRenderFunction = ( + ctx: any, + push: (item: any) => void, + parentInstance: ComponentInternalInstance +) => void +const compileCache: Record = Object.create(null) + +function ssrCompile( + template: string, + instance: ComponentInternalInstance +): SSRRenderFunction { + const cached = compileCache[template] + if (cached) { + return cached + } + + const { code } = compile(template, { + isCustomElement: instance.appContext.config.isCustomElement || NO, + isNativeTag: instance.appContext.config.isNativeTag || NO, + onError(err: CompilerError) { + if (__DEV__) { + const message = `Template compilation error: ${err.message}` + const codeFrame = + err.loc && + generateCodeFrame( + template as string, + err.loc.start.offset, + err.loc.end.offset + ) + warn(codeFrame ? `${message}\n${codeFrame}` : message) + } else { + throw err + } + } + }) + return (compileCache[template] = Function(code)()) +} + function renderComponentSubTree( instance: ComponentInternalInstance ): ResolvedSSRBuffer | Promise { @@ -134,6 +177,10 @@ function renderComponentSubTree( if (isFunction(comp)) { renderVNode(push, renderComponentRoot(instance), instance) } else { + if (!comp.ssrRender && !comp.render && isString(comp.template)) { + comp.ssrRender = ssrCompile(comp.template, instance) + } + if (comp.ssrRender) { // optimized // set current rendering instance for asset resolution @@ -143,11 +190,10 @@ function renderComponentSubTree( } else if (comp.render) { renderVNode(push, renderComponentRoot(instance), instance) } else { - // TODO on the fly template compilation support throw new Error( `Component ${ comp.name ? `${comp.name} ` : `` - } is missing render function.` + } is missing template or render function.` ) } }