Parcourir la source

Merge remote-tracking branch 'upstream/main'

三咲智子 Kevin Deng il y a 1 an
Parent
commit
b8713589de
33 fichiers modifiés avec 376 ajouts et 58 suppressions
  1. 24 0
      CHANGELOG.md
  2. 1 1
      package.json
  3. 2 1
      packages-private/sfc-playground/src/App.vue
  4. 1 1
      packages/compiler-core/package.json
  5. 1 1
      packages/compiler-core/src/transforms/vModel.ts
  6. 1 1
      packages/compiler-dom/package.json
  7. 23 0
      packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
  8. 17 0
      packages/compiler-sfc/__tests__/compileScript.spec.ts
  9. 1 1
      packages/compiler-sfc/package.json
  10. 1 1
      packages/compiler-sfc/src/script/definePropsDestructure.ts
  11. 20 1
      packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts
  12. 1 1
      packages/compiler-ssr/package.json
  13. 5 8
      packages/compiler-ssr/src/ssrCodegenTransform.ts
  14. 2 2
      packages/compiler-ssr/src/transforms/ssrVIf.ts
  15. 19 1
      packages/reactivity/__tests__/computed.spec.ts
  16. 1 1
      packages/reactivity/package.json
  17. 12 1
      packages/reactivity/src/dep.ts
  18. 18 8
      packages/reactivity/src/effect.ts
  19. 12 9
      packages/reactivity/src/ref.ts
  20. 146 0
      packages/runtime-core/__tests__/scheduler.spec.ts
  21. 1 1
      packages/runtime-core/package.json
  22. 7 5
      packages/runtime-core/src/apiDefineComponent.ts
  23. 3 1
      packages/runtime-core/src/componentProps.ts
  24. 27 3
      packages/runtime-core/src/hydrationStrategies.ts
  25. 6 2
      packages/runtime-core/src/scheduler.ts
  26. 1 1
      packages/runtime-dom/package.json
  27. 1 1
      packages/runtime-dom/src/components/Transition.ts
  28. 1 1
      packages/server-renderer/package.json
  29. 1 1
      packages/shared/package.json
  30. 1 1
      packages/vue-compat/package.json
  31. 7 1
      packages/vue/__tests__/e2e/hydration-strat-visible.html
  32. 11 0
      packages/vue/__tests__/e2e/hydrationStrategies.spec.ts
  33. 1 1
      packages/vue/package.json

+ 24 - 0
CHANGELOG.md

@@ -1,3 +1,27 @@
+## [3.5.7](https://github.com/vuejs/core/compare/v3.5.6...v3.5.7) (2024-09-20)
+
+
+### Bug Fixes
+
+* **compile-core:** fix v-model with newlines edge case ([#11960](https://github.com/vuejs/core/issues/11960)) ([6224288](https://github.com/vuejs/core/commit/62242886d705ece88dbcad45bb78072ecccad0ca)), closes [#8306](https://github.com/vuejs/core/issues/8306)
+* **compiler-sfc:** initialize scope with null prototype object ([#11963](https://github.com/vuejs/core/issues/11963)) ([215e154](https://github.com/vuejs/core/commit/215e15407294bf667261360218f975b88c99c2e5))
+* **hydration:** avoid observing non-Element node ([#11954](https://github.com/vuejs/core/issues/11954)) ([7257e6a](https://github.com/vuejs/core/commit/7257e6a34200409b3fc347d3bb807e11e2785974)), closes [#11952](https://github.com/vuejs/core/issues/11952)
+* **reactivity:** do not remove dep from depsMap when unsubbed by computed ([960706e](https://github.com/vuejs/core/commit/960706eebf73f08ebc9d5dd853a05def05e2c153))
+* **reactivity:** fix dev-only memory leak by updating dep.subsHead on sub removal ([5c8b76e](https://github.com/vuejs/core/commit/5c8b76ed6cfbbcee4cbaac0b72beab7291044e4f)), closes [#11956](https://github.com/vuejs/core/issues/11956)
+* **reactivity:** fix memory leak from dep instances of garbage collected objects ([235ea47](https://github.com/vuejs/core/commit/235ea4772ed2972914cf142da8b7ac1fb04f7585)), closes [#11979](https://github.com/vuejs/core/issues/11979) [#11971](https://github.com/vuejs/core/issues/11971)
+* **reactivity:** fix triggerRef call on ObjectRefImpl returned by toRef ([#11986](https://github.com/vuejs/core/issues/11986)) ([b030c8b](https://github.com/vuejs/core/commit/b030c8bc7327877efb98aa3d9a58eb287a6ff07a)), closes [#11982](https://github.com/vuejs/core/issues/11982)
+* **scheduler:** ensure recursive jobs can't be queued twice ([#11955](https://github.com/vuejs/core/issues/11955)) ([d18d6aa](https://github.com/vuejs/core/commit/d18d6aa1b20dc57a8103c51ec4d61e8e53ed936d))
+* **ssr:** don't render comments in TransitionGroup ([#11961](https://github.com/vuejs/core/issues/11961)) ([a2f6ede](https://github.com/vuejs/core/commit/a2f6edeb02faedbb673c4bc5c6a59d9a79a37d07)), closes [#11958](https://github.com/vuejs/core/issues/11958)
+* **transition:** respect `duration` setting even when it is `0` ([#11967](https://github.com/vuejs/core/issues/11967)) ([f927a4a](https://github.com/vuejs/core/commit/f927a4ae6f7c453f70ba89498ee0c737dc9866fd))
+* **types:** correct type inference of all-optional props ([#11644](https://github.com/vuejs/core/issues/11644)) ([9eca65e](https://github.com/vuejs/core/commit/9eca65ee9871d1ac878755afa9a3eb1b02030350)), closes [#11733](https://github.com/vuejs/core/issues/11733) [vuejs/language-tools#4704](https://github.com/vuejs/language-tools/issues/4704)
+
+
+### Performance Improvements
+
+* **hydration:** avoid observer if element is in viewport ([#11639](https://github.com/vuejs/core/issues/11639)) ([e075dfa](https://github.com/vuejs/core/commit/e075dfad5c7649c6045e3711687ec888e7aa1a39))
+
+
+
 ## [3.5.6](https://github.com/vuejs/core/compare/v3.5.5...v3.5.6) (2024-09-16)
 
 

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "private": true,
-  "version": "3.5.6",
+  "version": "3.5.7",
   "packageManager": "pnpm@9.10.0",
   "type": "module",
   "scripts": {

+ 2 - 1
packages-private/sfc-playground/src/App.vue

@@ -212,7 +212,8 @@ onMounted(() => {
     @keydown.ctrl.s.prevent
     @keydown.meta.s.prevent
     :ssr="useSSRMode"
-    :autoSave="autoSave"
+    :model-value="autoSave"
+    :editorOptions="{ autoSaveText: false }"
     :store="store"
     :showCompileOutput="true"
     :autoResize="true"

+ 1 - 1
packages/compiler-core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-core",
-  "version": "3.5.6",
+  "version": "3.5.7",
   "description": "@vue/compiler-core",
   "main": "index.js",
   "module": "dist/compiler-core.esm-bundler.js",

+ 1 - 1
packages/compiler-core/src/transforms/vModel.ts

@@ -31,7 +31,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
 
   // we assume v-model directives are always parsed
   // (not artificially created by a transform)
-  const rawExp = exp.loc.source
+  const rawExp = exp.loc.source.trim()
   const expString =
     exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : rawExp
 

+ 1 - 1
packages/compiler-dom/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-dom",
-  "version": "3.5.6",
+  "version": "3.5.7",
   "description": "@vue/compiler-dom",
   "main": "index.js",
   "module": "dist/compiler-dom.esm-bundler.js",

+ 23 - 0
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap

@@ -1084,6 +1084,29 @@ return (_ctx, _cache) => {
 }"
 `;
 
+exports[`SFC compile <script setup> > inlineTemplate mode > v-model w/ newlines codegen 1`] = `
+"import { unref as _unref, isRef as _isRef, vModelText as _vModelText, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+
+export default {
+  setup(__props) {
+
+        const count = ref(0)
+        
+return (_ctx, _cache) => {
+  return _withDirectives((_openBlock(), _createElementBlock("input", {
+    "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => (_isRef(count) ? (count).value = $event : null))
+  }, null, 512 /* NEED_PATCH */)), [
+    [_vModelText, 
+          _unref(count)
+          ]
+  ])
+}
+}
+
+}"
+`;
+
 exports[`SFC compile <script setup> > inlineTemplate mode > with defineExpose() 1`] = `
 "
 export default {

+ 17 - 0
packages/compiler-sfc/__tests__/compileScript.spec.ts

@@ -472,6 +472,23 @@ describe('SFC compile <script setup>', () => {
       assertCode(content)
     })
 
+    test('v-model w/ newlines codegen', () => {
+      const { content } = compile(
+        `<script setup>
+        const count = ref(0)
+        </script>
+        <template>
+          <input v-model="
+          count
+          ">
+        </template>
+        `,
+        { inlineTemplate: true },
+      )
+      expect(content).toMatch(`_isRef(count) ? (count).value = $event : null`)
+      assertCode(content)
+    })
+
     test('v-model should not generate ref assignment code for non-setup bindings', () => {
       const { content } = compile(
         `<script setup>

+ 1 - 1
packages/compiler-sfc/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-sfc",
-  "version": "3.5.6",
+  "version": "3.5.7",
   "description": "@vue/compiler-sfc",
   "main": "dist/compiler-sfc.cjs.js",
   "module": "dist/compiler-sfc.esm-browser.js",

+ 1 - 1
packages/compiler-sfc/src/script/definePropsDestructure.ts

@@ -102,7 +102,7 @@ export function transformDestructuredProps(
     return
   }
 
-  const rootScope: Scope = {}
+  const rootScope: Scope = Object.create(null)
   const scopeStack: Scope[] = [rootScope]
   let currentScope: Scope = rootScope
   const excludedIds = new WeakSet<Identifier>()

+ 20 - 1
packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts

@@ -39,7 +39,7 @@ describe('transition-group', () => {
   })
 
   // #11514
-  test('with static tag + comment', () => {
+  test('with static tag + v-if comment', () => {
     expect(
       compile(
         `<transition-group tag="ul"><div v-for="i in list"/><div v-if="false"></div></transition-group>`,
@@ -60,6 +60,25 @@ describe('transition-group', () => {
     `)
   })
 
+  // #11958
+  test('with static tag + comment', () => {
+    expect(
+      compile(
+        `<transition-group tag="ul"><div v-for="i in list"/><!--test--></transition-group>`,
+      ).code,
+    ).toMatchInlineSnapshot(`
+      "const { ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        _push(\`<ul\${_ssrRenderAttrs(_attrs)}>\`)
+        _ssrRenderList(_ctx.list, (i) => {
+          _push(\`<div></div>\`)
+        })
+        _push(\`</ul>\`)
+      }"
+    `)
+  })
+
   test('with dynamic tag', () => {
     expect(
       compile(

+ 1 - 1
packages/compiler-ssr/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-ssr",
-  "version": "3.5.6",
+  "version": "3.5.7",
   "description": "@vue/compiler-ssr",
   "main": "dist/compiler-ssr.cjs.js",
   "types": "dist/compiler-ssr.d.ts",

+ 5 - 8
packages/compiler-ssr/src/ssrCodegenTransform.ts

@@ -156,7 +156,7 @@ export function processChildren(
   context: SSRTransformContext,
   asFragment = false,
   disableNestedFragments = false,
-  disableCommentAsIfAlternate = false,
+  disableComment = false,
 ): void {
   if (asFragment) {
     context.pushStringPart(`<!--[-->`)
@@ -197,7 +197,9 @@ export function processChildren(
       case NodeTypes.COMMENT:
         // no need to escape comment here because the AST can only
         // contain valid comments.
-        context.pushStringPart(`<!--${child.content}-->`)
+        if (!disableComment) {
+          context.pushStringPart(`<!--${child.content}-->`)
+        }
         break
       case NodeTypes.INTERPOLATION:
         context.pushStringPart(
@@ -207,12 +209,7 @@ export function processChildren(
         )
         break
       case NodeTypes.IF:
-        ssrProcessIf(
-          child,
-          context,
-          disableNestedFragments,
-          disableCommentAsIfAlternate,
-        )
+        ssrProcessIf(child, context, disableNestedFragments, disableComment)
         break
       case NodeTypes.FOR:
         ssrProcessFor(child, context, disableNestedFragments)

+ 2 - 2
packages/compiler-ssr/src/transforms/ssrVIf.ts

@@ -27,7 +27,7 @@ export function ssrProcessIf(
   node: IfNode,
   context: SSRTransformContext,
   disableNestedFragments = false,
-  disableCommentAsIfAlternate = false,
+  disableComment = false,
 ): void {
   const [rootBranch] = node.branches
   const ifStatement = createIfStatement(
@@ -56,7 +56,7 @@ export function ssrProcessIf(
     }
   }
 
-  if (!currentIf.alternate && !disableCommentAsIfAlternate) {
+  if (!currentIf.alternate && !disableComment) {
     currentIf.alternate = createBlockStatement([
       createCallExpression(`_push`, ['`<!---->`']),
     ])

+ 19 - 1
packages/reactivity/__tests__/computed.spec.ts

@@ -1006,9 +1006,27 @@ describe('reactivity/computed', () => {
     expect(serializeInner(root)).toBe(`<button>Step</button><p>Step 2</p>`)
   })
 
-  it('manual trigger computed', () => {
+  test('manual trigger computed', () => {
     const cValue = computed(() => 1)
     triggerRef(cValue)
     expect(cValue.value).toBe(1)
   })
+
+  test('computed should remain live after losing all subscribers', () => {
+    const toggle = ref(true)
+    const state = reactive({
+      a: 1,
+    })
+    const p = computed(() => state.a + 1)
+    const pp = computed(() => {
+      return toggle.value ? p.value : 111
+    })
+
+    const { effect: e } = effect(() => pp.value)
+    e.stop()
+
+    expect(p.value).toBe(2)
+    state.a++
+    expect(p.value).toBe(3)
+  })
 })

+ 1 - 1
packages/reactivity/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/reactivity",
-  "version": "3.5.6",
+  "version": "3.5.7",
   "description": "@vue/reactivity",
   "main": "index.js",
   "module": "dist/reactivity.esm-bundler.js",

+ 12 - 1
packages/reactivity/src/dep.ts

@@ -82,6 +82,13 @@ export class Dep {
    */
   subsHead?: Link
 
+  /**
+   * For object property deps cleanup
+   */
+  target?: unknown = undefined
+  map?: KeyToDepMap = undefined
+  key?: unknown = undefined
+
   constructor(public computed?: ComputedRefImpl | undefined) {
     if (__DEV__) {
       this.subsHead = undefined
@@ -218,7 +225,8 @@ function addSub(link: Link) {
 // which maintains a Set of subscribers, but we simply store them as
 // raw Maps to reduce memory overhead.
 type KeyToDepMap = Map<any, Dep>
-const targetMap = new WeakMap<object, KeyToDepMap>()
+
+export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap()
 
 export const ITERATE_KEY: unique symbol = Symbol(
   __DEV__ ? 'Object iterate' : '',
@@ -249,6 +257,9 @@ export function track(target: object, type: TrackOpTypes, key: unknown): void {
     let dep = depsMap.get(key)
     if (!dep) {
       depsMap.set(key, (dep = new Dep()))
+      dep.target = target
+      dep.map = depsMap
+      dep.key = key
     }
     if (__DEV__) {
       dep.track({

+ 18 - 8
packages/reactivity/src/effect.ts

@@ -1,7 +1,7 @@
 import { extend, hasChanged } from '@vue/shared'
 import type { ComputedRefImpl } from './computed'
 import type { TrackOpTypes, TriggerOpTypes } from './constants'
-import { type Link, globalVersion } from './dep'
+import { type Link, globalVersion, targetMap } from './dep'
 import { activeEffectScope } from './effectScope'
 import { warn } from './warning'
 
@@ -399,7 +399,7 @@ export function refreshComputed(computed: ComputedRefImpl): undefined {
   }
 }
 
-function removeSub(link: Link) {
+function removeSub(link: Link, fromComputed = false) {
   const { dep, prevSub, nextSub } = link
   if (prevSub) {
     prevSub.nextSub = nextSub
@@ -413,14 +413,24 @@ function removeSub(link: Link) {
     // was previous tail, point new tail to prev
     dep.subs = prevSub
   }
+  if (__DEV__ && dep.subsHead === link) {
+    // was previous head, point new head to next
+    dep.subsHead = nextSub
+  }
 
-  if (!dep.subs && dep.computed) {
+  if (!dep.subs) {
     // last subscriber removed
-    // if computed, unsubscribe it from all its deps so this computed and its
-    // value can be GCed
-    dep.computed.flags &= ~EffectFlags.TRACKING
-    for (let l = dep.computed.deps; l; l = l.nextDep) {
-      removeSub(l)
+    if (dep.computed) {
+      // if computed, unsubscribe it from all its deps so this computed and its
+      // value can be GCed
+      dep.computed.flags &= ~EffectFlags.TRACKING
+      for (let l = dep.computed.deps; l; l = l.nextDep) {
+        removeSub(l, true)
+      }
+    } else if (dep.map && !fromComputed) {
+      // property dep, remove it from the owner depsMap
+      dep.map.delete(dep.key)
+      if (!dep.map.size) targetMap.delete(dep.target!)
     }
   }
 }

+ 12 - 9
packages/reactivity/src/ref.ts

@@ -182,15 +182,18 @@ class RefImpl<T = any> {
  * @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref}
  */
 export function triggerRef(ref: Ref): void {
-  if (__DEV__) {
-    ;(ref as unknown as RefImpl).dep.trigger({
-      target: ref,
-      type: TriggerOpTypes.SET,
-      key: 'value',
-      newValue: (ref as unknown as RefImpl)._value,
-    })
-  } else {
-    ;(ref as unknown as RefImpl).dep.trigger()
+  // ref may be an instance of ObjectRefImpl
+  if ((ref as unknown as RefImpl).dep) {
+    if (__DEV__) {
+      ;(ref as unknown as RefImpl).dep.trigger({
+        target: ref,
+        type: TriggerOpTypes.SET,
+        key: 'value',
+        newValue: (ref as unknown as RefImpl)._value,
+      })
+    } else {
+      ;(ref as unknown as RefImpl).dep.trigger()
+    }
   }
 }
 

+ 146 - 0
packages/runtime-core/__tests__/scheduler.spec.ts

@@ -517,6 +517,45 @@ describe('scheduler', () => {
     await nextTick()
   })
 
+  test('jobs can be re-queued after an error', async () => {
+    const err = new Error('test')
+    let shouldThrow = true
+
+    const job1: SchedulerJob = vi.fn(() => {
+      if (shouldThrow) {
+        shouldThrow = false
+        throw err
+      }
+    })
+    job1.id = 1
+
+    const job2: SchedulerJob = vi.fn()
+    job2.id = 2
+
+    queueJob(job1)
+    queueJob(job2)
+
+    try {
+      await nextTick()
+    } catch (e: any) {
+      expect(e).toBe(err)
+    }
+    expect(
+      `Unhandled error during execution of scheduler flush`,
+    ).toHaveBeenWarned()
+
+    expect(job1).toHaveBeenCalledTimes(1)
+    expect(job2).toHaveBeenCalledTimes(0)
+
+    queueJob(job1)
+    queueJob(job2)
+
+    await nextTick()
+
+    expect(job1).toHaveBeenCalledTimes(2)
+    expect(job2).toHaveBeenCalledTimes(1)
+  })
+
   test('should prevent self-triggering jobs by default', async () => {
     let count = 0
     const job = () => {
@@ -558,6 +597,113 @@ describe('scheduler', () => {
     expect(count).toBe(5)
   })
 
+  test('recursive jobs can only be queued once non-recursively', async () => {
+    const job: SchedulerJob = vi.fn()
+    job.id = 1
+    job.flags = SchedulerJobFlags.ALLOW_RECURSE
+
+    queueJob(job)
+    queueJob(job)
+
+    await nextTick()
+
+    expect(job).toHaveBeenCalledTimes(1)
+  })
+
+  test('recursive jobs can only be queued once recursively', async () => {
+    let recurse = true
+
+    const job: SchedulerJob = vi.fn(() => {
+      if (recurse) {
+        queueJob(job)
+        queueJob(job)
+        recurse = false
+      }
+    })
+    job.id = 1
+    job.flags = SchedulerJobFlags.ALLOW_RECURSE
+
+    queueJob(job)
+
+    await nextTick()
+
+    expect(job).toHaveBeenCalledTimes(2)
+  })
+
+  test(`recursive jobs can't be re-queued by other jobs`, async () => {
+    let recurse = true
+
+    const job1: SchedulerJob = () => {
+      if (recurse) {
+        // job2 is already queued, so this shouldn't do anything
+        queueJob(job2)
+        recurse = false
+      }
+    }
+    job1.id = 1
+
+    const job2: SchedulerJob = vi.fn(() => {
+      if (recurse) {
+        queueJob(job1)
+        queueJob(job2)
+      }
+    })
+    job2.id = 2
+    job2.flags = SchedulerJobFlags.ALLOW_RECURSE
+
+    queueJob(job2)
+
+    await nextTick()
+
+    expect(job2).toHaveBeenCalledTimes(2)
+  })
+
+  test('jobs are de-duplicated correctly when calling flushPreFlushCbs', async () => {
+    let recurse = true
+
+    const job1: SchedulerJob = vi.fn(() => {
+      queueJob(job3)
+      queueJob(job3)
+      flushPreFlushCbs()
+    })
+    job1.id = 1
+    job1.flags = SchedulerJobFlags.PRE
+
+    const job2: SchedulerJob = vi.fn(() => {
+      if (recurse) {
+        // job2 does not allow recurse, so this shouldn't do anything
+        queueJob(job2)
+
+        // job3 is already queued, so this shouldn't do anything
+        queueJob(job3)
+        recurse = false
+      }
+    })
+    job2.id = 2
+    job2.flags = SchedulerJobFlags.PRE
+
+    const job3: SchedulerJob = vi.fn(() => {
+      if (recurse) {
+        queueJob(job2)
+        queueJob(job3)
+
+        // The jobs are already queued, so these should have no effect
+        queueJob(job2)
+        queueJob(job3)
+      }
+    })
+    job3.id = 3
+    job3.flags = SchedulerJobFlags.ALLOW_RECURSE | SchedulerJobFlags.PRE
+
+    queueJob(job1)
+
+    await nextTick()
+
+    expect(job1).toHaveBeenCalledTimes(1)
+    expect(job2).toHaveBeenCalledTimes(1)
+    expect(job3).toHaveBeenCalledTimes(2)
+  })
+
   // #1947 flushPostFlushCbs should handle nested calls
   // e.g. app.mount inside app.mount
   test('flushPostFlushCbs', async () => {

+ 1 - 1
packages/runtime-core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/runtime-core",
-  "version": "3.5.6",
+  "version": "3.5.7",
   "description": "@vue/runtime-core",
   "main": "index.js",
   "module": "dist/runtime-core.esm-bundler.js",

+ 7 - 5
packages/runtime-core/src/apiDefineComponent.ts

@@ -209,11 +209,13 @@ export function defineComponent<
     ? TypeEmitsToOptions<TypeEmits>
     : RuntimeEmitsOptions,
   InferredProps = unknown extends TypeProps
-    ? string extends RuntimePropsKeys
-      ? ComponentObjectPropsOptions extends RuntimePropsOptions
-        ? {}
-        : ExtractPropTypes<RuntimePropsOptions>
-      : { [key in RuntimePropsKeys]?: any }
+    ? keyof TypeProps extends never
+      ? string extends RuntimePropsKeys
+        ? ComponentObjectPropsOptions extends RuntimePropsOptions
+          ? {}
+          : ExtractPropTypes<RuntimePropsOptions>
+        : { [key in RuntimePropsKeys]?: any }
+      : TypeProps
     : TypeProps,
   TypeRefs extends Record<string, unknown> = {},
   TypeEl extends Element = any,

+ 3 - 1
packages/runtime-core/src/componentProps.ts

@@ -125,7 +125,9 @@ type InferPropType<T, NullAsAny = true> = [T] extends [null]
               : InferPropType<U, false>
             : [T] extends [Prop<infer V, infer D>]
               ? unknown extends V
-                ? IfAny<V, V, D>
+                ? keyof V extends never
+                  ? IfAny<V, V, D>
+                  : V
                 : V
               : T
 

+ 27 - 3
packages/runtime-core/src/hydrationStrategies.ts

@@ -26,6 +26,16 @@ export const hydrateOnIdle: HydrationStrategyFactory<number> =
     return () => cancelIdleCallback(id)
   }
 
+function elementIsVisibleInViewport(el: Element) {
+  const { top, left, bottom, right } = el.getBoundingClientRect()
+  // eslint-disable-next-line no-restricted-globals
+  const { innerHeight, innerWidth } = window
+  return (
+    ((top > 0 && top < innerHeight) || (bottom > 0 && bottom < innerHeight)) &&
+    ((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth))
+  )
+}
+
 export const hydrateOnVisible: HydrationStrategyFactory<
   IntersectionObserverInit
 > = opts => (hydrate, forEach) => {
@@ -37,7 +47,15 @@ export const hydrateOnVisible: HydrationStrategyFactory<
       break
     }
   }, opts)
-  forEach(el => ob.observe(el))
+  forEach(el => {
+    if (!(el instanceof Element)) return
+    if (elementIsVisibleInViewport(el)) {
+      hydrate()
+      ob.disconnect()
+      return false
+    }
+    ob.observe(el)
+  })
   return () => ob.disconnect()
 }
 
@@ -85,14 +103,20 @@ export const hydrateOnInteraction: HydrationStrategyFactory<
     return teardown
   }
 
-export function forEachElement(node: Node, cb: (el: Element) => void): void {
+export function forEachElement(
+  node: Node,
+  cb: (el: Element) => void | false,
+): void {
   // fragment
   if (isComment(node) && node.data === '[') {
     let depth = 1
     let next = node.nextSibling
     while (next) {
       if (next.nodeType === DOMNodeTypes.ELEMENT) {
-        cb(next as Element)
+        const result = cb(next as Element)
+        if (result === false) {
+          break
+        }
       } else if (isComment(next)) {
         if (next.data === ']') {
           if (--depth === 0) break

+ 6 - 2
packages/runtime-core/src/scheduler.ts

@@ -162,7 +162,9 @@ export function flushPreFlushCbs(
         cb.flags! &= ~SchedulerJobFlags.QUEUED
       }
       cb()
-      cb.flags! &= ~SchedulerJobFlags.QUEUED
+      if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
+        cb.flags! &= ~SchedulerJobFlags.QUEUED
+      }
     }
   }
 }
@@ -239,7 +241,9 @@ function flushJobs(seen?: CountMap) {
           job.i,
           job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER,
         )
-        job.flags! &= ~SchedulerJobFlags.QUEUED
+        if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
+          job.flags! &= ~SchedulerJobFlags.QUEUED
+        }
       }
     }
   } finally {

+ 1 - 1
packages/runtime-dom/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/runtime-dom",
-  "version": "3.5.6",
+  "version": "3.5.7",
   "description": "@vue/runtime-dom",
   "main": "index.js",
   "module": "dist/runtime-dom.esm-bundler.js",

+ 1 - 1
packages/runtime-dom/src/components/Transition.ts

@@ -344,7 +344,7 @@ function whenTransitionEnds(
     }
   }
 
-  if (explicitTimeout) {
+  if (explicitTimeout != null) {
     return setTimeout(resolveIfNotStale, explicitTimeout)
   }
 

+ 1 - 1
packages/server-renderer/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/server-renderer",
-  "version": "3.5.6",
+  "version": "3.5.7",
   "description": "@vue/server-renderer",
   "main": "index.js",
   "module": "dist/server-renderer.esm-bundler.js",

+ 1 - 1
packages/shared/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/shared",
-  "version": "3.5.6",
+  "version": "3.5.7",
   "description": "internal utils shared across @vue packages",
   "main": "index.js",
   "module": "dist/shared.esm-bundler.js",

+ 1 - 1
packages/vue-compat/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/compat",
-  "version": "3.5.6",
+  "version": "3.5.7",
   "description": "Vue 3 compatibility build for Vue 2",
   "main": "index.js",
   "module": "dist/vue.runtime.esm-bundler.js",

+ 7 - 1
packages/vue/__tests__/e2e/hydration-strat-visible.html

@@ -11,9 +11,12 @@
 <script>
   const rootMargin = location.search.match(/rootMargin=(\d+)/)?.[1] ?? 0
   const isFragment = location.search.includes('?fragment')
+  const isVIf = location.search.includes('?v-if')
   if (isFragment) {
     document.getElementById('app').innerHTML =
       `<!--[--><!--[--><span>one</span><!--]--><button>0</button><span>two</span><!--]-->`
+  } else if (isVIf) {
+    document.getElementById('app').innerHTML = `<!---->`
   }
 
   window.isHydrated = false
@@ -24,6 +27,7 @@
     ref,
     onMounted,
     hydrateOnVisible,
+    createCommentVNode,
   } = Vue
 
   const Comp = {
@@ -39,7 +43,9 @@
           { onClick: () => count.value++ },
           count.value,
         )
-        if (isFragment) {
+        if (isVIf) {
+          return createCommentVNode('v-if', true)
+        } else if (isFragment) {
           return [[h('span', 'one')], button, h('span', 'two')]
         } else {
           return button

+ 11 - 0
packages/vue/__tests__/e2e/hydrationStrategies.spec.ts

@@ -65,6 +65,17 @@ describe('async component hydration strategies', () => {
     await assertHydrationSuccess()
   })
 
+  test('visible (root v-if) should not throw error', async () => {
+    const spy = vi.fn()
+    const currentPage = page()
+    currentPage.on('pageerror', spy)
+    await goToCase('visible', '?v-if')
+    await page().waitForFunction(() => window.isRootMounted)
+    expect(await page().evaluate(() => window.isHydrated)).toBe(false)
+    expect(spy).toBeCalledTimes(0)
+    currentPage.off('pageerror', spy)
+  })
+
   test('media query', async () => {
     await goToCase('media')
     await page().waitForFunction(() => window.isRootMounted)

+ 1 - 1
packages/vue/package.json

@@ -1,6 +1,6 @@
 {
   "name": "vue",
-  "version": "3.5.6",
+  "version": "3.5.7",
   "description": "The progressive JavaScript framework for building modern web UI.",
   "main": "index.js",
   "module": "dist/vue.runtime.esm-bundler.js",