Merge branch 'main' into renovate/lint
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details

This commit is contained in:
edison 2025-07-25 11:49:24 +08:00 committed by GitHub
commit b1f959f52c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 526 additions and 53 deletions

View File

@ -1,3 +1,23 @@
## [3.5.18](https://github.com/vuejs/core/compare/v3.5.17...v3.5.18) (2025-07-23)
### Bug Fixes
* **compiler-core:** avoid cached text vnodes retaining detached DOM nodes ([#13662](https://github.com/vuejs/core/issues/13662)) ([00695a5](https://github.com/vuejs/core/commit/00695a5b41b2d032deaeada83831ff83aa6bfd4e)), closes [#13661](https://github.com/vuejs/core/issues/13661)
* **compiler-core:** avoid self updates of `v-pre` ([#12556](https://github.com/vuejs/core/issues/12556)) ([21b685a](https://github.com/vuejs/core/commit/21b685ad9d9d0e6060fc7d07b719bf35f2d9ae1f))
* **compiler-core:** identifiers in function parameters should not be inferred as references ([#13548](https://github.com/vuejs/core/issues/13548)) ([9b02923](https://github.com/vuejs/core/commit/9b029239edf88558465b941e1e4c085f92b1ebff))
* **compiler-core:** recognize empty string as non-identifier ([#12553](https://github.com/vuejs/core/issues/12553)) ([ce93339](https://github.com/vuejs/core/commit/ce933390ad1c72bed258f7ad959a78f0e8acdf57))
* **compiler-core:** transform empty `v-bind` dynamic argument content correctly ([#12554](https://github.com/vuejs/core/issues/12554)) ([d3af67e](https://github.com/vuejs/core/commit/d3af67e878790892f9d34cfea15d13625aabe733))
* **compiler-sfc:** transform empty srcset w/ includeAbsolute: true ([#13639](https://github.com/vuejs/core/issues/13639)) ([d8e40ef](https://github.com/vuejs/core/commit/d8e40ef7e1c20ee86b294e7cf78e2de60d12830e)), closes [vitejs/vite-plugin-vue#631](https://github.com/vitejs/vite-plugin-vue/issues/631)
* **css-vars:** nullish v-bind in style should not lead to unexpected inheritance ([#12461](https://github.com/vuejs/core/issues/12461)) ([c85f1b5](https://github.com/vuejs/core/commit/c85f1b5a132eb8ec25f71b250e25e65a5c20964f)), closes [#12434](https://github.com/vuejs/core/issues/12434) [#12439](https://github.com/vuejs/core/issues/12439) [#7474](https://github.com/vuejs/core/issues/7474) [#7475](https://github.com/vuejs/core/issues/7475)
* **custom-element:** ensure exposed methods are accessible from custom elements by making them enumerable ([#13634](https://github.com/vuejs/core/issues/13634)) ([90573b0](https://github.com/vuejs/core/commit/90573b06bf6fb6c14c6bbff6c4e34e0ab108953a)), closes [#13632](https://github.com/vuejs/core/issues/13632)
* **hydration:** prevent lazy hydration for updated components ([#13511](https://github.com/vuejs/core/issues/13511)) ([a9269c6](https://github.com/vuejs/core/commit/a9269c642bf944560bc29adb5dae471c11cd9ee8)), closes [#13510](https://github.com/vuejs/core/issues/13510)
* **runtime-core:** ensure correct anchor el for unresolved async components ([#13560](https://github.com/vuejs/core/issues/13560)) ([7f29943](https://github.com/vuejs/core/commit/7f2994393dcdb82cacbf62e02b5ba5565f32588b)), closes [#13559](https://github.com/vuejs/core/issues/13559)
* **slots:** refine internal key checking to support slot names starting with an underscore ([#13612](https://github.com/vuejs/core/issues/13612)) ([c5f7db1](https://github.com/vuejs/core/commit/c5f7db11542bb2246363aef78c88a8e6cef0ee93)), closes [#13611](https://github.com/vuejs/core/issues/13611)
* **ssr:** ensure empty slots render as a comment node in Transition ([#13396](https://github.com/vuejs/core/issues/13396)) ([8cfc10a](https://github.com/vuejs/core/commit/8cfc10a80b9cbf5d801ab149e49b8506d192e7e1)), closes [#13394](https://github.com/vuejs/core/issues/13394)
## [3.5.17](https://github.com/vuejs/core/compare/v3.5.16...v3.5.17) (2025-06-18)

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "3.5.17",
"version": "3.5.18",
"packageManager": "pnpm@10.13.1",
"type": "module",
"scripts": {

View File

@ -60,7 +60,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */),
_createTextVNode("foo"),
_createTextVNode("foo", -1 /* CACHED */),
_createElementVNode("div", null, null, -1 /* CACHED */)
])))
}

View File

@ -301,6 +301,25 @@ describe('compiler: v-if', () => {
])
})
test('error on adjacent v-else', () => {
const onError = vi.fn()
const {
node: { branches },
} = parseWithIfTransform(
`<div v-if="false"/><div v-else/><div v-else/>`,
{ onError },
0,
)
expect(onError.mock.calls[0]).toMatchObject([
{
code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
loc: branches[branches.length - 1].loc,
},
])
})
test('error on user key', () => {
const onError = vi.fn()
// dynamic

View File

@ -1,4 +1,9 @@
import type { ExpressionNode, TransformContext } from '../src'
import { babelParse, walkIdentifiers } from '@vue/compiler-sfc'
import {
type ExpressionNode,
type TransformContext,
isReferencedIdentifier,
} from '../src'
import { type Position, createSimpleExpression } from '../src/ast'
import {
advancePositionWithClone,
@ -115,3 +120,18 @@ test('toValidAssetId', () => {
'_component_test_2797935797_1',
)
})
describe('isReferencedIdentifier', () => {
test('identifiers in function parameters should not be inferred as references', () => {
expect.assertions(4)
const ast = babelParse(`(({ title }) => [])`)
walkIdentifiers(
ast.program.body[0],
(node, parent, parentStack, isReference) => {
expect(isReference).toBe(false)
expect(isReferencedIdentifier(node, parent, parentStack)).toBe(false)
},
true,
)
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-core",
"version": "3.5.17",
"version": "3.5.18",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",

View File

@ -122,7 +122,7 @@ export function isReferencedIdentifier(
return false
}
if (isReferenced(id, parent)) {
if (isReferenced(id, parent, parentStack[parentStack.length - 2])) {
return true
}
@ -132,7 +132,8 @@ export function isReferencedIdentifier(
case 'AssignmentExpression':
case 'AssignmentPattern':
return true
case 'ObjectPattern':
case 'ObjectProperty':
return parent.key !== id && isInDestructureAssignment(parent, parentStack)
case 'ArrayPattern':
return isInDestructureAssignment(parent, parentStack)
}

View File

@ -24,7 +24,13 @@ import {
getVNodeHelper,
} from '../ast'
import type { TransformContext } from '../transform'
import { PatchFlags, isArray, isString, isSymbol } from '@vue/shared'
import {
PatchFlagNames,
PatchFlags,
isArray,
isString,
isSymbol,
} from '@vue/shared'
import { findDir, isSlotOutlet } from '../utils'
import {
GUARD_REACTIVE_PROPS,
@ -109,6 +115,15 @@ function walk(
? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context)
if (constantType >= ConstantTypes.CAN_CACHE) {
if (
child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION &&
child.codegenNode.arguments.length > 0
) {
child.codegenNode.arguments.push(
PatchFlags.CACHED +
(__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``),
)
}
toCache.push(child)
continue
}

View File

@ -141,9 +141,9 @@ export function processIf(
}
if (sibling && sibling.type === NodeTypes.IF) {
// Check if v-else was followed by v-else-if
// Check if v-else was followed by v-else-if or there are two adjacent v-else
if (
dir.name === 'else-if' &&
(dir.name === 'else-if' || dir.name === 'else') &&
sibling.branches[sibling.branches.length - 1].condition === undefined
) {
context.onError(

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-dom",
"version": "3.5.17",
"version": "3.5.18",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",

View File

@ -16,6 +16,16 @@ export function render(_ctx, _cache) {
}"
`;
exports[`compiler sfc: transform srcset > transform empty srcset w/ includeAbsolute: true 1`] = `
"import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = { srcset: " " }
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("img", _hoisted_1))
}"
`;
exports[`compiler sfc: transform srcset > transform srcset 1`] = `
"import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
import _imports_0 from './logo.png'

View File

@ -72,6 +72,14 @@ describe('compiler sfc: transform srcset', () => {
).toMatchSnapshot()
})
test('transform empty srcset w/ includeAbsolute: true', () => {
expect(
compileWithSrcset(`<img srcset=" " />`, {
includeAbsolute: true,
}).code,
).toMatchSnapshot()
})
test('transform srcset w/ stringify', () => {
const code = compileWithSrcset(
`<div>${src}</div>`,

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
"version": "3.5.17",
"version": "3.5.18",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",

View File

@ -71,6 +71,7 @@ export const transformSrcset: NodeTransform = (
const shouldProcessUrl = (url: string) => {
return (
url &&
!isExternalUrl(url) &&
!isDataUrl(url) &&
(options.includeAbsolute || isRelativeUrl(url))

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-ssr",
"version": "3.5.17",
"version": "3.5.18",
"description": "@vue/compiler-ssr",
"main": "dist/compiler-ssr.cjs.js",
"types": "dist/compiler-ssr.d.ts",

View File

@ -1,6 +1,6 @@
{
"name": "@vue/reactivity",
"version": "3.5.17",
"version": "3.5.18",
"description": "@vue/reactivity",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",

View File

@ -6,6 +6,7 @@ import {
nodeOps,
ref,
render,
serializeInner,
useSlots,
} from '@vue/runtime-test'
import { createBlock, normalizeVNode } from '../src/vnode'
@ -74,6 +75,10 @@ describe('component: slots', () => {
footer: ['f1', 'f2'],
})
expect(
'[Vue warn]: Non-function value encountered for slot "_inner". Prefer function slots for better performance.',
).toHaveBeenWarned()
expect(
'[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.',
).toHaveBeenWarned()
@ -82,8 +87,8 @@ describe('component: slots', () => {
'[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.',
).toHaveBeenWarned()
expect(slots).not.toHaveProperty('_inner')
expect(slots).not.toHaveProperty('foo')
expect(slots._inner()).toMatchObject([normalizeVNode('_inner')])
expect(slots.header()).toMatchObject([normalizeVNode('header')])
expect(slots.footer()).toMatchObject([
normalizeVNode('f1'),
@ -442,4 +447,22 @@ describe('component: slots', () => {
'Slot "default" invoked outside of the render function',
).toHaveBeenWarned()
})
test('slot name starts with underscore', () => {
const Comp = {
setup(_: any, { slots }: any) {
return () => slots._foo()
},
}
const App = {
setup() {
return () => h(Comp, null, { _foo: () => 'foo' })
},
}
const root = nodeOps.createElement('div')
createApp(App).mount(root)
expect(serializeInner(root)).toBe('foo')
})
})

View File

@ -2230,5 +2230,57 @@ describe('Suspense', () => {
fallback: [h('div'), h('div')],
})
})
// #13559
test('renders multiple async components in Suspense with v-for and updates on items change', async () => {
const CompAsyncSetup = defineAsyncComponent({
props: ['item'],
render(ctx: any) {
return h('div', ctx.item.name)
},
})
const items = ref([
{ id: 1, name: '111' },
{ id: 2, name: '222' },
{ id: 3, name: '333' },
])
const Comp = {
setup() {
return () =>
h(Suspense, null, {
default: () =>
h(
Fragment,
null,
items.value.map(item =>
h(CompAsyncSetup, { item, key: item.id }),
),
),
})
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
await nextTick()
await Promise.all(deps)
expect(serializeInner(root)).toBe(
`<div>111</div><div>222</div><div>333</div>`,
)
items.value = [
{ id: 4, name: '444' },
{ id: 5, name: '555' },
{ id: 6, name: '666' },
]
await nextTick()
await Promise.all(deps)
expect(serializeInner(root)).toBe(
`<div>444</div><div>555</div><div>666</div>`,
)
})
})
})

View File

@ -1160,6 +1160,69 @@ describe('SSR hydration', () => {
)
})
// #13510
test('update async component after parent mount before async component resolve', async () => {
const Comp = {
props: ['toggle'],
render(this: any) {
return h('h1', [
this.toggle ? 'Async component' : 'Updated async component',
])
},
}
let serverResolve: any
let AsyncComp = defineAsyncComponent(
() =>
new Promise(r => {
serverResolve = r
}),
)
const toggle = ref(true)
const App = {
setup() {
onMounted(() => {
// change state, after mount and before async component resolve
nextTick(() => (toggle.value = false))
})
return () => {
return h(AsyncComp, { toggle: toggle.value })
}
},
}
// server render
const htmlPromise = renderToString(h(App))
serverResolve(Comp)
const html = await htmlPromise
expect(html).toMatchInlineSnapshot(`"<h1>Async component</h1>"`)
// hydration
let clientResolve: any
AsyncComp = defineAsyncComponent(
() =>
new Promise(r => {
clientResolve = r
}),
)
const container = document.createElement('div')
container.innerHTML = html
createSSRApp(App).mount(container)
// resolve
clientResolve(Comp)
await new Promise(r => setTimeout(r))
// prevent lazy hydration since the component has been patched
expect('Skipping lazy hydration for component').toHaveBeenWarned()
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<h1>Updated async component</h1>"`,
)
})
test('hydrate safely when property used by async setup changed before render', async () => {
const toggle = ref(true)
@ -1677,6 +1740,35 @@ describe('SSR hydration', () => {
expect(`mismatch`).not.toHaveBeenWarned()
})
// #13394
test('transition appear work with empty content', async () => {
const show = ref(true)
const { vnode, container } = mountWithHydration(
`<template><!----></template>`,
function (this: any) {
return h(
Transition,
{ appear: true },
{
default: () =>
show.value
? renderSlot(this.$slots, 'default')
: createTextVNode('foo'),
},
)
},
)
// empty slot render as a comment node
expect(container.firstChild!.nodeType).toBe(Node.COMMENT_NODE)
expect(vnode.el).toBe(container.firstChild)
expect(`mismatch`).not.toHaveBeenWarned()
show.value = false
await nextTick()
expect(container.innerHTML).toBe('foo')
})
test('transition appear with v-if', () => {
const show = false
const { vnode, container } = mountWithHydration(

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-core",
"version": "3.5.17",
"version": "3.5.18",
"description": "@vue/runtime-core",
"main": "index.js",
"module": "dist/runtime-core.esm-bundler.js",

View File

@ -123,28 +123,30 @@ export function defineAsyncComponent<
__asyncHydrate(el, instance, hydrate) {
let patched = false
;(instance.bu || (instance.bu = [])).push(() => (patched = true))
const performHydrate = () => {
// skip hydration if the component has been patched
if (patched) {
if (__DEV__) {
warn(
`Skipping lazy hydration for component '${getComponentName(resolvedComp!) || resolvedComp!.__file}': ` +
`it was updated before lazy hydration performed.`,
)
}
return
}
hydrate()
}
const doHydrate = hydrateStrategy
? () => {
const performHydrate = () => {
// skip hydration if the component has been patched
if (__DEV__ && patched) {
warn(
`Skipping lazy hydration for component '${getComponentName(resolvedComp!)}': ` +
`it was updated before lazy hydration performed.`,
)
return
}
hydrate()
}
const teardown = hydrateStrategy(performHydrate, cb =>
forEachElement(el, cb),
)
if (teardown) {
;(instance.bum || (instance.bum = [])).push(teardown)
}
;(instance.u || (instance.u = [])).push(() => (patched = true))
}
: hydrate
: performHydrate
if (resolvedComp) {
doHydrate()
} else {

View File

@ -382,17 +382,17 @@ export function withDefaults<
}
export function useSlots(): SetupContext['slots'] {
return getContext().slots
return getContext('useSlots').slots
}
export function useAttrs(): SetupContext['attrs'] {
return getContext().attrs
return getContext('useAttrs').attrs
}
function getContext(): SetupContext {
function getContext(calledFunctionName: string): SetupContext {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
warn(`useContext() called without active instance.`)
warn(`${calledFunctionName}() called without active instance.`)
}
return i.setupContext || (i.setupContext = createSetupContext(i))
}

View File

@ -756,6 +756,7 @@ export function applyOptions(instance: ComponentInternalInstance): void {
Object.defineProperty(exposed, key, {
get: () => publicThis[key],
set: val => (publicThis[key] = val),
enumerable: true,
})
})
} else if (!instance.exposed) {

View File

@ -86,7 +86,8 @@ export type RawSlots = {
__?: number[]
}
const isInternalKey = (key: string) => key[0] === '_' || key === '$stable'
const isInternalKey = (key: string) =>
key === '_' || key === '__' || key === '_ctx' || key === '$stable'
const normalizeSlotValue = (value: unknown): VNode[] =>
isArray(value)

View File

@ -28,12 +28,10 @@ export function endMeasure(
if (instance.appContext.config.performance && isSupported()) {
const startTag = `vue-${type}-${instance.uid}`
const endTag = startTag + `:end`
const measureName = `<${formatComponentName(instance, instance.type)}> ${type}`
perf.mark(endTag)
perf.measure(
`<${formatComponentName(instance, instance.type)}> ${type}`,
startTag,
endTag,
)
perf.measure(measureName, startTag, endTag)
perf.clearMeasures(measureName)
perf.clearMarks(startTag)
perf.clearMarks(endTag)
}

View File

@ -1226,6 +1226,7 @@ function baseCreateRenderer(
if (!initialVNode.el) {
const placeholder = (instance.subTree = createVNode(Comment))
processCommentNode(null, placeholder, container!, anchor)
initialVNode.placeholder = placeholder.el
}
} else {
setupRenderEffect(
@ -1979,8 +1980,12 @@ function baseCreateRenderer(
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
const anchorVNode = c2[nextIndex + 1] as VNode
const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
nextIndex + 1 < l2
? // #13559, fallback to el placeholder for unresolved async component
anchorVNode.el || anchorVNode.placeholder
: parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// mount new
patch(

View File

@ -2,6 +2,7 @@ import type { SuspenseBoundary } from './components/Suspense'
import type { VNode, VNodeNormalizedRef, VNodeNormalizedRefAtom } from './vnode'
import {
EMPTY_OBJ,
NO,
ShapeFlags,
hasOwn,
isArray,
@ -77,7 +78,7 @@ export function setRef(
const rawSetupState = toRaw(setupState)
const canSetSetupRef =
setupState === EMPTY_OBJ
? () => false
? NO
: (key: string) => {
if (__DEV__) {
if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) {

View File

@ -196,6 +196,7 @@ export interface VNode<
// DOM
el: HostNode | null
placeholder: HostNode | null // async component el placeholder
anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target
targetStart: HostNode | null // teleport target start anchor
@ -711,6 +712,8 @@ export function cloneVNode<T, U>(
suspense: vnode.suspense,
ssContent: vnode.ssContent && cloneVNode(vnode.ssContent),
ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback),
placeholder: vnode.placeholder,
el: vnode.el,
anchor: vnode.anchor,
ctx: vnode.ctx,

View File

@ -1402,6 +1402,34 @@ describe('defineCustomElement', () => {
})
describe('expose', () => {
test('expose w/ options api', async () => {
const E = defineCustomElement({
data() {
return {
value: 0,
}
},
methods: {
foo() {
;(this as any).value++
},
},
expose: ['foo'],
render(_ctx: any) {
return h('div', null, _ctx.value)
},
})
customElements.define('my-el-expose-options-api', E)
container.innerHTML = `<my-el-expose-options-api></my-el-expose-options-api>`
const e = container.childNodes[0] as VueElement & {
foo: () => void
}
expect(e.shadowRoot!.innerHTML).toBe(`<div>0</div>`)
e.foo()
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(`<div>1</div>`)
})
test('expose attributes and callback', async () => {
type SetValue = (value: string) => void
let fn: MockedFunction<SetValue>

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-dom",
"version": "3.5.17",
"version": "3.5.18",
"description": "@vue/runtime-dom",
"main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js",

View File

@ -111,26 +111,106 @@ describe('ssr: slot', () => {
})
test('transition slot', async () => {
const ReusableTransition = {
template: `<transition><slot/></transition>`,
}
const ReusableTransitionWithAppear = {
template: `<transition appear><slot/></transition>`,
}
expect(
await renderToString(
createApp({
components: {
one: {
template: `<transition><slot/></transition>`,
},
one: ReusableTransition,
},
template: `<one><div v-if="false">foo</div></one>`,
}),
),
).toBe(`<!---->`)
expect(await renderToString(createApp(ReusableTransition))).toBe(`<!---->`)
expect(await renderToString(createApp(ReusableTransitionWithAppear))).toBe(
`<template><!----></template>`,
)
expect(
await renderToString(
createApp({
components: {
one: ReusableTransition,
},
template: `<one><slot/></one>`,
}),
),
).toBe(`<!---->`)
expect(
await renderToString(
createApp({
components: {
one: {
template: `<transition><slot/></transition>`,
},
one: ReusableTransitionWithAppear,
},
template: `<one><slot/></one>`,
}),
),
).toBe(`<template><!----></template>`)
expect(
await renderToString(
createApp({
render() {
return h(ReusableTransition, null, {
default: () => null,
})
},
}),
),
).toBe(`<!---->`)
expect(
await renderToString(
createApp({
render() {
return h(ReusableTransitionWithAppear, null, {
default: () => null,
})
},
}),
),
).toBe(`<template><!----></template>`)
expect(
await renderToString(
createApp({
render() {
return h(ReusableTransitionWithAppear, null, {
default: () => [],
})
},
}),
),
).toBe(`<template><!----></template>`)
expect(
await renderToString(
createApp({
render() {
return h(ReusableTransition, null, {
default: () => [],
})
},
}),
),
).toBe(`<!---->`)
expect(
await renderToString(
createApp({
components: {
one: ReusableTransition,
},
template: `<one><div v-if="true">foo</div></one>`,
}),

View File

@ -1,6 +1,6 @@
{
"name": "@vue/server-renderer",
"version": "3.5.17",
"version": "3.5.18",
"description": "@vue/server-renderer",
"main": "index.js",
"module": "dist/server-renderer.esm-bundler.js",

View File

@ -74,6 +74,8 @@ export function ssrRenderSlotInner(
)
} else if (fallbackRenderFn) {
fallbackRenderFn()
} else if (transition) {
push(`<!---->`)
}
} else {
// ssr slot.
@ -110,13 +112,19 @@ export function ssrRenderSlotInner(
end--
}
for (let i = start; i < end; i++) {
push(slotBuffer[i])
if (start < end) {
for (let i = start; i < end; i++) {
push(slotBuffer[i])
}
} else if (transition) {
push(`<!---->`)
}
}
}
} else if (fallbackRenderFn) {
fallbackRenderFn()
} else if (transition) {
push(`<!---->`)
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@vue/shared",
"version": "3.5.17",
"version": "3.5.18",
"description": "internal utils shared across @vue packages",
"main": "index.js",
"module": "dist/shared.esm-bundler.js",

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compat",
"version": "3.5.17",
"version": "3.5.18",
"description": "Vue 3 compatibility build for Vue 2",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",

View File

@ -0,0 +1,85 @@
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
import path from 'node:path'
const { page, html, click } = setupPuppeteer()
beforeEach(async () => {
await page().setContent(`<div id="app"></div>`)
await page().addScriptTag({
path: path.resolve(__dirname, '../../dist/vue.global.js'),
})
})
describe('not leaking', async () => {
// #13661
test(
'cached text vnodes should not retaining detached DOM nodes',
async () => {
const client = await page().createCDPSession()
await page().evaluate(async () => {
const { createApp, ref } = (window as any).Vue
createApp({
components: {
Comp1: {
template: `
<h1><slot></slot></h1>
<div>{{ test.length }}</div>
`,
setup() {
const test = ref([...Array(3000)].map((_, i) => ({ i })))
// @ts-expect-error
window.__REF__ = new WeakRef(test)
return { test }
},
},
Comp2: {
template: `<h2>comp2</h2>`,
},
},
template: `
<button id="toggleBtn" @click="click">button</button>
<Comp1 v-if="toggle">
<div>
<Comp2/>
text node
</div>
</Comp1>
`,
setup() {
const toggle = ref(true)
const click = () => (toggle.value = !toggle.value)
return { toggle, click }
},
}).mount('#app')
})
expect(await html('#app')).toBe(
`<button id="toggleBtn">button</button>` +
`<h1>` +
`<div>` +
`<h2>comp2</h2>` +
` text node ` +
`</div>` +
`</h1>` +
`<div>3000</div>`,
)
await click('#toggleBtn')
expect(await html('#app')).toBe(
`<button id="toggleBtn">button</button><!--v-if-->`,
)
const isCollected = async () =>
// @ts-expect-error
await page().evaluate(() => window.__REF__.deref() === undefined)
while ((await isCollected()) === false) {
await client.send('HeapProfiler.collectGarbage')
}
expect(await isCollected()).toBe(true)
},
E2E_TIMEOUT,
)
})

View File

@ -1,6 +1,6 @@
{
"name": "vue",
"version": "3.5.17",
"version": "3.5.18",
"description": "The progressive JavaScript framework for building modern web UI.",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",