Merge remote-tracking branch 'github/master' into changing_unwrap_ref

# Conflicts:
#	packages/reactivity/src/ref.ts
#	packages/runtime-core/__tests__/apiTemplateRef.spec.ts
#	packages/runtime-core/src/apiWatch.ts
This commit is contained in:
pikax
2020-04-08 21:21:04 +01:00
339 changed files with 26645 additions and 8965 deletions

View File

@@ -1 +1,16 @@
# @vue/server-renderer
``` js
const { createSSRApp } = require('vue')
const { renderToString } = require('@vue/server-renderer')
const app = createSSRApp({
data: () => ({ msg: 'hello' }),
template: `<div>{{ msg }}</div>`
})
;(async () => {
const html = await renderToString(app)
console.log(html)
})()
```

View File

@@ -0,0 +1,552 @@
import {
createApp,
h,
createCommentVNode,
withScopeId,
resolveComponent,
ComponentOptions,
ref,
defineComponent
} from 'vue'
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({
render() {
const Foo = resolveComponent('foo') as ComponentOptions
return h(Foo)
}
})
app.component('foo', {
render: () => h('div', 'foo')
})
const html = await renderToString(app)
expect(html).toBe(`<div>foo</div>`)
})
describe('components', () => {
test('vnode components', async () => {
expect(
await renderToString(
createApp({
data() {
return { msg: 'hello' }
},
render(this: any) {
return h('div', this.msg)
}
})
)
).toBe(`<div>hello</div>`)
})
test('option components returning render from setup', async () => {
expect(
await renderToString(
createApp({
setup() {
const msg = ref('hello')
return () => h('div', msg.value)
}
})
)
).toBe(`<div>hello</div>`)
})
test('setup components returning render from setup', async () => {
expect(
await renderToString(
createApp(
defineComponent((props: {}) => {
const msg = ref('hello')
return () => h('div', msg.value)
})
)
)
).toBe(`<div>hello</div>`)
})
test('optimized components', async () => {
expect(
await renderToString(
createApp({
data() {
return { msg: 'hello' }
},
ssrRender(ctx, push) {
push(`<div>${ctx.msg}</div>`)
}
})
)
).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(
'Template compilation error: Unexpected EOF in tag.\n' +
'1 | <\n' +
' | ^'
).toHaveBeenWarned()
})
})
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(`<div>parent<div>hello</div></div>`)
})
test('nested optimized components', async () => {
const Child = {
props: ['msg'],
ssrRender(ctx: any, push: any) {
push(`<div>${ctx.msg}</div>`)
}
}
expect(
await renderToString(
createApp({
ssrRender(_ctx, push, parent) {
push(`<div>parent`)
push(renderComponent(Child, { msg: 'hello' }, null, parent))
push(`</div>`)
}
})
)
).toBe(`<div>parent<div>hello</div></div>`)
})
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) {
push(`<div>${ctx.msg}</div>`)
}
}
const VNodeChild = {
props: ['msg'],
render(this: any) {
return h('div', this.msg)
}
}
const TemplateChild = {
props: ['msg'],
template: `<div>{{ msg }}</div>`
}
expect(
await renderToString(
createApp({
ssrRender(_ctx, push, parent) {
push(`<div>parent`)
push(
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>template</div></div>`
)
})
test('nested components with optimized slots', async () => {
const Child = {
props: ['msg'],
ssrRender(ctx: any, push: any, parent: any) {
push(`<div class="child">`)
ssrRenderSlot(
ctx.$slots,
'default',
{ msg: 'from slot' },
() => {
push(`fallback`)
},
push,
parent
)
push(`</div>`)
}
}
expect(
await renderToString(
createApp({
ssrRender(_ctx, push, parent) {
push(`<div>parent`)
push(
renderComponent(
Child,
{ msg: 'hello' },
{
// optimized slot using string push
default: ({ msg }: any, push: any, p: any) => {
push(`<span>${msg}</span>`)
},
// important to avoid slots being normalized
_: 1 as any
},
parent
)
)
push(`</div>`)
}
})
)
).toBe(
`<div>parent<div class="child">` +
`<!--[--><span>from slot</span><!--]-->` +
`</div></div>`
)
// test fallback
expect(
await renderToString(
createApp({
ssrRender(_ctx, push, parent) {
push(`<div>parent`)
push(renderComponent(Child, { msg: 'hello' }, null, parent))
push(`</div>`)
}
})
)
).toBe(
`<div>parent<div class="child"><!--[-->fallback<!--]--></div></div>`
)
})
test('nested components with vnode slots', async () => {
const Child = {
props: ['msg'],
ssrRender(ctx: any, push: any, parent: any) {
push(`<div class="child">`)
ssrRenderSlot(
ctx.$slots,
'default',
{ msg: 'from slot' },
null,
push,
parent
)
push(`</div>`)
}
}
expect(
await renderToString(
createApp({
ssrRender(_ctx, push, parent) {
push(`<div>parent`)
push(
renderComponent(
Child,
{ msg: 'hello' },
{
// bailed slots returning raw vnodes
default: ({ msg }: any) => {
return h('span', msg)
}
},
parent
)
)
push(`</div>`)
}
})
)
).toBe(
`<div>parent<div class="child">` +
`<!--[--><span>from slot</span><!--]-->` +
`</div></div>`
)
})
test('nested components with template slots', async () => {
const Child = {
props: ['msg'],
template: `<div class="child"><slot msg="from slot"></slot></div>`
}
const app = createApp({
components: { Child },
template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
})
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">` +
// no comment anchors because slot is used directly as element children
`<span>from slot</span>` +
`</div></div>`
)
})
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(`<div>${ctx.msg}</div>`)
}
}
expect(
await renderToString(
createApp({
ssrRender(_ctx, push, parent) {
push(`<div>parent`)
push(renderComponent(Child, null, null, parent))
push(`</div>`)
}
})
)
).toBe(`<div>parent<div>hello</div></div>`)
})
test('parallel async components', async () => {
const OptimizedChild = {
props: ['msg'],
async setup(props: any) {
return {
localMsg: props.msg + '!'
}
},
ssrRender(ctx: any, push: any) {
push(`<div>${ctx.localMsg}</div>`)
}
}
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(`<div>parent`)
push(
renderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
)
push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent))
push(`</div>`)
}
})
)
).toBe(`<div>parent<div>opt!</div><div>vnode!</div></div>`)
})
})
describe('vnode element', () => {
test('props', async () => {
expect(
await renderToString(
h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello')
)
).toBe(`<div id="foo&amp;" class="bar baz">hello</div>`)
})
test('text children', async () => {
expect(await renderToString(h('div', 'hello'))).toBe(`<div>hello</div>`)
})
test('array children', async () => {
expect(
await renderToString(
h('div', [
'foo',
h('span', 'bar'),
[h('span', 'baz')],
createCommentVNode('qux')
])
)
).toBe(
`<div>foo<span>bar</span><!--[--><span>baz</span><!--]--><!--qux--></div>`
)
})
test('void elements', async () => {
expect(await renderToString(h('input'))).toBe(`<input>`)
})
test('innerHTML', async () => {
expect(
await renderToString(
h(
'div',
{
innerHTML: `<span>hello</span>`
},
'ignored'
)
)
).toBe(`<div><span>hello</span></div>`)
})
test('textContent', async () => {
expect(
await renderToString(
h(
'div',
{
textContent: `<span>hello</span>`
},
'ignored'
)
)
).toBe(`<div>${escapeHtml(`<span>hello</span>`)}</div>`)
})
test('textarea value', async () => {
expect(
await renderToString(
h(
'textarea',
{
value: `<span>hello</span>`
},
'ignored'
)
)
).toBe(`<textarea>${escapeHtml(`<span>hello</span>`)}</textarea>`)
})
})
describe('scopeId', () => {
// note: here we are only testing scopeId handling for vdom serialization.
// compiled srr render functions will include scopeId directly in strings.
const withId = withScopeId('data-v-test')
const withChildId = withScopeId('data-v-child')
test('basic', async () => {
expect(
await renderToString(
withId(() => {
return h('div')
})()
)
).toBe(`<div data-v-test></div>`)
})
test('with slots', async () => {
const Child = {
__scopeId: 'data-v-child',
render: withChildId(function(this: any) {
return h('div', this.$slots.default())
})
}
const Parent = {
__scopeId: 'data-v-test',
render: withId(() => {
return h(Child, null, {
default: withId(() => h('span', 'slot'))
})
})
}
expect(await renderToString(h(Parent))).toBe(
`<div data-v-test data-v-child><span data-v-test data-v-child-s>slot</span></div>`
)
})
})
})

View File

@@ -0,0 +1,393 @@
import { renderToString } from '../src/renderToString'
import {
createApp,
h,
withDirectives,
vShow,
vModelText,
vModelRadio,
vModelCheckbox
} from 'vue'
describe('ssr: directives', () => {
describe('template v-show', () => {
test('basic', async () => {
expect(
await renderToString(
createApp({
template: `<div v-show="true"/>`
})
)
).toBe(`<div style=""></div>`)
expect(
await renderToString(
createApp({
template: `<div v-show="false"/>`
})
)
).toBe(`<div style="display:none;"></div>`)
})
test('with static style', async () => {
expect(
await renderToString(
createApp({
template: `<div style="color:red" v-show="false"/>`
})
)
).toBe(`<div style="color:red;display:none;"></div>`)
})
test('with dynamic style', async () => {
expect(
await renderToString(
createApp({
data: () => ({ style: { color: 'red' } }),
template: `<div :style="style" v-show="false"/>`
})
)
).toBe(`<div style="color:red;display:none;"></div>`)
})
test('with static + dynamic style', async () => {
expect(
await renderToString(
createApp({
data: () => ({ style: { color: 'red' } }),
template: `<div :style="style" style="font-size:12;" v-show="false"/>`
})
)
).toBe(`<div style="color:red;font-size:12;display:none;"></div>`)
})
})
describe('template v-model', () => {
test('text', async () => {
expect(
await renderToString(
createApp({
data: () => ({ text: 'hello' }),
template: `<input v-model="text">`
})
)
).toBe(`<input value="hello">`)
})
test('radio', async () => {
expect(
await renderToString(
createApp({
data: () => ({ selected: 'foo' }),
template: `<input type="radio" value="foo" v-model="selected">`
})
)
).toBe(`<input type="radio" value="foo" checked>`)
expect(
await renderToString(
createApp({
data: () => ({ selected: 'foo' }),
template: `<input type="radio" value="bar" v-model="selected">`
})
)
).toBe(`<input type="radio" value="bar">`)
// non-string values
expect(
await renderToString(
createApp({
data: () => ({ selected: 'foo' }),
template: `<input type="radio" :value="{}" v-model="selected">`
})
)
).toBe(`<input type="radio">`)
})
test('checkbox', async () => {
expect(
await renderToString(
createApp({
data: () => ({ checked: true }),
template: `<input type="checkbox" v-model="checked">`
})
)
).toBe(`<input type="checkbox" checked>`)
expect(
await renderToString(
createApp({
data: () => ({ checked: false }),
template: `<input type="checkbox" v-model="checked">`
})
)
).toBe(`<input type="checkbox">`)
expect(
await renderToString(
createApp({
data: () => ({ checked: ['foo'] }),
template: `<input type="checkbox" value="foo" v-model="checked">`
})
)
).toBe(`<input type="checkbox" value="foo" checked>`)
expect(
await renderToString(
createApp({
data: () => ({ checked: [] }),
template: `<input type="checkbox" value="foo" v-model="checked">`
})
)
).toBe(`<input type="checkbox" value="foo">`)
})
test('textarea', async () => {
expect(
await renderToString(
createApp({
data: () => ({ foo: 'hello' }),
template: `<textarea v-model="foo"/>`
})
)
).toBe(`<textarea>hello</textarea>`)
})
test('dynamic type', async () => {
expect(
await renderToString(
createApp({
data: () => ({ type: 'text', model: 'hello' }),
template: `<input :type="type" v-model="model">`
})
)
).toBe(`<input type="text" value="hello">`)
expect(
await renderToString(
createApp({
data: () => ({ type: 'checkbox', model: true }),
template: `<input :type="type" v-model="model">`
})
)
).toBe(`<input type="checkbox" checked>`)
expect(
await renderToString(
createApp({
data: () => ({ type: 'checkbox', model: false }),
template: `<input :type="type" v-model="model">`
})
)
).toBe(`<input type="checkbox">`)
expect(
await renderToString(
createApp({
data: () => ({ type: 'checkbox', model: ['hello'] }),
template: `<input :type="type" value="hello" v-model="model">`
})
)
).toBe(`<input type="checkbox" value="hello" checked>`)
expect(
await renderToString(
createApp({
data: () => ({ type: 'checkbox', model: [] }),
template: `<input :type="type" value="hello" v-model="model">`
})
)
).toBe(`<input type="checkbox" value="hello">`)
expect(
await renderToString(
createApp({
data: () => ({ type: 'radio', model: 'hello' }),
template: `<input :type="type" value="hello" v-model="model">`
})
)
).toBe(`<input type="radio" value="hello" checked>`)
expect(
await renderToString(
createApp({
data: () => ({ type: 'radio', model: 'hello' }),
template: `<input :type="type" value="bar" v-model="model">`
})
)
).toBe(`<input type="radio" value="bar">`)
})
test('with v-bind', async () => {
expect(
await renderToString(
createApp({
data: () => ({
obj: { type: 'radio', value: 'hello' },
model: 'hello'
}),
template: `<input v-bind="obj" v-model="model">`
})
)
).toBe(`<input type="radio" value="hello" checked>`)
})
})
describe('vnode v-show', () => {
test('basic', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('div'), [[vShow, true]])
}
})
)
).toBe(`<div></div>`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('div'), [[vShow, false]])
}
})
)
).toBe(`<div style="display:none;"></div>`)
})
test('with merge', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('div', {
style: {
color: 'red'
}
}),
[[vShow, false]]
)
}
})
)
).toBe(`<div style="color:red;display:none;"></div>`)
})
})
describe('vnode v-model', () => {
test('text', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('input'), [[vModelText, 'hello']])
}
})
)
).toBe(`<input value="hello">`)
})
test('radio', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('input', { type: 'radio', value: 'hello' }),
[[vModelRadio, 'hello']]
)
}
})
)
).toBe(`<input type="radio" value="hello" checked>`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('input', { type: 'radio', value: 'hello' }),
[[vModelRadio, 'foo']]
)
}
})
)
).toBe(`<input type="radio" value="hello">`)
})
test('checkbox', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('input', { type: 'checkbox' }), [
[vModelCheckbox, true]
])
}
})
)
).toBe(`<input type="checkbox" checked>`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('input', { type: 'checkbox' }), [
[vModelCheckbox, false]
])
}
})
)
).toBe(`<input type="checkbox">`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('input', { type: 'checkbox', value: 'foo' }),
[[vModelCheckbox, ['foo']]]
)
}
})
)
).toBe(`<input type="checkbox" value="foo" checked>`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('input', { type: 'checkbox', value: 'foo' }),
[[vModelCheckbox, []]]
)
}
})
)
).toBe(`<input type="checkbox" value="foo">`)
})
})
test('custom directive w/ getSSRProps', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('div'), [
[
{
getSSRProps({ value }) {
return { id: value }
}
},
'foo'
]
])
}
})
)
).toBe(`<div id="foo"></div>`)
})
})

View File

@@ -0,0 +1,29 @@
import { ssrInterpolate } from '../src/helpers/ssrInterpolate'
import { escapeHtml } from '@vue/shared'
test('ssr: interpolate', () => {
expect(ssrInterpolate(0)).toBe(`0`)
expect(ssrInterpolate(`foo`)).toBe(`foo`)
expect(ssrInterpolate(`<div>`)).toBe(`&lt;div&gt;`)
// should escape interpolated values
expect(ssrInterpolate([1, 2, 3])).toBe(
escapeHtml(JSON.stringify([1, 2, 3], null, 2))
)
expect(
ssrInterpolate({
foo: 1,
bar: `<div>`
})
).toBe(
escapeHtml(
JSON.stringify(
{
foo: 1,
bar: `<div>`
},
null,
2
)
)
)
})

View File

@@ -0,0 +1,176 @@
import {
ssrRenderAttrs,
ssrRenderClass,
ssrRenderStyle,
ssrRenderAttr
} from '../src/helpers/ssrRenderAttrs'
import { escapeHtml } from '@vue/shared'
describe('ssr: renderAttrs', () => {
test('ignore reserved props', () => {
expect(
ssrRenderAttrs({
key: 1,
ref: () => {},
onClick: () => {}
})
).toBe('')
})
test('normal attrs', () => {
expect(
ssrRenderAttrs({
id: 'foo',
title: 'bar'
})
).toBe(` id="foo" title="bar"`)
})
test('empty value attrs', () => {
expect(
ssrRenderAttrs({
'data-v-abc': ''
})
).toBe(` data-v-abc`)
})
test('escape attrs', () => {
expect(
ssrRenderAttrs({
id: '"><script'
})
).toBe(` id="&quot;&gt;&lt;script"`)
})
test('boolean attrs', () => {
expect(
ssrRenderAttrs({
checked: true,
multiple: false
})
).toBe(` checked`) // boolean attr w/ false should be ignored
})
test('ignore falsy values', () => {
expect(
ssrRenderAttrs({
foo: false,
title: null,
baz: undefined
})
).toBe(` foo="false"`) // non boolean should render `false` as is
})
test('ingore non-renderable values', () => {
expect(
ssrRenderAttrs({
foo: {},
bar: [],
baz: () => {}
})
).toBe(``)
})
test('props to attrs', () => {
expect(
ssrRenderAttrs({
readOnly: true, // simple lower case conversion
htmlFor: 'foobar' // special cases
})
).toBe(` readonly for="foobar"`)
})
test('preserve name on custom element', () => {
expect(
ssrRenderAttrs(
{
fooBar: 'ok'
},
'my-el'
)
).toBe(` fooBar="ok"`)
})
})
describe('ssr: renderAttr', () => {
test('basic', () => {
expect(ssrRenderAttr('foo', 'bar')).toBe(` foo="bar"`)
})
test('null and undefined', () => {
expect(ssrRenderAttr('foo', null)).toBe(``)
expect(ssrRenderAttr('foo', undefined)).toBe(``)
})
test('escape', () => {
expect(ssrRenderAttr('foo', '<script>')).toBe(
` foo="${escapeHtml(`<script>`)}"`
)
})
})
describe('ssr: renderClass', () => {
test('via renderProps', () => {
expect(
ssrRenderAttrs({
class: ['foo', 'bar']
})
).toBe(` class="foo bar"`)
})
test('standalone', () => {
expect(ssrRenderClass(`foo`)).toBe(`foo`)
expect(ssrRenderClass([`foo`, `bar`])).toBe(`foo bar`)
expect(ssrRenderClass({ foo: true, bar: false })).toBe(`foo`)
expect(ssrRenderClass([{ foo: true, bar: false }, `baz`])).toBe(`foo baz`)
})
test('escape class values', () => {
expect(ssrRenderClass(`"><script`)).toBe(`&quot;&gt;&lt;script`)
})
})
describe('ssr: renderStyle', () => {
test('via renderProps', () => {
expect(
ssrRenderAttrs({
style: {
color: 'red'
}
})
).toBe(` style="color:red;"`)
})
test('standalone', () => {
expect(ssrRenderStyle(`color:red`)).toBe(`color:red`)
expect(
ssrRenderStyle({
color: `red`
})
).toBe(`color:red;`)
expect(
ssrRenderStyle([
{ color: `red` },
{ fontSize: `15px` } // case conversion
])
).toBe(`color:red;font-size:15px;`)
})
test('number handling', () => {
expect(
ssrRenderStyle({
fontSize: 15, // should be ignored since font-size requires unit
opacity: 0.5
})
).toBe(`opacity:0.5;`)
})
test('escape inline CSS', () => {
expect(ssrRenderStyle(`"><script`)).toBe(`&quot;&gt;&lt;script`)
expect(
ssrRenderStyle({
color: `"><script`
})
).toBe(`color:&quot;&gt;&lt;script;`)
})
})

View File

@@ -0,0 +1,53 @@
import { ssrRenderList } from '../src/helpers/ssrRenderList'
describe('ssr: renderList', () => {
let stack: string[] = []
beforeEach(() => {
stack = []
})
it('should render items in an array', () => {
ssrRenderList(['1', '2', '3'], (item, index) =>
stack.push(`node ${index}: ${item}`)
)
expect(stack).toEqual(['node 0: 1', 'node 1: 2', 'node 2: 3'])
})
it('should render characters of a string', () => {
ssrRenderList('abc', (item, index) => stack.push(`node ${index}: ${item}`))
expect(stack).toEqual(['node 0: a', 'node 1: b', 'node 2: c'])
})
it('should render integers 1 through N when given a number N', () => {
ssrRenderList(3, (item, index) => stack.push(`node ${index}: ${item}`))
expect(stack).toEqual(['node 0: 1', 'node 1: 2', 'node 2: 3'])
})
it('should render properties in an object', () => {
ssrRenderList({ a: 1, b: 2, c: 3 }, (item, key, index) =>
stack.push(`node ${index}/${key}: ${item}`)
)
expect(stack).toEqual(['node 0/a: 1', 'node 1/b: 2', 'node 2/c: 3'])
})
it('should render an item for entry in an iterable', () => {
const iterable = function*() {
yield 1
yield 2
yield 3
}
ssrRenderList(iterable(), (item, index) =>
stack.push(`node ${index}: ${item}`)
)
expect(stack).toEqual(['node 0: 1', 'node 1: 2', 'node 2: 3'])
})
it('should not render items when source is undefined', () => {
ssrRenderList(undefined, (item, index) =>
stack.push(`node ${index}: ${item}`)
)
expect(stack).toEqual([])
})
})

View File

@@ -0,0 +1,125 @@
import { createApp, h, Suspense } from 'vue'
import { renderToString } from '../src/renderToString'
import { mockWarn } from '@vue/shared'
describe('SSR Suspense', () => {
mockWarn()
const ResolvingAsync = {
async setup() {
return () => h('div', 'async')
}
}
const RejectingAsync = {
setup() {
return new Promise((_, reject) => reject('foo'))
}
}
test('content', async () => {
const Comp = {
render() {
return h(Suspense, null, {
default: h(ResolvingAsync),
fallback: h('div', 'fallback')
})
}
}
expect(await renderToString(createApp(Comp))).toBe(`<div>async</div>`)
})
test('reject', async () => {
const Comp = {
render() {
return h(Suspense, null, {
default: h(RejectingAsync),
fallback: h('div', 'fallback')
})
}
}
expect(await renderToString(createApp(Comp))).toBe(`<!---->`)
expect('Uncaught error in async setup').toHaveBeenWarned()
expect('missing template').toHaveBeenWarned()
})
test('2 components', async () => {
const Comp = {
render() {
return h(Suspense, null, {
default: h('div', [h(ResolvingAsync), h(ResolvingAsync)]),
fallback: h('div', 'fallback')
})
}
}
expect(await renderToString(createApp(Comp))).toBe(
`<div><div>async</div><div>async</div></div>`
)
})
test('resolving component + rejecting component', async () => {
const Comp = {
render() {
return h(Suspense, null, {
default: h('div', [h(ResolvingAsync), h(RejectingAsync)]),
fallback: h('div', 'fallback')
})
}
}
expect(await renderToString(createApp(Comp))).toBe(
`<div><div>async</div><!----></div>`
)
expect('Uncaught error in async setup').toHaveBeenWarned()
expect('missing template or render function').toHaveBeenWarned()
})
test('failing suspense in passing suspense', async () => {
const Comp = {
render() {
return h(Suspense, null, {
default: h('div', [
h(ResolvingAsync),
h(Suspense, null, {
default: h('div', [h(RejectingAsync)]),
fallback: h('div', 'fallback 2')
})
]),
fallback: h('div', 'fallback 1')
})
}
}
expect(await renderToString(createApp(Comp))).toBe(
`<div><div>async</div><div><!----></div></div>`
)
expect('Uncaught error in async setup').toHaveBeenWarned()
expect('missing template').toHaveBeenWarned()
})
test('passing suspense in failing suspense', async () => {
const Comp = {
render() {
return h(Suspense, null, {
default: h('div', [
h(RejectingAsync),
h(Suspense, null, {
default: h('div', [h(ResolvingAsync)]),
fallback: h('div', 'fallback 2')
})
]),
fallback: h('div', 'fallback 1')
})
}
}
expect(await renderToString(createApp(Comp))).toBe(
`<div><!----><div><div>async</div></div></div>`
)
expect('Uncaught error in async setup').toHaveBeenWarned()
expect('missing template').toHaveBeenWarned()
})
})

View File

@@ -0,0 +1,115 @@
import { createApp, h, Teleport } from 'vue'
import { renderToString, SSRContext } from '../src/renderToString'
import { ssrRenderTeleport } from '../src/helpers/ssrRenderTeleport'
describe('ssrRenderTeleport', () => {
test('teleport rendering (compiled)', async () => {
const ctx: SSRContext = {}
const html = await renderToString(
createApp({
data() {
return { msg: 'hello' }
},
ssrRender(_ctx, _push, _parent) {
ssrRenderTeleport(
_push,
_push => {
_push(`<div>content</div>`)
},
'#target',
false,
_parent
)
}
}),
ctx
)
expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe(`<div>content</div><!---->`)
})
test('teleport rendering (compiled + disabled)', async () => {
const ctx: SSRContext = {}
const html = await renderToString(
createApp({
data() {
return { msg: 'hello' }
},
ssrRender(_ctx, _push, _parent) {
ssrRenderTeleport(
_push,
_push => {
_push(`<div>content</div>`)
},
'#target',
true,
_parent
)
}
}),
ctx
)
expect(html).toBe(
'<!--teleport start--><div>content</div><!--teleport end-->'
)
expect(ctx.teleports!['#target']).toBe(`<!---->`)
})
test('teleport rendering (vnode)', async () => {
const ctx: SSRContext = {}
const html = await renderToString(
h(
Teleport,
{
to: `#target`
},
h('span', 'hello')
),
ctx
)
expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe('<span>hello</span><!---->')
})
test('teleport rendering (vnode + disabled)', async () => {
const ctx: SSRContext = {}
const html = await renderToString(
h(
Teleport,
{
to: `#target`,
disabled: true
},
h('span', 'hello')
),
ctx
)
expect(html).toBe(
'<!--teleport start--><span>hello</span><!--teleport end-->'
)
expect(ctx.teleports!['#target']).toBe(`<!---->`)
})
test('multiple teleports with same target', async () => {
const ctx: SSRContext = {}
const html = await renderToString(
h('div', [
h(
Teleport,
{
to: `#target`
},
h('span', 'hello')
),
h(Teleport, { to: `#target` }, 'world')
]),
ctx
)
expect(html).toBe(
'<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>'
)
expect(ctx.teleports!['#target']).toBe(
'<span>hello</span><!---->world<!---->'
)
})
})

View File

@@ -0,0 +1,115 @@
import {
ssrRenderDynamicModel,
ssrGetDynamicModelProps
// ssrGetDynamicModelProps
} from '../src/helpers/ssrVModelHelpers'
describe('ssr: v-model helpers', () => {
test('ssrRenderDynamicModel', () => {
expect(ssrRenderDynamicModel(null, 'foo', null)).toBe(` value="foo"`)
expect(ssrRenderDynamicModel('text', 'foo', null)).toBe(` value="foo"`)
expect(ssrRenderDynamicModel('email', 'foo', null)).toBe(` value="foo"`)
expect(ssrRenderDynamicModel('checkbox', true, null)).toBe(` checked`)
expect(ssrRenderDynamicModel('checkbox', false, null)).toBe(``)
expect(ssrRenderDynamicModel('checkbox', [1], '1')).toBe(` checked`)
expect(ssrRenderDynamicModel('checkbox', [1], 1)).toBe(` checked`)
expect(ssrRenderDynamicModel('checkbox', [1], 0)).toBe(``)
expect(ssrRenderDynamicModel('radio', 'foo', 'foo')).toBe(` checked`)
expect(ssrRenderDynamicModel('radio', 1, '1')).toBe(` checked`)
expect(ssrRenderDynamicModel('radio', 1, 0)).toBe(``)
})
test('ssrGetDynamicModelProps', () => {
expect(ssrGetDynamicModelProps({}, 'foo')).toMatchObject({ value: 'foo' })
expect(
ssrGetDynamicModelProps(
{
type: 'text'
},
'foo'
)
).toMatchObject({ value: 'foo' })
expect(
ssrGetDynamicModelProps(
{
type: 'email'
},
'foo'
)
).toMatchObject({ value: 'foo' })
expect(
ssrGetDynamicModelProps(
{
type: 'checkbox'
},
true
)
).toMatchObject({ checked: true })
expect(
ssrGetDynamicModelProps(
{
type: 'checkbox'
},
false
)
).toBe(null)
expect(
ssrGetDynamicModelProps(
{
type: 'checkbox',
value: '1'
},
[1]
)
).toMatchObject({ checked: true })
expect(
ssrGetDynamicModelProps(
{
type: 'checkbox',
value: 1
},
[1]
)
).toMatchObject({ checked: true })
expect(
ssrGetDynamicModelProps(
{
type: 'checkbox',
value: 0
},
[1]
)
).toBe(null)
expect(
ssrGetDynamicModelProps(
{
type: 'radio',
value: 'foo'
},
'foo'
)
).toMatchObject({ checked: true })
expect(
ssrGetDynamicModelProps(
{
type: 'radio',
value: '1'
},
1
)
).toMatchObject({ checked: true })
expect(
ssrGetDynamicModelProps(
{
type: 'radio',
value: 0
},
1
)
).toBe(null)
})
})

View File

@@ -1,13 +1,13 @@
{
"name": "@vue/server-renderer",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.11",
"description": "@vue/server-renderer",
"main": "index.js",
"types": "dist/server-renderer.d.ts",
"files": [
"index.js",
"dist"
],
"types": "dist/server-renderer.d.ts",
"buildOptions": {
"formats": [
"cjs"
@@ -15,7 +15,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/vue.git"
"url": "git+https://github.com/vuejs/vue-next.git"
},
"keywords": [
"vue"
@@ -23,7 +23,14 @@
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/vue/issues"
"url": "https://github.com/vuejs/vue-next/issues"
},
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/server-renderer#readme"
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/server-renderer#readme",
"peerDependencies": {
"vue": "3.0.0-alpha.11"
},
"dependencies": {
"@vue/shared": "3.0.0-alpha.11",
"@vue/compiler-ssr": "3.0.0-alpha.11"
}
}

View File

@@ -0,0 +1,5 @@
import { escapeHtml, toDisplayString } from '@vue/shared'
export function ssrInterpolate(value: unknown): string {
return escapeHtml(toDisplayString(value))
}

View File

@@ -0,0 +1,95 @@
import { escapeHtml, stringifyStyle } from '@vue/shared'
import {
normalizeClass,
normalizeStyle,
propsToAttrMap,
isString,
isOn,
isSSRSafeAttrName,
isBooleanAttr,
makeMap
} from '@vue/shared'
const shouldIgnoreProp = makeMap(`key,ref,innerHTML,textContent`)
export function ssrRenderAttrs(
props: Record<string, unknown>,
tag?: string
): string {
let ret = ''
for (const key in props) {
if (
shouldIgnoreProp(key) ||
isOn(key) ||
(tag === 'textarea' && key === 'value')
) {
continue
}
const value = props[key]
if (key === 'class') {
ret += ` class="${ssrRenderClass(value)}"`
} else if (key === 'style') {
ret += ` style="${ssrRenderStyle(value)}"`
} else {
ret += ssrRenderDynamicAttr(key, value, tag)
}
}
return ret
}
// render an attr with dynamic (unknown) key.
export function ssrRenderDynamicAttr(
key: string,
value: unknown,
tag?: string
): string {
if (!isRenderableValue(value)) {
return ``
}
const attrKey =
tag && tag.indexOf('-') > 0
? key // preserve raw name on custom elements
: propsToAttrMap[key] || key.toLowerCase()
if (isBooleanAttr(attrKey)) {
return value === false ? `` : ` ${attrKey}`
} else if (isSSRSafeAttrName(attrKey)) {
return value === '' ? ` ${attrKey}` : ` ${attrKey}="${escapeHtml(value)}"`
} else {
console.warn(
`[@vue/server-renderer] Skipped rendering unsafe attribute name: ${attrKey}`
)
return ``
}
}
// Render a v-bind attr with static key. The key is pre-processed at compile
// time and we only need to check and escape value.
export function ssrRenderAttr(key: string, value: unknown): string {
if (!isRenderableValue(value)) {
return ``
}
return ` ${key}="${escapeHtml(value)}"`
}
function isRenderableValue(value: unknown): boolean {
if (value == null) {
return false
}
const type = typeof value
return type === 'string' || type === 'number' || type === 'boolean'
}
export function ssrRenderClass(raw: unknown): string {
return escapeHtml(normalizeClass(raw))
}
export function ssrRenderStyle(raw: unknown): string {
if (!raw) {
return ''
}
if (isString(raw)) {
return escapeHtml(raw)
}
const styles = normalizeStyle(raw)
return escapeHtml(stringifyStyle(styles))
}

View File

@@ -0,0 +1,29 @@
import { isArray, isString, isObject } from '@vue/shared'
export function ssrRenderList(
source: unknown,
renderItem: (value: unknown, key: string | number, index?: number) => void
) {
if (isArray(source) || isString(source)) {
for (let i = 0, l = source.length; i < l; i++) {
renderItem(source[i], i)
}
} else if (typeof source === 'number') {
for (let i = 0; i < source; i++) {
renderItem(i + 1, i)
}
} else if (isObject(source)) {
if (source[Symbol.iterator as any]) {
const arr = Array.from(source as Iterable<any>)
for (let i = 0, l = arr.length; i < l; i++) {
renderItem(arr[i], i)
}
} else {
const keys = Object.keys(source)
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
renderItem(source[key], key, i)
}
}
}
}

View File

@@ -0,0 +1,37 @@
import { Props, PushFn, renderVNodeChildren } from '../renderToString'
import { ComponentInternalInstance, Slot, Slots } from 'vue'
export type SSRSlots = Record<string, SSRSlot>
export type SSRSlot = (
props: Props,
push: PushFn,
parentComponent: ComponentInternalInstance | null,
scopeId: string | null
) => void
export function ssrRenderSlot(
slots: Slots | SSRSlots,
slotName: string,
slotProps: Props,
fallbackRenderFn: (() => void) | null,
push: PushFn,
parentComponent: ComponentInternalInstance
) {
// template-compiled slots are always rendered as fragments
push(`<!--[-->`)
const slotFn = slots[slotName]
if (slotFn) {
if (slotFn.length > 1) {
// only ssr-optimized slot fns accept more than 1 arguments
const scopeId = parentComponent && parentComponent.type.__scopeId
slotFn(slotProps, push, parentComponent, scopeId ? ` ${scopeId}-s` : ``)
} else {
// normal slot
renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent)
}
} else if (fallbackRenderFn) {
fallbackRenderFn()
}
push(`<!--]-->`)
}

View File

@@ -0,0 +1,14 @@
import { PushFn } from '../renderToString'
export async function ssrRenderSuspense(
push: PushFn,
{ default: renderContent }: Record<string, (() => void) | undefined>
) {
if (renderContent) {
push(`<!--[-->`)
renderContent()
push(`<!--]-->`)
} else {
push(`<!---->`)
}
}

View File

@@ -0,0 +1,42 @@
import { ComponentInternalInstance, ssrContextKey } from 'vue'
import {
SSRContext,
createBuffer,
PushFn,
SSRBufferItem
} from '../renderToString'
export function ssrRenderTeleport(
parentPush: PushFn,
contentRenderFn: (push: PushFn) => void,
target: string,
disabled: boolean,
parentComponent: ComponentInternalInstance
) {
parentPush('<!--teleport start-->')
let teleportContent: SSRBufferItem
if (disabled) {
contentRenderFn(parentPush)
teleportContent = `<!---->`
} else {
const { getBuffer, push } = createBuffer()
contentRenderFn(push)
push(`<!---->`) // teleport end anchor
teleportContent = getBuffer()
}
const context = parentComponent.appContext.provides[
ssrContextKey as any
] as SSRContext
const teleportBuffers =
context.__teleportBuffers || (context.__teleportBuffers = {})
if (teleportBuffers[target]) {
teleportBuffers[target].push(teleportContent)
} else {
teleportBuffers[target] = [teleportContent]
}
parentPush('<!--teleport end-->')
}

View File

@@ -0,0 +1,50 @@
import { looseEqual, looseIndexOf } from '@vue/shared'
import { ssrRenderAttr } from './ssrRenderAttrs'
export const ssrLooseEqual = looseEqual as (a: unknown, b: unknown) => boolean
export function ssrLooseContain(arr: unknown[], value: unknown): boolean {
return looseIndexOf(arr, value) > -1
}
// for <input :type="type" v-model="model" value="value">
export function ssrRenderDynamicModel(
type: unknown,
model: unknown,
value: unknown
) {
switch (type) {
case 'radio':
return looseEqual(model, value) ? ' checked' : ''
case 'checkbox':
return (Array.isArray(model)
? ssrLooseContain(model, value)
: model)
? ' checked'
: ''
default:
// text types
return ssrRenderAttr('value', model)
}
}
// for <input v-bind="obj" v-model="model">
export function ssrGetDynamicModelProps(
existingProps: any = {},
model: unknown
) {
const { type, value } = existingProps
switch (type) {
case 'radio':
return looseEqual(model, value) ? { checked: true } : null
case 'checkbox':
return (Array.isArray(model)
? ssrLooseContain(model, value)
: model)
? { checked: true }
: null
default:
// text types
return { value: model }
}
}

View File

@@ -1,3 +1,25 @@
export function renderToString() {
// TODO
}
// public
export { renderToString } from './renderToString'
// internal runtime helpers
export { renderComponent as ssrRenderComponent } from './renderToString'
export { ssrRenderSlot } from './helpers/ssrRenderSlot'
export {
ssrRenderClass,
ssrRenderStyle,
ssrRenderAttrs,
ssrRenderAttr,
ssrRenderDynamicAttr
} from './helpers/ssrRenderAttrs'
export { ssrInterpolate } from './helpers/ssrInterpolate'
export { ssrRenderList } from './helpers/ssrRenderList'
export { ssrRenderTeleport } from './helpers/ssrRenderTeleport'
export { ssrRenderSuspense } from './helpers/ssrRenderSuspense'
// v-model helpers
export {
ssrLooseEqual,
ssrLooseContain,
ssrRenderDynamicModel,
ssrGetDynamicModelProps
} from './helpers/ssrVModelHelpers'

View File

@@ -0,0 +1,402 @@
import {
App,
Component,
ComponentInternalInstance,
VNode,
VNodeArrayChildren,
createVNode,
Text,
Comment,
Fragment,
ssrUtils,
Slots,
createApp,
ssrContextKey,
warn,
DirectiveBinding,
VNodeProps,
mergeProps
} from 'vue'
import {
ShapeFlags,
isString,
isPromise,
isArray,
isFunction,
isVoidTag,
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'
import { ssrRenderTeleport } from './helpers/ssrRenderTeleport'
const {
isVNode,
createComponentInstance,
setCurrentRenderingInstance,
setupComponent,
renderComponentRoot,
normalizeVNode,
normalizeSuspenseChildren
} = ssrUtils
// Each component has a buffer array.
// A buffer array can contain one of the following:
// - plain string
// - A resolved buffer (recursive arrays of strings that can be unrolled
// synchronously)
// - An async buffer (a Promise that resolves to a resolved buffer)
export type SSRBuffer = SSRBufferItem[]
export type SSRBufferItem =
| string
| ResolvedSSRBuffer
| Promise<ResolvedSSRBuffer>
export type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
export type PushFn = (item: SSRBufferItem) => void
export type Props = Record<string, unknown>
export type SSRContext = {
[key: string]: any
teleports?: Record<string, string>
__teleportBuffers?: Record<string, SSRBuffer>
}
export function createBuffer() {
let appendable = false
let hasAsync = false
const buffer: SSRBuffer = []
return {
getBuffer(): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
// If the current component's buffer contains any Promise from async children,
// then it must return a Promise too. Otherwise this is a component that
// contains only sync children so we can avoid the async book-keeping overhead.
return hasAsync ? Promise.all(buffer) : (buffer as ResolvedSSRBuffer)
},
push(item: SSRBufferItem) {
const isStringItem = isString(item)
if (appendable && isStringItem) {
buffer[buffer.length - 1] += item as string
} else {
buffer.push(item)
}
appendable = isStringItem
if (!isStringItem && !isArray(item)) {
// promise
hasAsync = true
}
}
}
}
function unrollBuffer(buffer: ResolvedSSRBuffer): string {
let ret = ''
for (let i = 0; i < buffer.length; i++) {
const item = buffer[i]
if (isString(item)) {
ret += item
} else {
ret += unrollBuffer(item)
}
}
return ret
}
export async function renderToString(
input: App | VNode,
context: SSRContext = {}
): Promise<string> {
if (isVNode(input)) {
// raw vnode, wrap with app (for context)
return renderToString(createApp({ render: () => input }), context)
}
// rendering an app
const vnode = createVNode(input._component, input._props)
vnode.appContext = input._context
// provide the ssr context to the tree
input.provide(ssrContextKey, context)
const buffer = await renderComponentVNode(vnode)
await resolveTeleports(context)
return unrollBuffer(buffer)
}
export function renderComponent(
comp: Component,
props: Props | null = null,
children: Slots | SSRSlots | null = null,
parentComponent: ComponentInternalInstance | null = null
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
return renderComponentVNode(
createVNode(comp, props, children),
parentComponent
)
}
function renderComponentVNode(
vnode: VNode,
parentComponent: ComponentInternalInstance | null = null
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
const instance = createComponentInstance(vnode, parentComponent, null)
const res = setupComponent(instance, true /* isSSR */)
if (isPromise(res)) {
return res
.catch(err => {
warn(`[@vue/server-renderer]: Uncaught error in async setup:\n`, err)
})
.then(() => renderComponentSubTree(instance))
} else {
return renderComponentSubTree(instance)
}
}
function renderComponentSubTree(
instance: ComponentInternalInstance
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
const comp = instance.type as Component
const { getBuffer, push } = createBuffer()
if (isFunction(comp)) {
renderVNode(push, renderComponentRoot(instance), instance)
} else {
if (!instance.render && !comp.ssrRender && isString(comp.template)) {
comp.ssrRender = ssrCompile(comp.template, instance)
}
if (comp.ssrRender) {
// optimized
// set current rendering instance for asset resolution
setCurrentRenderingInstance(instance)
comp.ssrRender(instance.proxy, push, instance)
setCurrentRenderingInstance(null)
} else if (instance.render) {
renderVNode(push, renderComponentRoot(instance), instance)
} else {
warn(
`Component ${
comp.name ? `${comp.name} ` : ``
} is missing template or render function.`
)
push(`<!---->`)
}
}
return getBuffer()
}
type SSRRenderFunction = (
context: 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 = `[@vue/server-renderer] 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('require', code)(require))
}
function renderVNode(
push: PushFn,
vnode: VNode,
parentComponent: ComponentInternalInstance
) {
const { type, shapeFlag, children } = vnode
switch (type) {
case Text:
push(children as string)
break
case Comment:
push(children ? `<!--${children}-->` : `<!---->`)
break
case Fragment:
push(`<!--[-->`) // open
renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
push(`<!--]-->`) // close
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
renderElementVNode(push, vnode, parentComponent)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
push(renderComponentVNode(vnode, parentComponent))
} else if (shapeFlag & ShapeFlags.TELEPORT) {
renderTeleportVNode(push, vnode, parentComponent)
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
renderVNode(
push,
normalizeSuspenseChildren(vnode).content,
parentComponent
)
} else {
warn(
'[@vue/server-renderer] Invalid VNode type:',
type,
`(${typeof type})`
)
}
}
}
export function renderVNodeChildren(
push: PushFn,
children: VNodeArrayChildren,
parentComponent: ComponentInternalInstance
) {
for (let i = 0; i < children.length; i++) {
renderVNode(push, normalizeVNode(children[i]), parentComponent)
}
}
function renderElementVNode(
push: PushFn,
vnode: VNode,
parentComponent: ComponentInternalInstance
) {
const tag = vnode.type as string
let { props, children, shapeFlag, scopeId, dirs } = vnode
let openTag = `<${tag}`
if (dirs) {
props = applySSRDirectives(vnode, props, dirs)
}
if (props) {
openTag += ssrRenderAttrs(props, tag)
}
if (scopeId) {
openTag += ` ${scopeId}`
const treeOwnerId = parentComponent && parentComponent.type.__scopeId
// vnode's own scopeId and the current rendering component's scopeId is
// different - this is a slot content node.
if (treeOwnerId && treeOwnerId !== scopeId) {
openTag += ` ${treeOwnerId}-s`
}
}
push(openTag + `>`)
if (!isVoidTag(tag)) {
let hasChildrenOverride = false
if (props) {
if (props.innerHTML) {
hasChildrenOverride = true
push(props.innerHTML)
} else if (props.textContent) {
hasChildrenOverride = true
push(escapeHtml(props.textContent))
} else if (tag === 'textarea' && props.value) {
hasChildrenOverride = true
push(escapeHtml(props.value))
}
}
if (!hasChildrenOverride) {
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
push(escapeHtml(children as string))
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
renderVNodeChildren(
push,
children as VNodeArrayChildren,
parentComponent
)
}
}
push(`</${tag}>`)
}
}
function applySSRDirectives(
vnode: VNode,
rawProps: VNodeProps | null,
dirs: DirectiveBinding[]
): VNodeProps {
const toMerge: VNodeProps[] = []
for (let i = 0; i < dirs.length; i++) {
const binding = dirs[i]
const {
dir: { getSSRProps }
} = binding
if (getSSRProps) {
const props = getSSRProps(binding, vnode)
if (props) toMerge.push(props)
}
}
return mergeProps(rawProps || {}, ...toMerge)
}
function renderTeleportVNode(
push: PushFn,
vnode: VNode,
parentComponent: ComponentInternalInstance
) {
const target = vnode.props && vnode.props.to
const disabled = vnode.props && vnode.props.disabled
if (!target) {
warn(`[@vue/server-renderer] Teleport is missing target prop.`)
return []
}
if (!isString(target)) {
warn(
`[@vue/server-renderer] Teleport target must be a query selector string.`
)
return []
}
ssrRenderTeleport(
push,
push => {
renderVNodeChildren(
push,
vnode.children as VNodeArrayChildren,
parentComponent
)
},
target,
disabled || disabled === '',
parentComponent
)
}
async function resolveTeleports(context: SSRContext) {
if (context.__teleportBuffers) {
context.teleports = context.teleports || {}
for (const key in context.__teleportBuffers) {
// note: it's OK to await sequentially here because the Promises were
// created eagerly in parallel.
context.teleports[key] = unrollBuffer(
await Promise.all(context.__teleportBuffers[key])
)
}
}
}