Repository: vuejs/vue-next
Pull request #4974 (2021-11-19 14:52:31)

fix(compat): maintain compatConfig option in legacy functional comp
Legacy functional components could have own `compatConfig` which is lost when they are converted to function due to `COMPONENT_FUNCTIONAL` flag. This commit copies `compatConfig` to created function, so it continues to work in expected way



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6e6e949f..777c77960 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,25 @@
+## [3.2.23](https://github.com/vuejs/vue-next/compare/v3.2.22...v3.2.23) (2021-11-26)
+
+
+### Bug Fixes
+
+* **reactivity:** retain readonly proxies when setting as reactive property ([d145128](https://github.com/vuejs/vue-next/commit/d145128ab400f4563eb3727626d0942ea5f4980a)), closes [#4986](https://github.com/vuejs/vue-next/issues/4986)
+* **runtime-core:** fix component public instance has check for accessed non-existent properties ([aac0466](https://github.com/vuejs/vue-next/commit/aac0466cb8819fd132fbcc9c4d3e1014c14e2ad8)), closes [#4962](https://github.com/vuejs/vue-next/issues/4962)
+* **runtime-core:** handle error in async KeepAlive hooks ([#4978](https://github.com/vuejs/vue-next/issues/4978)) ([820a143](https://github.com/vuejs/vue-next/commit/820a14345798edc0ab673bae8ce3181e479d9cca))
+* **runtime-dom:** fix option element value patching edge case ([#4959](https://github.com/vuejs/vue-next/issues/4959)) ([89b2f92](https://github.com/vuejs/vue-next/commit/89b2f924fc82d7f71dcb8ffbacb386fd5cf9ade2)), closes [#4956](https://github.com/vuejs/vue-next/issues/4956)
+* **runtime-dom:** patchDOMProps should not set _value if element is custom element ([#4839](https://github.com/vuejs/vue-next/issues/4839)) ([1701bf3](https://github.com/vuejs/vue-next/commit/1701bf3968f001dd3a2bc9f41e3e7e0f1b13e922))
+* **types:** export ref-macros.d.ts ([1245709](https://github.com/vuejs/vue-next/commit/124570973df4ddfdd38e43bf1e92b9710321e5d9))
+* **types:** fix propType<any> type inference ([#4985](https://github.com/vuejs/vue-next/issues/4985)) ([3c449cd](https://github.com/vuejs/vue-next/commit/3c449cd408840d35987fb32b39737fbf093809d6)), closes [#4983](https://github.com/vuejs/vue-next/issues/4983)
+* **types:** scrip-setup+ts: ensure proper handling of `null` as default prop value. ([#4979](https://github.com/vuejs/vue-next/issues/4979)) ([f2d2d7b](https://github.com/vuejs/vue-next/commit/f2d2d7b2d236f256531ae9ad2048bd939c92d834)), closes [#4868](https://github.com/vuejs/vue-next/issues/4868)
+
+
+### Features
+
+* **compiler-sfc:** export resolveTemplateUsageCheckString for HMR plugin use ([#4908](https://github.com/vuejs/vue-next/issues/4908)) ([c61baac](https://github.com/vuejs/vue-next/commit/c61baac75a03b938bc728a8de961ba93736a0ff6))
+* **compiler-sfc:** expose properties for more accurate HMR ([68c45e7](https://github.com/vuejs/vue-next/commit/68c45e73da902e715df9614800a7ab43d6579198)), closes [#4358](https://github.com/vuejs/vue-next/issues/4358) [#4908](https://github.com/vuejs/vue-next/issues/4908)
+
+
+
 ## [3.2.22](https://github.com/vuejs/vue-next/compare/v3.2.21...v3.2.22) (2021-11-15)
 
 
diff --git a/package.json b/package.json
index ad57f52b8..91d3ac1b9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "private": true,
-  "version": "3.2.22",
+  "version": "3.2.23",
   "scripts": {
     "dev": "node scripts/dev.js",
     "build": "node scripts/build.js",
@@ -9,7 +9,7 @@
     "size-baseline": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler && cd packages/size-check && vite build",
     "lint": "eslint --ext .ts packages/*/src/**.ts",
     "format": "prettier --write --parser typescript \"packages/**/*.ts?(x)\"",
-    "test": "run-s test-unit test-e2e",
+    "test": "run-s \"test-unit -- {@}\" \"test-e2e -- {@}\" --",
     "test-unit": "jest --filter ./scripts/filter-unit.js",
     "test-e2e": "node scripts/build.js vue -f global -d && jest --filter ./scripts/filter-e2e.js --runInBand",
     "test-dts": "node scripts/build.js shared reactivity runtime-core runtime-dom -dt -f esm-bundler && npm run test-dts-only",
diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json
index ccf382947..2f8573b02 100644
--- a/packages/compiler-core/package.json
+++ b/packages/compiler-core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-core",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "@vue/compiler-core",
   "main": "index.js",
   "module": "dist/compiler-core.esm-bundler.js",
@@ -32,7 +32,7 @@
   },
   "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-core#readme",
   "dependencies": {
-    "@vue/shared": "3.2.22",
+    "@vue/shared": "3.2.23",
     "@babel/parser": "^7.15.0",
     "estree-walker": "^2.0.2",
     "source-map": "^0.6.1"
diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts
index 920ba8d4a..215792ed4 100644
--- a/packages/compiler-core/src/transforms/hoistStatic.ts
+++ b/packages/compiler-core/src/transforms/hoistStatic.ts
@@ -52,16 +52,6 @@ function walk(
   context: TransformContext,
   doNotHoistNode: boolean = false
 ) {
-  // Some transforms, e.g. transformAssetUrls from @vue/compiler-sfc, replaces
-  // static bindings with expressions. These expressions are guaranteed to be
-  // constant so they are still eligible for hoisting, but they are only
-  // available at runtime and therefore cannot be evaluated ahead of time.
-  // This is only a concern for pre-stringification (via transformHoist by
-  // @vue/compiler-dom), but doing it here allows us to perform only one full
-  // walk of the AST and allow `stringifyStatic` to stop walking as soon as its
-  // stringification threshold is met.
-  let canStringify = true
-
   const { children } = node
   const originalCount = children.length
   let hoistedCount = 0
@@ -77,9 +67,6 @@ function walk(
         ? ConstantTypes.NOT_CONSTANT
         : getConstantType(child, context)
       if (constantType > ConstantTypes.NOT_CONSTANT) {
-        if (constantType < ConstantTypes.CAN_STRINGIFY) {
-          canStringify = false
-        }
         if (constantType >= ConstantTypes.CAN_HOIST) {
           ;(child.codegenNode as VNodeCall).patchFlag =
             PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
@@ -110,17 +97,12 @@ function walk(
           }
         }
       }
-    } else if (child.type === NodeTypes.TEXT_CALL) {
-      const contentType = getConstantType(child.content, context)
-      if (contentType > 0) {
-        if (contentType < ConstantTypes.CAN_STRINGIFY) {
-          canStringify = false
-        }
-        if (contentType >= ConstantTypes.CAN_HOIST) {
-          child.codegenNode = context.hoist(child.codegenNode)
-          hoistedCount++
-        }
-      }
+    } else if (
+      child.type === NodeTypes.TEXT_CALL &&
+      getConstantType(child.content, context) >= ConstantTypes.CAN_HOIST
+    ) {
+      child.codegenNode = context.hoist(child.codegenNode)
+      hoistedCount++
     }
 
     // walk further
@@ -148,7 +130,7 @@ function walk(
     }
   }
 
-  if (canStringify && hoistedCount && context.transformHoist) {
+  if (hoistedCount && context.transformHoist) {
     context.transformHoist(children, context, node)
   }
 
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
index 416cf79e9..6e2443ea6 100644
--- a/packages/compiler-core/src/utils.ts
+++ b/packages/compiler-core/src/utils.ts
@@ -363,9 +363,6 @@ export function injectProp(
   context: TransformContext
 ) {
   let propsWithInjection: ObjectExpression | CallExpression | undefined
-  const originalProps =
-    node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
-
   /**
    * 1. mergeProps(...)
    * 2. toHandlers(...)
@@ -374,7 +371,7 @@ export function injectProp(
    *
    * we need to get the real props before normalization
    */
-  let props = originalProps
+  let props = node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
   let callPath: CallExpression[] = []
   let parentCall: CallExpression | undefined
   if (
diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json
index f9bd8bbc2..9ab4aaa5d 100644
--- a/packages/compiler-dom/package.json
+++ b/packages/compiler-dom/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-dom",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "@vue/compiler-dom",
   "main": "index.js",
   "module": "dist/compiler-dom.esm-bundler.js",
@@ -37,7 +37,7 @@
   },
   "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-dom#readme",
   "dependencies": {
-    "@vue/shared": "3.2.22",
-    "@vue/compiler-core": "3.2.22"
+    "@vue/shared": "3.2.23",
+    "@vue/compiler-core": "3.2.23"
   }
 }
diff --git a/packages/compiler-dom/src/transforms/stringifyStatic.ts b/packages/compiler-dom/src/transforms/stringifyStatic.ts
index 4f31f53ca..e257e3254 100644
--- a/packages/compiler-dom/src/transforms/stringifyStatic.ts
+++ b/packages/compiler-dom/src/transforms/stringifyStatic.ts
@@ -14,7 +14,8 @@ import {
   ElementTypes,
   PlainElementNode,
   JSChildNode,
-  TextCallNode
+  TextCallNode,
+  ConstantTypes
 } from '@vue/compiler-core'
 import {
   isVoidTag,
@@ -38,6 +39,12 @@ export const enum StringifyThresholds {
 
 type StringifiableNode = PlainElementNode | TextCallNode
 
+/**
+ * Regex for replacing placeholders for embedded constant variables
+ * (e.g. import URL string constants generated by compiler-sfc)
+ */
+const expReplaceRE = /__VUE_EXP_START__(.*?)__VUE_EXP_END__/g
+
 /**
  * Turn eligible hoisted static trees into stringified static nodes, e.g.
  *
@@ -80,7 +87,7 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
       const staticCall = createCallExpression(context.helper(CREATE_STATIC), [
         JSON.stringify(
           currentChunk.map(node => stringifyNode(node, context)).join('')
-        ),
+        ).replace(expReplaceRE, `" + $1 + "`),
         // the 2nd argument indicates the number of DOM nodes this static vnode
         // will insert / hydrate
         String(currentChunk.length)
@@ -165,7 +172,7 @@ const isNonStringifiable = /*#__PURE__*/ makeMap(
 
 /**
  * for a hoisted node, analyze it and return:
- * - false: bailed (contains runtime constant)
+ * - false: bailed (contains non-stringifiable props or runtime constant)
  * - [nc, ec] where
  *   - nc is the number of nodes inside
  *   - ec is the number of element with bindings inside
@@ -210,6 +217,13 @@ function analyzeNode(node: StringifiableNode): [number, number] | false {
         ) {
           return bail()
         }
+        if (
+          p.exp &&
+          (p.exp.type === NodeTypes.COMPOUND_EXPRESSION ||
+            p.exp.constType < ConstantTypes.CAN_STRINGIFY)
+        ) {
+          return bail()
+        }
       }
     }
     for (let i = 0; i < node.children.length; i++) {
@@ -273,8 +287,17 @@ function stringifyElement(
         res += `="${escapeHtml(p.value.content)}"`
       }
     } else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
+      const exp = p.exp as SimpleExpressionNode
+      if (exp.content[0] === '_') {
+        // internally generated string constant references
+        // e.g. imported URL strings via compiler-sfc transformAssetUrl plugin
+        res += ` ${(p.arg as SimpleExpressionNode).content}="__VUE_EXP_START__${
+          exp.content
+        }__VUE_EXP_END__"`
+        continue
+      }
       // constant v-bind, e.g. :foo="1"
-      let evaluated = evaluateConstant(p.exp as SimpleExpressionNode)
+      let evaluated = evaluateConstant(exp)
       if (evaluated != null) {
         const arg = p.arg && (p.arg as SimpleExpressionNode).content
         if (arg === 'class') {
diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json
index 48f547b11..59ddbc236 100644
--- a/packages/compiler-sfc/package.json
+++ b/packages/compiler-sfc/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-sfc",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "@vue/compiler-sfc",
   "main": "dist/compiler-sfc.cjs.js",
   "module": "dist/compiler-sfc.esm-browser.js",
@@ -33,11 +33,11 @@
   "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-sfc#readme",
   "dependencies": {
     "@babel/parser": "^7.15.0",
-    "@vue/compiler-core": "3.2.22",
-    "@vue/compiler-dom": "3.2.22",
-    "@vue/compiler-ssr": "3.2.22",
-    "@vue/ref-transform": "3.2.22",
-    "@vue/shared": "3.2.22",
+    "@vue/compiler-core": "3.2.23",
+    "@vue/compiler-dom": "3.2.23",
+    "@vue/compiler-ssr": "3.2.23",
+    "@vue/ref-transform": "3.2.23",
+    "@vue/shared": "3.2.23",
     "estree-walker": "^2.0.2",
     "magic-string": "^0.25.7",
     "source-map": "^0.6.1",
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index feaca8bd3..bec502741 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -65,7 +65,7 @@ const isBuiltInDir = makeMap(
 
 export interface SFCScriptCompileOptions {
   /**
-   * Scope ID for prefixing injected CSS varialbes.
+   * Scope ID for prefixing injected CSS variables.
    * This must be consistent with the `id` passed to `compileStyle`.
    */
   id: string
@@ -107,13 +107,13 @@ export interface SFCScriptCompileOptions {
   inlineTemplate?: boolean
   /**
    * Options for template compilation when inlining. Note these are options that
-   * would normally be pased to `compiler-sfc`'s own `compileTemplate()`, not
+   * would normally be passed to `compiler-sfc`'s own `compileTemplate()`, not
    * options passed to `compiler-dom`.
    */
   templateOptions?: Partial<SFCTemplateCompileOptions>
 }
 
-interface ImportBinding {
+export interface ImportBinding {
   isType: boolean
   imported: string
   source: string
@@ -335,11 +335,7 @@ export function compileScript(
 
     let isUsedInTemplate = true
     if (isTS && sfc.template && !sfc.template.src && !sfc.template.lang) {
-      isUsedInTemplate = new RegExp(
-        // #4274 escape $ since it's a special char in regex
-        // (and is the only regex special char that is valid in identifiers)
-        `[^\\w$_]${local.replace(/\$/g, '\\$')}[^\\w$_]`
-      ).test(resolveTemplateUsageCheckString(sfc))
+      isUsedInTemplate = isImportUsed(local, sfc)
     }
 
     userImports[local] = {
@@ -416,7 +412,7 @@ export function compileScript(
                 default: right
               }
             } else if (prop.value.type === 'Identifier') {
-              // simple destucture
+              // simple destructure
               propsDestructuredBindings[propKey] = {
                 local: prop.value.name
               }
@@ -636,7 +632,7 @@ export function compileScript(
 
   /**
    * check defaults. If the default object is an object literal with only
-   * static properties, we can directly generate more optimzied default
+   * static properties, we can directly generate more optimized default
    * declarations. Otherwise we will have to fallback to runtime merging.
    */
   function hasStaticWithDefaults() {
@@ -1441,6 +1437,7 @@ export function compileScript(
   return {
     ...scriptSetup,
     bindings: bindingMetadata,
+    imports: userImports,
     content: s.toString(),
     map: genSourceMap
       ? (s.generateMap({
@@ -1545,7 +1542,7 @@ function walkObjectPattern(
       }
     } else {
       // ...rest
-      // argument can only be identifer when destructuring
+      // argument can only be identifier when destructuring
       const type = isConst ? BindingTypes.SETUP_CONST : BindingTypes.SETUP_LET
       registerBinding(bindings, p.argument as Identifier, type)
     }
@@ -1577,7 +1574,7 @@ function walkPattern(
       : BindingTypes.SETUP_LET
     registerBinding(bindings, node, type)
   } else if (node.type === 'RestElement') {
-    // argument can only be identifer when destructuring
+    // argument can only be identifier when destructuring
     const type = isConst ? BindingTypes.SETUP_CONST : BindingTypes.SETUP_LET
     registerBinding(bindings, node.argument as Identifier, type)
   } else if (node.type === 'ObjectPattern') {
@@ -2018,3 +2015,40 @@ function stripTemplateString(str: string): string {
   }
   return ''
 }
+
+function isImportUsed(local: string, sfc: SFCDescriptor): boolean {
+  return new RegExp(
+    // #4274 escape $ since it's a special char in regex
+    // (and is the only regex special char that is valid in identifiers)
+    `[^\\w$_]${local.replace(/\$/g, '\\$')}[^\\w$_]`
+  ).test(resolveTemplateUsageCheckString(sfc))
+}
+
+/**
+ * Note: this comparison assumes the prev/next script are already identical,
+ * and only checks the special case where <script setup lang="ts"> unused import
+ * pruning result changes due to template changes.
+ */
+export function hmrShouldReload(
+  prevImports: Record<string, ImportBinding>,
+  next: SFCDescriptor
+): boolean {
+  if (
+    !next.scriptSetup ||
+    (next.scriptSetup.lang !== 'ts' && next.scriptSetup.lang !== 'tsx')
+  ) {
+    return false
+  }
+
+  // for each previous import, check if its used status remain the same based on
+  // the next descriptor's template
+  for (const key in prevImports) {
+    // if an import was previous unused, but now is used, we need to force
+    // reload so that the script now includes that import.
+    if (!prevImports[key].isUsedInTemplate && isImportUsed(key, next)) {
+      return true
+    }
+  }
+
+  return false
+}
diff --git a/packages/compiler-sfc/src/parse.ts b/packages/compiler-sfc/src/parse.ts
index 946435f6b..ed44e5368 100644
--- a/packages/compiler-sfc/src/parse.ts
+++ b/packages/compiler-sfc/src/parse.ts
@@ -11,6 +11,7 @@ import { RawSourceMap, SourceMapGenerator } from 'source-map'
 import { TemplateCompiler } from './compileTemplate'
 import { parseCssVars } from './cssVars'
 import { createCache } from './cache'
+import { hmrShouldReload, ImportBinding } from './compileScript'
 
 export interface SFCParseOptions {
   filename?: string
@@ -40,6 +41,7 @@ export interface SFCScriptBlock extends SFCBlock {
   type: 'script'
   setup?: string | boolean
   bindings?: BindingMetadata
+  imports?: Record<string, ImportBinding>
   /**
    * import('\@babel/types').Statement
    */
@@ -49,6 +51,7 @@ export interface SFCScriptBlock extends SFCBlock {
    */
   scriptSetupAst?: any[]
 }
+
 export interface SFCStyleBlock extends SFCBlock {
   type: 'style'
   scoped?: boolean
@@ -64,9 +67,21 @@ export interface SFCDescriptor {
   styles: SFCStyleBlock[]
   customBlocks: SFCBlock[]
   cssVars: string[]
-  // whether the SFC uses :slotted() modifier.
-  // this is used as a compiler optimization hint.
+  /**
+   * whether the SFC uses :slotted() modifier.
+   * this is used as a compiler optimization hint.
+   */
   slotted: boolean
+
+  /**
+   * compare with an existing descriptor to determine whether HMR should perform
+   * a reload vs. re-render.
+   *
+   * Note: this comparison assumes the prev/next script are already identical,
+   * and only checks the special case where <script setup lang="ts"> unused import
+   * pruning result changes due to template changes.
+   */
+  shouldForceReload: (prevImports: Record<string, ImportBinding>) => boolean
 }
 
 export interface SFCParseResult {
@@ -103,7 +118,8 @@ export function parse(
     styles: [],
     customBlocks: [],
     cssVars: [],
-    slotted: false
+    slotted: false,
+    shouldForceReload: prevImports => hmrShouldReload(prevImports, descriptor)
   }
 
   const errors: (CompilerError | SyntaxError)[] = []
diff --git a/packages/compiler-sfc/src/templateTransformAssetUrl.ts b/packages/compiler-sfc/src/templateTransformAssetUrl.ts
index 4246817df..2f6152b8e 100644
--- a/packages/compiler-sfc/src/templateTransformAssetUrl.ts
+++ b/packages/compiler-sfc/src/templateTransformAssetUrl.ts
@@ -162,7 +162,12 @@ function getImportsExpressionExp(
       exp = context.imports[existingIndex].exp as SimpleExpressionNode
     } else {
       name = `_imports_${context.imports.length}`
-      exp = createSimpleExpression(name, false, loc, ConstantTypes.CAN_HOIST)
+      exp = createSimpleExpression(
+        name,
+        false,
+        loc,
+        ConstantTypes.CAN_STRINGIFY
+      )
       context.imports.push({ exp, path })
     }
 
@@ -184,13 +189,13 @@ function getImportsExpressionExp(
         `_hoisted_${existingHoistIndex + 1}`,
         false,
         loc,
-        ConstantTypes.CAN_HOIST
+        ConstantTypes.CAN_STRINGIFY
       )
     }
     return context.hoist(
-      createSimpleExpression(hashExp, false, loc, ConstantTypes.CAN_HOIST)
+      createSimpleExpression(hashExp, false, loc, ConstantTypes.CAN_STRINGIFY)
     )
   } else {
-    return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_HOIST)
+    return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_STRINGIFY)
   }
 }
diff --git a/packages/compiler-sfc/src/templateTransformSrcset.ts b/packages/compiler-sfc/src/templateTransformSrcset.ts
index 02cda03ab..ccdcc721e 100644
--- a/packages/compiler-sfc/src/templateTransformSrcset.ts
+++ b/packages/compiler-sfc/src/templateTransformSrcset.ts
@@ -57,7 +57,7 @@ export const transformSrcset: NodeTransform = (
             return { url, descriptor }
           })
 
-          // data urls contains comma after the ecoding so we need to re-merge
+          // data urls contains comma after the encoding so we need to re-merge
           // them
           for (let i = 0; i < imageCandidates.length; i++) {
             const { url } = imageCandidates[i]
@@ -113,14 +113,14 @@ export const transformSrcset: NodeTransform = (
                     `_imports_${existingImportsIndex}`,
                     false,
                     attr.loc,
-                    ConstantTypes.CAN_HOIST
+                    ConstantTypes.CAN_STRINGIFY
                   )
                 } else {
                   exp = createSimpleExpression(
                     `_imports_${context.imports.length}`,
                     false,
                     attr.loc,
-                    ConstantTypes.CAN_HOIST
+                    ConstantTypes.CAN_STRINGIFY
                   )
                   context.imports.push({ exp, path })
                 }
@@ -131,7 +131,7 @@ export const transformSrcset: NodeTransform = (
                 `"${url}"`,
                 false,
                 attr.loc,
-                ConstantTypes.CAN_HOIST
+                ConstantTypes.CAN_STRINGIFY
               )
               compoundExpression.children.push(exp)
             }
@@ -146,7 +146,7 @@ export const transformSrcset: NodeTransform = (
           })
 
           const hoisted = context.hoist(compoundExpression)
-          hoisted.constType = ConstantTypes.CAN_HOIST
+          hoisted.constType = ConstantTypes.CAN_STRINGIFY
 
           node.props[index] = {
             type: NodeTypes.DIRECTIVE,
diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json
index b4091cb74..0a2ed6d42 100644
--- a/packages/compiler-ssr/package.json
+++ b/packages/compiler-ssr/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-ssr",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "@vue/compiler-ssr",
   "main": "dist/compiler-ssr.cjs.js",
   "types": "dist/compiler-ssr.d.ts",
@@ -28,7 +28,7 @@
   },
   "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-ssr#readme",
   "dependencies": {
-    "@vue/shared": "3.2.22",
-    "@vue/compiler-dom": "3.2.22"
+    "@vue/shared": "3.2.23",
+    "@vue/compiler-dom": "3.2.23"
   }
 }
diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json
index 18355fa3b..6e26d7f61 100644
--- a/packages/reactivity/package.json
+++ b/packages/reactivity/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/reactivity",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "@vue/reactivity",
   "main": "index.js",
   "module": "dist/reactivity.esm-bundler.js",
@@ -36,6 +36,6 @@
   },
   "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/reactivity#readme",
   "dependencies": {
-    "@vue/shared": "3.2.22"
+    "@vue/shared": "3.2.23"
   }
 }
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/index.ts b/packages/reactivity/src/index.ts
index d608e8b8f..c61999bd6 100644
--- a/packages/reactivity/src/index.ts
+++ b/packages/reactivity/src/index.ts
@@ -12,6 +12,7 @@ export {
   ToRef,
   ToRefs,
   UnwrapRef,
+  ShallowRef,
   ShallowUnwrapRef,
   RefUnwrapBailTypes
 } from './ref'
diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts
index fb43668e4..3e00541c7 100644
--- a/packages/reactivity/src/ref.ts
+++ b/packages/reactivity/src/ref.ts
@@ -77,7 +77,7 @@ export function ref(value?: unknown) {
 
 declare const ShallowRefMarker: unique symbol
 
-type ShallowRef<T = any> = Ref<T> & { [ShallowRefMarker]?: true }
+export type ShallowRef<T = any> = Ref<T> & { [ShallowRefMarker]?: true }
 
 export function shallowRef<T extends object>(
   value: T
@@ -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/ref-transform/package.json b/packages/ref-transform/package.json
index 08e65f865..56caf505e 100644
--- a/packages/ref-transform/package.json
+++ b/packages/ref-transform/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/ref-transform",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "@vue/ref-transform",
   "main": "dist/ref-transform.cjs.js",
   "files": [
@@ -29,8 +29,8 @@
   "homepage": "https://github.com/vuejs/vue-next/tree/dev/packages/ref-transform#readme",
   "dependencies": {
     "@babel/parser": "^7.15.0",
-    "@vue/compiler-core": "3.2.22",
-    "@vue/shared": "3.2.22",
+    "@vue/compiler-core": "3.2.23",
+    "@vue/shared": "3.2.23",
     "estree-walker": "^2.0.2",
     "magic-string": "^0.25.7"
   },
diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json
index c5bb243e6..656f63967 100644
--- a/packages/runtime-core/package.json
+++ b/packages/runtime-core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/runtime-core",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "@vue/runtime-core",
   "main": "index.js",
   "module": "dist/runtime-core.esm-bundler.js",
@@ -32,7 +32,7 @@
   },
   "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/runtime-core#readme",
   "dependencies": {
-    "@vue/shared": "3.2.22",
-    "@vue/reactivity": "3.2.22"
+    "@vue/shared": "3.2.23",
+    "@vue/reactivity": "3.2.23"
   }
 }
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/compat/componentFunctional.ts b/packages/runtime-core/src/compat/componentFunctional.ts
index 80af32a1d..1b1146bba 100644
--- a/packages/runtime-core/src/compat/componentFunctional.ts
+++ b/packages/runtime-core/src/compat/componentFunctional.ts
@@ -55,6 +55,7 @@ export function convertLegacyFunctionalComponent(comp: ComponentOptions) {
   }
   Func.props = comp.props
   Func.displayName = comp.name
+  Func.compatConfig = comp.compatConfig
   // v2 functional components do not inherit attrs
   Func.inheritAttrs = false
 
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 515bd16d2..cda15d8bd 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -60,7 +60,11 @@ import { markAttrsAccessed } from './componentRenderUtils'
 import { currentRenderingInstance } from './componentRenderContext'
 import { startMeasure, endMeasure } from './profiling'
 import { convertLegacyRenderFn } from './compat/renderFn'
-import { globalCompatConfig, validateCompatConfig } from './compat/compatConfig'
+import {
+  CompatConfig,
+  globalCompatConfig,
+  validateCompatConfig
+} from './compat/compatConfig'
 import { SchedulerJob } from './scheduler'
 
 export type Data = Record<string, unknown>
@@ -111,6 +115,7 @@ export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
   emits?: E | (keyof E)[]
   inheritAttrs?: boolean
   displayName?: string
+  compatConfig?: CompatConfig
 }
 
 export interface ClassComponent {
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/package.json b/packages/runtime-dom/package.json
index 38423f3d2..65e5ecf2e 100644
--- a/packages/runtime-dom/package.json
+++ b/packages/runtime-dom/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/runtime-dom",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "@vue/runtime-dom",
   "main": "index.js",
   "module": "dist/runtime-dom.esm-bundler.js",
@@ -35,8 +35,8 @@
   },
   "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/runtime-dom#readme",
   "dependencies": {
-    "@vue/shared": "3.2.22",
-    "@vue/runtime-core": "3.2.22",
+    "@vue/shared": "3.2.23",
+    "@vue/runtime-core": "3.2.23",
     "csstype": "^2.6.8"
   }
 }
diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts
index 5127e4a47..059fcac16 100644
--- a/packages/runtime-dom/src/apiCustomElement.ts
+++ b/packages/runtime-dom/src/apiCustomElement.ts
@@ -321,7 +321,7 @@ export class VueElement extends BaseClass {
         // HMR
         if (__DEV__) {
           instance.ceReload = newStyles => {
-            // alawys reset styles
+            // always reset styles
             if (this._styles) {
               this._styles.forEach(s => this.shadowRoot!.removeChild(s))
               this._styles.length = 0
diff --git a/packages/runtime-dom/src/components/TransitionGroup.ts b/packages/runtime-dom/src/components/TransitionGroup.ts
index 8c5739ac1..5c78be26d 100644
--- a/packages/runtime-dom/src/components/TransitionGroup.ts
+++ b/packages/runtime-dom/src/components/TransitionGroup.ts
@@ -27,13 +27,8 @@ import {
 } from '@vue/runtime-core'
 import { extend } from '@vue/shared'
 
-interface Position {
-  top: number
-  left: number
-}
-
-const positionMap = new WeakMap<VNode, Position>()
-const newPositionMap = new WeakMap<VNode, Position>()
+const positionMap = new WeakMap<VNode, DOMRect>()
+const newPositionMap = new WeakMap<VNode, DOMRect>()
 
 export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
   tag?: string
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/server-renderer/package.json b/packages/server-renderer/package.json
index 123574022..d171b9945 100644
--- a/packages/server-renderer/package.json
+++ b/packages/server-renderer/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/server-renderer",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "@vue/server-renderer",
   "main": "index.js",
   "module": "dist/server-renderer.esm-bundler.js",
@@ -31,10 +31,10 @@
   },
   "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/server-renderer#readme",
   "peerDependencies": {
-    "vue": "3.2.22"
+    "vue": "3.2.23"
   },
   "dependencies": {
-    "@vue/shared": "3.2.22",
-    "@vue/compiler-ssr": "3.2.22"
+    "@vue/shared": "3.2.23",
+    "@vue/compiler-ssr": "3.2.23"
   }
 }
diff --git a/packages/sfc-playground/package.json b/packages/sfc-playground/package.json
index c40350cc3..136a866b9 100644
--- a/packages/sfc-playground/package.json
+++ b/packages/sfc-playground/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/sfc-playground",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "private": true,
   "scripts": {
     "dev": "vite",
@@ -11,8 +11,8 @@
     "@vitejs/plugin-vue": "^1.9.3"
   },
   "dependencies": {
-    "vue": "3.2.22",
-    "@vue/repl": "^0.4.4",
+    "vue": "3.2.23",
+    "@vue/repl": "^0.4.5",
     "file-saver": "^2.0.5",
     "jszip": "^3.6.0"
   }
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 05df8729f..9a961d1b1 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/shared",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "internal utils shared across @vue packages",
   "main": "index.js",
   "module": "dist/shared.esm-bundler.js",
diff --git a/packages/size-check/package.json b/packages/size-check/package.json
index 1faa5490e..d6c2724bd 100644
--- a/packages/size-check/package.json
+++ b/packages/size-check/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/size-check",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "private": true,
   "scripts": {
     "build": "vite build"
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/template-explorer/local.html b/packages/template-explorer/local.html
index 1aab3c26f..6ec0ba20a 100644
--- a/packages/template-explorer/local.html
+++ b/packages/template-explorer/local.html
@@ -1,5 +1,5 @@
 <title>Vue Template Explorer</title>
-<link rel="stylesheet" data-name="vs/editor/editor.main" href="../../node_modules/monaco-editor/min/vs/editor/editor.main.css">
+<link rel="stylesheet" data-name="vs/editor/editor.main" href="./node_modules/monaco-editor/min/vs/editor/editor.main.css">
 <link rel="stylesheet" href="./style.css">
 
 <div id="header"></div>
diff --git a/packages/template-explorer/package.json b/packages/template-explorer/package.json
index 42290f87e..bf7fc9272 100644
--- a/packages/template-explorer/package.json
+++ b/packages/template-explorer/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/template-explorer",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "private": true,
   "buildOptions": {
     "formats": [
diff --git a/packages/template-explorer/src/index.ts b/packages/template-explorer/src/index.ts
index d083d35ca..1e8efdb95 100644
--- a/packages/template-explorer/src/index.ts
+++ b/packages/template-explorer/src/index.ts
@@ -40,6 +40,9 @@ window.init = () => {
       localStorage.getItem('state') ||
       `{}`
   )
+  // functions are not persistable, so delete it in case we sometimes need
+  // to debug with custom nodeTransforms
+  delete persistedState.options.nodeTransforms
 
   ssrMode.value = persistedState.ssr
   Object.assign(compilerOptions, persistedState.options)
diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json
index 46cedf241..9ed231bbb 100644
--- a/packages/vue-compat/package.json
+++ b/packages/vue-compat/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/compat",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "Vue 3 compatibility build for Vue 2",
   "main": "index.js",
   "module": "dist/vue.runtime.esm-bundler.js",
@@ -38,6 +38,6 @@
   },
   "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/vue-compat#readme",
   "peerDependencies": {
-    "vue": "3.2.22"
+    "vue": "3.2.23"
   }
 }
diff --git a/packages/vue/package.json b/packages/vue/package.json
index c8f914805..f1607b63b 100644
--- a/packages/vue/package.json
+++ b/packages/vue/package.json
@@ -1,6 +1,6 @@
 {
   "name": "vue",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "The progressive JavaScript framework for buiding modern web UI.",
   "main": "index.js",
   "module": "dist/vue.runtime.esm-bundler.js",
@@ -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",
@@ -61,10 +62,10 @@
   },
   "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/vue#readme",
   "dependencies": {
-    "@vue/shared": "3.2.22",
-    "@vue/compiler-dom": "3.2.22",
-    "@vue/runtime-dom": "3.2.22",
-    "@vue/compiler-sfc": "3.2.22",
-    "@vue/server-renderer": "3.2.22"
+    "@vue/shared": "3.2.23",
+    "@vue/compiler-dom": "3.2.23",
+    "@vue/runtime-dom": "3.2.23",
+    "@vue/compiler-sfc": "3.2.23",
+    "@vue/server-renderer": "3.2.23"
   }
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1fd96811e..4d38338b5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -100,7 +100,7 @@ importers:
     specifiers:
       '@babel/parser': ^7.15.0
       '@babel/types': ^7.15.0
-      '@vue/shared': 3.2.22
+      '@vue/shared': 3.2.23
       estree-walker: ^2.0.2
       source-map: ^0.6.1
     dependencies:
@@ -113,8 +113,8 @@ importers:
 
   packages/compiler-dom:
     specifiers:
-      '@vue/compiler-core': 3.2.22
-      '@vue/shared': 3.2.22
+      '@vue/compiler-core': 3.2.23
+      '@vue/shared': 3.2.23
     dependencies:
       '@vue/compiler-core': link:../compiler-core
       '@vue/shared': link:../shared
@@ -125,12 +125,12 @@ importers:
       '@babel/types': ^7.15.0
       '@types/estree': ^0.0.48
       '@types/lru-cache': ^5.1.0
-      '@vue/compiler-core': 3.2.22
-      '@vue/compiler-dom': 3.2.22
-      '@vue/compiler-ssr': 3.2.22
+      '@vue/compiler-core': 3.2.23
+      '@vue/compiler-dom': 3.2.23
+      '@vue/compiler-ssr': 3.2.23
       '@vue/consolidate': ^0.17.3
-      '@vue/ref-transform': 3.2.22
-      '@vue/shared': 3.2.22
+      '@vue/ref-transform': 3.2.23
+      '@vue/shared': 3.2.23
       estree-walker: ^2.0.2
       hash-sum: ^2.0.0
       lru-cache: ^5.1.1
@@ -168,15 +168,15 @@ importers:
 
   packages/compiler-ssr:
     specifiers:
-      '@vue/compiler-dom': 3.2.22
-      '@vue/shared': 3.2.22
+      '@vue/compiler-dom': 3.2.23
+      '@vue/shared': 3.2.23
     dependencies:
       '@vue/compiler-dom': link:../compiler-dom
       '@vue/shared': link:../shared
 
   packages/reactivity:
     specifiers:
-      '@vue/shared': 3.2.22
+      '@vue/shared': 3.2.23
     dependencies:
       '@vue/shared': link:../shared
 
@@ -184,8 +184,8 @@ importers:
     specifiers:
       '@babel/core': ^7.15.0
       '@babel/parser': ^7.15.0
-      '@vue/compiler-core': 3.2.22
-      '@vue/shared': 3.2.22
+      '@vue/compiler-core': 3.2.23
+      '@vue/shared': 3.2.23
       estree-walker: ^2.0.2
       magic-string: ^0.25.7
     dependencies:
@@ -199,16 +199,16 @@ importers:
 
   packages/runtime-core:
     specifiers:
-      '@vue/reactivity': 3.2.22
-      '@vue/shared': 3.2.22
+      '@vue/reactivity': 3.2.23
+      '@vue/shared': 3.2.23
     dependencies:
       '@vue/reactivity': link:../reactivity
       '@vue/shared': link:../shared
 
   packages/runtime-dom:
     specifiers:
-      '@vue/runtime-core': 3.2.22
-      '@vue/shared': 3.2.22
+      '@vue/runtime-core': 3.2.23
+      '@vue/shared': 3.2.23
       csstype: ^2.6.8
     dependencies:
       '@vue/runtime-core': link:../runtime-core
@@ -217,16 +217,16 @@ importers:
 
   packages/runtime-test:
     specifiers:
-      '@vue/runtime-core': 3.2.22
-      '@vue/shared': 3.2.22
+      '@vue/runtime-core': 3.2.23
+      '@vue/shared': 3.2.23
     dependencies:
       '@vue/runtime-core': link:../runtime-core
       '@vue/shared': link:../shared
 
   packages/server-renderer:
     specifiers:
-      '@vue/compiler-ssr': 3.2.22
-      '@vue/shared': 3.2.22
+      '@vue/compiler-ssr': 3.2.23
+      '@vue/shared': 3.2.23
     dependencies:
       '@vue/compiler-ssr': link:../compiler-ssr
       '@vue/shared': link:../shared
@@ -234,12 +234,12 @@ importers:
   packages/sfc-playground:
     specifiers:
       '@vitejs/plugin-vue': ^1.9.3
-      '@vue/repl': ^0.4.4
+      '@vue/repl': ^0.4.5
       file-saver: ^2.0.5
       jszip: ^3.6.0
-      vue: 3.2.22
+      vue: 3.2.23
     dependencies:
-      '@vue/repl': 0.4.4
+      '@vue/repl': 0.4.5
       file-saver: 2.0.5
       jszip: 3.7.1
       vue: link:../vue
@@ -265,11 +265,11 @@ importers:
 
   packages/vue:
     specifiers:
-      '@vue/compiler-dom': 3.2.22
-      '@vue/compiler-sfc': 3.2.22
-      '@vue/runtime-dom': 3.2.22
-      '@vue/server-renderer': 3.2.22
-      '@vue/shared': 3.2.22
+      '@vue/compiler-dom': 3.2.23
+      '@vue/compiler-sfc': 3.2.23
+      '@vue/runtime-dom': 3.2.23
+      '@vue/server-renderer': 3.2.23
+      '@vue/shared': 3.2.23
     dependencies:
       '@vue/compiler-dom': link:../compiler-dom
       '@vue/compiler-sfc': link:../compiler-sfc
@@ -1268,8 +1268,8 @@ packages:
     engines: {node: '>= 0.12.0'}
     dev: true
 
-  /@vue/repl/0.4.4:
-    resolution: {integrity: sha512-Mj0Zasc6V7bymDg7DNiN1bW/MZVNS4PmKzDkG9hq9ysrkrZoTHcwA+gcAxF+25Dfn0Tz8WSZD9FuEgEKoA681g==}
+  /@vue/repl/0.4.5:
+    resolution: {integrity: sha512-FnKLdekdKsgVaFTi2wwE0ZjQfxIOlCEFeU0TtO5Quaa13k4/VJvqTKsJGEqcdC4rc4E7avKe5HhnND0tEnC+ag==}
     peerDependencies:
       vue: ^3.2.13
     dev: false


Test patch:
diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap
new file mode 100644
index 000000000..efa75df3f
--- /dev/null
+++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`stringify static html should bail on bindings that are hoisted but not stringifiable 1`] = `
+"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
+
+const _hoisted_1 = /*#__PURE__*/_createElementVNode(\\"div\\", null, [
+  /*#__PURE__*/_createElementVNode(\\"span\\", { class: \\"foo\\" }, \\"foo\\"),
+  /*#__PURE__*/_createElementVNode(\\"span\\", { class: \\"foo\\" }, \\"foo\\"),
+  /*#__PURE__*/_createElementVNode(\\"span\\", { class: \\"foo\\" }, \\"foo\\"),
+  /*#__PURE__*/_createElementVNode(\\"span\\", { class: \\"foo\\" }, \\"foo\\"),
+  /*#__PURE__*/_createElementVNode(\\"span\\", { class: \\"foo\\" }, \\"foo\\"),
+  /*#__PURE__*/_createElementVNode(\\"img\\", { src: _imports_0_ })
+], -1 /* HOISTED */)
+const _hoisted_2 = [
+  _hoisted_1
+]
+
+return function render(_ctx, _cache) {
+  return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2))
+}"
+`;
+
+exports[`stringify static html should work with bindings that are non-static but stringifiable 1`] = `
+"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
+
+const _hoisted_1 = /*#__PURE__*/_createStaticVNode(\\"<div><span class=\\\\\\"foo\\\\\\">foo</span><span class=\\\\\\"foo\\\\\\">foo</span><span class=\\\\\\"foo\\\\\\">foo</span><span class=\\\\\\"foo\\\\\\">foo</span><span class=\\\\\\"foo\\\\\\">foo</span><img src=\\\\\\"\\" + _imports_0_ + \\"\\\\\\"></div>\\", 1)
+const _hoisted_2 = [
+  _hoisted_1
+]
+
+return function render(_ctx, _cache) {
+  return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2))
+}"
+`;
diff --git a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts
index 0923f0816..0beb42585 100644
--- a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts
+++ b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts
@@ -181,8 +181,8 @@ describe('stringify static html', () => {
     ])
   })
 
-  test('should bail on runtime constant v-bind bindings', () => {
-    const { ast } = compile(
+  test('should bail on bindings that are hoisted but not stringifiable', () => {
+    const { ast, code } = compile(
       `<div><div>${repeat(
         `<span class="foo">foo</span>`,
         StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
@@ -216,13 +216,62 @@ describe('stringify static html', () => {
     expect(ast.hoists).toMatchObject([
       {
         // the expression and the tree are still hoistable
-        // but if it's stringified it will be NodeTypes.CALL_EXPRESSION
+        // but should stay NodeTypes.VNODE_CALL
+        // if it's stringified it will be NodeTypes.JS_CALL_EXPRESSION
         type: NodeTypes.VNODE_CALL
       },
       {
         type: NodeTypes.JS_ARRAY_EXPRESSION
       }
     ])
+    expect(code).toMatchSnapshot()
+  })
+
+  test('should work with bindings that are non-static but stringifiable', () => {
+    // if a binding is non-static but marked as CAN_STRINGIFY, it means it's
+    // a known reference to a constant string.
+    const { ast, code } = compile(
+      `<div><div>${repeat(
+        `<span class="foo">foo</span>`,
+        StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
+      )}<img src="./foo" /></div></div>`,
+      {
+        hoistStatic: true,
+        prefixIdentifiers: true,
+        transformHoist: stringifyStatic,
+        nodeTransforms: [
+          node => {
+            if (node.type === NodeTypes.ELEMENT && node.tag === 'img') {
+              const exp = createSimpleExpression(
+                '_imports_0_',
+                false,
+                node.loc,
+                ConstantTypes.CAN_STRINGIFY
+              )
+              node.props[0] = {
+                type: NodeTypes.DIRECTIVE,
+                name: 'bind',
+                arg: createSimpleExpression('src', true),
+                exp,
+                modifiers: [],
+                loc: node.loc
+              }
+            }
+          }
+        ]
+      }
+    )
+    expect(ast.hoists).toMatchObject([
+      {
+        // the hoisted node should be NodeTypes.JS_CALL_EXPRESSION
+        // of `createStaticVNode()` instead of dynamic NodeTypes.VNODE_CALL
+        type: NodeTypes.JS_CALL_EXPRESSION
+      },
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION
+      }
+    ])
+    expect(code).toMatchSnapshot()
   })
 
   // #1128
diff --git a/packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap
index 37545d772..c0fceecee 100644
--- a/packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap
+++ b/packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap
@@ -74,6 +74,22 @@ export function render(_ctx, _cache) {
 }"
 `;
 
+exports[`compiler sfc: transform asset url transform with stringify 1`] = `
+"import { createElementVNode as _createElementVNode, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
+import _imports_0 from './bar.png'
+import _imports_1 from '/bar.png'
+
+
+const _hoisted_1 = /*#__PURE__*/_createStaticVNode(\\"<img src=\\\\\\"\\" + _imports_0 + \\"\\\\\\"><img src=\\\\\\"\\" + _imports_1 + \\"\\\\\\"><img src=\\\\\\"https://foo.bar/baz.png\\\\\\"><img src=\\\\\\"//foo.bar/baz.png\\\\\\"><img src=\\\\\\"\\" + _imports_0 + \\"\\\\\\">\\", 5)
+const _hoisted_6 = [
+  _hoisted_1
+]
+
+export function render(_ctx, _cache) {
+  return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_6))
+}"
+`;
+
 exports[`compiler sfc: transform asset url with explicit base 1`] = `
 "import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
 import _imports_0 from 'bar.png'
diff --git a/packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap
index 5cc0fe2a9..89e487f09 100644
--- a/packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap
+++ b/packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap
@@ -194,3 +194,28 @@ export function render(_ctx, _cache) {
   ], 64 /* STABLE_FRAGMENT */))
 }"
 `;
+
+exports[`compiler sfc: transform srcset transform srcset w/ stringify 1`] = `
+"import { createElementVNode as _createElementVNode, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
+import _imports_0 from './logo.png'
+import _imports_1 from '/logo.png'
+
+
+const _hoisted_1 = _imports_0
+const _hoisted_2 = _imports_0 + ' 2x'
+const _hoisted_3 = _imports_0 + ' 2x'
+const _hoisted_4 = _imports_0 + ', ' + _imports_0 + ' 2x'
+const _hoisted_5 = _imports_0 + ' 2x, ' + _imports_0
+const _hoisted_6 = _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
+const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
+const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x'
+const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x'
+const _hoisted_10 = /*#__PURE__*/_createStaticVNode(\\"<img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_1 + \\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_2 + \\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_3 + \\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_4 + \\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_5 + \\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_6 + \\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_7 + \\"\\\\\\"><img src=\\\\\\"/logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_8 + \\"\\\\\\"><img src=\\\\\\"https://example.com/logo.png\\\\\\" srcset=\\\\\\"https://example.com/logo.png, https://example.com/logo.png 2x\\\\\\"><img src=\\\\\\"/logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_9 + \\"\\\\\\"><img src=\\\\\\"data:image/png;base64,i\\\\\\" srcset=\\\\\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\\\\\">\\", 12)
+const _hoisted_22 = [
+  _hoisted_10
+]
+
+export function render(_ctx, _cache) {
+  return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_22))
+}"
+`;
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/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts
index 18e2cb5e9..44ef092aa 100644
--- a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts
+++ b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts
@@ -1,4 +1,9 @@
-import { generate, baseParse, transform } from '@vue/compiler-core'
+import {
+  generate,
+  baseParse,
+  transform,
+  TransformOptions
+} from '@vue/compiler-core'
 import {
   transformAssetUrl,
   createAssetUrlTransformWithOptions,
@@ -7,8 +12,13 @@ import {
 } from '../src/templateTransformAssetUrl'
 import { transformElement } from '../../compiler-core/src/transforms/transformElement'
 import { transformBind } from '../../compiler-core/src/transforms/vBind'
+import { stringifyStatic } from '../../compiler-dom/src/transforms/stringifyStatic'
 
-function compileWithAssetUrls(template: string, options?: AssetURLOptions) {
+function compileWithAssetUrls(
+  template: string,
+  options?: AssetURLOptions,
+  transformOptions?: TransformOptions
+) {
   const ast = baseParse(template)
   const t = options
     ? createAssetUrlTransformWithOptions(normalizeOptions(options))
@@ -17,7 +27,8 @@ function compileWithAssetUrls(template: string, options?: AssetURLOptions) {
     nodeTransforms: [t, transformElement],
     directiveTransforms: {
       bind: transformBind
-    }
+    },
+    ...transformOptions
   })
   return generate(ast, { mode: 'module' })
 }
@@ -131,4 +142,25 @@ describe('compiler sfc: transform asset url', () => {
 
     expect(code).toMatchSnapshot()
   })
+
+  test('transform with stringify', () => {
+    const { code } = compileWithAssetUrls(
+      `<div>` +
+        `<img src="./bar.png"/>` +
+        `<img src="/bar.png"/>` +
+        `<img src="https://foo.bar/baz.png"/>` +
+        `<img src="//foo.bar/baz.png"/>` +
+        `<img src="./bar.png"/>` +
+        `</div>`,
+      {
+        includeAbsolute: true
+      },
+      {
+        hoistStatic: true,
+        transformHoist: stringifyStatic
+      }
+    )
+    expect(code).toMatch(`_createStaticVNode`)
+    expect(code).toMatchSnapshot()
+  })
 })
diff --git a/packages/compiler-sfc/__tests__/templateTransformSrcset.spec.ts b/packages/compiler-sfc/__tests__/templateTransformSrcset.spec.ts
index 465ca5670..f88706c42 100644
--- a/packages/compiler-sfc/__tests__/templateTransformSrcset.spec.ts
+++ b/packages/compiler-sfc/__tests__/templateTransformSrcset.spec.ts
@@ -1,4 +1,9 @@
-import { generate, baseParse, transform } from '@vue/compiler-core'
+import {
+  generate,
+  baseParse,
+  transform,
+  TransformOptions
+} from '@vue/compiler-core'
 import {
   transformSrcset,
   createSrcsetTransformWithOptions
@@ -9,8 +14,13 @@ import {
   AssetURLOptions,
   normalizeOptions
 } from '../src/templateTransformAssetUrl'
+import { stringifyStatic } from '../../compiler-dom/src/transforms/stringifyStatic'
 
-function compileWithSrcset(template: string, options?: AssetURLOptions) {
+function compileWithSrcset(
+  template: string,
+  options?: AssetURLOptions,
+  transformOptions?: TransformOptions
+) {
   const ast = baseParse(template)
   const srcsetTransform = options
     ? createSrcsetTransformWithOptions(normalizeOptions(options))
@@ -19,7 +29,8 @@ function compileWithSrcset(template: string, options?: AssetURLOptions) {
     nodeTransforms: [srcsetTransform, transformElement],
     directiveTransforms: {
       bind: transformBind
-    }
+    },
+    ...transformOptions
   })
   return generate(ast, { mode: 'module' })
 }
@@ -59,4 +70,19 @@ describe('compiler sfc: transform srcset', () => {
       }).code
     ).toMatchSnapshot()
   })
+
+  test('transform srcset w/ stringify', () => {
+    const code = compileWithSrcset(
+      `<div>${src}</div>`,
+      {
+        includeAbsolute: true
+      },
+      {
+        hoistStatic: true,
+        transformHoist: stringifyStatic
+      }
+    ).code
+    expect(code).toMatch(`_createStaticVNode`)
+    expect(code).toMatchSnapshot()
+  })
 })
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/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts
index b416c37ac..0abe05ce9 100644
--- a/packages/reactivity/__tests__/ref.spec.ts
+++ b/packages/reactivity/__tests__/ref.spec.ts
@@ -35,7 +35,6 @@ describe('reactivity/ref', () => {
     // same value should not trigger
     a.value = 2
     expect(calls).toBe(2)
-    expect(dummy).toBe(2)
   })
 
   it('should make nested properties reactive', () => {
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/runtime-test/package.json b/packages/runtime-test/package.json
index aeab88bd9..16ff28cda 100644
--- a/packages/runtime-test/package.json
+++ b/packages/runtime-test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/runtime-test",
-  "version": "3.2.22",
+  "version": "3.2.23",
   "description": "@vue/runtime-test",
   "private": true,
   "main": "index.js",
@@ -25,7 +25,7 @@
   },
   "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/runtime-test#readme",
   "dependencies": {
-    "@vue/shared": "3.2.22",
-    "@vue/runtime-core": "3.2.22"
+    "@vue/shared": "3.2.23",
+    "@vue/runtime-core": "3.2.23"
   }
 }
diff --git a/packages/vue-compat/__tests__/componentFunctional.spec.ts b/packages/vue-compat/__tests__/componentFunctional.spec.ts
index 82587cbf8..e17534ce2 100644
--- a/packages/vue-compat/__tests__/componentFunctional.spec.ts
+++ b/packages/vue-compat/__tests__/componentFunctional.spec.ts
@@ -18,45 +18,80 @@ afterEach(() => {
   Vue.configureCompat({ MODE: 3 })
 })
 
-test('COMPONENT_FUNCTIONAL', async () => {
-  const func = {
-    name: 'Func',
-    functional: true,
-    props: {
-      x: String
-    },
-    inject: ['foo'],
-    render: (h: any, { data, props, injections, slots }: any) => {
-      return h('div', { id: props.x, class: data.class }, [
-        h('div', { class: 'inject' }, injections.foo),
-        h('div', { class: 'slot' }, slots().default)
-      ])
+describe('COMPONENT_FUNCTIONAL', () => {
+  test('basic usage', async () => {
+    const func = {
+      name: 'Func',
+      functional: true,
+      props: {
+        x: String
+      },
+      inject: ['foo'],
+      render: (h: any, { data, props, injections, slots }: any) => {
+        return h('div', { id: props.x, class: data.class }, [
+          h('div', { class: 'inject' }, injections.foo),
+          h('div', { class: 'slot' }, slots().default)
+        ])
+      }
     }
-  }
 
-  const vm = new Vue({
-    provide() {
-      return {
-        foo: 123
+    const vm = new Vue({
+      provide() {
+        return {
+          foo: 123
+        }
+      },
+      components: {
+        func
+      },
+      template: `<func class="foo" x="foo">hello</func>`
+    }).$mount()
+
+    expect(vm.$el.id).toBe('foo')
+    expect(vm.$el.className).toBe('foo')
+    expect(vm.$el.querySelector('.inject').textContent).toBe('123')
+    expect(vm.$el.querySelector('.slot').textContent).toBe('hello')
+    expect(vm.$el.outerHTML).toMatchInlineSnapshot(
+      `"<div id=\\"foo\\" class=\\"foo\\"><div class=\\"inject\\">123</div><div class=\\"slot\\">hello</div></div>"`
+    )
+
+    expect(
+      (
+        deprecationData[DeprecationTypes.COMPONENT_FUNCTIONAL]
+          .message as Function
+      )(func)
+    ).toHaveBeenWarned()
+  })
+
+  test('copies compatConfig option', () => {
+    const func = {
+      name: 'Func',
+      functional: true,
+      compatConfig: {
+        ATTR_FALSE_VALUE: 'suppress-warning' as const
+      },
+      render: (h: any) => {
+        // should not render required: false due to compatConfig
+        return h('div', { 'data-some-attr': false })
       }
-    },
-    components: {
-      func
-    },
-    template: `<func class="foo" x="foo">hello</func>`
-  }).$mount()
-
-  expect(vm.$el.id).toBe('foo')
-  expect(vm.$el.className).toBe('foo')
-  expect(vm.$el.querySelector('.inject').textContent).toBe('123')
-  expect(vm.$el.querySelector('.slot').textContent).toBe('hello')
-  expect(vm.$el.outerHTML).toMatchInlineSnapshot(
-    `"<div id=\\"foo\\" class=\\"foo\\"><div class=\\"inject\\">123</div><div class=\\"slot\\">hello</div></div>"`
-  )
-
-  expect(
-    (
-      deprecationData[DeprecationTypes.COMPONENT_FUNCTIONAL].message as Function
-    )(func)
-  ).toHaveBeenWarned()
+    }
+
+    const vm = new Vue({
+      components: { func },
+      template: `<func class="foo" x="foo">hello</func>`
+    }).$mount()
+
+    expect(vm.$el.outerHTML).toMatchInlineSnapshot(`"<div></div>"`)
+    expect(
+      (
+        deprecationData[DeprecationTypes.COMPONENT_FUNCTIONAL]
+          .message as Function
+      )(func)
+    ).toHaveBeenWarned()
+    expect(
+      (deprecationData[DeprecationTypes.ATTR_FALSE_VALUE].message as Function)(
+        func
+      )
+    ).not.toHaveBeenWarned()
+  })
 })
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({