feat(ssr): renderToStream (#1197)
This commit is contained in:
		
							parent
							
								
									e0d19a6953
								
							
						
					
					
						commit
						6bc0e0a31a
					
				| @ -11,9 +11,8 @@ import { | ||||
|   defineAsyncComponent, | ||||
|   defineComponent | ||||
| } from '@vue/runtime-dom' | ||||
| import { renderToString } from '@vue/server-renderer' | ||||
| import { renderToString, SSRContext } from '@vue/server-renderer' | ||||
| import { mockWarn } from '@vue/shared' | ||||
| import { SSRContext } from 'packages/server-renderer/src/renderToString' | ||||
| 
 | ||||
| function mountWithHydration(html: string, render: () => any) { | ||||
|   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 | ||||
| } from 'vue' | ||||
| import { escapeHtml, mockWarn } from '@vue/shared' | ||||
| import { renderToString, renderComponent } from '../src/renderToString' | ||||
| import { renderToString } from '../src/renderToString' | ||||
| import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot' | ||||
| import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent' | ||||
| 
 | ||||
| mockWarn() | ||||
| 
 | ||||
| @ -145,7 +146,7 @@ describe('ssr: renderToString', () => { | ||||
|           createApp({ | ||||
|             ssrRender(_ctx, push, parent) { | ||||
|               push(`<div>parent`) | ||||
|               push(renderComponent(Child, { msg: 'hello' }, null, parent)) | ||||
|               push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent)) | ||||
|               push(`</div>`) | ||||
|             } | ||||
|           }) | ||||
| @ -194,11 +195,13 @@ describe('ssr: renderToString', () => { | ||||
|             ssrRender(_ctx, push, parent) { | ||||
|               push(`<div>parent`) | ||||
|               push( | ||||
|                 renderComponent(OptimizedChild, { msg: 'opt' }, null, parent) | ||||
|                 ssrRenderComponent(OptimizedChild, { msg: 'opt' }, null, parent) | ||||
|               ) | ||||
|               push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent)) | ||||
|               push( | ||||
|                 renderComponent( | ||||
|                 ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent) | ||||
|               ) | ||||
|               push( | ||||
|                 ssrRenderComponent( | ||||
|                   TemplateChild, | ||||
|                   { msg: 'template' }, | ||||
|                   null, | ||||
| @ -239,7 +242,7 @@ describe('ssr: renderToString', () => { | ||||
|             ssrRender(_ctx, push, parent) { | ||||
|               push(`<div>parent`) | ||||
|               push( | ||||
|                 renderComponent( | ||||
|                 ssrRenderComponent( | ||||
|                   Child, | ||||
|                   { msg: 'hello' }, | ||||
|                   { | ||||
| @ -269,7 +272,7 @@ describe('ssr: renderToString', () => { | ||||
|           createApp({ | ||||
|             ssrRender(_ctx, push, parent) { | ||||
|               push(`<div>parent`) | ||||
|               push(renderComponent(Child, { msg: 'hello' }, null, parent)) | ||||
|               push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent)) | ||||
|               push(`</div>`) | ||||
|             } | ||||
|           }) | ||||
| @ -302,7 +305,7 @@ describe('ssr: renderToString', () => { | ||||
|             ssrRender(_ctx, push, parent) { | ||||
|               push(`<div>parent`) | ||||
|               push( | ||||
|                 renderComponent( | ||||
|                 ssrRenderComponent( | ||||
|                   Child, | ||||
|                   { msg: 'hello' }, | ||||
|                   { | ||||
| @ -388,7 +391,7 @@ describe('ssr: renderToString', () => { | ||||
|           createApp({ | ||||
|             ssrRender(_ctx, push, parent) { | ||||
|               push(`<div>parent`) | ||||
|               push(renderComponent(Child, null, null, parent)) | ||||
|               push(ssrRenderComponent(Child, null, null, parent)) | ||||
|               push(`</div>`) | ||||
|             } | ||||
|           }) | ||||
| @ -427,9 +430,11 @@ describe('ssr: renderToString', () => { | ||||
|             ssrRender(_ctx, push, parent) { | ||||
|               push(`<div>parent`) | ||||
|               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>`) | ||||
|             } | ||||
|           }) | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| 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' | ||||
| 
 | ||||
| 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 { Props, PushFn, renderVNodeChildren } from '../render' | ||||
| 
 | ||||
| export type SSRSlots = Record<string, SSRSlot> | ||||
| 
 | ||||
| export type SSRSlot = ( | ||||
|   props: Props, | ||||
|   push: PushFn, | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { PushFn } from '../renderToString' | ||||
| import { PushFn } from '../render' | ||||
| 
 | ||||
| export async function ssrRenderSuspense( | ||||
|   push: PushFn, | ||||
|  | ||||
| @ -1,10 +1,5 @@ | ||||
| import { ComponentInternalInstance, ssrContextKey } from 'vue' | ||||
| import { | ||||
|   SSRContext, | ||||
|   createBuffer, | ||||
|   PushFn, | ||||
|   SSRBufferItem | ||||
| } from '../renderToString' | ||||
| import { createBuffer, PushFn, SSRBufferItem, SSRContext } from '../render' | ||||
| 
 | ||||
| export function ssrRenderTeleport( | ||||
|   parentPush: PushFn, | ||||
|  | ||||
| @ -1,9 +1,12 @@ | ||||
| // public
 | ||||
| export { renderToString, SSRContext } from './renderToString' | ||||
| export { SSRContext } from './render' | ||||
| export { renderToString } from './renderToString' | ||||
| export { renderToStream } from './renderToStream' | ||||
| 
 | ||||
| // internal runtime helpers
 | ||||
| export { renderComponent as ssrRenderComponent } from './renderToString' | ||||
| export { ssrRenderComponent } from './helpers/ssrRenderComponent' | ||||
| export { ssrRenderSlot } from './helpers/ssrRenderSlot' | ||||
| export { ssrRenderTeleport } from './helpers/ssrRenderTeleport' | ||||
| export { | ||||
|   ssrRenderClass, | ||||
|   ssrRenderStyle, | ||||
| @ -13,7 +16,6 @@ export { | ||||
| } 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
 | ||||
|  | ||||
							
								
								
									
										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 { | ||||
|   App, | ||||
|   Component, | ||||
|   ComponentInternalInstance, | ||||
|   VNode, | ||||
|   VNodeArrayChildren, | ||||
|   createVNode, | ||||
|   Text, | ||||
|   Comment, | ||||
|   Static, | ||||
|   Fragment, | ||||
|   ssrUtils, | ||||
|   Slots, | ||||
|   createApp, | ||||
|   createVNode, | ||||
|   ssrContextKey, | ||||
|   warn, | ||||
|   DirectiveBinding, | ||||
|   VNodeProps, | ||||
|   mergeProps | ||||
|   ssrUtils, | ||||
|   VNode | ||||
| } from 'vue' | ||||
| import { | ||||
|   ShapeFlags, | ||||
|   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' | ||||
| import { isPromise, isString } from '@vue/shared' | ||||
| import { SSRContext, renderComponentVNode, SSRBuffer } from './render' | ||||
| 
 | ||||
| const { | ||||
|   isVNode, | ||||
|   createComponentInstance, | ||||
|   setCurrentRenderingInstance, | ||||
|   setupComponent, | ||||
|   renderComponentRoot, | ||||
|   normalizeVNode, | ||||
|   normalizeSuspenseChildren | ||||
| } = ssrUtils | ||||
| const { isVNode } = 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 { | ||||
| async function unrollBuffer(buffer: SSRBuffer): Promise<string> { | ||||
|   let ret = '' | ||||
|   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)) { | ||||
|       ret += item | ||||
|     } else { | ||||
|       ret += unrollBuffer(item) | ||||
|       ret += await unrollBuffer(item as SSRBuffer) | ||||
|     } | ||||
|   } | ||||
|   return ret | ||||
| @ -127,272 +45,7 @@ export async function renderToString( | ||||
| 
 | ||||
|   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(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 | ||||
|   ) | ||||
|   return unrollBuffer(buffer as SSRBuffer) | ||||
| } | ||||
| 
 | ||||
| async function resolveTeleports(context: SSRContext) { | ||||
| @ -401,9 +54,9 @@ async function resolveTeleports(context: SSRContext) { | ||||
|     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]) | ||||
|       ) | ||||
|       context.teleports[key] = await unrollBuffer((await Promise.all( | ||||
|         context.__teleportBuffers[key] | ||||
|       )) as SSRBuffer) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user