| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- import {
- type App,
- type VNode,
- createApp,
- createVNode,
- ssrContextKey,
- ssrUtils,
- } from 'vue'
- import { isPromise, isString } from '@vue/shared'
- import { type SSRBuffer, type SSRContext, renderComponentVNode } from './render'
- import type { Readable, Writable } from 'node:stream'
- import { resolveTeleports } from './renderToString'
- const { isVNode } = ssrUtils
- export interface SimpleReadable {
- push(chunk: string | null): void
- destroy(err: any): void
- }
- async function unrollBuffer(
- buffer: SSRBuffer,
- stream: SimpleReadable,
- ): Promise<void> {
- if (buffer.hasAsync) {
- 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)
- }
- }
- } else {
- // sync buffer can be more efficiently unrolled without unnecessary await
- // ticks
- unrollBufferSync(buffer, stream)
- }
- }
- function unrollBufferSync(buffer: SSRBuffer, stream: SimpleReadable) {
- for (let i = 0; i < buffer.length; i++) {
- let item = buffer[i]
- if (isString(item)) {
- stream.push(item)
- } else {
- // since this is a sync buffer, child buffers are never promises
- unrollBufferSync(item as SSRBuffer, stream)
- }
- }
- }
- export function renderToSimpleStream<T extends SimpleReadable>(
- input: App | VNode,
- context: SSRContext,
- stream: T,
- ): T {
- if (isVNode(input)) {
- // raw vnode, wrap with app (for context)
- return renderToSimpleStream(
- createApp({ render: () => input }),
- context,
- stream,
- )
- }
- // 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)
- Promise.resolve(renderComponentVNode(vnode))
- .then(buffer => unrollBuffer(buffer, stream))
- .then(() => resolveTeleports(context))
- .then(() => {
- if (context.__watcherHandles) {
- for (const unwatch of context.__watcherHandles) {
- unwatch()
- }
- }
- })
- .then(() => stream.push(null))
- .catch(error => {
- stream.destroy(error)
- })
- return stream
- }
- /**
- * @deprecated
- */
- export function renderToStream(
- input: App | VNode,
- context: SSRContext = {},
- ): Readable {
- console.warn(
- `[@vue/server-renderer] renderToStream is deprecated - use renderToNodeStream instead.`,
- )
- return renderToNodeStream(input, context)
- }
- export function renderToNodeStream(
- input: App | VNode,
- context: SSRContext = {},
- ): Readable {
- const stream: Readable = __CJS__
- ? new (require('node:stream').Readable)({ read() {} })
- : null
- if (!stream) {
- throw new Error(
- `ESM build of renderToStream() does not support renderToNodeStream(). ` +
- `Use pipeToNodeWritable() with an existing Node.js Writable stream ` +
- `instance instead.`,
- )
- }
- return renderToSimpleStream(input, context, stream)
- }
- export function pipeToNodeWritable(
- input: App | VNode,
- context: SSRContext | undefined = {},
- writable: Writable,
- ): void {
- renderToSimpleStream(input, context, {
- push(content) {
- if (content != null) {
- writable.write(content)
- } else {
- writable.end()
- }
- },
- destroy(err) {
- writable.destroy(err)
- },
- })
- }
- export function renderToWebStream(
- input: App | VNode,
- context: SSRContext = {},
- ): ReadableStream {
- if (typeof ReadableStream !== 'function') {
- throw new Error(
- `ReadableStream constructor is not available in the global scope. ` +
- `If the target environment does support web streams, consider using ` +
- `pipeToWebWritable() with an existing WritableStream instance instead.`,
- )
- }
- const encoder = new TextEncoder()
- let cancelled = false
- return new ReadableStream({
- start(controller) {
- renderToSimpleStream(input, context, {
- push(content) {
- if (cancelled) return
- if (content != null) {
- controller.enqueue(encoder.encode(content))
- } else {
- controller.close()
- }
- },
- destroy(err) {
- controller.error(err)
- },
- })
- },
- cancel() {
- cancelled = true
- },
- })
- }
- export function pipeToWebWritable(
- input: App | VNode,
- context: SSRContext | undefined = {},
- writable: WritableStream,
- ): void {
- const writer = writable.getWriter()
- const encoder = new TextEncoder()
- // #4287 CloudFlare workers do not implement `ready` property
- let hasReady = false
- try {
- hasReady = isPromise(writer.ready)
- } catch (e: any) {}
- renderToSimpleStream(input, context, {
- async push(content) {
- if (hasReady) {
- await writer.ready
- }
- if (content != null) {
- return writer.write(encoder.encode(content))
- } else {
- return writer.close()
- }
- },
- destroy(err) {
- // TODO better error handling?
- // eslint-disable-next-line no-console
- console.log(err)
- writer.close()
- },
- })
- }
|