renderToStream.ts 5.0 KB

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