From dcc68ef7d48973abd8dd3178b46e50e3b0785ea4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 11 Jan 2024 16:18:00 +0800 Subject: [PATCH 1/9] fix(hydration): do not warn against bindings w/ object values --- .../runtime-core/__tests__/hydration.spec.ts | 34 ++++++++++------ packages/runtime-core/src/hydration.ts | 39 ++++++++++++------- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index cd0c61982b8..17d7b8dbcee 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -1080,13 +1080,11 @@ describe('SSR hydration', () => { }) test('force hydrate prop with `.prop` modifier', () => { - const { container } = mountWithHydration( - '', - () => - h('input', { - type: 'checkbox', - '.indeterminate': true, - }), + const { container } = mountWithHydration('', () => + h('input', { + type: 'checkbox', + '.indeterminate': true, + }), ) expect((container.firstChild! as any).indeterminate).toBe(true) }) @@ -1475,6 +1473,16 @@ describe('SSR hydration', () => { mountWithHydration(``, () => h('textarea', { value: 'foo' }), ) @@ -1483,11 +1491,10 @@ describe('SSR hydration', () => { ) expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() - mountWithHydration(`
`, () => h('div', { id: 'foo' })) + mountWithHydration(``, () => + h('textarea', { value: 'bar' }), + ) expect(`Hydration attribute mismatch`).toHaveBeenWarned() - - mountWithHydration(`
`, () => h('div', { id: 'foo' })) - expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2) }) test('boolean attr handling', () => { @@ -1504,5 +1511,10 @@ describe('SSR hydration', () => { ) expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() }) + + test('should not warn against object values', () => { + mountWithHydration(``, () => h('input', { from: {} })) + expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() + }) }) }) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 8f095f6167f..4df4eecece9 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -21,6 +21,7 @@ import { isBooleanAttr, isKnownHtmlAttr, isKnownSvgAttr, + isObject, isOn, isReservedProp, isString, @@ -759,12 +760,17 @@ function propHasMismatch( expected = includeBooleanAttr(clientValue) } else { // #10000 some attrs such as textarea.value can't be get by `hasAttribute` - actual = el.hasAttribute(key) - ? el.getAttribute(key) - : key in el - ? el[key as keyof typeof el] - : '' - expected = clientValue == null ? '' : String(clientValue) + if (el.hasAttribute(key)) { + actual = el.getAttribute(key) + } else if (key in el) { + const serverValue = el[key as keyof typeof el] + if (!isObject(serverValue)) { + actual = serverValue == null ? '' : String(serverValue) + } + } + if (!isObject(clientValue)) { + expected = clientValue == null ? '' : String(clientValue) + } } if (actual !== expected) { mismatchType = `attribute` @@ -775,15 +781,20 @@ function propHasMismatch( if (mismatchType) { const format = (v: any) => v === false ? `(not rendered)` : `${mismatchKey}="${v}"` - warn( - `Hydration ${mismatchType} mismatch on`, - el, + const preSegment = `Hydration ${mismatchType} mismatch on` + const postSegment = `\n - rendered on server: ${format(actual)}` + - `\n - expected on client: ${format(expected)}` + - `\n Note: this mismatch is check-only. The DOM will not be rectified ` + - `in production due to performance overhead.` + - `\n You should fix the source of the mismatch.`, - ) + `\n - expected on client: ${format(expected)}` + + `\n Note: this mismatch is check-only. The DOM will not be rectified ` + + `in production due to performance overhead.` + + `\n You should fix the source of the mismatch.` + if (__TEST__) { + // during tests, log the full message in one single string for easier + // debugging. + warn(`${preSegment} ${el.tagName}${postSegment}`) + } else { + warn(preSegment, el, postSegment) + } return true } return false From cd419aec3cb615eaea8b2324356f38f4c0ff1fcc Mon Sep 17 00:00:00 2001 From: cyx <30902641+Duncanxyz@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:11:35 +0800 Subject: [PATCH 2/9] fix(runtime-dom): unify behavior for v-show + style display binding (#10075) close #10074 --- packages/runtime-dom/src/modules/style.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/runtime-dom/src/modules/style.ts b/packages/runtime-dom/src/modules/style.ts index ce112ab87ed..6341c8a120e 100644 --- a/packages/runtime-dom/src/modules/style.ts +++ b/packages/runtime-dom/src/modules/style.ts @@ -7,6 +7,7 @@ type Style = string | Record | null export function patchStyle(el: Element, prev: Style, next: Style) { const style = (el as HTMLElement).style + const currentDisplay = style.display const isCssString = isString(next) if (next && !isCssString) { if (prev && !isString(prev)) { @@ -20,7 +21,6 @@ export function patchStyle(el: Element, prev: Style, next: Style) { setStyle(style, key, next[key]) } } else { - const currentDisplay = style.display if (isCssString) { if (prev !== next) { // #9821 @@ -33,12 +33,12 @@ export function patchStyle(el: Element, prev: Style, next: Style) { } else if (prev) { el.removeAttribute('style') } - // indicates that the `display` of the element is controlled by `v-show`, - // so we always keep the current `display` value regardless of the `style` - // value, thus handing over control to `v-show`. - if (vShowOldKey in el) { - style.display = currentDisplay - } + } + // indicates that the `display` of the element is controlled by `v-show`, + // so we always keep the current `display` value regardless of the `style` + // value, thus handing over control to `v-show`. + if (vShowOldKey in el) { + style.display = currentDisplay } } From 07b19a53a5f313a219ea87546846b79d89e39e96 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 11 Jan 2024 17:14:49 +0800 Subject: [PATCH 3/9] test: test case for style binding w/ object value + v-show ref #10074 --- .../__tests__/directives/vShow.spec.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/runtime-dom/__tests__/directives/vShow.spec.ts b/packages/runtime-dom/__tests__/directives/vShow.spec.ts index 5e837650854..70b69f2df1c 100644 --- a/packages/runtime-dom/__tests__/directives/vShow.spec.ts +++ b/packages/runtime-dom/__tests__/directives/vShow.spec.ts @@ -151,6 +151,32 @@ describe('runtime-dom: v-show directive', () => { expect($div.style.display).toEqual('') }) + test('the value of `display` set by v-show should not be overwritten by the style attribute when updated (object value)', async () => { + const style = ref({ + display: 'block', + width: '100px', + }) + const display = ref(false) + const component = defineComponent({ + render() { + return withVShow(h('div', { style: style.value }), display.value) + }, + }) + render(h(component), root) + + const $div = root.children[0] + + expect($div.style.display).toEqual('none') + + style.value.width = '50px' + await nextTick() + expect($div.style.display).toEqual('none') + + display.value = true + await nextTick() + expect($div.style.display).toEqual('block') + }) + // #2583, #2757 test('the value of `display` set by v-show should not be overwritten by the style attribute when updated (with Transition)', async () => { const style = ref('width: 100px') From bcda96b525801eb7a1d397300fb3f2f9b827ddfb Mon Sep 17 00:00:00 2001 From: edison Date: Thu, 11 Jan 2024 17:27:53 +0800 Subject: [PATCH 4/9] fix(suspense): avoid double-patching nested suspense when parent suspense is not resolved (#10055) close #8678 --- .../__tests__/components/Suspense.spec.ts | 135 ++++++++++++++++++ .../runtime-core/src/components/Suspense.ts | 12 ++ 2 files changed, 147 insertions(+) diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 7c3f8ff8b3d..928da872faf 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -1641,6 +1641,141 @@ describe('Suspense', () => { expect(serializeInner(root)).toBe(expected) }) + //#8678 + test('nested suspense (child suspense update before parent suspense resolve)', async () => { + const calls: string[] = [] + + const InnerA = defineAsyncComponent( + { + setup: () => { + calls.push('innerA created') + onMounted(() => { + calls.push('innerA mounted') + }) + return () => h('div', 'innerA') + }, + }, + 10, + ) + + const InnerB = defineAsyncComponent( + { + setup: () => { + calls.push('innerB created') + onMounted(() => { + calls.push('innerB mounted') + }) + return () => h('div', 'innerB') + }, + }, + 10, + ) + + const OuterA = defineAsyncComponent( + { + setup: (_, { slots }: any) => { + calls.push('outerA created') + onMounted(() => { + calls.push('outerA mounted') + }) + return () => + h(Fragment, null, [h('div', 'outerA'), slots.default?.()]) + }, + }, + 5, + ) + + const OuterB = defineAsyncComponent( + { + setup: (_, { slots }: any) => { + calls.push('outerB created') + onMounted(() => { + calls.push('outerB mounted') + }) + return () => + h(Fragment, null, [h('div', 'outerB'), slots.default?.()]) + }, + }, + 5, + ) + + const outerToggle = ref(false) + const innerToggle = ref(false) + + /** + * + * + * + * + * + * + * + */ + const Comp = { + setup() { + return () => + h(Suspense, null, { + default: [ + h(outerToggle.value ? OuterB : OuterA, null, { + default: () => + h(Suspense, null, { + default: h(innerToggle.value ? InnerB : InnerA), + }), + }), + ], + fallback: h('div', 'fallback outer'), + }) + }, + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(`
fallback outer
`) + + // mount outer component + await Promise.all(deps) + await nextTick() + + expect(serializeInner(root)).toBe(`
outerA
`) + expect(calls).toEqual([`outerA created`, `outerA mounted`]) + + // mount inner component + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
outerA
innerA
`) + + expect(calls).toEqual([ + 'outerA created', + 'outerA mounted', + 'innerA created', + 'innerA mounted', + ]) + + calls.length = 0 + deps.length = 0 + + // toggle both outer and inner components + outerToggle.value = true + innerToggle.value = true + await nextTick() + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
outerB
`) + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
outerB
innerB
`) + + // innerB only mount once + expect(calls).toEqual([ + 'outerB created', + 'outerB mounted', + 'innerB created', + 'innerB mounted', + ]) + }) + // #6416 test('KeepAlive with Suspense', async () => { const Async = defineAsyncComponent({ diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index ac023ae60cd..9b3d6765d9c 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -91,6 +91,18 @@ export const SuspenseImpl = { rendererInternals, ) } else { + // #8678 if the current suspense needs to be patched and parentSuspense has + // not been resolved. this means that both the current suspense and parentSuspense + // need to be patched. because parentSuspense's pendingBranch includes the + // current suspense, it will be processed twice: + // 1. current patch + // 2. mounting along with the pendingBranch of parentSuspense + // it is necessary to skip the current patch to avoid multiple mounts + // of inner components. + if (parentSuspense && parentSuspense.deps > 0) { + n2.suspense = n1.suspense + return + } patchSuspense( n1, n2, From 92514db70821fce5aaa7dd903bfcb19252b26c4c Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 11 Jan 2024 18:04:32 +0800 Subject: [PATCH 5/9] build: add node production/development condition in package exports (#10036) follow up of #9977 --- packages/compiler-core/package.json | 14 ++++++++++++++ packages/compiler-dom/package.json | 14 ++++++++++++++ packages/compiler-sfc/package.json | 10 ++++++++++ packages/reactivity/package.json | 14 ++++++++++++++ packages/runtime-core/package.json | 14 ++++++++++++++ packages/runtime-dom/package.json | 14 ++++++++++++++ packages/server-renderer/package.json | 14 ++++++++++++++ packages/shared/package.json | 14 ++++++++++++++ packages/vue-compat/package.json | 14 ++++++++++++++ packages/vue/package.json | 5 +++++ 10 files changed, 127 insertions(+) diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index a5ff635918f..d5ef8b4d300 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -9,6 +9,20 @@ "index.js", "dist" ], + "exports": { + ".": { + "types": "./dist/compiler-core.d.ts", + "node": { + "production": "./dist/compiler-core.cjs.prod.js", + "development": "./dist/compiler-core.cjs.js", + "default": "./index.js" + }, + "module": "./dist/compiler-core.esm-bundler.js", + "import": "./dist/compiler-core.esm-bundler.js", + "require": "./index.js" + }, + "./*": "./*" + }, "buildOptions": { "name": "VueCompilerCore", "compat": true, diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json index 2e7206e14e1..bc7b9a8c65b 100644 --- a/packages/compiler-dom/package.json +++ b/packages/compiler-dom/package.json @@ -11,6 +11,20 @@ "index.js", "dist" ], + "exports": { + ".": { + "types": "./dist/compiler-dom.d.ts", + "node": { + "production": "./dist/compiler-dom.cjs.prod.js", + "development": "./dist/compiler-dom.cjs.js", + "default": "./index.js" + }, + "module": "./dist/compiler-dom.esm-bundler.js", + "import": "./dist/compiler-dom.esm-bundler.js", + "require": "./index.js" + }, + "./*": "./*" + }, "sideEffects": false, "buildOptions": { "name": "VueCompilerDOM", diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 21eda5a62cf..1c02868772e 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -8,6 +8,16 @@ "files": [ "dist" ], + "exports": { + ".": { + "types": "./dist/compiler-sfc.d.ts", + "node": "./dist/compiler-sfc.cjs.js", + "module": "./dist/compiler-sfc.esm-browser.js", + "import": "./dist/compiler-sfc.esm-browser.js", + "require": "./dist/compiler-sfc.cjs.js" + }, + "./*": "./*" + }, "buildOptions": { "name": "VueCompilerSFC", "formats": [ diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index 2f53964baf7..ed7928c1213 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -11,6 +11,20 @@ "index.js", "dist" ], + "exports": { + ".": { + "types": "./dist/reactivity.d.ts", + "node": { + "production": "./dist/reactivity.cjs.prod.js", + "development": "./dist/reactivity.cjs.js", + "default": "./index.js" + }, + "module": "./dist/reactivity.esm-bundler.js", + "import": "./dist/reactivity.esm-bundler.js", + "require": "./index.js" + }, + "./*": "./*" + }, "sideEffects": false, "repository": { "type": "git", diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index 588adc20448..28a72a46e3f 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -9,6 +9,20 @@ "index.js", "dist" ], + "exports": { + ".": { + "types": "./dist/runtime-core.d.ts", + "node": { + "production": "./dist/runtime-core.cjs.prod.js", + "development": "./dist/runtime-core.cjs.js", + "default": "./index.js" + }, + "module": "./dist/runtime-core.esm-bundler.js", + "import": "./dist/runtime-core.esm-bundler.js", + "require": "./index.js" + }, + "./*": "./*" + }, "buildOptions": { "name": "VueRuntimeCore", "formats": [ diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json index f786057f38e..3c2d8e2e3ee 100644 --- a/packages/runtime-dom/package.json +++ b/packages/runtime-dom/package.json @@ -10,6 +10,20 @@ "index.js", "dist" ], + "exports": { + ".": { + "types": "./dist/runtime-dom.d.ts", + "node": { + "production": "./dist/runtime-dom.cjs.prod.js", + "development": "./dist/runtime-dom.cjs.js", + "default": "./index.js" + }, + "module": "./dist/runtime-dom.esm-bundler.js", + "import": "./dist/runtime-dom.esm-bundler.js", + "require": "./index.js" + }, + "./*": "./*" + }, "sideEffects": false, "buildOptions": { "name": "VueRuntimeDOM", diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index 48e2458a553..3ff0817c694 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -9,6 +9,20 @@ "index.js", "dist" ], + "exports": { + ".": { + "types": "./dist/server-renderer.d.ts", + "node": { + "production": "./dist/server-renderer.cjs.prod.js", + "development": "./dist/server-renderer.cjs.js", + "default": "./index.js" + }, + "module": "./dist/server-renderer.esm-bundler.js", + "import": "./dist/server-renderer.esm-bundler.js", + "require": "./index.js" + }, + "./*": "./*" + }, "buildOptions": { "name": "VueServerRenderer", "formats": [ diff --git a/packages/shared/package.json b/packages/shared/package.json index 0e19df2c95d..3e21d4a3b99 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -9,6 +9,20 @@ "index.js", "dist" ], + "exports": { + ".": { + "types": "./dist/shared.d.ts", + "node": { + "production": "./dist/shared.cjs.prod.js", + "development": "./dist/shared.cjs.js", + "default": "./index.js" + }, + "module": "./dist/shared.esm-bundler.js", + "import": "./dist/shared.esm-bundler.js", + "require": "./index.js" + }, + "./*": "./*" + }, "sideEffects": false, "buildOptions": { "formats": [ diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json index 4aa8e9d73ca..46d0bd9600e 100644 --- a/packages/vue-compat/package.json +++ b/packages/vue-compat/package.json @@ -10,6 +10,20 @@ "index.js", "dist" ], + "exports": { + ".": { + "types": "./dist/vue.d.ts", + "node": { + "production": "./dist/vue.cjs.prod.js", + "development": "./dist/vue.cjs.js", + "default": "./index.js" + }, + "module": "./dist/vue.esm-bundler.js", + "import": "./dist/vue.esm-bundler.js", + "require": "./index.js" + }, + "./*": "./*" + }, "buildOptions": { "name": "Vue", "filename": "vue", diff --git a/packages/vue/package.json b/packages/vue/package.json index 597c4acac95..a686179d48d 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -25,6 +25,11 @@ }, "require": { "types": "./dist/vue.d.ts", + "node": { + "production": "./dist/vue.cjs.prod.js", + "development": "./dist/vue.cjs.js", + "default": "./index.js" + }, "default": "./index.js" } }, From 8fda856a82507eb9f62338dbbf9fdda3672a52a4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 11 Jan 2024 17:57:47 +0800 Subject: [PATCH 6/9] refactor: split out useModel implementation and tests --- packages/dts-test/setupHelpers.test-d.ts | 4 +- .../__tests__/apiSetupHelpers.spec.ts | 521 ----------------- .../__tests__/helpers/useModel.spec.ts | 529 ++++++++++++++++++ packages/runtime-core/src/apiSetupHelpers.ts | 111 +--- packages/runtime-core/src/helpers/useModel.ts | 88 +++ packages/runtime-core/src/index.ts | 2 +- 6 files changed, 632 insertions(+), 623 deletions(-) create mode 100644 packages/runtime-core/__tests__/helpers/useModel.spec.ts create mode 100644 packages/runtime-core/src/helpers/useModel.ts diff --git a/packages/dts-test/setupHelpers.test-d.ts b/packages/dts-test/setupHelpers.test-d.ts index 9588cb9b209..c749e80a5c7 100644 --- a/packages/dts-test/setupHelpers.test-d.ts +++ b/packages/dts-test/setupHelpers.test-d.ts @@ -2,18 +2,18 @@ import { type Ref, type Slots, type VNode, + defineComponent, defineEmits, defineModel, defineProps, defineSlots, toRefs, useAttrs, + useModel, useSlots, withDefaults, } from 'vue' import { describe, expectType } from './utils' -import { defineComponent } from 'vue' -import { useModel } from 'vue' describe('defineProps w/ type declaration', () => { // type declaration diff --git a/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts b/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts index 04df0ae593a..04e9c1c86db 100644 --- a/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts +++ b/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts @@ -1,28 +1,18 @@ import { type ComponentInternalInstance, type ComputedRef, - Fragment, - type Ref, type SetupContext, Suspense, computed, createApp, - createBlock, - createElementBlock, - createElementVNode, - createVNode, defineComponent, getCurrentInstance, h, - nextTick, nodeOps, onMounted, - openBlock, - ref, render, serializeInner, shallowReactive, - watch, } from '@vue/runtime-test' import { createPropsRestProxy, @@ -32,7 +22,6 @@ import { mergeDefaults, mergeModels, useAttrs, - useModel, useSlots, withAsyncContext, withDefaults, @@ -185,516 +174,6 @@ describe('SFC