renderToStream.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import {
  2. App,
  3. VNode,
  4. createVNode,
  5. ssrUtils,
  6. createApp,
  7. ssrContextKey
  8. } from 'vue'
  9. import { isString, isPromise } from '@vue/shared'
  10. import { renderComponentVNode, SSRBuffer, SSRContext } from './render'
  11. import { Readable, Writable } from 'stream'
  12. const { isVNode } = ssrUtils
  13. export interface SimpleReadable {
  14. push(chunk: string | null): void
  15. destroy(err: any): void
  16. }
  17. async function unrollBuffer(
  18. buffer: SSRBuffer,
  19. stream: SimpleReadable
  20. ): Promise<void> {
  21. if (buffer.hasAsync) {
  22. for (let i = 0; i < buffer.length; i++) {
  23. let item = buffer[i]
  24. if (isPromise(item)) {
  25. item = await item
  26. }
  27. if (isString(item)) {
  28. stream.push(item)
  29. } else {
  30. await unrollBuffer(item, stream)
  31. }
  32. }
  33. } else {
  34. // sync buffer can be more efficiently unrolled without unnecessary await
  35. // ticks
  36. unrollBufferSync(buffer, stream)
  37. }
  38. }
  39. function unrollBufferSync(buffer: SSRBuffer, stream: SimpleReadable) {
  40. for (let i = 0; i < buffer.length; i++) {
  41. let item = buffer[i]
  42. if (isString(item)) {
  43. stream.push(item)
  44. } else {
  45. // since this is a sync buffer, child buffers are never promises
  46. unrollBufferSync(item as SSRBuffer, stream)
  47. }
  48. }
  49. }
  50. export function renderToSimpleStream<T extends SimpleReadable>(
  51. input: App | VNode,
  52. context: SSRContext,
  53. stream: T
  54. ): T {
  55. if (isVNode(input)) {
  56. // raw vnode, wrap with app (for context)
  57. return renderToSimpleStream(
  58. createApp({ render: () => input }),
  59. context,
  60. stream
  61. )
  62. }
  63. // rendering an app
  64. const vnode = createVNode(input._component, input._props)
  65. vnode.appContext = input._context
  66. // provide the ssr context to the tree
  67. input.provide(ssrContextKey, context)
  68. Promise.resolve(renderComponentVNode(vnode))
  69. .then(buffer => unrollBuffer(buffer, stream))
  70. .then(() => stream.push(null))
  71. .catch(error => {
  72. stream.destroy(error)
  73. })
  74. return stream
  75. }
  76. /**
  77. * @deprecated
  78. */
  79. export function renderToStream(
  80. input: App | VNode,
  81. context: SSRContext = {}
  82. ): Readable {
  83. console.warn(
  84. `[@vue/server-renderer] renderToStream is deprecated - use renderToNodeStream instead.`
  85. )
  86. return renderToNodeStream(input, context)
  87. }
  88. export function renderToNodeStream(
  89. input: App | VNode,
  90. context: SSRContext = {}
  91. ): Readable {
  92. const stream: Readable = __NODE_JS__
  93. ? new (require('stream').Readable)()
  94. : null
  95. if (!stream) {
  96. throw new Error(
  97. `ESM build of renderToStream() does not support renderToNodeStream(). ` +
  98. `Use pipeToNodeWritable() with an existing Node.js Writable stream ` +
  99. `instance instead.`
  100. )
  101. }
  102. return renderToSimpleStream(input, context, stream)
  103. }
  104. export function pipeToNodeWritable(
  105. input: App | VNode,
  106. context: SSRContext = {},
  107. writable: Writable
  108. ) {
  109. renderToSimpleStream(input, context, {
  110. push(content) {
  111. if (content != null) {
  112. writable.write(content)
  113. } else {
  114. writable.end()
  115. }
  116. },
  117. destroy(err) {
  118. writable.destroy(err)
  119. }
  120. })
  121. }
  122. export function renderToWebStream(
  123. input: App | VNode,
  124. context: SSRContext = {}
  125. ): ReadableStream {
  126. if (typeof ReadableStream !== 'function') {
  127. throw new Error(
  128. `ReadableStream constructor is not available in the global scope. ` +
  129. `If the target environment does support web streams, consider using ` +
  130. `pipeToWebWritable() with an existing WritableStream instance instead.`
  131. )
  132. }
  133. const encoder = new TextEncoder()
  134. let cancelled = false
  135. return new ReadableStream({
  136. start(controller) {
  137. renderToSimpleStream(input, context, {
  138. push(content) {
  139. if (cancelled) return
  140. if (content != null) {
  141. controller.enqueue(encoder.encode(content))
  142. } else {
  143. controller.close()
  144. }
  145. },
  146. destroy(err) {
  147. controller.error(err)
  148. }
  149. })
  150. },
  151. cancel() {
  152. cancelled = true
  153. }
  154. })
  155. }
  156. export function pipeToWebWritable(
  157. input: App | VNode,
  158. context: SSRContext = {},
  159. writable: WritableStream
  160. ): void {
  161. const writer = writable.getWriter()
  162. const encoder = new TextEncoder()
  163. // #4287 CloudFlare workers do not implement `ready` property
  164. let hasReady = false
  165. try {
  166. hasReady = isPromise(writer.ready)
  167. } catch (e: any) {}
  168. renderToSimpleStream(input, context, {
  169. async push(content) {
  170. if (hasReady) {
  171. await writer.ready
  172. }
  173. if (content != null) {
  174. return writer.write(encoder.encode(content))
  175. } else {
  176. return writer.close()
  177. }
  178. },
  179. destroy(err) {
  180. // TODO better error handling?
  181. console.log(err)
  182. writer.close()
  183. }
  184. })
  185. }