Skip to content

refactor(runtime-vapor): split fragment.ts into domain modules#14951

Merged
edison1105 merged 24 commits into
minorfrom
edison/refactor/fragment
Jun 15, 2026
Merged

refactor(runtime-vapor): split fragment.ts into domain modules#14951
edison1105 merged 24 commits into
minorfrom
edison/refactor/fragment

Conversation

@edison1105

@edison1105 edison1105 commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • Bug Fixes

    • Improved hydration mismatch recovery and node recreation handling
    • Fixed transition leave/defer behavior during component switching
    • Enhanced slot fallback rendering and visibility reconciliation
  • New Features

    • Added keyed branch support for keep-alive caching
    • Improved slot boundary tracking for better fallback management
    • Enhanced transition hook behavior for out-in modes and rapid toggles
  • Tests

    • Added comprehensive hydration-anchor and mismatch coverage
    • Expanded transition and transition-group edge-case testing

Pure code motion, no behavior change. fragment.ts had grown into a
1372-line integrator mixing the core branch pipeline with slot
boundary ambient state, the slot fallback state machine, and
hydration slot-boundary cursor state. Split by domain:

- slotBoundary.ts: SlotBoundaryContext + currentSlotBoundary ambient
  state, trackSlotBoundaryDirtying, hasSlotFallback (leaf module)
- slotFragment.ts: the SlotFallbackState machine (render/commit/
  recheck/dispose) + detachBlock + getRedirectedBoundary
- dom/hydrateFragment.ts: hydrating slot boundary cursor state
  (withHydratingSlotBoundary, getCurrentSlotEndAnchor,
  isHydratingSlotFallbackActive et al), which is hydration-cursor
  state shared by apiCreateFor/vdomInterop rather than slot logic
- EMPTY_BLOCK moves to block.ts next to the Block types

The SlotFragment class itself stays in fragment.ts: `extends
DynamicFragment` reads the base class binding at module evaluation
time, and componentSlots.ts (which constructs SlotFragment) sits
inside fragment.ts's evaluation closure, so hoisting the class into
another module can hit the base class before initialization
depending on entry order. The fallback machinery is plain function
declarations and therefore cycle-safe.

DynamicFragment.hydrate() and its helpers also stay for now:
CloseAnchorOwner is a const enum and isolatedModules forbids
cross-module const enum usage; they move together in a follow-up.

index.ts export surface is unchanged.
…nction

Move the hydration anchor-resolution logic out of the DynamicFragment prototype into hydrateDynamicFragmentAnchor() in dom/hydrateFragment.ts, along with its helpers (CloseAnchorOwner, getDynamicCloseOwner, queueAnchorInsert, isReusableDynamicFragmentAnchor) and the deferred hydration cursor handling (prepareDeferredHydrationAnchor).

fragment.ts now only value-imports the two entry points; the reverse dependency from dom/hydrateFragment.ts stays type-only. Behavior is unchanged and the optional interop/teleport `hydrate` field on VaporFragment is untouched.
… plan

Split hydrateDynamicFragmentAnchor() into a pure decision step and a side-effect step: resolveDynamicAnchor(frag, isEmpty) maps each SSR output shape to an AnchorPlan ('reuse' | 'create' | 'create-cleanup' | 'create-before-slot-end'), and executeAnchorPlan(frag, plan) performs all mutations (anchor marking, boundary cleanup, queued inserts, stale block reset). The per-path comments documenting each SSR shape stay on the corresponding resolver branch, and the resolver is covered by direct plan-level unit tests in __tests__/dom/hydrateFragment.spec.ts.

Also narrow isSlotFragment() to `val is SlotFragment` so the hydration module reads `forwarded` through the predicate instead of an `as any as SlotFragment` downcast.

Behavior is unchanged; app-only and SSR bundle sizes are byte-identical.
…ment core

Replace the inline isLeaving deferral and leave-mode scheduling in
DynamicFragment.update() with two registry slots
(deferBranchUpdateDuringLeave / removeBranchWithLeave) implemented in
components/Transition.ts, following the existing registerTransitionHooks
pattern. The now-unused applyTransitionLeaveHooks slot is removed; the
impl is called directly within the Transition module. The pending field
stays on DynamicFragment but is now exclusively owned by the Transition
module.
…Branch

Narrow the KeepAlive surface that DynamicFragment.renderBranch needs to
understand down to two intent-revealing context methods:

- rename VaporKeepAliveContext.getScope to acquireBranchScope (pre-render
  kept-alive scope reuse, implementation unchanged)
- add runBranchRender(frag, fn): owns the keyed cache-key context and marks
  shape flags once the render settles, replacing the inline
  withCurrentCacheKey dispatch and the processShapeFlag call in the render
  finally block

The cache-key wrapping range and the setBlockKey -> applyTransitionHooks ->
processShapeFlag order are preserved. withCurrentCacheKey disappears from
core imports; processShapeFlag stays on the interface for the component /
interop boundary-isolation callers. keepAlive.ts gains a type-only
DynamicFragment import (no new runtime cycle).
… core

SlotFragment now hooks into DynamicFragment.update() through explicit
extension points instead of the core pipeline branching on slot state:

- drop the shouldInsert 4th parameter of update() (its only caller was
  SlotFragment.updateContent). The insertion gate is now an overridable
  getBranchParent(): the base class returns anchor.parentNode and
  SlotFragment returns null while its fallback is active, which is
  bit-equivalent to the old flag (no removal/insertion of the detached
  content, same notifyUpdated condition). update() is back to
  (render?, key?, noScope?), shrinking the signature contract the vShow
  monkey-patch forwards.
- replace the two `isHydrating && !this.isSlot` reads with an autoHydrate
  constructor parameter (default true). SlotFragment opts out since
  updateSlot owns its hydration timing. isSlot is demoted to a pure marker
  consumed only by the isSlotFragment predicate.

Other update() entry points on SlotFragment (updateSlot via updateContent)
and the plain DynamicFragment callers in componentSlots/component were
audited; behavior is unchanged.
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c3c40f92-fe58-4f90-b309-29627283db45

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch edison/refactor/fragment

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown

Size Report

Bundles

File Size Gzip Brotli
compiler-dom.global.prod.js 86.7 kB 30.3 kB 26.6 kB
runtime-dom.global.prod.js 113 kB 42.7 kB 38.2 kB
vue.global.prod.js 173 kB 62.5 kB 55.7 kB

Usages

Name Size Gzip Brotli
createApp (CAPI only) 51.6 kB 20.1 kB 18.4 kB
createApp 60.6 kB 23.5 kB 21.3 kB
createApp + vaporInteropPlugin 114 kB (+437 B) 40.9 kB (+179 B) 36.8 kB (+73 B)
createVaporApp 28.1 kB 10.9 kB 10 kB
createSSRApp 65.2 kB 25.3 kB 22.9 kB
createVaporSSRApp 33 kB (+798 B) 12.7 kB (+257 B) 11.6 kB (+219 B)
defineCustomElement 67.3 kB 25.5 kB 23.2 kB
defineVaporCustomElement 49.9 kB (+140 B) 17.6 kB (+43 B) 16.2 kB (+37 B)
overall 75.9 kB 28.9 kB 26.2 kB

@pkg-pr-new

pkg-pr-new Bot commented Jun 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@vue/compiler-core

pnpm add https://pkg.pr.new/@vue/compiler-core@14951
npm i https://pkg.pr.new/@vue/compiler-core@14951
yarn add https://pkg.pr.new/@vue/compiler-core@14951.tgz

@vue/compiler-dom

pnpm add https://pkg.pr.new/@vue/compiler-dom@14951
npm i https://pkg.pr.new/@vue/compiler-dom@14951
yarn add https://pkg.pr.new/@vue/compiler-dom@14951.tgz

@vue/compiler-sfc

pnpm add https://pkg.pr.new/@vue/compiler-sfc@14951
npm i https://pkg.pr.new/@vue/compiler-sfc@14951
yarn add https://pkg.pr.new/@vue/compiler-sfc@14951.tgz

@vue/compiler-ssr

pnpm add https://pkg.pr.new/@vue/compiler-ssr@14951
npm i https://pkg.pr.new/@vue/compiler-ssr@14951
yarn add https://pkg.pr.new/@vue/compiler-ssr@14951.tgz

@vue/compiler-vapor

pnpm add https://pkg.pr.new/@vue/compiler-vapor@14951
npm i https://pkg.pr.new/@vue/compiler-vapor@14951
yarn add https://pkg.pr.new/@vue/compiler-vapor@14951.tgz

@vue/reactivity

pnpm add https://pkg.pr.new/@vue/reactivity@14951
npm i https://pkg.pr.new/@vue/reactivity@14951
yarn add https://pkg.pr.new/@vue/reactivity@14951.tgz

@vue/runtime-core

pnpm add https://pkg.pr.new/@vue/runtime-core@14951
npm i https://pkg.pr.new/@vue/runtime-core@14951
yarn add https://pkg.pr.new/@vue/runtime-core@14951.tgz

@vue/runtime-dom

pnpm add https://pkg.pr.new/@vue/runtime-dom@14951
npm i https://pkg.pr.new/@vue/runtime-dom@14951
yarn add https://pkg.pr.new/@vue/runtime-dom@14951.tgz

@vue/runtime-vapor

pnpm add https://pkg.pr.new/@vue/runtime-vapor@14951
npm i https://pkg.pr.new/@vue/runtime-vapor@14951
yarn add https://pkg.pr.new/@vue/runtime-vapor@14951.tgz

@vue/server-renderer

pnpm add https://pkg.pr.new/@vue/server-renderer@14951
npm i https://pkg.pr.new/@vue/server-renderer@14951
yarn add https://pkg.pr.new/@vue/server-renderer@14951.tgz

@vue/shared

pnpm add https://pkg.pr.new/@vue/shared@14951
npm i https://pkg.pr.new/@vue/shared@14951
yarn add https://pkg.pr.new/@vue/shared@14951.tgz

vue

pnpm add https://pkg.pr.new/vue@14951
npm i https://pkg.pr.new/vue@14951
yarn add https://pkg.pr.new/vue@14951.tgz

@vue/compat

pnpm add https://pkg.pr.new/@vue/compat@14951
npm i https://pkg.pr.new/@vue/compat@14951
yarn add https://pkg.pr.new/@vue/compat@14951.tgz

commit: e9bde7e

@edison1105 edison1105 added the scope: vapor related to vapor mode label Jun 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/runtime-vapor/src/fragment.ts (1)

137-166: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

runWithFragmentCtx() no longer restores currentInstance.

Direct deferred-render callers still use this exported helper as the fragment-context wrapper. In packages/runtime-vapor/src/vdomInterop.ts:1557-1718, slot rendering goes through runWithFragmentCtx(frag, ...); after this refactor that path only restores slot-owner / slot-boundary / keep-alive state, so it can now run under the ambient instance (or null) instead of fragment.renderInstance. Please either move the instance swap into the exported helper or switch those call sites to a public wrapper that restores the full render context.

🤖 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/fragment.ts` around lines 137 - 166,
runWithFragmentCtx currently restores slot-owner, slot-boundary, and keep-alive
state but omits restoring currentInstance (so callers like vdomInterop that rely
on fragment.renderInstance run under the wrong ambient instance). Fix by saving
and restoring the instance around fn: capture prevInstance (via the existing
setCurrentInstance / setCurrentRenderInstance helper that manages
currentInstance) before applying fragment.renderInstance, set currentInstance to
fragment.renderInstance when entering, and restore prevInstance in the finally
block; alternatively update all call sites that call runWithFragmentCtx(frag,
...) (e.g., vdomInterop code paths) to use a new public wrapper that sets and
restores the full render context including currentInstance instead of calling
runWithFragmentCtx directly. Ensure you reference runWithFragmentCtx,
fragment.renderInstance, currentInstance and the setCurrentInstance helper when
making the change.
🤖 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.

Outside diff comments:
In `@packages/runtime-vapor/src/fragment.ts`:
- Around line 137-166: runWithFragmentCtx currently restores slot-owner,
slot-boundary, and keep-alive state but omits restoring currentInstance (so
callers like vdomInterop that rely on fragment.renderInstance run under the
wrong ambient instance). Fix by saving and restoring the instance around fn:
capture prevInstance (via the existing setCurrentInstance /
setCurrentRenderInstance helper that manages currentInstance) before applying
fragment.renderInstance, set currentInstance to fragment.renderInstance when
entering, and restore prevInstance in the finally block; alternatively update
all call sites that call runWithFragmentCtx(frag, ...) (e.g., vdomInterop code
paths) to use a new public wrapper that sets and restores the full render
context including currentInstance instead of calling runWithFragmentCtx
directly. Ensure you reference runWithFragmentCtx, fragment.renderInstance,
currentInstance and the setCurrentInstance helper when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cf25dfc9-d56a-4d3b-b7e2-5fac90a74002

📥 Commits

Reviewing files that changed from the base of the PR and between 0135bdb and 2df6963.

📒 Files selected for processing (15)
  • packages/runtime-vapor/__tests__/componentSlots.spec.ts
  • packages/runtime-vapor/__tests__/dom/hydrateFragment.spec.ts
  • packages/runtime-vapor/src/apiCreateFor.ts
  • packages/runtime-vapor/src/block.ts
  • packages/runtime-vapor/src/componentSlots.ts
  • packages/runtime-vapor/src/components/KeepAlive.ts
  • packages/runtime-vapor/src/components/Teleport.ts
  • packages/runtime-vapor/src/components/Transition.ts
  • packages/runtime-vapor/src/dom/hydrateFragment.ts
  • packages/runtime-vapor/src/fragment.ts
  • packages/runtime-vapor/src/keepAlive.ts
  • packages/runtime-vapor/src/slotBoundary.ts
  • packages/runtime-vapor/src/slotFragment.ts
  • packages/runtime-vapor/src/transition.ts
  • packages/runtime-vapor/src/vdomInterop.ts

…ntics

During hydration, prop setters now adopt server-rendered values outright
instead of re-writing them:

- dev / __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: mismatches are
  warned but never patched, matching vdom's hydrateElement props loop
  (warn-only; SSR values are preserved)
- pure prod: trust-skip with zero DOM reads/writes, only warming the
  per-element caches so the first post-hydration update diffs correctly
- force carve-outs preserved: input/option value & indeterminate and
  v-bind .prop modifiers still write through
- v-html (setHtml/setBlockHtml): server-rendered innerHTML is trusted
  as-is in all builds (vdom never compares nor writes innerHTML during
  hydration)
When handleMismatch rebuilds a node from the client template, the
server never rendered it, but its render effects still run with
isHydrating === true, so prop setters stay check-only (warn + cache,
no DOM write). Dynamic attrs/props/class/style were silently dropped,
and the polluted cache could skip later updates with the same value.

Mark recreated nodes and their template-born descendants with $rcn in
the mismatch path, before adopting server children so adopted content
keeps normal check-only semantics. Hydration-mode prop setters now
write through on marked nodes, matching vdom's "mismatch recovery =
full client mount" behavior.

Text inside recreated nodes no longer emits the redundant "Hydration
text mismatch" warning — the node mismatch was already reported,
aligning with vdom's single-warning behavior.
Mirror VDOM's same-vnode guard so keys like 1 and '1' do not collide through
the String($key) leaving-cache slot. Also simplify an equivalent in-out branch
condition.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/runtime-vapor/src/dom/prop.ts (1)

504-513: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Write block HTML on recreated hydration nodes.

setHtml() skips the trust-SSR fast path for recreated nodes, but setBlockHtml() returns for every hydration block. If mismatch recovery recreated the target block, this caches value without writing the client innerHTML, leaving the newly created DOM stale.

Suggested fix
 export function setBlockHtml(
   block: Block & { $html?: string },
   value: any,
 ): void {
   value = value == null ? '' : unsafeToTrustedHTML(value)
   // trust SSR content during hydration, see setHtml
-  if (isHydrating) {
+  if (isHydrating && !normalizeBlock(block).some(isRecreatedNode)) {
     block.$html = value
     return
   }
   if (block.$html !== value) {
     setHtmlToBlock(block, (block.$html = value))
   }
 }
🤖 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/prop.ts` around lines 504 - 513, The
setBlockHtml() function returns early during hydration without checking if the
block was recreated. If mismatch recovery recreated the target block, this
leaves the newly created DOM stale because the innerHTML is never written.
Modify the hydration check in setBlockHtml() to verify whether the block is a
recreated node (similar to how setHtml() handles this scenario), and only return
early if it is not recreated; for recreated nodes, allow the function to
continue and write the HTML content to ensure the client DOM is properly
updated.
🤖 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/__tests__/components/Transition.spec.ts`:
- Around line 556-557: Replace the conditional invocation pattern of the
leaveDone callback with explicit assertion-then-invocation to ensure tests fail
deterministically if the callback is undefined. At
packages/runtime-vapor/__tests__/components/Transition.spec.ts lines 556-557,
replace the optional call leaveDone && leaveDone() with
expect(leaveDone).toBeDefined(); leaveDone!(). Apply the same fix at
packages/runtime-vapor/__tests__/components/Transition.spec.ts lines 587-588, at
packages/runtime-vapor/__tests__/components/Transition.spec.ts lines 821-823,
and at packages/runtime-vapor/__tests__/components/TransitionGroup.spec.ts lines
273-274, replacing each instance of the optional leaveDone invocation pattern
with the assertion followed by non-null assertion invocation.

In `@packages/runtime-vapor/__tests__/hydration.spec.ts`:
- Around line 7200-7214: The console.warn spy created at line 7200 is never
restored, which can cause cross-test leakage in subsequent tests. Additionally,
the assertion at line 7213 uses call.includes() to check array membership, but
this misses substring matches when the warning payload is a single concatenated
string. Restore the warn spy after the test completes (add warn.mockRestore() in
the finally block or after the test), and fix the substring check by verifying
that the actual warning message string (accessed via call[0] or call.join())
contains the substring '(start of fragment)' instead of checking array
membership.

---

Outside diff comments:
In `@packages/runtime-vapor/src/dom/prop.ts`:
- Around line 504-513: The setBlockHtml() function returns early during
hydration without checking if the block was recreated. If mismatch recovery
recreated the target block, this leaves the newly created DOM stale because the
innerHTML is never written. Modify the hydration check in setBlockHtml() to
verify whether the block is a recreated node (similar to how setHtml() handles
this scenario), and only return early if it is not recreated; for recreated
nodes, allow the function to continue and write the HTML content to ensure the
client DOM is properly updated.
🪄 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: f053f136-0551-4410-8697-c32de832c22b

📥 Commits

Reviewing files that changed from the base of the PR and between 97310bf and e9bde7e.

📒 Files selected for processing (11)
  • packages/runtime-core/src/hydration.ts
  • packages/runtime-core/src/index.ts
  • packages/runtime-vapor/__tests__/components/Transition.spec.ts
  • packages/runtime-vapor/__tests__/components/TransitionGroup.spec.ts
  • packages/runtime-vapor/__tests__/hydration.spec.ts
  • packages/runtime-vapor/src/components/Teleport.ts
  • packages/runtime-vapor/src/components/Transition.ts
  • packages/runtime-vapor/src/directives/vShow.ts
  • packages/runtime-vapor/src/dom/hydration.ts
  • packages/runtime-vapor/src/dom/prop.ts
  • packages/runtime-vapor/src/dom/template.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/runtime-vapor/src/components/Teleport.ts

Comment thread packages/runtime-vapor/__tests__/components/Transition.spec.ts
Comment thread packages/runtime-vapor/__tests__/hydration.spec.ts
@edison1105 edison1105 marked this pull request as ready for review June 15, 2026 01:26
@edison1105 edison1105 merged commit 5689b88 into minor Jun 15, 2026
17 checks passed
@edison1105 edison1105 deleted the edison/refactor/fragment branch June 15, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: vapor related to vapor mode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant