createTextVNode,
createVNode,
defineComponent,
- effectScope,
getCurrentInstance,
h,
onErrorCaptured,
- onScopeDispose,
onServerPrefetch,
reactive,
ref,
expect(html).toBe(`<div>hello</div>`)
})
- test('cleans up component effect scopes after each render', async () => {
- const cleanups: number[] = []
- const app = createApp({
- setup() {
- onScopeDispose(() => {
- cleanups.push(1)
- })
- return () => h('div', 'ok')
- },
- })
-
- expect(cleanups).toEqual([])
- expect(await render(app)).toBe(`<div>ok</div>`)
- expect(cleanups).toEqual([1])
- })
-
- test('concurrent renders isolate scope cleanup ownership', async () => {
- const cleaned: string[] = []
-
- const deferred = () => {
- let resolve!: () => void
- const promise = new Promise<void>(r => {
- resolve = r
- })
- return { promise, resolve }
- }
-
- const gateA = deferred()
- const gateB = deferred()
-
- const makeApp = (id: string, gate: ReturnType<typeof deferred>) =>
- createApp({
- async setup() {
- onScopeDispose(() => {
- cleaned.push(id)
- })
- await gate.promise
- return () => h('div', id)
- },
- })
-
- const pA = render(makeApp('A', gateA))
- const pB = render(makeApp('B', gateB))
-
- gateB.resolve()
- expect(await pB).toBe(`<div>B</div>`)
- expect(cleaned).toEqual(['B'])
-
- gateA.resolve()
- expect(await pA).toBe(`<div>A</div>`)
- expect(cleaned.sort()).toEqual(['A', 'B'])
- })
-
- test('detached scopes created during SSR are not auto-stopped', async () => {
- let detachedStopped = false
- let detached: any
-
- const app = createApp({
- setup() {
- detached = effectScope(true)
- detached.run(() => {
- onScopeDispose(() => {
- detachedStopped = true
- })
- })
- return () => h('div', 'detached')
- },
- })
-
- expect(await render(app)).toBe(`<div>detached</div>`)
- expect(detached.active).toBe(true)
- expect(detachedStopped).toBe(false)
-
- detached.stop()
- expect(detached.active).toBe(false)
- expect(detachedStopped).toBe(true)
- })
-
test('multiple onServerPrefetch', async () => {
const msg = Promise.resolve('hello')
const msg2 = Promise.resolve('hi')
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
- expect(ctx.__watcherHandles!.length).toBe(0)
+ expect(ctx.__watcherHandles!.length).toBe(1)
expect(html).toMatch('hello world')
})
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
- expect(ctx.__watcherHandles!.length).toBe(0)
+ expect(ctx.__watcherHandles!.length).toBe(1)
expect(html).toMatch('changed again')
await nextTick()
expect(msg).toBe('changed again')
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
- expect(ctx.__watcherHandles!.length).toBe(0)
+ expect(ctx.__watcherHandles!.length).toBe(1)
expect(html).toMatch('changed again')
await nextTick()
expect(msg).toBe('changed again')
type VNodeArrayChildren,
type VNodeProps,
mergeProps,
- ssrContextKey,
ssrUtils,
warn,
} from 'vue'
* @internal
*/
__watcherHandles?: (() => void)[]
- /**
- * @internal
- */
- __instanceScopes?: { stop: () => void }[]
-}
-
-export function cleanupContext(context: SSRContext): void {
- let firstError: unknown
- if (context.__watcherHandles) {
- for (const unwatch of context.__watcherHandles) {
- try {
- unwatch()
- } catch (err) {
- if (firstError === undefined) firstError = err
- }
- }
- context.__watcherHandles.length = 0
- }
- if (context.__instanceScopes) {
- for (const scope of context.__instanceScopes) {
- try {
- scope.stop()
- } catch (err) {
- if (firstError === undefined) firstError = err
- }
- }
- context.__instanceScopes.length = 0
- }
- if (firstError !== undefined) {
- throw firstError
- }
}
// Each component has a buffer array.
parentComponent,
null,
))
- const context = instance.appContext.provides[ssrContextKey as any] as
- | SSRContext
- | undefined
- if (context) {
- ;(context.__instanceScopes || (context.__instanceScopes = [])).push(
- instance.scope,
- )
- }
if (__DEV__) pushWarningContext(vnode)
const res = setupComponent(instance, true /* isSSR */)
if (__DEV__) popWarningContext()
ssrUtils,
} from 'vue'
import { isPromise, isString } from '@vue/shared'
-import {
- type SSRBuffer,
- type SSRContext,
- cleanupContext,
- renderComponentVNode,
-} from './render'
+import { type SSRBuffer, type SSRContext, renderComponentVNode } from './render'
import type { Readable, Writable } from 'node:stream'
import { resolveTeleports } from './renderToString'
function unrollBufferSync(buffer: SSRBuffer, stream: SimpleReadable) {
for (let i = 0; i < buffer.length; i++) {
- const item = buffer[i]
+ let item = buffer[i]
if (isString(item)) {
stream.push(item)
} else {
// provide the ssr context to the tree
input.provide(ssrContextKey, context)
- let cleaned = false
- const finalize = () => {
- if (cleaned) return
- cleaned = true
- cleanupContext(context)
- }
-
- Promise.resolve()
- .then(() => renderComponentVNode(vnode))
+ Promise.resolve(renderComponentVNode(vnode))
.then(buffer => unrollBuffer(buffer, stream))
.then(() => resolveTeleports(context))
.then(() => {
- finalize()
- return stream.push(null)
+ if (context.__watcherHandles) {
+ for (const unwatch of context.__watcherHandles) {
+ unwatch()
+ }
+ }
})
+ .then(() => stream.push(null))
.catch(error => {
- try {
- finalize()
- } catch {
- // preserve original render error as the stream failure reason
- }
stream.destroy(error)
})
ssrUtils,
} from 'vue'
import { isPromise, isString } from '@vue/shared'
-import {
- type SSRBuffer,
- type SSRContext,
- cleanupContext,
- renderComponentVNode,
-} from './render'
+import { type SSRBuffer, type SSRContext, renderComponentVNode } from './render'
const { isVNode } = ssrUtils
vnode.appContext = input._context
// provide the ssr context to the tree
input.provide(ssrContextKey, context)
- try {
- const buffer = await renderComponentVNode(vnode)
+ const buffer = await renderComponentVNode(vnode)
- const result = await unrollBuffer(buffer as SSRBuffer)
+ const result = await unrollBuffer(buffer as SSRBuffer)
- await resolveTeleports(context)
+ await resolveTeleports(context)
- return result
- } finally {
- cleanupContext(context)
+ if (context.__watcherHandles) {
+ for (const unwatch of context.__watcherHandles) {
+ unwatch()
+ }
}
+
+ return result
}
export async function resolveTeleports(context: SSRContext): Promise<void> {