import type { ComponentInternalInstance } from './component'
import { devtoolsComponentUpdated } from './devtools'
-import { setBlockTracking } from './vnode'
+import { blockStack, closeBlock, setBlockTracking } from './vnode'
/**
* mark the current rendering instance for asset resolution (e.g.
setBlockTracking(-1)
}
const prevInstance = setCurrentRenderingInstance(ctx)
+ const prevStackSize = blockStack.length
let res
try {
res = fn(...args)
} finally {
+ // close blocks left dangling when the slot throws mid-block
+ // inline blocks (for example `v-if`) have no helper to unwind themselves (#15070)
+ for (let i = blockStack.length; i > prevStackSize; i--) closeBlock()
setCurrentRenderingInstance(prevInstance)
if (renderFnWithContext._d) {
setBlockTracking(1)
Fragment,
type VNode,
type VNodeArrayChildren,
+ blockStack,
+ closeBlock,
createBlock,
createVNode,
isVNode,
if (slot && (slot as ContextualRenderFn)._c) {
;(slot as ContextualRenderFn)._d = false
}
+ const prevStackSize = blockStack.length
openBlock()
- const validSlotContent = slot && ensureValidVNode(slot(props))
- const slotKey =
- props.key ||
- // slot content array of a dynamic conditional slot may have a branch
- // key attached in the `createSlots` helper, respect that
- (validSlotContent && (validSlotContent as any).key)
- const rendered = createBlock(
- Fragment,
- {
- key:
- (slotKey && !isSymbol(slotKey) ? slotKey : `_${name}`) +
- // #7256 force differentiate fallback content from actual content
- (!validSlotContent && fallback ? '_fb' : ''),
- },
- validSlotContent || (fallback ? fallback() : []),
- validSlotContent && (slots as RawSlots)._ === SlotFlags.STABLE
- ? PatchFlags.STABLE_FRAGMENT
- : PatchFlags.BAIL,
- )
+ let rendered: VNode
+ try {
+ const validSlotContent = slot && ensureValidVNode(slot(props))
+ const slotKey =
+ props.key ||
+ // slot content array of a dynamic conditional slot may have a branch
+ // key attached in the `createSlots` helper, respect that
+ (validSlotContent && (validSlotContent as any).key)
+ rendered = createBlock(
+ Fragment,
+ {
+ key:
+ (slotKey && !isSymbol(slotKey) ? slotKey : `_${name}`) +
+ // #7256 force differentiate fallback content from actual content
+ (!validSlotContent && fallback ? '_fb' : ''),
+ },
+ validSlotContent || (fallback ? fallback() : []),
+ validSlotContent && (slots as RawSlots)._ === SlotFlags.STABLE
+ ? PatchFlags.STABLE_FRAGMENT
+ : PatchFlags.BAIL,
+ )
+ } catch (err) {
+ // close blocks left dangling when the slot throws mid-block
+ // they would otherwise retain every vnode created afterwards (#15070)
+ for (let i = blockStack.length; i > prevStackSize; i--) closeBlock()
+ throw err
+ } finally {
+ if (slot && (slot as ContextualRenderFn)._c) {
+ ;(slot as ContextualRenderFn)._d = true
+ }
+ }
if (!noSlotted && rendered.scopeId) {
rendered.slotScopeIds = [rendered.scopeId + '-s']
}
- if (slot && (slot as ContextualRenderFn)._c) {
- ;(slot as ContextualRenderFn)._d = true
- }
return rendered
}
--- /dev/null
+import { createSSRApp } from 'vue'
+import { renderToString } from '../src/renderToString'
+
+const gc = () =>
+ new Promise<void>(resolve => {
+ setTimeout(() => {
+ global.gc!()
+ resolve()
+ })
+ })
+
+const Card = {
+ props: { tag: { type: String, default: 'div' } },
+ template: `<component :is="tag" class="card"><slot /></component>`,
+}
+
+const Page = {
+ components: { Card },
+ props: ['payload'],
+ template: `<main><Card>{{ payload.title }}</Card></main>`,
+}
+
+describe.skipIf(!global.gc)('ssr: render error leak', () => {
+ // #15070
+ test('should GC apps created after a render error in a forwarded slot', async () => {
+ const createApp = (payload: any) => createSSRApp(Page, { payload })
+
+ expect(await renderToString(createApp({ title: 'ok' }))).toContain('ok')
+
+ await expect(renderToString(createApp(null))).rejects.toThrow(
+ `Cannot read properties of null`,
+ )
+
+ const weakRefs: { deref(): unknown | undefined }[] = []
+ const renderOnce = async () => {
+ const app = createApp({ title: 'ok' })
+ // @ts-expect-error ES2021 API
+ weakRefs.push(new WeakRef(app))
+ expect(await renderToString(app)).toContain('ok')
+ }
+ for (let i = 0; i < 20; i++) {
+ await renderOnce()
+ }
+
+ for (let i = 0; i < 5; i++) {
+ await gc()
+ }
+
+ expect(weakRefs.filter(ref => ref.deref()).length).toBe(0)
+ })
+
+ test('should GC apps created after a render error in an inline block (`v-if`)', async () => {
+ const PageIf = {
+ props: ['payload'],
+ template: `<component :is="'div'"><div v-if="payload.list">{{ payload.list.missing.x }}</div></component>`,
+ }
+ const createApp = (payload: any) => createSSRApp(PageIf, { payload })
+
+ expect(
+ await renderToString(createApp({ list: { missing: { x: 'ok' } } })),
+ ).toContain('ok')
+
+ await expect(renderToString(createApp({ list: {} }))).rejects.toThrow(
+ `Cannot read properties of undefined`,
+ )
+
+ const weakRefs: { deref(): unknown | undefined }[] = []
+ const renderOnce = async () => {
+ const app = createApp({ list: { missing: { x: 'ok' } } })
+ // @ts-expect-error ES2021 API
+ weakRefs.push(new WeakRef(app))
+ expect(await renderToString(app)).toContain('ok')
+ }
+ for (let i = 0; i < 20; i++) {
+ await renderOnce()
+ }
+
+ for (let i = 0; i < 5; i++) {
+ await gc()
+ }
+
+ expect(weakRefs.filter(ref => ref.deref()).length).toBe(0)
+ })
+})
'**/e2e/**',
'**/{vue,vue-compat,runtime-dom}/**',
'packages/server-renderer/__tests__/ssrWatch.spec.ts',
+ 'packages/server-renderer/__tests__/ssrRender.spec.ts',
],
},
},
test: {
name: 'unit-gc',
pool: 'forks',
- include: ['packages/server-renderer/__tests__/ssrWatch.spec.ts'],
+ include: [
+ 'packages/server-renderer/__tests__/ssrWatch.spec.ts',
+ 'packages/server-renderer/__tests__/ssrRender.spec.ts',
+ ],
execArgv: ['--expose-gc'],
},
},