diff --git a/CHANGELOG.md b/CHANGELOG.md
index d490aa19d..2a0b96332 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/package.json b/package.json
index 5b51534bf..e75d5ca1a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"private": true,
- "version": "3.5.17",
+ "version": "3.5.18",
"packageManager": "pnpm@10.13.1",
"type": "module",
"scripts": {
diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap
index b8bef22c4..91a82db5b 100644
--- a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap
@@ -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 */)
])))
}
diff --git a/packages/compiler-core/__tests__/transforms/vIf.spec.ts b/packages/compiler-core/__tests__/transforms/vIf.spec.ts
index 2c2fedab0..73b6e2215 100644
--- a/packages/compiler-core/__tests__/transforms/vIf.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vIf.spec.ts
@@ -301,6 +301,25 @@ describe('compiler: v-if', () => {
])
})
+ test('error on adjacent v-else', () => {
+ const onError = vi.fn()
+
+ const {
+ node: { branches },
+ } = parseWithIfTransform(
+ `
`,
+ { 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
diff --git a/packages/compiler-core/__tests__/utils.spec.ts b/packages/compiler-core/__tests__/utils.spec.ts
index 2d377a271..000b10e11 100644
--- a/packages/compiler-core/__tests__/utils.spec.ts
+++ b/packages/compiler-core/__tests__/utils.spec.ts
@@ -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,
+ )
+ })
+})
diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json
index a3c188a66..a59342aeb 100644
--- a/packages/compiler-core/package.json
+++ b/packages/compiler-core/package.json
@@ -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",
diff --git a/packages/compiler-core/src/babelUtils.ts b/packages/compiler-core/src/babelUtils.ts
index 52fabeea8..51614612b 100644
--- a/packages/compiler-core/src/babelUtils.ts
+++ b/packages/compiler-core/src/babelUtils.ts
@@ -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)
}
diff --git a/packages/compiler-core/src/transforms/cacheStatic.ts b/packages/compiler-core/src/transforms/cacheStatic.ts
index 239ee689a..0f112e19c 100644
--- a/packages/compiler-core/src/transforms/cacheStatic.ts
+++ b/packages/compiler-core/src/transforms/cacheStatic.ts
@@ -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
}
diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts
index 54c505407..8bf5c6a32 100644
--- a/packages/compiler-core/src/transforms/vIf.ts
+++ b/packages/compiler-core/src/transforms/vIf.ts
@@ -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(
diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json
index 95a56f045..38b62f050 100644
--- a/packages/compiler-dom/package.json
+++ b/packages/compiler-dom/package.json
@@ -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",
diff --git a/packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap
index 0469ffaba..28e0af71f 100644
--- a/packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap
+++ b/packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap
@@ -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'
diff --git a/packages/compiler-sfc/__tests__/templateTransformSrcset.spec.ts b/packages/compiler-sfc/__tests__/templateTransformSrcset.spec.ts
index 491731f94..68239bbd1 100644
--- a/packages/compiler-sfc/__tests__/templateTransformSrcset.spec.ts
+++ b/packages/compiler-sfc/__tests__/templateTransformSrcset.spec.ts
@@ -72,6 +72,14 @@ describe('compiler sfc: transform srcset', () => {
).toMatchSnapshot()
})
+ test('transform empty srcset w/ includeAbsolute: true', () => {
+ expect(
+ compileWithSrcset(`
`, {
+ includeAbsolute: true,
+ }).code,
+ ).toMatchSnapshot()
+ })
+
test('transform srcset w/ stringify', () => {
const code = compileWithSrcset(
`${src}
`,
diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json
index 45468177a..ff1f2e48d 100644
--- a/packages/compiler-sfc/package.json
+++ b/packages/compiler-sfc/package.json
@@ -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",
diff --git a/packages/compiler-sfc/src/template/transformSrcset.ts b/packages/compiler-sfc/src/template/transformSrcset.ts
index 8f00f86e3..40fba4882 100644
--- a/packages/compiler-sfc/src/template/transformSrcset.ts
+++ b/packages/compiler-sfc/src/template/transformSrcset.ts
@@ -71,6 +71,7 @@ export const transformSrcset: NodeTransform = (
const shouldProcessUrl = (url: string) => {
return (
+ url &&
!isExternalUrl(url) &&
!isDataUrl(url) &&
(options.includeAbsolute || isRelativeUrl(url))
diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json
index 31681468f..d11be67c4 100644
--- a/packages/compiler-ssr/package.json
+++ b/packages/compiler-ssr/package.json
@@ -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",
diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json
index d861a5fb1..a26334ee8 100644
--- a/packages/reactivity/package.json
+++ b/packages/reactivity/package.json
@@ -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",
diff --git a/packages/runtime-core/__tests__/componentSlots.spec.ts b/packages/runtime-core/__tests__/componentSlots.spec.ts
index ad87e5367..765fce33e 100644
--- a/packages/runtime-core/__tests__/componentSlots.spec.ts
+++ b/packages/runtime-core/__tests__/componentSlots.spec.ts
@@ -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')
+ })
})
diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts
index 65e801de2..563c91a17 100644
--- a/packages/runtime-core/__tests__/components/Suspense.spec.ts
+++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts
@@ -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(
+ `111
222
333
`,
+ )
+
+ items.value = [
+ { id: 4, name: '444' },
+ { id: 5, name: '555' },
+ { id: 6, name: '666' },
+ ]
+ await nextTick()
+ await Promise.all(deps)
+ expect(serializeInner(root)).toBe(
+ `444
555
666
`,
+ )
+ })
})
})
diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts
index 4a9e0fac2..6828e61ec 100644
--- a/packages/runtime-core/__tests__/hydration.spec.ts
+++ b/packages/runtime-core/__tests__/hydration.spec.ts
@@ -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(`"Async component
"`)
+
+ // 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(
+ `"Updated async component
"`,
+ )
+ })
+
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(
+ ``,
+ 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(
diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json
index 1e60e27cb..5c2541c9c 100644
--- a/packages/runtime-core/package.json
+++ b/packages/runtime-core/package.json
@@ -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",
diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts
index cb675f06e..ab4ab51b6 100644
--- a/packages/runtime-core/src/apiAsyncComponent.ts
+++ b/packages/runtime-core/src/apiAsyncComponent.ts
@@ -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 {
diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts
index 2ddaeb509..209b3364c 100644
--- a/packages/runtime-core/src/apiSetupHelpers.ts
+++ b/packages/runtime-core/src/apiSetupHelpers.ts
@@ -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))
}
diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts
index 5db6a0a17..25d21477c 100644
--- a/packages/runtime-core/src/componentOptions.ts
+++ b/packages/runtime-core/src/componentOptions.ts
@@ -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) {
diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts
index 6114f6c86..380728750 100644
--- a/packages/runtime-core/src/componentSlots.ts
+++ b/packages/runtime-core/src/componentSlots.ts
@@ -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)
diff --git a/packages/runtime-core/src/profiling.ts b/packages/runtime-core/src/profiling.ts
index 1984f5a21..4e832a7c4 100644
--- a/packages/runtime-core/src/profiling.ts
+++ b/packages/runtime-core/src/profiling.ts
@@ -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)
}
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index a57be791a..f046e93ad 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -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(
diff --git a/packages/runtime-core/src/rendererTemplateRef.ts b/packages/runtime-core/src/rendererTemplateRef.ts
index ca21030dc..cea9e5b84 100644
--- a/packages/runtime-core/src/rendererTemplateRef.ts
+++ b/packages/runtime-core/src/rendererTemplateRef.ts
@@ -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])) {
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index a8c5340cd..cd1ef948d 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -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(
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,
diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts
index c44840df5..07ea09148 100644
--- a/packages/runtime-dom/__tests__/customElement.spec.ts
+++ b/packages/runtime-dom/__tests__/customElement.spec.ts
@@ -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 = ``
+ const e = container.childNodes[0] as VueElement & {
+ foo: () => void
+ }
+ expect(e.shadowRoot!.innerHTML).toBe(`0
`)
+ e.foo()
+ await nextTick()
+ expect(e.shadowRoot!.innerHTML).toBe(`1
`)
+ })
test('expose attributes and callback', async () => {
type SetValue = (value: string) => void
let fn: MockedFunction
diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json
index 4d71206e2..729406f7a 100644
--- a/packages/runtime-dom/package.json
+++ b/packages/runtime-dom/package.json
@@ -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",
diff --git a/packages/server-renderer/__tests__/ssrSlot.spec.ts b/packages/server-renderer/__tests__/ssrSlot.spec.ts
index 4cc7fd97e..214e6ee84 100644
--- a/packages/server-renderer/__tests__/ssrSlot.spec.ts
+++ b/packages/server-renderer/__tests__/ssrSlot.spec.ts
@@ -111,26 +111,106 @@ describe('ssr: slot', () => {
})
test('transition slot', async () => {
+ const ReusableTransition = {
+ template: ``,
+ }
+
+ const ReusableTransitionWithAppear = {
+ template: ``,
+ }
+
expect(
await renderToString(
createApp({
components: {
- one: {
- template: ``,
- },
+ one: ReusableTransition,
},
template: `foo
`,
}),
),
).toBe(``)
+ expect(await renderToString(createApp(ReusableTransition))).toBe(``)
+
+ expect(await renderToString(createApp(ReusableTransitionWithAppear))).toBe(
+ ``,
+ )
+
+ expect(
+ await renderToString(
+ createApp({
+ components: {
+ one: ReusableTransition,
+ },
+ template: ``,
+ }),
+ ),
+ ).toBe(``)
+
expect(
await renderToString(
createApp({
components: {
- one: {
- template: ``,
- },
+ one: ReusableTransitionWithAppear,
+ },
+ template: ``,
+ }),
+ ),
+ ).toBe(``)
+
+ expect(
+ await renderToString(
+ createApp({
+ render() {
+ return h(ReusableTransition, null, {
+ default: () => null,
+ })
+ },
+ }),
+ ),
+ ).toBe(``)
+
+ expect(
+ await renderToString(
+ createApp({
+ render() {
+ return h(ReusableTransitionWithAppear, null, {
+ default: () => null,
+ })
+ },
+ }),
+ ),
+ ).toBe(``)
+
+ expect(
+ await renderToString(
+ createApp({
+ render() {
+ return h(ReusableTransitionWithAppear, null, {
+ default: () => [],
+ })
+ },
+ }),
+ ),
+ ).toBe(``)
+
+ expect(
+ await renderToString(
+ createApp({
+ render() {
+ return h(ReusableTransition, null, {
+ default: () => [],
+ })
+ },
+ }),
+ ),
+ ).toBe(``)
+
+ expect(
+ await renderToString(
+ createApp({
+ components: {
+ one: ReusableTransition,
},
template: `foo
`,
}),
diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json
index 18ede1625..0fe43c72c 100644
--- a/packages/server-renderer/package.json
+++ b/packages/server-renderer/package.json
@@ -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",
diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts
index 19aa4ce63..2f93a12de 100644
--- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts
+++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts
@@ -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(``)
}
}
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 648179768..63a10598c 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -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",
diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json
index 091dc1030..dbb356958 100644
--- a/packages/vue-compat/package.json
+++ b/packages/vue-compat/package.json
@@ -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",
diff --git a/packages/vue/__tests__/e2e/memory-leak.spec.ts b/packages/vue/__tests__/e2e/memory-leak.spec.ts
new file mode 100644
index 000000000..2412cea2b
--- /dev/null
+++ b/packages/vue/__tests__/e2e/memory-leak.spec.ts
@@ -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(``)
+ 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: `
+
+ {{ test.length }}
+ `,
+ setup() {
+ const test = ref([...Array(3000)].map((_, i) => ({ i })))
+ // @ts-expect-error
+ window.__REF__ = new WeakRef(test)
+
+ return { test }
+ },
+ },
+ Comp2: {
+ template: `comp2
`,
+ },
+ },
+ template: `
+
+
+
+
+ text node
+
+
+ `,
+ setup() {
+ const toggle = ref(true)
+ const click = () => (toggle.value = !toggle.value)
+ return { toggle, click }
+ },
+ }).mount('#app')
+ })
+
+ expect(await html('#app')).toBe(
+ `` +
+ `` +
+ `
` +
+ `
comp2
` +
+ ` text node ` +
+ `` +
+ `` +
+ `3000
`,
+ )
+
+ await click('#toggleBtn')
+ expect(await html('#app')).toBe(
+ ``,
+ )
+
+ 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,
+ )
+})
diff --git a/packages/vue/package.json b/packages/vue/package.json
index 5d5704cb3..897bdec9e 100644
--- a/packages/vue/package.json
+++ b/packages/vue/package.json
@@ -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",