diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts
index c7a7cfb9..051f59b4 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(``)
})
- 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(
+ ``
+ )
+ })
+
+ 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(``)
+ ).toBe(
+ ``
+ )
})
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 b8b47c54..2d30373e 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 1b0eff4c..074d98b0 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.`
)
}
}