fix(runtime-vapor): skip teleport ranges for logical hydration siblings#14832
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Size ReportBundles
Usages
|
@vue/compiler-core
@vue/compiler-dom
@vue/compiler-sfc
@vue/compiler-ssr
@vue/compiler-vapor
@vue/reactivity
@vue/runtime-core
@vue/runtime-dom
@vue/runtime-vapor
@vue/server-renderer
@vue/shared
vue
@vue/compat
commit: |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/runtime-vapor/src/dom/hydration.ts`:
- Around line 235-241: The function nextLogicalSibling uses non-null assertions
on locateEndAnchor results which can be null; update nextLogicalSibling to
defensively handle a null return from locateEndAnchor (both the '[' branch and
the 'teleport start' branch) by checking the result of locateEndAnchor(node)
(and locateEndAnchor(node, 'teleport start','teleport end')) before accessing
.nextSibling and return null (or fallback to node.nextSibling only when safe)
instead of using the bang operator; refer to the nextLogicalSibling,
locateEndAnchor, and isComment symbols to locate the code and mirror the
defensive pattern used by other callers (e.g., using optional chaining or
explicit null checks).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 598588ad-0df0-47f5-ad55-c9768aca1d36
📒 Files selected for processing (7)
packages/runtime-vapor/__tests__/hydration.spec.tspackages/runtime-vapor/src/apiCreateFor.tspackages/runtime-vapor/src/component.tspackages/runtime-vapor/src/components/TransitionGroup.tspackages/runtime-vapor/src/dom/hydration.tspackages/runtime-vapor/src/dom/node.tspackages/runtime-vapor/src/fragment.ts
| export function nextLogicalSibling(node: Node): Node | null { | ||
| return isComment(node, '[') | ||
| ? _next(locateEndAnchor(node)!) | ||
| ? locateEndAnchor(node)!.nextSibling | ||
| : isComment(node, 'teleport start') | ||
| ? _next(locateEndAnchor(node, 'teleport start', 'teleport end')!) | ||
| : _next(node) | ||
| ? locateEndAnchor(node, 'teleport start', 'teleport end')!.nextSibling | ||
| : node.nextSibling | ||
| } |
There was a problem hiding this comment.
Potential null dereference if locateEndAnchor returns null.
locateEndAnchor can return null (line 295) when no matching end anchor exists. The non-null assertions on lines 237 and 239 followed by .nextSibling access will throw if the SSR output is malformed or truncated.
Other callers handle this defensively (e.g., line 409: end || undefined).
🛡️ Proposed fix with null guards
export function nextLogicalSibling(node: Node): Node | null {
- return isComment(node, '[')
- ? locateEndAnchor(node)!.nextSibling
- : isComment(node, 'teleport start')
- ? locateEndAnchor(node, 'teleport start', 'teleport end')!.nextSibling
- : node.nextSibling
+ if (isComment(node, '[')) {
+ const end = locateEndAnchor(node)
+ return end ? end.nextSibling : null
+ }
+ if (isComment(node, 'teleport start')) {
+ const end = locateEndAnchor(node, 'teleport start', 'teleport end')
+ return end ? end.nextSibling : null
+ }
+ return node.nextSibling
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function nextLogicalSibling(node: Node): Node | null { | |
| return isComment(node, '[') | |
| ? _next(locateEndAnchor(node)!) | |
| ? locateEndAnchor(node)!.nextSibling | |
| : isComment(node, 'teleport start') | |
| ? _next(locateEndAnchor(node, 'teleport start', 'teleport end')!) | |
| : _next(node) | |
| ? locateEndAnchor(node, 'teleport start', 'teleport end')!.nextSibling | |
| : node.nextSibling | |
| } | |
| export function nextLogicalSibling(node: Node): Node | null { | |
| if (isComment(node, '[')) { | |
| const end = locateEndAnchor(node) | |
| return end ? end.nextSibling : null | |
| } | |
| if (isComment(node, 'teleport start')) { | |
| const end = locateEndAnchor(node, 'teleport start', 'teleport end') | |
| return end ? end.nextSibling : null | |
| } | |
| return node.nextSibling | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/runtime-vapor/src/dom/hydration.ts` around lines 235 - 241, The
function nextLogicalSibling uses non-null assertions on locateEndAnchor results
which can be null; update nextLogicalSibling to defensively handle a null return
from locateEndAnchor (both the '[' branch and the 'teleport start' branch) by
checking the result of locateEndAnchor(node) (and locateEndAnchor(node,
'teleport start','teleport end')) before accessing .nextSibling and return null
(or fallback to node.nextSibling only when safe) instead of using the bang
operator; refer to the nextLogicalSibling, locateEndAnchor, and isComment
symbols to locate the code and mirror the defensive pattern used by other
callers (e.g., using optional chaining or explicit null checks).
There was a problem hiding this comment.
I don't think this should be guarded here. nextLogicalSibling() preserves the same invariant as the previous locateNextNode() implementation, which already used locateEndAnchor(...)! for Vue-owned structural marker pairs.
A missing fragment / Teleport end marker means the SSR structural range is corrupted or truncated. Returning null from the normal logical stepping helper would silently change boundary cleanup behavior and could hide the broken range. The defensive end || undefined path is specific to cleanup after a node has already been selected for removal, not the general sibling-stepping contract.
There was a problem hiding this comment.
@edison1105, that's a clear and well-reasoned distinction — thanks for the explanation. You're right that:
- Invariant parity:
locateNextNodealready usedlocateEndAnchor(...)!, sonextLogicalSiblingpreserves the same contract rather than weakening it. - Fail-loud is correct here: A missing
]orteleport endmarker means the SSR output is structurally corrupted. Returningnullfrom the stepping helper would silently short-circuitcleanupHydrationTail/finalizeHydrationBoundaryloops, potentially leaving orphaned SSR nodes without surfacing the real problem. - Context of the defensive pattern: The
end || undefinedguard (e.g., inremoveFragmentNodes) belongs to the cleanup/removal contract where the node being cleaned up has already been selected, and graceful no-op behavior is appropriate. That's a fundamentally different concern from the general sibling-stepping path.
I'm withdrawing the suggestion — the non-null assertions are intentional and correct.
✏️ Learnings added
Learnt from: edison1105
Repo: vuejs/core PR: 14832
File: packages/runtime-vapor/src/dom/hydration.ts:235-241
Timestamp: 2026-05-18T01:05:23.718Z
Learning: In `packages/runtime-vapor/src/dom/hydration.ts`, the `nextLogicalSibling` function (and its predecessor `locateNextNode`) intentionally uses non-null assertions (`!`) on `locateEndAnchor` results. Vue's SSR renderer always emits matched structural marker pairs (`[`/`]` for fragments, `teleport start`/`teleport end` for teleports). A missing end marker indicates fatal SSR output corruption; returning `null` from the stepping helper would silently short-circuit boundary cleanup loops (`cleanupHydrationTail`, `finalizeHydrationBoundary`) and hide the corruption. The defensive `end || undefined` pattern used elsewhere (e.g., `removeFragmentNodes`) is specific to cleanup/removal paths where a graceful no-op is appropriate, not the general sibling-stepping contract.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: edison1105
Repo: vuejs/core PR: 14786
File: packages/runtime-vapor/src/apiCreateFor.ts:93-95
Timestamp: 2026-05-07T03:25:36.641Z
Learning: In `packages/runtime-vapor` hydration-cursor code paths that use `enterHydrationCursor` (e.g., cursor callers in `apiCreateFor.ts` and related hydration helpers like `createIf`, slots, and dynamic fragments), do not add local try/finally exception-protection solely around cursor entry. If hydration throws, the outer hydration/component cleanup already restores the global hydration state, and there is no supported case where later hydration continues after an inner throw. If exception-safe cursor scoping becomes necessary in the future, implement it as a shared, reusable cursor-scope API (so the behavior is consistent across all cursor users) rather than one-off local try/catch/try/finally patterns in individual files.
Summary by CodeRabbit
Release Notes
Bug Fixes
Tests