Repository: vuejs/vue-next
Pull request #4908 (2021-11-06 09:17:26)

fix(hmr): Use resolveTemplateUsageCheckString in vite/plugin-vue.
fix: #4358 with https://github.com/vitejs/vite/pull/5561

ygj6: vitejs/vite#5561 use `resolveTemplateUsageCheckString` to determine whether the reference variable in the template has been changed.

```ts
function needGlobHMR(
  prev: SFCDescriptor | null,
  next: SFCDescriptor | null
): boolean {
  const isDynamicUpdate = resolveTemplateUsageCheckString(prev!) !== resolveTemplateUsageCheckString(next!)
  const prevSetup = prev!.scriptSetup
  const nextSetup = next!.scriptSetup
  const isSetupWithTS = !!(
    prevSetup?.setup &&
    nextSetup?.setup &&
    (prevSetup.lang === 'ts' || prevSetup.lang === 'tsx')
    && (nextSetup.lang === 'ts' || nextSetup.lang === 'tsx')
  )
  return isDynamicUpdate && isSetupWithTS
}
```


Patch:
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index feaca8bd3..4a33f6e9d 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -1960,7 +1960,7 @@ function getObjectOrArrayExpressionKeys(value: Node): string[] {
 
 const templateUsageCheckCache = createCache<string>()
 
-function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
+export function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
   const { content, ast } = sfc.template!
   const cached = templateUsageCheckCache.get(content)
   if (cached) {
diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts
index 1c4bb14ee..fc835b66a 100644
--- a/packages/compiler-sfc/src/index.ts
+++ b/packages/compiler-sfc/src/index.ts
@@ -2,7 +2,7 @@
 export { parse } from './parse'
 export { compileTemplate } from './compileTemplate'
 export { compileStyle, compileStyleAsync } from './compileStyle'
-export { compileScript } from './compileScript'
+export { compileScript, resolveTemplateUsageCheckString } from './compileScript'
 export { rewriteDefault } from './rewriteDefault'
 export {
   shouldTransform as shouldTransformRef,
diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts
index 29367007c..0fa1c4bc2 100644
--- a/packages/reactivity/src/baseHandlers.ts
+++ b/packages/reactivity/src/baseHandlers.ts
@@ -7,7 +7,8 @@ import {
   readonlyMap,
   reactiveMap,
   shallowReactiveMap,
-  shallowReadonlyMap
+  shallowReadonlyMap,
+  isReadonly
 } from './reactive'
 import { TrackOpTypes, TriggerOpTypes } from './operations'
 import {
@@ -146,7 +147,7 @@ function createSetter(shallow = false) {
     receiver: object
   ): boolean {
     let oldValue = (target as any)[key]
-    if (!shallow) {
+    if (!shallow && !isReadonly(value)) {
       value = toRaw(value)
       oldValue = toRaw(oldValue)
       if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts
index 231e15e60..351fc0c26 100644
--- a/packages/reactivity/src/effect.ts
+++ b/packages/reactivity/src/effect.ts
@@ -23,7 +23,7 @@ let effectTrackDepth = 0
 export let trackOpBit = 1
 
 /**
- * The bitwise track markers support at most 30 levels op recursion.
+ * The bitwise track markers support at most 30 levels of recursion.
  * This value is chosen to enable modern JS engines to use a SMI on all platforms.
  * When recursion depth is greater, fall back to using a full cleanup.
  */
diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts
index fb43668e4..93eb4fa89 100644
--- a/packages/reactivity/src/ref.ts
+++ b/packages/reactivity/src/ref.ts
@@ -190,9 +190,7 @@ export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
 }
 
 export type ToRefs<T = any> = {
-  // #2687: somehow using ToRef<T[K]> here turns the resulting type into
-  // a union of multiple Ref<*> types instead of a single Ref<* | *> type.
-  [K in keyof T]: T[K] extends Ref ? T[K] : Ref<UnwrapRef<T[K]>>
+  [K in keyof T]: ToRef<T[K]>
 }
 export function toRefs<T extends object>(object: T): ToRefs<T> {
   if (__DEV__ && !isProxy(object)) {
diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts
index fc1866af9..02ca5d53e 100644
--- a/packages/runtime-core/src/apiSetupHelpers.ts
+++ b/packages/runtime-core/src/apiSetupHelpers.ts
@@ -131,6 +131,7 @@ type InferDefaults<T> = {
 }
 
 type InferDefault<P, T> = T extends
+  | null
   | number
   | string
   | boolean
diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts
index 924f40a73..c99949222 100644
--- a/packages/runtime-core/src/componentProps.ts
+++ b/packages/runtime-core/src/componentProps.ts
@@ -39,6 +39,7 @@ import { createPropsDefaultThis } from './compat/props'
 import { isCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig'
 import { DeprecationTypes } from './compat/compatConfig'
 import { shouldSkipAttr } from './compat/attrsFallthrough'
+import { IfAny } from './helpers/typeUtils'
 
 export type ComponentPropsOptions<P = Data> =
   | ComponentObjectPropsOptions<P>
@@ -115,7 +116,7 @@ type InferPropType<T> = [T] extends [null]
     : InferPropType<U>
   : [T] extends [Prop<infer V, infer D>]
   ? unknown extends V
-    ? D
+    ? IfAny<V, V, D>
     : V
   : T
 
diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts
index dd385a815..8298aff42 100644
--- a/packages/runtime-core/src/componentPublicInstance.ts
+++ b/packages/runtime-core/src/componentPublicInstance.ts
@@ -248,11 +248,11 @@ if (__COMPAT__) {
 }
 
 const enum AccessTypes {
+  OTHER,
   SETUP,
   DATA,
   PROPS,
-  CONTEXT,
-  OTHER
+  CONTEXT
 }
 
 export interface ComponentRenderContext {
@@ -437,7 +437,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
   ) {
     let normalizedProps
     return (
-      accessCache![key] !== undefined ||
+      !!accessCache![key] ||
       (data !== EMPTY_OBJ && hasOwn(data, key)) ||
       (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) ||
       ((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts
index cb628bf65..27bfb5f52 100644
--- a/packages/runtime-core/src/components/BaseTransition.ts
+++ b/packages/runtime-core/src/components/BaseTransition.ts
@@ -161,7 +161,11 @@ const BaseTransitionImpl: ComponentOptions = {
       const rawProps = toRaw(props)
       const { mode } = rawProps
       // check mode
-      if (__DEV__ && mode && !['in-out', 'out-in', 'default'].includes(mode)) {
+      if (
+        __DEV__ &&
+        mode &&
+        mode !== 'in-out' && mode !== 'out-in' && mode !== 'default'
+      ) {
         warn(`invalid <transition> mode: ${mode}`)
       }
 
diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts
index 30ad4400d..223a7c602 100644
--- a/packages/runtime-core/src/components/KeepAlive.ts
+++ b/packages/runtime-core/src/components/KeepAlive.ts
@@ -381,7 +381,7 @@ function registerKeepAliveHook(
         }
         current = current.parent
       }
-      hook()
+      return hook()
     })
   injectHook(type, wrappedHook, target)
   // In addition to registering it on the target instance, we walk up the parent
diff --git a/packages/runtime-core/src/helpers/typeUtils.ts b/packages/runtime-core/src/helpers/typeUtils.ts
index 204543e6d..8caba54c6 100644
--- a/packages/runtime-core/src/helpers/typeUtils.ts
+++ b/packages/runtime-core/src/helpers/typeUtils.ts
@@ -6,3 +6,5 @@ export type UnionToIntersection<U> = (
 
 // make keys required but keep undefined values
 export type LooseRequired<T> = { [P in string & keyof T]: T[P] }
+
+export type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N
diff --git a/packages/runtime-dom/src/modules/props.ts b/packages/runtime-dom/src/modules/props.ts
index d70692396..5092911e4 100644
--- a/packages/runtime-dom/src/modules/props.ts
+++ b/packages/runtime-dom/src/modules/props.ts
@@ -26,12 +26,23 @@ export function patchDOMProp(
     return
   }
 
-  if (key === 'value' && el.tagName !== 'PROGRESS') {
+  if (
+    key === 'value' &&
+    el.tagName !== 'PROGRESS' &&
+    // custom elements may use _value internally
+    !el.tagName.includes('-')
+  ) {
     // store value as _value as well since
     // non-string values will be stringified.
     el._value = value
     const newValue = value == null ? '' : value
-    if (el.value !== newValue) {
+    if (
+      el.value !== newValue ||
+      // #4956: always set for OPTION elements because its value falls back to
+      // textContent if no value attribute is present. And setting .value for
+      // OPTION has no side effect
+      el.tagName === 'OPTION'
+    ) {
       el.value = newValue
     }
     if (value == null) {
diff --git a/packages/size-check/vite.config.js b/packages/size-check/vite.config.js
index a14a91c27..b8d714e0f 100644
--- a/packages/size-check/vite.config.js
+++ b/packages/size-check/vite.config.js
@@ -5,6 +5,7 @@ export default {
       output: {
         entryFileNames: `[name].js`
       }
-    }
+    },
+    minify: 'terser'
   }
 }
diff --git a/packages/vue/package.json b/packages/vue/package.json
index c8f914805..bc2b8e7b0 100644
--- a/packages/vue/package.json
+++ b/packages/vue/package.json
@@ -33,7 +33,8 @@
       "require": "./compiler-sfc/index.js"
     },
     "./dist/*": "./dist/*",
-    "./package.json": "./package.json"
+    "./package.json": "./package.json",
+    "./ref-macros.d.ts": "./ref-macros.d.ts"
   },
   "buildOptions": {
     "name": "Vue",


Test patch:
diff --git a/packages/compiler-sfc/__tests__/compileScript.spec.ts b/packages/compiler-sfc/__tests__/compileScript.spec.ts
index 694619174..85b9f9a41 100644
--- a/packages/compiler-sfc/__tests__/compileScript.spec.ts
+++ b/packages/compiler-sfc/__tests__/compileScript.spec.ts
@@ -292,7 +292,7 @@ defineExpose({ foo: 123 })
       let foo = $ref(1)
       </script>
       `,
-        { refSugar: true }
+        { refTransform: true }
       )
       assertCode(content)
       expect(content).toMatch(`import { ref } from 'vue'`)
@@ -1090,7 +1090,7 @@ const emit = defineEmits(['a', 'b'])
   describe('async/await detection', () => {
     function assertAwaitDetection(code: string, shouldAsync = true) {
       const { content } = compile(`<script setup>${code}</script>`, {
-        refSugar: true
+        refTransform: true
       })
       if (shouldAsync) {
         expect(content).toMatch(`let __temp, __restore`)
diff --git a/packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts b/packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts
index 34dc9efea..d2f525890 100644
--- a/packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts
+++ b/packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts
@@ -5,7 +5,7 @@ import { compileSFCScript as compile, assertCode } from './utils'
 // transform can be found in <root>/packages/ref-transform/__tests__
 describe('sfc ref transform', () => {
   function compileWithRefTransform(src: string) {
-    return compile(src, { refSugar: true })
+    return compile(src, { refTransform: true })
   }
 
   test('$ unwrapping', () => {
@@ -156,7 +156,7 @@ describe('sfc ref transform', () => {
           bar
         })
       </script>`,
-          { refSugar: true }
+          { refTransform: true }
         )
       ).toThrow(`cannot reference locally declared variables`)
 
@@ -168,7 +168,7 @@ describe('sfc ref transform', () => {
           bar
         })
       </script>`,
-          { refSugar: true }
+          { refTransform: true }
         )
       ).toThrow(`cannot reference locally declared variables`)
     })
diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts
index 63426bb5e..185bb3107 100644
--- a/packages/reactivity/__tests__/readonly.spec.ts
+++ b/packages/reactivity/__tests__/readonly.spec.ts
@@ -465,4 +465,13 @@ describe('reactivity/readonly', () => {
       'Set operation on key "randomProperty" failed: target is readonly.'
     ).toHaveBeenWarned()
   })
+
+  // #4986
+  test('setting a readonly object as a property of a reactive object should retain readonly proxy', () => {
+    const r = readonly({})
+    const rr = reactive({}) as any
+    rr.foo = r
+    expect(rr.foo).toBe(r)
+    expect(isReadonly(rr.foo)).toBe(true)
+  })
 })
diff --git a/packages/runtime-core/__tests__/componentPublicInstance.spec.ts b/packages/runtime-core/__tests__/componentPublicInstance.spec.ts
index c4c456b5f..0274351f8 100644
--- a/packages/runtime-core/__tests__/componentPublicInstance.spec.ts
+++ b/packages/runtime-core/__tests__/componentPublicInstance.spec.ts
@@ -195,6 +195,11 @@ describe('component: proxy', () => {
     expect('$foobar' in instanceProxy).toBe(false)
     expect('baz' in instanceProxy).toBe(false)
 
+    // #4962 triggering getter should not cause non-existent property to
+    // pass the has check
+    instanceProxy.baz
+    expect('baz' in instanceProxy).toBe(false)
+
     // set non-existent (goes into proxyTarget sink)
     instanceProxy.baz = 1
     expect('baz' in instanceProxy).toBe(true)
diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts
index 4dce5d144..b60931e1f 100644
--- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts
+++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts
@@ -16,7 +16,9 @@ import {
   cloneVNode,
   provide,
   defineAsyncComponent,
-  Component
+  Component,
+  createApp,
+  onActivated
 } from '@vue/runtime-test'
 import { KeepAliveProps } from '../../src/components/KeepAlive'
 
@@ -874,4 +876,31 @@ describe('KeepAlive', () => {
     await nextTick()
     expect(serializeInner(root)).toBe('<p>1</p>')
   })
+
+  // #4976
+  test('handle error in async onActivated', async () => {
+    const err = new Error('foo')
+    const handler = jest.fn()
+
+    const app = createApp({
+      setup() {
+        return () => h(KeepAlive, null, () => h(Child))
+      }
+    })
+
+    const Child = {
+      setup() {
+        onActivated(async () => {
+          throw err
+        })
+      },
+      render() {}
+    }
+
+    app.config.errorHandler = handler
+    app.mount(nodeOps.createElement('div'))
+
+    await nextTick()
+    expect(handler).toHaveBeenCalledWith(err, {}, 'activated hook')
+  })
 })
diff --git a/packages/runtime-dom/__tests__/patchProps.spec.ts b/packages/runtime-dom/__tests__/patchProps.spec.ts
index e1ee7928c..1b97c6ea1 100644
--- a/packages/runtime-dom/__tests__/patchProps.spec.ts
+++ b/packages/runtime-dom/__tests__/patchProps.spec.ts
@@ -25,13 +25,50 @@ describe('runtime-dom: props patching', () => {
     expect((el as any)._value).toBe(obj)
   })
 
+  test('value for custom elements', () => {
+    class TestElement extends HTMLElement {
+      constructor() {
+        super()
+      }
+
+      // intentionally uses _value because this is used in "normal" HTMLElement for storing the object of the set property value
+      private _value: any
+      get value() {
+        return this._value
+      }
+
+      set value(val) {
+        this._value = val
+        this.setterCalled++
+      }
+
+      public setterCalled: number = 0
+    }
+    window.customElements.define('test-element', TestElement)
+    const el = document.createElement('test-element') as TestElement
+    patchProp(el, 'value', null, 'foo')
+    expect(el.value).toBe('foo')
+    expect(el.setterCalled).toBe(1)
+    patchProp(el, 'value', null, null)
+    expect(el.value).toBe('')
+    expect(el.setterCalled).toBe(2)
+    expect(el.getAttribute('value')).toBe(null)
+    const obj = {}
+    patchProp(el, 'value', null, obj)
+    expect(el.value).toBe(obj)
+    expect(el.setterCalled).toBe(3)
+  })
+
   // For <input type="text">, setting el.value won't create a `value` attribute
   // so we need to add tests for other elements
   test('value for non-text input', () => {
     const el = document.createElement('option')
+    el.textContent = 'foo' // #4956
     patchProp(el, 'value', null, 'foo')
+    expect(el.getAttribute('value')).toBe('foo')
     expect(el.value).toBe('foo')
     patchProp(el, 'value', null, null)
+    el.textContent = ''
     expect(el.value).toBe('')
     // #3475
     expect(el.getAttribute('value')).toBe(null)
diff --git a/packages/vue/__tests__/Transition.spec.ts b/packages/vue/__tests__/Transition.spec.ts
index bcb1fcb3b..97b3ccd96 100644
--- a/packages/vue/__tests__/Transition.spec.ts
+++ b/packages/vue/__tests__/Transition.spec.ts
@@ -1969,6 +1969,21 @@ describe('e2e: Transition', () => {
     ).toHaveBeenWarned()
   })
 
+  test('warn when invalid transition mode', () => {
+    createApp({
+      template: `
+        <div id="container">
+          <transition name="test" mode="none">
+            <div class="test">content</div>
+          </transition>
+        </div>
+      `
+    }).mount(document.createElement('div'))
+    expect(
+      `invalid <transition> mode: none`
+    ).toHaveBeenWarned()
+  })
+
   // #3227
   test(`HOC w/ merged hooks`, async () => {
     const innerSpy = jest.fn()
diff --git a/test-dts/component.test-d.ts b/test-dts/component.test-d.ts
index 93c3ea410..d8d3934b9 100644
--- a/test-dts/component.test-d.ts
+++ b/test-dts/component.test-d.ts
@@ -10,7 +10,8 @@ import {
   ShallowUnwrapRef,
   FunctionalComponent,
   ComponentPublicInstance,
-  toRefs
+  toRefs,
+  IsAny
 } from './index'
 
 declare function extractComponentOptions<Props, RawBindings>(
@@ -62,6 +63,7 @@ describe('object props', () => {
     ffff: Ref<(a: number, b: string) => { a: boolean }>
     validated: Ref<string | undefined>
     object: Ref<object | undefined>
+    zzz: any
   }
 
   describe('defineComponent', () => {
@@ -130,7 +132,8 @@ describe('object props', () => {
           // validator requires explicit annotation
           validator: (val: unknown) => val !== ''
         },
-        object: Object as PropType<object>
+        object: Object as PropType<object>,
+        zzz: Object as PropType<any>
       },
       setup(props) {
         const refs = toRefs(props)
@@ -152,6 +155,7 @@ describe('object props', () => {
         expectType<ExpectedRefs['ffff']>(refs.ffff)
         expectType<ExpectedRefs['validated']>(refs.validated)
         expectType<ExpectedRefs['object']>(refs.object)
+        expectType<IsAny<typeof props.zzz>>(true)
 
         return {
           setupA: 1,
diff --git a/test-dts/index.d.ts b/test-dts/index.d.ts
index 3d8d288fa..59eadcb92 100644
--- a/test-dts/index.d.ts
+++ b/test-dts/index.d.ts
@@ -14,3 +14,5 @@ export type IsUnion<T, U extends T = T> = (T extends any
   : never) extends false
   ? false
   : true
+
+export type IsAny<T> = 0 extends (1 & T) ? true : false
diff --git a/test-dts/ref.test-d.ts b/test-dts/ref.test-d.ts
index d4462db9e..5b5274c98 100644
--- a/test-dts/ref.test-d.ts
+++ b/test-dts/ref.test-d.ts
@@ -10,8 +10,7 @@ import {
   toRef,
   toRefs,
   ToRefs,
-  shallowReactive,
-  watch
+  shallowReactive
 } from './index'
 
 function plainType(arg: number | Ref<number>) {
@@ -185,28 +184,45 @@ const p2 = proxyRefs(r2)
 expectType<number>(p2.a)
 expectType<Ref<string>>(p2.obj.k)
 
-// toRef
-const obj = {
-  a: 1,
-  b: ref(1)
-}
-expectType<Ref<number>>(toRef(obj, 'a'))
-expectType<Ref<number>>(toRef(obj, 'b'))
+// toRef and toRefs
+{
+  const obj: {
+    a: number
+    b: Ref<number>
+    c: number | string
+  } = {
+    a: 1,
+    b: ref(1),
+    c: 1
+  }
 
-const objWithUnionProp: { a: string | number } = {
-  a: 1
-}
+  // toRef
+  expectType<Ref<number>>(toRef(obj, 'a'))
+  expectType<Ref<number>>(toRef(obj, 'b'))
+  // Should not distribute Refs over union
+  expectType<Ref<number | string>>(toRef(obj, 'c'))
+
+  // toRefs
+  expectType<{
+    a: Ref<number>
+    b: Ref<number>
+    // Should not distribute Refs over union
+    c: Ref<number | string>
+  }>(toRefs(obj))
+
+  // Both should not do any unwrapping
+  const someReactive = shallowReactive({
+    a: {
+      b: ref(42)
+    }
+  })
 
-watch(toRef(objWithUnionProp, 'a'), value => {
-  expectType<string | number>(value)
-})
+  const toRefResult = toRef(someReactive, 'a')
+  const toRefsResult = toRefs(someReactive)
 
-// toRefs
-const objRefs = toRefs(obj)
-expectType<{
-  a: Ref<number>
-  b: Ref<number>
-}>(objRefs)
+  expectType<Ref<number>>(toRefResult.value.b)
+  expectType<Ref<number>>(toRefsResult.a.value.b)
+}
 
 // #2687
 interface AppData {
@@ -238,20 +254,6 @@ function testUnrefGenerics<T>(p: T | Ref<T>) {
 
 testUnrefGenerics(1)
 
-// #4732
-describe('ref in shallow reactive', () => {
-  const baz = shallowReactive({
-    foo: {
-      bar: ref(42)
-    }
-  })
-
-  const foo = toRef(baz, 'foo')
-
-  expectType<Ref<number>>(foo.value.bar)
-  expectType<number>(foo.value.bar.value)
-})
-
 // #4771
 describe('shallow reactive in reactive', () => {
   const baz = reactive({