Browse Source

chore: Merge branch 'main' into minor

daiwei 5 months ago
parent
commit
8cb33d79d2
29 changed files with 949 additions and 101 deletions
  1. 14 0
      changelogs/CHANGELOG-3.5.md
  2. 35 0
      packages-private/dts-test/defineComponent.test-d.tsx
  3. 58 0
      packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
  4. 19 0
      packages/compiler-core/__tests__/transforms/transformText.spec.ts
  5. 51 1
      packages/compiler-core/__tests__/transforms/vIf.spec.ts
  6. 79 1
      packages/compiler-core/__tests__/transforms/vSlot.spec.ts
  7. 1 9
      packages/compiler-core/src/parser.ts
  8. 11 12
      packages/compiler-core/src/transforms/vIf.ts
  9. 4 10
      packages/compiler-core/src/transforms/vSlot.ts
  10. 21 0
      packages/compiler-core/src/utils.ts
  11. 28 0
      packages/compiler-dom/__tests__/transforms/Transition.spec.ts
  12. 17 0
      packages/compiler-dom/__tests__/transforms/__snapshots__/Transition.spec.ts.snap
  13. 3 4
      packages/compiler-dom/src/transforms/Transition.ts
  14. 4 4
      packages/compiler-ssr/src/transforms/ssrVModel.ts
  15. 13 0
      packages/reactivity/__tests__/readonly.spec.ts
  16. 212 4
      packages/reactivity/__tests__/ref.spec.ts
  17. 45 9
      packages/reactivity/src/arrayInstrumentations.ts
  18. 5 5
      packages/reactivity/src/baseHandlers.ts
  19. 36 7
      packages/reactivity/src/ref.ts
  20. 43 0
      packages/runtime-core/__tests__/apiInject.spec.ts
  21. 163 0
      packages/runtime-core/__tests__/component.spec.ts
  22. 36 0
      packages/runtime-core/__tests__/components/Suspense.spec.ts
  23. 1 1
      packages/runtime-core/src/apiDefineComponent.ts
  24. 5 5
      packages/runtime-core/src/apiInject.ts
  25. 10 6
      packages/runtime-core/src/component.ts
  26. 2 2
      packages/runtime-core/src/componentOptions.ts
  27. 4 10
      packages/runtime-core/src/componentPublicInstance.ts
  28. 6 2
      packages/runtime-core/src/components/Suspense.ts
  29. 23 9
      packages/runtime-dom/src/jsx.ts

+ 14 - 0
changelogs/CHANGELOG-3.5.md

@@ -1,3 +1,17 @@
+## [3.5.25](https://github.com/vuejs/core/compare/v3.5.24...v3.5.25) (2025-11-24)
+
+
+### Bug Fixes
+
+* **compiler:** share logic for comments and whitespace ([#13550](https://github.com/vuejs/core/issues/13550)) ([2214f7a](https://github.com/vuejs/core/commit/2214f7ab2940bcb751cd20130c020d895db6c042))
+* **provide:** warn when using `provide` after mounting ([#13954](https://github.com/vuejs/core/issues/13954)) ([247b2c2](https://github.com/vuejs/core/commit/247b2c2067afc4dee52f9f7bc194f3aab347ac55)), closes [#13921](https://github.com/vuejs/core/issues/13921) [#13924](https://github.com/vuejs/core/issues/13924)
+* **reactivity:** correctly wrap iterated array items to preserve their readonly status ([#14120](https://github.com/vuejs/core/issues/14120)) ([301020b](https://github.com/vuejs/core/commit/301020b481e85d03b0c96000f3221372063c41c6))
+* **reactivity:** toRef edge cases for ref unwrapping ([#12420](https://github.com/vuejs/core/issues/12420)) ([0d2357e](https://github.com/vuejs/core/commit/0d2357e6974678d5484751c869f429dc6ea85582))
+* **runtime-core:** keep options API typing intact when expose is used ([#14118](https://github.com/vuejs/core/issues/14118)) ([8f82f23](https://github.com/vuejs/core/commit/8f82f238463160284e504d1751d61b72dabb395e)), closes [#14117](https://github.com/vuejs/core/issues/14117) [vuejs/language-tools#5069](https://github.com/vuejs/language-tools/issues/5069)
+* **suspense:** defer clearing fallback vnode el in case it has dirs ([#14080](https://github.com/vuejs/core/issues/14080)) ([c0f63dd](https://github.com/vuejs/core/commit/c0f63ddbfa8fa221d66b683b5c26e471851c2b50)), closes [#14078](https://github.com/vuejs/core/issues/14078)
+
+
+
 ## [3.5.24](https://github.com/vuejs/core/compare/v3.5.23...v3.5.24) (2025-11-07)
 
 

+ 35 - 0
packages-private/dts-test/defineComponent.test-d.tsx

@@ -2107,3 +2107,38 @@ defineComponent({
     expectType<string>(this.$props)
   },
 })
+
+// #14117
+defineComponent({
+  setup() {
+    const setup1 = ref('setup1')
+    const setup2 = ref('setup2')
+    return { setup1, setup2 }
+  },
+  data() {
+    return {
+      data1: 1,
+    }
+  },
+  props: {
+    props1: {
+      type: String,
+    },
+  },
+  methods: {
+    methods1() {
+      return `methods1`
+    },
+  },
+  computed: {
+    computed1() {
+      this.setup1
+      this.setup2
+      this.data1
+      this.props1
+      this.methods1()
+      return `computed1`
+    },
+  },
+  expose: ['setup1'],
+})

+ 58 - 0
packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap

@@ -139,6 +139,24 @@ return function render(_ctx, _cache) {
 }"
 `;
 
+exports[`compiler: transform component slots > named slots w/ implicit default slot containing non-breaking space 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+  with (_ctx) {
+    const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+
+    const _component_Comp = _resolveComponent("Comp")
+
+    return (_openBlock(), _createBlock(_component_Comp, null, {
+      one: _withCtx(() => ["foo"]),
+      default: _withCtx(() => ["   "]),
+      _: 1 /* STABLE */
+    }))
+  }
+}"
+`;
+
 exports[`compiler: transform component slots > nested slots scoping 1`] = `
 "const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
 
@@ -232,6 +250,20 @@ return function render(_ctx, _cache) {
 }"
 `;
 
+exports[`compiler: transform component slots > with whitespace: 'preserve' > implicit default slot with non-breaking space 1`] = `
+"const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue
+
+return function render(_ctx, _cache) {
+  const _component_Comp = _resolveComponent("Comp")
+
+  return (_openBlock(), _createBlock(_component_Comp, null, {
+    header: _withCtx(() => [" Header "]),
+    default: _withCtx(() => ["\\n         \\n        "]),
+    _: 1 /* STABLE */
+  }))
+}"
+`;
+
 exports[`compiler: transform component slots > with whitespace: 'preserve' > named default slot + implicit whitespace content 1`] = `
 "const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue
 
@@ -268,6 +300,32 @@ return function render(_ctx, _cache) {
 }"
 `;
 
+exports[`compiler: transform component slots > with whitespace: 'preserve' > named slot with v-if + v-else and comments 1`] = `
+"const { createTextVNode: _createTextVNode, createCommentVNode: _createCommentVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, openBlock: _openBlock, createBlock: _createBlock } = Vue
+
+return function render(_ctx, _cache) {
+  const _component_Comp = _resolveComponent("Comp")
+
+  return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 2 /* DYNAMIC */ }, [
+    ok
+      ? {
+          name: "one",
+          fn: _withCtx(() => [
+            _createTextVNode("foo")
+          ]),
+          key: "0"
+        }
+      : {
+          name: "two",
+          fn: _withCtx(() => [
+            _createTextVNode("baz")
+          ]),
+          key: "1"
+        }
+  ]), 1024 /* DYNAMIC_SLOTS */))
+}"
+`;
+
 exports[`compiler: transform component slots > with whitespace: 'preserve' > should not generate whitespace only default slot 1`] = `
 "const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue
 

+ 19 - 0
packages/compiler-core/__tests__/transforms/transformText.spec.ts

@@ -4,6 +4,7 @@ import {
   type ForNode,
   NodeTypes,
   generate,
+  isWhitespaceText,
   baseParse as parse,
   transform,
 } from '../../src'
@@ -109,6 +110,24 @@ describe('compiler: transform text', () => {
     expect(generate(root).code).toMatchSnapshot()
   })
 
+  test('whitespace text', () => {
+    const root = transformWithTextOpt(`<div/>hello<div/>  <div/>`)
+    expect(root.children.length).toBe(5)
+    expect(root.children[0].type).toBe(NodeTypes.ELEMENT)
+    expect(root.children[1].type).toBe(NodeTypes.TEXT_CALL)
+    expect(root.children[2].type).toBe(NodeTypes.ELEMENT)
+    expect(root.children[3].type).toBe(NodeTypes.TEXT_CALL)
+    expect(root.children[4].type).toBe(NodeTypes.ELEMENT)
+
+    expect(root.children.map(isWhitespaceText)).toEqual([
+      false,
+      false,
+      false,
+      true,
+      false,
+    ])
+  })
+
   test('consecutive text mixed with elements', () => {
     const root = transformWithTextOpt(
       `<div/>{{ foo }} bar {{ baz }}<div/>hello<div/>`,

+ 51 - 1
packages/compiler-core/__tests__/transforms/vIf.spec.ts

@@ -266,6 +266,31 @@ describe('compiler: v-if', () => {
           loc: node3.loc,
         },
       ])
+
+      const { node: node4 } = parseWithIfTransform(
+        `<div v-if="bar"/>foo<div v-else/>`,
+        { onError },
+        2,
+      )
+      expect(onError.mock.calls[3]).toMatchObject([
+        {
+          code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
+          loc: node4.loc,
+        },
+      ])
+
+      // Non-breaking space
+      const { node: node5 } = parseWithIfTransform(
+        `<div v-if="bar"/>\u00a0<div v-else/>`,
+        { onError },
+        2,
+      )
+      expect(onError.mock.calls[4]).toMatchObject([
+        {
+          code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
+          loc: node5.loc,
+        },
+      ])
     })
 
     test('error on v-else-if missing adjacent v-if or v-else-if', () => {
@@ -305,6 +330,31 @@ describe('compiler: v-if', () => {
         },
       ])
 
+      const { node: node4 } = parseWithIfTransform(
+        `<div v-if="bar"/>foo<div v-else-if="foo"/>`,
+        { onError },
+        2,
+      )
+      expect(onError.mock.calls[3]).toMatchObject([
+        {
+          code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
+          loc: node4.loc,
+        },
+      ])
+
+      // Non-breaking space
+      const { node: node5 } = parseWithIfTransform(
+        `<div v-if="bar"/>\u00a0<div v-else-if="foo"/>`,
+        { onError },
+        2,
+      )
+      expect(onError.mock.calls[4]).toMatchObject([
+        {
+          code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
+          loc: node5.loc,
+        },
+      ])
+
       const {
         node: { branches },
       } = parseWithIfTransform(
@@ -313,7 +363,7 @@ describe('compiler: v-if', () => {
         0,
       )
 
-      expect(onError.mock.calls[3]).toMatchObject([
+      expect(onError.mock.calls[5]).toMatchObject([
         {
           code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
           loc: branches[branches.length - 1].loc,

+ 79 - 1
packages/compiler-core/__tests__/transforms/vSlot.spec.ts

@@ -28,8 +28,12 @@ import { createObjectMatcher } from '../testUtils'
 import { PatchFlags } from '@vue/shared'
 import { transformFor } from '../../src/transforms/vFor'
 import { transformIf } from '../../src/transforms/vIf'
+import { transformText } from '../../src/transforms/transformText'
 
-function parseWithSlots(template: string, options: CompilerOptions = {}) {
+function parseWithSlots(
+  template: string,
+  options: CompilerOptions & { transformText?: boolean } = {},
+) {
   const ast = parse(template, {
     whitespace: options.whitespace,
   })
@@ -43,6 +47,7 @@ function parseWithSlots(template: string, options: CompilerOptions = {}) {
       transformSlotOutlet,
       transformElement,
       trackSlotScopes,
+      ...(options.transformText ? [transformText] : []),
     ],
     directiveTransforms: {
       on: transformOn,
@@ -307,6 +312,40 @@ describe('compiler: transform component slots', () => {
     expect(generate(root).code).toMatchSnapshot()
   })
 
+  test('named slots w/ implicit default slot containing non-breaking space', () => {
+    const { root, slots } = parseWithSlots(
+      `<Comp>
+        \u00a0
+        <template #one>foo</template>
+      </Comp>`,
+    )
+    expect(slots).toMatchObject(
+      createSlotMatcher({
+        one: {
+          type: NodeTypes.JS_FUNCTION_EXPRESSION,
+          params: undefined,
+          returns: [
+            {
+              type: NodeTypes.TEXT,
+              content: `foo`,
+            },
+          ],
+        },
+        default: {
+          type: NodeTypes.JS_FUNCTION_EXPRESSION,
+          params: undefined,
+          returns: [
+            {
+              type: NodeTypes.TEXT,
+              content: ` \u00a0 `,
+            },
+          ],
+        },
+      }),
+    )
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
   test('dynamically named slots', () => {
     const { root, slots } = parseWithSlots(
       `<Comp>
@@ -1011,6 +1050,27 @@ describe('compiler: transform component slots', () => {
       expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
     })
 
+    test('implicit default slot with non-breaking space', () => {
+      const source = `
+      <Comp>
+        &nbsp;
+        <template #header> Header </template>
+      </Comp>
+      `
+      const { root } = parseWithSlots(source, {
+        whitespace: 'preserve',
+      })
+
+      const slots = (root as any).children[0].codegenNode.children
+        .properties as ObjectExpression['properties']
+
+      expect(
+        slots.some(p => (p.key as SimpleExpressionNode).content === 'default'),
+      ).toBe(true)
+
+      expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
+    })
+
     test('named slot with v-if + v-else', () => {
       const source = `
         <Comp>
@@ -1024,5 +1084,23 @@ describe('compiler: transform component slots', () => {
 
       expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
     })
+
+    test('named slot with v-if + v-else and comments', () => {
+      const source = `
+        <Comp>
+          <template #one v-if="ok">foo</template>
+          <!-- start -->
+
+          <!-- end -->
+          <template #two v-else>baz</template>
+        </Comp>
+      `
+      const { root } = parseWithSlots(source, {
+        transformText: true,
+        whitespace: 'preserve',
+      })
+
+      expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
+    })
   })
 })

+ 1 - 9
packages/compiler-core/src/parser.ts

@@ -40,6 +40,7 @@ import {
 } from './errors'
 import {
   forAliasRE,
+  isAllWhitespace,
   isCoreComponent,
   isSimpleIdentifier,
   isStaticArgOf,
@@ -881,15 +882,6 @@ function condenseWhitespace(nodes: TemplateChildNode[]): TemplateChildNode[] {
   return removedWhitespace ? nodes.filter(Boolean) : nodes
 }
 
-function isAllWhitespace(str: string) {
-  for (let i = 0; i < str.length; i++) {
-    if (!isWhitespace(str.charCodeAt(i))) {
-      return false
-    }
-  }
-  return true
-}
-
 function hasNewlineChar(str: string) {
   for (let i = 0; i < str.length; i++) {
     const c = str.charCodeAt(i)

+ 11 - 12
packages/compiler-core/src/transforms/vIf.ts

@@ -32,7 +32,13 @@ import { processExpression } from './transformExpression'
 import { validateBrowserExpression } from '../validateExpression'
 import { cloneLoc } from '../parser'
 import { CREATE_COMMENT, FRAGMENT } from '../runtimeHelpers'
-import { findDir, findProp, getMemoedVNodeCall, injectProp } from '../utils'
+import {
+  findDir,
+  findProp,
+  getMemoedVNodeCall,
+  injectProp,
+  isCommentOrWhitespace,
+} from '../utils'
 import { PatchFlags } from '@vue/shared'
 
 export const transformIf: NodeTransform = createStructuralDirectiveTransform(
@@ -125,18 +131,11 @@ export function processIf(
     let i = siblings.indexOf(node)
     while (i-- >= -1) {
       const sibling = siblings[i]
-      if (sibling && sibling.type === NodeTypes.COMMENT) {
-        context.removeNode(sibling)
-        __DEV__ && comments.unshift(sibling)
-        continue
-      }
-
-      if (
-        sibling &&
-        sibling.type === NodeTypes.TEXT &&
-        !sibling.content.trim().length
-      ) {
+      if (sibling && isCommentOrWhitespace(sibling)) {
         context.removeNode(sibling)
+        if (__DEV__ && sibling.type === NodeTypes.COMMENT) {
+          comments.unshift(sibling)
+        }
         continue
       }
 

+ 4 - 10
packages/compiler-core/src/transforms/vSlot.ts

@@ -26,9 +26,11 @@ import {
   assert,
   findDir,
   hasScopeRef,
+  isCommentOrWhitespace,
   isStaticExp,
   isTemplateNode,
   isVSlot,
+  isWhitespaceText,
 } from '../utils'
 import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
 import { createForLoopParams, finalizeForParseResult } from './vFor'
@@ -230,7 +232,7 @@ export function buildSlots(
       let prev
       while (j--) {
         prev = children[j]
-        if (prev.type !== NodeTypes.COMMENT && isNonWhitespaceContent(prev)) {
+        if (!isCommentOrWhitespace(prev)) {
           break
         }
       }
@@ -327,7 +329,7 @@ export function buildSlots(
       // #3766
       // with whitespace: 'preserve', whitespaces between slots will end up in
       // implicitDefaultChildren. Ignore if all implicit children are whitespaces.
-      implicitDefaultChildren.some(node => isNonWhitespaceContent(node))
+      !implicitDefaultChildren.every(isWhitespaceText)
     ) {
       // implicit default slot (mixed with named slots)
       if (hasNamedDefaultSlot) {
@@ -419,11 +421,3 @@ function hasForwardedSlots(children: TemplateChildNode[]): boolean {
   }
   return false
 }
-
-function isNonWhitespaceContent(node: TemplateChildNode): boolean {
-  if (node.type !== NodeTypes.TEXT && node.type !== NodeTypes.TEXT_CALL)
-    return true
-  return node.type === NodeTypes.TEXT
-    ? !!node.content.trim()
-    : isNonWhitespaceContent(node.content)
-}

+ 21 - 0
packages/compiler-core/src/utils.ts

@@ -43,6 +43,7 @@ import type { PropsExpression } from './transforms/transformElement'
 import { parseExpression } from '@babel/parser'
 import type { Expression, Node } from '@babel/types'
 import { unwrapTSNode } from './babelUtils'
+import { isWhitespace } from './tokenizer'
 
 export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
   p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
@@ -604,3 +605,23 @@ export function isSingleIfBlock(parent: ParentNode): boolean {
 }
 
 export const forAliasRE: RegExp = /([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)/
+
+export function isAllWhitespace(str: string): boolean {
+  for (let i = 0; i < str.length; i++) {
+    if (!isWhitespace(str.charCodeAt(i))) {
+      return false
+    }
+  }
+  return true
+}
+
+export function isWhitespaceText(node: TemplateChildNode): boolean {
+  return (
+    (node.type === NodeTypes.TEXT && isAllWhitespace(node.content)) ||
+    (node.type === NodeTypes.TEXT_CALL && isWhitespaceText(node.content))
+  )
+}
+
+export function isCommentOrWhitespace(node: TemplateChildNode): boolean {
+  return node.type === NodeTypes.COMMENT || isWhitespaceText(node)
+}

+ 28 - 0
packages/compiler-dom/__tests__/transforms/Transition.spec.ts

@@ -135,6 +135,18 @@ describe('Transition multi children warnings', () => {
       false,
     )
   })
+
+  test('non-breaking spaces are treated as normal text', () => {
+    checkWarning(
+      `
+      <transition>
+        \u00a0
+        <div>foo</div>
+      </transition>
+      `,
+      true,
+    )
+  })
 })
 
 test('inject persisted when child has v-show', () => {
@@ -164,3 +176,19 @@ test('the v-if/else-if/else branches in Transition should ignore comments', () =
     `).code,
   ).toMatchSnapshot()
 })
+
+test('comments and preserved whitespace are ignored', () => {
+  expect(
+    compile(
+      `
+      <transition>
+        <!-- foo --> <!-- bar -->
+        <div>foo bar</div>
+      </transition>
+      `,
+      {
+        whitespace: 'preserve',
+      },
+    ).code,
+  ).toMatchSnapshot()
+})

+ 17 - 0
packages/compiler-dom/__tests__/transforms/__snapshots__/Transition.spec.ts.snap

@@ -1,5 +1,22 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
+exports[`comments and preserved whitespace are ignored 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+  with (_ctx) {
+    const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, Transition: _Transition, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+
+    return (_openBlock(), _createBlock(_Transition, null, {
+      default: _withCtx(() => [
+        _createElementVNode("div", null, "foo bar")
+      ]),
+      _: 1 /* STABLE */
+    }))
+  }
+}"
+`;
+
 exports[`inject persisted when child has v-show 1`] = `
 "const _Vue = Vue
 

+ 3 - 4
packages/compiler-dom/src/transforms/Transition.ts

@@ -5,6 +5,7 @@ import {
   type IfBranchNode,
   type NodeTransform,
   NodeTypes,
+  isCommentOrWhitespace,
 } from '@vue/compiler-core'
 import { TRANSITION } from '../runtimeHelpers'
 import { DOMErrorCodes, createDOMCompilerError } from '../errors'
@@ -65,11 +66,9 @@ export function postTransformTransition(
 function defaultHasMultipleChildren(
   node: ComponentNode | IfBranchNode,
 ): boolean {
-  // #1352 filter out potential comment nodes.
+  // filter out potential comment nodes (#1352) and whitespace (#4637)
   const children = (node.children = node.children.filter(
-    c =>
-      c.type !== NodeTypes.COMMENT &&
-      !(c.type === NodeTypes.TEXT && !c.content.trim()),
+    c => !isCommentOrWhitespace(c),
   ))
   const child = children[0]
   return (

+ 4 - 4
packages/compiler-ssr/src/transforms/ssrVModel.ts

@@ -83,11 +83,11 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
 
   if (node.tagType === ElementTypes.ELEMENT) {
     const res: DirectiveTransformResult = { props: [] }
-    const defaultProps = [
-      // default value binding for text type inputs
-      createObjectProperty(`value`, model),
-    ]
     if (node.tag === 'input') {
+      const defaultProps = [
+        // default value binding for text type inputs
+        createObjectProperty(`value`, model),
+      ]
       const type = findProp(node, 'type')
       if (type) {
         const value = findValueBinding(node)

+ 13 - 0
packages/reactivity/__tests__/readonly.spec.ts

@@ -172,6 +172,19 @@ describe('reactivity/readonly', () => {
       expect(dummy).toBe(1)
       expect(`target is readonly`).toHaveBeenWarnedTimes(2)
     })
+
+    it('should maintain identity when iterating readonly ref array', () => {
+      const list = readonly(ref([{}, {}, {}]))
+      const computedList = computed(() => {
+        const newList: any[] = []
+        list.value.forEach(x => newList.push(x))
+        return newList
+      })
+
+      expect(list.value[0]).toBe(computedList.value[0])
+      expect(isReadonly(computedList.value[0])).toBe(true)
+      expect(isReactive(computedList.value[0])).toBe(true)
+    })
   })
 
   const maps = [Map, WeakMap]

+ 212 - 4
packages/reactivity/__tests__/ref.spec.ts

@@ -16,6 +16,7 @@ import {
   isShallow,
   readonly,
   shallowReactive,
+  shallowReadonly,
 } from '../src/reactive'
 
 describe('reactivity/ref', () => {
@@ -308,18 +309,83 @@ describe('reactivity/ref', () => {
     a.x = 4
     expect(dummyX).toBe(4)
 
-    // should keep ref
-    const r = { x: ref(1) }
-    expect(toRef(r, 'x')).toBe(r.x)
+    // a ref in a non-reactive object should be unwrapped
+    const r: any = { x: ref(1) }
+    const t = toRef(r, 'x')
+    expect(t.value).toBe(1)
+
+    r.x.value = 2
+    expect(t.value).toBe(2)
+
+    t.value = 3
+    expect(t.value).toBe(3)
+    expect(r.x.value).toBe(3)
+
+    // with a default
+    const u = toRef(r, 'x', 7)
+    expect(u.value).toBe(3)
+
+    r.x.value = undefined
+    expect(r.x.value).toBeUndefined()
+    expect(t.value).toBeUndefined()
+    expect(u.value).toBe(7)
+
+    u.value = 7
+    expect(r.x.value).toBe(7)
+    expect(t.value).toBe(7)
+    expect(u.value).toBe(7)
   })
 
   test('toRef on array', () => {
-    const a = reactive(['a', 'b'])
+    const a: any = reactive(['a', 'b'])
     const r = toRef(a, 1)
     expect(r.value).toBe('b')
     r.value = 'c'
     expect(r.value).toBe('c')
     expect(a[1]).toBe('c')
+
+    a[1] = ref('d')
+    expect(isRef(a[1])).toBe(true)
+    expect(r.value).toBe('d')
+    r.value = 'e'
+    expect(isRef(a[1])).toBe(true)
+    expect(a[1].value).toBe('e')
+
+    const s = toRef(a, 2, 'def')
+    const len = toRef(a, 'length')
+
+    expect(s.value).toBe('def')
+    expect(len.value).toBe(2)
+
+    a.push('f')
+    expect(s.value).toBe('f')
+    expect(len.value).toBe(3)
+
+    len.value = 2
+
+    expect(s.value).toBe('def')
+    expect(len.value).toBe(2)
+
+    const symbol = Symbol()
+    const t = toRef(a, 'foo')
+    const u = toRef(a, symbol)
+    expect(t.value).toBeUndefined()
+    expect(u.value).toBeUndefined()
+
+    const foo = ref(3)
+    const bar = ref(5)
+    a.foo = foo
+    a[symbol] = bar
+    expect(t.value).toBe(3)
+    expect(u.value).toBe(5)
+
+    t.value = 4
+    u.value = 6
+
+    expect(a.foo).toBe(4)
+    expect(foo.value).toBe(4)
+    expect(a[symbol]).toBe(6)
+    expect(bar.value).toBe(6)
   })
 
   test('toRef default value', () => {
@@ -345,6 +411,148 @@ describe('reactivity/ref', () => {
     expect(isReadonly(x)).toBe(true)
   })
 
+  test('toRef lazy evaluation of properties inside a proxy', () => {
+    const fn = vi.fn(() => 5)
+    const num = computed(fn)
+    const a = toRef({ num }, 'num')
+    const b = toRef(reactive({ num }), 'num')
+    const c = toRef(readonly({ num }), 'num')
+    const d = toRef(shallowReactive({ num }), 'num')
+    const e = toRef(shallowReadonly({ num }), 'num')
+
+    expect(fn).not.toHaveBeenCalled()
+
+    expect(a.value).toBe(5)
+    expect(b.value).toBe(5)
+    expect(c.value).toBe(5)
+    expect(d.value).toBe(5)
+    expect(e.value).toBe(5)
+    expect(fn).toHaveBeenCalledTimes(1)
+  })
+
+  test('toRef with shallowReactive/shallowReadonly', () => {
+    const r = ref(0)
+    const s1 = shallowReactive<{ foo: any }>({ foo: r })
+    const t1 = toRef(s1, 'foo', 2)
+    const s2 = shallowReadonly(s1)
+    const t2 = toRef(s2, 'foo', 3)
+
+    expect(r.value).toBe(0)
+    expect(s1.foo.value).toBe(0)
+    expect(t1.value).toBe(0)
+    expect(s2.foo.value).toBe(0)
+    expect(t2.value).toBe(0)
+
+    s1.foo = ref(1)
+
+    expect(r.value).toBe(0)
+    expect(s1.foo.value).toBe(1)
+    expect(t1.value).toBe(1)
+    expect(s2.foo.value).toBe(1)
+    expect(t2.value).toBe(1)
+
+    s1.foo.value = undefined
+
+    expect(r.value).toBe(0)
+    expect(s1.foo.value).toBeUndefined()
+    expect(t1.value).toBe(2)
+    expect(s2.foo.value).toBeUndefined()
+    expect(t2.value).toBe(3)
+
+    t1.value = 2
+
+    expect(r.value).toBe(0)
+    expect(s1.foo.value).toBe(2)
+    expect(t1.value).toBe(2)
+    expect(s2.foo.value).toBe(2)
+    expect(t2.value).toBe(2)
+
+    t2.value = 4
+
+    expect(r.value).toBe(0)
+    expect(s1.foo.value).toBe(4)
+    expect(t1.value).toBe(4)
+    expect(s2.foo.value).toBe(4)
+    expect(t2.value).toBe(4)
+
+    s1.foo = undefined
+
+    expect(r.value).toBe(0)
+    expect(s1.foo).toBeUndefined()
+    expect(t1.value).toBe(2)
+    expect(s2.foo).toBeUndefined()
+    expect(t2.value).toBe(3)
+  })
+
+  test('toRef for shallowReadonly around reactive', () => {
+    const get = vi.fn(() => 3)
+    const set = vi.fn()
+    const num = computed({ get, set })
+    const t = toRef(shallowReadonly(reactive({ num })), 'num')
+
+    expect(get).not.toHaveBeenCalled()
+    expect(set).not.toHaveBeenCalled()
+
+    t.value = 1
+
+    expect(
+      'Set operation on key "num" failed: target is readonly',
+    ).toHaveBeenWarned()
+
+    expect(get).not.toHaveBeenCalled()
+    expect(set).not.toHaveBeenCalled()
+
+    expect(t.value).toBe(3)
+
+    expect(get).toHaveBeenCalledTimes(1)
+    expect(set).not.toHaveBeenCalled()
+  })
+
+  test('toRef for readonly around shallowReactive', () => {
+    const get = vi.fn(() => 3)
+    const set = vi.fn()
+    const num = computed({ get, set })
+    const t: Ref<number> = toRef(readonly(shallowReactive({ num })), 'num')
+
+    expect(get).not.toHaveBeenCalled()
+    expect(set).not.toHaveBeenCalled()
+
+    t.value = 1
+
+    expect(
+      'Set operation on key "num" failed: target is readonly',
+    ).toHaveBeenWarned()
+
+    expect(get).not.toHaveBeenCalled()
+    expect(set).not.toHaveBeenCalled()
+
+    expect(t.value).toBe(3)
+
+    expect(get).toHaveBeenCalledTimes(1)
+    expect(set).not.toHaveBeenCalled()
+  })
+
+  test(`toRef doesn't bypass the proxy when getting/setting a nested ref`, () => {
+    const r = ref(2)
+    const obj = shallowReactive({ num: r })
+    const t = toRef(obj, 'num')
+
+    expect(t.value).toBe(2)
+
+    effect(() => {
+      t.value = 3
+    })
+
+    expect(t.value).toBe(3)
+    expect(r.value).toBe(3)
+
+    const s = ref(4)
+    obj.num = s
+
+    expect(t.value).toBe(3)
+    expect(s.value).toBe(3)
+  })
+
   test('toRefs', () => {
     const a = reactive({
       x: 1,

+ 45 - 9
packages/reactivity/src/arrayInstrumentations.ts

@@ -1,7 +1,15 @@
 import { isArray } from '@vue/shared'
 import { TrackOpTypes } from './constants'
 import { ARRAY_ITERATE_KEY, track } from './dep'
-import { isProxy, isShallow, toRaw, toReactive } from './reactive'
+import {
+  isProxy,
+  isReactive,
+  isReadonly,
+  isShallow,
+  toRaw,
+  toReactive,
+  toReadonly,
+} from './reactive'
 import { endBatch, setActiveSub, startBatch } from './system'
 
 /**
@@ -24,11 +32,18 @@ export function shallowReadArray<T>(arr: T[]): T[] {
   return arr
 }
 
+function toWrapped(target: unknown, item: unknown) {
+  if (isReadonly(target)) {
+    return isReactive(target) ? toReadonly(toReactive(item)) : toReadonly(item)
+  }
+  return toReactive(item)
+}
+
 export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
   __proto__: null,
 
   [Symbol.iterator]() {
-    return iterator(this, Symbol.iterator, toReactive)
+    return iterator(this, Symbol.iterator, item => toWrapped(this, item))
   },
 
   concat(...args: unknown[]) {
@@ -39,7 +54,7 @@ export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
 
   entries() {
     return iterator(this, 'entries', (value: [number, unknown]) => {
-      value[1] = toReactive(value[1])
+      value[1] = toWrapped(this, value[1])
       return value
     })
   },
@@ -55,14 +70,28 @@ export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
     fn: (item: unknown, index: number, array: unknown[]) => unknown,
     thisArg?: unknown,
   ) {
-    return apply(this, 'filter', fn, thisArg, v => v.map(toReactive), arguments)
+    return apply(
+      this,
+      'filter',
+      fn,
+      thisArg,
+      v => v.map((item: unknown) => toWrapped(this, item)),
+      arguments,
+    )
   },
 
   find(
     fn: (item: unknown, index: number, array: unknown[]) => boolean,
     thisArg?: unknown,
   ) {
-    return apply(this, 'find', fn, thisArg, toReactive, arguments)
+    return apply(
+      this,
+      'find',
+      fn,
+      thisArg,
+      item => toWrapped(this, item),
+      arguments,
+    )
   },
 
   findIndex(
@@ -76,7 +105,14 @@ export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
     fn: (item: unknown, index: number, array: unknown[]) => boolean,
     thisArg?: unknown,
   ) {
-    return apply(this, 'findLast', fn, thisArg, toReactive, arguments)
+    return apply(
+      this,
+      'findLast',
+      fn,
+      thisArg,
+      item => toWrapped(this, item),
+      arguments,
+    )
   },
 
   findLastIndex(
@@ -189,7 +225,7 @@ export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
   },
 
   values() {
-    return iterator(this, 'values', toReactive)
+    return iterator(this, 'values', item => toWrapped(this, item))
   },
 }
 
@@ -257,7 +293,7 @@ function apply(
   if (arr !== self) {
     if (needsWrap) {
       wrappedFn = function (this: unknown, item, index) {
-        return fn.call(this, toReactive(item), index, self)
+        return fn.call(this, toWrapped(self, item), index, self)
       }
     } else if (fn.length > 2) {
       wrappedFn = function (this: unknown, item, index) {
@@ -281,7 +317,7 @@ function reduce(
   if (arr !== self) {
     if (!isShallow(self)) {
       wrappedFn = function (this: unknown, acc, item, index) {
-        return fn.call(this, acc, toReactive(item), index, self)
+        return fn.call(this, acc, toWrapped(self, item), index, self)
       }
     } else if (fn.length > 3) {
       wrappedFn = function (this: unknown, acc, item, index) {

+ 5 - 5
packages/reactivity/src/baseHandlers.ts

@@ -151,13 +151,14 @@ class MutableReactiveHandler extends BaseReactiveHandler {
     receiver: object,
   ): boolean {
     let oldValue = target[key]
+    const isArrayWithIntegerKey = isArray(target) && isIntegerKey(key)
     if (!this._isShallow) {
       const isOldValueReadonly = isReadonly(oldValue)
       if (!isShallow(value) && !isReadonly(value)) {
         oldValue = toRaw(oldValue)
         value = toRaw(value)
       }
-      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
+      if (!isArrayWithIntegerKey && isRef(oldValue) && !isRef(value)) {
         if (isOldValueReadonly) {
           if (__DEV__) {
             warn(
@@ -175,10 +176,9 @@ class MutableReactiveHandler extends BaseReactiveHandler {
       // in shallow mode, objects are set as-is regardless of reactive or not
     }
 
-    const hadKey =
-      isArray(target) && isIntegerKey(key)
-        ? Number(key) < target.length
-        : hasOwn(target, key)
+    const hadKey = isArrayWithIntegerKey
+      ? Number(key) < target.length
+      : hasOwn(target, key)
     const result = Reflect.set(
       target,
       key,

+ 36 - 7
packages/reactivity/src/ref.ts

@@ -3,6 +3,7 @@ import {
   hasChanged,
   isArray,
   isFunction,
+  isIntegerKey,
   isObject,
 } from '@vue/shared'
 import type { ComputedRef, WritableComputedRef } from './computed'
@@ -12,6 +13,8 @@ import { getDepFromReactive } from './dep'
 import {
   type Builtin,
   type ShallowReactiveMarker,
+  type Target,
+  isProxy,
   isReactive,
   isReadonly,
   isShallow,
@@ -405,23 +408,52 @@ class ObjectRefImpl<T extends object, K extends keyof T> {
   public readonly [ReactiveFlags.IS_REF] = true
   public _value: T[K] = undefined!
 
+  private readonly _raw: T
+  private readonly _shallow: boolean
+
   constructor(
     private readonly _object: T,
     private readonly _key: K,
     private readonly _defaultValue?: T[K],
-  ) {}
+  ) {
+    this._raw = toRaw(_object)
+
+    let shallow = true
+    let obj = _object
+
+    // For an array with integer key, refs are not unwrapped
+    if (!isArray(_object) || !isIntegerKey(String(_key))) {
+      // Otherwise, check each proxy layer for unwrapping
+      do {
+        shallow = !isProxy(obj) || isShallow(obj)
+      } while (shallow && (obj = (obj as Target)[ReactiveFlags.RAW]))
+    }
+
+    this._shallow = shallow
+  }
 
   get value() {
-    const val = this._object[this._key]
+    let val = this._object[this._key]
+    if (this._shallow) {
+      val = unref(val)
+    }
     return (this._value = val === undefined ? this._defaultValue! : val)
   }
 
   set value(newVal) {
+    if (this._shallow && isRef(this._raw[this._key])) {
+      const nestedRef = this._object[this._key]
+      if (isRef(nestedRef)) {
+        nestedRef.value = newVal
+        return
+      }
+    }
+
     this._object[this._key] = newVal
   }
 
   get dep(): ReactiveNode | undefined {
-    return getDepFromReactive(toRaw(this._object), this._key)
+    return getDepFromReactive(this._raw, this._key)
   }
 }
 
@@ -518,10 +550,7 @@ function propertyToRef(
   key: string,
   defaultValue?: unknown,
 ) {
-  const val = source[key]
-  return isRef(val)
-    ? val
-    : (new ObjectRefImpl(source, key, defaultValue) as any)
+  return new ObjectRefImpl(source, key, defaultValue) as any
 }
 
 /**

+ 43 - 0
packages/runtime-core/__tests__/apiInject.spec.ts

@@ -6,6 +6,7 @@ import {
   hasInjectionContext,
   inject,
   nextTick,
+  onMounted,
   provide,
   reactive,
   readonly,
@@ -372,4 +373,46 @@ describe('api: provide/inject', () => {
       })
     })
   })
+
+  describe('warnings for incorrect usage', () => {
+    it('should warn when inject() is called outside setup', () => {
+      inject('foo', 'bar')
+      expect(`inject() can only be used`).toHaveBeenWarned()
+    })
+
+    it('should warn when provide() is called outside setup', () => {
+      provide('foo', 'bar')
+      expect(`provide() can only be used`).toHaveBeenWarned()
+    })
+
+    it('should warn when provide() is called from a render function', () => {
+      const Provider = {
+        setup() {
+          return () => {
+            provide('foo', 'bar')
+          }
+        },
+      }
+
+      const root = nodeOps.createElement('div')
+      render(h(Provider), root)
+      expect(`provide() can only be used`).toHaveBeenWarned()
+    })
+
+    it('should warn when provide() is called from onMounted', () => {
+      const Provider = {
+        setup() {
+          onMounted(() => {
+            provide('foo', 'bar')
+          })
+
+          return () => null
+        },
+      }
+
+      const root = nodeOps.createElement('div')
+      render(h(Provider), root)
+      expect(`provide() can only be used`).toHaveBeenWarned()
+    })
+  })
 })

+ 163 - 0
packages/runtime-core/__tests__/component.spec.ts

@@ -0,0 +1,163 @@
+import {
+  type ComponentInternalInstance,
+  getCurrentInstance,
+  h,
+  nodeOps,
+  render,
+} from '@vue/runtime-test'
+import { type GenericComponent, formatComponentName } from '../src/component'
+
+describe('formatComponentName', () => {
+  test('default name', () => {
+    let instance: ComponentInternalInstance | null = null
+    const Comp = {
+      setup() {
+        instance = getCurrentInstance()
+        return () => null
+      },
+    } as GenericComponent
+    render(h(Comp), nodeOps.createElement('div'))
+
+    expect(formatComponentName(null, Comp)).toBe('Anonymous')
+    expect(formatComponentName(null, Comp, true)).toBe('App')
+    expect(formatComponentName(instance, Comp)).toBe('Anonymous')
+    expect(formatComponentName(instance, Comp, true)).toBe('App')
+  })
+
+  test('name option', () => {
+    let instance: ComponentInternalInstance | null = null
+    const Comp = {
+      name: 'number-input',
+      setup() {
+        instance = getCurrentInstance()
+        return () => null
+      },
+    }
+    render(h(Comp), nodeOps.createElement('div'))
+
+    expect(formatComponentName(null, Comp)).toBe('NumberInput')
+    expect(formatComponentName(instance, Comp, true)).toBe('NumberInput')
+  })
+
+  test('self recursive name', () => {
+    let instance: ComponentInternalInstance | null = null
+    const Comp = {
+      components: {} as any,
+      setup() {
+        instance = getCurrentInstance()
+        return () => null
+      },
+    }
+    Comp.components.ToggleButton = Comp
+    render(h(Comp), nodeOps.createElement('div'))
+
+    expect(formatComponentName(instance, Comp as GenericComponent)).toBe(
+      'ToggleButton',
+    )
+  })
+
+  test('name from parent', () => {
+    let instance: ComponentInternalInstance | null = null
+    const Comp = {
+      setup() {
+        instance = getCurrentInstance()
+        return () => null
+      },
+    }
+    const Parent = {
+      components: {
+        list_item: Comp,
+      },
+      render() {
+        return h(Comp)
+      },
+    }
+    render(h(Parent), nodeOps.createElement('div'))
+
+    expect(formatComponentName(instance, Comp as GenericComponent)).toBe(
+      'ListItem',
+    )
+  })
+
+  test('functional components', () => {
+    const UserAvatar = () => null
+    expect(formatComponentName(null, UserAvatar)).toBe('UserAvatar')
+    UserAvatar.displayName = 'UserPicture'
+    expect(formatComponentName(null, UserAvatar)).toBe('UserPicture')
+    expect(formatComponentName(null, () => null)).toBe('Anonymous')
+  })
+
+  test('Name from file', () => {
+    const Comp = {
+      __file: './src/locale-dropdown.vue',
+    }
+
+    expect(formatComponentName(null, Comp)).toBe('LocaleDropdown')
+  })
+
+  test('inferred name', () => {
+    const Comp = {
+      __name: 'MainSidebar',
+    }
+
+    expect(formatComponentName(null, Comp)).toBe('MainSidebar')
+  })
+
+  test('global component', () => {
+    let instance: ComponentInternalInstance | null = null
+    const Comp = {
+      setup() {
+        instance = getCurrentInstance()
+        return () => null
+      },
+    }
+    render(h(Comp), nodeOps.createElement('div'))
+
+    instance!.appContext.components.FieldLabel = Comp
+
+    expect(formatComponentName(instance, Comp as GenericComponent)).toBe(
+      'FieldLabel',
+    )
+  })
+
+  test('name precedence', () => {
+    let instance: ComponentInternalInstance | null = null
+    const Dummy = () => null
+    const Comp: Record<string, any> = {
+      components: { Dummy },
+      setup() {
+        instance = getCurrentInstance()
+        return () => null
+      },
+    }
+    const Parent = {
+      components: { Dummy } as any,
+      render() {
+        return h(Comp)
+      },
+    }
+    render(h(Parent), nodeOps.createElement('div'))
+
+    expect(formatComponentName(instance, Comp)).toBe('Anonymous')
+    expect(formatComponentName(instance, Comp, true)).toBe('App')
+
+    instance!.appContext.components.CompA = Comp
+    expect(formatComponentName(instance, Comp)).toBe('CompA')
+    expect(formatComponentName(instance, Comp, true)).toBe('CompA')
+
+    Parent.components.CompB = Comp
+    expect(formatComponentName(instance, Comp)).toBe('CompB')
+
+    Comp.components.CompC = Comp
+    expect(formatComponentName(instance, Comp)).toBe('CompC')
+
+    Comp.__file = './CompD.js'
+    expect(formatComponentName(instance, Comp)).toBe('CompD')
+
+    Comp.__name = 'CompE'
+    expect(formatComponentName(instance, Comp)).toBe('CompE')
+
+    Comp.name = 'CompF'
+    expect(formatComponentName(instance, Comp)).toBe('CompF')
+  })
+})

+ 36 - 0
packages/runtime-core/__tests__/components/Suspense.spec.ts

@@ -24,6 +24,7 @@ import {
   shallowRef,
   watch,
   watchEffect,
+  withDirectives,
 } from '@vue/runtime-test'
 import { computed, createApp, defineComponent, inject, provide } from 'vue'
 import type { RawSlots } from 'packages/runtime-core/src/componentSlots'
@@ -2358,5 +2359,40 @@ describe('Suspense', () => {
         `<div>444</div><div>555</div><div>666</div>`,
       )
     })
+
+    test('should call unmounted directive once when fallback is replaced by resolved async component', async () => {
+      const Comp = {
+        render() {
+          return h('div', null, 'comp')
+        },
+      }
+      const Foo = defineAsyncComponent({
+        render() {
+          return h(Comp)
+        },
+      })
+      const unmounted = vi.fn(el => {
+        el.foo = null
+      })
+      const vDir = {
+        unmounted,
+      }
+      const App = {
+        setup() {
+          return () => {
+            return h(Suspense, null, {
+              fallback: () => withDirectives(h('div'), [[vDir, true]]),
+              default: () => h(Foo),
+            })
+          }
+        },
+      }
+      const root = nodeOps.createElement('div')
+      render(h(App), root)
+
+      await Promise.all(deps)
+      await nextTick()
+      expect(unmounted).toHaveBeenCalledTimes(1)
+    })
   })
 })

+ 1 - 1
packages/runtime-core/src/apiDefineComponent.ts

@@ -272,7 +272,7 @@ export function defineComponent<
         Slots,
         LocalComponents,
         Directives,
-        Exposed
+        string
       >
     >,
 ): DefineComponent<

+ 5 - 5
packages/runtime-core/src/apiInject.ts

@@ -1,5 +1,5 @@
 import { isFunction } from '@vue/shared'
-import { getCurrentGenericInstance } from './component'
+import { currentInstance, getCurrentGenericInstance } from './component'
 import { currentApp } from './apiCreateApp'
 import { warn } from './warning'
 
@@ -11,12 +11,12 @@ export function provide<T, K = InjectionKey<T> | string | number>(
   key: K,
   value: K extends InjectionKey<infer V> ? V : T,
 ): void {
-  const currentInstance = getCurrentGenericInstance()
-  if (!currentInstance) {
-    if (__DEV__) {
+  if (__DEV__) {
+    if (!currentInstance || currentInstance.isMounted) {
       warn(`provide() can only be used inside setup().`)
     }
-  } else {
+  }
+  if (currentInstance) {
     let provides = currentInstance.provides
     // by default an instance inherits its parent's provides object
     // but when it needs to provide values of its own, it creates its

+ 10 - 6
packages/runtime-core/src/component.ts

@@ -951,7 +951,7 @@ function setupStatefulComponent(
         // bail here and wait for re-entry.
         instance.asyncDep = setupResult
         if (__DEV__ && !instance.suspense) {
-          const name = Component.name ?? 'Anonymous'
+          const name = formatComponentName(instance, Component)
           warn(
             `Component <${name}>: setup function returned a promise, but no ` +
               `<Suspense> boundary was found in the parent component tree. ` +
@@ -1291,9 +1291,11 @@ export function formatComponentName(
     }
   }
 
-  if (!name && instance && instance.parent) {
+  if (!name && instance) {
     // try to infer the name based on reverse resolution
-    const inferFromRegistry = (registry: Record<string, any> | undefined) => {
+    const inferFromRegistry = (
+      registry: Record<string, any> | undefined | null,
+    ) => {
       for (const key in registry) {
         if (registry[key] === Component) {
           return key
@@ -1301,10 +1303,12 @@ export function formatComponentName(
       }
     }
     name =
-      inferFromRegistry(
-        (instance as ComponentInternalInstance).components ||
+      inferFromRegistry((instance as ComponentInternalInstance).components) ||
+      (instance.parent &&
+        inferFromRegistry(
           (instance.parent.type as ComponentOptions).components,
-      ) || inferFromRegistry(instance.appContext.components)
+        )) ||
+      inferFromRegistry(instance.appContext.components)
   }
 
   return name ? classify(name) : isRoot ? `App` : `Anonymous`

+ 2 - 2
packages/runtime-core/src/componentOptions.ts

@@ -1173,7 +1173,7 @@ export type ComponentOptionsWithoutProps<
       S,
       LC,
       Directives,
-      Exposed
+      string
     >
   >
 
@@ -1235,7 +1235,7 @@ export type ComponentOptionsWithArrayProps<
       S,
       LC,
       Directives,
-      Exposed
+      string
     >
   >
 

+ 4 - 10
packages/runtime-core/src/componentPublicInstance.ts

@@ -432,7 +432,6 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
     // is the multiple hasOwn() calls. It's much faster to do a simple property
     // access on a plain object, so we use an accessCache object (with null
     // prototype) to memoize what access type a key corresponds to.
-    let normalizedProps
     if (key[0] !== '$') {
       const n = accessCache![key]
       if (n !== undefined) {
@@ -457,12 +456,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
       ) {
         accessCache![key] = AccessTypes.DATA
         return data[key]
-      } else if (
-        // only cache other properties when instance has declared (thus stable)
-        // props
-        (normalizedProps = instance.propsOptions[0]) &&
-        hasOwn(normalizedProps, key)
-      ) {
+      } else if (hasOwn(props, key)) {
         accessCache![key] = AccessTypes.PROPS
         return props![key]
       } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
@@ -585,11 +579,11 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
 
   has(
     {
-      _: { data, setupState, accessCache, ctx, appContext, propsOptions, type },
+      _: { data, setupState, accessCache, ctx, appContext, props, type },
     }: ComponentRenderContext,
     key: string,
   ) {
-    let normalizedProps, cssModules
+    let cssModules
     return !!(
       accessCache![key] ||
       (__FEATURE_OPTIONS_API__ &&
@@ -597,7 +591,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
         key[0] !== '$' &&
         hasOwn(data, key)) ||
       hasSetupBinding(setupState, key) ||
-      ((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
+      hasOwn(props, key) ||
       hasOwn(ctx, key) ||
       hasOwn(publicPropertiesMap, key) ||
       hasOwn(appContext.config.globalProperties, key) ||

+ 6 - 2
packages/runtime-core/src/components/Suspense.ts

@@ -20,6 +20,7 @@ import {
   type RendererInternals,
   type RendererNode,
   type SetupRenderEffectFn,
+  queuePostRenderEffect,
 } from '../renderer'
 import { queuePostFlushCb } from '../scheduler'
 import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils'
@@ -577,9 +578,12 @@ function createSuspenseBoundary(
           }
           unmount(activeBranch, parentComponent, suspense, true)
           // clear el reference from fallback vnode to allow GC
-          // only clear immediately if there's no delayed transition
           if (!delayEnter && isInFallback && vnode.ssFallback) {
-            vnode.ssFallback.el = null
+            queuePostRenderEffect(
+              () => (vnode.ssFallback!.el = null),
+              undefined,
+              suspense,
+            )
           }
         }
         if (!delayEnter) {

+ 23 - 9
packages/runtime-dom/src/jsx.ts

@@ -286,6 +286,19 @@ export interface HTMLAttributes extends AriaAttributes, EventHandlers<Events> {
   contextmenu?: string | undefined
   dir?: string | undefined
   draggable?: Booleanish | undefined
+  enterkeyhint?:
+    | 'enter'
+    | 'done'
+    | 'go'
+    | 'next'
+    | 'previous'
+    | 'search'
+    | 'send'
+    | undefined
+  /**
+   * @deprecated Use `enterkeyhint` instead.
+   */
+  enterKeyHint?: HTMLAttributes['enterkeyhint']
   hidden?: Booleanish | '' | 'hidden' | 'until-found' | undefined
   id?: string | undefined
   inert?: Booleanish | undefined
@@ -346,6 +359,14 @@ export interface HTMLAttributes extends AriaAttributes, EventHandlers<Events> {
    * @see https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is
    */
   is?: string | undefined
+  /**
+   * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/exportparts
+   */
+  exportparts?: string
+  /**
+   * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/part
+   */
+  part?: string
 }
 
 type HTMLAttributeReferrerPolicy =
@@ -498,6 +519,7 @@ export interface ImgHTMLAttributes extends HTMLAttributes {
   alt?: string | undefined
   crossorigin?: 'anonymous' | 'use-credentials' | '' | undefined
   decoding?: 'async' | 'auto' | 'sync' | undefined
+  fetchpriority?: 'high' | 'low' | 'auto' | undefined
   height?: Numberish | undefined
   loading?: 'eager' | 'lazy' | undefined
   referrerpolicy?: HTMLAttributeReferrerPolicy | undefined
@@ -547,15 +569,6 @@ export interface InputHTMLAttributes extends HTMLAttributes {
   checked?: Booleanish | any[] | Set<any> | undefined // for IDE v-model multi-checkbox support
   crossorigin?: string | undefined
   disabled?: Booleanish | undefined
-  enterKeyHint?:
-    | 'enter'
-    | 'done'
-    | 'go'
-    | 'next'
-    | 'previous'
-    | 'search'
-    | 'send'
-    | undefined
   form?: string | undefined
   formaction?: string | undefined
   formenctype?: string | undefined
@@ -1288,6 +1301,7 @@ export interface IntrinsicElementAttributes {
   polyline: SVGAttributes
   radialGradient: SVGAttributes
   rect: SVGAttributes
+  set: SVGAttributes
   stop: SVGAttributes
   switch: SVGAttributes
   symbol: SVGAttributes