Merge tag 'v3.5.0-alpha.5'
This commit is contained in:
commit
d23095e866
|
@ -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.
|
||||
|
||||
|
|
|
@ -31,4 +31,4 @@ jobs:
|
|||
- name: Run prettier
|
||||
run: pnpm run format
|
||||
|
||||
- uses: autofix-ci/action@2891949f3779a1cafafae1523058501de3d4e944
|
||||
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c
|
||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -1,6 +1,31 @@
|
|||
# [3.5.0-alpha.4](https://github.com/vuejs/core/compare/v3.4.34...v3.5.0-alpha.4) (2024-07-24)
|
||||
# [3.5.0-alpha.5](https://github.com/vuejs/core/compare/v3.4.35...v3.5.0-alpha.5) (2024-07-31)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **hydration:** support suppressing hydration mismatch via data-allow-mismatch ([94fb2b8](https://github.com/vuejs/core/commit/94fb2b8106a66bcca1a3f922a246a29fdd1274b1))
|
||||
* lazy hydration strategies for async components ([#11458](https://github.com/vuejs/core/issues/11458)) ([d14a11c](https://github.com/vuejs/core/commit/d14a11c1cdcee88452f17ce97758743c863958f4))
|
||||
|
||||
|
||||
|
||||
## [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)
|
||||
|
|
26
package.json
26
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"private": true,
|
||||
"version": "3.5.0-alpha.4",
|
||||
"packageManager": "pnpm@9.5.0",
|
||||
"version": "3.5.0-alpha.5",
|
||||
"packageManager": "pnpm@9.6.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.js vue vue-vapor",
|
||||
|
@ -66,22 +66,22 @@
|
|||
"@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",
|
||||
"@vitest/ui": "^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",
|
||||
|
@ -89,23 +89,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"
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compiler-core",
|
||||
"version": "3.5.0-alpha.4",
|
||||
"version": "3.5.0-alpha.5",
|
||||
"description": "@vue/compiler-core",
|
||||
"main": "index.js",
|
||||
"module": "dist/compiler-core.esm-bundler.js",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compiler-dom",
|
||||
"version": "3.5.0-alpha.4",
|
||||
"version": "3.5.0-alpha.5",
|
||||
"description": "@vue/compiler-dom",
|
||||
"main": "index.js",
|
||||
"module": "dist/compiler-dom.esm-bundler.js",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compiler-sfc",
|
||||
"version": "3.5.0-alpha.4",
|
||||
"version": "3.5.0-alpha.5",
|
||||
"description": "@vue/compiler-sfc",
|
||||
"main": "dist/compiler-sfc.cjs.js",
|
||||
"module": "dist/compiler-sfc.esm-browser.js",
|
||||
|
@ -50,7 +50,7 @@
|
|||
"@vue/shared": "workspace:*",
|
||||
"estree-walker": "catalog:",
|
||||
"magic-string": "catalog:",
|
||||
"postcss": "^8.4.39",
|
||||
"postcss": "^8.4.40",
|
||||
"source-map-js": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -61,7 +61,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"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compiler-ssr",
|
||||
"version": "3.5.0-alpha.4",
|
||||
"version": "3.5.0-alpha.5",
|
||||
"description": "@vue/compiler-ssr",
|
||||
"main": "dist/compiler-ssr.cjs.js",
|
||||
"types": "dist/compiler-ssr.d.ts",
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/reactivity",
|
||||
"version": "3.5.0-alpha.4",
|
||||
"version": "3.5.0-alpha.5",
|
||||
"description": "@vue/reactivity",
|
||||
"main": "index.js",
|
||||
"module": "dist/reactivity.esm-bundler.js",
|
||||
|
|
|
@ -54,16 +54,14 @@ export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
|
|||
fn: (item: unknown, index: number, array: unknown[]) => unknown,
|
||||
thisArg?: unknown,
|
||||
) {
|
||||
const result = apply(this, 'filter', fn, thisArg)
|
||||
return isProxy(this) && !isShallow(this) ? result.map(toReactive) : result
|
||||
return apply(this, 'filter', fn, thisArg, v => v.map(toReactive))
|
||||
},
|
||||
|
||||
find(
|
||||
fn: (item: unknown, index: number, array: unknown[]) => boolean,
|
||||
thisArg?: unknown,
|
||||
) {
|
||||
const result = apply(this, 'find', fn, thisArg)
|
||||
return isProxy(this) && !isShallow(this) ? toReactive(result) : result
|
||||
return apply(this, 'find', fn, thisArg, toReactive)
|
||||
},
|
||||
|
||||
findIndex(
|
||||
|
@ -77,8 +75,7 @@ export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
|
|||
fn: (item: unknown, index: number, array: unknown[]) => boolean,
|
||||
thisArg?: unknown,
|
||||
) {
|
||||
const result = apply(this, 'findLast', fn, thisArg)
|
||||
return isProxy(this) && !isShallow(this) ? toReactive(result) : result
|
||||
return apply(this, 'findLast', fn, thisArg, toReactive)
|
||||
},
|
||||
|
||||
findLastIndex(
|
||||
|
@ -237,11 +234,14 @@ function apply(
|
|||
method: ArrayMethods,
|
||||
fn: (item: unknown, index: number, array: unknown[]) => unknown,
|
||||
thisArg?: unknown,
|
||||
wrappedRetFn?: (result: any) => unknown,
|
||||
) {
|
||||
const arr = shallowReadArray(self)
|
||||
let needsWrap = false
|
||||
let wrappedFn = fn
|
||||
if (arr !== self) {
|
||||
if (!isShallow(self)) {
|
||||
needsWrap = !isShallow(self)
|
||||
if (needsWrap) {
|
||||
wrappedFn = function (this: unknown, item, index) {
|
||||
return fn.call(this, toReactive(item), index, self)
|
||||
}
|
||||
|
@ -252,7 +252,8 @@ function apply(
|
|||
}
|
||||
}
|
||||
// @ts-expect-error our code is limited to es2016 but user code is not
|
||||
return arr[method](wrappedFn, thisArg)
|
||||
const result = arr[method](wrappedFn, thisArg)
|
||||
return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result
|
||||
}
|
||||
|
||||
// instrument reduce and reduceRight to take ARRAY_ITERATE dependency
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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[] = []
|
||||
|
@ -1824,4 +2002,136 @@ describe('SSR hydration', () => {
|
|||
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
|
||||
describe('data-allow-mismatch', () => {
|
||||
test('element text content', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div data-allow-mismatch="text">foo</div>`,
|
||||
() => h('div', 'bar'),
|
||||
)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div data-allow-mismatch="text">bar</div>',
|
||||
)
|
||||
expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('not enough children', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div data-allow-mismatch="children"></div>`,
|
||||
() => h('div', [h('span', 'foo'), h('span', 'bar')]),
|
||||
)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>',
|
||||
)
|
||||
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('too many children', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
|
||||
() => h('div', [h('span', 'foo')]),
|
||||
)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div data-allow-mismatch="children"><span>foo</span></div>',
|
||||
)
|
||||
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('complete mismatch', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
|
||||
() => h('div', [h('div', 'foo'), h('p', 'bar')]),
|
||||
)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div data-allow-mismatch="children"><div>foo</div><p>bar</p></div>',
|
||||
)
|
||||
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('fragment mismatch removal', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
|
||||
() => h('div', [h('span', 'replaced')]),
|
||||
)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div data-allow-mismatch="children"><span>replaced</span></div>',
|
||||
)
|
||||
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('fragment not enough children', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
|
||||
() => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
|
||||
)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
|
||||
)
|
||||
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('fragment too many children', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
|
||||
() => h('div', [[h('div', 'foo')], h('div', 'baz')]),
|
||||
)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>',
|
||||
)
|
||||
// fragment ends early and attempts to hydrate the extra <div>bar</div>
|
||||
// as 2nd fragment child.
|
||||
expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
|
||||
// excessive children removal
|
||||
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('comment mismatch (element)', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div data-allow-mismatch="children"><span></span></div>`,
|
||||
() => h('div', [createCommentVNode('hi')]),
|
||||
)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div data-allow-mismatch="children"><!--hi--></div>',
|
||||
)
|
||||
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('comment mismatch (text)', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div data-allow-mismatch="children">foobar</div>`,
|
||||
() => h('div', [createCommentVNode('hi')]),
|
||||
)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div data-allow-mismatch="children"><!--hi--></div>',
|
||||
)
|
||||
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('class mismatch', () => {
|
||||
mountWithHydration(
|
||||
`<div class="foo bar" data-allow-mismatch="class"></div>`,
|
||||
() => h('div', { class: 'foo' }),
|
||||
)
|
||||
expect(`Hydration class mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('style mismatch', () => {
|
||||
mountWithHydration(
|
||||
`<div style="color:red;" data-allow-mismatch="style"></div>`,
|
||||
() => h('div', { style: { color: 'green' } }),
|
||||
)
|
||||
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('attr mismatch', () => {
|
||||
mountWithHydration(`<div data-allow-mismatch="attribute"></div>`, () =>
|
||||
h('div', { id: 'foo' }),
|
||||
)
|
||||
mountWithHydration(
|
||||
`<div id="bar" data-allow-mismatch="attribute"></div>`,
|
||||
() => h('div', { id: 'foo' }),
|
||||
)
|
||||
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/runtime-core",
|
||||
"version": "3.5.0-alpha.4",
|
||||
"version": "3.5.0-alpha.5",
|
||||
"description": "@vue/runtime-core",
|
||||
"main": "index.js",
|
||||
"module": "dist/runtime-core.esm-bundler.js",
|
||||
|
|
|
@ -16,6 +16,7 @@ import { ErrorCodes, handleError } from './errorHandling'
|
|||
import { isKeepAlive } from './components/KeepAlive'
|
||||
import { queueJob } from './scheduler'
|
||||
import { markAsyncBoundary } from './helpers/useId'
|
||||
import { type HydrationStrategy, forEachElement } from './hydrationStrategies'
|
||||
|
||||
export type AsyncComponentResolveResult<T = Component> = T | { default: T } // es modules
|
||||
|
||||
|
@ -30,6 +31,7 @@ export interface AsyncComponentOptions<T = any> {
|
|||
delay?: number
|
||||
timeout?: number
|
||||
suspensible?: boolean
|
||||
hydrate?: HydrationStrategy
|
||||
onError?: (
|
||||
error: Error,
|
||||
retry: () => void,
|
||||
|
@ -54,6 +56,7 @@ export function defineAsyncComponent<
|
|||
loadingComponent,
|
||||
errorComponent,
|
||||
delay = 200,
|
||||
hydrate: hydrateStrategy,
|
||||
timeout, // undefined = never times out
|
||||
suspensible = true,
|
||||
onError: userOnError,
|
||||
|
@ -118,6 +121,24 @@ export function defineAsyncComponent<
|
|||
|
||||
__asyncLoader: load,
|
||||
|
||||
__asyncHydrate(el, instance, hydrate) {
|
||||
const doHydrate = hydrateStrategy
|
||||
? () => {
|
||||
const teardown = hydrateStrategy(hydrate, cb =>
|
||||
forEachElement(el, cb),
|
||||
)
|
||||
if (teardown) {
|
||||
;(instance.bum || (instance.bum = [])).push(teardown)
|
||||
}
|
||||
}
|
||||
: hydrate
|
||||
if (resolvedComp) {
|
||||
doHydrate()
|
||||
} else {
|
||||
load().then(() => !instance.isUnmounted && doHydrate())
|
||||
}
|
||||
},
|
||||
|
||||
get __asyncResolved() {
|
||||
return resolvedComp
|
||||
},
|
||||
|
|
|
@ -35,7 +35,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,
|
||||
|
|
|
@ -200,6 +200,15 @@ export interface ComponentOptionsBase<
|
|||
* @internal
|
||||
*/
|
||||
__asyncResolved?: ConcreteComponent
|
||||
/**
|
||||
* Exposed for lazy hydration
|
||||
* @internal
|
||||
*/
|
||||
__asyncHydrate?: (
|
||||
el: Element,
|
||||
instance: ComponentInternalInstance,
|
||||
hydrate: () => void,
|
||||
) => void
|
||||
|
||||
// Type differentiators ------------------------------------------------------
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -60,9 +60,9 @@ export function renderList(
|
|||
let ret: VNodeChild[]
|
||||
const cached = (cache && cache[index!]) as VNode[] | undefined
|
||||
const sourceIsArray = isArray(source)
|
||||
const sourceIsReactiveArray = sourceIsArray && isReactive(source)
|
||||
|
||||
if (sourceIsArray || isString(source)) {
|
||||
const sourceIsReactiveArray = sourceIsArray && isReactive(source)
|
||||
if (sourceIsReactiveArray) {
|
||||
source = shallowReadArray(source)
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ export type RootHydrateFunction = (
|
|||
container: (Element | ShadowRoot) & { _vnode?: VNode },
|
||||
) => void
|
||||
|
||||
enum DOMNodeTypes {
|
||||
export enum DOMNodeTypes {
|
||||
ELEMENT = 1,
|
||||
TEXT = 3,
|
||||
COMMENT = 8,
|
||||
|
@ -75,7 +75,7 @@ const getContainerType = (container: Element): 'svg' | 'mathml' | undefined => {
|
|||
return undefined
|
||||
}
|
||||
|
||||
const isComment = (node: Node): node is Comment =>
|
||||
export const isComment = (node: Node): node is Comment =>
|
||||
node.nodeType === DOMNodeTypes.COMMENT
|
||||
|
||||
// Note: hydration is DOM-specific
|
||||
|
@ -405,18 +405,20 @@ export function createHydrationFunctions(
|
|||
)
|
||||
let hasWarned = false
|
||||
while (next) {
|
||||
if (
|
||||
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
||||
!hasWarned
|
||||
) {
|
||||
warn(
|
||||
`Hydration children mismatch on`,
|
||||
el,
|
||||
`\nServer rendered element contains more child nodes than client vdom.`,
|
||||
)
|
||||
hasWarned = true
|
||||
if (!isMismatchAllowed(el, MismatchTypes.CHILDREN)) {
|
||||
if (
|
||||
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
||||
!hasWarned
|
||||
) {
|
||||
warn(
|
||||
`Hydration children mismatch on`,
|
||||
el,
|
||||
`\nServer rendered element contains more child nodes than client vdom.`,
|
||||
)
|
||||
hasWarned = true
|
||||
}
|
||||
logMismatchError()
|
||||
}
|
||||
logMismatchError()
|
||||
|
||||
// The SSRed DOM contains more nodes than it should. Remove them.
|
||||
const cur = next
|
||||
|
@ -425,14 +427,16 @@ export function createHydrationFunctions(
|
|||
}
|
||||
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||
if (el.textContent !== vnode.children) {
|
||||
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
||||
warn(
|
||||
`Hydration text content mismatch on`,
|
||||
el,
|
||||
`\n - rendered on server: ${el.textContent}` +
|
||||
`\n - expected on client: ${vnode.children as string}`,
|
||||
)
|
||||
logMismatchError()
|
||||
if (!isMismatchAllowed(el, MismatchTypes.TEXT)) {
|
||||
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
||||
warn(
|
||||
`Hydration text content mismatch on`,
|
||||
el,
|
||||
`\n - rendered on server: ${el.textContent}` +
|
||||
`\n - expected on client: ${vnode.children as string}`,
|
||||
)
|
||||
logMismatchError()
|
||||
}
|
||||
|
||||
el.textContent = vnode.children as string
|
||||
}
|
||||
|
@ -562,18 +566,20 @@ export function createHydrationFunctions(
|
|||
// because server rendered HTML won't contain a text node
|
||||
insert((vnode.el = createText('')), container)
|
||||
} else {
|
||||
if (
|
||||
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
||||
!hasWarned
|
||||
) {
|
||||
warn(
|
||||
`Hydration children mismatch on`,
|
||||
container,
|
||||
`\nServer rendered element contains fewer child nodes than client vdom.`,
|
||||
)
|
||||
hasWarned = true
|
||||
if (!isMismatchAllowed(container, MismatchTypes.CHILDREN)) {
|
||||
if (
|
||||
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
||||
!hasWarned
|
||||
) {
|
||||
warn(
|
||||
`Hydration children mismatch on`,
|
||||
container,
|
||||
`\nServer rendered element contains fewer child nodes than client vdom.`,
|
||||
)
|
||||
hasWarned = true
|
||||
}
|
||||
logMismatchError()
|
||||
}
|
||||
logMismatchError()
|
||||
|
||||
// the SSRed DOM didn't contain enough nodes. Mount the missing ones.
|
||||
patch(
|
||||
|
@ -637,19 +643,21 @@ export function createHydrationFunctions(
|
|||
slotScopeIds: string[] | null,
|
||||
isFragment: boolean,
|
||||
): Node | null => {
|
||||
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
||||
warn(
|
||||
`Hydration node mismatch:\n- rendered on server:`,
|
||||
node,
|
||||
node.nodeType === DOMNodeTypes.TEXT
|
||||
? `(text)`
|
||||
: isComment(node) && node.data === '['
|
||||
? `(start of fragment)`
|
||||
: ``,
|
||||
`\n- expected on client:`,
|
||||
vnode.type,
|
||||
)
|
||||
logMismatchError()
|
||||
if (!isMismatchAllowed(node.parentElement!, MismatchTypes.CHILDREN)) {
|
||||
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
||||
warn(
|
||||
`Hydration node mismatch:\n- rendered on server:`,
|
||||
node,
|
||||
node.nodeType === DOMNodeTypes.TEXT
|
||||
? `(text)`
|
||||
: isComment(node) && node.data === '['
|
||||
? `(start of fragment)`
|
||||
: ``,
|
||||
`\n- expected on client:`,
|
||||
vnode.type,
|
||||
)
|
||||
logMismatchError()
|
||||
}
|
||||
|
||||
vnode.el = null
|
||||
|
||||
|
@ -747,7 +755,7 @@ function propHasMismatch(
|
|||
vnode: VNode,
|
||||
instance: ComponentInternalInstance | null,
|
||||
): boolean {
|
||||
let mismatchType: string | undefined
|
||||
let mismatchType: MismatchTypes | undefined
|
||||
let mismatchKey: string | undefined
|
||||
let actual: string | boolean | null | undefined
|
||||
let expected: string | boolean | null | undefined
|
||||
|
@ -757,7 +765,8 @@ function propHasMismatch(
|
|||
actual = el.getAttribute('class')
|
||||
expected = normalizeClass(clientValue)
|
||||
if (!isSetEqual(toClassSet(actual || ''), toClassSet(expected))) {
|
||||
mismatchType = mismatchKey = `class`
|
||||
mismatchType = MismatchTypes.CLASS
|
||||
mismatchKey = `class`
|
||||
}
|
||||
} else if (key === 'style') {
|
||||
// style might be in different order, but that doesn't affect cascade
|
||||
|
@ -782,7 +791,8 @@ function propHasMismatch(
|
|||
}
|
||||
|
||||
if (!isMapEqual(actualMap, expectedMap)) {
|
||||
mismatchType = mismatchKey = 'style'
|
||||
mismatchType = MismatchTypes.STYLE
|
||||
mismatchKey = 'style'
|
||||
}
|
||||
} else if (
|
||||
(el instanceof SVGElement && isKnownSvgAttr(key)) ||
|
||||
|
@ -808,15 +818,15 @@ function propHasMismatch(
|
|||
: false
|
||||
}
|
||||
if (actual !== expected) {
|
||||
mismatchType = `attribute`
|
||||
mismatchType = MismatchTypes.ATTRIBUTE
|
||||
mismatchKey = key
|
||||
}
|
||||
}
|
||||
|
||||
if (mismatchType) {
|
||||
if (mismatchType != null && !isMismatchAllowed(el, mismatchType)) {
|
||||
const format = (v: any) =>
|
||||
v === false ? `(not rendered)` : `${mismatchKey}="${v}"`
|
||||
const preSegment = `Hydration ${mismatchType} mismatch on`
|
||||
const preSegment = `Hydration ${MismatchTypeString[mismatchType]} mismatch on`
|
||||
const postSegment =
|
||||
`\n - rendered on server: ${format(actual)}` +
|
||||
`\n - expected on client: ${format(expected)}` +
|
||||
|
@ -898,3 +908,48 @@ function resolveCssVars(
|
|||
resolveCssVars(instance.parent, instance.vnode, expectedMap)
|
||||
}
|
||||
}
|
||||
|
||||
const allowMismatchAttr = 'data-allow-mismatch'
|
||||
|
||||
enum MismatchTypes {
|
||||
TEXT = 0,
|
||||
CHILDREN = 1,
|
||||
CLASS = 2,
|
||||
STYLE = 3,
|
||||
ATTRIBUTE = 4,
|
||||
}
|
||||
|
||||
const MismatchTypeString: Record<MismatchTypes, string> = {
|
||||
[MismatchTypes.TEXT]: 'text',
|
||||
[MismatchTypes.CHILDREN]: 'children',
|
||||
[MismatchTypes.CLASS]: 'class',
|
||||
[MismatchTypes.STYLE]: 'style',
|
||||
[MismatchTypes.ATTRIBUTE]: 'attribute',
|
||||
} as const
|
||||
|
||||
function isMismatchAllowed(
|
||||
el: Element | null,
|
||||
allowedType: MismatchTypes,
|
||||
): boolean {
|
||||
if (
|
||||
allowedType === MismatchTypes.TEXT ||
|
||||
allowedType === MismatchTypes.CHILDREN
|
||||
) {
|
||||
while (el && !el.hasAttribute(allowMismatchAttr)) {
|
||||
el = el.parentElement
|
||||
}
|
||||
}
|
||||
const allowedAttr = el && el.getAttribute(allowMismatchAttr)
|
||||
if (allowedAttr == null) {
|
||||
return false
|
||||
} else if (allowedAttr === '') {
|
||||
return true
|
||||
} else {
|
||||
const list = allowedAttr.split(',')
|
||||
// text is a subset of children
|
||||
if (allowedType === MismatchTypes.TEXT && list.includes('children')) {
|
||||
return true
|
||||
}
|
||||
return allowedAttr.split(',').includes(MismatchTypeString[allowedType])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
import { isString } from '@vue/shared'
|
||||
import { DOMNodeTypes, isComment } from './hydration'
|
||||
|
||||
/**
|
||||
* A lazy hydration strategy for async components.
|
||||
* @param hydrate - call this to perform the actual hydration.
|
||||
* @param forEachElement - iterate through the root elements of the component's
|
||||
* non-hydrated DOM, accounting for possible fragments.
|
||||
* @returns a teardown function to be called if the async component is unmounted
|
||||
* before it is hydrated. This can be used to e.g. remove DOM event
|
||||
* listeners.
|
||||
*/
|
||||
export type HydrationStrategy = (
|
||||
hydrate: () => void,
|
||||
forEachElement: (cb: (el: Element) => any) => void,
|
||||
) => (() => void) | void
|
||||
|
||||
export type HydrationStrategyFactory<Options = any> = (
|
||||
options?: Options,
|
||||
) => HydrationStrategy
|
||||
|
||||
export const hydrateOnIdle: HydrationStrategyFactory = () => hydrate => {
|
||||
const id = requestIdleCallback(hydrate)
|
||||
return () => cancelIdleCallback(id)
|
||||
}
|
||||
|
||||
export const hydrateOnVisible: HydrationStrategyFactory<string | number> =
|
||||
(margin = 0) =>
|
||||
(hydrate, forEach) => {
|
||||
const ob = new IntersectionObserver(
|
||||
entries => {
|
||||
for (const e of entries) {
|
||||
if (!e.isIntersecting) continue
|
||||
ob.disconnect()
|
||||
hydrate()
|
||||
break
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: isString(margin) ? margin : margin + 'px',
|
||||
},
|
||||
)
|
||||
forEach(el => ob.observe(el))
|
||||
return () => ob.disconnect()
|
||||
}
|
||||
|
||||
export const hydrateOnMediaQuery: HydrationStrategyFactory<string> =
|
||||
query => hydrate => {
|
||||
if (query) {
|
||||
const mql = matchMedia(query)
|
||||
if (mql.matches) {
|
||||
hydrate()
|
||||
} else {
|
||||
mql.addEventListener('change', hydrate, { once: true })
|
||||
return () => mql.removeEventListener('change', hydrate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const hydrateOnInteraction: HydrationStrategyFactory<
|
||||
string | string[]
|
||||
> =
|
||||
(interactions = []) =>
|
||||
(hydrate, forEach) => {
|
||||
if (isString(interactions)) interactions = [interactions]
|
||||
let hasHydrated = false
|
||||
const doHydrate = (e: Event) => {
|
||||
if (!hasHydrated) {
|
||||
hasHydrated = true
|
||||
teardown()
|
||||
hydrate()
|
||||
// replay event
|
||||
e.target!.dispatchEvent(new (e.constructor as any)(e.type, e))
|
||||
}
|
||||
}
|
||||
const teardown = () => {
|
||||
forEach(el => {
|
||||
for (const i of interactions) {
|
||||
el.removeEventListener(i, doHydrate)
|
||||
}
|
||||
})
|
||||
}
|
||||
forEach(el => {
|
||||
for (const i of interactions) {
|
||||
el.addEventListener(i, doHydrate, { once: true })
|
||||
}
|
||||
})
|
||||
return teardown
|
||||
}
|
||||
|
||||
export function forEachElement(node: Node, cb: (el: Element) => void) {
|
||||
// fragment
|
||||
if (isComment(node) && node.data === '[') {
|
||||
let depth = 1
|
||||
let next = node.nextSibling
|
||||
while (next) {
|
||||
if (next.nodeType === DOMNodeTypes.ELEMENT) {
|
||||
cb(next as Element)
|
||||
} else if (isComment(next)) {
|
||||
if (next.data === ']') {
|
||||
if (--depth === 0) break
|
||||
} else if (next.data === '[') {
|
||||
depth++
|
||||
}
|
||||
}
|
||||
next = next.nextSibling
|
||||
}
|
||||
} else {
|
||||
cb(node as Element)
|
||||
}
|
||||
}
|
|
@ -65,6 +65,12 @@ export { useAttrs, useSlots } from './apiSetupHelpers'
|
|||
export { useModel } from './helpers/useModel'
|
||||
export { useTemplateRef } from './helpers/useTemplateRef'
|
||||
export { useId } from './helpers/useId'
|
||||
export {
|
||||
hydrateOnIdle,
|
||||
hydrateOnVisible,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnInteraction,
|
||||
} from './hydrationStrategies'
|
||||
|
||||
// <script setup> API ----------------------------------------------------------
|
||||
|
||||
|
@ -329,6 +335,10 @@ export type {
|
|||
AsyncComponentOptions,
|
||||
AsyncComponentLoader,
|
||||
} from './apiAsyncComponent'
|
||||
export type {
|
||||
HydrationStrategy,
|
||||
HydrationStrategyFactory,
|
||||
} from './hydrationStrategies'
|
||||
export type { HMRRuntime } from './hmr'
|
||||
|
||||
// Internal API ----------------------------------------------------------------
|
||||
|
|
|
@ -1338,16 +1338,11 @@ function baseCreateRenderer(
|
|||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isAsyncWrapperVNode &&
|
||||
!(type as ComponentOptions).__asyncResolved
|
||||
) {
|
||||
;(type as ComponentOptions).__asyncLoader!().then(
|
||||
// note: we are moving the render call into an async callback,
|
||||
// which means it won't track dependencies - but it's ok because
|
||||
// a server-rendered async wrapper is already in resolved state
|
||||
// and it will never need to change.
|
||||
() => !instance.isUnmounted && hydrateSubTree(),
|
||||
if (isAsyncWrapperVNode) {
|
||||
;(type as ComponentOptions).__asyncHydrate!(
|
||||
el as Element,
|
||||
instance,
|
||||
hydrateSubTree,
|
||||
)
|
||||
} else {
|
||||
hydrateSubTree()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/runtime-dom",
|
||||
"version": "3.5.0-alpha.4",
|
||||
"version": "3.5.0-alpha.5",
|
||||
"description": "@vue/runtime-dom",
|
||||
"main": "index.js",
|
||||
"module": "dist/runtime-dom.esm-bundler.js",
|
||||
|
|
|
@ -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-->`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/server-renderer",
|
||||
"version": "3.5.0-alpha.4",
|
||||
"version": "3.5.0-alpha.5",
|
||||
"description": "@vue/server-renderer",
|
||||
"main": "index.js",
|
||||
"module": "dist/server-renderer.esm-bundler.js",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"serve": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vitejs/plugin-vue": "^5.1.1",
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
@ -82,7 +82,7 @@ function toggleDark() {
|
|||
pkg="vue"
|
||||
label="Vue Version"
|
||||
>
|
||||
<li>
|
||||
<li :class="{ active: vueVersion === `@${currentCommit}` }">
|
||||
<a @click="resetVueVersion">This Commit ({{ currentCommit }})</a>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/shared",
|
||||
"version": "3.5.0-alpha.4",
|
||||
"version": "3.5.0-alpha.5",
|
||||
"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.0-alpha.4",
|
||||
"version": "3.5.0-alpha.5",
|
||||
"description": "Vue 3 compatibility build for Vue 2",
|
||||
"main": "index.js",
|
||||
"module": "dist/vue.runtime.esm-bundler.js",
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -30,12 +30,19 @@ export async function expectByPolling(
|
|||
}
|
||||
}
|
||||
|
||||
export function setupPuppeteer() {
|
||||
export function setupPuppeteer(args?: string[]) {
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
|
||||
const resolvedOptions = args
|
||||
? {
|
||||
...puppeteerOptions,
|
||||
args: [...puppeteerOptions.args!, ...args],
|
||||
}
|
||||
: puppeteerOptions
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await puppeteer.launch(puppeteerOptions)
|
||||
browser = await puppeteer.launch(resolvedOptions)
|
||||
}, 20000)
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<script src="../../dist/vue.global.js"></script>
|
||||
|
||||
<div><span id="custom-trigger">click here to hydrate</span></div>
|
||||
<div id="app"><button>0</button></div>
|
||||
|
||||
<script>
|
||||
window.isHydrated = false
|
||||
const { createSSRApp, defineAsyncComponent, h, ref, onMounted } = Vue
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
const count = ref(0)
|
||||
onMounted(() => {
|
||||
console.log('hydrated')
|
||||
window.isHydrated = true
|
||||
})
|
||||
return () => {
|
||||
return h('button', { onClick: () => count.value++ }, count.value)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const AsyncComp = defineAsyncComponent({
|
||||
loader: () => Promise.resolve(Comp),
|
||||
hydrate: (hydrate, el) => {
|
||||
const triggerEl = document.getElementById('custom-trigger')
|
||||
triggerEl.addEventListener('click', hydrate, { once: true })
|
||||
return () => {
|
||||
window.teardownCalled = true
|
||||
triggerEl.removeEventListener('click', hydrate)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const show = window.show = ref(true)
|
||||
createSSRApp({
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
window.isRootMounted = true
|
||||
})
|
||||
return () => show.value ? h(AsyncComp) : 'off'
|
||||
}
|
||||
}).mount('#app')
|
||||
</script>
|
|
@ -0,0 +1,36 @@
|
|||
<script src="../../dist/vue.global.js"></script>
|
||||
|
||||
<div id="app"><button>0</button></div>
|
||||
|
||||
<script>
|
||||
window.isHydrated = false
|
||||
const { createSSRApp, defineAsyncComponent, h, ref, onMounted, hydrateOnIdle } = Vue
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
const count = ref(0)
|
||||
onMounted(() => {
|
||||
console.log('hydrated')
|
||||
window.isHydrated = true
|
||||
})
|
||||
return () => h('button', { onClick: () => count.value++ }, count.value)
|
||||
},
|
||||
}
|
||||
|
||||
const AsyncComp = defineAsyncComponent({
|
||||
loader: () => new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
console.log('resolve')
|
||||
resolve(Comp)
|
||||
requestIdleCallback(() => {
|
||||
console.log('busy')
|
||||
})
|
||||
}, 10)
|
||||
}),
|
||||
hydrate: hydrateOnIdle()
|
||||
})
|
||||
|
||||
createSSRApp({
|
||||
render: () => h(AsyncComp)
|
||||
}).mount('#app')
|
||||
</script>
|
|
@ -0,0 +1,48 @@
|
|||
<script src="../../dist/vue.global.js"></script>
|
||||
|
||||
<div>click to hydrate</div>
|
||||
<div id="app"><button>0</button></div>
|
||||
<style>body { margin: 0 }</style>
|
||||
|
||||
<script>
|
||||
const isFragment = location.search.includes('?fragment')
|
||||
if (isFragment) {
|
||||
document.getElementById('app').innerHTML =
|
||||
`<!--[--><!--[--><span>one</span><!--]--><button>0</button><span>two</span><!--]-->`
|
||||
}
|
||||
|
||||
window.isHydrated = false
|
||||
const { createSSRApp, defineAsyncComponent, h, ref, onMounted, hydrateOnInteraction } = Vue
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
const count = ref(0)
|
||||
onMounted(() => {
|
||||
console.log('hydrated')
|
||||
window.isHydrated = true
|
||||
})
|
||||
return () => {
|
||||
const button = h('button', { onClick: () => count.value++ }, count.value)
|
||||
if (isFragment) {
|
||||
return [[h('span', 'one')], button, h('span', 'two')]
|
||||
} else {
|
||||
return button
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const AsyncComp = defineAsyncComponent({
|
||||
loader: () => Promise.resolve(Comp),
|
||||
hydrate: hydrateOnInteraction(['click', 'wheel'])
|
||||
})
|
||||
|
||||
createSSRApp({
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
window.isRootMounted = true
|
||||
})
|
||||
return () => h(AsyncComp)
|
||||
}
|
||||
}).mount('#app')
|
||||
</script>
|
|
@ -0,0 +1,36 @@
|
|||
<script src="../../dist/vue.global.js"></script>
|
||||
|
||||
<div>resize the window width to < 500px to hydrate</div>
|
||||
<div id="app"><button>0</button></div>
|
||||
|
||||
<script>
|
||||
window.isHydrated = false
|
||||
const { createSSRApp, defineAsyncComponent, h, ref, onMounted, hydrateOnMediaQuery } = Vue
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
const count = ref(0)
|
||||
onMounted(() => {
|
||||
console.log('hydrated')
|
||||
window.isHydrated = true
|
||||
})
|
||||
return () => {
|
||||
return h('button', { onClick: () => count.value++ }, count.value)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const AsyncComp = defineAsyncComponent({
|
||||
loader: () => Promise.resolve(Comp),
|
||||
hydrate: hydrateOnMediaQuery('(max-width:500px)')
|
||||
})
|
||||
|
||||
createSSRApp({
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
window.isRootMounted = true
|
||||
})
|
||||
return () => h(AsyncComp)
|
||||
}
|
||||
}).mount('#app')
|
||||
</script>
|
|
@ -0,0 +1,49 @@
|
|||
<script src="../../dist/vue.global.js"></script>
|
||||
|
||||
<div style="height: 1000px">scroll to the bottom to hydrate</div>
|
||||
<div id="app"><button>0</button></div>
|
||||
<style>body { margin: 0 }</style>
|
||||
|
||||
<script>
|
||||
const rootMargin = location.search.match(/rootMargin=(\d+)/)?.[1] ?? 0
|
||||
const isFragment = location.search.includes('?fragment')
|
||||
if (isFragment) {
|
||||
document.getElementById('app').innerHTML =
|
||||
`<!--[--><!--[--><span>one</span><!--]--><button>0</button><span>two</span><!--]-->`
|
||||
}
|
||||
|
||||
window.isHydrated = false
|
||||
const { createSSRApp, defineAsyncComponent, h, ref, onMounted, hydrateOnVisible } = Vue
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
const count = ref(0)
|
||||
onMounted(() => {
|
||||
console.log('hydrated')
|
||||
window.isHydrated = true
|
||||
})
|
||||
return () => {
|
||||
const button = h('button', { onClick: () => count.value++ }, count.value)
|
||||
if (isFragment) {
|
||||
return [[h('span', 'one')], button, h('span', 'two')]
|
||||
} else {
|
||||
return button
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const AsyncComp = defineAsyncComponent({
|
||||
loader: () => Promise.resolve(Comp),
|
||||
hydrate: hydrateOnVisible(rootMargin + 'px')
|
||||
})
|
||||
|
||||
createSSRApp({
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
window.isRootMounted = true
|
||||
})
|
||||
return () => h(AsyncComp)
|
||||
}
|
||||
}).mount('#app')
|
||||
</script>
|
|
@ -0,0 +1,118 @@
|
|||
import path from 'node:path'
|
||||
import { setupPuppeteer } from './e2eUtils'
|
||||
import type { Ref } from '../../src/runtime'
|
||||
|
||||
declare const window: Window & {
|
||||
isHydrated: boolean
|
||||
isRootMounted: boolean
|
||||
teardownCalled?: boolean
|
||||
show: Ref<boolean>
|
||||
}
|
||||
|
||||
describe('async component hydration strategies', () => {
|
||||
const { page, click, text, count } = setupPuppeteer(['--window-size=800,600'])
|
||||
|
||||
async function goToCase(name: string, query = '') {
|
||||
const file = `file://${path.resolve(__dirname, `./hydration-strat-${name}.html${query}`)}`
|
||||
await page().goto(file)
|
||||
}
|
||||
|
||||
async function assertHydrationSuccess(n = '1') {
|
||||
await click('button')
|
||||
expect(await text('button')).toBe(n)
|
||||
}
|
||||
|
||||
test('idle', async () => {
|
||||
const messages: string[] = []
|
||||
page().on('console', e => messages.push(e.text()))
|
||||
|
||||
await goToCase('idle')
|
||||
// not hydrated yet
|
||||
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
|
||||
// wait for hydration
|
||||
await page().waitForFunction(() => window.isHydrated)
|
||||
// assert message order: hyration should happen after already queued main thread work
|
||||
expect(messages.slice(1)).toMatchObject(['resolve', 'busy', 'hydrated'])
|
||||
await assertHydrationSuccess()
|
||||
})
|
||||
|
||||
test('visible', async () => {
|
||||
await goToCase('visible')
|
||||
await page().waitForFunction(() => window.isRootMounted)
|
||||
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
|
||||
// scroll down
|
||||
await page().evaluate(() => window.scrollTo({ top: 1000 }))
|
||||
await page().waitForFunction(() => window.isHydrated)
|
||||
await assertHydrationSuccess()
|
||||
})
|
||||
|
||||
test('visible (with rootMargin)', async () => {
|
||||
await goToCase('visible', '?rootMargin=1000')
|
||||
await page().waitForFunction(() => window.isRootMounted)
|
||||
// should hydrate without needing to scroll
|
||||
await page().waitForFunction(() => window.isHydrated)
|
||||
await assertHydrationSuccess()
|
||||
})
|
||||
|
||||
test('visible (fragment)', async () => {
|
||||
await goToCase('visible', '?fragment')
|
||||
await page().waitForFunction(() => window.isRootMounted)
|
||||
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
|
||||
expect(await count('span')).toBe(2)
|
||||
// scroll down
|
||||
await page().evaluate(() => window.scrollTo({ top: 1000 }))
|
||||
await page().waitForFunction(() => window.isHydrated)
|
||||
await assertHydrationSuccess()
|
||||
})
|
||||
|
||||
test('media query', async () => {
|
||||
await goToCase('media')
|
||||
await page().waitForFunction(() => window.isRootMounted)
|
||||
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
|
||||
// resize
|
||||
await page().setViewport({ width: 400, height: 600 })
|
||||
await page().waitForFunction(() => window.isHydrated)
|
||||
await assertHydrationSuccess()
|
||||
})
|
||||
|
||||
test('interaction', async () => {
|
||||
await goToCase('interaction')
|
||||
await page().waitForFunction(() => window.isRootMounted)
|
||||
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
|
||||
await click('button')
|
||||
await page().waitForFunction(() => window.isHydrated)
|
||||
// should replay event
|
||||
expect(await text('button')).toBe('1')
|
||||
await assertHydrationSuccess('2')
|
||||
})
|
||||
|
||||
test('interaction (fragment)', async () => {
|
||||
await goToCase('interaction', '?fragment')
|
||||
await page().waitForFunction(() => window.isRootMounted)
|
||||
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
|
||||
await click('button')
|
||||
await page().waitForFunction(() => window.isHydrated)
|
||||
// should replay event
|
||||
expect(await text('button')).toBe('1')
|
||||
await assertHydrationSuccess('2')
|
||||
})
|
||||
|
||||
test('custom', async () => {
|
||||
await goToCase('custom')
|
||||
await page().waitForFunction(() => window.isRootMounted)
|
||||
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
|
||||
await click('#custom-trigger')
|
||||
await page().waitForFunction(() => window.isHydrated)
|
||||
await assertHydrationSuccess()
|
||||
})
|
||||
|
||||
test('custom teardown', async () => {
|
||||
await goToCase('custom')
|
||||
await page().waitForFunction(() => window.isRootMounted)
|
||||
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
|
||||
await page().evaluate(() => (window.show.value = false))
|
||||
expect(await text('#app')).toBe('off')
|
||||
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
|
||||
expect(await page().evaluate(() => window.teardownCalled)).toBe(true)
|
||||
})
|
||||
})
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "vue",
|
||||
"version": "3.5.0-alpha.4",
|
||||
"version": "3.5.0-alpha.5",
|
||||
"description": "The progressive JavaScript framework for building modern web UI.",
|
||||
"main": "index.js",
|
||||
"module": "dist/vue.runtime.esm-bundler.js",
|
||||
|
|
1255
pnpm-lock.yaml
1255
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue