Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
80 changes: 80 additions & 0 deletions packages/server-renderer/__tests__/render.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import {
createTextVNode,
createVNode,
defineComponent,
effectScope,
getCurrentInstance,
h,
onErrorCaptured,
onScopeDispose,
onServerPrefetch,
reactive,
ref,
Expand Down Expand Up @@ -1002,6 +1004,84 @@ function testRender(type: string, render: typeof renderToString) {
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')
Expand Down
6 changes: 3 additions & 3 deletions packages/server-renderer/__tests__/ssrWatch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('ssr: watch', () => {
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)

expect(ctx.__watcherHandles!.length).toBe(1)
expect(ctx.__watcherHandles!.length).toBe(0)

expect(html).toMatch('hello world')
})
Expand Down Expand Up @@ -61,7 +61,7 @@ describe('ssr: watch', () => {
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)

expect(ctx.__watcherHandles!.length).toBe(1)
expect(ctx.__watcherHandles!.length).toBe(0)
expect(html).toMatch('changed again')
await nextTick()
expect(msg).toBe('changed again')
Expand Down Expand Up @@ -229,7 +229,7 @@ describe('ssr: watchEffect', () => {
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)

expect(ctx.__watcherHandles!.length).toBe(1)
expect(ctx.__watcherHandles!.length).toBe(0)
expect(html).toMatch('changed again')
await nextTick()
expect(msg).toBe('changed again')
Expand Down
40 changes: 40 additions & 0 deletions packages/server-renderer/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type VNodeArrayChildren,
type VNodeProps,
mergeProps,
ssrContextKey,
ssrUtils,
warn,
} from 'vue'
Expand Down Expand Up @@ -55,6 +56,37 @@ export type SSRContext = {
* @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
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (firstError !== undefined) {
throw firstError
}
}

// Each component has a buffer array.
Expand Down Expand Up @@ -98,6 +130,14 @@ export function renderComponentVNode(
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()
Expand Down
32 changes: 23 additions & 9 deletions packages/server-renderer/src/renderToStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
ssrUtils,
} from 'vue'
import { isPromise, isString } from '@vue/shared'
import { type SSRBuffer, type SSRContext, renderComponentVNode } from './render'
import {
type SSRBuffer,
type SSRContext,
cleanupContext,
renderComponentVNode,
} from './render'
import type { Readable, Writable } from 'node:stream'
import { resolveTeleports } from './renderToString'

Expand Down Expand Up @@ -43,7 +48,7 @@ async function unrollBuffer(

function unrollBufferSync(buffer: SSRBuffer, stream: SimpleReadable) {
for (let i = 0; i < buffer.length; i++) {
let item = buffer[i]
const item = buffer[i]
if (isString(item)) {
stream.push(item)
} else {
Expand Down Expand Up @@ -73,18 +78,27 @@ export function renderToSimpleStream<T extends SimpleReadable>(
// provide the ssr context to the tree
input.provide(ssrContextKey, context)

Promise.resolve(renderComponentVNode(vnode))
let cleaned = false
const finalize = () => {
if (cleaned) return
cleaned = true
cleanupContext(context)
}

Promise.resolve()
.then(() => renderComponentVNode(vnode))
.then(buffer => unrollBuffer(buffer, stream))
.then(() => resolveTeleports(context))
.then(() => {
if (context.__watcherHandles) {
for (const unwatch of context.__watcherHandles) {
unwatch()
}
}
finalize()
return stream.push(null)
})
.then(() => stream.push(null))
.catch(error => {
try {
finalize()
} catch {
// preserve original render error as the stream failure reason
}
stream.destroy(error)
})
Comment thread
edison1105 marked this conversation as resolved.

Expand Down
23 changes: 13 additions & 10 deletions packages/server-renderer/src/renderToString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
ssrUtils,
} from 'vue'
import { isPromise, isString } from '@vue/shared'
import { type SSRBuffer, type SSRContext, renderComponentVNode } from './render'
import {
type SSRBuffer,
type SSRContext,
cleanupContext,
renderComponentVNode,
} from './render'

const { isVNode } = ssrUtils

Expand Down Expand Up @@ -81,19 +86,17 @@ export async function renderToString(
vnode.appContext = input._context
// provide the ssr context to the tree
input.provide(ssrContextKey, context)
const buffer = await renderComponentVNode(vnode)
try {
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)

if (context.__watcherHandles) {
for (const unwatch of context.__watcherHandles) {
unwatch()
}
return result
} finally {
cleanupContext(context)
}

return result
}

export async function resolveTeleports(context: SSRContext): Promise<void> {
Expand Down
Loading