feat(ssr): renderToStream (#1197)
This commit is contained in:
		
							parent
							
								
									e0d19a6953
								
							
						
					
					
						commit
						6bc0e0a31a
					
				| @ -11,9 +11,8 @@ import { | |||||||
|   defineAsyncComponent, |   defineAsyncComponent, | ||||||
|   defineComponent |   defineComponent | ||||||
| } from '@vue/runtime-dom' | } from '@vue/runtime-dom' | ||||||
| import { renderToString } from '@vue/server-renderer' | import { renderToString, SSRContext } from '@vue/server-renderer' | ||||||
| import { mockWarn } from '@vue/shared' | import { mockWarn } from '@vue/shared' | ||||||
| import { SSRContext } from 'packages/server-renderer/src/renderToString' |  | ||||||
| 
 | 
 | ||||||
| function mountWithHydration(html: string, render: () => any) { | function mountWithHydration(html: string, render: () => any) { | ||||||
|   const container = document.createElement('div') |   const container = document.createElement('div') | ||||||
|  | |||||||
							
								
								
									
										605
									
								
								packages/server-renderer/__tests__/renderToStream.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										605
									
								
								packages/server-renderer/__tests__/renderToStream.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,605 @@ | |||||||
|  | import { | ||||||
|  |   createApp, | ||||||
|  |   h, | ||||||
|  |   createCommentVNode, | ||||||
|  |   withScopeId, | ||||||
|  |   resolveComponent, | ||||||
|  |   ComponentOptions, | ||||||
|  |   ref, | ||||||
|  |   defineComponent, | ||||||
|  |   createTextVNode, | ||||||
|  |   createStaticVNode | ||||||
|  | } from 'vue' | ||||||
|  | import { escapeHtml, mockWarn } from '@vue/shared' | ||||||
|  | import { renderToStream as _renderToStream } from '../src/renderToStream' | ||||||
|  | import { Readable } from 'stream' | ||||||
|  | import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot' | ||||||
|  | import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent' | ||||||
|  | 
 | ||||||
|  | mockWarn() | ||||||
|  | 
 | ||||||
|  | const promisifyStream = (stream: Readable) => { | ||||||
|  |   return new Promise((resolve, reject) => { | ||||||
|  |     let result = '' | ||||||
|  |     stream.on('data', data => { | ||||||
|  |       result += data | ||||||
|  |     }) | ||||||
|  |     stream.on('error', () => { | ||||||
|  |       reject(result) | ||||||
|  |     }) | ||||||
|  |     stream.on('end', () => { | ||||||
|  |       resolve(result) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const renderToStream = (app: any, context?: any) => | ||||||
|  |   promisifyStream(_renderToStream(app, context)) | ||||||
|  | 
 | ||||||
|  | describe('ssr: renderToStream', () => { | ||||||
|  |   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 renderToStream(app) | ||||||
|  |     expect(html).toBe(`<div>foo</div>`) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   describe('components', () => { | ||||||
|  |     test('vnode components', async () => { | ||||||
|  |       expect( | ||||||
|  |         await renderToStream( | ||||||
|  |           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 renderToStream( | ||||||
|  |           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 renderToStream( | ||||||
|  |           createApp( | ||||||
|  |             defineComponent((props: {}) => { | ||||||
|  |               const msg = ref('hello') | ||||||
|  |               return () => h('div', msg.value) | ||||||
|  |             }) | ||||||
|  |           ) | ||||||
|  |         ) | ||||||
|  |       ).toBe(`<div>hello</div>`) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     test('optimized components', async () => { | ||||||
|  |       expect( | ||||||
|  |         await renderToStream( | ||||||
|  |           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 renderToStream( | ||||||
|  |             createApp({ | ||||||
|  |               data() { | ||||||
|  |                 return { msg: 'hello' } | ||||||
|  |               }, | ||||||
|  |               template: `<div>{{ msg }}</div>` | ||||||
|  |             }) | ||||||
|  |           ) | ||||||
|  |         ).toBe(`<div>hello</div>`) | ||||||
|  |       }) | ||||||
|  | 
 | ||||||
|  |       test('handle compiler errors', async () => { | ||||||
|  |         await renderToStream(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 renderToStream( | ||||||
|  |           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 renderToStream( | ||||||
|  |           createApp({ | ||||||
|  |             ssrRender(_ctx, push, parent) { | ||||||
|  |               push(`<div>parent`) | ||||||
|  |               push(ssrRenderComponent(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 renderToStream(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 renderToStream( | ||||||
|  |           createApp({ | ||||||
|  |             ssrRender(_ctx, push, parent) { | ||||||
|  |               push(`<div>parent`) | ||||||
|  |               push( | ||||||
|  |                 ssrRenderComponent(OptimizedChild, { msg: 'opt' }, null, parent) | ||||||
|  |               ) | ||||||
|  |               push( | ||||||
|  |                 ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent) | ||||||
|  |               ) | ||||||
|  |               push( | ||||||
|  |                 ssrRenderComponent( | ||||||
|  |                   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 renderToStream( | ||||||
|  |           createApp({ | ||||||
|  |             ssrRender(_ctx, push, parent) { | ||||||
|  |               push(`<div>parent`) | ||||||
|  |               push( | ||||||
|  |                 ssrRenderComponent( | ||||||
|  |                   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 renderToStream( | ||||||
|  |           createApp({ | ||||||
|  |             ssrRender(_ctx, push, parent) { | ||||||
|  |               push(`<div>parent`) | ||||||
|  |               push(ssrRenderComponent(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 renderToStream( | ||||||
|  |           createApp({ | ||||||
|  |             ssrRender(_ctx, push, parent) { | ||||||
|  |               push(`<div>parent`) | ||||||
|  |               push( | ||||||
|  |                 ssrRenderComponent( | ||||||
|  |                   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 renderToStream(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 renderToStream(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 resolved render context from setup()
 | ||||||
|  |         async setup() { | ||||||
|  |           return { | ||||||
|  |             msg: 'hello' | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         ssrRender(ctx: any, push: any) { | ||||||
|  |           push(`<div>${ctx.msg}</div>`) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       expect( | ||||||
|  |         await renderToStream( | ||||||
|  |           createApp({ | ||||||
|  |             ssrRender(_ctx, push, parent) { | ||||||
|  |               push(`<div>parent`) | ||||||
|  |               push(ssrRenderComponent(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 renderToStream( | ||||||
|  |           createApp({ | ||||||
|  |             ssrRender(_ctx, push, parent) { | ||||||
|  |               push(`<div>parent`) | ||||||
|  |               push( | ||||||
|  |                 ssrRenderComponent(OptimizedChild, { msg: 'opt' }, null, parent) | ||||||
|  |               ) | ||||||
|  |               push( | ||||||
|  |                 ssrRenderComponent(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 renderToStream( | ||||||
|  |           h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello') | ||||||
|  |         ) | ||||||
|  |       ).toBe(`<div id="foo&" class="bar baz">hello</div>`) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     test('text children', async () => { | ||||||
|  |       expect(await renderToStream(h('div', 'hello'))).toBe(`<div>hello</div>`) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     test('array children', async () => { | ||||||
|  |       expect( | ||||||
|  |         await renderToStream( | ||||||
|  |           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 renderToStream(h('input'))).toBe(`<input>`) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     test('innerHTML', async () => { | ||||||
|  |       expect( | ||||||
|  |         await renderToStream( | ||||||
|  |           h( | ||||||
|  |             'div', | ||||||
|  |             { | ||||||
|  |               innerHTML: `<span>hello</span>` | ||||||
|  |             }, | ||||||
|  |             'ignored' | ||||||
|  |           ) | ||||||
|  |         ) | ||||||
|  |       ).toBe(`<div><span>hello</span></div>`) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     test('textContent', async () => { | ||||||
|  |       expect( | ||||||
|  |         await renderToStream( | ||||||
|  |           h( | ||||||
|  |             'div', | ||||||
|  |             { | ||||||
|  |               textContent: `<span>hello</span>` | ||||||
|  |             }, | ||||||
|  |             'ignored' | ||||||
|  |           ) | ||||||
|  |         ) | ||||||
|  |       ).toBe(`<div>${escapeHtml(`<span>hello</span>`)}</div>`) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     test('textarea value', async () => { | ||||||
|  |       expect( | ||||||
|  |         await renderToStream( | ||||||
|  |           h( | ||||||
|  |             'textarea', | ||||||
|  |             { | ||||||
|  |               value: `<span>hello</span>` | ||||||
|  |             }, | ||||||
|  |             'ignored' | ||||||
|  |           ) | ||||||
|  |         ) | ||||||
|  |       ).toBe(`<textarea>${escapeHtml(`<span>hello</span>`)}</textarea>`) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   describe('raw vnode types', () => { | ||||||
|  |     test('Text', async () => { | ||||||
|  |       expect(await renderToStream(createTextVNode('hello <div>'))).toBe( | ||||||
|  |         `hello <div>` | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     test('Comment', async () => { | ||||||
|  |       // https://www.w3.org/TR/html52/syntax.html#comments
 | ||||||
|  |       expect( | ||||||
|  |         await renderToStream( | ||||||
|  |           h('div', [ | ||||||
|  |             createCommentVNode('>foo'), | ||||||
|  |             createCommentVNode('->foo'), | ||||||
|  |             createCommentVNode('<!--foo-->'), | ||||||
|  |             createCommentVNode('--!>foo<!-') | ||||||
|  |           ]) | ||||||
|  |         ) | ||||||
|  |       ).toBe(`<div><!--foo--><!--foo--><!--foo--><!--foo--></div>`) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     test('Static', async () => { | ||||||
|  |       const content = `<div id="ok">hello<span>world</span></div>` | ||||||
|  |       expect(await renderToStream(createStaticVNode(content, 1))).toBe(content) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   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 renderToStream( | ||||||
|  |           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 renderToStream(h(Parent))).toBe( | ||||||
|  |         `<div data-v-test data-v-child><span data-v-test data-v-child-s>slot</span></div>` | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
| @ -11,8 +11,9 @@ import { | |||||||
|   createStaticVNode |   createStaticVNode | ||||||
| } from 'vue' | } from 'vue' | ||||||
| import { escapeHtml, mockWarn } from '@vue/shared' | import { escapeHtml, mockWarn } from '@vue/shared' | ||||||
| import { renderToString, renderComponent } from '../src/renderToString' | import { renderToString } from '../src/renderToString' | ||||||
| import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot' | import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot' | ||||||
|  | import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent' | ||||||
| 
 | 
 | ||||||
| mockWarn() | mockWarn() | ||||||
| 
 | 
 | ||||||
| @ -145,7 +146,7 @@ describe('ssr: renderToString', () => { | |||||||
|           createApp({ |           createApp({ | ||||||
|             ssrRender(_ctx, push, parent) { |             ssrRender(_ctx, push, parent) { | ||||||
|               push(`<div>parent`) |               push(`<div>parent`) | ||||||
|               push(renderComponent(Child, { msg: 'hello' }, null, parent)) |               push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent)) | ||||||
|               push(`</div>`) |               push(`</div>`) | ||||||
|             } |             } | ||||||
|           }) |           }) | ||||||
| @ -194,11 +195,13 @@ describe('ssr: renderToString', () => { | |||||||
|             ssrRender(_ctx, push, parent) { |             ssrRender(_ctx, push, parent) { | ||||||
|               push(`<div>parent`) |               push(`<div>parent`) | ||||||
|               push( |               push( | ||||||
|                 renderComponent(OptimizedChild, { msg: 'opt' }, null, parent) |                 ssrRenderComponent(OptimizedChild, { msg: 'opt' }, null, parent) | ||||||
|               ) |               ) | ||||||
|               push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent)) |  | ||||||
|               push( |               push( | ||||||
|                 renderComponent( |                 ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent) | ||||||
|  |               ) | ||||||
|  |               push( | ||||||
|  |                 ssrRenderComponent( | ||||||
|                   TemplateChild, |                   TemplateChild, | ||||||
|                   { msg: 'template' }, |                   { msg: 'template' }, | ||||||
|                   null, |                   null, | ||||||
| @ -239,7 +242,7 @@ describe('ssr: renderToString', () => { | |||||||
|             ssrRender(_ctx, push, parent) { |             ssrRender(_ctx, push, parent) { | ||||||
|               push(`<div>parent`) |               push(`<div>parent`) | ||||||
|               push( |               push( | ||||||
|                 renderComponent( |                 ssrRenderComponent( | ||||||
|                   Child, |                   Child, | ||||||
|                   { msg: 'hello' }, |                   { msg: 'hello' }, | ||||||
|                   { |                   { | ||||||
| @ -269,7 +272,7 @@ describe('ssr: renderToString', () => { | |||||||
|           createApp({ |           createApp({ | ||||||
|             ssrRender(_ctx, push, parent) { |             ssrRender(_ctx, push, parent) { | ||||||
|               push(`<div>parent`) |               push(`<div>parent`) | ||||||
|               push(renderComponent(Child, { msg: 'hello' }, null, parent)) |               push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent)) | ||||||
|               push(`</div>`) |               push(`</div>`) | ||||||
|             } |             } | ||||||
|           }) |           }) | ||||||
| @ -302,7 +305,7 @@ describe('ssr: renderToString', () => { | |||||||
|             ssrRender(_ctx, push, parent) { |             ssrRender(_ctx, push, parent) { | ||||||
|               push(`<div>parent`) |               push(`<div>parent`) | ||||||
|               push( |               push( | ||||||
|                 renderComponent( |                 ssrRenderComponent( | ||||||
|                   Child, |                   Child, | ||||||
|                   { msg: 'hello' }, |                   { msg: 'hello' }, | ||||||
|                   { |                   { | ||||||
| @ -388,7 +391,7 @@ describe('ssr: renderToString', () => { | |||||||
|           createApp({ |           createApp({ | ||||||
|             ssrRender(_ctx, push, parent) { |             ssrRender(_ctx, push, parent) { | ||||||
|               push(`<div>parent`) |               push(`<div>parent`) | ||||||
|               push(renderComponent(Child, null, null, parent)) |               push(ssrRenderComponent(Child, null, null, parent)) | ||||||
|               push(`</div>`) |               push(`</div>`) | ||||||
|             } |             } | ||||||
|           }) |           }) | ||||||
| @ -427,9 +430,11 @@ describe('ssr: renderToString', () => { | |||||||
|             ssrRender(_ctx, push, parent) { |             ssrRender(_ctx, push, parent) { | ||||||
|               push(`<div>parent`) |               push(`<div>parent`) | ||||||
|               push( |               push( | ||||||
|                 renderComponent(OptimizedChild, { msg: 'opt' }, null, parent) |                 ssrRenderComponent(OptimizedChild, { msg: 'opt' }, null, parent) | ||||||
|  |               ) | ||||||
|  |               push( | ||||||
|  |                 ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent) | ||||||
|               ) |               ) | ||||||
|               push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent)) |  | ||||||
|               push(`</div>`) |               push(`</div>`) | ||||||
|             } |             } | ||||||
|           }) |           }) | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import { createApp, h, Teleport } from 'vue' | import { createApp, h, Teleport } from 'vue' | ||||||
| import { renderToString, SSRContext } from '../src/renderToString' | import { renderToString } from '../src/renderToString' | ||||||
|  | import { SSRContext } from '../src/render' | ||||||
| import { ssrRenderTeleport } from '../src/helpers/ssrRenderTeleport' | import { ssrRenderTeleport } from '../src/helpers/ssrRenderTeleport' | ||||||
| 
 | 
 | ||||||
| describe('ssrRenderTeleport', () => { | describe('ssrRenderTeleport', () => { | ||||||
|  | |||||||
							
								
								
									
										46
									
								
								packages/server-renderer/src/helpers/ssrCompile.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								packages/server-renderer/src/helpers/ssrCompile.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | import { ComponentInternalInstance, warn } from 'vue' | ||||||
|  | import { compile } from '@vue/compiler-ssr' | ||||||
|  | import { generateCodeFrame, NO } from '@vue/shared' | ||||||
|  | import { CompilerError } from '@vue/compiler-core' | ||||||
|  | import { PushFn } from '../render' | ||||||
|  | 
 | ||||||
|  | type SSRRenderFunction = ( | ||||||
|  |   context: any, | ||||||
|  |   push: PushFn, | ||||||
|  |   parentInstance: ComponentInternalInstance | ||||||
|  | ) => void | ||||||
|  | 
 | ||||||
|  | const compileCache: Record<string, SSRRenderFunction> = Object.create(null) | ||||||
|  | 
 | ||||||
|  | export 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)) | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								packages/server-renderer/src/helpers/ssrRenderComponent.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								packages/server-renderer/src/helpers/ssrRenderComponent.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | import { Component, ComponentInternalInstance, createVNode, Slots } from 'vue' | ||||||
|  | import { Props, renderComponentVNode, SSRBuffer } from '../render' | ||||||
|  | import { SSRSlots } from './ssrRenderSlot' | ||||||
|  | 
 | ||||||
|  | export function ssrRenderComponent( | ||||||
|  |   comp: Component, | ||||||
|  |   props: Props | null = null, | ||||||
|  |   children: Slots | SSRSlots | null = null, | ||||||
|  |   parentComponent: ComponentInternalInstance | null = null | ||||||
|  | ): SSRBuffer | Promise<SSRBuffer> { | ||||||
|  |   return renderComponentVNode( | ||||||
|  |     createVNode(comp, props, children), | ||||||
|  |     parentComponent | ||||||
|  |   ) | ||||||
|  | } | ||||||
| @ -1,8 +1,7 @@ | |||||||
| import { Props, PushFn, renderVNodeChildren } from '../renderToString' |  | ||||||
| import { ComponentInternalInstance, Slot, Slots } from 'vue' | import { ComponentInternalInstance, Slot, Slots } from 'vue' | ||||||
|  | import { Props, PushFn, renderVNodeChildren } from '../render' | ||||||
| 
 | 
 | ||||||
| export type SSRSlots = Record<string, SSRSlot> | export type SSRSlots = Record<string, SSRSlot> | ||||||
| 
 |  | ||||||
| export type SSRSlot = ( | export type SSRSlot = ( | ||||||
|   props: Props, |   props: Props, | ||||||
|   push: PushFn, |   push: PushFn, | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { PushFn } from '../renderToString' | import { PushFn } from '../render' | ||||||
| 
 | 
 | ||||||
| export async function ssrRenderSuspense( | export async function ssrRenderSuspense( | ||||||
|   push: PushFn, |   push: PushFn, | ||||||
|  | |||||||
| @ -1,10 +1,5 @@ | |||||||
| import { ComponentInternalInstance, ssrContextKey } from 'vue' | import { ComponentInternalInstance, ssrContextKey } from 'vue' | ||||||
| import { | import { createBuffer, PushFn, SSRBufferItem, SSRContext } from '../render' | ||||||
|   SSRContext, |  | ||||||
|   createBuffer, |  | ||||||
|   PushFn, |  | ||||||
|   SSRBufferItem |  | ||||||
| } from '../renderToString' |  | ||||||
| 
 | 
 | ||||||
| export function ssrRenderTeleport( | export function ssrRenderTeleport( | ||||||
|   parentPush: PushFn, |   parentPush: PushFn, | ||||||
|  | |||||||
| @ -1,9 +1,12 @@ | |||||||
| // public
 | // public
 | ||||||
| export { renderToString, SSRContext } from './renderToString' | export { SSRContext } from './render' | ||||||
|  | export { renderToString } from './renderToString' | ||||||
|  | export { renderToStream } from './renderToStream' | ||||||
| 
 | 
 | ||||||
| // internal runtime helpers
 | // internal runtime helpers
 | ||||||
| export { renderComponent as ssrRenderComponent } from './renderToString' | export { ssrRenderComponent } from './helpers/ssrRenderComponent' | ||||||
| export { ssrRenderSlot } from './helpers/ssrRenderSlot' | export { ssrRenderSlot } from './helpers/ssrRenderSlot' | ||||||
|  | export { ssrRenderTeleport } from './helpers/ssrRenderTeleport' | ||||||
| export { | export { | ||||||
|   ssrRenderClass, |   ssrRenderClass, | ||||||
|   ssrRenderStyle, |   ssrRenderStyle, | ||||||
| @ -13,7 +16,6 @@ export { | |||||||
| } from './helpers/ssrRenderAttrs' | } from './helpers/ssrRenderAttrs' | ||||||
| export { ssrInterpolate } from './helpers/ssrInterpolate' | export { ssrInterpolate } from './helpers/ssrInterpolate' | ||||||
| export { ssrRenderList } from './helpers/ssrRenderList' | export { ssrRenderList } from './helpers/ssrRenderList' | ||||||
| export { ssrRenderTeleport } from './helpers/ssrRenderTeleport' |  | ||||||
| export { ssrRenderSuspense } from './helpers/ssrRenderSuspense' | export { ssrRenderSuspense } from './helpers/ssrRenderSuspense' | ||||||
| 
 | 
 | ||||||
| // v-model helpers
 | // v-model helpers
 | ||||||
|  | |||||||
							
								
								
									
										286
									
								
								packages/server-renderer/src/render.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								packages/server-renderer/src/render.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,286 @@ | |||||||
|  | import { | ||||||
|  |   Comment, | ||||||
|  |   Component, | ||||||
|  |   ComponentInternalInstance, | ||||||
|  |   DirectiveBinding, | ||||||
|  |   Fragment, | ||||||
|  |   mergeProps, | ||||||
|  |   ssrUtils, | ||||||
|  |   Static, | ||||||
|  |   Text, | ||||||
|  |   VNode, | ||||||
|  |   VNodeArrayChildren, | ||||||
|  |   VNodeProps, | ||||||
|  |   warn | ||||||
|  | } from 'vue' | ||||||
|  | import { | ||||||
|  |   escapeHtml, | ||||||
|  |   escapeHtmlComment, | ||||||
|  |   isFunction, | ||||||
|  |   isPromise, | ||||||
|  |   isString, | ||||||
|  |   isVoidTag, | ||||||
|  |   ShapeFlags | ||||||
|  | } from '@vue/shared' | ||||||
|  | import { ssrRenderAttrs } from './helpers/ssrRenderAttrs' | ||||||
|  | import { ssrCompile } from './helpers/ssrCompile' | ||||||
|  | import { ssrRenderTeleport } from './helpers/ssrRenderTeleport' | ||||||
|  | 
 | ||||||
|  | const { | ||||||
|  |   createComponentInstance, | ||||||
|  |   setCurrentRenderingInstance, | ||||||
|  |   setupComponent, | ||||||
|  |   renderComponentRoot, | ||||||
|  |   normalizeVNode, | ||||||
|  |   normalizeSuspenseChildren | ||||||
|  | } = ssrUtils | ||||||
|  | 
 | ||||||
|  | export type SSRBuffer = SSRBufferItem[] | ||||||
|  | export type SSRBufferItem = string | SSRBuffer | Promise<SSRBuffer> | ||||||
|  | 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> | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 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 function createBuffer() { | ||||||
|  |   let appendable = false | ||||||
|  |   const buffer: SSRBuffer = [] | ||||||
|  |   return { | ||||||
|  |     getBuffer(): SSRBuffer { | ||||||
|  |       // Return static buffer and await on items during unroll stage
 | ||||||
|  |       return buffer | ||||||
|  |     }, | ||||||
|  |     push(item: SSRBufferItem) { | ||||||
|  |       const isStringItem = isString(item) | ||||||
|  |       if (appendable && isStringItem) { | ||||||
|  |         buffer[buffer.length - 1] += item as string | ||||||
|  |       } else { | ||||||
|  |         buffer.push(item) | ||||||
|  |       } | ||||||
|  |       appendable = isStringItem | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function renderComponentVNode( | ||||||
|  |   vnode: VNode, | ||||||
|  |   parentComponent: ComponentInternalInstance | null = null | ||||||
|  | ): SSRBuffer | Promise<SSRBuffer> { | ||||||
|  |   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 | ||||||
|  | ): SSRBuffer | Promise<SSRBuffer> { | ||||||
|  |   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() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function renderVNode( | ||||||
|  |   push: PushFn, | ||||||
|  |   vnode: VNode, | ||||||
|  |   parentComponent: ComponentInternalInstance | ||||||
|  | ) { | ||||||
|  |   const { type, shapeFlag, children } = vnode | ||||||
|  |   switch (type) { | ||||||
|  |     case Text: | ||||||
|  |       push(escapeHtml(children as string)) | ||||||
|  |       break | ||||||
|  |     case Comment: | ||||||
|  |       push( | ||||||
|  |         children ? `<!--${escapeHtmlComment(children as string)}-->` : `<!---->` | ||||||
|  |       ) | ||||||
|  |       break | ||||||
|  |     case Static: | ||||||
|  |       push(children as string) | ||||||
|  |       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 | ||||||
|  |   ) | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								packages/server-renderer/src/renderToStream.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								packages/server-renderer/src/renderToStream.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | |||||||
|  | import { | ||||||
|  |   App, | ||||||
|  |   VNode, | ||||||
|  |   createVNode, | ||||||
|  |   ssrUtils, | ||||||
|  |   createApp, | ||||||
|  |   ssrContextKey | ||||||
|  | } from 'vue' | ||||||
|  | import { isString, isPromise } from '@vue/shared' | ||||||
|  | import { renderComponentVNode, SSRBuffer, SSRContext } from './render' | ||||||
|  | import { Readable } from 'stream' | ||||||
|  | 
 | ||||||
|  | const { isVNode } = ssrUtils | ||||||
|  | 
 | ||||||
|  | async function unrollBuffer( | ||||||
|  |   buffer: SSRBuffer, | ||||||
|  |   stream: Readable | ||||||
|  | ): Promise<void> { | ||||||
|  |   for (let i = 0; i < buffer.length; i++) { | ||||||
|  |     let item = buffer[i] | ||||||
|  |     if (isPromise(item)) { | ||||||
|  |       item = await item | ||||||
|  |     } | ||||||
|  |     if (isString(item)) { | ||||||
|  |       stream.push(item) | ||||||
|  |     } else { | ||||||
|  |       await unrollBuffer(item, stream) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function renderToStream( | ||||||
|  |   input: App | VNode, | ||||||
|  |   context: SSRContext = {} | ||||||
|  | ): Readable { | ||||||
|  |   if (isVNode(input)) { | ||||||
|  |     // raw vnode, wrap with app (for context)
 | ||||||
|  |     return renderToStream(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 stream = new Readable() | ||||||
|  | 
 | ||||||
|  |   Promise.resolve(renderComponentVNode(vnode)) | ||||||
|  |     .then(buffer => unrollBuffer(buffer, stream)) | ||||||
|  |     .then(() => { | ||||||
|  |       stream.push(null) | ||||||
|  |     }) | ||||||
|  |     .catch(error => { | ||||||
|  |       stream.destroy(error) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |   return stream | ||||||
|  | } | ||||||
| @ -1,109 +1,27 @@ | |||||||
| import { | import { | ||||||
|   App, |   App, | ||||||
|   Component, |  | ||||||
|   ComponentInternalInstance, |  | ||||||
|   VNode, |  | ||||||
|   VNodeArrayChildren, |  | ||||||
|   createVNode, |  | ||||||
|   Text, |  | ||||||
|   Comment, |  | ||||||
|   Static, |  | ||||||
|   Fragment, |  | ||||||
|   ssrUtils, |  | ||||||
|   Slots, |  | ||||||
|   createApp, |   createApp, | ||||||
|  |   createVNode, | ||||||
|   ssrContextKey, |   ssrContextKey, | ||||||
|   warn, |   ssrUtils, | ||||||
|   DirectiveBinding, |   VNode | ||||||
|   VNodeProps, |  | ||||||
|   mergeProps |  | ||||||
| } from 'vue' | } from 'vue' | ||||||
| import { | import { isPromise, isString } from '@vue/shared' | ||||||
|   ShapeFlags, | import { SSRContext, renderComponentVNode, SSRBuffer } from './render' | ||||||
|   isString, |  | ||||||
|   isPromise, |  | ||||||
|   isArray, |  | ||||||
|   isFunction, |  | ||||||
|   isVoidTag, |  | ||||||
|   escapeHtml, |  | ||||||
|   NO, |  | ||||||
|   generateCodeFrame, |  | ||||||
|   escapeHtmlComment |  | ||||||
| } 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 { | const { isVNode } = ssrUtils | ||||||
|   isVNode, |  | ||||||
|   createComponentInstance, |  | ||||||
|   setCurrentRenderingInstance, |  | ||||||
|   setupComponent, |  | ||||||
|   renderComponentRoot, |  | ||||||
|   normalizeVNode, |  | ||||||
|   normalizeSuspenseChildren |  | ||||||
| } = ssrUtils |  | ||||||
| 
 | 
 | ||||||
| // Each component has a buffer array.
 | async function unrollBuffer(buffer: SSRBuffer): Promise<string> { | ||||||
| // 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 = '' |   let ret = '' | ||||||
|   for (let i = 0; i < buffer.length; i++) { |   for (let i = 0; i < buffer.length; i++) { | ||||||
|     const item = buffer[i] |     let item = buffer[i] | ||||||
|  |     if (isPromise(item)) { | ||||||
|  |       item = await item | ||||||
|  |     } | ||||||
|     if (isString(item)) { |     if (isString(item)) { | ||||||
|       ret += item |       ret += item | ||||||
|     } else { |     } else { | ||||||
|       ret += unrollBuffer(item) |       ret += await unrollBuffer(item as SSRBuffer) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   return ret |   return ret | ||||||
| @ -127,272 +45,7 @@ export async function renderToString( | |||||||
| 
 | 
 | ||||||
|   await resolveTeleports(context) |   await resolveTeleports(context) | ||||||
| 
 | 
 | ||||||
|   return unrollBuffer(buffer) |   return unrollBuffer(buffer as SSRBuffer) | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 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(escapeHtml(children as string)) |  | ||||||
|       break |  | ||||||
|     case Comment: |  | ||||||
|       push( |  | ||||||
|         children ? `<!--${escapeHtmlComment(children as string)}-->` : `<!---->` |  | ||||||
|       ) |  | ||||||
|       break |  | ||||||
|     case Static: |  | ||||||
|       push(children as string) |  | ||||||
|       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) { | async function resolveTeleports(context: SSRContext) { | ||||||
| @ -401,9 +54,9 @@ async function resolveTeleports(context: SSRContext) { | |||||||
|     for (const key in context.__teleportBuffers) { |     for (const key in context.__teleportBuffers) { | ||||||
|       // note: it's OK to await sequentially here because the Promises were
 |       // note: it's OK to await sequentially here because the Promises were
 | ||||||
|       // created eagerly in parallel.
 |       // created eagerly in parallel.
 | ||||||
|       context.teleports[key] = unrollBuffer( |       context.teleports[key] = await unrollBuffer((await Promise.all( | ||||||
|         await Promise.all(context.__teleportBuffers[key]) |         context.__teleportBuffers[key] | ||||||
|       ) |       )) as SSRBuffer) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user