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({
).toBe(`<div>hello</div>`)
})
+ describe('template components', () => {
+ test('render', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ data() {
+ return { msg: 'hello' }
+ },
+ template: `<div>{{ msg }}</div>`
+ })
+ )
+ ).toBe(`<div>hello</div>`)
+ })
+
+ 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'],
).toBe(`<div>parent<div>hello</div></div>`)
})
- test('mixing optimized / vnode components', async () => {
+ test('nested template components', async () => {
+ const Child = {
+ props: ['msg'],
+ template: `<div>{{ msg }}</div>`
+ }
+ const app = createApp({
+ template: `<div>parent<Child msg="hello" /></div>`
+ })
+ app.component('Child', Child)
+
+ expect(await renderToString(app)).toBe(
+ `<div>parent<div>hello</div></div>`
+ )
+ })
+
+ test('mixing optimized / vnode / template components', async () => {
const OptimizedChild = {
props: ['msg'],
ssrRender(ctx: any, push: any) {
}
}
+ const TemplateChild = {
+ props: ['msg'],
+ template: `<div>{{ msg }}</div>`
+ }
+
expect(
await renderToString(
createApp({
renderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
)
push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent))
+ push(
+ renderComponent(
+ TemplateChild,
+ { msg: 'template' },
+ null,
+ parent
+ )
+ )
push(`</div>`)
}
})
)
- ).toBe(`<div>parent<div>opt</div><div>vnode</div></div>`)
+ ).toBe(
+ `<div>parent<div>opt</div><div>vnode</div><div>template</div></div>`
+ )
})
test('nested components with optimized slots', async () => {
)
})
+ test('nested components with template slots', async () => {
+ const Child = {
+ props: ['msg'],
+ template: `<div class="child"><slot msg="from slot"></slot></div>`
+ }
+
+ const app = createApp({
+ template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
+ })
+ app.component('Child', Child)
+
+ expect(await renderToString(app)).toBe(
+ `<div>parent<div class="child">` +
+ `<!----><span>from slot</span><!---->` +
+ `</div></div>`
+ )
+ })
+
+ 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: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
+ })
+ app.component('Child', Child)
+
+ expect(await renderToString(app)).toBe(
+ `<div>parent<div class="child">` +
+ `<span>from slot</span>` +
+ `</div></div>`
+ )
+ })
+
test('async components', async () => {
const Child = {
// should wait for resovled render context from setup()
Portal,
ShapeFlags,
ssrUtils,
- Slots
+ Slots,
+ warn
} from 'vue'
import {
isString,
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,
}
}
+type SSRRenderFunction = (
+ ctx: any,
+ push: (item: any) => void,
+ parentInstance: ComponentInternalInstance
+) => void
+const compileCache: Record<string, SSRRenderFunction> = 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<ResolvedSSRBuffer> {
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
} 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.`
)
}
}