mirror of https://github.com/vuejs/core.git
Merge branch 'main' into renovate/lint
This commit is contained in:
commit
b1f959f52c
20
CHANGELOG.md
20
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)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"private": true,
|
||||
"version": "3.5.17",
|
||||
"version": "3.5.18",
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
@ -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 */)
|
||||
])))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>`,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -71,6 +71,7 @@ export const transformSrcset: NodeTransform = (
|
|||
|
||||
const shouldProcessUrl = (url: string) => {
|
||||
return (
|
||||
url &&
|
||||
!isExternalUrl(url) &&
|
||||
!isDataUrl(url) &&
|
||||
(options.includeAbsolute || isRelativeUrl(url))
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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>`,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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])) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>`,
|
||||
}),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(`<!---->`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
})
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue