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:
@@ -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)
|
||||
})()
|
||||
```
|
||||
|
||||
552
packages/server-renderer/__tests__/renderToString.spec.ts
Normal file
552
packages/server-renderer/__tests__/renderToString.spec.ts
Normal 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&" 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>`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
393
packages/server-renderer/__tests__/ssrDirectives.spec.ts
Normal file
393
packages/server-renderer/__tests__/ssrDirectives.spec.ts
Normal 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>`)
|
||||
})
|
||||
})
|
||||
29
packages/server-renderer/__tests__/ssrInterpolate.spec.ts
Normal file
29
packages/server-renderer/__tests__/ssrInterpolate.spec.ts
Normal 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(`<div>`)
|
||||
// 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
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
176
packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts
Normal file
176
packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts
Normal 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=""><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(`"><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(`"><script`)
|
||||
expect(
|
||||
ssrRenderStyle({
|
||||
color: `"><script`
|
||||
})
|
||||
).toBe(`color:"><script;`)
|
||||
})
|
||||
})
|
||||
53
packages/server-renderer/__tests__/ssrRenderList.spec.ts
Normal file
53
packages/server-renderer/__tests__/ssrRenderList.spec.ts
Normal 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([])
|
||||
})
|
||||
})
|
||||
125
packages/server-renderer/__tests__/ssrSuspense.spec.ts
Normal file
125
packages/server-renderer/__tests__/ssrSuspense.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
115
packages/server-renderer/__tests__/ssrTeleport.spec.ts
Normal file
115
packages/server-renderer/__tests__/ssrTeleport.spec.ts
Normal 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<!---->'
|
||||
)
|
||||
})
|
||||
})
|
||||
115
packages/server-renderer/__tests__/ssrVModelHelpers.spec.ts
Normal file
115
packages/server-renderer/__tests__/ssrVModelHelpers.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
5
packages/server-renderer/src/helpers/ssrInterpolate.ts
Normal file
5
packages/server-renderer/src/helpers/ssrInterpolate.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { escapeHtml, toDisplayString } from '@vue/shared'
|
||||
|
||||
export function ssrInterpolate(value: unknown): string {
|
||||
return escapeHtml(toDisplayString(value))
|
||||
}
|
||||
95
packages/server-renderer/src/helpers/ssrRenderAttrs.ts
Normal file
95
packages/server-renderer/src/helpers/ssrRenderAttrs.ts
Normal 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))
|
||||
}
|
||||
29
packages/server-renderer/src/helpers/ssrRenderList.ts
Normal file
29
packages/server-renderer/src/helpers/ssrRenderList.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
packages/server-renderer/src/helpers/ssrRenderSlot.ts
Normal file
37
packages/server-renderer/src/helpers/ssrRenderSlot.ts
Normal 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(`<!--]-->`)
|
||||
}
|
||||
14
packages/server-renderer/src/helpers/ssrRenderSuspense.ts
Normal file
14
packages/server-renderer/src/helpers/ssrRenderSuspense.ts
Normal 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(`<!---->`)
|
||||
}
|
||||
}
|
||||
42
packages/server-renderer/src/helpers/ssrRenderTeleport.ts
Normal file
42
packages/server-renderer/src/helpers/ssrRenderTeleport.ts
Normal 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-->')
|
||||
}
|
||||
50
packages/server-renderer/src/helpers/ssrVModelHelpers.ts
Normal file
50
packages/server-renderer/src/helpers/ssrVModelHelpers.ts
Normal 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 }
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
402
packages/server-renderer/src/renderToString.ts
Normal file
402
packages/server-renderer/src/renderToString.ts
Normal 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])
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user