chore: Merge branch 'main' into minor

This commit is contained in:
Evan You 2024-07-31 16:49:22 +08:00
commit f35080daf5
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
21 changed files with 927 additions and 717 deletions

View File

@ -82,7 +82,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
## Development Setup
You will need [Node.js](https://nodejs.org) **version 18.12+**, and [PNPM](https://pnpm.io) **version 8+**.
You will need [Node.js](https://nodejs.org) with minimum version as specified in the [`.node-version`](https://github.com/vuejs/core/blob/main/.node-version) file, and [PNPM](https://pnpm.io) with minimum version as specified in the [`"packageManager"` field in `package.json`](https://github.com/vuejs/core/blob/main/package.json#L4).
We also recommend installing [@antfu/ni](https://github.com/antfu/ni) to help switching between repos using different package managers. `ni` also provides the handy `nr` command which running npm scripts easier.

View File

@ -31,4 +31,4 @@ jobs:
- name: Run prettier
run: pnpm run format
- uses: autofix-ci/action@2891949f3779a1cafafae1523058501de3d4e944
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c

View File

@ -1,6 +1,21 @@
# [3.5.0-alpha.4](https://github.com/vuejs/core/compare/v3.4.34...v3.5.0-alpha.4) (2024-07-24)
## [3.4.35](https://github.com/vuejs/core/compare/v3.4.34...v3.4.35) (2024-07-31)
### Bug Fixes
* **teleport/ssr:** fix Teleport hydration regression due to targetStart anchor addition ([7b18cdb](https://github.com/vuejs/core/commit/7b18cdb0b53a94007ca6a3675bf41b5d3153fec6))
* **teleport/ssr:** ensure targetAnchor and targetStart not null during hydration ([#11456](https://github.com/vuejs/core/issues/11456)) ([12667da](https://github.com/vuejs/core/commit/12667da4879f980dcf2c50e36f3642d085a87d71)), closes [#11400](https://github.com/vuejs/core/issues/11400)
* **types/ref:** allow getter and setter types to be unrelated ([#11442](https://github.com/vuejs/core/issues/11442)) ([e0b2975](https://github.com/vuejs/core/commit/e0b2975ef65ae6a0be0aa0a0df43fb887c665251))
### Performance Improvements
* **runtime-core:** improve efficiency of normalizePropsOptions ([#11409](https://github.com/vuejs/core/issues/11409)) ([5680142](https://github.com/vuejs/core/commit/5680142e68096c42e66da9f4c6220d040d7c56ba)), closes [#9739](https://github.com/vuejs/core/issues/9739)
# [3.5.0-alpha.4](https://github.com/vuejs/core/compare/v3.4.34...v3.5.0-alpha.4) (2024-07-24)
### Bug Fixes
* **suspense/hydration:** fix hydration timing of async component inside suspense ([1b8e197](https://github.com/vuejs/core/commit/1b8e197a5b65d67a9703b8511786fb81df9aa7cc)), closes [#6638](https://github.com/vuejs/core/issues/6638)

View File

@ -1,7 +1,7 @@
{
"private": true,
"version": "3.5.0-alpha.4",
"packageManager": "pnpm@9.5.0",
"packageManager": "pnpm@9.6.0",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
@ -66,21 +66,21 @@
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.6.13",
"@swc/core": "^1.7.3",
"@types/hash-sum": "^1.0.2",
"@types/node": "^20.14.10",
"@types/node": "^20.14.13",
"@types/semver": "^7.5.8",
"@vitest/coverage-istanbul": "^1.6.0",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^4.1.0",
"conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1",
"esbuild": "^0.23.0",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.6.0",
"eslint-plugin-import-x": "^0.5.3",
"eslint": "^9.8.0",
"eslint-plugin-import-x": "^3.1.0",
"eslint-plugin-vitest": "^0.5.4",
"estree-walker": "catalog:",
"jsdom": "^24.1.0",
"jsdom": "^24.1.1",
"lint-staged": "^15.2.7",
"lodash": "^4.17.21",
"magic-string": "^0.30.10",
@ -88,23 +88,23 @@
"marked": "^12.0.2",
"npm-run-all2": "^6.2.2",
"picocolors": "^1.0.1",
"prettier": "^3.3.2",
"prettier": "^3.3.3",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.3",
"puppeteer": "~22.12.1",
"puppeteer": "~22.14.0",
"rimraf": "^5.0.9",
"rollup": "^4.18.1",
"rollup": "^4.19.1",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.6.2",
"semver": "^7.6.3",
"serve": "^14.2.3",
"simple-git-hooks": "^2.11.1",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.6.3",
"tsx": "^4.16.2",
"typescript": "~5.4.5",
"typescript-eslint": "^7.15.0",
"typescript-eslint": "^7.17.0",
"vite": "catalog:",
"vitest": "^1.6.0"
},

View File

@ -49,7 +49,7 @@
"@vue/shared": "workspace:*",
"estree-walker": "catalog:",
"magic-string": "catalog:",
"postcss": "^8.4.39",
"postcss": "^8.4.40",
"source-map-js": "catalog:"
},
"devDependencies": {
@ -60,7 +60,7 @@
"merge-source-map": "^1.1.0",
"minimatch": "^9.0.5",
"postcss-modules": "^6.0.0",
"postcss-selector-parser": "^6.1.0",
"postcss-selector-parser": "^6.1.1",
"pug": "^3.0.3",
"sass": "^1.77.8"
}

View File

@ -173,6 +173,16 @@ describe('ref with generic', <T extends { name: string }>() => {
expectType<string>(ss.value.name)
})
describe('allow getter and setter types to be unrelated', <T>() => {
const a = { b: ref(0) }
const c = ref(a)
c.value = a
const d = {} as T
const e = ref(d)
e.value = d
})
// shallowRef
type Status = 'initial' | 'ready' | 'invalidating'
const shallowStatus = shallowRef<Status>('initial')

View File

@ -1,5 +1,6 @@
import {
type ComputedRef,
type MaybeRef,
type Ref,
computed,
defineComponent,
@ -203,3 +204,10 @@ defineComponent({
expectType<{ foo: string }>(value)
})
}
{
const css: MaybeRef<string> = ''
watch(ref(css), value => {
expectType<string>(value)
})
}

View File

@ -169,19 +169,6 @@ function createForEach(isReadonly: boolean, isShallow: boolean) {
}
}
interface Iterable {
[Symbol.iterator](): Iterator
}
interface Iterator {
next(value?: any): IterationResult
}
interface IterationResult {
value: any
done: boolean
}
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
@ -190,7 +177,7 @@ function createIterableMethod(
return function (
this: IterableCollections,
...args: unknown[]
): Iterable & Iterator {
): Iterable<unknown> & Iterator<unknown> {
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const targetIsMap = isMap(rawTarget)

View File

@ -23,8 +23,9 @@ import { warn } from './warning'
declare const RefSymbol: unique symbol
export declare const RawSymbol: unique symbol
export interface Ref<T = any> {
value: T
export interface Ref<T = any, S = T> {
get value(): T
set value(_: S)
/**
* Type differentiator only.
* We need this to be in public d.ts but don't want it to show up in IDE
@ -51,7 +52,7 @@ export function isRef(r: any): r is Ref {
* @param value - The object to wrap in the ref.
* @see {@link https://vuejs.org/api/reactivity-core.html#ref}
*/
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T>(value: T): Ref<UnwrapRef<T>, UnwrapRef<T> | T>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value, false)

View File

@ -265,7 +265,7 @@ describe('SSR hydration', () => {
const fn = vi.fn()
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport'
teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!--teleport anchor-->`
teleportContainer.innerHTML = `<!--teleport start anchor--><span>foo</span><span class="foo"></span><!--teleport anchor-->`
document.body.appendChild(teleportContainer)
const { vnode, container } = mountWithHydration(
@ -281,13 +281,14 @@ describe('SSR hydration', () => {
expect(vnode.anchor).toBe(container.lastChild)
expect(vnode.target).toBe(teleportContainer)
expect(vnode.targetStart).toBe(teleportContainer.childNodes[0])
expect((vnode.children as VNode[])[0].el).toBe(
teleportContainer.childNodes[0],
)
expect((vnode.children as VNode[])[1].el).toBe(
teleportContainer.childNodes[1],
)
expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[2])
expect((vnode.children as VNode[])[1].el).toBe(
teleportContainer.childNodes[2],
)
expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[3])
// event handler
triggerEvent('click', teleportContainer.querySelector('.foo')!)
@ -296,7 +297,7 @@ describe('SSR hydration', () => {
msg.value = 'bar'
await nextTick()
expect(teleportContainer.innerHTML).toBe(
`<span>bar</span><span class="bar"></span><!--teleport anchor-->`,
`<!--teleport start anchor--><span>bar</span><span class="bar"></span><!--teleport anchor-->`,
)
})
@ -326,7 +327,7 @@ describe('SSR hydration', () => {
const teleportHtml = ctx.teleports!['#teleport2']
expect(teleportHtml).toMatchInlineSnapshot(
`"<span>foo</span><span class="foo"></span><!--teleport anchor--><span>foo2</span><span class="foo2"></span><!--teleport anchor-->"`,
`"<!--teleport start anchor--><span>foo</span><span class="foo"></span><!--teleport anchor--><!--teleport start anchor--><span>foo2</span><span class="foo2"></span><!--teleport anchor-->"`,
)
teleportContainer.innerHTML = teleportHtml
@ -342,16 +343,18 @@ describe('SSR hydration', () => {
expect(teleportVnode2.anchor).toBe(container.childNodes[4])
expect(teleportVnode1.target).toBe(teleportContainer)
expect(teleportVnode1.targetStart).toBe(teleportContainer.childNodes[0])
expect((teleportVnode1 as any).children[0].el).toBe(
teleportContainer.childNodes[0],
teleportContainer.childNodes[1],
)
expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[2])
expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[3])
expect(teleportVnode2.target).toBe(teleportContainer)
expect(teleportVnode2.targetStart).toBe(teleportContainer.childNodes[4])
expect((teleportVnode2 as any).children[0].el).toBe(
teleportContainer.childNodes[3],
teleportContainer.childNodes[5],
)
expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[5])
expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[7])
// // event handler
triggerEvent('click', teleportContainer.querySelector('.foo')!)
@ -363,7 +366,7 @@ describe('SSR hydration', () => {
msg.value = 'bar'
await nextTick()
expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
`"<span>bar</span><span class="bar"></span><!--teleport anchor--><span>bar2</span><span class="bar2"></span><!--teleport anchor-->"`,
`"<!--teleport start anchor--><span>bar</span><span class="bar"></span><!--teleport anchor--><!--teleport start anchor--><span>bar2</span><span class="bar2"></span><!--teleport anchor-->"`,
)
})
@ -390,7 +393,9 @@ describe('SSR hydration', () => {
)
const teleportHtml = ctx.teleports!['#teleport3']
expect(teleportHtml).toMatchInlineSnapshot(`"<!--teleport anchor-->"`)
expect(teleportHtml).toMatchInlineSnapshot(
`"<!--teleport start anchor--><!--teleport anchor-->"`,
)
teleportContainer.innerHTML = teleportHtml
document.body.appendChild(teleportContainer)
@ -413,7 +418,8 @@ describe('SSR hydration', () => {
expect(children[2].el).toBe(container.childNodes[6])
expect(teleportVnode.target).toBe(teleportContainer)
expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[0])
expect(teleportVnode.targetStart).toBe(teleportContainer.childNodes[0])
expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[1])
// // event handler
triggerEvent('click', container.querySelector('.foo')!)
@ -454,7 +460,7 @@ describe('SSR hydration', () => {
test('Teleport (as component root)', () => {
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport4'
teleportContainer.innerHTML = `hello<!--teleport anchor-->`
teleportContainer.innerHTML = `<!--teleport start anchor-->hello<!--teleport anchor-->`
document.body.appendChild(teleportContainer)
const wrapper = {
@ -483,7 +489,7 @@ describe('SSR hydration', () => {
test('Teleport (nested)', () => {
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport5'
teleportContainer.innerHTML = `<div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><div>child</div><!--teleport anchor-->`
teleportContainer.innerHTML = `<!--teleport start anchor--><div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><!--teleport start anchor--><div>child</div><!--teleport anchor-->`
document.body.appendChild(teleportContainer)
const { vnode, container } = mountWithHydration(
@ -498,7 +504,7 @@ describe('SSR hydration', () => {
expect(vnode.anchor).toBe(container.lastChild)
const childDivVNode = (vnode as any).children[0]
const div = teleportContainer.firstChild
const div = teleportContainer.childNodes[1]
expect(childDivVNode.el).toBe(div)
expect(vnode.targetAnchor).toBe(div?.nextSibling)
@ -512,6 +518,178 @@ describe('SSR hydration', () => {
)
})
test('Teleport unmount (full integration)', async () => {
const Comp1 = {
template: `
<Teleport to="#target">
<span>Teleported Comp1</span>
</Teleport>
`,
}
const Comp2 = {
template: `
<div>Comp2</div>
`,
}
const toggle = ref(true)
const App = {
template: `
<div>
<Comp1 v-if="toggle"/>
<Comp2 v-else/>
</div>
`,
components: {
Comp1,
Comp2,
},
setup() {
return { toggle }
},
}
const container = document.createElement('div')
const teleportContainer = document.createElement('div')
teleportContainer.id = 'target'
document.body.appendChild(teleportContainer)
// server render
const ctx: SSRContext = {}
container.innerHTML = await renderToString(h(App), ctx)
expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--></div>',
)
teleportContainer.innerHTML = ctx.teleports!['#target']
// hydrate
createSSRApp(App).mount(container)
expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--></div>',
)
expect(teleportContainer.innerHTML).toBe(
'<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
)
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
toggle.value = false
await nextTick()
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
expect(teleportContainer.innerHTML).toBe('')
})
test('Teleport unmount (mismatch + full integration)', async () => {
const Comp1 = {
template: `
<Teleport to="#target">
<span>Teleported Comp1</span>
</Teleport>
`,
}
const Comp2 = {
template: `
<div>Comp2</div>
`,
}
const toggle = ref(true)
const App = {
template: `
<div>
<Comp1 v-if="toggle"/>
<Comp2 v-else/>
</div>
`,
components: {
Comp1,
Comp2,
},
setup() {
return { toggle }
},
}
const container = document.createElement('div')
const teleportContainer = document.createElement('div')
teleportContainer.id = 'target'
document.body.appendChild(teleportContainer)
// server render
container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--></div>',
)
expect(teleportContainer.innerHTML).toBe('')
// hydrate
createSSRApp(App).mount(container)
expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--></div>',
)
expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
expect(`Hydration children mismatch`).toHaveBeenWarned()
toggle.value = false
await nextTick()
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
expect(teleportContainer.innerHTML).toBe('')
})
test('Teleport target change (mismatch + full integration)', async () => {
const target = ref('#target1')
const Comp = {
template: `
<Teleport :to="target">
<span>Teleported</span>
</Teleport>
`,
setup() {
return { target }
},
}
const App = {
template: `
<div>
<Comp />
</div>
`,
components: {
Comp,
},
}
const container = document.createElement('div')
const teleportContainer1 = document.createElement('div')
teleportContainer1.id = 'target1'
const teleportContainer2 = document.createElement('div')
teleportContainer2.id = 'target2'
document.body.appendChild(teleportContainer1)
document.body.appendChild(teleportContainer2)
// server render
container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--></div>',
)
expect(teleportContainer1.innerHTML).toBe('')
expect(teleportContainer2.innerHTML).toBe('')
// hydrate
createSSRApp(App).mount(container)
expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--></div>',
)
expect(teleportContainer1.innerHTML).toBe('<span>Teleported</span>')
expect(teleportContainer2.innerHTML).toBe('')
expect(`Hydration children mismatch`).toHaveBeenWarned()
target.value = '#target2'
await nextTick()
expect(teleportContainer1.innerHTML).toBe('')
expect(teleportContainer2.innerHTML).toBe('<span>Teleported</span>')
})
// compile SSR + client render fn from the same template & hydrate
test('full compiler integration', async () => {
const mounted: string[] = []

View File

@ -47,7 +47,7 @@ import { useSSRContext } from './helpers/useSsrContext'
export type WatchEffect = (onCleanup: OnCleanup) => void
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
export type WatchSource<T = any> = Ref<T, any> | ComputedRef<T> | (() => T)
export type WatchCallback<V = any, OV = any> = (
value: V,

View File

@ -176,12 +176,10 @@ export type ExtractDefaultPropTypes<O> = O extends object
{ [K in keyof Pick<O, DefaultKeys<O>>]: InferPropType<O[K]> }
: {}
type NormalizedProp =
| null
| (PropOptions & {
[BooleanFlags.shouldCast]?: boolean
[BooleanFlags.shouldCastTrue]?: boolean
})
type NormalizedProp = PropOptions & {
[BooleanFlags.shouldCast]?: boolean
[BooleanFlags.shouldCastTrue]?: boolean
}
// normalized value is a tuple of the actual normalized options
// and an array of prop keys that need value casting (booleans and defaults)
@ -566,16 +564,36 @@ export function normalizePropsOptions(
const opt = raw[key]
const prop: NormalizedProp = (normalized[normalizedKey] =
isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt))
if (prop) {
const booleanIndex = getTypeIndex(Boolean, prop.type)
const stringIndex = getTypeIndex(String, prop.type)
prop[BooleanFlags.shouldCast] = booleanIndex > -1
prop[BooleanFlags.shouldCastTrue] =
stringIndex < 0 || booleanIndex < stringIndex
// if the prop needs boolean casting or default value
if (booleanIndex > -1 || hasOwn(prop, 'default')) {
needCastKeys.push(normalizedKey)
const propType = prop.type
let shouldCast = false
let shouldCastTrue = true
if (isArray(propType)) {
for (let index = 0; index < propType.length; ++index) {
const type = propType[index]
const typeName = isFunction(type) && type.name
if (typeName === 'Boolean') {
shouldCast = true
break
} else if (typeName === 'String') {
// If we find `String` before `Boolean`, e.g. `[String, Boolean]`,
// we need to handle the casting slightly differently. Props
// passed as `<Comp checked="">` or `<Comp checked="checked">`
// will either be treated as strings or converted to a boolean
// `true`, depending on the order of the types.
shouldCastTrue = false
}
}
} else {
shouldCast = isFunction(propType) && propType.name === 'Boolean'
}
prop[BooleanFlags.shouldCast] = shouldCast
prop[BooleanFlags.shouldCastTrue] = shouldCastTrue
// if the prop needs boolean casting or default value
if (shouldCast || hasOwn(prop, 'default')) {
needCastKeys.push(normalizedKey)
}
}
}
@ -597,6 +615,7 @@ function validatePropName(key: string) {
return false
}
// dev only
// use function string name to check type constructors
// so that it works across vms / iframes.
function getType(ctor: Prop<any> | null): string {
@ -619,22 +638,6 @@ function getType(ctor: Prop<any> | null): string {
return ''
}
function isSameType(a: Prop<any> | null, b: Prop<any> | null): boolean {
return getType(a) === getType(b)
}
function getTypeIndex(
type: Prop<any>,
expectedTypes: PropType<any> | void | null | true,
): number {
if (isArray(expectedTypes)) {
return expectedTypes.findIndex(t => isSameType(t, type))
} else if (isFunction(expectedTypes)) {
return isSameType(expectedTypes, type) ? 0 : -1
}
return -1
}
/**
* dev only
*/

View File

@ -112,13 +112,8 @@ export const TeleportImpl = {
const mainAnchor = (n2.anchor = __DEV__
? createComment('teleport end')
: createText(''))
const targetStart = (n2.targetStart = createText(''))
const targetAnchor = (n2.targetAnchor = createText(''))
insert(placeholder, container, anchor)
insert(mainAnchor, container, anchor)
// attach a special property so we can skip teleported content in
// renderer's nextSibling search
targetStart[TeleportEndKey] = targetAnchor
const mount = (container: RendererElement, anchor: RendererNode) => {
// Teleport *always* has Array children. This is enforced in both the
@ -139,9 +134,8 @@ export const TeleportImpl = {
const mountToTarget = () => {
const target = (n2.target = resolveTarget(n2.props, querySelector))
const targetAnchor = prepareAnchor(target, n2, createText, insert)
if (target) {
insert(targetStart, target)
insert(targetAnchor, target)
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
if (namespace !== 'svg' && isTargetSVG(target)) {
namespace = 'svg'
@ -375,7 +369,7 @@ function hydrateTeleport(
slotScopeIds: string[] | null,
optimized: boolean,
{
o: { nextSibling, parentNode, querySelector },
o: { nextSibling, parentNode, querySelector, insert, createText },
}: RendererInternals<Node, Element>,
hydrateChildren: (
node: Node | null,
@ -407,7 +401,8 @@ function hydrateTeleport(
slotScopeIds,
optimized,
)
vnode.targetAnchor = targetNode
vnode.targetStart = targetNode
vnode.targetAnchor = targetNode && nextSibling(targetNode)
} else {
vnode.anchor = nextSibling(node)
@ -416,21 +411,29 @@ function hydrateTeleport(
// could be nested teleports
let targetAnchor = targetNode
while (targetAnchor) {
targetAnchor = nextSibling(targetAnchor)
if (
targetAnchor &&
targetAnchor.nodeType === 8 &&
(targetAnchor as Comment).data === 'teleport anchor'
) {
vnode.targetAnchor = targetAnchor
;(target as TeleportTargetElement)._lpa =
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
break
if (targetAnchor && targetAnchor.nodeType === 8) {
if ((targetAnchor as Comment).data === 'teleport start anchor') {
vnode.targetStart = targetAnchor
} else if ((targetAnchor as Comment).data === 'teleport anchor') {
vnode.targetAnchor = targetAnchor
;(target as TeleportTargetElement)._lpa =
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
break
}
}
targetAnchor = nextSibling(targetAnchor)
}
// #11400 if the HTML corresponding to Teleport is not embedded in the
// correct position on the final page during SSR. the targetAnchor will
// always be null, we need to manually add targetAnchor to ensure
// Teleport it can properly unmount or move
if (!vnode.targetAnchor) {
prepareAnchor(target, vnode, createText, insert)
}
hydrateChildren(
targetNode,
targetNode && nextSibling(targetNode),
vnode,
target,
parentComponent,
@ -469,3 +472,24 @@ function updateCssVars(vnode: VNode) {
ctx.ut()
}
}
function prepareAnchor(
target: RendererElement | null,
vnode: TeleportVNode,
createText: RendererOptions['createText'],
insert: RendererOptions['insert'],
) {
const targetStart = (vnode.targetStart = createText(''))
const targetAnchor = (vnode.targetAnchor = createText(''))
// attach a special property, so we can skip teleported content in
// renderer's nextSibling search
targetStart[TeleportEndKey] = targetAnchor
if (target) {
insert(targetStart, target)
insert(targetAnchor, target)
}
return targetAnchor
}

View File

@ -28,7 +28,7 @@ describe('ssrRenderTeleport', () => {
)
expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe(
`<div>content</div><!--teleport anchor-->`,
`<!--teleport start anchor--><div>content</div><!--teleport anchor-->`,
)
})
@ -56,7 +56,9 @@ describe('ssrRenderTeleport', () => {
expect(html).toBe(
'<!--teleport start--><div>content</div><!--teleport end-->',
)
expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
expect(ctx.teleports!['#target']).toBe(
`<!--teleport start anchor--><!--teleport anchor-->`,
)
})
test('teleport rendering (vnode)', async () => {
@ -73,7 +75,7 @@ describe('ssrRenderTeleport', () => {
)
expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe(
'<span>hello</span><!--teleport anchor-->',
'<!--teleport start anchor--><span>hello</span><!--teleport anchor-->',
)
})
@ -93,7 +95,9 @@ describe('ssrRenderTeleport', () => {
expect(html).toBe(
'<!--teleport start--><span>hello</span><!--teleport end-->',
)
expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
expect(ctx.teleports!['#target']).toBe(
`<!--teleport start anchor--><!--teleport anchor-->`,
)
})
test('multiple teleports with same target', async () => {
@ -115,7 +119,8 @@ describe('ssrRenderTeleport', () => {
'<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>',
)
expect(ctx.teleports!['#target']).toBe(
'<span>hello</span><!--teleport anchor-->world<!--teleport anchor-->',
'<!--teleport start anchor--><span>hello</span><!--teleport anchor-->' +
'<!--teleport start anchor-->world<!--teleport anchor-->',
)
})
@ -134,7 +139,7 @@ describe('ssrRenderTeleport', () => {
)
expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe(
`<div>content</div><!--teleport anchor-->`,
`<!--teleport start anchor--><div>content</div><!--teleport anchor-->`,
)
})
@ -169,7 +174,7 @@ describe('ssrRenderTeleport', () => {
await p
expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe(
`<div>content</div><!--teleport anchor-->`,
`<!--teleport start anchor--><div>content</div><!--teleport anchor-->`,
)
})
})

View File

@ -29,9 +29,10 @@ export function ssrRenderTeleport(
if (disabled) {
contentRenderFn(parentPush)
teleportContent = `<!--teleport anchor-->`
teleportContent = `<!--teleport start anchor--><!--teleport anchor-->`
} else {
const { getBuffer, push } = createBuffer()
push(`<!--teleport start anchor-->`)
contentRenderFn(push)
push(`<!--teleport anchor-->`)
teleportContent = getBuffer()

View File

@ -9,7 +9,7 @@
"serve": "vite preview"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"@vitejs/plugin-vue": "^5.1.1",
"vite": "catalog:"
},
"dependencies": {

View File

@ -80,7 +80,7 @@ function toggleDark() {
pkg="vue"
label="Vue Version"
>
<li>
<li :class="{ active: vueVersion === `@${currentCommit}` }">
<a @click="resetVueVersion">This Commit ({{ currentCommit }})</a>
</li>
<li>

View File

@ -74,7 +74,12 @@ onMounted(() => {
<ul class="versions" :class="{ expanded }">
<li v-if="!versions"><a>loading versions...</a></li>
<li v-for="ver of versions" :class="{ active: ver === version }">
<li
v-for="(ver, index) of versions"
:class="{
active: ver === version || (version === 'latest' && index === 0),
}"
>
<a @click="setVersion(ver)">v{{ ver }}</a>
</li>
<div @click="expanded = false">

View File

@ -11,7 +11,7 @@
"vue": "^3.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"vite": "^5.3.3"
"@vitejs/plugin-vue": "^5.1.1",
"vite": "^5.3.5"
}
}

View File

@ -8,7 +8,7 @@ describe('e2e: Transition', () => {
const baseUrl = `file://${path.resolve(__dirname, './transition.html')}`
const duration = process.env.CI ? 200 : 50
const buffer = process.env.CI ? 20 : 5
const buffer = 20
const transitionFinish = (time = duration) => timeout(time + buffer)
@ -29,8 +29,6 @@ describe('e2e: Transition', () => {
test(
'basic transition',
async () => {
await page().goto(baseUrl)
await page().waitForSelector('#app')
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
createApp({
@ -1296,8 +1294,6 @@ describe('e2e: Transition', () => {
test(
'wrapping transition + fallthrough attrs',
async () => {
await page().goto(baseUrl)
await page().waitForSelector('#app')
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
createApp({

File diff suppressed because it is too large Load Diff